From f53dda7ed177aabb9c45ecedd435c96c3e835143 Mon Sep 17 00:00:00 2001 From: Shayon Mukherjee Date: Wed, 3 Jun 2026 11:23:37 -0700 Subject: [PATCH] fsgofer: add extension prepare hooks Custom fsgofer extensions may need to prepare state before the gofer drops capabilities and enters its final root. For example, an extension may need to register gofer flags, open resources, and pass file descriptor numbers through the gofer re-exec path. This is a proposal to allow extensions to optionally define `SetFlags` and `PrepareGofer` methods. `SetFlags` can add gofer flags during command setup. `PrepareGofer` runs after root setup and before capability application and its `FlagOverrides` are merged into the arguments used for gofer re-exec. This keeps the base Extension interface focused on mount handling while allowing extensions that need early preparation to opt in. No behavior changes when no registered extension defines these optional methods. Related: https://github.com/google/gvisor/pull/12950 FUTURE_COPYBARA_INTEGRATE_REVIEW=https://github.com/google/gvisor/pull/13223 from shayonj:s/gofer-extension-setup b52d4ab4cc4c5eb9ea62ec544ec262a91d777810 PiperOrigin-RevId: 926157510 --- g3doc/user_guide/filesystem.md | 14 +++- runsc/cmd/gofer.go | 13 ++++ runsc/fsgofer/extension/BUILD | 2 + runsc/fsgofer/extension/extension.go | 56 ++++++++++++++++ runsc/fsgofer/extension/extension_test.go | 82 +++++++++++++++++++++++ 5 files changed, 164 insertions(+), 3 deletions(-) diff --git a/g3doc/user_guide/filesystem.md b/g3doc/user_guide/filesystem.md index ed6d02578d..d68cdbf3f6 100644 --- a/g3doc/user_guide/filesystem.md +++ b/g3doc/user_guide/filesystem.md @@ -291,8 +291,13 @@ To build a custom gofer: 1. Implement the `extension.Extension` interface: `Name`, `TryHandleMount`, and `SeccompRules`. -2. Register your extension in `init()` or early `main()`. -3. Build a runsc binary that imports `runsc/cli` and your extension package. +2. Optionally define a `SetFlags(*flag.FlagSet)` method on the extension type + if it needs gofer flags. +3. Optionally define a `PrepareGofer(extension.GoferPrepareContext)` method on + the extension type if it needs to run setup before the gofer drops + capabilities and enters its final root. +4. Register your extension in `init()` or early `main()`. +5. Build a runsc binary that imports `runsc/cli` and your extension package. The extension's `TryHandleMount` returns a `lisafs.ConnectionImpl` and `lisafs.ConnectionOpts` for mounts it handles, or a nil implementation to @@ -300,4 +305,7 @@ decline. All mounts share the same `lisafs.Server`, preserving server-side filesystem tree synchronization across stock and extension-backed mounts. Configuration may be read from OCI annotations and mount fields such as source, type, and options. `SeccompRules` declares any additional syscalls the extension -needs beyond the stock gofer allowlist. +needs beyond the stock gofer allowlist. `PrepareGofer` can return +`FlagOverrides` for state that must survive gofer re-exec, such as file +descriptor numbers. Extensions that pass file descriptors this way must clear +`FD_CLOEXEC` on those descriptors before returning. diff --git a/runsc/cmd/gofer.go b/runsc/cmd/gofer.go index 41cb9ede00..2cfbae6091 100644 --- a/runsc/cmd/gofer.go +++ b/runsc/cmd/gofer.go @@ -155,6 +155,8 @@ func (g *Gofer) SetFlags(f *flag.FlagSet) { // Profiling flags. g.profileFDs.SetFromFlags(f) + + extension.SetFlags(f) } // Execute implements subcommands.Command. @@ -210,10 +212,21 @@ func (g *Gofer) Execute(_ context.Context, f *flag.FlagSet, args ...any) subcomm defer cleanupUnmounter() } } + extensionPrepare, err := extension.PrepareGofer(extension.GoferPrepareContext{ + Spec: spec, + ContainerID: containerID, + BundleDir: g.bundleDir, + }) + if err != nil { + util.Fatalf("preparing gofer extensions: %v", err) + } if g.applyCaps { overrides := g.syncFDs.flags() overrides["apply-caps"] = "false" overrides["setup-root"] = "false" + for key, value := range extensionPrepare.FlagOverrides { + overrides[key] = value + } args := sandboxsetup.PrepareArgs(g.Name(), f, overrides) capsToApply := goferCaps if conf.GetHostUDS().AllowOpen() { diff --git a/runsc/fsgofer/extension/BUILD b/runsc/fsgofer/extension/BUILD index c57397accf..e1b5f0d297 100644 --- a/runsc/fsgofer/extension/BUILD +++ b/runsc/fsgofer/extension/BUILD @@ -12,6 +12,7 @@ go_library( deps = [ "//pkg/lisafs", "//pkg/seccomp", + "//runsc/flag", "@com_github_opencontainers_runtime_spec//specs-go:go_default_library", ], ) @@ -24,6 +25,7 @@ go_test( deps = [ "//pkg/lisafs", "//pkg/seccomp", + "//runsc/flag", "@com_github_opencontainers_runtime_spec//specs-go:go_default_library", ], ) diff --git a/runsc/fsgofer/extension/extension.go b/runsc/fsgofer/extension/extension.go index c463afaec9..975e504ec1 100644 --- a/runsc/fsgofer/extension/extension.go +++ b/runsc/fsgofer/extension/extension.go @@ -20,6 +20,7 @@ import ( specs "github.com/opencontainers/runtime-spec/specs-go" "gvisor.dev/gvisor/pkg/lisafs" "gvisor.dev/gvisor/pkg/seccomp" + "gvisor.dev/gvisor/runsc/flag" ) // Extension is implemented by alternative LisaFS backends. The first @@ -47,6 +48,29 @@ type Extension interface { SeccompRules() seccomp.SyscallRules } +type setFlags interface { + SetFlags(f *flag.FlagSet) +} + +// GoferPrepareContext contains inputs available while preparing the gofer, +// before it drops capabilities and enters its final root. +type GoferPrepareContext struct { + Spec *specs.Spec + ContainerID string + BundleDir string +} + +// GoferPrepareResult contains state for gofer re-exec. +type GoferPrepareResult struct { + // FlagOverrides are applied after setup. File descriptor values must refer + // to descriptors with FD_CLOEXEC cleared. + FlagOverrides map[string]string +} + +type prepareGofer interface { + PrepareGofer(ctx GoferPrepareContext) (GoferPrepareResult, error) +} + var registered []Extension // Register adds e to the extension list. Must be called during init or @@ -59,3 +83,35 @@ func Register(e Extension) { func Registered() []Extension { return registered } + +// SetFlags lets registered extensions add gofer flags. +func SetFlags(f *flag.FlagSet) { + for _, e := range registered { + if setter, ok := e.(setFlags); ok { + setter.SetFlags(f) + } + } +} + +// PrepareGofer lets registered extensions prepare state and merges flag +// overrides for gofer re-exec. +func PrepareGofer(ctx GoferPrepareContext) (GoferPrepareResult, error) { + var result GoferPrepareResult + for _, e := range registered { + prepare, ok := e.(prepareGofer) + if !ok { + continue + } + extensionResult, err := prepare.PrepareGofer(ctx) + if err != nil { + return GoferPrepareResult{}, err + } + for key, value := range extensionResult.FlagOverrides { + if result.FlagOverrides == nil { + result.FlagOverrides = make(map[string]string) + } + result.FlagOverrides[key] = value + } + } + return result, nil +} diff --git a/runsc/fsgofer/extension/extension_test.go b/runsc/fsgofer/extension/extension_test.go index a888e924eb..1294751027 100644 --- a/runsc/fsgofer/extension/extension_test.go +++ b/runsc/fsgofer/extension/extension_test.go @@ -15,11 +15,13 @@ package extension import ( + "errors" "testing" specs "github.com/opencontainers/runtime-spec/specs-go" "gvisor.dev/gvisor/pkg/lisafs" "gvisor.dev/gvisor/pkg/seccomp" + "gvisor.dev/gvisor/runsc/flag" ) type fakeExtension struct { @@ -38,6 +40,29 @@ func (fakeExtension) SeccompRules() seccomp.SyscallRules { return seccomp.NewSyscallRules() } +type flagExtension struct { + fakeExtension + setFlagsSeen *bool +} + +func (f flagExtension) SetFlags(*flag.FlagSet) { + if f.setFlagsSeen != nil { + *f.setFlagsSeen = true + } +} + +type prepareExtension struct { + fakeExtension + prepareGofer func(GoferPrepareContext) (GoferPrepareResult, error) +} + +func (f prepareExtension) PrepareGofer(ctx GoferPrepareContext) (GoferPrepareResult, error) { + if f.prepareGofer == nil { + return GoferPrepareResult{}, nil + } + return f.prepareGofer(ctx) +} + func TestRegisterAndRegistered(t *testing.T) { registered = nil @@ -54,3 +79,60 @@ func TestRegisterAndRegistered(t *testing.T) { t.Fatalf("Registered() = %v, want [%v %v]", got, e1, e2) } } + +func TestSetFlags(t *testing.T) { + registered = nil + + called := false + Register(fakeExtension{name: "first"}) + Register(flagExtension{fakeExtension: fakeExtension{name: "second"}, setFlagsSeen: &called}) + SetFlags(&flag.FlagSet{}) + + if !called { + t.Fatal("SetFlags did not call extension") + } +} + +func TestPrepareGofer(t *testing.T) { + registered = nil + + Register(fakeExtension{name: "first"}) + Register(prepareExtension{ + fakeExtension: fakeExtension{name: "second"}, + prepareGofer: func(ctx GoferPrepareContext) (GoferPrepareResult, error) { + if ctx.ContainerID != "container" || ctx.BundleDir != "/bundle" { + t.Fatalf("GoferPrepareContext = %+v", ctx) + } + return GoferPrepareResult{FlagOverrides: map[string]string{"first-fd": "3"}}, nil + }, + }) + Register(prepareExtension{ + fakeExtension: fakeExtension{name: "third"}, + prepareGofer: func(GoferPrepareContext) (GoferPrepareResult, error) { + return GoferPrepareResult{FlagOverrides: map[string]string{"second-fd": "4"}}, nil + }, + }) + + got, err := PrepareGofer(GoferPrepareContext{ContainerID: "container", BundleDir: "/bundle"}) + if err != nil { + t.Fatalf("PrepareGofer: %v", err) + } + if got.FlagOverrides["first-fd"] != "3" || got.FlagOverrides["second-fd"] != "4" { + t.Fatalf("PrepareGofer overrides = %v", got.FlagOverrides) + } +} + +func TestPrepareGoferError(t *testing.T) { + registered = nil + want := errors.New("setup failed") + Register(prepareExtension{ + fakeExtension: fakeExtension{name: "first"}, + prepareGofer: func(GoferPrepareContext) (GoferPrepareResult, error) { + return GoferPrepareResult{}, want + }, + }) + + if _, err := PrepareGofer(GoferPrepareContext{}); !errors.Is(err, want) { + t.Fatalf("PrepareGofer error = %v, want %v", err, want) + } +}