Skip to content

Commit f360499

Browse files
authored
Add support for SELinux AVC statistics (#599)
Add interfaces to read the SELinux AVC runtime statistics from /sys/fs/selinux/avc/cache_stats and /sys/fs/selinux/avc/hash_stats. Refactored from prometheus/node_exporter#2418 Signed-off-by: Christian Göttsche <[email protected]>
1 parent 661a63e commit f360499

File tree

9 files changed

+414
-0
lines changed

9 files changed

+414
-0
lines changed

Diff for: internal/fs/fs.go

+3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ const (
2828

2929
// DefaultConfigfsMountPoint is the common mount point of the configfs.
3030
DefaultConfigfsMountPoint = "/sys/kernel/config"
31+
32+
// DefaultSelinuxMountPoint is the common mount point of the selinuxfs.
33+
DefaultSelinuxMountPoint = "/sys/fs/selinux"
3134
)
3235

3336
// FS represents a pseudo-filesystem, normally /proc or /sys, which provides an

Diff for: selinuxfs/avc_cache_stats.go

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright 2023 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
//go:build linux && !noselinux
15+
// +build linux,!noselinux
16+
17+
package selinuxfs
18+
19+
import (
20+
"bufio"
21+
"fmt"
22+
"os"
23+
"strconv"
24+
"strings"
25+
)
26+
27+
// SELinux access vector cache statistics.
28+
type AVCStat struct {
29+
// Number of total lookups
30+
Lookups uint64
31+
// Number of total hits
32+
Hits uint64
33+
// Number of total misses
34+
Misses uint64
35+
// Number of total allocations
36+
Allocations uint64
37+
// Number of total reclaims
38+
Reclaims uint64
39+
// Number of total frees
40+
Frees uint64
41+
}
42+
43+
// ParseAVCStats returns the total SELinux access vector cache statistics,
44+
// or error on failure.
45+
func (fs FS) ParseAVCStats() (AVCStat, error) {
46+
avcStat := AVCStat{}
47+
48+
file, err := os.Open(fs.selinux.Path("avc/cache_stats"))
49+
if err != nil {
50+
return avcStat, err
51+
}
52+
defer file.Close()
53+
54+
scanner := bufio.NewScanner(file)
55+
scanner.Scan() // Skip header
56+
57+
for scanner.Scan() {
58+
avcValues := strings.Fields(scanner.Text())
59+
60+
if len(avcValues) != 6 {
61+
return avcStat, fmt.Errorf("invalid AVC stat line: %s",
62+
scanner.Text())
63+
}
64+
65+
lookups, err := strconv.ParseUint(avcValues[0], 0, 64)
66+
if err != nil {
67+
return avcStat, fmt.Errorf("could not parse expected integer value for lookups")
68+
}
69+
70+
hits, err := strconv.ParseUint(avcValues[1], 0, 64)
71+
if err != nil {
72+
return avcStat, fmt.Errorf("could not parse expected integer value for hits")
73+
}
74+
75+
misses, err := strconv.ParseUint(avcValues[2], 0, 64)
76+
if err != nil {
77+
return avcStat, fmt.Errorf("could not parse expected integer value for misses")
78+
}
79+
80+
allocations, err := strconv.ParseUint(avcValues[3], 0, 64)
81+
if err != nil {
82+
return avcStat, fmt.Errorf("could not parse expected integer value for allocations")
83+
}
84+
85+
reclaims, err := strconv.ParseUint(avcValues[4], 0, 64)
86+
if err != nil {
87+
return avcStat, fmt.Errorf("could not parse expected integer value for reclaims")
88+
}
89+
90+
frees, err := strconv.ParseUint(avcValues[5], 0, 64)
91+
if err != nil {
92+
return avcStat, fmt.Errorf("could not parse expected integer value for frees")
93+
}
94+
95+
avcStat.Lookups += lookups
96+
avcStat.Hits += hits
97+
avcStat.Misses += misses
98+
avcStat.Allocations += allocations
99+
avcStat.Reclaims += reclaims
100+
avcStat.Frees += frees
101+
}
102+
103+
return avcStat, scanner.Err()
104+
}

Diff for: selinuxfs/avc_cache_stats_test.go

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright 2023 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
//go:build linux && !noselinux
15+
// +build linux,!noselinux
16+
17+
package selinuxfs
18+
19+
import (
20+
"testing"
21+
)
22+
23+
func TestAVCStats(t *testing.T) {
24+
fs, err := NewFS(selinuxTestFixtures)
25+
if err != nil {
26+
t.Fatal(err)
27+
}
28+
29+
avcStats, err := fs.ParseAVCStats()
30+
if err != nil {
31+
t.Fatal(err)
32+
}
33+
34+
if want, got := uint64(91590784), avcStats.Lookups; want != got {
35+
t.Errorf("want avcstat lookups %v, got %v", want, got)
36+
}
37+
38+
if want, got := uint64(91569452), avcStats.Hits; want != got {
39+
t.Errorf("want avcstat hits %v, got %v", want, got)
40+
}
41+
42+
if want, got := uint64(21332), avcStats.Misses; want != got {
43+
t.Errorf("want avcstat misses %v, got %v", want, got)
44+
}
45+
46+
if want, got := uint64(21332), avcStats.Allocations; want != got {
47+
t.Errorf("want avcstat allocations %v, got %v", want, got)
48+
}
49+
50+
if want, got := uint64(20400), avcStats.Reclaims; want != got {
51+
t.Errorf("want avcstat reclaims %v, got %v", want, got)
52+
}
53+
54+
if want, got := uint64(20826), avcStats.Frees; want != got {
55+
t.Errorf("want avcstat frees %v, got %v", want, got)
56+
}
57+
}

Diff for: selinuxfs/avc_hash_stats.go

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright 2023 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
//go:build linux && !noselinux
15+
// +build linux,!noselinux
16+
17+
package selinuxfs
18+
19+
import (
20+
"bufio"
21+
"fmt"
22+
"os"
23+
"strconv"
24+
"strings"
25+
)
26+
27+
// SELinux access vector cache hashtable statistics.
28+
type AVCHashStat struct {
29+
// Number of entries
30+
Entries uint64
31+
// Number of buckets used
32+
BucketsUsed uint64
33+
// Number of buckets available
34+
BucketsAvailable uint64
35+
// Length of the longest chain
36+
LongestChain uint64
37+
}
38+
39+
// ParseAVCHashStats returns the SELinux access vector cache hashtable
40+
// statistics, or error on failure.
41+
func (fs FS) ParseAVCHashStats() (AVCHashStat, error) {
42+
avcHashStat := AVCHashStat{}
43+
44+
file, err := os.Open(fs.selinux.Path("avc/hash_stats"))
45+
if err != nil {
46+
return avcHashStat, err
47+
}
48+
defer file.Close()
49+
50+
scanner := bufio.NewScanner(file)
51+
52+
scanner.Scan()
53+
entriesValue := strings.TrimPrefix(scanner.Text(), "entries: ")
54+
55+
scanner.Scan()
56+
bucketsValues := strings.Split(scanner.Text(), "buckets used: ")
57+
bucketsValuesTuple := strings.Split(bucketsValues[1], "/")
58+
59+
scanner.Scan()
60+
longestChainValue := strings.TrimPrefix(scanner.Text(), "longest chain: ")
61+
62+
avcHashStat.Entries, err = strconv.ParseUint(entriesValue, 0, 64)
63+
if err != nil {
64+
return avcHashStat, fmt.Errorf("could not parse expected integer value for hash entries")
65+
}
66+
67+
avcHashStat.BucketsUsed, err = strconv.ParseUint(bucketsValuesTuple[0], 0, 64)
68+
if err != nil {
69+
return avcHashStat, fmt.Errorf("could not parse expected integer value for hash buckets used")
70+
}
71+
72+
avcHashStat.BucketsAvailable, err = strconv.ParseUint(bucketsValuesTuple[1], 0, 64)
73+
if err != nil {
74+
return avcHashStat, fmt.Errorf("could not parse expected integer value for hash buckets available")
75+
}
76+
77+
avcHashStat.LongestChain, err = strconv.ParseUint(longestChainValue, 0, 64)
78+
if err != nil {
79+
return avcHashStat, fmt.Errorf("could not parse expected integer value for hash longest chain")
80+
}
81+
82+
return avcHashStat, scanner.Err()
83+
}

Diff for: selinuxfs/avc_hash_stats_test.go

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2023 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
//go:build linux && !noselinux
15+
// +build linux,!noselinux
16+
17+
package selinuxfs
18+
19+
import (
20+
"testing"
21+
)
22+
23+
func TestAVCHashStat(t *testing.T) {
24+
fs, err := NewFS(selinuxTestFixtures)
25+
if err != nil {
26+
t.Fatal(err)
27+
}
28+
29+
avcHashStats, err := fs.ParseAVCHashStats()
30+
if err != nil {
31+
t.Fatal(err)
32+
}
33+
34+
if want, got := uint64(503), avcHashStats.Entries; want != got {
35+
t.Errorf("want avc hash stat entries %v, got %v", want, got)
36+
}
37+
38+
if want, got := uint64(512), avcHashStats.BucketsAvailable; want != got {
39+
t.Errorf("want avc hash stat buckets available %v, got %v", want, got)
40+
}
41+
42+
if want, got := uint64(257), avcHashStats.BucketsUsed; want != got {
43+
t.Errorf("want avc hash stat buckets used %v, got %v", want, got)
44+
}
45+
46+
if want, got := uint64(8), avcHashStats.LongestChain; want != got {
47+
t.Errorf("want avc hash stat longest chain %v, got %v", want, got)
48+
}
49+
}

Diff for: selinuxfs/fs.go

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2023 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
//go:build linux && !noselinux
15+
// +build linux,!noselinux
16+
17+
package selinuxfs
18+
19+
import (
20+
"github.com/prometheus/procfs/internal/fs"
21+
)
22+
23+
// FS represents the pseudo-filesystem selinixfs, which provides an interface to
24+
// SELinux data structures.
25+
type FS struct {
26+
selinux fs.FS
27+
}
28+
29+
// DefaultMountPoint is the common mount point of the selinuxfs filesystem.
30+
const DefaultMountPoint = fs.DefaultSelinuxMountPoint
31+
32+
// NewDefaultFS returns a new FS mounted under the default mountPoint. It will error
33+
// if the mount point can't be read.
34+
func NewDefaultFS() (FS, error) {
35+
return NewFS(DefaultMountPoint)
36+
}
37+
38+
// NewFS returns a new FS mounted under the given mountPoint. It will error
39+
// if the mount point can't be read.
40+
func NewFS(mountPoint string) (FS, error) {
41+
fs, err := fs.NewFS(mountPoint)
42+
if err != nil {
43+
return FS{}, err
44+
}
45+
return FS{fs}, nil
46+
}

Diff for: selinuxfs/fs_test.go

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2023 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
//go:build linux && !noselinux
15+
// +build linux,!noselinux
16+
17+
package selinuxfs
18+
19+
import "testing"
20+
21+
const (
22+
selinuxTestFixtures = "testdata/fixtures" + DefaultMountPoint
23+
)
24+
25+
func TestNewFS(t *testing.T) {
26+
if _, err := NewFS("foobar"); err == nil {
27+
t.Error("want NewFS to fail for non-existing mount point")
28+
}
29+
30+
if _, err := NewFS("doc.go"); err == nil {
31+
t.Error("want NewFS to fail if mount point is not a directory")
32+
}
33+
34+
if _, err := NewFS(selinuxTestFixtures); err != nil {
35+
t.Error("want NewFS to succeed if mount point exists")
36+
}
37+
}

Diff for: selinuxfs/testdata/fixtures

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../testdata/fixtures

0 commit comments

Comments
 (0)