From 8d12833c695c850ff3d5ce560f4b53a09c99ed82 Mon Sep 17 00:00:00 2001 From: Omry Yadan Date: Tue, 3 Nov 2026 20:41:09 +0800 Subject: [PATCH] F1.2: Prepare declared objects through pinned descriptors Add an inactive private Linux preparer for explicitly declared managed directories and files. Confine lookups to held descriptors, verify existing provenance and numeric metadata without repair, create directories parent-first and files exclusively, and return held identities with explicit descriptor ownership. Prepare directory metadata in private same-mount staging before no-replace publication. Preserve kernel ACL, umask and setgid inheritance, reject aliases and mount crossings, and retain inhibited inert effects on failure. Selected subordinate setup and exclusive unattached-resource authority remain the caller contracts implemented by F1.3/F2. Public mapped execution remains gated. Validation: both temporary-directory mount layouts, ten focused race runs, all selected tracked Go packages and multi-identity conformance passed on this exact candidate. The existing host-specific tmpfs exclusion remains explicit. Full current-agent review and a distinct fresh independent Sol review are clean; complete exact-head CI and remote PR review remain publication gates. --- .../managed_mount_objects_linux.go | 320 ++++++++++++ .../managed_mount_objects_linux_test.go | 489 ++++++++++++++++++ .../managed_mount_objects_race_linux_test.go | 251 +++++++++ 3 files changed, 1060 insertions(+) create mode 100644 internal/dockerdeploy/managed_mount_objects_linux.go create mode 100644 internal/dockerdeploy/managed_mount_objects_linux_test.go create mode 100644 internal/dockerdeploy/managed_mount_objects_race_linux_test.go diff --git a/internal/dockerdeploy/managed_mount_objects_linux.go b/internal/dockerdeploy/managed_mount_objects_linux.go new file mode 100644 index 00000000..51a74980 --- /dev/null +++ b/internal/dockerdeploy/managed_mount_objects_linux.go @@ -0,0 +1,320 @@ +//go:build linux + +package dockerdeploy + +import ( + "context" + "crypto/rand" + "errors" + "fmt" + "os" + "path" + "sort" + "strings" + + "github.com/omry/reploy/internal/blueprint" + "golang.org/x/sys/unix" +) + +type managedMountObjectIdentityV1 struct { + Device, Inode, Mount uint64 + UID, GID, Mode uint32 +} + +type preparedManagedMountObjectV1 struct { + FD int + Identity managedMountObjectIdentityV1 +} + +// prepareManagedMountObjectsV1 is an inert Linux primitive, not admission. +// The caller must hold exclusive, unattached managed-storage authority and run +// inside the selected setup mapping. root, staging and recorded descriptors are borrowed; +// recorded descriptors must come from that resource's retained provenance. +// staging must be a distinct, private, setup-owned directory on the same mount, +// inaccessible to workload writers for the entire call. F1.3/F2 supply and fence +// that authority; matching UID alone does not establish its provenance. +// Success transfers ownership of every returned descriptor to the caller. +// Failure closes owned descriptors, leaves any newly created inert objects in +// place (including private staging entries) and requires the caller to retain +// inhibition for explicit recovery. Staging's default ACL is temporary setup state. +// Existing objects are never chowned, chmodded, truncated or adopted by path. +func prepareManagedMountObjectsV1(ctx context.Context, root, staging int, objects []blueprint.ManagedMountObject, recorded map[string]int) (result map[string]preparedManagedMountObjectV1, err error) { + ordered, err := orderManagedMountObjectsV1(objects) + if err != nil { + return nil, err + } + rootIdentity, err := managedMountObjectIdentityForFDV1(root) + if err != nil || rootIdentity.Mode&unix.S_IFMT != unix.S_IFDIR { + return nil, fmt.Errorf("managed root must be a held directory: %v", err) + } + stagingIdentity, err := managedMountObjectIdentityForFDV1(staging) + if err != nil || stagingIdentity.Mode&unix.S_IFMT != unix.S_IFDIR || stagingIdentity.Mode&0o7777 != 0o700 || + stagingIdentity.UID != uint32(os.Geteuid()) || stagingIdentity.Mount != rootIdentity.Mount || + stagingIdentity.Device == rootIdentity.Device && stagingIdentity.Inode == rootIdentity.Inode { + return nil, fmt.Errorf("managed staging must be a distinct private setup-owned directory on the managed mount: %v", err) + } + prepared := make(map[string]preparedManagedMountObjectV1, len(ordered)) + defer func() { + if err != nil { + for _, object := range prepared { + unix.Close(object.FD) + } + } + }() + for _, declaration := range ordered { + if err = ctx.Err(); err != nil { + return nil, err + } + object, prepareErr := prepareManagedMountObjectV1(root, staging, rootIdentity.Mount, declaration, recorded, prepared) + if prepareErr != nil { + return nil, fmt.Errorf("prepare managed object %q: %w", declaration.Path, prepareErr) + } + prepared[declaration.Path] = object + } + if err = ctx.Err(); err != nil { + return nil, err + } + return prepared, nil +} + +func orderManagedMountObjectsV1(objects []blueprint.ManagedMountObject) ([]blueprint.ManagedMountObject, error) { + if len(objects) == 0 { + return nil, fmt.Errorf("managed preparation requires an explicit root declaration") + } + ordered := append([]blueprint.ManagedMountObject(nil), objects...) + seen := make(map[string]bool, len(objects)) + for _, object := range ordered { + name := object.Path + if name == "" || path.IsAbs(name) || path.Clean(name) != name || name == ".." || strings.HasPrefix(name, "../") || strings.ContainsAny(name, "\\\x00\r\n\t") { + return nil, fmt.Errorf("managed object requires a normalized relative Linux path") + } + if _, duplicate := seen[name]; duplicate { + return nil, fmt.Errorf("duplicate managed object %q", name) + } + if name == "." && !object.Directory || object.Identity.Permissions != nil && *object.Identity.Permissions > 0o7777 { + return nil, fmt.Errorf("invalid managed object declaration %q", name) + } + seen[name] = object.Directory + } + if directory, exists := seen["."]; !exists || !directory { + return nil, fmt.Errorf("managed preparation requires a directory root") + } + for _, object := range ordered { + for parent := path.Dir(object.Path); parent != "."; parent = path.Dir(parent) { + if directory, declared := seen[parent]; declared && !directory { + return nil, fmt.Errorf("managed object %q has a file ancestor", object.Path) + } + } + } + sort.Slice(ordered, func(i, j int) bool { + a, b := ordered[i], ordered[j] + if a.Path == "." || b.Path == "." { + return a.Path == "." + } + if a.Directory != b.Directory { + return a.Directory + } + if depthA, depthB := strings.Count(a.Path, "/"), strings.Count(b.Path, "/"); depthA != depthB { + return depthA < depthB + } + return a.Path < b.Path + }) + return ordered, nil +} + +func openManagedMountObjectV1(root int, name string, flags int) (int, error) { + return unix.Openat2(root, name, &unix.OpenHow{ + Flags: uint64(flags | unix.O_CLOEXEC | unix.O_NOFOLLOW), + Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS | + unix.RESOLVE_NO_MAGICLINKS | unix.RESOLVE_NO_XDEV, + }) +} + +func managedMountObjectIdentityForFDV1(fd int) (managedMountObjectIdentityV1, error) { + var stat unix.Stat_t + if err := unix.Fstat(fd, &stat); err != nil { + return managedMountObjectIdentityV1{}, err + } + if kind := stat.Mode & unix.S_IFMT; kind != unix.S_IFDIR && (kind != unix.S_IFREG || stat.Nlink != 1) { + return managedMountObjectIdentityV1{}, fmt.Errorf("managed object must be a directory or unaliased regular file") + } + var mount unix.Statx_t + if err := unix.Statx(fd, "", unix.AT_EMPTY_PATH, unix.STATX_MNT_ID, &mount); err != nil { + return managedMountObjectIdentityV1{}, err + } + if mount.Mask&unix.STATX_MNT_ID == 0 { + return managedMountObjectIdentityV1{}, fmt.Errorf("kernel did not report the held object's mount identity") + } + return managedMountObjectIdentityV1{Device: uint64(stat.Dev), Inode: stat.Ino, Mount: mount.Mnt_id, UID: stat.Uid, GID: stat.Gid, Mode: stat.Mode}, nil +} + +func verifyManagedMountObjectV1(fd int, mount uint64, declaration blueprint.ManagedMountObject, expected int) (managedMountObjectIdentityV1, error) { + actual, err := managedMountObjectIdentityForFDV1(fd) + if err != nil { + return actual, err + } + retained, err := managedMountObjectIdentityForFDV1(expected) + if err != nil { + return actual, fmt.Errorf("recorded descriptor: %w", err) + } + kind := uint32(unix.S_IFREG) + if declaration.Directory { + kind = unix.S_IFDIR + } + identity := declaration.Identity + if actual != retained || actual.Mount != mount || actual.Mode&unix.S_IFMT != kind || actual.UID != identity.UID || actual.GID != identity.GID || + identity.Permissions != nil && actual.Mode&0o7777 != *identity.Permissions { + return actual, fmt.Errorf("managed object identity, type or declared metadata differs from recorded authority") + } + return actual, nil +} + +func prepareManagedMountObjectV1(root, staging int, mount uint64, declaration blueprint.ManagedMountObject, recorded map[string]int, prepared map[string]preparedManagedMountObjectV1) (preparedManagedMountObjectV1, error) { + name := declaration.Path + fd, err := openManagedMountObjectV1(root, name, unix.O_PATH) + if err == nil { + expected, exists := recorded[name] + if !exists { + unix.Close(fd) + return preparedManagedMountObjectV1{}, fmt.Errorf("existing object has no retained descriptor provenance") + } + identity, err := verifyManagedMountObjectV1(fd, mount, declaration, expected) + if err != nil { + unix.Close(fd) + return preparedManagedMountObjectV1{}, err + } + return preparedManagedMountObjectV1{FD: fd, Identity: identity}, nil + } + if !errors.Is(err, unix.ENOENT) || name == "." { + return preparedManagedMountObjectV1{}, err + } + if _, exists := recorded[name]; exists { + return preparedManagedMountObjectV1{}, fmt.Errorf("recorded managed object is missing") + } + parentName := path.Dir(name) + parent, err := openManagedMountObjectV1(root, parentName, unix.O_PATH|unix.O_DIRECTORY) + if err != nil { + return preparedManagedMountObjectV1{}, err + } + defer unix.Close(parent) + expected, exists := recorded[parentName] + if object, ready := prepared[parentName]; ready { + expected, exists = object.FD, true + } + if !exists { + return preparedManagedMountObjectV1{}, fmt.Errorf("parent has no retained descriptor provenance") + } + parentIdentity, err := managedMountObjectIdentityForFDV1(parent) + if err != nil { + return preparedManagedMountObjectV1{}, err + } + retained, err := managedMountObjectIdentityForFDV1(expected) + if err != nil || parentIdentity != retained || parentIdentity.Mount != mount { + return preparedManagedMountObjectV1{}, fmt.Errorf("managed parent was substituted") + } + return createManagedMountObjectV1(parent, staging, path.Base(name), mount, declaration) +} + +// Reuse kernel ACL inheritance rather than interpreting or rebuilding ACLs. +// Only the private staging directory's default ACL changes, never the parent. +func inheritManagedMountDirectoryACLV1(parent, staging int) error { + fd, err := openManagedMountObjectV1(parent, ".", unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NONBLOCK) + if err != nil { + return err + } + defer unix.Close(fd) + acl := make([]byte, 65536) // Linux's maximum extended-attribute value size. + n, err := unix.Fgetxattr(fd, "system.posix_acl_default", acl) + if errors.Is(err, unix.ENODATA) || errors.Is(err, unix.EOPNOTSUPP) { + err = unix.Fremovexattr(staging, "system.posix_acl_default") + if errors.Is(err, unix.ENODATA) || errors.Is(err, unix.EOPNOTSUPP) { + return nil + } + return err + } + if err != nil { + return err + } + return unix.Fsetxattr(staging, "system.posix_acl_default", acl[:n], 0) +} + +func createManagedMountObjectV1(parent, staging int, leaf string, mount uint64, declaration blueprint.ManagedMountObject) (result preparedManagedMountObjectV1, err error) { + mode := uint32(0o644) + if declaration.Directory { + mode = 0o755 + } + // Files return their creation FD atomically. Directories must be pinned and + // receive metadata privately before their destination name becomes visible. + var fd int + var inheritedMode uint32 + var stagingName string + var stagingFD int + if declaration.Directory { + parentIdentity, statErr := managedMountObjectIdentityForFDV1(parent) + if statErr != nil { + return result, statErr + } + inheritedMode = parentIdentity.Mode & unix.S_ISGID + stagingFD, err = openManagedMountObjectV1(staging, ".", unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NONBLOCK) + if err != nil { + return result, err + } + defer unix.Close(stagingFD) + if err = inheritManagedMountDirectoryACLV1(parent, stagingFD); err != nil { + return result, err + } + stagingName = "reploy-dir-" + rand.Text() + if err = unix.Mkdirat(stagingFD, stagingName, mode); err != nil { + return result, err + } + fd, err = openManagedMountObjectV1(stagingFD, stagingName, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NONBLOCK) + } else { + fd, err = unix.Openat(parent, leaf, unix.O_RDWR|unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW|unix.O_CLOEXEC|unix.O_NONBLOCK, mode) + } + if err != nil { + return result, err + } + defer func() { + if err != nil { + unix.Close(fd) + } + }() + identity, err := managedMountObjectIdentityForFDV1(fd) + if err != nil { + return result, err + } + if identity.Mount != mount || identity.UID != uint32(os.Geteuid()) { + return result, fmt.Errorf("new object differs from exclusive setup creation") + } + // Preserve the creation mode after umask when no mode was requested. + mode = identity.Mode&0o7777 | inheritedMode + if declaration.Identity.Permissions != nil { + mode = *declaration.Identity.Permissions + } + if err = unix.Fchown(fd, int(declaration.Identity.UID), int(declaration.Identity.GID)); err != nil { + return result, err + } + if err = unix.Fchmod(fd, mode); err != nil { + return result, err + } + if declaration.Directory { + if err = unix.Renameat2(stagingFD, stagingName, parent, leaf, unix.RENAME_NOREPLACE); err != nil { + return result, err + } + } + identity, err = managedMountObjectIdentityForFDV1(fd) + if err != nil { + return result, err + } + observed, err := openManagedMountObjectV1(parent, leaf, unix.O_PATH) + if err != nil { + return result, err + } + defer unix.Close(observed) + declared := declaration + declared.Identity.Permissions = &mode + if _, err = verifyManagedMountObjectV1(observed, mount, declared, fd); err != nil { + return result, err + } + return preparedManagedMountObjectV1{FD: fd, Identity: identity}, nil +} diff --git a/internal/dockerdeploy/managed_mount_objects_linux_test.go b/internal/dockerdeploy/managed_mount_objects_linux_test.go new file mode 100644 index 00000000..7219f5c1 --- /dev/null +++ b/internal/dockerdeploy/managed_mount_objects_linux_test.go @@ -0,0 +1,489 @@ +//go:build linux + +package dockerdeploy + +import ( + "context" + "encoding/binary" + "errors" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/omry/reploy/internal/blueprint" + "golang.org/x/sys/unix" +) + +type managedObjectCancelAtCompletion struct { + context.Context + checks int +} + +func (ctx *managedObjectCancelAtCompletion) Err() error { + ctx.checks++ + if ctx.checks >= 3 { + return context.Canceled + } + return nil +} + +func managedObjectTestFD(t *testing.T, name string) int { + t.Helper() + fd, err := unix.Open(name, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { unix.Close(fd) }) + return fd +} + +func managedObjectTestStagingFD(t *testing.T) int { + t.Helper() + name := t.TempDir() + if err := os.Chmod(name, 0o700); err != nil { + t.Fatal(err) + } + return managedObjectTestFD(t, name) +} + +func managedObjectTestDeclaration(name string, directory bool, mode *uint32) blueprint.ManagedMountObject { + return blueprint.ManagedMountObject{Path: name, Directory: directory, Identity: blueprint.MountMetadataIdentity{ + UID: uint32(os.Geteuid()), GID: uint32(os.Getegid()), Permissions: mode, + }} +} + +func managedObjectTestMode(mode uint32) *uint32 { return &mode } + +func closePreparedManagedObjects(t *testing.T, objects map[string]preparedManagedMountObjectV1) { + t.Helper() + for name, object := range objects { + if err := unix.Close(object.FD); err != nil { + t.Errorf("close %s: %v", name, err) + } + } +} + +func TestManagedMountObjectsCreateAndVerify(t *testing.T) { + stagingFD := managedObjectTestStagingFD(t) + root := t.TempDir() + fd := managedObjectTestFD(t, root) + objects := []blueprint.ManagedMountObject{ + managedObjectTestDeclaration("queue/active/log", false, managedObjectTestMode(0o640)), + managedObjectTestDeclaration("queue/active", true, managedObjectTestMode(0o750)), + managedObjectTestDeclaration(".", true, nil), + managedObjectTestDeclaration("queue", true, managedObjectTestMode(0o2750)), + managedObjectTestDeclaration("zero", false, managedObjectTestMode(0)), + } + result, err := prepareManagedMountObjectsV1(context.Background(), fd, stagingFD, objects, map[string]int{".": fd}) + if err != nil { + t.Fatal(err) + } + defer closePreparedManagedObjects(t, result) + if len(result) != len(objects) { + t.Fatalf("prepared %d objects, want %d", len(result), len(objects)) + } + for _, object := range objects { + got := result[object.Path] + if got.Identity.UID != object.Identity.UID || got.Identity.GID != object.Identity.GID { + t.Errorf("%s: wrong ownership: %+v", object.Path, got.Identity) + } + if object.Identity.Permissions != nil && got.Identity.Mode&0o7777 != *object.Identity.Permissions { + t.Errorf("%s: mode %#o", object.Path, got.Identity.Mode) + } + flags, err := unix.FcntlInt(uintptr(got.FD), unix.F_GETFD, 0) + if err != nil || flags&unix.FD_CLOEXEC == 0 { + t.Errorf("%s: descriptor not close-on-exec: %d %v", object.Path, flags, err) + } + } + log := filepath.Join(root, "queue/active/log") + if err := os.WriteFile(log, []byte("application data survives verification"), 0o600); err != nil { + t.Fatal(err) + } + recorded := make(map[string]int) + for name, object := range result { + recorded[name] = object.FD + } + verified, err := prepareManagedMountObjectsV1(context.Background(), fd, stagingFD, objects, recorded) + if err != nil { + t.Fatal(err) + } + closePreparedManagedObjects(t, verified) + data, err := os.ReadFile(log) + if err != nil || string(data) != "application data survives verification" { + t.Fatalf("verification changed file contents: %q %v", data, err) + } +} + +func TestManagedMountObjectsRefuseMalformedManifestBeforeEffects(t *testing.T) { + stagingFD := managedObjectTestStagingFD(t) + for _, name := range []string{"", "/escape", "..", "../escape", "a/../b", "a//b", "a/", "a\\b", "a\x00b", "a\nb", "a\tb"} { + t.Run(strings.ReplaceAll(name, "/", "_"), func(t *testing.T) { + root := t.TempDir() + fd := managedObjectTestFD(t, root) + objects := []blueprint.ManagedMountObject{managedObjectTestDeclaration(".", true, nil), managedObjectTestDeclaration("first", true, nil), managedObjectTestDeclaration(name, false, nil)} + if result, err := prepareManagedMountObjectsV1(context.Background(), fd, stagingFD, objects, map[string]int{".": fd}); err == nil || result != nil { + t.Fatalf("accepted malformed manifest: %v", result) + } + entries, err := os.ReadDir(root) + if err != nil || len(entries) != 0 { + t.Fatalf("invalid manifest had effects: %v %v", entries, err) + } + }) + } + root := managedObjectTestDeclaration(".", true, nil) + for _, objects := range [][]blueprint.ManagedMountObject{ + nil, + {managedObjectTestDeclaration("file", false, nil)}, + {managedObjectTestDeclaration(".", false, nil)}, + {root, root}, + {root, managedObjectTestDeclaration("a", false, nil), managedObjectTestDeclaration("a/b", false, nil)}, + {root, managedObjectTestDeclaration("mode", false, managedObjectTestMode(0o10000))}, + } { + if _, err := orderManagedMountObjectsV1(objects); err == nil { + t.Fatalf("accepted invalid declarations: %+v", objects) + } + } +} + +func TestManagedMountObjectsExistingObjectsRequireExactAuthority(t *testing.T) { + stagingFD := managedObjectTestStagingFD(t) + for _, variant := range []string{"no root provenance", "no file provenance", "wrong root", "wrong file", "missing recorded file", "wrong owner", "wrong group", "wrong mode", "wrong type", "hardlink", "symlink", "fifo", "socket"} { + t.Run(variant, func(t *testing.T) { + root, foreign := t.TempDir(), t.TempDir() + file, canary := filepath.Join(root, "file"), filepath.Join(foreign, "canary") + for _, p := range []string{file, canary} { + if err := os.WriteFile(p, []byte("retained data"), 0o600); err != nil { + t.Fatal(err) + } + } + fd := managedObjectTestFD(t, root) + fileFD := managedObjectTestFD(t, file) + recorded := map[string]int{".": fd, "file": fileFD} + objects := []blueprint.ManagedMountObject{managedObjectTestDeclaration(".", true, nil), managedObjectTestDeclaration("file", false, managedObjectTestMode(0o600))} + switch variant { + case "no root provenance": + delete(recorded, ".") + case "no file provenance": + delete(recorded, "file") + case "wrong root": + recorded["."] = managedObjectTestFD(t, foreign) + case "wrong file": + recorded["file"] = managedObjectTestFD(t, canary) + case "missing recorded file": + if err := os.Remove(file); err != nil { + t.Fatal(err) + } + case "wrong owner": + objects[1].Identity.UID++ + case "wrong group": + objects[1].Identity.GID++ + case "wrong mode": + objects[1].Identity.Permissions = managedObjectTestMode(0o644) + case "wrong type": + objects[1].Directory = true + case "hardlink": + if err := os.Link(file, filepath.Join(foreign, "alias")); err != nil { + t.Fatal(err) + } + case "symlink", "fifo", "socket": + if err := os.Remove(file); err != nil { + t.Fatal(err) + } + var err error + switch variant { + case "symlink": + err = os.Symlink(canary, file) + case "fifo": + err = unix.Mkfifo(file, 0o600) + case "socket": + s, socketErr := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) + if socketErr != nil { + t.Fatal(socketErr) + } + defer unix.Close(s) + err = unix.Bind(s, &unix.SockaddrUnix{Name: file}) + } + if err != nil { + t.Fatal(err) + } + } + before, err := os.Stat(canary) + if err != nil { + t.Fatal(err) + } + result, err := prepareManagedMountObjectsV1(context.Background(), fd, stagingFD, objects, recorded) + if err == nil || result != nil { + t.Fatalf("accepted %s: %+v", variant, result) + } + after, err := os.Stat(canary) + data, readErr := os.ReadFile(canary) + if err != nil || readErr != nil || string(data) != "retained data" || before.Mode() != after.Mode() || !before.ModTime().Equal(after.ModTime()) { + t.Fatalf("foreign canary changed: %q %v %v", data, err, readErr) + } + }) + } +} + +func TestManagedMountObjectsDoNotCreateOrAdoptImplicitParents(t *testing.T) { + stagingFD := managedObjectTestStagingFD(t) + root := t.TempDir() + fd := managedObjectTestFD(t, root) + objects := []blueprint.ManagedMountObject{managedObjectTestDeclaration(".", true, nil), managedObjectTestDeclaration("parent/child", false, nil)} + if _, err := prepareManagedMountObjectsV1(context.Background(), fd, stagingFD, objects, map[string]int{".": fd}); err == nil { + t.Fatal("created implicit parent") + } + if err := os.Mkdir(filepath.Join(root, "parent"), 0o700); err != nil { + t.Fatal(err) + } + if _, err := prepareManagedMountObjectsV1(context.Background(), fd, stagingFD, objects, map[string]int{".": fd}); err == nil { + t.Fatal("adopted unrecorded parent") + } + parent := managedObjectTestFD(t, filepath.Join(root, "parent")) + result, err := prepareManagedMountObjectsV1(context.Background(), fd, stagingFD, objects, map[string]int{".": fd, "parent": parent}) + if err != nil { + t.Fatal(err) + } + closePreparedManagedObjects(t, result) +} + +func TestManagedMountObjectsRefuseNestedMountWithoutHostMutation(t *testing.T) { + root := managedObjectTestFD(t, "/") + proc := managedObjectTestFD(t, "/proc") + rootStat, err := managedMountObjectIdentityForFDV1(root) + if err != nil { + t.Fatal(err) + } + procStat, err := managedMountObjectIdentityForFDV1(proc) + if err != nil { + t.Fatal(err) + } + if procStat.Mount == rootStat.Mount { + t.Fatal("test requires Linux procfs to be a distinct mount") + } + // Isolate the confined lookup from preparation's private staging prerequisite. + // The filesystem containing the test temporary directory is irrelevant here. + fd, err := openManagedMountObjectV1(root, "proc", unix.O_PATH|unix.O_DIRECTORY) + if fd >= 0 { + unix.Close(fd) + } + if !errors.Is(err, unix.EXDEV) { + t.Fatalf("nested mount lookup did not refuse crossing: %v", err) + } +} + +func TestManagedMountObjectsCancelAndCloseOwnedDescriptors(t *testing.T) { + stagingFD := managedObjectTestStagingFD(t) + root := t.TempDir() + fd := managedObjectTestFD(t, root) + objects := []blueprint.ManagedMountObject{managedObjectTestDeclaration(".", true, nil), managedObjectTestDeclaration("a", true, nil), managedObjectTestDeclaration("missing-parent/failure", false, nil)} + count := func() int { + fds, err := os.ReadDir("/proc/self/fd") + if err != nil { + t.Fatal(err) + } + return len(fds) + } + before := count() + for i := 0; i < 20; i++ { + // First call creates a; later calls refuse its missing provenance. + if result, err := prepareManagedMountObjectsV1(context.Background(), fd, stagingFD, objects, map[string]int{".": fd}); err == nil || result != nil { + t.Fatalf("failure returned successful descriptors: %v %v", result, err) + } + } + if after := count(); after != before { + t.Fatalf("owned descriptors leaked: before %d after %d", before, after) + } + if _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); err != nil { + t.Fatalf("borrowed root was closed: %v", err) + } + ctx, cancel := context.WithCancel(context.Background()) + cancel() + if result, err := prepareManagedMountObjectsV1(ctx, fd, stagingFD, objects, map[string]int{".": fd}); !errors.Is(err, context.Canceled) || result != nil { + t.Fatalf("ignored cancellation: %v %v", result, err) + } + entries, err := os.ReadDir(filepath.Join(root, "a")) + if err != nil || len(entries) != 0 { + t.Fatalf("failed preparation changed inert directory contents: %v %v", entries, err) + } +} + +func TestManagedMountObjectsOrderIsStableAndDoesNotMutateInput(t *testing.T) { + objects := []blueprint.ManagedMountObject{ + managedObjectTestDeclaration("z/file", false, nil), managedObjectTestDeclaration("z/b", true, nil), + managedObjectTestDeclaration("z", true, nil), managedObjectTestDeclaration(".", true, nil), + managedObjectTestDeclaration("!first", true, nil), managedObjectTestDeclaration("a", true, nil), managedObjectTestDeclaration("a/file", false, nil), + } + before := append([]blueprint.ManagedMountObject(nil), objects...) + ordered, err := orderManagedMountObjectsV1(objects) + if err != nil { + t.Fatal(err) + } + names := make([]string, len(ordered)) + for i, object := range ordered { + names[i] = object.Path + } + if !reflect.DeepEqual(names, []string{".", "!first", "a", "z", "z/b", "a/file", "z/file"}) || !reflect.DeepEqual(objects, before) { + t.Fatalf("bad order %v or mutated input", names) + } +} + +func TestManagedMountObjectsInvalidDescriptorsNeverAcquireAuthority(t *testing.T) { + stagingFD := managedObjectTestStagingFD(t) + root, foreign := t.TempDir(), t.TempDir() + file := filepath.Join(root, "file") + if err := os.WriteFile(file, []byte("unchanged"), 0o600); err != nil { + t.Fatal(err) + } + rootFD, fileFD := managedObjectTestFD(t, root), managedObjectTestFD(t, file) + objects := []blueprint.ManagedMountObject{managedObjectTestDeclaration(".", true, nil)} + for _, badRoot := range []int{-1, fileFD} { + if result, err := prepareManagedMountObjectsV1(context.Background(), badRoot, stagingFD, objects, map[string]int{".": rootFD}); err == nil || result != nil { + t.Fatalf("accepted invalid root %d: %v %v", badRoot, result, err) + } + } + if result, err := prepareManagedMountObjectsV1(context.Background(), rootFD, stagingFD, objects, map[string]int{".": -1}); err == nil || result != nil { + t.Fatalf("accepted invalid retained root: %v %v", result, err) + } + if err := os.Mkdir(filepath.Join(root, "parent"), 0o700); err != nil { + t.Fatal(err) + } + objects = append(objects, managedObjectTestDeclaration("parent/new", false, nil)) + for _, badParent := range []int{-1, managedObjectTestFD(t, foreign)} { + if result, err := prepareManagedMountObjectsV1(context.Background(), rootFD, stagingFD, objects, map[string]int{".": rootFD, "parent": badParent}); err == nil || result != nil { + t.Fatalf("accepted invalid retained parent: %v %v", result, err) + } + } + entries, err := os.ReadDir(filepath.Join(root, "parent")) + if err != nil || len(entries) != 0 { + t.Fatalf("invalid parent authority had effects: %v %v", entries, err) + } +} + +func TestManagedMountObjectsCancellationBeforeReceiptClosesPartialSuccess(t *testing.T) { + stagingFD := managedObjectTestStagingFD(t) + root := t.TempDir() + fd := managedObjectTestFD(t, root) + ctx := &managedObjectCancelAtCompletion{Context: context.Background()} + objects := []blueprint.ManagedMountObject{managedObjectTestDeclaration(".", true, nil), managedObjectTestDeclaration("new", false, nil)} + result, err := prepareManagedMountObjectsV1(ctx, fd, stagingFD, objects, map[string]int{".": fd}) + if !errors.Is(err, context.Canceled) || result != nil { + t.Fatalf("completion cancellation returned success: %v %v", result, err) + } + info, err := os.Stat(filepath.Join(root, "new")) + if err != nil || !info.Mode().IsRegular() || info.Size() != 0 { + t.Fatalf("partial inert object was removed or changed: %v %v", info, err) + } + if _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); err != nil { + t.Fatalf("borrowed root closed: %v", err) + } +} + +func TestManagedMountObjectsExclusiveCreationCannotAdoptExistingLeaf(t *testing.T) { + stagingFD := managedObjectTestStagingFD(t) + root := t.TempDir() + fd := managedObjectTestFD(t, root) + for _, directory := range []bool{false, true} { + name := "file" + if directory { + name = "dir" + if err := os.Mkdir(filepath.Join(root, name), 0o700); err != nil { + t.Fatal(err) + } + } else if err := os.WriteFile(filepath.Join(root, name), []byte("retained"), 0o600); err != nil { + t.Fatal(err) + } + identity, err := managedMountObjectIdentityForFDV1(fd) + if err != nil { + t.Fatal(err) + } + for _, parent := range []int{fd, -1} { + _, err := createManagedMountObjectV1(parent, stagingFD, name, identity.Mount, managedObjectTestDeclaration(name, directory, managedObjectTestMode(0o777))) + if parent == fd && !errors.Is(err, unix.EEXIST) || parent == -1 && !errors.Is(err, unix.EBADF) { + t.Fatalf("exclusive creation %s parent %d: %v", name, parent, err) + } + } + } + data, err := os.ReadFile(filepath.Join(root, "file")) + if err != nil || string(data) != "retained" { + t.Fatalf("exclusive creation changed existing file: %q %v", data, err) + } +} + +func TestManagedMountObjectsStagingAuthorityRequiredBeforeEffects(t *testing.T) { + root, permissive := t.TempDir(), t.TempDir() + if err := os.Chmod(permissive, 0o755); err != nil { + t.Fatal(err) + } + rootFD := managedObjectTestFD(t, root) + objects := []blueprint.ManagedMountObject{managedObjectTestDeclaration(".", true, nil), managedObjectTestDeclaration("new", true, nil)} + for _, staging := range []int{-1, rootFD, managedObjectTestFD(t, permissive), managedObjectTestFD(t, "/proc")} { + result, err := prepareManagedMountObjectsV1(context.Background(), rootFD, staging, objects, map[string]int{".": rootFD}) + if err == nil || result != nil { + t.Fatalf("invalid staging %d accepted: %v %v", staging, result, err) + } + } + entries, err := os.ReadDir(root) + if err != nil || len(entries) != 0 { + t.Fatalf("invalid staging had effects: %v %v", entries, err) + } +} + +func TestManagedMountObjectsDirectoryStagingPreservesInheritedPolicy(t *testing.T) { + root, staging := t.TempDir(), t.TempDir() + if err := os.Chmod(staging, 0o700); err != nil { + t.Fatal(err) + } + // A minimal default ACL; the kernel, not the preparer, interprets it. + acl := make([]byte, 4+3*8) + binary.LittleEndian.PutUint32(acl, 2) + for i, entry := range [][2]uint16{{1, 7}, {4, 5}, {32, 1}} { + binary.LittleEndian.PutUint16(acl[4+i*8:], entry[0]) + binary.LittleEndian.PutUint16(acl[6+i*8:], entry[1]) + binary.LittleEndian.PutUint32(acl[8+i*8:], ^uint32(0)) + } + if err := unix.Setxattr(root, "system.posix_acl_default", acl, 0); err != nil { + if errors.Is(err, unix.EOPNOTSUPP) { + t.Skip("test filesystem does not support POSIX ACLs") + } + t.Fatal(err) + } + if err := unix.Chmod(root, 0o2700); err != nil { + t.Fatal(err) + } + control := filepath.Join(root, "control") + if err := os.Mkdir(control, 0o755); err != nil { + t.Fatal(err) + } + rootFD, stagingFD := managedObjectTestFD(t, root), managedObjectTestFD(t, staging) + objects := []blueprint.ManagedMountObject{managedObjectTestDeclaration(".", true, nil), managedObjectTestDeclaration("staged", true, nil)} + result, err := prepareManagedMountObjectsV1(context.Background(), rootFD, stagingFD, objects, map[string]int{".": rootFD}) + if err != nil { + t.Fatal(err) + } + defer closePreparedManagedObjects(t, result) + var expected unix.Stat_t + if err := unix.Stat(control, &expected); err != nil { + t.Fatal(err) + } + if got := result["staged"].Identity.Mode & 0o7777; got != expected.Mode&0o7777 { + t.Fatalf("staging changed inherited permissions: got %#o, want %#o", got, expected.Mode&0o7777) + } + got := make([]byte, len(acl)) + n, err := unix.Getxattr(filepath.Join(root, "staged"), "system.posix_acl_default", got) + if err != nil || n != len(acl) || !reflect.DeepEqual(got, acl) { + t.Fatalf("staging lost the inherited default ACL: %v %v", got, err) + } + // Reusing staging for a parent without a default ACL must not retain it. + plain := t.TempDir() + plainFD := managedObjectTestFD(t, plain) + result2, err := prepareManagedMountObjectsV1(context.Background(), plainFD, stagingFD, objects, map[string]int{".": plainFD}) + if err != nil { + t.Fatal(err) + } + defer closePreparedManagedObjects(t, result2) + if _, err := unix.Getxattr(filepath.Join(plain, "staged"), "system.posix_acl_default", got); !errors.Is(err, unix.ENODATA) { + t.Fatalf("staging leaked the previous parent's default ACL: %v", err) + } +} diff --git a/internal/dockerdeploy/managed_mount_objects_race_linux_test.go b/internal/dockerdeploy/managed_mount_objects_race_linux_test.go new file mode 100644 index 00000000..56ddfea1 --- /dev/null +++ b/internal/dockerdeploy/managed_mount_objects_race_linux_test.go @@ -0,0 +1,251 @@ +//go:build linux + +package dockerdeploy + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/omry/reploy/internal/blueprint" + "golang.org/x/sys/unix" +) + +func TestManagedMountObjectsPinnedRootSurvivesNameSubstitution(t *testing.T) { + stagingFD := managedObjectTestStagingFD(t) + base := t.TempDir() + root, held, foreign := filepath.Join(base, "root"), filepath.Join(base, "held"), filepath.Join(base, "foreign") + for _, name := range []string{root, foreign} { + if err := os.Mkdir(name, 0o700); err != nil { + t.Fatal(err) + } + } + fd := managedObjectTestFD(t, root) + if err := os.Rename(root, held); err != nil { + t.Fatal(err) + } + if err := os.Symlink(foreign, root); err != nil { + t.Fatal(err) + } + objects := []blueprint.ManagedMountObject{managedObjectTestDeclaration(".", true, nil), managedObjectTestDeclaration("new", false, managedObjectTestMode(0o640))} + result, err := prepareManagedMountObjectsV1(context.Background(), fd, stagingFD, objects, map[string]int{".": fd}) + if err != nil { + t.Fatal(err) + } + closePreparedManagedObjects(t, result) + if _, err := os.Stat(filepath.Join(held, "new")); err != nil { + t.Fatalf("held root not used: %v", err) + } + entries, err := os.ReadDir(foreign) + if err != nil || len(entries) != 0 { + t.Fatalf("foreign replacement was changed: %v %v", entries, err) + } +} + +func TestManagedMountObjectsPinnedLeafSurvivesNameSubstitution(t *testing.T) { + stagingFD := managedObjectTestStagingFD(t) + root, foreign := t.TempDir(), t.TempDir() + leaf, canary := filepath.Join(root, "file"), filepath.Join(foreign, "canary") + if err := os.WriteFile(canary, []byte("foreign"), 0o600); err != nil { + t.Fatal(err) + } + fd := managedObjectTestFD(t, root) + objects := []blueprint.ManagedMountObject{managedObjectTestDeclaration(".", true, nil), managedObjectTestDeclaration("file", false, nil)} + result, err := prepareManagedMountObjectsV1(context.Background(), fd, stagingFD, objects, map[string]int{".": fd}) + if err != nil { + t.Fatal(err) + } + defer closePreparedManagedObjects(t, result) + if err := os.Rename(leaf, filepath.Join(root, "retained")); err != nil { + t.Fatal(err) + } + if err := os.Symlink(canary, leaf); err != nil { + t.Fatal(err) + } + // The returned FD is the created inode, not a later lookup of its name. + if _, err := unix.Write(result["file"].FD, []byte("held inode")); err != nil { + t.Fatal(err) + } + data, err := os.ReadFile(canary) + if err != nil || string(data) != "foreign" { + t.Fatalf("foreign target changed: %q %v", data, err) + } + data, err = os.ReadFile(filepath.Join(root, "retained")) + if err != nil || string(data) != "held inode" { + t.Fatalf("returned descriptor lost its inode: %q %v", data, err) + } + recorded := map[string]int{".": fd, "file": result["file"].FD} + if retry, err := prepareManagedMountObjectsV1(context.Background(), fd, stagingFD, objects, recorded); err == nil || retry != nil { + t.Fatalf("later preparation accepted substituted leaf: %v %v", retry, err) + } +} + +func TestManagedMountObjectsParentReplacementRaceCannotWriteForeignTree(t *testing.T) { + stagingFD := managedObjectTestStagingFD(t) + root, foreign := t.TempDir(), t.TempDir() + parent, held := filepath.Join(root, "parent"), filepath.Join(root, "held") + if err := os.Mkdir(parent, 0o700); err != nil { + t.Fatal(err) + } + canary := filepath.Join(foreign, "canary") + if err := os.WriteFile(canary, []byte("unchanged"), 0o600); err != nil { + t.Fatal(err) + } + rootFD, parentFD := managedObjectTestFD(t, root), managedObjectTestFD(t, parent) + recorded := map[string]int{".": rootFD, "parent": parentFD} + ready, stop, finished := make(chan struct{}), make(chan struct{}), make(chan struct{}) + var once sync.Once + go func() { + defer close(finished) + defer once.Do(func() { close(ready) }) + for { + select { + case <-stop: + return + default: + } + if err := os.Rename(parent, held); err != nil { + t.Error(err) + return + } + if err := os.Symlink(foreign, parent); err != nil { + t.Error(err) + return + } + once.Do(func() { close(ready) }) + if err := os.Remove(parent); err != nil { + t.Error(err) + return + } + if err := os.Rename(held, parent); err != nil { + t.Error(err) + return + } + } + }() + <-ready + for i := 0; i < 100; i++ { + objects := []blueprint.ManagedMountObject{managedObjectTestDeclaration(".", true, nil), managedObjectTestDeclaration(fmt.Sprintf("parent/file-%03d", i), false, managedObjectTestMode(0o600))} + result, err := prepareManagedMountObjectsV1(context.Background(), rootFD, stagingFD, objects, recorded) + if err == nil { + closePreparedManagedObjects(t, result) + } else if result != nil { + t.Error("failure returned partial success") + } + } + close(stop) + <-finished + entries, err := os.ReadDir(foreign) + if err != nil || len(entries) != 1 || entries[0].Name() != "canary" { + t.Fatalf("preparation escaped to foreign tree: %v %v", entries, err) + } + data, err := os.ReadFile(canary) + if err != nil || string(data) != "unchanged" { + t.Fatalf("foreign canary changed: %q %v", data, err) + } +} + +func TestManagedMountObjectsCreationHonorsUmask(t *testing.T) { + stagingFD := managedObjectTestStagingFD(t) + if os.Getenv("REPLOY_TEST_MANAGED_OBJECT_UMASK") != "1" { + command := exec.Command(os.Args[0], "-test.run=^TestManagedMountObjectsCreationHonorsUmask$") + command.Env = append(os.Environ(), "REPLOY_TEST_MANAGED_OBJECT_UMASK=1") + if output, err := command.CombinedOutput(); err != nil { + t.Fatalf("isolated umask proof: %v\n%s", err, output) + } + return + } + // Isolate process-wide umask from all other tests and the invoking agent. + unix.Umask(0o077) + root := t.TempDir() + fd := managedObjectTestFD(t, root) + objects := []blueprint.ManagedMountObject{ + managedObjectTestDeclaration(".", true, nil), managedObjectTestDeclaration("dir", true, nil), + managedObjectTestDeclaration("dir/file", false, nil), managedObjectTestDeclaration("explicit", false, managedObjectTestMode(0o640)), + } + result, err := prepareManagedMountObjectsV1(context.Background(), fd, stagingFD, objects, map[string]int{".": fd}) + if err != nil { + t.Fatal(err) + } + defer closePreparedManagedObjects(t, result) + for name, mode := range map[string]uint32{"dir": 0o700, "dir/file": 0o600, "explicit": 0o640} { + if got := result[name].Identity.Mode & 0o7777; got != mode { + t.Errorf("%s: mode %#o, want %#o", name, got, mode) + } + } +} + +func TestManagedMountObjectsDirectoryReplacementCannotChangeForeignMetadata(t *testing.T) { + old := runtime.GOMAXPROCS(2) + defer runtime.GOMAXPROCS(old) + root, outside, staging := t.TempDir(), t.TempDir(), t.TempDir() + if err := os.Chmod(staging, 0o700); err != nil { + t.Fatal(err) + } + foreign := filepath.Join(outside, "foreign") + if err := os.Mkdir(foreign, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(foreign, "canary"), []byte("retained"), 0o600); err != nil { + t.Fatal(err) + } + rfd, ofd := managedObjectTestFD(t, root), managedObjectTestFD(t, outside) + ffd, sfd := managedObjectTestFD(t, foreign), managedObjectTestFD(t, staging) + deadline := time.Now().Add(5 * time.Second) + exchanges := 0 + for i := 0; i < 2000 && time.Now().Before(deadline); i++ { + name := fmt.Sprintf("new-%d", i) + var stop atomic.Bool + ready, done := make(chan struct{}), make(chan error, 1) + go func() { + close(ready) + for !stop.Load() { + err := unix.Renameat2(rfd, name, ofd, "foreign", unix.RENAME_EXCHANGE) + if err == nil || err != unix.ENOENT { + done <- err + return + } + runtime.Gosched() + } + done <- unix.ENOENT + }() + <-ready + objects := []blueprint.ManagedMountObject{managedObjectTestDeclaration(".", true, nil), managedObjectTestDeclaration(name, true, managedObjectTestMode(0o711))} + result, prepareErr := prepareManagedMountObjectsV1(context.Background(), rfd, sfd, objects, map[string]int{".": rfd}) + stop.Store(true) + exchangeErr := <-done + closePreparedManagedObjects(t, result) + var actual unix.Stat_t + if err := unix.Fstat(ffd, &actual); err != nil { + t.Fatal(err) + } + if exchangeErr == nil { + exchanges++ + if err := unix.Renameat2(rfd, name, ofd, "foreign", unix.RENAME_EXCHANGE); err != nil { + t.Fatal(err) + } + } else if exchangeErr != unix.ENOENT { + t.Fatal(exchangeErr) + } + if actual.Mode&0o7777 != 0o700 { + t.Fatalf("foreign metadata changed at iteration %d: mode %#o, preparation error %v", i, actual.Mode&0o7777, prepareErr) + } + if err := unix.Unlinkat(rfd, name, unix.AT_REMOVEDIR); err != nil && err != unix.ENOENT { + t.Fatal(err) + } + } + if exchanges == 0 { + t.Fatal("replacement control never exchanged a published directory") + } + data, err := os.ReadFile(filepath.Join(foreign, "canary")) + if err != nil || string(data) != "retained" { + t.Fatalf("foreign contents changed: %q %v", data, err) + } +}