diff --git a/internal/fs/fs.go b/internal/fs/fs.go index 3c18c7610..3a43e8391 100644 --- a/internal/fs/fs.go +++ b/internal/fs/fs.go @@ -28,6 +28,9 @@ const ( // DefaultConfigfsMountPoint is the common mount point of the configfs. DefaultConfigfsMountPoint = "/sys/kernel/config" + + // DefaultSelinuxMountPoint is the common mount point of the selinuxfs. + DefaultSelinuxMountPoint = "/sys/fs/selinux" ) // FS represents a pseudo-filesystem, normally /proc or /sys, which provides an diff --git a/selinuxfs/avc_cache_stats.go b/selinuxfs/avc_cache_stats.go new file mode 100644 index 000000000..0c37e0f07 --- /dev/null +++ b/selinuxfs/avc_cache_stats.go @@ -0,0 +1,104 @@ +// Copyright 2023 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build linux && !noselinux +// +build linux,!noselinux + +package selinuxfs + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + +// SELinux access vector cache statistics. +type AVCStat struct { + // Number of total lookups + Lookups uint64 + // Number of total hits + Hits uint64 + // Number of total misses + Misses uint64 + // Number of total allocations + Allocations uint64 + // Number of total reclaims + Reclaims uint64 + // Number of total frees + Frees uint64 +} + +// ParseAVCStats returns the total SELinux access vector cache statistics, +// or error on failure. +func (fs FS) ParseAVCStats() (AVCStat, error) { + avcStat := AVCStat{} + + file, err := os.Open(fs.selinux.Path("avc/cache_stats")) + if err != nil { + return avcStat, err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + scanner.Scan() // Skip header + + for scanner.Scan() { + avcValues := strings.Fields(scanner.Text()) + + if len(avcValues) != 6 { + return avcStat, fmt.Errorf("invalid AVC stat line: %s", + scanner.Text()) + } + + lookups, err := strconv.ParseUint(avcValues[0], 0, 64) + if err != nil { + return avcStat, fmt.Errorf("could not parse expected integer value for lookups") + } + + hits, err := strconv.ParseUint(avcValues[1], 0, 64) + if err != nil { + return avcStat, fmt.Errorf("could not parse expected integer value for hits") + } + + misses, err := strconv.ParseUint(avcValues[2], 0, 64) + if err != nil { + return avcStat, fmt.Errorf("could not parse expected integer value for misses") + } + + allocations, err := strconv.ParseUint(avcValues[3], 0, 64) + if err != nil { + return avcStat, fmt.Errorf("could not parse expected integer value for allocations") + } + + reclaims, err := strconv.ParseUint(avcValues[4], 0, 64) + if err != nil { + return avcStat, fmt.Errorf("could not parse expected integer value for reclaims") + } + + frees, err := strconv.ParseUint(avcValues[5], 0, 64) + if err != nil { + return avcStat, fmt.Errorf("could not parse expected integer value for frees") + } + + avcStat.Lookups += lookups + avcStat.Hits += hits + avcStat.Misses += misses + avcStat.Allocations += allocations + avcStat.Reclaims += reclaims + avcStat.Frees += frees + } + + return avcStat, scanner.Err() +} diff --git a/selinuxfs/avc_cache_stats_test.go b/selinuxfs/avc_cache_stats_test.go new file mode 100644 index 000000000..62560884e --- /dev/null +++ b/selinuxfs/avc_cache_stats_test.go @@ -0,0 +1,57 @@ +// Copyright 2023 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build linux && !noselinux +// +build linux,!noselinux + +package selinuxfs + +import ( + "testing" +) + +func TestAVCStats(t *testing.T) { + fs, err := NewFS(selinuxTestFixtures) + if err != nil { + t.Fatal(err) + } + + avcStats, err := fs.ParseAVCStats() + if err != nil { + t.Fatal(err) + } + + if want, got := uint64(91590784), avcStats.Lookups; want != got { + t.Errorf("want avcstat lookups %v, got %v", want, got) + } + + if want, got := uint64(91569452), avcStats.Hits; want != got { + t.Errorf("want avcstat hits %v, got %v", want, got) + } + + if want, got := uint64(21332), avcStats.Misses; want != got { + t.Errorf("want avcstat misses %v, got %v", want, got) + } + + if want, got := uint64(21332), avcStats.Allocations; want != got { + t.Errorf("want avcstat allocations %v, got %v", want, got) + } + + if want, got := uint64(20400), avcStats.Reclaims; want != got { + t.Errorf("want avcstat reclaims %v, got %v", want, got) + } + + if want, got := uint64(20826), avcStats.Frees; want != got { + t.Errorf("want avcstat frees %v, got %v", want, got) + } +} diff --git a/selinuxfs/avc_hash_stats.go b/selinuxfs/avc_hash_stats.go new file mode 100644 index 000000000..1d01755fd --- /dev/null +++ b/selinuxfs/avc_hash_stats.go @@ -0,0 +1,83 @@ +// Copyright 2023 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build linux && !noselinux +// +build linux,!noselinux + +package selinuxfs + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + +// SELinux access vector cache hashtable statistics. +type AVCHashStat struct { + // Number of entries + Entries uint64 + // Number of buckets used + BucketsUsed uint64 + // Number of buckets available + BucketsAvailable uint64 + // Length of the longest chain + LongestChain uint64 +} + +// ParseAVCHashStats returns the SELinux access vector cache hashtable +// statistics, or error on failure. +func (fs FS) ParseAVCHashStats() (AVCHashStat, error) { + avcHashStat := AVCHashStat{} + + file, err := os.Open(fs.selinux.Path("avc/hash_stats")) + if err != nil { + return avcHashStat, err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + + scanner.Scan() + entriesValue := strings.TrimPrefix(scanner.Text(), "entries: ") + + scanner.Scan() + bucketsValues := strings.Split(scanner.Text(), "buckets used: ") + bucketsValuesTuple := strings.Split(bucketsValues[1], "/") + + scanner.Scan() + longestChainValue := strings.TrimPrefix(scanner.Text(), "longest chain: ") + + avcHashStat.Entries, err = strconv.ParseUint(entriesValue, 0, 64) + if err != nil { + return avcHashStat, fmt.Errorf("could not parse expected integer value for hash entries") + } + + avcHashStat.BucketsUsed, err = strconv.ParseUint(bucketsValuesTuple[0], 0, 64) + if err != nil { + return avcHashStat, fmt.Errorf("could not parse expected integer value for hash buckets used") + } + + avcHashStat.BucketsAvailable, err = strconv.ParseUint(bucketsValuesTuple[1], 0, 64) + if err != nil { + return avcHashStat, fmt.Errorf("could not parse expected integer value for hash buckets available") + } + + avcHashStat.LongestChain, err = strconv.ParseUint(longestChainValue, 0, 64) + if err != nil { + return avcHashStat, fmt.Errorf("could not parse expected integer value for hash longest chain") + } + + return avcHashStat, scanner.Err() +} diff --git a/selinuxfs/avc_hash_stats_test.go b/selinuxfs/avc_hash_stats_test.go new file mode 100644 index 000000000..46dd59c83 --- /dev/null +++ b/selinuxfs/avc_hash_stats_test.go @@ -0,0 +1,49 @@ +// Copyright 2023 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build linux && !noselinux +// +build linux,!noselinux + +package selinuxfs + +import ( + "testing" +) + +func TestAVCHashStat(t *testing.T) { + fs, err := NewFS(selinuxTestFixtures) + if err != nil { + t.Fatal(err) + } + + avcHashStats, err := fs.ParseAVCHashStats() + if err != nil { + t.Fatal(err) + } + + if want, got := uint64(503), avcHashStats.Entries; want != got { + t.Errorf("want avc hash stat entries %v, got %v", want, got) + } + + if want, got := uint64(512), avcHashStats.BucketsAvailable; want != got { + t.Errorf("want avc hash stat buckets available %v, got %v", want, got) + } + + if want, got := uint64(257), avcHashStats.BucketsUsed; want != got { + t.Errorf("want avc hash stat buckets used %v, got %v", want, got) + } + + if want, got := uint64(8), avcHashStats.LongestChain; want != got { + t.Errorf("want avc hash stat longest chain %v, got %v", want, got) + } +} diff --git a/selinuxfs/fs.go b/selinuxfs/fs.go new file mode 100644 index 000000000..f3c713721 --- /dev/null +++ b/selinuxfs/fs.go @@ -0,0 +1,46 @@ +// Copyright 2023 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build linux && !noselinux +// +build linux,!noselinux + +package selinuxfs + +import ( + "github.com/prometheus/procfs/internal/fs" +) + +// FS represents the pseudo-filesystem selinixfs, which provides an interface to +// SELinux data structures. +type FS struct { + selinux fs.FS +} + +// DefaultMountPoint is the common mount point of the selinuxfs filesystem. +const DefaultMountPoint = fs.DefaultSelinuxMountPoint + +// NewDefaultFS returns a new FS mounted under the default mountPoint. It will error +// if the mount point can't be read. +func NewDefaultFS() (FS, error) { + return NewFS(DefaultMountPoint) +} + +// NewFS returns a new FS mounted under the given mountPoint. It will error +// if the mount point can't be read. +func NewFS(mountPoint string) (FS, error) { + fs, err := fs.NewFS(mountPoint) + if err != nil { + return FS{}, err + } + return FS{fs}, nil +} diff --git a/selinuxfs/fs_test.go b/selinuxfs/fs_test.go new file mode 100644 index 000000000..64b60f63d --- /dev/null +++ b/selinuxfs/fs_test.go @@ -0,0 +1,37 @@ +// Copyright 2023 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build linux && !noselinux +// +build linux,!noselinux + +package selinuxfs + +import "testing" + +const ( + selinuxTestFixtures = "testdata/fixtures" + DefaultMountPoint +) + +func TestNewFS(t *testing.T) { + if _, err := NewFS("foobar"); err == nil { + t.Error("want NewFS to fail for non-existing mount point") + } + + if _, err := NewFS("doc.go"); err == nil { + t.Error("want NewFS to fail if mount point is not a directory") + } + + if _, err := NewFS(selinuxTestFixtures); err != nil { + t.Error("want NewFS to succeed if mount point exists") + } +} diff --git a/selinuxfs/testdata/fixtures b/selinuxfs/testdata/fixtures new file mode 120000 index 000000000..5fdc97bed --- /dev/null +++ b/selinuxfs/testdata/fixtures @@ -0,0 +1 @@ +../../testdata/fixtures \ No newline at end of file diff --git a/testdata/fixtures.ttar b/testdata/fixtures.ttar index 61e879ee2..4bc508b8d 100644 --- a/testdata/fixtures.ttar +++ b/testdata/fixtures.ttar @@ -14793,6 +14793,40 @@ Lines: 1 4096 Mode: 444 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Directory: fixtures/sys/fs/selinux +Mode: 755 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Directory: fixtures/sys/fs/selinux/avc +Mode: 755 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/fs/selinux/avc/cache_stats +Lines: 17 +lookups hits misses allocations reclaims frees +6603240 6601784 1456 1456 1424 1479 +5110559 5109586 973 973 1152 1162 +6720426 6719001 1425 1425 1488 1515 +4713015 4711743 1272 1272 1200 1211 +6949114 6947453 1661 1661 1504 1555 +5624578 5622931 1647 1647 1328 1347 +5884601 5882982 1619 1619 1488 1525 +3170931 3170061 870 870 832 858 +7336127 7334467 1660 1660 1632 1644 +2853424 2852652 772 772 704 712 +7596237 7594408 1829 1829 1936 1960 +4491113 4490186 927 927 832 854 +7319238 7317738 1500 1500 1504 1538 +4945119 4944151 968 968 912 943 +7610170 7608283 1887 1887 1648 1695 +4662892 4662026 866 866 816 828 +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/fs/selinux/avc/hash_stats +Lines: 3 +entries: 503 +buckets used: 257/512 +longest chain: 8 +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Directory: fixtures/sys/fs/xfs Mode: 755 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -