aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-05-04 02:07:52 +0900
committerOphestra <cat@gensokyo.uk>2026-05-04 02:29:56 +0900
commitda11b26ec16ef9ed61045fdbb03c4090286bfeba (patch)
treeef6120c9c274276eb90976dc49a1a0808a3914b9
parent024489e800a3324e2305546598b606357606e274 (diff)
container/initoverlay: configure via fsconfig
This works around the page size limit at the cost of negligible performance regressions. Closes #34. Signed-off-by: Ophestra <cat@gensokyo.uk>
-rw-r--r--check/overlay.go8
-rw-r--r--container/container_test.go24
-rw-r--r--container/dispatcher.go5
-rw-r--r--container/dispatcher_test.go8
-rw-r--r--container/initoverlay.go52
-rw-r--r--container/initoverlay_test.go66
-rw-r--r--container/path_test.go4
-rw-r--r--internal/pkg/testdata/main.go36
-rw-r--r--test/sandbox/case/device.nix4
-rw-r--r--test/sandbox/case/mapuid.nix4
-rw-r--r--test/sandbox/case/tty.nix4
11 files changed, 124 insertions, 91 deletions
diff --git a/check/overlay.go b/check/overlay.go
index fbbcd1e6..c417e709 100644
--- a/check/overlay.go
+++ b/check/overlay.go
@@ -4,15 +4,23 @@ import "strings"
const (
// SpecialOverlayEscape is the escape string for overlay mount options.
+ //
+ // Deprecated: This is no longer used and will be removed in 0.5.
SpecialOverlayEscape = `\`
// SpecialOverlayOption is the separator string between overlay mount options.
+ //
+ // Deprecated: This is no longer used and will be removed in 0.5.
SpecialOverlayOption = ","
// SpecialOverlayPath is the separator string between overlay paths.
+ //
+ // Deprecated: This is no longer used and will be removed in 0.5.
SpecialOverlayPath = ":"
)
// EscapeOverlayDataSegment escapes a string for formatting into the data
// argument of an overlay mount system call.
+//
+// Deprecated: This is no longer used and will be removed in 0.5.
func EscapeOverlayDataSegment(s string) string {
if s == "" {
return ""
diff --git a/container/container_test.go b/container/container_test.go
index 41d39f68..1bdc28a1 100644
--- a/container/container_test.go
+++ b/container/container_test.go
@@ -16,6 +16,7 @@ import (
"strings"
"syscall"
"testing"
+ _ "unsafe" // for go:linkname
"hakurei.app/check"
"hakurei.app/command"
@@ -233,6 +234,9 @@ func earlyMnt(mnt ...*vfs.MountInfoEntry) func(*testing.T, context.Context) []*v
return func(*testing.T, context.Context) []*vfs.MountInfoEntry { return mnt }
}
+//go:linkname toHost hakurei.app/container.toHost
+func toHost(name string) string
+
var containerTestCases = []struct {
name string
filter bool
@@ -332,13 +336,15 @@ var containerTestCases = []struct {
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
- "rw,lowerdir="+
- container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*check.Absolute).String())+":"+
- container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
+ "rw"+
+ ",lowerdir+="+
+ toHost(ctx.Value(testVal("lower0")).(*check.Absolute).String())+
+ ",lowerdir+="+
+ toHost(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
",upperdir="+
- container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*check.Absolute).String())+
+ toHost(ctx.Value(testVal("upper")).(*check.Absolute).String())+
",workdir="+
- container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(*check.Absolute).String())+
+ toHost(ctx.Value(testVal("work")).(*check.Absolute).String())+
",redirect_dir=nofollow,uuid=on,userxattr"),
}
},
@@ -388,9 +394,11 @@ var containerTestCases = []struct {
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
- "ro,lowerdir="+
- container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*check.Absolute).String())+":"+
- container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
+ "ro"+
+ ",lowerdir+="+
+ toHost(ctx.Value(testVal("lower0")).(*check.Absolute).String())+
+ ",lowerdir+="+
+ toHost(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
",redirect_dir=nofollow,userxattr"),
}
},
diff --git a/container/dispatcher.go b/container/dispatcher.go
index e583dc21..4636a469 100644
--- a/container/dispatcher.go
+++ b/container/dispatcher.go
@@ -65,6 +65,8 @@ type syscallDispatcher interface {
remount(msg message.Msg, target string, flags uintptr) error
// mountTmpfs provides mountTmpfs.
mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error
+ // mountOverlay provides mountOverlay.
+ mountOverlay(target string, options [][2]string) error
// ensureFile provides ensureFile.
ensureFile(name string, perm, pperm os.FileMode) error
// mustLoopback provides mustLoopback.
@@ -169,6 +171,9 @@ func (direct) remount(msg message.Msg, target string, flags uintptr) error {
func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
return mountTmpfs(k, fsname, target, flags, size, perm)
}
+func (k direct) mountOverlay(target string, options [][2]string) error {
+ return mountOverlay(target, options)
+}
func (direct) ensureFile(name string, perm, pperm os.FileMode) error {
return ensureFile(name, perm, pperm)
}
diff --git a/container/dispatcher_test.go b/container/dispatcher_test.go
index c8a7b492..49631854 100644
--- a/container/dispatcher_test.go
+++ b/container/dispatcher_test.go
@@ -468,6 +468,14 @@ func (k *kstub) mountTmpfs(fsname, target string, flags uintptr, size int, perm
stub.CheckArg(k.Stub, "perm", perm, 4))
}
+func (k *kstub) mountOverlay(target string, options [][2]string) error {
+ k.Helper()
+ return k.Expects("mountOverlay").Error(
+ stub.CheckArg(k.Stub, "target", target, 0),
+ stub.CheckArgReflect(k.Stub, "options", options, 1),
+ )
+}
+
func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
k.Helper()
return k.Expects("ensureFile").Error(
diff --git a/container/initoverlay.go b/container/initoverlay.go
index 910ce425..e4cb0f5a 100644
--- a/container/initoverlay.go
+++ b/container/initoverlay.go
@@ -4,9 +4,9 @@ import (
"encoding/gob"
"fmt"
"slices"
- "strings"
"hakurei.app/check"
+ "hakurei.app/ext"
"hakurei.app/fhs"
)
@@ -150,7 +150,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
if v, err := k.evalSymlinks(o.Upper.String()); err != nil {
return err
} else {
- o.upper = check.EscapeOverlayDataSegment(toHost(v))
+ o.upper = toHost(v)
}
}
@@ -158,7 +158,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
if v, err := k.evalSymlinks(o.Work.String()); err != nil {
return err
} else {
- o.work = check.EscapeOverlayDataSegment(toHost(v))
+ o.work = toHost(v)
}
}
}
@@ -168,12 +168,39 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
if v, err := k.evalSymlinks(a.String()); err != nil {
return err
} else {
- o.lower[i] = check.EscapeOverlayDataSegment(toHost(v))
+ o.lower[i] = toHost(v)
}
}
return nil
}
+// mountOverlay sets up an overlay mount via [ext.FS].
+func mountOverlay(target string, options [][2]string) error {
+ fs, err := ext.OpenFS(SourceOverlay, 0)
+ if err != nil {
+ return err
+ }
+ if err = fs.SetString("source", SourceOverlay); err != nil {
+ _ = fs.Close()
+ return err
+ }
+ for _, option := range options {
+ if err = fs.SetString(option[0], option[1]); err != nil {
+ _ = fs.Close()
+ return err
+ }
+ }
+ if err = fs.SetFlag(OptionOverlayUserxattr); err != nil {
+ _ = fs.Close()
+ return err
+ }
+ if err = fs.Mount(target, 0); err != nil {
+ _ = fs.Close()
+ return err
+ }
+ return fs.Close()
+}
+
func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
target := o.Target.String()
if !o.noPrefix {
@@ -194,7 +221,7 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
}
}
- options := make([]string, 0, 4)
+ options := make([][2]string, 0, 2+len(o.lower))
if o.upper == zeroString && o.work == zeroString { // readonly
if len(o.Lower) < 2 {
@@ -205,15 +232,16 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
if len(o.Lower) == 0 {
return &OverlayArgumentError{OverlayEmptyLower, zeroString}
}
- options = append(options,
- OptionOverlayUpperdir+"="+o.upper,
- OptionOverlayWorkdir+"="+o.work)
+ options = append(options, [][2]string{
+ {OptionOverlayUpperdir, o.upper},
+ {OptionOverlayWorkdir, o.work},
+ }...)
+ }
+ for _, lower := range o.lower {
+ options = append(options, [2]string{OptionOverlayLowerdir + "+", lower})
}
- options = append(options,
- OptionOverlayLowerdir+"="+strings.Join(o.lower, check.SpecialOverlayPath),
- OptionOverlayUserxattr)
- return k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, check.SpecialOverlayOption))
+ return k.mountOverlay(target, options)
}
func (o *MountOverlayOp) late(*setupState, syscallDispatcher) error { return nil }
diff --git a/container/initoverlay_test.go b/container/initoverlay_test.go
index 30dd3237..f0cde7ff 100644
--- a/container/initoverlay_test.go
+++ b/container/initoverlay_test.go
@@ -97,13 +97,12 @@ func TestMountOverlayOp(t *testing.T) {
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0705)}, nil, nil),
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil),
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.work.*"}, "overlay.work.32768", nil),
- call("mount", stub.ExpectArgs{"overlay", "/sysroot", "overlay", uintptr(0), "" +
- "upperdir=overlay.upper.32768," +
- "workdir=overlay.work.32768," +
- "lowerdir=" +
- `/host/var/lib/planterette/base/debian\:f92c9052:` +
- `/host/var/lib/planterette/app/org.chromium.Chromium@debian\:f92c9052,` +
- "userxattr"}, nil, nil),
+ call("mountOverlay", stub.ExpectArgs{"/sysroot", [][2]string{
+ {"upperdir", "overlay.upper.32768"},
+ {"workdir", "overlay.work.32768"},
+ {"lowerdir+", `/host/var/lib/planterette/base/debian:f92c9052`},
+ {"lowerdir+", `/host/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052`},
+ }}, nil, nil),
}, nil},
{"short lower ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
@@ -129,11 +128,10 @@ func TestMountOverlayOp(t *testing.T) {
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil),
}, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/nix/store", os.FileMode(0755)}, nil, nil),
- call("mount", stub.ExpectArgs{"overlay", "/nix/store", "overlay", uintptr(0), "" +
- "lowerdir=" +
- "/host/mnt-root/nix/.ro-store:" +
- "/host/mnt-root/nix/.ro-store0," +
- "userxattr"}, nil, nil),
+ call("mountOverlay", stub.ExpectArgs{"/nix/store", [][2]string{
+ {"lowerdir+", "/host/mnt-root/nix/.ro-store"},
+ {"lowerdir+", "/host/mnt-root/nix/.ro-store0"},
+ }}, nil, nil),
}, nil},
{"success ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
@@ -147,11 +145,10 @@ func TestMountOverlayOp(t *testing.T) {
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil),
}, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil),
- call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
- "lowerdir=" +
- "/host/mnt-root/nix/.ro-store:" +
- "/host/mnt-root/nix/.ro-store0," +
- "userxattr"}, nil, nil),
+ call("mountOverlay", stub.ExpectArgs{"/sysroot/nix/store", [][2]string{
+ {"lowerdir+", "/host/mnt-root/nix/.ro-store"},
+ {"lowerdir+", "/host/mnt-root/nix/.ro-store0"},
+ }}, nil, nil),
}, nil},
{"nil lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
@@ -219,7 +216,11 @@ func TestMountOverlayOp(t *testing.T) {
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
}, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
- call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "upperdir=/host/mnt-root/nix/.rw-store/.upper,workdir=/host/mnt-root/nix/.rw-store/.work,lowerdir=/host/mnt-root/nix/ro-store,userxattr"}, nil, stub.UniqueError(0)),
+ call("mountOverlay", stub.ExpectArgs{"/sysroot/nix/store", [][2]string{
+ {"upperdir", "/host/mnt-root/nix/.rw-store/.upper"},
+ {"workdir", "/host/mnt-root/nix/.rw-store/.work"},
+ {"lowerdir+", "/host/mnt-root/nix/ro-store"},
+ }}, nil, stub.UniqueError(0)),
}, stub.UniqueError(0)},
{"success single layer", &Params{ParentPerm: 0700}, &MountOverlayOp{
@@ -233,11 +234,11 @@ func TestMountOverlayOp(t *testing.T) {
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
}, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
- call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
- "upperdir=/host/mnt-root/nix/.rw-store/.upper," +
- "workdir=/host/mnt-root/nix/.rw-store/.work," +
- "lowerdir=/host/mnt-root/nix/ro-store," +
- "userxattr"}, nil, nil),
+ call("mountOverlay", stub.ExpectArgs{"/sysroot/nix/store", [][2]string{
+ {"upperdir", "/host/mnt-root/nix/.rw-store/.upper"},
+ {"workdir", "/host/mnt-root/nix/.rw-store/.work"},
+ {"lowerdir+", "/host/mnt-root/nix/ro-store"},
+ }}, nil, nil),
}, nil},
{"success", &Params{ParentPerm: 0700}, &MountOverlayOp{
@@ -261,16 +262,15 @@ func TestMountOverlayOp(t *testing.T) {
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store3"}, "/mnt-root/nix/ro-store3", nil),
}, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
- call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
- "upperdir=/host/mnt-root/nix/.rw-store/.upper," +
- "workdir=/host/mnt-root/nix/.rw-store/.work," +
- "lowerdir=" +
- "/host/mnt-root/nix/ro-store:" +
- "/host/mnt-root/nix/ro-store0:" +
- "/host/mnt-root/nix/ro-store1:" +
- "/host/mnt-root/nix/ro-store2:" +
- "/host/mnt-root/nix/ro-store3," +
- "userxattr"}, nil, nil),
+ call("mountOverlay", stub.ExpectArgs{"/sysroot/nix/store", [][2]string{
+ {"upperdir", "/host/mnt-root/nix/.rw-store/.upper"},
+ {"workdir", "/host/mnt-root/nix/.rw-store/.work"},
+ {"lowerdir+", "/host/mnt-root/nix/ro-store"},
+ {"lowerdir+", "/host/mnt-root/nix/ro-store0"},
+ {"lowerdir+", "/host/mnt-root/nix/ro-store1"},
+ {"lowerdir+", "/host/mnt-root/nix/ro-store2"},
+ {"lowerdir+", "/host/mnt-root/nix/ro-store3"},
+ }}, nil, nil),
}, nil},
})
diff --git a/container/path_test.go b/container/path_test.go
index 03406fb0..85e285e2 100644
--- a/container/path_test.go
+++ b/container/path_test.go
@@ -10,7 +10,6 @@ import (
"testing"
"unsafe"
- "hakurei.app/check"
"hakurei.app/vfs"
)
@@ -50,9 +49,6 @@ func TestToHost(t *testing.T) {
}
}
-// InternalToHostOvlEscape exports toHost passed to [check.EscapeOverlayDataSegment].
-func InternalToHostOvlEscape(s string) string { return check.EscapeOverlayDataSegment(toHost(s)) }
-
func TestCreateFile(t *testing.T) {
t.Run("nonexistent", func(t *testing.T) {
t.Run("mkdir", func(t *testing.T) {
diff --git a/internal/pkg/testdata/main.go b/internal/pkg/testdata/main.go
index f79f7917..564a1736 100644
--- a/internal/pkg/testdata/main.go
+++ b/internal/pkg/testdata/main.go
@@ -14,7 +14,6 @@ import (
"strconv"
"strings"
- "hakurei.app/check"
"hakurei.app/fhs"
"hakurei.app/vfs"
)
@@ -159,43 +158,24 @@ func main() {
m.Source != "overlay" || m.FsType != "overlay" {
log.Fatal("unexpected root mount entry")
}
- var lowerdir string
+ var lowerdir []string
for _, o := range strings.Split(m.FsOptstr, ",") {
- const lowerdirKey = "lowerdir="
+ const lowerdirKey = "lowerdir+="
if strings.HasPrefix(o, lowerdirKey) {
- lowerdir = o[len(lowerdirKey):]
+ lowerdir = append(lowerdir, o[len(lowerdirKey):])
}
}
if !layers {
- if filepath.Base(lowerdir) != checksumEmptyDir {
+ if len(lowerdir) != 1 || filepath.Base(lowerdir[0]) != checksumEmptyDir {
log.Fatal("unexpected artifact checksum")
}
} else {
ident = "p1t_drXr34i-jZNuxDMLaMOdL6tZvQqhavNafGynGqxOZoXAUTSn7kqNh3Ovv3DT"
- lowerdirsEscaped := strings.Split(lowerdir, ":")
- lowerdirs := lowerdirsEscaped[:0]
- // ignore the option separator since it does not appear in ident
- for i, e := range lowerdirsEscaped {
- if len(e) > 0 &&
- e[len(e)-1] == check.SpecialOverlayEscape[0] &&
- (len(e) == 1 || e[len(e)-2] != check.SpecialOverlayEscape[0]) {
- // ignore escaped pathname separator since it does not
- // appear in ident
-
- e = e[:len(e)-1]
- if len(lowerdirsEscaped) != i {
- lowerdirsEscaped[i+1] = e + lowerdirsEscaped[i+1]
- continue
- }
- }
- lowerdirs = append(lowerdirs, e)
- }
-
- if len(lowerdirs) != 2 ||
- filepath.Base(lowerdirs[0]) != "MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU" ||
- filepath.Base(lowerdirs[1]) != "nY_CUdiaUM1OL4cPr5TS92FCJ3rCRV7Hm5oVTzAvMXwC03_QnTRfQ5PPs7mOU9fK" {
- log.Fatalf("unexpected lowerdirs %s", strings.Join(lowerdirs, ", "))
+ if len(lowerdir) != 2 ||
+ filepath.Base(lowerdir[0]) != "MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU" ||
+ filepath.Base(lowerdir[1]) != "nY_CUdiaUM1OL4cPr5TS92FCJ3rCRV7Hm5oVTzAvMXwC03_QnTRfQ5PPs7mOU9fK" {
+ log.Fatalf("unexpected lowerdirs %s", strings.Join(lowerdir, ", "))
}
}
} else {
diff --git a/test/sandbox/case/device.nix b/test/sandbox/case/device.nix
index b9dc4b33..d37768a4 100644
--- a/test/sandbox/case/device.nix
+++ b/test/sandbox/case/device.nix
@@ -237,8 +237,8 @@ in
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
- (ent "/" "/.hakurei/.ro-store" "rw,relatime" "overlay" "overlay" "ro,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,redirect_dir=nofollow,userxattr")
- (ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,userxattr")
+ (ent "/" "/.hakurei/.ro-store" "rw,relatime" "overlay" "overlay" "ro,lowerdir+=/host/nix/.ro-store,lowerdir+=/host/nix/.rw-store/upper,redirect_dir=nofollow,userxattr")
+ (ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir+=/host/nix/.ro-store,lowerdir+=/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,userxattr")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/var/lib/hakurei/u0/a4" "/var/lib/hakurei/u0/a4" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
diff --git a/test/sandbox/case/mapuid.nix b/test/sandbox/case/mapuid.nix
index 8b2a635e..7aa78a9b 100644
--- a/test/sandbox/case/mapuid.nix
+++ b/test/sandbox/case/mapuid.nix
@@ -264,8 +264,8 @@ in
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
- (ent "/" "/.hakurei/.ro-store" "rw,relatime" "overlay" "overlay" "ro,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,redirect_dir=nofollow,userxattr")
- (ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,userxattr")
+ (ent "/" "/.hakurei/.ro-store" "rw,relatime" "overlay" "overlay" "ro,lowerdir+=/host/nix/.ro-store,lowerdir+=/host/nix/.rw-store/upper,redirect_dir=nofollow,userxattr")
+ (ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir+=/host/nix/.ro-store,lowerdir+=/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,userxattr")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/var/lib/hakurei/u0/a3" "/var/lib/hakurei/u0/a3" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent ignore "/run/user/1000/pulse/native" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
diff --git a/test/sandbox/case/tty.nix b/test/sandbox/case/tty.nix
index 7c00baed..ccd404a9 100644
--- a/test/sandbox/case/tty.nix
+++ b/test/sandbox/case/tty.nix
@@ -270,8 +270,8 @@ in
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
- (ent "/" "/.hakurei/.ro-store" "rw,relatime" "overlay" "overlay" "ro,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,redirect_dir=nofollow,userxattr")
- (ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,uuid=on,userxattr")
+ (ent "/" "/.hakurei/.ro-store" "rw,relatime" "overlay" "overlay" "ro,lowerdir+=/host/nix/.ro-store,lowerdir+=/host/nix/.rw-store/upper,redirect_dir=nofollow,userxattr")
+ (ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir+=/host/nix/.ro-store,lowerdir+=/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,uuid=on,userxattr")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/var/lib/hakurei/u0/a2" "/var/lib/hakurei/u0/a2" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")