Skip to content

Commit

Permalink
procfs: add tests for our safe /proc helpers
Browse files Browse the repository at this point in the history
This ensures we test all of the fallbacks even if we don't use them in
practice when doing MkdirAll in our test suite. This raises the test
coverage to 77.7%.

Signed-off-by: Aleksa Sarai <[email protected]>
  • Loading branch information
cyphar committed Jun 28, 2024
1 parent 96da223 commit a258c4f
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 8 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ jobs:
go-version: ${{ matrix.go-version }}
- name: unit tests
run: go test -v -cover ./...
# TODO: Merge the code coverage stats from both runs...
- name: unit tests (root)
if: ${{ ! startsWith(matrix.os, 'windows-') }}
run: sudo go test -v -cover ./...
18 changes: 10 additions & 8 deletions procfs_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,7 @@ type procThreadSelfCloser func()
//
// This is similar to ProcThreadSelf from runc, but with extra hardening
// applied and using *os.File.
func procThreadSelf(subpath string) (_ *os.File, _ procThreadSelfCloser, Err error) {
procRoot, err := getProcRoot()
if err != nil {
return nil, nil, err
}

func procThreadSelf(procRoot *os.File, subpath string) (_ *os.File, _ procThreadSelfCloser, Err error) {
haveProcThreadSelfOnce.Do(func() {
// If the kernel doesn't support thread-self, it doesn't matter which
// /proc handle we use.
Expand Down Expand Up @@ -256,7 +251,10 @@ func procThreadSelf(subpath string) (_ *os.File, _ procThreadSelfCloser, Err err
}

// Grab the handle.
var handle *os.File
var (
handle *os.File
err error
)
if hasOpenat2() {
// We prefer being able to use RESOLVE_NO_XDEV if we can, to be
// absolutely sure we are operating on a clean /proc handle that
Expand Down Expand Up @@ -293,7 +291,11 @@ func procThreadSelf(subpath string) (_ *os.File, _ procThreadSelfCloser, Err err
}

func rawProcSelfFdReadlink(fd int) (string, error) {
procSelfFd, closer, err := procThreadSelf("fd/")
procRoot, err := getProcRoot()
if err != nil {
return "", err
}
procSelfFd, closer, err := procThreadSelf(procRoot, "fd/")
if err != nil {
return "", fmt.Errorf("get safe /proc/thread-self/fd handle: %w", err)
}
Expand Down
144 changes: 144 additions & 0 deletions procfs_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
//go:build linux

// Copyright (C) 2024 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package securejoin

import (
"errors"
"os"
"path"
"runtime"
"testing"

"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
)

func setupProcOvermount(t *testing.T) {
requireRoot(t)

// Lock our thread because we need to create a custom mount namespace. Each
// test run is run in its own goroutine (this is not _explicitly_
// guaranteed by Go but t.FailNow() uses Goexit, which means it has to be
// true in practice) so locking the test to this thread means the other
// tests will run on different goroutines.
//
// There is no UnlockOSThread() here, to ensure that the Go runtime will
// kill this thread once this goroutine returns (ensuring no other
// goroutines run in this context).
runtime.LockOSThread()

// New mount namespace (we are multi-threaded with a shared fs so we need
// CLONE_FS to split us from the other threads in the Go process).
err := unix.Unshare(unix.CLONE_FS | unix.CLONE_NEWNS)
require.Nil(t, err, "new mount namespace")

// Private /.
err = unix.Mount("", "/", "", unix.MS_PRIVATE|unix.MS_REC, "")
require.Nil(t, err)

// Figure out what /proc/thread-self is for this system.
procRoot, err := unsafeHostProcRoot()
require.Nil(t, err, "get real host /proc during setup")
defer procRoot.Close()

procThreadSelf, closer, err := procThreadSelf(procRoot, ".")
require.Nil(t, err, "get real host /proc/thread-self during setup")
procThreadSelfPath := procThreadSelf.Name()
_ = procThreadSelf.Close()
closer() // LockOSThread stacks, so we can call this safely.

// Create some overmounts on /proc.
for _, mount := range []struct {
source, targetSubPath, fsType string
flags uintptr
}{
// A tmpfs on top of /proc/thread-self/fdinfo to check whether
// verifyProcRoot() works on old kernels.
{"tmpfs", "fdinfo", "tmpfs", 0},
// A bind-mount of noop-write real procfs file on top of
// /proc/thread-self/attr/current so we can test whether
// verifyProcRoot() works for the file case.
//
// We don't use procThreadSelf for files in filepath-securejoin, but
// this is to test the runc-equivalent behaviour for when this logic is
// moved to libpathrs.
{"/proc/self/sched", "attr/current", "", unix.MS_BIND},
// TODO: Add a test for bind-mounting on top of magic-links. We can't
// detect this at the moment (and maybe in the fd case this will
// be blocked by the kernel...) but it'd be nice to have the
// problem written down.
// TODO: Add a test for mounting on top of /proc/self or
// /proc/thread-self. This should be detected with openat2.
} {
target := path.Join(procThreadSelfPath, mount.targetSubPath)
err := unix.Mount(mount.source, target, mount.fsType, mount.flags, "")
require.Nilf(t, err, "mount(%s, [%s/]%s, %s, 0x%x)", mount.source, procThreadSelfPath, mount.targetSubPath, mount.fsType, mount.flags)
}
}

type procRootFunc func() (*os.File, error)

func testProcThreadSelf(t *testing.T, procRoot *os.File, subpath string, expectErr bool) {
handle, closer, err := procThreadSelf(procRoot, subpath)
if expectErr {
if err == nil || !errors.Is(err, errUnsafeProcfs) {
t.Errorf("should have detected /proc/thread-self/%s overmount: %v", subpath, err)
}
} else {
require.Nil(t, err)
_ = handle.Close()
closer() // LockOSThread stacks, so we can call this safely.
}
}

func testProcOvermount(t *testing.T, procRootFn procRootFunc, expectOvermounts bool) {
setupProcOvermount(t)

procRoot, err := procRootFn()
require.Nil(t, err)
defer procRoot.Close()

// We expect to always detect tmpfs overmounts if we have a /proc with
// overmounts.
detectFdinfo := expectOvermounts
// We only expect to detect procfs bind-mounts if there are /proc
// overmounts and we have openat2.
detectAttrCurrent := expectOvermounts && hasOpenat2()

testProcThreadSelf(t, procRoot, "fdinfo", detectFdinfo)
testProcThreadSelf(t, procRoot, "attr/current", detectAttrCurrent)
}

func TestProcOvermount_unsafeHostProcRoot(t *testing.T) {
withWithoutOpenat2(t, func(t *testing.T) {
// If we use the host /proc directly, we should see overmounts.
testProcOvermount(t, unsafeHostProcRoot, true)
})
}

func TestProcOvermount_newPrivateProcMount(t *testing.T) {
if !hasPrivateMounts() {
t.Skip("test requires fsopen/open_tree support")
}
withWithoutOpenat2(t, func(t *testing.T) {
// If we create our own procfs, the overmounts shouldn't appear.
testProcOvermount(t, newPrivateProcMount, false)
})
}

func TestProcOvermount_clonePrivateProcMount(t *testing.T) {
if !hasPrivateMounts() {
t.Skip("test requires fsopen/open_tree support")
}
withWithoutOpenat2(t, func(t *testing.T) {
// If we use open_tree(2), we don't use AT_RECURSIVE when running in
// this test (because the overmounts are not locked mounts) and so we
// don't expect to see overmounts.
// TODO: Explicitly test AT_RECURSIVE
testProcOvermount(t, clonePrivateProcMount, false)
})
}
6 changes: 6 additions & 0 deletions util_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import (
"github.com/stretchr/testify/require"
)

func requireRoot(t *testing.T) {
if os.Geteuid() != 0 {
t.Skip("test requires root")
}
}

func withWithoutOpenat2(t *testing.T, testFn func(t *testing.T)) {
for _, useOpenat2 := range []bool{true, false} {
useOpenat2 := useOpenat2 // copy iterator
Expand Down

0 comments on commit a258c4f

Please sign in to comment.