1package archive // import "github.com/docker/docker/pkg/archive" 2 3import ( 4 "fmt" 5 "io/ioutil" 6 "os" 7 "os/exec" 8 "path/filepath" 9 "syscall" 10 "testing" 11 12 "github.com/docker/docker/pkg/reexec" 13 "github.com/docker/docker/pkg/system" 14 rsystem "github.com/opencontainers/runc/libcontainer/system" 15 "github.com/pkg/errors" 16 "golang.org/x/sys/unix" 17 "gotest.tools/assert" 18 "gotest.tools/skip" 19) 20 21// setupOverlayTestDir creates files in a directory with overlay whiteouts 22// Tree layout 23// . 24// ├── d1 # opaque, 0700 25// │ └── f1 # empty file, 0600 26// ├── d2 # opaque, 0750 27// │ └── f1 # empty file, 0660 28// └── d3 # 0700 29// └── f1 # whiteout, 0644 30func setupOverlayTestDir(t *testing.T, src string) { 31 skip.If(t, os.Getuid() != 0, "skipping test that requires root") 32 skip.If(t, rsystem.RunningInUserNS(), "skipping test that requires initial userns (trusted.overlay.opaque xattr cannot be set in userns, even with Ubuntu kernel)") 33 // Create opaque directory containing single file and permission 0700 34 err := os.Mkdir(filepath.Join(src, "d1"), 0700) 35 assert.NilError(t, err) 36 37 err = system.Lsetxattr(filepath.Join(src, "d1"), "trusted.overlay.opaque", []byte("y"), 0) 38 assert.NilError(t, err) 39 40 err = ioutil.WriteFile(filepath.Join(src, "d1", "f1"), []byte{}, 0600) 41 assert.NilError(t, err) 42 43 // Create another opaque directory containing single file but with permission 0750 44 err = os.Mkdir(filepath.Join(src, "d2"), 0750) 45 assert.NilError(t, err) 46 47 err = system.Lsetxattr(filepath.Join(src, "d2"), "trusted.overlay.opaque", []byte("y"), 0) 48 assert.NilError(t, err) 49 50 err = ioutil.WriteFile(filepath.Join(src, "d2", "f1"), []byte{}, 0660) 51 assert.NilError(t, err) 52 53 // Create regular directory with deleted file 54 err = os.Mkdir(filepath.Join(src, "d3"), 0700) 55 assert.NilError(t, err) 56 57 err = system.Mknod(filepath.Join(src, "d3", "f1"), unix.S_IFCHR, 0) 58 assert.NilError(t, err) 59} 60 61func checkOpaqueness(t *testing.T, path string, opaque string) { 62 xattrOpaque, err := system.Lgetxattr(path, "trusted.overlay.opaque") 63 assert.NilError(t, err) 64 65 if string(xattrOpaque) != opaque { 66 t.Fatalf("Unexpected opaque value: %q, expected %q", string(xattrOpaque), opaque) 67 } 68 69} 70 71func checkOverlayWhiteout(t *testing.T, path string) { 72 stat, err := os.Stat(path) 73 assert.NilError(t, err) 74 75 statT, ok := stat.Sys().(*syscall.Stat_t) 76 if !ok { 77 t.Fatalf("Unexpected type: %t, expected *syscall.Stat_t", stat.Sys()) 78 } 79 if statT.Rdev != 0 { 80 t.Fatalf("Non-zero device number for whiteout") 81 } 82} 83 84func checkFileMode(t *testing.T, path string, perm os.FileMode) { 85 stat, err := os.Stat(path) 86 assert.NilError(t, err) 87 88 if stat.Mode() != perm { 89 t.Fatalf("Unexpected file mode for %s: %o, expected %o", path, stat.Mode(), perm) 90 } 91} 92 93func TestOverlayTarUntar(t *testing.T) { 94 oldmask, err := system.Umask(0) 95 assert.NilError(t, err) 96 defer system.Umask(oldmask) 97 98 src, err := ioutil.TempDir("", "docker-test-overlay-tar-src") 99 assert.NilError(t, err) 100 defer os.RemoveAll(src) 101 102 setupOverlayTestDir(t, src) 103 104 dst, err := ioutil.TempDir("", "docker-test-overlay-tar-dst") 105 assert.NilError(t, err) 106 defer os.RemoveAll(dst) 107 108 options := &TarOptions{ 109 Compression: Uncompressed, 110 WhiteoutFormat: OverlayWhiteoutFormat, 111 } 112 archive, err := TarWithOptions(src, options) 113 assert.NilError(t, err) 114 defer archive.Close() 115 116 err = Untar(archive, dst, options) 117 assert.NilError(t, err) 118 119 checkFileMode(t, filepath.Join(dst, "d1"), 0700|os.ModeDir) 120 checkFileMode(t, filepath.Join(dst, "d2"), 0750|os.ModeDir) 121 checkFileMode(t, filepath.Join(dst, "d3"), 0700|os.ModeDir) 122 checkFileMode(t, filepath.Join(dst, "d1", "f1"), 0600) 123 checkFileMode(t, filepath.Join(dst, "d2", "f1"), 0660) 124 checkFileMode(t, filepath.Join(dst, "d3", "f1"), os.ModeCharDevice|os.ModeDevice) 125 126 checkOpaqueness(t, filepath.Join(dst, "d1"), "y") 127 checkOpaqueness(t, filepath.Join(dst, "d2"), "y") 128 checkOpaqueness(t, filepath.Join(dst, "d3"), "") 129 checkOverlayWhiteout(t, filepath.Join(dst, "d3", "f1")) 130} 131 132func TestOverlayTarAUFSUntar(t *testing.T) { 133 oldmask, err := system.Umask(0) 134 assert.NilError(t, err) 135 defer system.Umask(oldmask) 136 137 src, err := ioutil.TempDir("", "docker-test-overlay-tar-src") 138 assert.NilError(t, err) 139 defer os.RemoveAll(src) 140 141 setupOverlayTestDir(t, src) 142 143 dst, err := ioutil.TempDir("", "docker-test-overlay-tar-dst") 144 assert.NilError(t, err) 145 defer os.RemoveAll(dst) 146 147 archive, err := TarWithOptions(src, &TarOptions{ 148 Compression: Uncompressed, 149 WhiteoutFormat: OverlayWhiteoutFormat, 150 }) 151 assert.NilError(t, err) 152 defer archive.Close() 153 154 err = Untar(archive, dst, &TarOptions{ 155 Compression: Uncompressed, 156 WhiteoutFormat: AUFSWhiteoutFormat, 157 }) 158 assert.NilError(t, err) 159 160 checkFileMode(t, filepath.Join(dst, "d1"), 0700|os.ModeDir) 161 checkFileMode(t, filepath.Join(dst, "d1", WhiteoutOpaqueDir), 0700) 162 checkFileMode(t, filepath.Join(dst, "d2"), 0750|os.ModeDir) 163 checkFileMode(t, filepath.Join(dst, "d2", WhiteoutOpaqueDir), 0750) 164 checkFileMode(t, filepath.Join(dst, "d3"), 0700|os.ModeDir) 165 checkFileMode(t, filepath.Join(dst, "d1", "f1"), 0600) 166 checkFileMode(t, filepath.Join(dst, "d2", "f1"), 0660) 167 checkFileMode(t, filepath.Join(dst, "d3", WhiteoutPrefix+"f1"), 0600) 168} 169 170func unshareCmd(cmd *exec.Cmd) { 171 cmd.SysProcAttr = &syscall.SysProcAttr{ 172 Cloneflags: syscall.CLONE_NEWUSER | syscall.CLONE_NEWNS, 173 UidMappings: []syscall.SysProcIDMap{ 174 { 175 ContainerID: 0, 176 HostID: os.Geteuid(), 177 Size: 1, 178 }, 179 }, 180 GidMappings: []syscall.SysProcIDMap{ 181 { 182 ContainerID: 0, 183 HostID: os.Getegid(), 184 Size: 1, 185 }, 186 }, 187 } 188} 189 190const ( 191 reexecSupportsUserNSOverlay = "docker-test-supports-userns-overlay" 192 reexecMknodChar0 = "docker-test-userns-mknod-char0" 193 reexecSetOpaque = "docker-test-userns-set-opaque" 194) 195 196func supportsOverlay(dir string) error { 197 lower := filepath.Join(dir, "l") 198 upper := filepath.Join(dir, "u") 199 work := filepath.Join(dir, "w") 200 merged := filepath.Join(dir, "m") 201 for _, s := range []string{lower, upper, work, merged} { 202 if err := os.MkdirAll(s, 0700); err != nil { 203 return err 204 } 205 } 206 mOpts := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", lower, upper, work) 207 if err := syscall.Mount("overlay", merged, "overlay", uintptr(0), mOpts); err != nil { 208 return errors.Wrapf(err, "failed to mount overlay (%s) on %s", mOpts, merged) 209 } 210 if err := syscall.Unmount(merged, 0); err != nil { 211 return errors.Wrapf(err, "failed to unmount %s", merged) 212 } 213 return nil 214} 215 216// supportsUserNSOverlay returns nil error if overlay is supported in userns. 217// Only Ubuntu and a few distros support overlay in userns (by patching the kernel). 218// https://lists.ubuntu.com/archives/kernel-team/2014-February/038091.html 219// As of kernel 4.19, the patch is not merged to the upstream. 220func supportsUserNSOverlay() error { 221 tmp, err := ioutil.TempDir("", "docker-test-supports-userns-overlay") 222 if err != nil { 223 return err 224 } 225 defer os.RemoveAll(tmp) 226 cmd := reexec.Command(reexecSupportsUserNSOverlay, tmp) 227 unshareCmd(cmd) 228 out, err := cmd.CombinedOutput() 229 if err != nil { 230 return errors.Wrapf(err, "output: %q", string(out)) 231 } 232 return nil 233} 234 235// isOpaque returns nil error if the dir has trusted.overlay.opaque=y. 236// isOpaque needs to be called in the initial userns. 237func isOpaque(dir string) error { 238 xattrOpaque, err := system.Lgetxattr(dir, "trusted.overlay.opaque") 239 if err != nil { 240 return errors.Wrapf(err, "failed to read opaque flag of %s", dir) 241 } 242 if string(xattrOpaque) != "y" { 243 return errors.Errorf("expected \"y\", got %q", string(xattrOpaque)) 244 } 245 return nil 246} 247 248func TestReexecUserNSOverlayWhiteoutConverter(t *testing.T) { 249 skip.If(t, os.Getuid() != 0, "skipping test that requires root") 250 skip.If(t, rsystem.RunningInUserNS(), "skipping test that requires initial userns") 251 if err := supportsUserNSOverlay(); err != nil { 252 t.Skipf("skipping test that requires kernel support for overlay-in-userns: %v", err) 253 } 254 tmp, err := ioutil.TempDir("", "docker-test-userns-overlay") 255 assert.NilError(t, err) 256 defer os.RemoveAll(tmp) 257 258 char0 := filepath.Join(tmp, "char0") 259 cmd := reexec.Command(reexecMknodChar0, char0) 260 unshareCmd(cmd) 261 out, err := cmd.CombinedOutput() 262 assert.NilError(t, err, string(out)) 263 assert.NilError(t, isChar0(char0)) 264 265 opaqueDir := filepath.Join(tmp, "opaquedir") 266 err = os.MkdirAll(opaqueDir, 0755) 267 assert.NilError(t, err, string(out)) 268 cmd = reexec.Command(reexecSetOpaque, opaqueDir) 269 unshareCmd(cmd) 270 out, err = cmd.CombinedOutput() 271 assert.NilError(t, err, string(out)) 272 assert.NilError(t, isOpaque(opaqueDir)) 273} 274 275func init() { 276 reexec.Register(reexecSupportsUserNSOverlay, func() { 277 if err := supportsOverlay(os.Args[1]); err != nil { 278 panic(err) 279 } 280 }) 281 reexec.Register(reexecMknodChar0, func() { 282 if err := mknodChar0Overlay(os.Args[1]); err != nil { 283 panic(err) 284 } 285 }) 286 reexec.Register(reexecSetOpaque, func() { 287 if err := replaceDirWithOverlayOpaque(os.Args[1]); err != nil { 288 panic(err) 289 } 290 }) 291 if reexec.Init() { 292 os.Exit(0) 293 } 294} 295