aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOphestra Umiker <cat@ophivana.moe>2024-10-08 18:02:38 +0900
committerOphestra Umiker <cat@ophivana.moe>2024-10-08 18:02:38 +0900
commit7c7999e9e527bc245560ea6804a867276acaae6f (patch)
tree903be2bec90b79b9202f91ec4cede8c869abd4fb
parentc6223771dbc9cf8a4a44424759591adb496f2ce5 (diff)
helper: implementation of helper.Helper using bwrap
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
-rw-r--r--helper/bwrap.go144
-rw-r--r--helper/bwrap_test.go112
-rw-r--r--helper/helper_test.go6
-rw-r--r--helper/stub.go92
4 files changed, 340 insertions, 14 deletions
diff --git a/helper/bwrap.go b/helper/bwrap.go
new file mode 100644
index 00000000..4ac1032c
--- /dev/null
+++ b/helper/bwrap.go
@@ -0,0 +1,144 @@
+package helper
+
+import (
+ "errors"
+ "io"
+ "os/exec"
+ "strconv"
+ "sync"
+
+ "git.ophivana.moe/cat/fortify/helper/bwrap"
+)
+
+// BubblewrapName is the file name or path to bubblewrap.
+var BubblewrapName = "bwrap"
+
+type bubblewrap struct {
+ // bwrap child file name
+ name string
+
+ // bwrap pipes
+ p *pipes
+ // sealed bwrap config
+ config *bwrap.Config
+ // returns an array of arguments passed directly
+ // to the child process spawned by bwrap
+ argF func(argsFD, statFD int) []string
+
+ // pipes received by the child
+ // nil if no pipes are required
+ cp *pipes
+
+ lock sync.RWMutex
+ *exec.Cmd
+}
+
+func (b *bubblewrap) StartNotify(ready chan error) error {
+ b.lock.Lock()
+ defer b.lock.Unlock()
+
+ if ready != nil && b.cp == nil {
+ panic("attempted to start with status monitoring on a bwrap child initialised without pipes")
+ }
+
+ // Check for doubled Start calls before we defer failure cleanup. If the prior
+ // call to Start succeeded, we don't want to spuriously close its pipes.
+ if b.Cmd.Process != nil {
+ return errors.New("exec: already started")
+ }
+
+ // prepare bwrap pipe and args
+ if argsFD, _, err := b.p.prepareCmd(b.Cmd); err != nil {
+ return err
+ } else {
+ b.Cmd.Args = append(b.Cmd.Args, "--args", strconv.Itoa(argsFD), "--", b.name)
+ }
+
+ // prepare child args and pipes if enabled
+ if b.cp != nil {
+ b.cp.ready = ready
+ if argsFD, statFD, err := b.cp.prepareCmd(b.Cmd); err != nil {
+ return err
+ } else {
+ b.Cmd.Args = append(b.Cmd.Args, b.argF(argsFD, statFD)...)
+ }
+ } else {
+ b.Cmd.Args = append(b.Cmd.Args, b.argF(-1, -1)...)
+ }
+
+ if ready != nil {
+ b.Cmd.Env = append(b.Cmd.Env, FortifyHelper+"=1", FortifyStatus+"=1")
+ } else if b.cp != nil {
+ b.Cmd.Env = append(b.Cmd.Env, FortifyHelper+"=1", FortifyStatus+"=0")
+ } else {
+ b.Cmd.Env = append(b.Cmd.Env, FortifyHelper+"=1", FortifyStatus+"=-1")
+ }
+
+ if err := b.Cmd.Start(); err != nil {
+ return err
+ }
+
+ // write bwrap args first
+ if err := b.p.readyWriteArgs(); err != nil {
+ return err
+ }
+
+ // write child args if enabled
+ if b.cp != nil {
+ if err := b.cp.readyWriteArgs(); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (b *bubblewrap) Close() error {
+ if b.cp == nil {
+ panic("attempted to close bwrap child initialised without pipes")
+ }
+
+ return b.cp.closeStatus()
+}
+
+func (b *bubblewrap) Start() error {
+ return b.StartNotify(nil)
+}
+
+func (b *bubblewrap) Unwrap() *exec.Cmd {
+ return b.Cmd
+}
+
+// MustNewBwrap initialises a new Bwrap instance with wt as the null-terminated argument writer.
+// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
+// Function argF returns an array of arguments passed directly to the child process.
+func MustNewBwrap(conf *bwrap.Config, wt io.WriterTo, name string, argF func(argsFD, statFD int) []string) Helper {
+ b, err := NewBwrap(conf, wt, name, argF)
+ if err != nil {
+ panic(err.Error())
+ } else {
+ return b
+ }
+}
+
+// NewBwrap initialises a new Bwrap instance with wt as the null-terminated argument writer.
+// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
+// Function argF returns an array of arguments passed directly to the child process.
+func NewBwrap(conf *bwrap.Config, wt io.WriterTo, name string, argF func(argsFD, statFD int) []string) (Helper, error) {
+ b := new(bubblewrap)
+
+ if args, err := NewCheckedArgs(conf.Args()); err != nil {
+ return nil, err
+ } else {
+ b.p = &pipes{args: args}
+ }
+
+ b.argF = argF
+ b.name = name
+ if wt != nil {
+ b.cp = &pipes{args: wt}
+ }
+ b.Cmd = execCommand(BubblewrapName)
+
+ return b, nil
+}
diff --git a/helper/bwrap_test.go b/helper/bwrap_test.go
new file mode 100644
index 00000000..8fd2d4be
--- /dev/null
+++ b/helper/bwrap_test.go
@@ -0,0 +1,112 @@
+package helper_test
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ "git.ophivana.moe/cat/fortify/helper"
+ "git.ophivana.moe/cat/fortify/helper/bwrap"
+)
+
+func TestBwrap(t *testing.T) {
+ sc := &bwrap.Config{
+ Unshare: nil,
+ Net: true,
+ UserNS: false,
+ Hostname: "localhost",
+ Chdir: "/nonexistent",
+ Clearenv: true,
+ NewSession: true,
+ DieWithParent: true,
+ AsInit: true,
+ }
+
+ t.Run("nonexistent bwrap name", func(t *testing.T) {
+ bubblewrapName := helper.BubblewrapName
+ helper.BubblewrapName = "/nonexistent"
+ t.Cleanup(func() {
+ helper.BubblewrapName = bubblewrapName
+ })
+
+ h := helper.MustNewBwrap(sc, argsWt, "fortify", argF)
+
+ if err := h.Start(); !errors.Is(err, os.ErrNotExist) {
+ t.Errorf("Start() error = %v, wantErr %v",
+ err, os.ErrNotExist)
+ }
+ })
+
+ t.Run("valid new helper nil check", func(t *testing.T) {
+ if got := helper.MustNewBwrap(sc, argsWt, "fortify", argF); got == nil {
+ t.Errorf("MustNewBwrap(%#v, %#v, %#v) got nil",
+ sc, argsWt, "fortify")
+ return
+ }
+ })
+
+ t.Run("invalid bwrap config new helper panic", func(t *testing.T) {
+ defer func() {
+ wantPanic := "argument contains null character"
+ if r := recover(); r != wantPanic {
+ t.Errorf("MustNewBwrap: panic = %q, want %q",
+ r, wantPanic)
+ }
+ }()
+
+ helper.MustNewBwrap(&bwrap.Config{Hostname: "\x00"}, nil, "fortify", argF)
+ })
+
+ t.Run("start notify without pipes panic", func(t *testing.T) {
+ defer func() {
+ wantPanic := "attempted to start with status monitoring on a bwrap child initialised without pipes"
+ if r := recover(); r != wantPanic {
+ t.Errorf("StartNotify: panic = %q, want %q",
+ r, wantPanic)
+ }
+ }()
+
+ panic(fmt.Sprintf("unreachable: %v",
+ helper.MustNewBwrap(sc, nil, "fortify", argF).StartNotify(make(chan error))))
+ })
+
+ t.Run("start without pipes", func(t *testing.T) {
+ helper.InternalReplaceExecCommand(t)
+
+ h := helper.MustNewBwrap(sc, nil, "crash-test-dummy", argFChecked)
+ cmd := h.Unwrap()
+
+ stdout, stderr := new(strings.Builder), new(strings.Builder)
+ cmd.Stdout, cmd.Stderr = stdout, stderr
+
+ t.Run("close without pipes panic", func(t *testing.T) {
+ defer func() {
+ wantPanic := "attempted to close bwrap child initialised without pipes"
+ if r := recover(); r != wantPanic {
+ t.Errorf("Close: panic = %q, want %q",
+ r, wantPanic)
+ }
+ }()
+
+ panic(fmt.Sprintf("unreachable: %v",
+ h.Close()))
+ })
+
+ if err := h.Start(); err != nil {
+ t.Errorf("Start() error = %v",
+ err)
+ return
+ }
+
+ if err := h.Wait(); err != nil {
+ t.Errorf("Wait() err = %v stderr = %s",
+ err, stderr)
+ }
+ })
+
+ t.Run("implementation compliance", func(t *testing.T) {
+ testHelper(t, func() helper.Helper { return helper.MustNewBwrap(sc, argsWt, "crash-test-dummy", argF) })
+ })
+}
diff --git a/helper/helper_test.go b/helper/helper_test.go
index 23cff669..da1dae90 100644
--- a/helper/helper_test.go
+++ b/helper/helper_test.go
@@ -23,11 +23,15 @@ var (
argsWt = helper.MustNewCheckedArgs(wantArgs)
)
-func argF(argsFD int, statFD int) []string {
+func argF(argsFD, statFD int) []string {
if argsFD == -1 {
panic("invalid args fd")
}
+ return argFChecked(argsFD, statFD)
+}
+
+func argFChecked(argsFD, statFD int) []string {
if statFD == -1 {
return []string{"--args", strconv.Itoa(argsFD)}
} else {
diff --git a/helper/stub.go b/helper/stub.go
index b6b5d72c..78559b5e 100644
--- a/helper/stub.go
+++ b/helper/stub.go
@@ -6,15 +6,19 @@ import (
"os"
"os/exec"
"strconv"
+ "strings"
"syscall"
"testing"
+
+ "git.ophivana.moe/cat/fortify/helper/bwrap"
)
// InternalChildStub is an internal function but exported because it is cross-package;
// it is part of the implementation of the helper stub.
func InternalChildStub() {
// this test mocks the helper process
- if os.Getenv(FortifyHelper) != "1" {
+ if os.Getenv(FortifyHelper) != "1" ||
+ os.Getenv(FortifyStatus) == "-1" { // this indicates the stub is being invoked as a bwrap child without pipes
return
}
@@ -22,6 +26,35 @@ func InternalChildStub() {
statFD := flag.Int("fd", -1, "")
_ = flag.CommandLine.Parse(os.Args[4:])
+ switch os.Args[3] {
+ case "bwrap":
+ bwrapStub(argsFD, statFD)
+ default:
+ genericStub(argsFD, statFD)
+ }
+
+ os.Exit(0)
+}
+
+// InternalReplaceExecCommand is an internal function but exported because it is cross-package;
+// it is part of the implementation of the helper stub.
+func InternalReplaceExecCommand(t *testing.T) {
+ t.Cleanup(func() {
+ execCommand = exec.Command
+ })
+
+ // replace execCommand to have the resulting *exec.Cmd launch TestHelperChildStub
+ execCommand = func(name string, arg ...string) *exec.Cmd {
+ // pass through nonexistent path
+ if name == "/nonexistent" && len(arg) == 0 {
+ return exec.Command(name)
+ }
+
+ return exec.Command(os.Args[0], append([]string{"-test.run=TestHelperChildStub", "--", name}, arg...)...)
+ }
+}
+
+func genericStub(argsFD, statFD *int) {
// simulate args pipe behaviour
func() {
if *argsFD == -1 {
@@ -89,20 +122,53 @@ func InternalChildStub() {
}
}
-// InternalReplaceExecCommand is an internal function but exported because it is cross-package;
-// it is part of the implementation of the helper stub.
-func InternalReplaceExecCommand(t *testing.T) {
- t.Cleanup(func() {
- execCommand = exec.Command
- })
+func bwrapStub(argsFD, statFD *int) {
+ // the bwrap launcher does not ever launch with sync fd
+ if *statFD != -1 {
+ panic("attempted to launch bwrap with status monitoring")
+ }
- // replace execCommand to have the resulting *exec.Cmd launch TestHelperChildStub
- execCommand = func(name string, arg ...string) *exec.Cmd {
- // pass through nonexistent path
- if name == "/nonexistent" && len(arg) == 0 {
- return exec.Command(name)
+ // test args pipe behaviour
+ func() {
+ if *argsFD == -1 {
+ panic("attempted to start bwrap without passing args pipe fd")
}
- return exec.Command(os.Args[0], append([]string{"-test.run=TestHelperChildStub", "--", name}, arg...)...)
+ f := os.NewFile(uintptr(*argsFD), "|0")
+ if f == nil {
+ panic("attempted to start helper without args pipe")
+ }
+
+ got, want := new(strings.Builder), new(strings.Builder)
+
+ if _, err := io.Copy(got, f); err != nil {
+ panic("cannot read args: " + err.Error())
+ }
+
+ // hardcoded bwrap configuration used by test
+ if _, err := MustNewCheckedArgs((&bwrap.Config{
+ Unshare: nil,
+ Net: true,
+ UserNS: false,
+ Hostname: "localhost",
+ Chdir: "/nonexistent",
+ Clearenv: true,
+ NewSession: true,
+ DieWithParent: true,
+ AsInit: true,
+ }).Args()).WriteTo(want); err != nil {
+ panic("cannot read want: " + err.Error())
+ }
+
+ if got.String() != want.String() {
+ panic("bad bwrap args\ngot: " + got.String() + "\nwant: " + want.String())
+ }
+ }()
+
+ if err := syscall.Exec(
+ os.Args[0],
+ append([]string{os.Args[0], "-test.run=TestHelperChildStub", "--"}, flag.CommandLine.Args()...),
+ os.Environ()); err != nil {
+ panic("cannot start general stub: " + err.Error())
}
}