helper: clean up interface
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m37s
Test / Fpkg (push) Successful in 3m40s
Test / Data race detector (push) Successful in 3m54s
Test / Flake checks (push) Successful in 59s

The helper interface was messy due to odd context acquisition order. That has changed, so this cleans it up.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-03-15 00:27:44 +09:00
parent 9e18d1de77
commit f443d315ad
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
8 changed files with 95 additions and 87 deletions

View File

@ -39,9 +39,14 @@ func (p *Proxy) Start(ctx context.Context, output io.Writer, sandbox bool) error
c, cancel := context.WithCancelCause(ctx) c, cancel := context.WithCancelCause(ctx)
if !sandbox { if !sandbox {
h = helper.New(c, p.seal, p.name, argF) h = helper.NewDirect(c, p.seal, p.name, argF, func(cmd *exec.Cmd) {
// xdg-dbus-proxy does not need to inherit the environment if output != nil {
h.SetEnv(make([]string, 0)) cmd.Stdout, cmd.Stderr = output, output
}
// xdg-dbus-proxy does not need to inherit the environment
cmd.Env = make([]string, 0)
}, true)
} else { } else {
// look up absolute path if name is just a file name // look up absolute path if name is just a file name
toolPath := p.name toolPath := p.name
@ -111,14 +116,15 @@ func (p *Proxy) Start(ctx context.Context, output io.Writer, sandbox bool) error
bc.Bind(k, k) bc.Bind(k, k)
} }
h = helper.MustNewBwrap(c, bc, toolPath, true, p.seal, argF, nil, nil) h = helper.MustNewBwrap(c, bc, toolPath, true, p.seal, argF, func(cmd *exec.Cmd) {
if output != nil {
cmd.Stdout, cmd.Stderr = output, output
}
}, nil, nil, true)
p.bwrap = bc p.bwrap = bc
} }
if output != nil { if err := h.Start(); err != nil {
h.SetStdout(output).SetStderr(output)
}
if err := h.Start(true); err != nil {
cancel(err) cancel(err)
return err return err
} }

View File

@ -5,6 +5,7 @@ import (
"errors" "errors"
"io" "io"
"os" "os"
"os/exec"
"slices" "slices"
"strconv" "strconv"
"sync" "sync"
@ -24,14 +25,11 @@ type bubblewrap struct {
// name of the command to run in bwrap // name of the command to run in bwrap
name string name string
// whether to set process group id
setpgid bool
lock sync.RWMutex lock sync.RWMutex
*helperCmd *helperCmd
} }
func (b *bubblewrap) Start(stat bool) error { func (b *bubblewrap) Start() error {
b.lock.Lock() b.lock.Lock()
defer b.lock.Unlock() defer b.lock.Unlock()
@ -41,7 +39,7 @@ func (b *bubblewrap) Start(stat bool) error {
return errors.New("exec: already started") return errors.New("exec: already started")
} }
args := b.finalise(stat) args := b.finalise()
b.Cmd.Args = slices.Grow(b.Cmd.Args, 4+len(args)) b.Cmd.Args = slices.Grow(b.Cmd.Args, 4+len(args))
b.Cmd.Args = append(b.Cmd.Args, "--args", strconv.Itoa(int(b.argsFd)), "--", b.name) b.Cmd.Args = append(b.Cmd.Args, "--args", strconv.Itoa(int(b.argsFd)), "--", b.name)
b.Cmd.Args = append(b.Cmd.Args, args...) b.Cmd.Args = append(b.Cmd.Args, args...)
@ -53,12 +51,17 @@ func (b *bubblewrap) Start(stat bool) error {
// Function argF returns an array of arguments passed directly to the child process. // Function argF returns an array of arguments passed directly to the child process.
func MustNewBwrap( func MustNewBwrap(
ctx context.Context, ctx context.Context,
conf *bwrap.Config, name string, setpgid bool, conf *bwrap.Config,
wt io.WriterTo, argF func(argsFD, statFD int) []string, name string,
setpgid bool,
wt io.WriterTo,
argF func(argsFD, statFD int) []string,
cmdF func(cmd *exec.Cmd),
extraFiles []*os.File, extraFiles []*os.File,
syncFd *os.File, syncFd *os.File,
stat bool,
) Helper { ) Helper {
b, err := NewBwrap(ctx, conf, name, setpgid, wt, argF, extraFiles, syncFd) b, err := NewBwrap(ctx, conf, name, setpgid, wt, argF, cmdF, extraFiles, syncFd, stat)
if err != nil { if err != nil {
panic(err.Error()) panic(err.Error())
} else { } else {
@ -71,19 +74,26 @@ func MustNewBwrap(
// Function argF returns an array of arguments passed directly to the child process. // Function argF returns an array of arguments passed directly to the child process.
func NewBwrap( func NewBwrap(
ctx context.Context, ctx context.Context,
conf *bwrap.Config, name string, setpgid bool, conf *bwrap.Config,
wt io.WriterTo, argF func(argsFd, statFd int) []string, name string,
setpgid bool,
wt io.WriterTo,
argF func(argsFd, statFd int) []string,
cmdF func(cmd *exec.Cmd),
extraFiles []*os.File, extraFiles []*os.File,
syncFd *os.File, syncFd *os.File,
stat bool,
) (Helper, error) { ) (Helper, error) {
b := new(bubblewrap) b := new(bubblewrap)
b.name = name b.name = name
b.setpgid = setpgid b.helperCmd = newHelperCmd(ctx, BubblewrapName, wt, argF, extraFiles, stat)
b.helperCmd = newHelperCmd(b, ctx, BubblewrapName, wt, argF, extraFiles) if setpgid {
if b.setpgid {
b.Cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} b.Cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
} }
if cmdF != nil {
cmdF(b.helperCmd.Cmd)
}
if v, err := NewCheckedArgs(conf.Args(syncFd, b.extraFiles, &b.files)); err != nil { if v, err := NewCheckedArgs(conf.Args(syncFd, b.extraFiles, &b.files)); err != nil {
return nil, err return nil, err

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"os" "os"
"os/exec"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -33,11 +34,11 @@ func TestBwrap(t *testing.T) {
h := helper.MustNewBwrap( h := helper.MustNewBwrap(
context.Background(), context.Background(),
sc, "fortify", false, sc, "fortify", false,
argsWt, argF, argsWt, argF, nil,
nil, nil, nil, nil, false,
) )
if err := h.Start(false); !errors.Is(err, os.ErrNotExist) { if err := h.Start(); !errors.Is(err, os.ErrNotExist) {
t.Errorf("Start: error = %v, wantErr %v", t.Errorf("Start: error = %v, wantErr %v",
err, os.ErrNotExist) err, os.ErrNotExist)
} }
@ -47,8 +48,8 @@ func TestBwrap(t *testing.T) {
if got := helper.MustNewBwrap( if got := helper.MustNewBwrap(
context.TODO(), context.TODO(),
sc, "fortify", false, sc, "fortify", false,
argsWt, argF, argsWt, argF, nil,
nil, nil, nil, nil, false,
); got == nil { ); got == nil {
t.Errorf("MustNewBwrap(%#v, %#v, %#v) got nil", t.Errorf("MustNewBwrap(%#v, %#v, %#v) got nil",
sc, argsWt, "fortify") sc, argsWt, "fortify")
@ -68,8 +69,8 @@ func TestBwrap(t *testing.T) {
helper.MustNewBwrap( helper.MustNewBwrap(
context.TODO(), context.TODO(),
&bwrap.Config{Hostname: "\x00"}, "fortify", false, &bwrap.Config{Hostname: "\x00"}, "fortify", false,
nil, argF, nil, argF, nil,
nil, nil, nil, nil, false,
) )
}) })
@ -78,17 +79,14 @@ func TestBwrap(t *testing.T) {
c, cancel := context.WithTimeout(context.Background(), 5*time.Second) c, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
stdout, stderr := new(strings.Builder), new(strings.Builder)
h := helper.MustNewBwrap( h := helper.MustNewBwrap(
c, c, sc, "crash-test-dummy", false,
sc, "crash-test-dummy", false, nil, argFChecked, func(cmd *exec.Cmd) { cmd.Stdout, cmd.Stderr = stdout, stderr },
nil, argFChecked, nil, nil, false,
nil, nil,
) )
stdout, stderr := new(strings.Builder), new(strings.Builder) if err := h.Start(); err != nil {
h.SetStdout(stdout).SetStderr(stderr)
if err := h.Start(false); err != nil {
t.Errorf("Start: error = %v", t.Errorf("Start: error = %v",
err) err)
return return
@ -101,11 +99,10 @@ func TestBwrap(t *testing.T) {
}) })
t.Run("implementation compliance", func(t *testing.T) { t.Run("implementation compliance", func(t *testing.T) {
testHelper(t, func(ctx context.Context) helper.Helper { testHelper(t, func(ctx context.Context, cmdF func(cmd *exec.Cmd), stat bool) helper.Helper {
return helper.MustNewBwrap( return helper.MustNewBwrap(
ctx, ctx, sc, "crash-test-dummy", false,
sc, "crash-test-dummy", false, argsWt, argF, cmdF, nil, nil, stat,
argsWt, argF, nil, nil,
) )
}) })
}) })

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"io" "io"
"os/exec"
"sync" "sync"
"git.gensokyo.uk/security/fortify/helper/proc" "git.gensokyo.uk/security/fortify/helper/proc"
@ -15,7 +16,7 @@ type direct struct {
*helperCmd *helperCmd
} }
func (h *direct) Start(stat bool) error { func (h *direct) Start() error {
h.lock.Lock() h.lock.Lock()
defer h.lock.Unlock() defer h.lock.Unlock()
@ -25,15 +26,25 @@ func (h *direct) Start(stat bool) error {
return errors.New("exec: already started") return errors.New("exec: already started")
} }
args := h.finalise(stat) args := h.finalise()
h.Cmd.Args = append(h.Cmd.Args, args...) h.Cmd.Args = append(h.Cmd.Args, args...)
return proc.Fulfill(h.ctx, &h.ExtraFiles, h.Cmd.Start, h.files, h.extraFiles) return proc.Fulfill(h.ctx, &h.ExtraFiles, h.Cmd.Start, h.files, h.extraFiles)
} }
// New initialises a new direct Helper instance with wt as the null-terminated argument writer. // NewDirect initialises a new direct Helper instance with wt as the null-terminated argument writer.
// Function argF returns an array of arguments passed directly to the child process. // Function argF returns an array of arguments passed directly to the child process.
func New(ctx context.Context, wt io.WriterTo, name string, argF func(argsFd, statFd int) []string) Helper { func NewDirect(
ctx context.Context,
wt io.WriterTo,
name string,
argF func(argsFd, statFd int) []string,
cmdF func(cmd *exec.Cmd),
stat bool,
) Helper {
d := new(direct) d := new(direct)
d.helperCmd = newHelperCmd(d, ctx, name, wt, argF, nil) d.helperCmd = newHelperCmd(ctx, name, wt, argF, nil, stat)
if cmdF != nil {
cmdF(d.helperCmd.Cmd)
}
return d return d
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"os" "os"
"os/exec"
"testing" "testing"
"git.gensokyo.uk/security/fortify/helper" "git.gensokyo.uk/security/fortify/helper"
@ -11,16 +12,16 @@ import (
func TestDirect(t *testing.T) { func TestDirect(t *testing.T) {
t.Run("start non-existent helper path", func(t *testing.T) { t.Run("start non-existent helper path", func(t *testing.T) {
h := helper.New(context.Background(), argsWt, "/nonexistent", argF) h := helper.NewDirect(context.Background(), argsWt, "/nonexistent", argF, nil, false)
if err := h.Start(false); !errors.Is(err, os.ErrNotExist) { if err := h.Start(); !errors.Is(err, os.ErrNotExist) {
t.Errorf("Start: error = %v, wantErr %v", t.Errorf("Start: error = %v, wantErr %v",
err, os.ErrNotExist) err, os.ErrNotExist)
} }
}) })
t.Run("valid new helper nil check", func(t *testing.T) { t.Run("valid new helper nil check", func(t *testing.T) {
if got := helper.New(context.TODO(), argsWt, "fortify", argF); got == nil { if got := helper.NewDirect(context.TODO(), argsWt, "fortify", argF, nil, false); got == nil {
t.Errorf("New(%q, %q) got nil", t.Errorf("New(%q, %q) got nil",
argsWt, "fortify") argsWt, "fortify")
return return
@ -28,6 +29,8 @@ func TestDirect(t *testing.T) {
}) })
t.Run("implementation compliance", func(t *testing.T) { t.Run("implementation compliance", func(t *testing.T) {
testHelper(t, func(ctx context.Context) helper.Helper { return helper.New(ctx, argsWt, "crash-test-dummy", argF) }) testHelper(t, func(ctx context.Context, cmdF func(cmd *exec.Cmd), stat bool) helper.Helper {
return helper.NewDirect(ctx, argsWt, "crash-test-dummy", argF, cmdF, stat)
})
}) })
} }

View File

@ -26,32 +26,22 @@ const (
) )
type Helper interface { type Helper interface {
// SetStdin sets the standard input of Helper.
SetStdin(r io.Reader) Helper
// SetStdout sets the standard output of Helper.
SetStdout(w io.Writer) Helper
// SetStderr sets the standard error of Helper.
SetStderr(w io.Writer) Helper
// SetEnv sets the environment of Helper.
SetEnv(env []string) Helper
// Start starts the helper process. // Start starts the helper process.
// A status pipe is passed to the helper if stat is true. Start() error
Start(stat bool) error // Wait blocks until Helper exits.
// Wait blocks until Helper exits and releases all its resources.
Wait() error Wait() error
fmt.Stringer fmt.Stringer
} }
func newHelperCmd( func newHelperCmd(
h Helper, ctx context.Context, name string, ctx context.Context, name string,
wt io.WriterTo, argF func(argsFd, statFd int) []string, wt io.WriterTo, argF func(argsFd, statFd int) []string,
extraFiles []*os.File, extraFiles []*os.File, stat bool,
) (cmd *helperCmd) { ) (cmd *helperCmd) {
cmd = new(helperCmd) cmd = new(helperCmd)
cmd.r = h
cmd.ctx = ctx cmd.ctx = ctx
cmd.hasStatFd = stat
cmd.Cmd = commandContext(ctx, name) cmd.Cmd = commandContext(ctx, name)
cmd.Cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGTERM) } cmd.Cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGTERM) }
@ -77,14 +67,13 @@ func newHelperCmd(
// helperCmd wraps Cmd and implements methods shared across all Helper implementations. // helperCmd wraps Cmd and implements methods shared across all Helper implementations.
type helperCmd struct { type helperCmd struct {
// ref to parent
r Helper
// returns an array of arguments passed directly // returns an array of arguments passed directly
// to the helper process // to the helper process
argF func(statFd int) []string argF func(statFd int) []string
// whether argsFd is present // whether argsFd is present
hasArgsFd bool hasArgsFd bool
// whether statFd is present
hasStatFd bool
// closes statFd // closes statFd
stat io.Closer stat io.Closer
@ -97,13 +86,8 @@ type helperCmd struct {
*exec.Cmd *exec.Cmd
} }
func (h *helperCmd) SetStdin(r io.Reader) Helper { h.Stdin = r; return h.r }
func (h *helperCmd) SetStdout(w io.Writer) Helper { h.Stdout = w; return h.r }
func (h *helperCmd) SetStderr(w io.Writer) Helper { h.Stderr = w; return h.r }
func (h *helperCmd) SetEnv(env []string) Helper { h.Env = env; return h.r }
// finalise sets up the underlying [exec.Cmd] object. // finalise sets up the underlying [exec.Cmd] object.
func (h *helperCmd) finalise(stat bool) (args []string) { func (h *helperCmd) finalise() (args []string) {
h.Env = slices.Grow(h.Env, 2) h.Env = slices.Grow(h.Env, 2)
if h.hasArgsFd { if h.hasArgsFd {
h.Cmd.Env = append(h.Env, FortifyHelper+"=1") h.Cmd.Env = append(h.Env, FortifyHelper+"=1")
@ -112,7 +96,7 @@ func (h *helperCmd) finalise(stat bool) (args []string) {
} }
statFd := -1 statFd := -1
if stat { if h.hasStatFd {
f := proc.NewStat(&h.stat) f := proc.NewStat(&h.stat)
statFd = int(proc.InitFile(f, h.extraFiles)) statFd = int(proc.InitFile(f, h.extraFiles))
h.files = append(h.files, f) h.files = append(h.files, f)

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"os/exec"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
@ -46,15 +47,13 @@ func argFChecked(argsFd, statFd int) (args []string) {
} }
// this function tests an implementation of the helper.Helper interface // this function tests an implementation of the helper.Helper interface
func testHelper(t *testing.T, createHelper func(ctx context.Context) helper.Helper) { func testHelper(t *testing.T, createHelper func(ctx context.Context, cmdF func(cmd *exec.Cmd), stat bool) helper.Helper) {
helper.InternalReplaceExecCommand(t) helper.InternalReplaceExecCommand(t)
t.Run("start helper with status channel and wait", func(t *testing.T) { t.Run("start helper with status channel and wait", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
h := createHelper(ctx)
stdout, stderr := new(strings.Builder), new(strings.Builder) stdout, stderr := new(strings.Builder), new(strings.Builder)
h.SetStdout(stdout).SetStderr(stderr) h := createHelper(ctx, func(cmd *exec.Cmd) { cmd.Stdout, cmd.Stderr = stdout, stderr }, true)
t.Run("wait not yet started helper", func(t *testing.T) { t.Run("wait not yet started helper", func(t *testing.T) {
defer func() { defer func() {
@ -67,7 +66,7 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context) helper.Help
}) })
t.Log("starting helper stub") t.Log("starting helper stub")
if err := h.Start(true); err != nil { if err := h.Start(); err != nil {
t.Errorf("Start: error = %v", err) t.Errorf("Start: error = %v", err)
cancel() cancel()
return return
@ -77,7 +76,7 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context) helper.Help
t.Run("start already started helper", func(t *testing.T) { t.Run("start already started helper", func(t *testing.T) {
wantErr := "exec: already started" wantErr := "exec: already started"
if err := h.Start(true); err != nil && err.Error() != wantErr { if err := h.Start(); err != nil && err.Error() != wantErr {
t.Errorf("Start: error = %v, wantErr %v", t.Errorf("Start: error = %v, wantErr %v",
err, wantErr) err, wantErr)
return return
@ -108,12 +107,10 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context) helper.Help
t.Run("start helper and wait", func(t *testing.T) { t.Run("start helper and wait", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
h := createHelper(ctx)
stdout, stderr := new(strings.Builder), new(strings.Builder) stdout, stderr := new(strings.Builder), new(strings.Builder)
h.SetStdout(stdout).SetStderr(stderr) h := createHelper(ctx, func(cmd *exec.Cmd) { cmd.Stdout, cmd.Stderr = stdout, stderr }, false)
if err := h.Start(false); err != nil { if err := h.Start(); err != nil {
t.Errorf("Start() error = %v", t.Errorf("Start() error = %v",
err) err)
return return

View File

@ -131,15 +131,15 @@ func Main() {
ctx, ctx,
conf, path.Join(fst.Tmp, "sbin/init0"), false, conf, path.Join(fst.Tmp, "sbin/init0"), false,
nil, func(int, int) []string { return make([]string, 0) }, nil, func(int, int) []string { return make([]string, 0) },
func(cmd *exec.Cmd) { cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr },
extraFiles, extraFiles,
syncFd, syncFd,
false,
); err != nil { ); err != nil {
log.Fatalf("malformed sandbox config: %v", err) log.Fatalf("malformed sandbox config: %v", err)
} else { } else {
b.SetStdin(os.Stdin).SetStdout(os.Stdout).SetStderr(os.Stderr)
// run and pass through exit code // run and pass through exit code
if err = b.Start(false); err != nil { if err = b.Start(); err != nil {
log.Fatalf("cannot start target process: %v", err) log.Fatalf("cannot start target process: %v", err)
} else if err = b.Wait(); err != nil { } else if err = b.Wait(); err != nil {
var exitError *exec.ExitError var exitError *exec.ExitError