Skip to content

Commit b9a9304

Browse files
committed
archive: add test for chmod fallback on nodev mounts (skipped)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
1 parent c4f28d2 commit b9a9304

3 files changed

Lines changed: 162 additions & 4 deletions

File tree

archive_linux_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,98 @@ func TestOverlayTarAUFSUntar(t *testing.T) {
222222
checkFileMode(t, filepath.Join(dst, "d2", "f1"), 0o660)
223223
checkFileMode(t, filepath.Join(dst, "d3", WhiteoutPrefix+"f1"), 0o600)
224224
}
225+
226+
// TestChmodNoSymlinkFallback verifies that the chmod fallback applies modes to
227+
// non-symlink entries, including device nodes on a nodev mount.
228+
//
229+
// Regression test for https://github.com/moby/go-archive/issues/98
230+
// Regression test for https://github.com/moby/moby/issues/53299
231+
func TestChmodNoSymlinkFallback(t *testing.T) {
232+
for _, tc := range []struct {
233+
name string
234+
nodev bool
235+
create func(string) error
236+
broken bool
237+
238+
needsRoot bool
239+
}{
240+
{
241+
name: "regular-file",
242+
create: func(p string) error {
243+
return os.WriteFile(p, nil, 0o600)
244+
},
245+
},
246+
{
247+
name: "directory",
248+
create: func(p string) error {
249+
return os.Mkdir(p, 0o700)
250+
},
251+
},
252+
{
253+
name: "fifo",
254+
create: func(p string) error {
255+
return unix.Mkfifo(p, 0o600)
256+
},
257+
},
258+
{
259+
name: "character-device-on-nodev",
260+
nodev: true,
261+
create: func(p string) error {
262+
return mknod(p, unix.S_IFCHR|0o600, unix.Mkdev(1, 3))
263+
},
264+
needsRoot: true,
265+
broken: true,
266+
},
267+
{
268+
name: "block-device-on-nodev",
269+
nodev: true,
270+
create: func(p string) error {
271+
return mknod(p, unix.S_IFBLK|0o600, unix.Mkdev(7, 0))
272+
},
273+
needsRoot: true,
274+
broken: true,
275+
},
276+
{
277+
// see https://github.com/moby/moby/issues/53299
278+
name: "ptmx-device",
279+
create: func(entryPath string) error {
280+
return mknod(entryPath, unix.S_IFCHR|0o666, unix.Mkdev(5, 2))
281+
},
282+
needsRoot: true,
283+
broken: true,
284+
},
285+
} {
286+
t.Run(tc.name, func(t *testing.T) {
287+
if tc.broken {
288+
t.Skip("FIXME: fallback cannot open device nodes on nodev mounts")
289+
}
290+
if tc.needsRoot {
291+
skip.If(t, os.Getuid() != 0, "requires root")
292+
skip.If(t, userns.RunningInUserNS(), "requires initial user namespace")
293+
}
294+
295+
tmpDir := t.TempDir()
296+
if tc.nodev {
297+
assert.NilError(t, unix.Mount("tmpfs", tmpDir, "tmpfs", unix.MS_NODEV, ""))
298+
t.Cleanup(func() {
299+
assert.Check(t, unix.Unmount(tmpDir, 0))
300+
})
301+
}
302+
303+
entryPath := filepath.Join(tmpDir, tc.name)
304+
assert.NilError(t, tc.create(entryPath))
305+
306+
parent, err := os.Open(tmpDir)
307+
assert.NilError(t, err)
308+
defer parent.Close()
309+
310+
// #nosec G115 -- file descriptors fit in int on supported platforms.
311+
err = chmodNoSymlinkFallback(int(parent.Fd()), tc.name, tc.name, 0o640)
312+
assert.NilError(t, err, "entryPath: %s", entryPath)
313+
314+
fi, err := os.Lstat(entryPath)
315+
assert.NilError(t, err)
316+
assert.Equal(t, fi.Mode().Perm(), os.FileMode(0o640))
317+
})
318+
}
319+
}

archive_nolinux_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//go:build !linux && !windows
2+
3+
package archive
4+
5+
import (
6+
"os"
7+
"path/filepath"
8+
"testing"
9+
10+
"golang.org/x/sys/unix"
11+
"gotest.tools/v3/assert"
12+
)
13+
14+
// TestChmodNoSymlinkFallback verifies that the chmod fallback applies modes to
15+
// non-symlink entries.
16+
func TestChmodNoSymlinkFallback(t *testing.T) {
17+
for _, tc := range []struct {
18+
name string
19+
create func(string) error
20+
}{
21+
{
22+
name: "regular-file",
23+
create: func(p string) error {
24+
return os.WriteFile(p, nil, 0o600)
25+
},
26+
},
27+
{
28+
name: "directory",
29+
create: func(p string) error {
30+
return os.Mkdir(p, 0o700)
31+
},
32+
},
33+
{
34+
name: "fifo",
35+
create: func(p string) error {
36+
return unix.Mkfifo(p, 0o600)
37+
},
38+
},
39+
} {
40+
t.Run(tc.name, func(t *testing.T) {
41+
tmpDir := t.TempDir()
42+
entryPath := filepath.Join(tmpDir, tc.name)
43+
assert.NilError(t, tc.create(entryPath))
44+
45+
parent, err := os.Open(tmpDir)
46+
assert.NilError(t, err)
47+
defer parent.Close()
48+
49+
// #nosec G115 -- file descriptors fit in int on supported platforms.
50+
err = chmodNoSymlinkFallback(int(parent.Fd()), tc.name, tc.name, 0o640)
51+
assert.NilError(t, err, "entryPath: %s", entryPath)
52+
53+
fi, err := os.Lstat(entryPath)
54+
assert.NilError(t, err)
55+
assert.Equal(t, fi.Mode().Perm(), os.FileMode(0o640))
56+
})
57+
}
58+
}

archive_unix.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,15 @@ func chmodNoSymlink(root *os.Root, name string, mode os.FileMode) error {
126126
}
127127

128128
// Fallback for systems that cannot perform fchmodat with AT_SYMLINK_NOFOLLOW.
129-
// Open the entry without following symlinks and apply the mode through the
130-
// resulting file descriptor.
131-
// #nosec G115 -- ignore integer overflow conversion for parent.Fd
132-
fd, err := unix.Openat(int(parent.Fd()), base, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0)
129+
return chmodNoSymlinkFallback(int(parent.Fd()), base, name, perm) // #nosec G115 -- ignore integer overflow conversion for parent.Fd
130+
}
131+
132+
// chmodNoSymlinkFallback applies mode without following the final path
133+
// component on systems without fchmodat2 support.
134+
//
135+
// Callers must have already excluded symlink entries.
136+
func chmodNoSymlinkFallback(parentFD int, base, name string, perm uint32) error {
137+
fd, err := unix.Openat(parentFD, base, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0)
133138
if err != nil {
134139
return &os.PathError{Op: "openat", Path: name, Err: err}
135140
}

0 commit comments

Comments
 (0)