Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ require (
k8s.io/apimachinery v0.36.1
k8s.io/client-go v0.36.1
k8s.io/kube-aggregator v0.36.1
k8s.io/mount-utils v0.36.1
sigs.k8s.io/controller-runtime v0.24.1
)

Expand Down Expand Up @@ -248,7 +247,6 @@ require (
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/sys/mountinfo v0.7.2 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
Expand Down
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -646,8 +646,6 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
Expand Down Expand Up @@ -1154,8 +1152,6 @@ k8s.io/kube-aggregator v0.36.1 h1:IzNeRsJcTtgsiCyTgCR1pSwWCrXC1QZQWMTcBw18cFQ=
k8s.io/kube-aggregator v0.36.1/go.mod h1:ROrIm5irUhVUJsKVCgBAAcXpK5IiqpdCn0Ka7LYMGs4=
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
k8s.io/mount-utils v0.36.1 h1:NWDFsdv+jfqPfa/LisnbEn1QyPNYjMNkmfEORXhyvZA=
k8s.io/mount-utils v0.36.1/go.mod h1:+I47UOG6FiUGVSy7VanjU/mQXLShMo3M7xBpGLzCub8=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4=
Expand Down
3 changes: 1 addition & 2 deletions pkg/common/containerinfo/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"k8s.io/apimachinery/pkg/types"
"k8s.io/mount-utils"
)

var (
Expand Down Expand Up @@ -77,7 +76,7 @@ func (e *Extractor) extractInfo(pid int32, log hclog.Logger, extractPodUID bool)
func (e *Extractor) extractPodUIDAndContainerIDFromMountInfo(pid int32, log hclog.Logger, extractPodUID bool) (types.UID, string, error) {
mountInfoPath := filepath.Join(e.RootDir, "/proc", fmt.Sprint(pid), "mountinfo")

mountInfos, err := mount.ParseMountInfo(mountInfoPath)
mountInfos, err := parseMountInfo(mountInfoPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return "", "", nil
Expand Down
8 changes: 8 additions & 0 deletions pkg/common/containerinfo/extract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,12 @@ func TestExtractContainerID(t *testing.T) {
t.Run("has multiple cgroup mounts on slash v2", func(t *testing.T) {
assertFound(t, "testdata/docker/cgroup-mount-at-slash/v2", testContainerID)
})

t.Run("tolerates tmpfs mount with empty source", func(t *testing.T) {
// Regression test for #7036: a tmpfs mount has no source, which renders
// as an empty field in /proc/<pid>/mountinfo. The container ID must
// still be extracted from the cgroup mount on the same file rather than
// the whole file being rejected.
assertFound(t, "testdata/docker/tmpfs-empty-source", testContainerID)
})
}
102 changes: 102 additions & 0 deletions pkg/common/containerinfo/mountinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//go:build !windows

package containerinfo

import (
"fmt"
"os"
"strings"
)

// mountInfo holds the subset of /proc/<pid>/mountinfo fields that the
// container info extractor consumes. The full mountinfo line format is
// documented in proc(5).
type mountInfo struct {
// Root is the pathname of the directory in the filesystem which forms the
// root of this mount (field 4).
Root string
// FsType is the filesystem type (the first field after the "-" separator).
FsType string
}

// parseMountInfo parses /proc/<pid>/mountinfo.
//
// It exists because k8s.io/mount-utils ParseMountInfo splits each line with
// strings.Fields, which collapses runs of whitespace and therefore drops an
// empty mount source field. Per proc(5) the mount source (the field after the
// filesystem type) may be empty (for example a tmpfs mount has no source), and
// the kernel escapes any real whitespace in a path as octal (\040). A line
// like:
//
// 119 206 0:68 / /local rw,relatime - tmpfs rw,size=8192k
//
// is therefore valid: the double space between "tmpfs" and "rw,size=8192k"
// unambiguously means an empty source. The upstream parser counts that as 9
// fields (it expects at least 10) and rejects the entire file, which made the
// docker and k8s workload attestors fail attestation outright. This parser is
// position-aware around the "-" separator so an empty source is tolerated.
func parseMountInfo(filename string) ([]mountInfo, error) {
content, err := os.ReadFile(filename)
if err != nil {
return nil, err
}

var infos []mountInfo
for _, line := range strings.Split(string(content), "\n") {

Check failure on line 45 in pkg/common/containerinfo/mountinfo.go

View workflow job for this annotation

GitHub Actions / lint (linux)

stringsseq: Ranging over SplitSeq is more efficient (modernize)
if line == "" {
continue
}
info, err := parseMountInfoLine(line)
if err != nil {
return nil, err
}
infos = append(infos, info)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems useful to do and could simplify things a bit.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in the same commit (4af7f2f). Swapped ReadFile + Split for bufio.Scanner, so it streams line by line now.

return infos, nil
}

// parseMountInfoLine parses a single mountinfo line. The format (see proc(5))
// is, in order:
//
// (1) mount ID (2) parent ID (3) major:minor (4) root (5) mount point
// (6) mount options (7..) zero or more optional fields (8) a "-" separator
// (9) filesystem type (10) mount source (11) super options
//
// Only the fields the extractor needs (root and filesystem type) are returned.
// Fields up to the "-" separator are whitespace-separated and never empty, so
// strings.Fields is safe there. The three fields after the separator are taken
// by position so an empty source (field 10) is preserved rather than collapsed.
func parseMountInfoLine(line string) (mountInfo, error) {
// Split the line into the part before the "-" separator and the part after
// it. The separator is a standalone "-" token, so it is surrounded by
// spaces. Splitting on " - " keeps it unambiguous: optional-field tags
// before the separator never equal a bare "-", and the post-separator
// fields are matched by position below.
sep := strings.Index(line, " - ")

Check failure on line 75 in pkg/common/containerinfo/mountinfo.go

View workflow job for this annotation

GitHub Actions / lint (linux)

stringscut: strings.Index can be simplified using strings.Cut (modernize)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at what strings offers, I think strings.Split(line, " ") should do what we want here and maybe it would simplify things a bit. What do you think?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Switched to strings.Split(line, " ") so empty fields survive as empty strings in the slice. This lets us locate the separator and all post-separator fields by index directly, no more separate Index + Fields split. Pushed in 4af7f2f.

if sep < 0 {
return mountInfo{}, fmt.Errorf("missing separator in mountinfo line: %s", line)
}

before := strings.Fields(line[:sep])
// Fields before the separator: mount ID, parent ID, major:minor, root,
// mount point, mount options, then zero or more optional fields. The root
// is field index 3 (zero-based).
if len(before) < 6 {
return mountInfo{}, fmt.Errorf("expected at least 6 fields before separator in mountinfo line: %s", line)
}

// After the separator there are exactly three positional fields:
// filesystem type, mount source, and super options. The source may be
// empty, so parse by splitting into at most three fields and keeping the
// first (filesystem type). Trailing whitespace from an empty source does
// not affect the filesystem type, which is the first non-empty token.
after := strings.Fields(line[sep+len(" - "):])
if len(after) < 1 {
return mountInfo{}, fmt.Errorf("missing filesystem type in mountinfo line: %s", line)
}

return mountInfo{
Root: before[3],
FsType: after[0],
}, nil
}
90 changes: 90 additions & 0 deletions pkg/common/containerinfo/mountinfo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//go:build !windows

package containerinfo

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestParseMountInfoLine(t *testing.T) {
for _, tt := range []struct {
name string
line string
wantRoot string
wantType string
wantErr string
}{
{
name: "normal cgroup2 line",
line: "1543 1542 0:32 /some/root /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw,nsdelegate",
wantRoot: "/some/root",
wantType: "cgroup2",
},
{
name: "optional fields before separator",
line: "573 572 0:33 /docker/abc /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime master:11 - cgroup cgroup rw,name=systemd",
wantRoot: "/docker/abc",
wantType: "cgroup",
},
{
// Regression for #7036: a tmpfs mount has no source, so the field
// after the filesystem type is empty. strings.Fields would collapse
// it and the upstream parser rejected the whole file.
name: "tmpfs with empty source",
line: "119 206 0:68 / /local rw,relatime - tmpfs rw,size=8192k",
wantRoot: "/",
wantType: "tmpfs",
},
{
name: "missing separator",
line: "1543 1542 0:32 /some/root /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime cgroup2 cgroup rw",
wantErr: "missing separator",
},
{
name: "too few fields before separator",
line: "1543 1542 0:32 - cgroup2 cgroup rw",
wantErr: "expected at least 6 fields before separator",
},
{
name: "missing filesystem type after separator",
line: "1543 1542 0:32 /some/root /sys/fs/cgroup rw - ",
wantErr: "missing filesystem type",
},
} {
t.Run(tt.name, func(t *testing.T) {
info, err := parseMountInfoLine(tt.line)
if tt.wantErr != "" {
assert.ErrorContains(t, err, tt.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tt.wantRoot, info.Root)
assert.Equal(t, tt.wantType, info.FsType)
})
}
}

func TestParseMountInfo(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "mountinfo")
content := "" +
"2356 2355 0:30 /../containerid /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw\n" +
"119 206 0:68 / /local rw,relatime - tmpfs rw,size=8192k\n"
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))

infos, err := parseMountInfo(path)
require.NoError(t, err)
require.Len(t, infos, 2)

assert.Equal(t, "/../containerid", infos[0].Root)
assert.Equal(t, "cgroup2", infos[0].FsType)

// The tmpfs line with an empty source must still parse.
assert.Equal(t, "/", infos[1].Root)
assert.Equal(t, "tmpfs", infos[1].FsType)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
2356 2355 0:30 /../0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw
119 206 0:68 / /local rw,relatime - tmpfs rw,size=8192k
Loading