Skip to content

Commit 3daca2a

Browse files
committed
archive: test chmod fallback without procfs in chroot
Add a regression test that runs the chmod fallback after switching into a temporary root without procfs mounted. This reproduces the environment used by chrootarchive, where resolving an O_PATH descriptor through /proc/self/fd is unavailable. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
1 parent f357902 commit 3daca2a

1 file changed

Lines changed: 76 additions & 0 deletions

File tree

archive_linux_chrooted_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//go:build linux
2+
3+
package archive
4+
5+
import (
6+
"errors"
7+
"fmt"
8+
"os"
9+
"path/filepath"
10+
"testing"
11+
12+
"github.com/moby/go-archive/internal/mounttree"
13+
"github.com/moby/go-archive/internal/unshare"
14+
"github.com/moby/sys/mount"
15+
"github.com/moby/sys/userns"
16+
"golang.org/x/sys/unix"
17+
"gotest.tools/v3/assert"
18+
"gotest.tools/v3/skip"
19+
)
20+
21+
// TestChmodNoSymlinkFallbackInChrootWithoutProc verifies that the chmod
22+
// fallback works after switching into a root without procfs mounted.
23+
//
24+
// The test lives in the archive package instead of chrootarchive because it
25+
// exercises the unexported fallback directly. Reproducing chrootarchive's
26+
// root-switching setup here is a small workaround to keep the test focused;
27+
// moving the shared extraction internals into an internal package may provide
28+
// a cleaner boundary in the future.
29+
func TestChmodNoSymlinkFallbackInChrootWithoutProc(t *testing.T) {
30+
skip.If(t, os.Getuid() != 0, "test requires root")
31+
skip.If(t, userns.RunningInUserNS(), "test requires the initial user namespace")
32+
33+
const name = "target-dir/target-file"
34+
35+
root := t.TempDir()
36+
assert.NilError(t, os.Mkdir(filepath.Join(root, filepath.Dir(name)), 0o755))
37+
assert.NilError(t, os.WriteFile(filepath.Join(root, name), nil, 0o600))
38+
39+
setupFn := func() error {
40+
if err := mount.MakeRSlave("/"); err != nil {
41+
return err
42+
}
43+
return mounttree.SwitchRoot(root)
44+
}
45+
46+
var testErr error
47+
err := unshare.Go(unix.CLONE_FS|unix.CLONE_NEWNS, setupFn, func() {
48+
if _, err := os.Stat("/proc/self/fd"); !errors.Is(err, os.ErrNotExist) {
49+
testErr = fmt.Errorf("/proc/self/fd: expected not to exist, got %w", err)
50+
return
51+
}
52+
53+
parentFD, err := unix.Open("/"+filepath.Dir(name), unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
54+
if err != nil {
55+
testErr = err
56+
return
57+
}
58+
defer unix.Close(parentFD)
59+
60+
if err := chmodNoSymlinkFallback(parentFD, filepath.Base(name), name, 0o644); err != nil {
61+
testErr = err
62+
return
63+
}
64+
65+
var stat unix.Stat_t
66+
if err := unix.Stat("/"+name, &stat); err != nil {
67+
testErr = err
68+
return
69+
}
70+
if got := stat.Mode & 0o777; got != 0o644 {
71+
testErr = fmt.Errorf("file mode = %#o, want %#o", got, uint32(0o644))
72+
}
73+
})
74+
assert.NilError(t, err)
75+
assert.NilError(t, testErr)
76+
}

0 commit comments

Comments
 (0)