Skip to content

Overlay size option: --overlay2=all:memory,size=1g #11723

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions g3doc/user_guide/filesystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ Self-backed rootfs overlay (`--overlay2=root:self`) is enabled by default in
runsc for performance. If you need to propagate rootfs changes to the host
filesystem, then disable it with `--overlay2=none`.

Overlay has `size=` option which is passed as `size=` tmpfs mount option.
For example, `--overlay2=root:memory,size=2g`.

## Directfs

Directfs is a feature that allows the sandbox process to directly access the
Expand Down
22 changes: 18 additions & 4 deletions runsc/boot/gofer_conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,19 +124,24 @@ func (l *GoferMountConfLowerType) Set(v string) error {

// GoferMountConf describes how a gofer mount is configured in the sandbox.
type GoferMountConf struct {
Upper GoferMountConfUpperType `json:"upper"`
Lower GoferMountConfLowerType `json:"lower"`
Upper GoferMountConfUpperType `json:"upper"`
Size string `json:"size,omitempty"`
}

// String returns a human-readable string representing the gofer mount config.
func (g GoferMountConf) String() string {
return fmt.Sprintf("%s:%s", g.Lower, g.Upper)
res := fmt.Sprintf("%s:%s", g.Lower, g.Upper)
if g.Size != "" {
res += ":size=" + g.Size
}
return res
}

// Set sets the value. Set(String()) should be idempotent.
func (g *GoferMountConf) Set(v string) error {
parts := strings.Split(v, ":")
if len(parts) != 2 {
if len(parts) < 2 || len(parts) > 3 {
return fmt.Errorf("invalid gofer mount config format: %q", v)
}
if err := g.Lower.Set(parts[0]); err != nil {
Expand All @@ -145,8 +150,17 @@ func (g *GoferMountConf) Set(v string) error {
if err := g.Upper.Set(parts[1]); err != nil {
return err
}
g.Size = ""
if len(parts) >= 3 {
sizeArg := parts[2]
size, cut := strings.CutPrefix(sizeArg, "size=")
if !cut {
return fmt.Errorf("invalid gofer mount config format: %q", v)
}
g.Size = size
}
if !g.valid() {
return fmt.Errorf("invalid gofer mount config: %+v", g)
return fmt.Errorf("invalid gofer mount config: %q", v)
}
return nil
}
Expand Down
27 changes: 26 additions & 1 deletion runsc/boot/gofer_conf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,32 @@ func TestGoferConfFlags(t *testing.T) {
}
for i := range want {
if want[i] != got[i] {
t.Errorf("gofer conf is incorrect: want = %d, got = %d", want[i], got[i])
t.Errorf("gofer conf is incorrect: want = %s, got = %s", want[i], got[i])
}
}
}

func TestGoferMountConfSetGet(t *testing.T) {
t.Run("Without size", func(t *testing.T) {
conf := GoferMountConf{}
err := conf.Set("lisafs:anon")
if err != nil {
t.Fatalf("Expect success: %v", err)
}
s := conf.String()
if s != "lisafs:anon" {
t.Fatalf("Expected lisafs:anon, got %s", s)
}
})
t.Run("With size", func(t *testing.T) {
conf := GoferMountConf{}
err := conf.Set("lisafs:anon:size=1719")
if err != nil {
t.Fatalf("Expect success: %v", err)
}
s := conf.String()
if s != "lisafs:anon:size=1719" {
t.Fatalf("Expected lisafs:anon:size=1719, got %s", s)
}
})
}
29 changes: 29 additions & 0 deletions runsc/boot/mount_hints.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,9 @@ func (p *PodMountHints) FindMount(mountSrc string) *MountHint {
type RootfsHint struct {
Mount specs.Mount
Overlay config.OverlayMedium
// Size of overlay tmpfs. Passed as `size={Size}` to tmpfs mount.
// Use default if unspecified.
Size string
}

func (r *RootfsHint) setSource(val string) error {
Expand All @@ -258,6 +261,30 @@ func (r *RootfsHint) setType(val string) error {
return nil
}

func (r *RootfsHint) setOption(key, val string) error {
switch key {
case "size":
r.Size = val
default:
return fmt.Errorf("invalid rootfs option: %s=%s", key, val)
}
return nil
}

func (r *RootfsHint) setOptions(val string) error {
for _, option := range strings.Split(val, ",") {
parts := strings.SplitN(option, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("rootfs options must be key=value: %s", option)
}
key, val := parts[0], parts[1]
if err := r.setOption(key, val); err != nil {
return err
}
}
return nil
}

func (r *RootfsHint) setField(key, val string) error {
switch key {
case "source":
Expand All @@ -266,6 +293,8 @@ func (r *RootfsHint) setField(key, val string) error {
return r.setType(val)
case "overlay":
return r.Overlay.Set(val)
case "options":
return r.setOptions(val)
default:
return fmt.Errorf("invalid rootfs annotation: %s=%s", key, val)
}
Expand Down
4 changes: 4 additions & 0 deletions runsc/boot/mount_hints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ func TestRootfsHintHappy(t *testing.T) {
RootfsPrefix + "source": imagePath,
RootfsPrefix + "type": erofs.Name,
RootfsPrefix + "overlay": config.MemoryOverlay.String(),
RootfsPrefix + "options": "size=100m",
},
}
hint, err := NewRootfsHint(spec)
Expand All @@ -275,6 +276,9 @@ func TestRootfsHintHappy(t *testing.T) {
if hint.Overlay != config.MemoryOverlay {
t.Errorf("rootfs overlay, want: %q, got: %q", config.MemoryOverlay, hint.Overlay)
}
if hint.Size != "100m" {
t.Errorf("rootfs size, want: 100m, got: %q", hint.Size)
}
}

// TestRootfsHintErrors tests that proper errors will be returned when parsing
Expand Down
6 changes: 6 additions & 0 deletions runsc/boot/vfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,12 @@ func (c *containerMounter) configureOverlay(ctx context.Context, conf *config.Co
// filesystem specific options.
upperOpts := *lowerOpts
upperOpts.GetFilesystemOptions = vfs.GetFilesystemOptions{InternalMount: true}
if mountConf.Size != "" {
if upperOpts.GetFilesystemOptions.Data != "" {
upperOpts.GetFilesystemOptions.Data += ","
}
upperOpts.GetFilesystemOptions.Data += "size=" + mountConf.Size
}

overlayOpts := *lowerOpts
overlayOpts.GetFilesystemOptions = vfs.GetFilesystemOptions{InternalMount: true}
Expand Down
64 changes: 60 additions & 4 deletions runsc/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -909,24 +909,42 @@ type Overlay2 struct {
rootMount bool
subMounts bool
medium OverlayMedium
// Size of overlay upper layer.
// Passed as is to tmpfs mount, as `size={size}`.
// Empty means use default.
// Size is applied to each overlay independently and not shared by overlays.
size string
}

func defaultOverlay2() *Overlay2 {
// Rootfs overlay is enabled by default and backed by a file in rootfs itself.
return &Overlay2{rootMount: true, subMounts: false, medium: SelfOverlay}
}

func setOverlay2Err(v string) error {
return fmt.Errorf("expected format is --overlay2={mount}:{medium}[,size={size}], got %q", v)
}

// `--overlay2=...` param `size=`.
const overlay2SizeEq = "size="

// Set implements flag.Value. Set(String()) should be idempotent.
func (o *Overlay2) Set(v string) error {
if v == "none" {
o.rootMount = false
o.subMounts = false
o.medium = NoOverlay
o.size = ""
return nil
}
vs := strings.Split(v, ":")
parts := strings.Split(v, ",")
if len(parts) < 1 {
return setOverlay2Err(v)
}

vs := strings.Split(parts[0], ":")
if len(vs) != 2 {
return fmt.Errorf("expected format is --overlay2={mount}:{medium}, got %q", v)
return setOverlay2Err(v)
}

switch mount := vs[0]; mount {
Expand All @@ -939,7 +957,25 @@ func (o *Overlay2) Set(v string) error {
return fmt.Errorf("unexpected mount specifier for --overlay2: %q", mount)
}

return o.medium.Set(vs[1])
err := o.medium.Set(vs[1])
if err != nil {
return err
}

if len(parts) == 1 {
o.size = ""
} else if len(parts) == 2 {
sizeArg := parts[1]
size, cut := strings.CutPrefix(sizeArg, overlay2SizeEq)
if !cut {
return setOverlay2Err(v)
}
o.size = size
} else {
return setOverlay2Err(v)
}

return nil
}

// Get implements flag.Value.
Expand All @@ -961,7 +997,13 @@ func (o Overlay2) String() string {
default:
panic("invalid state of subMounts = true and rootMount = false")
}
return res + ":" + o.medium.String()

var sizeSuffix string
if o.size != "" {
sizeSuffix = fmt.Sprintf(",%s%s", overlay2SizeEq, o.size)
}

return res + ":" + o.medium.String() + sizeSuffix
}

// Enabled returns true if the overlay option is enabled for any mounts.
Expand All @@ -977,6 +1019,13 @@ func (o *Overlay2) RootOverlayMedium() OverlayMedium {
return o.medium
}

func (o *Overlay2) RootOverlaySize() string {
if !o.rootMount {
return ""
}
return o.size
}

// SubMountOverlayMedium returns the overlay medium config of submounts.
func (o *Overlay2) SubMountOverlayMedium() OverlayMedium {
if !o.subMounts {
Expand All @@ -985,6 +1034,13 @@ func (o *Overlay2) SubMountOverlayMedium() OverlayMedium {
return o.medium
}

func (o *Overlay2) SubMountOverlaySize() string {
if !o.subMounts {
return ""
}
return o.size
}

// Medium returns the overlay medium config.
func (o Overlay2) Medium() OverlayMedium {
return o.medium
Expand Down
34 changes: 34 additions & 0 deletions runsc/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,11 @@ func TestInvalidFlags(t *testing.T) {
value: "root:dir=tmp",
error: "overlay host file directory should be an absolute path, got \"tmp\"",
},
{
name: "overlay2",
value: "root:memory,sz=sdg",
error: "expected format is --overlay2",
},
} {
t.Run(tc.name, func(t *testing.T) {
testFlags := flag.NewFlagSet("test", flag.ContinueOnError)
Expand Down Expand Up @@ -781,3 +786,32 @@ root = "%s"
}

}

func TestParseSerializeOverlay2(t *testing.T) {
t.Run("Without size", func(t *testing.T) {
o := Overlay2{}
err := o.Set("all:memory")
if err != nil {
t.Fatalf("Set failed: %v", err)
}
if o.RootOverlaySize() != "" || o.SubMountOverlaySize() != "" {
t.Fatalf("Size mismatch, expecting empty, got %q, %q", o.RootOverlaySize(), o.SubMountOverlaySize())
}
if o.String() != "all:memory" {
t.Fatalf("String mismatch, expecting all:memory, got %q", o.String())
}
})
t.Run("With size", func(t *testing.T) {
o := Overlay2{}
err := o.Set("root:memory,size=1g")
if err != nil {
t.Fatalf("Set failed: %v", err)
}
if o.RootOverlaySize() != "1g" || o.SubMountOverlaySize() != "" {
t.Fatalf("Size mismatch, expecting 1g, empty, got %q, %q", o.RootOverlaySize(), o.SubMountOverlaySize())
}
if o.String() != "root:memory,size=1g" {
t.Fatalf("String mismatch, expecting ll:memory,size=1g, got %q", o.String())
}
})
}
7 changes: 6 additions & 1 deletion runsc/config/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,12 @@ func RegisterFlags(flagSet *flag.FlagSet) {
flagSet.Var(fileAccessTypePtr(FileAccessExclusive), "file-access", "specifies which filesystem validation to use for the root mount: exclusive (default), shared.")
flagSet.Var(fileAccessTypePtr(FileAccessShared), "file-access-mounts", "specifies which filesystem validation to use for volumes other than the root mount: shared (default), exclusive.")
flagSet.Bool("overlay", false, "DEPRECATED: use --overlay2=all:memory to achieve the same effect")
flagSet.Var(defaultOverlay2(), flagOverlay2, "wrap mounts with overlayfs. Format is {mount}:{medium}, where 'mount' can be 'root' or 'all' and medium can be 'memory', 'self' or 'dir=/abs/dir/path' in which filestore will be created. 'none' will turn overlay mode off.")
flagSet.Var(defaultOverlay2(), flagOverlay2, "wrap mounts with overlayfs. Format is\n"+
"* 'none' to turn overlay mode off\n"+
"* {mount}:{medium}[,size={size}], where\n"+
" 'mount' can be 'root' or 'all'\n"+
" 'medium' can be 'memory', 'self' or 'dir=/abs/dir/path' in which filestore will be created\n"+
" 'size' optional parameter overrides default overlay upper layer size\n")
flagSet.Bool("fsgofer-host-uds", false, "DEPRECATED: use host-uds=all")
flagSet.Var(hostUDSPtr(HostUDSNone), flagHostUDS, "controls permission to access host Unix-domain sockets. Values: none|open|create|all, default: none")
flagSet.Var(hostFifoPtr(HostFifoNone), "host-fifo", "controls permission to access host FIFOs (or named pipes). Values: none|open, default: none")
Expand Down
Loading