diff options
| author | Ophestra <cat@gensokyo.uk> | 2025-08-11 02:52:32 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2025-08-11 04:56:42 +0900 |
| commit | e99d7affb0c128fa82722f9b3d79c99b5e5918cd (patch) | |
| tree | 387e08d6c4363d8b9e0462f069c140fbdb15c917 /container | |
| parent | 41ac2be9658b49c68cb793f3a6f0c5932d82e1c2 (diff) | |
container: use absolute for pathname
This is simultaneously more efficient and less error-prone. This change caused minor API changes in multiple other packages.
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'container')
| -rw-r--r-- | container/autoetc.go | 12 | ||||
| -rw-r--r-- | container/autoroot.go | 18 | ||||
| -rw-r--r-- | container/container.go | 21 | ||||
| -rw-r--r-- | container/container_test.go | 79 | ||||
| -rw-r--r-- | container/init.go | 4 | ||||
| -rw-r--r-- | container/init_test.go | 12 | ||||
| -rw-r--r-- | container/ops.go | 266 | ||||
| -rw-r--r-- | container/path.go | 34 |
8 files changed, 245 insertions, 201 deletions
diff --git a/container/autoetc.go b/container/autoetc.go index 3e23c160..56879125 100644 --- a/container/autoetc.go +++ b/container/autoetc.go @@ -10,9 +10,9 @@ func init() { gob.Register(new(AutoEtcOp)) } // Etc appends an [Op] that expands host /etc into a toplevel symlink mirror with /etc semantics. // This is not a generic setup op. It is implemented here to reduce ipc overhead. -func (f *Ops) Etc(host, prefix string) *Ops { +func (f *Ops) Etc(host *Absolute, prefix string) *Ops { e := &AutoEtcOp{prefix} - f.Mkdir(FHSEtc, 0755) + f.Mkdir(AbsFHSEtc, 0755) f.Bind(host, e.hostPath(), 0) *f = append(*f, e) return f @@ -28,7 +28,7 @@ func (e *AutoEtcOp) apply(*Params) error { if err := os.MkdirAll(target, 0755); err != nil { return wrapErrSelf(err) } - if d, err := os.ReadDir(toSysroot(e.hostPath())); err != nil { + if d, err := os.ReadDir(toSysroot(e.hostPath().String())); err != nil { return wrapErrSelf(err) } else { for _, ent := range d { @@ -54,8 +54,10 @@ func (e *AutoEtcOp) apply(*Params) error { return nil } -func (e *AutoEtcOp) hostPath() string { return FHSEtc + e.hostRel() } -func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix } + +// bypasses abs check, use with caution! +func (e *AutoEtcOp) hostPath() *Absolute { return &Absolute{FHSEtc + e.hostRel()} } +func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix } func (e *AutoEtcOp) Is(op Op) bool { ve, ok := op.(*AutoEtcOp) diff --git a/container/autoroot.go b/container/autoroot.go index 5ed9b6c7..70b504eb 100644 --- a/container/autoroot.go +++ b/container/autoroot.go @@ -4,21 +4,21 @@ import ( "encoding/gob" "fmt" "os" - "path" - . "syscall" + "syscall" ) func init() { gob.Register(new(AutoRootOp)) } // Root appends an [Op] that expands a directory into a toplevel bind mount mirror on container root. // This is not a generic setup op. It is implemented here to reduce ipc overhead. -func (f *Ops) Root(host, prefix string, flags int) *Ops { +func (f *Ops) Root(host *Absolute, prefix string, flags int) *Ops { *f = append(*f, &AutoRootOp{host, prefix, flags, nil}) return f } type AutoRootOp struct { - Host, Prefix string + Host *Absolute + Prefix string // passed through to bindMount Flags int @@ -29,11 +29,11 @@ type AutoRootOp struct { } func (r *AutoRootOp) early(params *Params) error { - if !path.IsAbs(r.Host) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", r.Host)) + if r.Host == nil { + return syscall.EBADE } - if d, err := os.ReadDir(r.Host); err != nil { + if d, err := os.ReadDir(r.Host.String()); err != nil { return wrapErrSelf(err) } else { r.resolved = make([]Op, 0, len(d)) @@ -41,8 +41,8 @@ func (r *AutoRootOp) early(params *Params) error { name := ent.Name() if IsAutoRootBindable(name) { op := &BindMountOp{ - Source: path.Join(r.Host, name), - Target: FHSRoot + name, + Source: r.Host.Append(name), + Target: AbsFHSRoot.Append(name), Flags: r.Flags, } if err = op.early(params); err != nil { diff --git a/container/container.go b/container/container.go index 54cee25f..3d6caa66 100644 --- a/container/container.go +++ b/container/container.go @@ -9,7 +9,6 @@ import ( "io" "os" "os/exec" - "path" "strconv" . "syscall" "time" @@ -53,11 +52,11 @@ type ( // Params holds container configuration and is safe to serialise. Params struct { // Working directory in the container. - Dir string + Dir *Absolute // Initial process environment. Env []string - // Absolute path of initial process in the container. Overrides name. - Path string + // Pathname of initial process in the container. + Path *Absolute // Initial process argv. Args []string // Deliver SIGINT to the initial process on context cancellation. @@ -188,14 +187,16 @@ func (p *Container) Serve() error { setup := p.setup p.setup = nil - if !path.IsAbs(p.Path) { + if p.Path == nil { p.cancel() - return msg.WrapErr(EINVAL, - fmt.Sprintf("invalid executable path %q", p.Path)) + return msg.WrapErr(EINVAL, "invalid executable pathname") } + // do not transmit nil + if p.Dir == nil { + p.Dir = AbsFHSRoot + } if p.SeccompRules == nil { - // do not transmit nil p.SeccompRules = make([]seccomp.NativeRule, 0) } @@ -232,11 +233,11 @@ func (p *Container) ProcessState() *os.ProcessState { // New returns the address to a new instance of [Container] that requires further initialisation before use. func New(ctx context.Context) *Container { - return &Container{ctx: ctx, Params: Params{Dir: FHSRoot, Ops: new(Ops)}} + return &Container{ctx: ctx, Params: Params{Ops: new(Ops)}} } // NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields. -func NewCommand(ctx context.Context, pathname, name string, args ...string) *Container { +func NewCommand(ctx context.Context, pathname *Absolute, name string, args ...string) *Container { z := New(ctx) z.Path = pathname z.Args = append([]string{name}, args...) diff --git a/container/container_test.go b/container/container_test.go index 10c20659..8016aba6 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -10,7 +10,6 @@ import ( "os" "os/exec" "os/signal" - "path" "strconv" "strings" "syscall" @@ -77,7 +76,7 @@ var containerTestCases = []struct { {"tmpfs", true, false, false, true, earlyOps(new(container.Ops). - Tmpfs(hst.Tmp, 0, 0755), + Tmpfs(hst.AbsTmp, 0, 0755), ), earlyMnt( ent("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore), @@ -86,7 +85,7 @@ var containerTestCases = []struct { {"dev", true, true /* go test output is not a tty */, false, false, earlyOps(new(container.Ops). - Dev("/dev", true), + Dev(container.MustAbs("/dev"), true), ), earlyMnt( ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore), @@ -103,7 +102,7 @@ var containerTestCases = []struct { {"dev no mqueue", true, true /* go test output is not a tty */, false, false, earlyOps(new(container.Ops). - Dev("/dev", false), + Dev(container.MustAbs("/dev"), false), ), earlyMnt( ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore), @@ -119,20 +118,20 @@ var containerTestCases = []struct { {"overlay", true, false, false, true, func(t *testing.T) (*container.Ops, context.Context) { - tempDir := t.TempDir() + tempDir := container.MustAbs(t.TempDir()) lower0, lower1, upper, work := - path.Join(tempDir, "lower0"), - path.Join(tempDir, "lower1"), - path.Join(tempDir, "upper"), - path.Join(tempDir, "work") - for _, name := range []string{lower0, lower1, upper, work} { - if err := os.Mkdir(name, 0755); err != nil { + tempDir.Append("lower0"), + tempDir.Append("lower1"), + tempDir.Append("upper"), + tempDir.Append("work") + for _, a := range []*container.Absolute{lower0, lower1, upper, work} { + if err := os.Mkdir(a.String(), 0755); err != nil { t.Fatalf("Mkdir: error = %v", err) } } return new(container.Ops). - Overlay(hst.Tmp, upper, work, lower0, lower1), + Overlay(hst.AbsTmp, upper, work, lower0, lower1), context.WithValue(context.WithValue(context.WithValue(context.WithValue(t.Context(), testVal("lower1"), lower1), testVal("lower0"), lower0), @@ -143,12 +142,12 @@ var containerTestCases = []struct { return []*vfs.MountInfoEntry{ ent("/", hst.Tmp, "rw", "overlay", "overlay", "rw,lowerdir="+ - container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(string))+":"+ - container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(string))+ + container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+ + container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+ ",upperdir="+ - container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(string))+ + container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*container.Absolute).String())+ ",workdir="+ - container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(string))+ + container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(*container.Absolute).String())+ ",redirect_dir=nofollow,uuid=on,userxattr"), } }, @@ -156,18 +155,18 @@ var containerTestCases = []struct { {"overlay ephemeral", true, false, false, true, func(t *testing.T) (*container.Ops, context.Context) { - tempDir := t.TempDir() + tempDir := container.MustAbs(t.TempDir()) lower0, lower1 := - path.Join(tempDir, "lower0"), - path.Join(tempDir, "lower1") - for _, name := range []string{lower0, lower1} { - if err := os.Mkdir(name, 0755); err != nil { + tempDir.Append("lower0"), + tempDir.Append("lower1") + for _, a := range []*container.Absolute{lower0, lower1} { + if err := os.Mkdir(a.String(), 0755); err != nil { t.Fatalf("Mkdir: error = %v", err) } } return new(container.Ops). - OverlayEphemeral(hst.Tmp, lower0, lower1), + OverlayEphemeral(hst.AbsTmp, lower0, lower1), t.Context() }, func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry { @@ -180,17 +179,17 @@ var containerTestCases = []struct { {"overlay readonly", true, false, false, true, func(t *testing.T) (*container.Ops, context.Context) { - tempDir := t.TempDir() + tempDir := container.MustAbs(t.TempDir()) lower0, lower1 := - path.Join(tempDir, "lower0"), - path.Join(tempDir, "lower1") - for _, name := range []string{lower0, lower1} { - if err := os.Mkdir(name, 0755); err != nil { + tempDir.Append("lower0"), + tempDir.Append("lower1") + for _, a := range []*container.Absolute{lower0, lower1} { + if err := os.Mkdir(a.String(), 0755); err != nil { t.Fatalf("Mkdir: error = %v", err) } } return new(container.Ops). - OverlayReadonly(hst.Tmp, lower0, lower1), + OverlayReadonly(hst.AbsTmp, lower0, lower1), context.WithValue(context.WithValue(t.Context(), testVal("lower1"), lower1), testVal("lower0"), lower0) @@ -199,8 +198,8 @@ var containerTestCases = []struct { return []*vfs.MountInfoEntry{ ent("/", hst.Tmp, "rw", "overlay", "overlay", "ro,lowerdir="+ - container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(string))+":"+ - container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(string))+ + container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+ + container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+ ",redirect_dir=nofollow,userxattr"), } }, @@ -252,7 +251,7 @@ func TestContainer(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout) defer cancel() - var libPaths []string + var libPaths []*container.Absolute c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i)) c.Uid = tc.uid c.Gid = tc.gid @@ -273,11 +272,11 @@ func TestContainer(t *testing.T) { c.HostNet = tc.net c. - Readonly(pathReadonly, 0755). - Tmpfs("/tmp", 0, 0755). - Place("/etc/hostname", []byte(c.Hostname)) + Readonly(container.MustAbs(pathReadonly), 0755). + Tmpfs(container.MustAbs("/tmp"), 0, 0755). + Place(container.MustAbs("/etc/hostname"), []byte(c.Hostname)) // needs /proc to check mountinfo - c.Proc("/proc") + c.Proc(container.MustAbs("/proc")) // mountinfo cannot be resolved directly by helper due to libPaths nondeterminism mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths)) @@ -286,9 +285,9 @@ func TestContainer(t *testing.T) { // Bind(os.Args[0], helperInnerPath, 0) ent(ignore, helperInnerPath, "ro,nosuid,nodev,relatime", ignore, ignore, ignore), ) - for _, name := range libPaths { + for _, a := range libPaths { // Bind(name, name, 0) - mnt = append(mnt, ent(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore)) + mnt = append(mnt, ent(ignore, a.String(), "ro,nosuid,nodev,relatime", ignore, ignore, ignore)) } mnt = append(mnt, wantMnt...) mnt = append(mnt, @@ -308,10 +307,10 @@ func TestContainer(t *testing.T) { _, _ = output.WriteTo(os.Stdout) t.Fatalf("cannot serialise expected mount points: %v", err) } - c.Place(pathWantMnt, want.Bytes()) + c.Place(container.MustAbs(pathWantMnt), want.Bytes()) if tc.ro { - c.Remount("/", syscall.MS_RDONLY) + c.Remount(container.MustAbs("/"), syscall.MS_RDONLY) } if err := c.Start(); err != nil { @@ -392,7 +391,7 @@ func testContainerCancel( } func TestContainerString(t *testing.T) { - c := container.NewCommand(t.Context(), "/run/current-system/sw/bin/ldd", "ldd", "/usr/bin/env") + c := container.NewCommand(t.Context(), container.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env") c.SeccompFlags |= seccomp.AllowMultiarch c.SeccompRules = seccomp.Preset( seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY, diff --git a/container/init.go b/container/init.go index 0ede1d7f..03ec8836 100644 --- a/container/init.go +++ b/container/init.go @@ -268,12 +268,12 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) { } Umask(oldmask) - cmd := exec.Command(params.Path) + cmd := exec.Command(params.Path.String()) cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr cmd.Args = params.Args cmd.Env = params.Env cmd.ExtraFiles = extraFiles - cmd.Dir = params.Dir + cmd.Dir = params.Dir.String() msg.Verbosef("starting initial program %s", params.Path) if err := cmd.Start(); err != nil { diff --git a/container/init_test.go b/container/init_test.go index 7716458c..9e6832d1 100644 --- a/container/init_test.go +++ b/container/init_test.go @@ -21,6 +21,10 @@ const ( helperInnerPath = "/usr/bin/helper" ) +var ( + absHelperInnerPath = container.MustAbs(helperInnerPath) +) + var helperCommands []func(c command.Command) func TestMain(m *testing.M) { @@ -46,10 +50,10 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } -func helperNewContainerLibPaths(ctx context.Context, libPaths *[]string, args ...string) (c *container.Container) { - c = container.NewCommand(ctx, helperInnerPath, "helper", args...) +func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*container.Absolute, args ...string) (c *container.Container) { + c = container.NewCommand(ctx, absHelperInnerPath, "helper", args...) c.Env = append(c.Env, envDoCheck+"=1") - c.Bind(os.Args[0], helperInnerPath, 0) + c.Bind(container.MustAbs(os.Args[0]), absHelperInnerPath, 0) // in case test has cgo enabled if entries, err := ldd.Exec(ctx, os.Args[0]); err != nil { @@ -65,5 +69,5 @@ func helperNewContainerLibPaths(ctx context.Context, libPaths *[]string, args .. } func helperNewContainer(ctx context.Context, args ...string) (c *container.Container) { - return helperNewContainerLibPaths(ctx, new([]string), args...) + return helperNewContainerLibPaths(ctx, new([]*container.Absolute), args...) } diff --git a/container/ops.go b/container/ops.go index 927c60a1..be30fcce 100644 --- a/container/ops.go +++ b/container/ops.go @@ -47,22 +47,22 @@ func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) } func init() { gob.Register(new(RemountOp)) } // Remount appends an [Op] that applies [RemountOp.Flags] on container path [RemountOp.Target]. -func (f *Ops) Remount(target string, flags uintptr) *Ops { +func (f *Ops) Remount(target *Absolute, flags uintptr) *Ops { *f = append(*f, &RemountOp{target, flags}) return f } type RemountOp struct { - Target string + Target *Absolute Flags uintptr } func (*RemountOp) early(*Params) error { return nil } func (r *RemountOp) apply(*Params) error { - if !path.IsAbs(r.Target) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", r.Target)) + if r.Target == nil { + return EBADE } - return wrapErrSuffix(hostProc.remount(toSysroot(r.Target), r.Flags), + return wrapErrSuffix(hostProc.remount(toSysroot(r.Target.String()), r.Flags), fmt.Sprintf("cannot remount %q:", r.Target)) } @@ -73,13 +73,13 @@ func (r *RemountOp) String() string { return fmt.Sprintf("%q flags %#x", r.Targe func init() { gob.Register(new(BindMountOp)) } // Bind appends an [Op] that bind mounts host path [BindMountOp.Source] on container path [BindMountOp.Target]. -func (f *Ops) Bind(source, target string, flags int) *Ops { - *f = append(*f, &BindMountOp{source, "", target, flags}) +func (f *Ops) Bind(source, target *Absolute, flags int) *Ops { + *f = append(*f, &BindMountOp{nil, source, target, flags}) return f } type BindMountOp struct { - Source, sourceFinal, Target string + sourceFinal, Source, Target *Absolute Flags int } @@ -94,24 +94,24 @@ const ( ) func (b *BindMountOp) early(*Params) error { - if !path.IsAbs(b.Source) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", b.Source)) + if b.Source == nil || b.Target == nil { + return EBADE } - if v, err := filepath.EvalSymlinks(b.Source); err != nil { + if pathname, err := filepath.EvalSymlinks(b.Source.String()); err != nil { if os.IsNotExist(err) && b.Flags&BindOptional != 0 { - b.sourceFinal = "\x00" + // leave sourceFinal as nil return nil } return wrapErrSelf(err) } else { - b.sourceFinal = v - return nil + b.sourceFinal, err = NewAbs(pathname) + return err } } func (b *BindMountOp) apply(*Params) error { - if b.sourceFinal == "\x00" { + if b.sourceFinal == nil { if b.Flags&BindOptional == 0 { // unreachable return EBADE @@ -119,12 +119,8 @@ func (b *BindMountOp) apply(*Params) error { return nil } - if !path.IsAbs(b.sourceFinal) || !path.IsAbs(b.Target) { - return msg.WrapErr(EBADE, "path is not absolute") - } - - source := toHost(b.sourceFinal) - target := toSysroot(b.Target) + source := toHost(b.sourceFinal.String()) + target := toSysroot(b.Target.String()) // this perm value emulates bwrap behaviour as it clears bits from 0755 based on // op->perms which is never set for any bind setup op so always results in 0700 @@ -161,60 +157,62 @@ func (b *BindMountOp) String() string { func init() { gob.Register(new(MountProcOp)) } // Proc appends an [Op] that mounts a private instance of proc. -func (f *Ops) Proc(dest string) *Ops { - *f = append(*f, MountProcOp(dest)) +func (f *Ops) Proc(target *Absolute) *Ops { + *f = append(*f, &MountProcOp{target}) return f } -type MountProcOp string - -func (p MountProcOp) early(*Params) error { return nil } -func (p MountProcOp) apply(params *Params) error { - v := string(p) +type MountProcOp struct { + Target *Absolute +} - if !path.IsAbs(v) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v)) +func (p *MountProcOp) early(*Params) error { return nil } +func (p *MountProcOp) apply(params *Params) error { + if p.Target == nil { + return EBADE } - - target := toSysroot(v) + target := toSysroot(p.Target.String()) if err := os.MkdirAll(target, params.ParentPerm); err != nil { return wrapErrSelf(err) } return wrapErrSuffix(Mount(SourceProc, target, FstypeProc, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString), - fmt.Sprintf("cannot mount proc on %q:", v)) + fmt.Sprintf("cannot mount proc on %q:", p.Target.String())) } -func (p MountProcOp) Is(op Op) bool { vp, ok := op.(MountProcOp); return ok && p == vp } -func (MountProcOp) prefix() string { return "mounting" } -func (p MountProcOp) String() string { return fmt.Sprintf("proc on %q", string(p)) } +func (p *MountProcOp) Is(op Op) bool { + vp, ok := op.(*MountProcOp) + return ok && ((p == nil && vp == nil) || p == vp) +} +func (*MountProcOp) prefix() string { return "mounting" } +func (p *MountProcOp) String() string { return fmt.Sprintf("proc on %q", p.Target) } func init() { gob.Register(new(MountDevOp)) } // Dev appends an [Op] that mounts a subset of host /dev. -func (f *Ops) Dev(dest string, mqueue bool) *Ops { - *f = append(*f, &MountDevOp{dest, mqueue, false}) +func (f *Ops) Dev(target *Absolute, mqueue bool) *Ops { + *f = append(*f, &MountDevOp{target, mqueue, false}) return f } // DevWritable appends an [Op] that mounts a writable subset of host /dev. // There is usually no good reason to write to /dev, so this should always be followed by a [RemountOp]. -func (f *Ops) DevWritable(dest string, mqueue bool) *Ops { - *f = append(*f, &MountDevOp{dest, mqueue, true}) +func (f *Ops) DevWritable(target *Absolute, mqueue bool) *Ops { + *f = append(*f, &MountDevOp{target, mqueue, true}) return f } type MountDevOp struct { - Target string + Target *Absolute Mqueue bool Write bool } func (d *MountDevOp) early(*Params) error { return nil } func (d *MountDevOp) apply(params *Params) error { - if !path.IsAbs(d.Target) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", d.Target)) + if d.Target == nil { + return EBADE } - target := toSysroot(d.Target) + target := toSysroot(d.Target.String()) if err := mountTmpfs(SourceTmpfsDevtmpfs, target, MS_NOSUID|MS_NODEV, 0, params.ParentPerm); err != nil { return err @@ -314,20 +312,20 @@ func (d *MountDevOp) String() string { func init() { gob.Register(new(MountTmpfsOp)) } // Tmpfs appends an [Op] that mounts tmpfs on container path [MountTmpfsOp.Path]. -func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops { - *f = append(*f, &MountTmpfsOp{SourceTmpfsEphemeral, dest, MS_NOSUID | MS_NODEV, size, perm}) +func (f *Ops) Tmpfs(target *Absolute, size int, perm os.FileMode) *Ops { + *f = append(*f, &MountTmpfsOp{SourceTmpfsEphemeral, target, MS_NOSUID | MS_NODEV, size, perm}) return f } // Readonly appends an [Op] that mounts read-only tmpfs on container path [MountTmpfsOp.Path]. -func (f *Ops) Readonly(dest string, perm os.FileMode) *Ops { - *f = append(*f, &MountTmpfsOp{SourceTmpfsReadonly, dest, MS_RDONLY | MS_NOSUID | MS_NODEV, 0, perm}) +func (f *Ops) Readonly(target *Absolute, perm os.FileMode) *Ops { + *f = append(*f, &MountTmpfsOp{SourceTmpfsReadonly, target, MS_RDONLY | MS_NOSUID | MS_NODEV, 0, perm}) return f } type MountTmpfsOp struct { FSName string - Path string + Path *Absolute Flags uintptr Size int Perm os.FileMode @@ -335,13 +333,13 @@ type MountTmpfsOp struct { func (t *MountTmpfsOp) early(*Params) error { return nil } func (t *MountTmpfsOp) apply(*Params) error { - if !path.IsAbs(t.Path) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path)) + if t.Path == nil { + return EBADE } if t.Size < 0 || t.Size > math.MaxUint>>1 { return msg.WrapErr(EBADE, fmt.Sprintf("size %d out of bounds", t.Size)) } - return mountTmpfs(t.FSName, toSysroot(t.Path), t.Flags, t.Size, t.Perm) + return mountTmpfs(t.FSName, toSysroot(t.Path.String()), t.Flags, t.Size, t.Perm) } func (t *MountTmpfsOp) Is(op Op) bool { vt, ok := op.(*MountTmpfsOp); return ok && *t == *vt } @@ -351,7 +349,7 @@ func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d func init() { gob.Register(new(MountOverlayOp)) } // Overlay appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target]. -func (f *Ops) Overlay(target, state, work string, layers ...string) *Ops { +func (f *Ops) Overlay(target, state, work *Absolute, layers ...*Absolute) *Ops { *f = append(*f, &MountOverlayOp{ Target: target, Lower: layers, @@ -363,94 +361,94 @@ func (f *Ops) Overlay(target, state, work string, layers ...string) *Ops { // OverlayEphemeral appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target] // with an ephemeral upperdir and workdir. -func (f *Ops) OverlayEphemeral(target string, layers ...string) *Ops { - return f.Overlay(target, SourceTmpfsEphemeral, zeroString, layers...) +func (f *Ops) OverlayEphemeral(target *Absolute, layers ...*Absolute) *Ops { + return f.Overlay(target, AbsFHSRoot, nil, layers...) } // OverlayReadonly appends an [Op] that mounts the overlay pseudo filesystem readonly on [MountOverlayOp.Target] -func (f *Ops) OverlayReadonly(target string, layers ...string) *Ops { - return f.Overlay(target, zeroString, zeroString, layers...) +func (f *Ops) OverlayReadonly(target *Absolute, layers ...*Absolute) *Ops { + return f.Overlay(target, nil, nil, layers...) } type MountOverlayOp struct { - Target string + Target *Absolute - // formatted for [OptionOverlayLowerdir], resolved, prefixed and escaped during early; - Lower []string - // formatted for [OptionOverlayUpperdir], resolved, prefixed and escaped during early; + // Any filesystem, does not need to be on a writable filesystem. + Lower []*Absolute + // formatted for [OptionOverlayLowerdir], resolved, prefixed and escaped during early + lower []string + // The upperdir is normally on a writable filesystem. // - // If Work is an empty string and Upper holds the special value [SourceTmpfsEphemeral], + // If Work is nil and Upper holds the special value [FHSRoot], // an ephemeral upperdir and workdir will be set up. // // If both Work and Upper are empty strings, upperdir and workdir is omitted and the overlay is mounted readonly. - Upper string - // formatted for [OptionOverlayWorkdir], resolved, prefixed and escaped during early; - Work string + Upper *Absolute + // formatted for [OptionOverlayUpperdir], resolved, prefixed and escaped during early + upper string + // The workdir needs to be an empty directory on the same filesystem as upperdir. + Work *Absolute + // formatted for [OptionOverlayWorkdir], resolved, prefixed and escaped during early + work string ephemeral bool } func (o *MountOverlayOp) early(*Params) error { - if o.Work == zeroString { - switch o.Upper { - case SourceTmpfsEphemeral: // ephemeral + if o.Work == nil && o.Upper != nil { + switch o.Upper.String() { + case FHSRoot: // ephemeral o.ephemeral = true // intermediate root not yet available - case zeroString: // readonly - default: return msg.WrapErr(EINVAL, fmt.Sprintf("upperdir has unexpected value %q", o.Upper)) } } + // readonly handled in apply if !o.ephemeral { - if o.Upper != o.Work && (o.Upper == zeroString || o.Work == zeroString) { + if o.Upper != o.Work && (o.Upper == nil || o.Work == nil) { // unreachable return msg.WrapErr(ENOTRECOVERABLE, "impossible overlay state reached") } - if o.Upper != zeroString { - if !path.IsAbs(o.Upper) { - return msg.WrapErr(EBADE, fmt.Sprintf("upperdir %q is not absolute", o.Upper)) - } - if v, err := filepath.EvalSymlinks(o.Upper); err != nil { + if o.Upper != nil { + if v, err := filepath.EvalSymlinks(o.Upper.String()); err != nil { return wrapErrSelf(err) } else { - o.Upper = escapeOverlayDataSegment(toHost(v)) + o.upper = escapeOverlayDataSegment(toHost(v)) } } - if o.Work != zeroString { - if !path.IsAbs(o.Work) { - return msg.WrapErr(EBADE, fmt.Sprintf("workdir %q is not absolute", o.Work)) - } - if v, err := filepath.EvalSymlinks(o.Work); err != nil { + if o.Work != nil { + if v, err := filepath.EvalSymlinks(o.Work.String()); err != nil { return wrapErrSelf(err) } else { - o.Work = escapeOverlayDataSegment(toHost(v)) + o.work = escapeOverlayDataSegment(toHost(v)) } } } - for i := range o.Lower { - if !path.IsAbs(o.Lower[i]) { - return msg.WrapErr(EBADE, fmt.Sprintf("lowerdir %q is not absolute", o.Lower[i])) + o.lower = make([]string, len(o.Lower)) + for i, a := range o.Lower { + if a == nil { + return EBADE } - if v, err := filepath.EvalSymlinks(o.Lower[i]); err != nil { + if v, err := filepath.EvalSymlinks(a.String()); err != nil { return wrapErrSelf(err) } else { - o.Lower[i] = escapeOverlayDataSegment(toHost(v)) + o.lower[i] = escapeOverlayDataSegment(toHost(v)) } } return nil } func (o *MountOverlayOp) apply(params *Params) error { - if !path.IsAbs(o.Target) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", o.Target)) + if o.Target == nil { + return EBADE } - target := toSysroot(o.Target) + target := toSysroot(o.Target.String()) if err := os.MkdirAll(target, params.ParentPerm); err != nil { return wrapErrSelf(err) } @@ -458,17 +456,17 @@ func (o *MountOverlayOp) apply(params *Params) error { if o.ephemeral { var err error // these directories are created internally, therefore early (absolute, symlink, prefix, escape) is bypassed - if o.Upper, err = os.MkdirTemp(FHSRoot, intermediatePatternOverlayUpper); err != nil { + if o.upper, err = os.MkdirTemp(FHSRoot, intermediatePatternOverlayUpper); err != nil { return wrapErrSelf(err) } - if o.Work, err = os.MkdirTemp(FHSRoot, intermediatePatternOverlayWork); err != nil { + if o.work, err = os.MkdirTemp(FHSRoot, intermediatePatternOverlayWork); err != nil { return wrapErrSelf(err) } } options := make([]string, 0, 4) - if o.Upper == zeroString && o.Work == zeroString { // readonly + if o.upper == zeroString && o.work == zeroString { // readonly if len(o.Lower) < 2 { return msg.WrapErr(EINVAL, "readonly overlay requires at least two lowerdir") } @@ -478,11 +476,11 @@ func (o *MountOverlayOp) apply(params *Params) error { return msg.WrapErr(EINVAL, "overlay requires at least one lowerdir") } options = append(options, - OptionOverlayUpperdir+"="+o.Upper, - OptionOverlayWorkdir+"="+o.Work) + OptionOverlayUpperdir+"="+o.upper, + OptionOverlayWorkdir+"="+o.work) } options = append(options, - OptionOverlayLowerdir+"="+strings.Join(o.Lower, SpecialOverlayPath), + OptionOverlayLowerdir+"="+strings.Join(o.lower, SpecialOverlayPath), OptionOverlayUserxattr) return wrapErrSuffix(Mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, SpecialOverlayOption)), @@ -505,70 +503,73 @@ func (o *MountOverlayOp) String() string { func init() { gob.Register(new(SymlinkOp)) } // Link appends an [Op] that creates a symlink in the container filesystem. -func (f *Ops) Link(target, linkName string) *Ops { - *f = append(*f, &SymlinkOp{target, linkName}) +func (f *Ops) Link(target *Absolute, linkName string, dereference bool) *Ops { + *f = append(*f, &SymlinkOp{target, linkName, dereference}) return f } -type SymlinkOp [2]string +type SymlinkOp struct { + Target *Absolute + // LinkName is an arbitrary uninterpreted pathname. + LinkName string + + // Dereference causes LinkName to be dereferenced during early. + Dereference bool +} func (l *SymlinkOp) early(*Params) error { - if strings.HasPrefix(l[0], "*") { - l[0] = l[0][1:] - if !path.IsAbs(l[0]) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l[0])) + if l.Dereference { + if !isAbs(l.LinkName) { + return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l.LinkName)) } - if name, err := os.Readlink(l[0]); err != nil { + if name, err := os.Readlink(l.LinkName); err != nil { return wrapErrSelf(err) } else { - l[0] = name + l.LinkName = name } } return nil } + func (l *SymlinkOp) apply(params *Params) error { - // symlink target is an arbitrary path value, so only validate link name here - if !path.IsAbs(l[1]) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l[1])) + if l.Target == nil { + return EBADE } - - target := toSysroot(l[1]) + target := toSysroot(l.Target.String()) if err := os.MkdirAll(path.Dir(target), params.ParentPerm); err != nil { return wrapErrSelf(err) } - if err := os.Symlink(l[0], target); err != nil { + if err := os.Symlink(l.LinkName, target); err != nil { return wrapErrSelf(err) } return nil } -func (l *SymlinkOp) Is(op Op) bool { vl, ok := op.(*SymlinkOp); return ok && *l == *vl } -func (*SymlinkOp) prefix() string { return "creating" } -func (l *SymlinkOp) String() string { return fmt.Sprintf("symlink on %q target %q", l[1], l[0]) } +func (l *SymlinkOp) Is(op Op) bool { vl, ok := op.(*SymlinkOp); return ok && *l == *vl } +func (*SymlinkOp) prefix() string { return "creating" } +func (l *SymlinkOp) String() string { + return fmt.Sprintf("symlink on %q linkname %q", l.Target, l.LinkName) +} func init() { gob.Register(new(MkdirOp)) } // Mkdir appends an [Op] that creates a directory in the container filesystem. -func (f *Ops) Mkdir(dest string, perm os.FileMode) *Ops { - *f = append(*f, &MkdirOp{dest, perm}) +func (f *Ops) Mkdir(name *Absolute, perm os.FileMode) *Ops { + *f = append(*f, &MkdirOp{name, perm}) return f } type MkdirOp struct { - Path string + Path *Absolute Perm os.FileMode } func (m *MkdirOp) early(*Params) error { return nil } func (m *MkdirOp) apply(*Params) error { - if !path.IsAbs(m.Path) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", m.Path)) + if m.Path == nil { + return EBADE } - - if err := os.MkdirAll(toSysroot(m.Path), m.Perm); err != nil { - return wrapErrSelf(err) - } - return nil + return wrapErrSelf(os.MkdirAll(toSysroot(m.Path.String()), m.Perm)) } func (m *MkdirOp) Is(op Op) bool { vm, ok := op.(*MkdirOp); return ok && m == vm } @@ -578,10 +579,13 @@ func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m func init() { gob.Register(new(TmpfileOp)) } // Place appends an [Op] that places a file in container path [TmpfileOp.Path] containing [TmpfileOp.Data]. -func (f *Ops) Place(name string, data []byte) *Ops { *f = append(*f, &TmpfileOp{name, data}); return f } +func (f *Ops) Place(name *Absolute, data []byte) *Ops { + *f = append(*f, &TmpfileOp{name, data}) + return f +} // PlaceP is like Place but writes the address of [TmpfileOp.Data] to the pointer dataP points to. -func (f *Ops) PlaceP(name string, dataP **[]byte) *Ops { +func (f *Ops) PlaceP(name *Absolute, dataP **[]byte) *Ops { t := &TmpfileOp{Path: name} *dataP = &t.Data @@ -590,14 +594,14 @@ func (f *Ops) PlaceP(name string, dataP **[]byte) *Ops { } type TmpfileOp struct { - Path string + Path *Absolute Data []byte } func (t *TmpfileOp) early(*Params) error { return nil } func (t *TmpfileOp) apply(params *Params) error { - if !path.IsAbs(t.Path) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path)) + if t.Path == nil { + return EBADE } var tmpPath string @@ -613,7 +617,7 @@ func (t *TmpfileOp) apply(params *Params) error { tmpPath = f.Name() } - target := toSysroot(t.Path) + target := toSysroot(t.Path.String()) if err := ensureFile(target, 0444, params.ParentPerm); err != nil { return err } else if err = hostProc.bindMount( diff --git a/container/path.go b/container/path.go index 28a56a13..fe6d6d4c 100644 --- a/container/path.go +++ b/container/path.go @@ -13,6 +13,8 @@ import ( "hakurei.app/container/vfs" ) +/* constants in this file bypass abs check, be extremely careful when changing them! */ + const ( // FHSRoot points to the file system root. FHSRoot = "/" @@ -49,6 +51,38 @@ const ( FHSSys = "/sys/" ) +var ( + // AbsFHSRoot is [FHSRoot] as [Absolute]. + AbsFHSRoot = &Absolute{FHSRoot} + // AbsFHSEtc is [FHSEtc] as [Absolute]. + AbsFHSEtc = &Absolute{FHSEtc} + // AbsFHSTmp is [FHSTmp] as [Absolute]. + AbsFHSTmp = &Absolute{FHSTmp} + + // AbsFHSRun is [FHSRun] as [Absolute]. + AbsFHSRun = &Absolute{FHSRun} + // AbsFHSRunUser is [FHSRunUser] as [Absolute]. + AbsFHSRunUser = &Absolute{FHSRunUser} + + // AbsFHSUsrBin is [FHSUsrBin] as [Absolute]. + AbsFHSUsrBin = &Absolute{FHSUsrBin} + + // AbsFHSVar is [FHSVar] as [Absolute]. + AbsFHSVar = &Absolute{FHSVar} + // AbsFHSVarLib is [FHSVarLib] as [Absolute]. + AbsFHSVarLib = &Absolute{FHSVarLib} + + // AbsFHSDev is [FHSDev] as [Absolute]. + AbsFHSDev = &Absolute{FHSDev} + // AbsFHSProc is [FHSProc] as [Absolute]. + AbsFHSProc = &Absolute{FHSProc} + // AbsFHSSys is [FHSSys] as [Absolute]. + AbsFHSSys = &Absolute{FHSSys} + + // AbsNonexistent is [Nonexistent] as [Absolute]. + AbsNonexistent = &Absolute{Nonexistent} +) + const ( // Nonexistent is a path that cannot exist. // /proc is chosen because a system with covered /proc is unsupported by this package. |
