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 24583a5bbd..48e20a1cd1 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) + } +}