Skip to content
Open
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
14 changes: 11 additions & 3 deletions g3doc/user_guide/filesystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,13 +291,21 @@ 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
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.
13 changes: 13 additions & 0 deletions runsc/cmd/gofer.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ func (g *Gofer) SetFlags(f *flag.FlagSet) {

// Profiling flags.
g.profileFDs.SetFromFlags(f)

extension.SetFlags(f)
}

// Execute implements subcommands.Command.
Expand Down Expand Up @@ -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() {
Expand Down
2 changes: 2 additions & 0 deletions runsc/fsgofer/extension/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ go_library(
deps = [
"//pkg/lisafs",
"//pkg/seccomp",
"//runsc/flag",
"@com_github_opencontainers_runtime_spec//specs-go:go_default_library",
],
)
Expand All @@ -24,6 +25,7 @@ go_test(
deps = [
"//pkg/lisafs",
"//pkg/seccomp",
"//runsc/flag",
"@com_github_opencontainers_runtime_spec//specs-go:go_default_library",
],
)
56 changes: 56 additions & 0 deletions runsc/fsgofer/extension/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
}
82 changes: 82 additions & 0 deletions runsc/fsgofer/extension/extension_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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

Expand All @@ -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)
}
}