diff --git a/internal/pkg/dir_test.go b/internal/pkg/dir_test.go index 3038526..c6cf1c4 100644 --- a/internal/pkg/dir_test.go +++ b/internal/pkg/dir_test.go @@ -278,7 +278,7 @@ func TestFlatten(t *testing.T) { "checksum": {Mode: fs.ModeDir | 0700}, "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9": {Mode: fs.ModeDir | 0500}, - "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9/check": {Mode: 0400, Data: []byte{0x0}}, + "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9/check": {Mode: 0400, Data: []byte{0}}, "checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU": {Mode: fs.ModeDir | 0500}, "checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb": {Mode: 0400, Data: []byte{}}, @@ -294,7 +294,7 @@ func TestFlatten(t *testing.T) { {Mode: fs.ModeDir | 0700, Path: "checksum"}, {Mode: fs.ModeDir | 0500, Path: "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9"}, - {Mode: 0400, Path: "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9/check", Data: []byte{0x0}}, + {Mode: 0400, Path: "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9/check", Data: []byte{0}}, {Mode: fs.ModeDir | 0500, Path: "checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU"}, {Mode: 0400, Path: "checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb", Data: []byte{}}, @@ -350,6 +350,36 @@ func TestFlatten(t *testing.T) { {Mode: fs.ModeDir | 0700, Path: "temp"}, {Mode: fs.ModeDir | 0700, Path: "work"}, }, pkg.MustDecode("bBQVFIt0FnOulljgpLnGtuzHSFgwiCMjc4pmc4rHRqXKQ60Q5aBVYp5f6aH9VdZi")}, + + {"sample exec container overlay root", fstest.MapFS{ + ".": {Mode: fs.ModeDir | 0700}, + + "checksum": {Mode: fs.ModeDir | 0700}, + "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9": {Mode: fs.ModeDir | 0500}, + "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9/check": {Mode: 0400, Data: []byte{0}}, + "checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU": {Mode: fs.ModeDir | 0500}, + + "identifier": {Mode: fs.ModeDir | 0700}, + "identifier/cIjP14zs5el6W_BQhufL_c0vWg-V6Z6pDpsbEa3sYtZ1381u1bKnH3N16RIrw-1S": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9")}, + "identifier/nfeISfLeFDr1k-g3hpE1oZ440kTqDdfF8TDpoLdbTPqaMMIl95oiqcvqjRkMjubA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, + + "temp": {Mode: fs.ModeDir | 0700}, + "work": {Mode: fs.ModeDir | 0700}, + }, []pkg.FlatEntry{ + {Mode: fs.ModeDir | 0700, Path: "."}, + + {Mode: fs.ModeDir | 0700, Path: "checksum"}, + {Mode: fs.ModeDir | 0500, Path: "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9"}, + {Mode: 0400, Path: "checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9/check", Data: []byte{0}}, + {Mode: fs.ModeDir | 0500, Path: "checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU"}, + + {Mode: fs.ModeDir | 0700, Path: "identifier"}, + {Mode: fs.ModeSymlink | 0777, Path: "identifier/cIjP14zs5el6W_BQhufL_c0vWg-V6Z6pDpsbEa3sYtZ1381u1bKnH3N16RIrw-1S", Data: []byte("../checksum/GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9")}, + {Mode: fs.ModeSymlink | 0777, Path: "identifier/nfeISfLeFDr1k-g3hpE1oZ440kTqDdfF8TDpoLdbTPqaMMIl95oiqcvqjRkMjubA", Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, + + {Mode: fs.ModeDir | 0700, Path: "temp"}, + {Mode: fs.ModeDir | 0700, Path: "work"}, + }, pkg.MustDecode("gFT9kprYBqEJKifJIl2sHn_3TgULWVLTU4DrYAHiGcRmcdFRZ0YtjiROW820cAEc")}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/internal/pkg/exec.go b/internal/pkg/exec.go index 930624f..c1e0cc3 100644 --- a/internal/pkg/exec.go +++ b/internal/pkg/exec.go @@ -91,6 +91,13 @@ func (a *execNetArtifact) Cure(c *CureContext) error { // The working and temporary directories are both created and mounted writable // on /work and /tmp respectively. // +// If the first path targets [fhs.AbsRoot], it is made writable via an overlay +// mount with writes going to an ephemeral tmpfs bound to the lifetime of the +// container. This is primarily to make it possible for [container] to set up +// mount points targeting paths not available in the [Artifact] backing root, +// and to accommodate poorly written programs that insist on writing to awkward +// paths, it must not be used as scratch space. +// // If checksum is non-nil, the resulting [Artifact] implements [KnownChecksum] // and its container runs in the host net namespace. // @@ -262,6 +269,10 @@ func (a *execArtifact) cure(c *CureContext, hostNet bool) (err error) { z.Dir, z.Env, z.Path, z.Args = a.dir, a.env, a.path, a.args z.Grow(len(paths) + 4) + if len(paths) > 0 && paths[0][1].Is(fhs.AbsRoot) { + z.OverlayEphemeral(fhs.AbsRoot, paths[0][0]) + paths = paths[1:] + } for _, b := range paths { z.Bind(b[0], b[1], 0) } diff --git a/internal/pkg/exec_test.go b/internal/pkg/exec_test.go index 5a14f78..76bb756 100644 --- a/internal/pkg/exec_test.go +++ b/internal/pkg/exec_test.go @@ -26,6 +26,10 @@ var testtoolBin []byte func TestExec(t *testing.T) { t.Parallel() + wantChecksumOffline := pkg.MustDecode( + "GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9", + ) + checkWithCache(t, []cacheTestCase{ {"offline", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { c.SetStrict(true) @@ -59,9 +63,7 @@ func TestExec(t *testing.T) { }, }), pkg.MustPath("/opt", testtool), - ), ignorePathname, pkg.MustDecode( - "GPa4aBakdSJd7Tz7LYj_VJFoojzyZinmVcG3k6M5xI6CZ821J5sXLhLDDuS47gi9", - ), nil}, + ), ignorePathname, wantChecksumOffline, nil}, {"error passthrough", pkg.NewExec( t.Context(), @@ -155,6 +157,38 @@ func TestExec(t *testing.T) { testtoolDestroy(t, base, c) }, pkg.MustDecode("bBQVFIt0FnOulljgpLnGtuzHSFgwiCMjc4pmc4rHRqXKQ60Q5aBVYp5f6aH9VdZi")}, + + {"overlay root", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { + c.SetStrict(true) + testtool, testtoolDestroy := newTesttool() + + msg := message.New(log.New(os.Stderr, "container: ", 0)) + msg.SwapVerbose(testing.Verbose()) + + cureMany(t, c, []cureStep{ + {"container", pkg.NewExec( + t.Context(), + msg, + 0, + nil, + check.MustAbs("/work"), + []string{"HAKUREI_TEST=1", "HAKUREI_ROOT=1"}, + check.MustAbs("/opt/bin/testtool"), + []string{"testtool"}, + + pkg.MustPath("/", stubArtifact{ + kind: pkg.KindTar, + params: []byte("empty directory"), + cure: func(c *pkg.CureContext) error { + return os.MkdirAll(c.GetWorkDir().String(), 0700) + }, + }), + pkg.MustPath("/opt", testtool), + ), ignorePathname, wantChecksumOffline, nil}, + }) + + testtoolDestroy(t, base, c) + }, pkg.MustDecode("gFT9kprYBqEJKifJIl2sHn_3TgULWVLTU4DrYAHiGcRmcdFRZ0YtjiROW820cAEc")}, }) } diff --git a/internal/pkg/testdata/main.go b/internal/pkg/testdata/main.go index 257d748..bbf2340 100644 --- a/internal/pkg/testdata/main.go +++ b/internal/pkg/testdata/main.go @@ -9,6 +9,7 @@ import ( "os" "path" "slices" + "strings" "syscall" "hakurei.app/container/fhs" @@ -28,9 +29,14 @@ func main() { if !slices.Equal(os.Args, wantArgs) { log.Fatalf("Args: %q, want %q", os.Args, wantArgs) } - if wantEnv := []string{ - "HAKUREI_TEST=1", - }; !slices.Equal(wantEnv, os.Environ()) { + + var overlayRoot bool + wantEnv := []string{"HAKUREI_TEST=1"} + if len(os.Environ()) == 2 { + overlayRoot = true + wantEnv = []string{"HAKUREI_TEST=1", "HAKUREI_ROOT=1"} + } + if !slices.Equal(wantEnv, os.Environ()) { log.Fatalf("Environ: %q, want %q", os.Environ(), wantEnv) } const wantExec = "/opt/bin/testtool" @@ -51,11 +57,6 @@ func main() { log.Fatalf("Hostname: %q, want %q", hostname, wantHostname) } - ident := "oHuqV7p0v1Vd8IdAzjyYM8sfCS0P2LR5tfv5cb6Gbf2ZWUm8Ec-7hYPJ_qr183m7" - if hostNet { - ident = "I3T53NtN6HPAyyodHtq2B0clcsoS1nPdvCEb-Zc5K-hoqFGL2od1mftHhwG7gX1S" - } - var m *vfs.MountInfo if f, err := os.Open(fhs.Proc + "self/mountinfo"); err != nil { log.Fatalf("Open: error = %v", err) @@ -82,20 +83,47 @@ func main() { } } + const checksumEmptyDir = "MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU" + ident := "oHuqV7p0v1Vd8IdAzjyYM8sfCS0P2LR5tfv5cb6Gbf2ZWUm8Ec-7hYPJ_qr183m7" log.Println(m) - if m.Root != "/sysroot" || m.Target != "/" { - log.Fatal("unexpected root mount entry") - } next := func() { m = m.Next; log.Println(m) } - next() - if path.Base(m.Root) != "OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb" { - log.Fatal("unexpected file artifact checksum") - } + if overlayRoot { + ident = "cIjP14zs5el6W_BQhufL_c0vWg-V6Z6pDpsbEa3sYtZ1381u1bKnH3N16RIrw-1S" - next() - if path.Base(m.Root) != "MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU" { - log.Fatal("unexpected artifact checksum") + if m.Root != "/" || m.Target != "/" || + m.Source != "overlay" || m.FsType != "overlay" { + log.Fatal("unexpected root mount entry") + } + var lowerdir string + for _, o := range strings.Split(m.FsOptstr, ",") { + const lowerdirKey = "lowerdir=" + if strings.HasPrefix(o, lowerdirKey) { + lowerdir = o[len(lowerdirKey):] + } + } + if path.Base(lowerdir) != checksumEmptyDir { + log.Fatal("unexpected artifact checksum") + } + + } else { + if hostNet { + ident = "I3T53NtN6HPAyyodHtq2B0clcsoS1nPdvCEb-Zc5K-hoqFGL2od1mftHhwG7gX1S" + } + + if m.Root != "/sysroot" || m.Target != "/" { + log.Fatal("unexpected root mount entry") + } + + next() + if path.Base(m.Root) != "OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb" { + log.Fatal("unexpected file artifact checksum") + } + + next() + if path.Base(m.Root) != checksumEmptyDir { + log.Fatal("unexpected artifact checksum") + } } next() // testtool artifact