Skip to content

Commit

Permalink
Add support for SELinux AVC statistics (prometheus#599)
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
cgzones authored Jun 3, 2024
1 parent 661a63e commit f360499
Show file tree
Hide file tree
Showing 9 changed files with 414 additions and 0 deletions.
3 changes: 3 additions & 0 deletions internal/fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
104 changes: 104 additions & 0 deletions selinuxfs/avc_cache_stats.go
Original file line number Diff line number Diff line change
@@ -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()
}
57 changes: 57 additions & 0 deletions selinuxfs/avc_cache_stats_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
83 changes: 83 additions & 0 deletions selinuxfs/avc_hash_stats.go
Original file line number Diff line number Diff line change
@@ -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()
}
49 changes: 49 additions & 0 deletions selinuxfs/avc_hash_stats_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
46 changes: 46 additions & 0 deletions selinuxfs/fs.go
Original file line number Diff line number Diff line change
@@ -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
}
37 changes: 37 additions & 0 deletions selinuxfs/fs_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
1 change: 1 addition & 0 deletions selinuxfs/testdata/fixtures
Loading

0 comments on commit f360499

Please sign in to comment.