Compare commits

...

18 Commits

Author SHA1 Message Date
82b28c589a
release: 0.2.13
Some checks failed
Release / Create release (push) Failing after 1m51s
Test / Create distribution (push) Successful in 1m43s
Test / Run NixOS test (push) Successful in 4m32s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-13 23:39:29 +09:00
fe7d208cf7
helper: use generic extra files interface
All checks were successful
Test / Create distribution (push) Successful in 1m38s
Test / Run NixOS test (push) Successful in 4m36s
This replaces the pipes object and integrates context into helper process lifecycle.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-13 23:34:15 +09:00
60c2873750
helper/proc: cancel ec on parent ctx
All checks were successful
Test / Create distribution (push) Successful in 1m31s
Test / Run NixOS test (push) Successful in 4m13s
This allows errors written during a timeout to be received and handled.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-13 23:08:28 +09:00
d1d20c06fb
helper/seccomp: use sync.Once for closeWrite
All checks were successful
Test / Create distribution (push) Successful in 1m29s
Test / Run NixOS test (push) Successful in 4m13s
This makes the code much cleaner, and eliminates the intermittent ErrInvalid errors.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-13 22:49:16 +09:00
1e6a059668
helper/seccomp: benchmark exporter
All checks were successful
Test / Create distribution (push) Successful in 1m44s
Test / Run NixOS test (push) Successful in 4m32s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-13 22:37:51 +09:00
318df0f7e1
nix: test syscall filter
All checks were successful
Test / Create distribution (push) Successful in 1m30s
Test / Run NixOS test (push) Successful in 4m17s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-13 22:01:16 +09:00
58eb8f971d
proc/pipe: implement args and stat file
All checks were successful
Test / Create distribution (push) Successful in 1m30s
Test / Run NixOS test (push) Successful in 4m11s
This is a generic implementation of helper/pipe.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-13 19:57:24 +09:00
0a1d7c01cd
helper/proc: count dispatched errs
All checks were successful
Test / Create distribution (push) Successful in 1m28s
Test / Run NixOS test (push) Successful in 3m59s
This helps debug implementation errors of [proc.File].

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-13 19:55:37 +09:00
60ca1c6c55
helper/proc: store file addresses in linked list
All checks were successful
Test / Create distribution (push) Successful in 1m28s
Test / Run NixOS test (push) Successful in 4m5s
Storing extra files as a slice requires the caller to allocate a large enough slice before initialising any file and never grow the slice.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-13 17:42:12 +09:00
099da78af5
helper/seccomp: eliminate data race on pfd
All checks were successful
Test / Create distribution (push) Successful in 2m10s
Test / Run NixOS test (push) Successful in 4m50s
Turns out the doc comment on os.File was lying about its methods being safe for concurrent use. The race detector picked up a data race from concurrent use of Fd and Close.

This change eliminates that by calling Fd in the prepare routine.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-13 10:40:51 +09:00
18466cfd02
helper/proc: declare generic extra files interface
All checks were successful
Test / Create distribution (push) Successful in 1m29s
Test / Run NixOS test (push) Successful in 4m4s
Helpers use extra files for various purposes. This provides a generic interface for implementing the fulfillment of these extra files without having to specifically handle them in the process creation code.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-11 16:34:47 +09:00
e14923ae53
helper/proc: move package out of internal
All checks were successful
Test / Create distribution (push) Successful in 1m32s
Test / Run NixOS test (push) Successful in 4m6s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-08 13:03:45 +09:00
7aff3ead3a
nix: vm test remove unnecessary setup
All checks were successful
Test / Create distribution (push) Successful in 1m27s
Test / Run NixOS test (push) Successful in 4m10s
This step is no longer required as the NixOS module is responsible for home directory creation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-07 22:29:56 +09:00
72fb13dccc
dbus: lock for read in public args interface
All checks were successful
Test / Create distribution (push) Successful in 1m27s
Test / Run NixOS test (push) Successful in 4m2s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-07 13:42:29 +09:00
a48386bd56
system/dbus: dump messages on early fault
All checks were successful
Test / Create distribution (push) Successful in 1m27s
Test / Run NixOS test (push) Successful in 4m14s
In the current app implementation this gets dumped in the wait method after resuming output. Wait is never called in an early fault condition, so any error messages get lost.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-07 13:20:56 +09:00
2e52191404
system/dbus: dump method prints msgbuf
All checks were successful
Test / Create distribution (push) Successful in 1m27s
Test / Run NixOS test (push) Successful in 4m1s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-07 13:16:54 +09:00
568d7758d5
helper/seccomp: panic on invalid closeWrite use
All checks were successful
Test / Create distribution (push) Successful in 1m46s
Test / Run NixOS test (push) Successful in 4m39s
Returning an error here puts exporter in an invalid state. The caller should guard against this condition instead.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-07 12:58:20 +09:00
5b7b3fa9a4
helper/seccomp: implement reader interface via pipe
All checks were successful
Test / Create distribution (push) Successful in 1m6s
Test / Run NixOS test (push) Successful in 2m44s
This also does not require the libc tmpfile call.

BPF programs emitted by libseccomp seems to be deterministic. The tests would catch regressions as it verifies the program against known good output backed by manual testing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-03 19:43:03 +09:00
43 changed files with 1049 additions and 892 deletions

View File

@ -1,9 +1,11 @@
package dbus_test package dbus_test
import ( import (
"context"
"errors" "errors"
"strings" "strings"
"testing" "testing"
"time"
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/helper" "git.gensokyo.uk/security/fortify/helper"
@ -141,7 +143,7 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
t.Run("unsealed start of "+id, func(t *testing.T) { t.Run("unsealed start of "+id, func(t *testing.T) {
want := "proxy not sealed" want := "proxy not sealed"
if err := p.Start(nil, nil, sandbox, false); err == nil || err.Error() != want { if err := p.Start(context.Background(), nil, sandbox); err == nil || err.Error() != want {
t.Errorf("Start() error = %v, wantErr %q", t.Errorf("Start() error = %v, wantErr %q",
err, errors.New(want)) err, errors.New(want))
return return
@ -149,7 +151,7 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
}) })
t.Run("unsealed wait of "+id, func(t *testing.T) { t.Run("unsealed wait of "+id, func(t *testing.T) {
wantErr := "proxy not started" wantErr := "dbus: not started"
if err := p.Wait(); err == nil || err.Error() != wantErr { if err := p.Wait(); err == nil || err.Error() != wantErr {
t.Errorf("Wait() error = %v, wantErr %v", t.Errorf("Wait() error = %v, wantErr %v",
err, wantErr) err, wantErr)
@ -175,7 +177,10 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
} }
t.Run("sealed start of "+id, func(t *testing.T) { t.Run("sealed start of "+id, func(t *testing.T) {
if err := p.Start(nil, output, sandbox, false); err != nil { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := p.Start(ctx, output, sandbox); err != nil {
t.Fatalf("Start(nil, nil) error = %v", t.Fatalf("Start(nil, nil) error = %v",
err) err)
} }
@ -189,22 +194,8 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
} }
}) })
t.Run("sealed closing of "+id+" without status", func(t *testing.T) {
wantPanic := "attempted to close helper with no status pipe"
defer func() {
if r := recover(); r != wantPanic {
t.Errorf("Close() panic = %v, wantPanic %v",
r, wantPanic)
}
}()
if err := p.Close(); err != nil {
t.Errorf("Close() error = %v",
err)
}
})
t.Run("started wait of "+id, func(t *testing.T) { t.Run("started wait of "+id, func(t *testing.T) {
p.Close()
if err := p.Wait(); err != nil { if err := p.Wait(); err != nil {
t.Errorf("Wait() error = %v\noutput: %s", t.Errorf("Wait() error = %v\noutput: %s",
err, output.String()) err, output.String())

View File

@ -1,6 +1,7 @@
package dbus package dbus
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -19,29 +20,21 @@ var ProxyName = "xdg-dbus-proxy"
type Proxy struct { type Proxy struct {
helper helper.Helper helper helper.Helper
bwrap *bwrap.Config bwrap *bwrap.Config
ctx context.Context
cancel context.CancelCauseFunc
name string name string
session [2]string session [2]string
system [2]string system [2]string
sysP bool
seal io.WriterTo seal io.WriterTo
lock sync.RWMutex lock sync.RWMutex
} }
func (p *Proxy) Session() [2]string { func (p *Proxy) Session() [2]string { return p.session }
return p.session func (p *Proxy) System() [2]string { return p.system }
} func (p *Proxy) Sealed() bool { p.lock.RLock(); defer p.lock.RUnlock(); return p.seal != nil }
func (p *Proxy) System() [2]string {
return p.system
}
func (p *Proxy) Sealed() bool {
p.lock.RLock()
defer p.lock.RUnlock()
return p.seal != nil
}
var ( var (
ErrConfig = errors.New("no configuration to seal") ErrConfig = errors.New("no configuration to seal")
@ -56,7 +49,7 @@ func (p *Proxy) String() string {
defer p.lock.RUnlock() defer p.lock.RUnlock()
if p.helper != nil { if p.helper != nil {
return p.helper.Unwrap().String() return p.helper.String()
} }
if p.seal != nil { if p.seal != nil {
@ -66,7 +59,14 @@ func (p *Proxy) String() string {
return "(unsealed dbus proxy)" return "(unsealed dbus proxy)"
} }
// BwrapStatic builds static bwrap args. This omits any fd-dependant args.
func (p *Proxy) BwrapStatic() []string { func (p *Proxy) BwrapStatic() []string {
p.lock.RLock()
defer p.lock.RUnlock()
if p.bwrap == nil {
return nil
}
return p.bwrap.Args() return p.bwrap.Args()
} }
@ -89,6 +89,7 @@ func (p *Proxy) Seal(session, system *Config) error {
} }
if system != nil { if system != nil {
args = append(args, system.Args(p.system)...) args = append(args, system.Args(p.system)...)
p.sysP = true
} }
if seal, err := helper.NewCheckedArgs(args); err != nil { if seal, err := helper.NewCheckedArgs(args); err != nil {
return err return err

View File

@ -1,8 +1,10 @@
package dbus package dbus
import ( import (
"context"
"errors" "errors"
"io" "io"
"os"
"os/exec" "os/exec"
"path" "path"
"path/filepath" "path/filepath"
@ -14,9 +16,8 @@ import (
"git.gensokyo.uk/security/fortify/ldd" "git.gensokyo.uk/security/fortify/ldd"
) )
// Start launches the D-Bus proxy and sets up the Wait method. // Start launches the D-Bus proxy.
// ready should be buffered and must only be received from once. func (p *Proxy) Start(ctx context.Context, output io.Writer, sandbox bool) error {
func (p *Proxy) Start(ready chan error, output io.Writer, sandbox, seccomp bool) error {
p.lock.Lock() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
@ -26,7 +27,6 @@ func (p *Proxy) Start(ready chan error, output io.Writer, sandbox, seccomp bool)
var ( var (
h helper.Helper h helper.Helper
cmd *exec.Cmd
argF = func(argsFD, statFD int) []string { argF = func(argsFD, statFD int) []string {
if statFD == -1 { if statFD == -1 {
@ -39,9 +39,8 @@ func (p *Proxy) Start(ready chan error, output io.Writer, sandbox, seccomp bool)
if !sandbox { if !sandbox {
h = helper.New(p.seal, p.name, argF) h = helper.New(p.seal, p.name, argF)
cmd = h.Unwrap()
// xdg-dbus-proxy does not need to inherit the environment // xdg-dbus-proxy does not need to inherit the environment
cmd.Env = []string{} h.SetEnv(make([]string, 0))
} 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
@ -56,7 +55,7 @@ func (p *Proxy) Start(ready chan error, output io.Writer, sandbox, seccomp bool)
// resolve libraries by parsing ldd output // resolve libraries by parsing ldd output
var proxyDeps []*ldd.Entry var proxyDeps []*ldd.Entry
if toolPath != "/nonexistent-xdg-dbus-proxy" { if toolPath != "/nonexistent-xdg-dbus-proxy" {
if l, err := ldd.Exec(toolPath); err != nil { if l, err := ldd.Exec(ctx, toolPath); err != nil {
return err return err
} else { } else {
proxyDeps = l proxyDeps = l
@ -73,10 +72,6 @@ func (p *Proxy) Start(ready chan error, output io.Writer, sandbox, seccomp bool)
DieWithParent: true, DieWithParent: true,
} }
if !seccomp {
bc.Syscall = nil
}
// resolve proxy socket directories // resolve proxy socket directories
bindTarget := make(map[string]struct{}, 2) bindTarget := make(map[string]struct{}, 2)
for _, ps := range []string{p.session[1], p.system[1]} { for _, ps := range []string{p.session[1], p.system[1]} {
@ -116,35 +111,65 @@ func (p *Proxy) Start(ready chan error, output io.Writer, sandbox, seccomp bool)
} }
h = helper.MustNewBwrap(bc, toolPath, p.seal, argF, nil, nil) h = helper.MustNewBwrap(bc, toolPath, p.seal, argF, nil, nil)
cmd = h.Unwrap()
p.bwrap = bc p.bwrap = bc
} }
if output != nil { if output != nil {
cmd.Stdout = output h.Stdout(output).Stderr(output)
cmd.Stderr = output
} }
if err := h.StartNotify(ready); err != nil { c, cancel := context.WithCancelCause(ctx)
if err := h.Start(c, true); err != nil {
cancel(err)
return err return err
} }
p.helper = h p.helper = h
p.ctx = c
p.cancel = cancel
return nil return nil
} }
// Wait waits for xdg-dbus-proxy to exit or fault. var proxyClosed = errors.New("proxy closed")
// Wait blocks until xdg-dbus-proxy exits and releases resources.
func (p *Proxy) Wait() error { func (p *Proxy) Wait() error {
p.lock.RLock() p.lock.RLock()
defer p.lock.RUnlock() defer p.lock.RUnlock()
if p.helper == nil { if p.helper == nil {
return errors.New("proxy not started") return errors.New("dbus: not started")
} }
return p.helper.Wait() errs := make([]error, 3)
errs[0] = p.helper.Wait()
if p.cancel == nil &&
errors.Is(errs[0], context.Canceled) &&
errors.Is(context.Cause(p.ctx), proxyClosed) {
errs[0] = nil
} }
// Close closes the status file descriptor passed to xdg-dbus-proxy, causing it to stop. // ensure socket removal so ephemeral directory is empty at revert
func (p *Proxy) Close() error { if err := os.Remove(p.session[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
return p.helper.Close() errs[1] = err
}
if p.sysP {
if err := os.Remove(p.system[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
errs[2] = err
}
}
return errors.Join(errs...)
}
// Close cancels the context passed to the helper instance attached to xdg-dbus-proxy.
func (p *Proxy) Close() {
p.lock.Lock()
defer p.lock.Unlock()
if p.cancel == nil {
panic("dbus: not started")
}
p.cancel(proxyClosed)
p.cancel = nil
} }

6
flake.lock generated
View File

@ -23,11 +23,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1737672001, "lastModified": 1739333913,
"narHash": "sha256-YnHJJ19wqmibLQdUeq9xzE6CjrMA568KN/lFPuSVs4I=", "narHash": "sha256-JXt5FtySR+yBm5ny8zG/hX1IybF/7R66jZfXxXSb6wY=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "035f8c0853c2977b24ffc4d0a42c74f00b182cd8", "rev": "7d83f668aee9e41d574c398a9bb569047e8a3f5d",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -1,111 +1,47 @@
package helper package helper
import ( import (
"context"
"errors" "errors"
"io" "io"
"os" "os"
"os/exec" "slices"
"strconv" "strconv"
"sync" "sync"
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/helper/proc"
) )
// BubblewrapName is the file name or path to bubblewrap. // BubblewrapName is the file name or path to bubblewrap.
var BubblewrapName = "bwrap" var BubblewrapName = "bwrap"
type bubblewrap struct { type bubblewrap struct {
// bwrap child file name // final args fd of bwrap process
argsFd uintptr
// name of the command to run in bwrap
name string name string
// bwrap pipes
control *pipes
// 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
controlPt *pipes
lock sync.RWMutex lock sync.RWMutex
*exec.Cmd *helperCmd
} }
func (b *bubblewrap) StartNotify(ready chan error) error { func (b *bubblewrap) Start(ctx context.Context, stat bool) error {
b.lock.Lock() b.lock.Lock()
defer b.lock.Unlock() defer b.lock.Unlock()
if ready != nil && b.controlPt == 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 // 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. // call to Start succeeded, we don't want to spuriously close its pipes.
if b.Cmd.Process != nil { if b.Cmd != nil && b.Cmd.Process != nil {
return errors.New("exec: already started") return errors.New("exec: already started")
} }
// prepare bwrap pipe and args args := b.finalise(ctx, stat)
if argsFD, _, err := b.control.prepareCmd(b.Cmd); err != nil { b.Cmd.Args = slices.Grow(b.Cmd.Args, 4+len(args))
return err b.Cmd.Args = append(b.Cmd.Args, "--args", strconv.Itoa(int(b.argsFd)), "--", b.name)
} else { b.Cmd.Args = append(b.Cmd.Args, args...)
b.Cmd.Args = append(b.Cmd.Args, "--args", strconv.Itoa(argsFD), "--", b.name) return proc.Fulfill(ctx, b.Cmd, b.files, b.extraFiles)
}
// prepare child args and pipes if enabled
if b.controlPt != nil {
b.controlPt.ready = ready
if argsFD, statFD, err := b.controlPt.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.controlPt != 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.control.readyWriteArgs(); err != nil {
return err
}
// write child args if enabled
if b.controlPt != nil {
if err := b.controlPt.readyWriteArgs(); err != nil {
return err
}
}
return nil
}
func (b *bubblewrap) Close() error {
if b.controlPt == nil {
panic("attempted to close bwrap child initialised without pipes")
}
return b.controlPt.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. // MustNewBwrap initialises a new Bwrap instance with wt as the null-terminated argument writer.
@ -130,27 +66,23 @@ 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(
conf *bwrap.Config, name string, conf *bwrap.Config, 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,
syncFd *os.File, syncFd *os.File,
) (Helper, error) { ) (Helper, error) {
b := new(bubblewrap) b := new(bubblewrap)
b.argF = argF
b.name = name b.name = name
if wt != nil { b.helperCmd = newHelperCmd(b, BubblewrapName, wt, argF, extraFiles)
b.controlPt = &pipes{args: wt}
}
b.Cmd = execCommand(BubblewrapName)
b.control = new(pipes)
args := conf.Args() args := conf.Args()
if fdArgs, err := conf.FDArgs(syncFd, &extraFiles); err != nil { conf.FDArgs(syncFd, &args, b.extraFiles, &b.files)
return nil, err if v, err := NewCheckedArgs(args); err != nil {
} else if b.control.args, err = NewCheckedArgs(append(args, fdArgs...)); err != nil {
return nil, err return nil, err
} else { } else {
b.Cmd.ExtraFiles = extraFiles f := proc.NewWriterTo(v)
b.argsFd = proc.InitFile(f, b.extraFiles)
b.files = append(b.files, f)
} }
return b, nil return b, nil

View File

@ -1,12 +1,10 @@
package bwrap package bwrap
import ( import (
"encoding/gob"
"os" "os"
"slices" "slices"
"strconv"
"git.gensokyo.uk/security/fortify/internal/proc" "git.gensokyo.uk/security/fortify/helper/proc"
) )
type Builder interface { type Builder interface {
@ -20,68 +18,8 @@ type FSBuilder interface {
} }
type FDBuilder interface { type FDBuilder interface {
Len() int proc.File
Append(args *[]string, extraFiles *[]*os.File) error Builder
}
func init() {
gob.Register(new(pairF))
gob.Register(new(stringF))
}
type pairF [3]string
func (p *pairF) Path() string {
return p[2]
}
func (p *pairF) Len() int {
return len(p) // compiler replaces this with 3
}
func (p *pairF) Append(args *[]string) {
*args = append(*args, p[0], p[1], p[2])
}
type stringF [2]string
func (s stringF) Path() string {
return s[1]
}
func (s stringF) Len() int {
return len(s) // compiler replaces this with 2
}
func (s stringF) Append(args *[]string) {
*args = append(*args, s[0], s[1])
}
type fileF struct {
name string
file *os.File
}
func (f *fileF) Len() int {
if f.file == nil {
return 0
}
return 2
}
func (f *fileF) Append(args *[]string, extraFiles *[]*os.File) error {
if f.file == nil {
return nil
}
extraFile(args, extraFiles, f.name, f.file)
return nil
}
func extraFile(args *[]string, extraFiles *[]*os.File, name string, f *os.File) {
if f == nil {
return
}
*args = append(*args, name, strconv.Itoa(int(proc.ExtraFileSlice(extraFiles, f))))
} }
// Args returns a slice of bwrap args corresponding to c. // Args returns a slice of bwrap args corresponding to c.
@ -115,24 +53,36 @@ func (c *Config) Args() (args []string) {
return return
} }
func (c *Config) FDArgs(syncFd *os.File, extraFiles *[]*os.File) (args []string, err error) { func (c *Config) FDArgs(syncFd *os.File, args *[]string, extraFiles *proc.ExtraFilesPre, files *[]proc.File) {
builders := []FDBuilder{ builders := []FDBuilder{
&seccompBuilder{c}, c.seccompArgs(),
&fileF{positionalArgs[SyncFd], syncFd}, newFile(positionalArgs[SyncFd], syncFd),
} }
argc := 0 argc := 0
fc := 0
for _, b := range builders { for _, b := range builders {
argc += b.Len() l := b.Len()
if l < 1 {
continue
}
argc += l
fc++
proc.InitFile(b, extraFiles)
} }
args = make([]string, 0, argc) fc++ // allocate extra slot for stat fd
*extraFiles = slices.Grow(*extraFiles, len(builders)) *args = slices.Grow(*args, argc)
*files = slices.Grow(*files, fc)
for _, b := range builders { for _, b := range builders {
if err = b.Append(&args, extraFiles); err != nil { if b.Len() < 1 {
break continue
} }
b.Append(args)
*files = append(*files, b)
} }
return return
} }

View File

@ -2,8 +2,9 @@ package bwrap
import ( import (
"fmt" "fmt"
"os" "strconv"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/helper/seccomp" "git.gensokyo.uk/security/fortify/helper/seccomp"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
) )
@ -23,35 +24,13 @@ type SyscallPolicy struct {
Bluetooth bool `json:"bluetooth"` Bluetooth bool `json:"bluetooth"`
} }
type seccompBuilder struct { func (c *Config) seccompArgs() FDBuilder {
config *Config // explicitly disable syscall filter
}
func (s *seccompBuilder) Len() int {
if s == nil {
return 0
}
return 2
}
func (s *seccompBuilder) Append(args *[]string, extraFiles *[]*os.File) error {
if s == nil {
return nil
}
if f, err := s.config.resolveSeccomp(); err != nil {
return err
} else {
extraFile(args, extraFiles, positionalArgs[Seccomp], f)
return nil
}
}
func (c *Config) resolveSeccomp() (*os.File, error) {
if c.Syscall == nil { if c.Syscall == nil {
return nil, nil // nil File skips builder
return new(seccompBuilder)
} }
// resolve seccomp filter opts
var ( var (
opts seccomp.SyscallOpts opts seccomp.SyscallOpts
optd []string optd []string
@ -86,5 +65,22 @@ func (c *Config) resolveSeccomp() (*os.File, error) {
seccomp.CPrintln(fmt.Sprintf("seccomp flags: %s", optd)) seccomp.CPrintln(fmt.Sprintf("seccomp flags: %s", optd))
} }
return seccomp.Export(opts) return &seccompBuilder{seccomp.NewFile(opts)}
}
type seccompBuilder struct{ proc.File }
func (s *seccompBuilder) Len() int {
if s == nil || s.File == nil {
return 0
}
return 2
}
func (s *seccompBuilder) Append(args *[]string) {
if s == nil || s.File == nil {
return
}
*args = append(*args, positionalArgs[Seccomp], strconv.Itoa(int(s.Fd())))
} }

52
helper/bwrap/trivial.go Normal file
View File

@ -0,0 +1,52 @@
package bwrap
import (
"context"
"encoding/gob"
"os"
"strconv"
"git.gensokyo.uk/security/fortify/helper/proc"
)
func init() {
gob.Register(new(pairF))
gob.Register(new(stringF))
}
type pairF [3]string
func (p *pairF) Path() string { return p[2] }
func (p *pairF) Len() int { return len(p) }
func (p *pairF) Append(args *[]string) { *args = append(*args, p[0], p[1], p[2]) }
type stringF [2]string
func (s stringF) Path() string { return s[1] }
func (s stringF) Len() int { return len(s) /* compiler replaces this with 2 */ }
func (s stringF) Append(args *[]string) { *args = append(*args, s[0], s[1]) }
func newFile(name string, f *os.File) FDBuilder { return &fileF{name: name, file: f} }
type fileF struct {
name string
file *os.File
proc.BaseFile
}
func (f *fileF) ErrCount() int { return 0 }
func (f *fileF) Fulfill(_ context.Context, _ func(error)) error { f.Set(f.file); return nil }
func (f *fileF) Len() int {
if f.file == nil {
return 0
}
return 2
}
func (f *fileF) Append(args *[]string) {
if f.file == nil {
return
}
*args = append(*args, f.name, strconv.Itoa(int(f.Fd())))
}

View File

@ -1,11 +1,12 @@
package helper_test package helper_test
import ( import (
"context"
"errors" "errors"
"fmt"
"os" "os"
"strings" "strings"
"testing" "testing"
"time"
"git.gensokyo.uk/security/fortify/helper" "git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.gensokyo.uk/security/fortify/helper/bwrap"
@ -13,9 +14,7 @@ import (
func TestBwrap(t *testing.T) { func TestBwrap(t *testing.T) {
sc := &bwrap.Config{ sc := &bwrap.Config{
Unshare: nil,
Net: true, Net: true,
UserNS: false,
Hostname: "localhost", Hostname: "localhost",
Chdir: "/nonexistent", Chdir: "/nonexistent",
Clearenv: true, Clearenv: true,
@ -37,8 +36,8 @@ func TestBwrap(t *testing.T) {
nil, nil, nil, nil,
) )
if err := h.Start(); !errors.Is(err, os.ErrNotExist) { if err := h.Start(context.Background(), false); !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)
} }
}) })
@ -71,23 +70,6 @@ func TestBwrap(t *testing.T) {
) )
}) })
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, "fortify",
nil, argF,
nil, nil,
).StartNotify(make(chan error))))
})
t.Run("start without pipes", func(t *testing.T) { t.Run("start without pipes", func(t *testing.T) {
helper.InternalReplaceExecCommand(t) helper.InternalReplaceExecCommand(t)
@ -96,26 +78,15 @@ func TestBwrap(t *testing.T) {
nil, argFChecked, nil, argFChecked,
nil, nil, nil, nil,
) )
cmd := h.Unwrap()
stdout, stderr := new(strings.Builder), new(strings.Builder) stdout, stderr := new(strings.Builder), new(strings.Builder)
cmd.Stdout, cmd.Stderr = stdout, stderr h.Stdout(stdout).Stderr(stderr)
t.Run("close without pipes panic", func(t *testing.T) { c, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer func() { defer cancel()
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", if err := h.Start(c, false); err != nil {
h.Close())) t.Errorf("Start: error = %v",
})
if err := h.Start(); err != nil {
t.Errorf("Start() error = %v",
err) err)
return return
} }

View File

@ -1,93 +1,40 @@
package helper package helper
import ( import (
"context"
"errors" "errors"
"io" "io"
"os/exec"
"sync" "sync"
"git.gensokyo.uk/security/fortify/helper/proc"
) )
// direct wraps *exec.Cmd and manages status and args fd. // direct wraps *exec.Cmd and manages status and args fd.
// Args is always 3 and status if set is always 4. // Args is always 3 and status if set is always 4.
type direct struct { type direct struct {
// helper pipes
// cannot be nil
p *pipes
// returns an array of arguments passed directly
// to the helper process
argF func(argsFD, statFD int) []string
lock sync.RWMutex lock sync.RWMutex
*exec.Cmd *helperCmd
} }
func (h *direct) StartNotify(ready chan error) error { func (h *direct) Start(ctx context.Context, stat bool) error {
h.lock.Lock() h.lock.Lock()
defer h.lock.Unlock() defer h.lock.Unlock()
// Check for doubled Start calls before we defer failure cleanup. If the prior // 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. // call to Start succeeded, we don't want to spuriously close its pipes.
if h.Cmd.Process != nil { if h.Cmd != nil && h.Cmd.Process != nil {
return errors.New("exec: already started") return errors.New("exec: already started")
} }
h.p.ready = ready args := h.finalise(ctx, stat)
if argsFD, statFD, err := h.p.prepareCmd(h.Cmd); err != nil { h.Cmd.Args = append(h.Cmd.Args, args...)
return err return proc.Fulfill(ctx, h.Cmd, h.files, h.extraFiles)
} else {
h.Cmd.Args = append(h.Cmd.Args, h.argF(argsFD, statFD)...)
}
if ready != nil {
h.Cmd.Env = append(h.Cmd.Env, FortifyHelper+"=1", FortifyStatus+"=1")
} else {
h.Cmd.Env = append(h.Cmd.Env, FortifyHelper+"=1", FortifyStatus+"=0")
}
if err := h.Cmd.Start(); err != nil {
return err
}
if err := h.p.readyWriteArgs(); err != nil {
return err
}
return nil
}
func (h *direct) Wait() error {
h.lock.RLock()
defer h.lock.RUnlock()
if h.Cmd.Process == nil {
return errors.New("exec: not started")
}
defer h.p.mustClosePipes()
if h.Cmd.ProcessState != nil {
return errors.New("exec: Wait was already called")
}
return h.Cmd.Wait()
}
func (h *direct) Close() error {
return h.p.closeStatus()
}
func (h *direct) Start() error {
return h.StartNotify(nil)
}
func (h *direct) Unwrap() *exec.Cmd {
return h.Cmd
} }
// New initialises a new direct Helper instance with wt as the null-terminated argument writer. // New 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(wt io.WriterTo, name string, argF func(argsFD, statFD int) []string) Helper { func New(wt io.WriterTo, name string, argF func(argsFd, statFd int) []string) Helper {
if wt == nil { d := new(direct)
panic("attempted to create helper with invalid argument writer") d.helperCmd = newHelperCmd(d, name, wt, argF, nil)
} return d
return &direct{p: &pipes{args: wt}, argF: argF, Cmd: execCommand(name)}
} }

View File

@ -1,6 +1,7 @@
package helper_test package helper_test
import ( import (
"context"
"errors" "errors"
"os" "os"
"testing" "testing"
@ -12,8 +13,8 @@ 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(argsWt, "/nonexistent", argF) h := helper.New(argsWt, "/nonexistent", argF)
if err := h.Start(); !errors.Is(err, os.ErrNotExist) { if err := h.Start(context.Background(), false); !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)
} }
}) })
@ -26,18 +27,6 @@ func TestDirect(t *testing.T) {
} }
}) })
t.Run("invalid new helper panic", func(t *testing.T) {
defer func() {
wantPanic := "attempted to create helper with invalid argument writer"
if r := recover(); r != wantPanic {
t.Errorf("New: panic = %q, want %q",
r, wantPanic)
}
}()
helper.New(nil, "fortify", argF)
})
t.Run("implementation compliance", func(t *testing.T) { t.Run("implementation compliance", func(t *testing.T) {
testHelper(t, func() helper.Helper { return helper.New(argsWt, "crash-test-dummy", argF) }) testHelper(t, func() helper.Helper { return helper.New(argsWt, "crash-test-dummy", argF) })
}) })

View File

@ -2,35 +2,133 @@
package helper package helper
import ( import (
"errors" "context"
"fmt"
"io"
"os"
"os/exec" "os/exec"
"slices"
"syscall"
"time"
"git.gensokyo.uk/security/fortify/helper/proc"
) )
var ( var (
ErrStatusFault = errors.New("generic status pipe fault") WaitDelay = 2 * time.Second
ErrStatusRead = errors.New("unexpected status response")
) )
const ( const (
// FortifyHelper is set for the process launched by Helper. // FortifyHelper is set to 1 when args fd is enabled and 0 otherwise.
FortifyHelper = "FORTIFY_HELPER" FortifyHelper = "FORTIFY_HELPER"
// FortifyStatus is 1 when sync fd is enabled and 0 otherwise. // FortifyStatus is set to 1 when stat fd is enabled and 0 otherwise.
FortifyStatus = "FORTIFY_STATUS" FortifyStatus = "FORTIFY_STATUS"
) )
type Helper interface { type Helper interface {
// StartNotify starts the helper process. // Stdin sets the standard input of Helper.
// A status pipe is passed to the helper if ready is not nil. Stdin(r io.Reader) Helper
StartNotify(ready chan error) error // Stdout sets the standard output of Helper.
Stdout(w io.Writer) Helper
// Stderr sets the standard error of Helper.
Stderr(w io.Writer) Helper
// SetEnv sets the environment of Helper.
SetEnv(env []string) Helper
// Start starts the helper process. // Start starts the helper process.
Start() error // A status pipe is passed to the helper if stat is true.
// Close closes the status pipe. Start(ctx context.Context, stat bool) error
// If helper is started without the status pipe, Close panics. // Wait blocks until Helper exits and releases all its resources.
Close() error
// Wait calls wait on the child process and cleans up pipes.
Wait() error Wait() error
// Unwrap returns the underlying exec.Cmd instance.
Unwrap() *exec.Cmd fmt.Stringer
} }
var execCommand = exec.Command func newHelperCmd(
h Helper, name string,
wt io.WriterTo, argF func(argsFd, statFd int) []string,
extraFiles []*os.File,
) (cmd *helperCmd) {
cmd = new(helperCmd)
cmd.r = h
cmd.name = name
cmd.extraFiles = new(proc.ExtraFilesPre)
for _, f := range extraFiles {
_, v := cmd.extraFiles.Append()
*v = f
}
argsFd := -1
if wt != nil {
f := proc.NewWriterTo(wt)
argsFd = int(proc.InitFile(f, cmd.extraFiles))
cmd.files = append(cmd.files, f)
cmd.hasArgsFd = true
}
cmd.argF = func(statFd int) []string { return argF(argsFd, statFd) }
return
}
// helperCmd wraps Cmd and implements methods shared across all Helper implementations.
type helperCmd struct {
// ref to parent
r Helper
// returns an array of arguments passed directly
// to the helper process
argF func(statFd int) []string
// whether argsFd is present
hasArgsFd bool
// closes statFd
stat io.Closer
// deferred extraFiles fulfillment
files []proc.File
// passed through to [proc.Fulfill] and [proc.InitFile]
extraFiles *proc.ExtraFilesPre
name string
stdin io.Reader
stdout, stderr io.Writer
env []string
*exec.Cmd
}
func (h *helperCmd) Stdin(r io.Reader) Helper { h.stdin = r; return h.r }
func (h *helperCmd) Stdout(w io.Writer) Helper { h.stdout = w; return h.r }
func (h *helperCmd) Stderr(w io.Writer) Helper { h.stderr = w; return h.r }
func (h *helperCmd) SetEnv(env []string) Helper { h.env = env; return h.r }
// finalise initialises the underlying [exec.Cmd] object.
func (h *helperCmd) finalise(ctx context.Context, stat bool) (args []string) {
h.Cmd = commandContext(ctx, h.name)
h.Cmd.Stdin, h.Cmd.Stdout, h.Cmd.Stderr = h.stdin, h.stdout, h.stderr
h.Cmd.Env = slices.Grow(h.env, 2)
if h.hasArgsFd {
h.Cmd.Env = append(h.Cmd.Env, FortifyHelper+"=1")
} else {
h.Cmd.Env = append(h.Cmd.Env, FortifyHelper+"=0")
}
h.Cmd.Cancel = func() error { return h.Cmd.Process.Signal(syscall.SIGTERM) }
h.Cmd.WaitDelay = WaitDelay
statFd := -1
if stat {
f := proc.NewStat(&h.stat)
statFd = int(proc.InitFile(f, h.extraFiles))
h.files = append(h.files, f)
h.Cmd.Env = append(h.Cmd.Env, FortifyStatus+"=1")
// stat is populated on fulfill
h.Cmd.Cancel = func() error { return h.stat.Close() }
} else {
h.Cmd.Env = append(h.Cmd.Env, FortifyStatus+"=0")
}
return h.argF(statFd)
}
var commandContext = exec.CommandContext

View File

@ -1,6 +1,9 @@
package helper_test package helper_test
import ( import (
"context"
"errors"
"fmt"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
@ -23,20 +26,23 @@ var (
argsWt = helper.MustNewCheckedArgs(wantArgs) argsWt = helper.MustNewCheckedArgs(wantArgs)
) )
func argF(argsFD, statFD int) []string { func argF(argsFd, statFd int) []string {
if argsFD == -1 { if argsFd == -1 {
panic("invalid args fd") panic("invalid args fd")
} }
return argFChecked(argsFD, statFD) return argFChecked(argsFd, statFd)
} }
func argFChecked(argsFD, statFD int) []string { func argFChecked(argsFd, statFd int) (args []string) {
if statFD == -1 { args = make([]string, 0, 4)
return []string{"--args", strconv.Itoa(argsFD)} if argsFd > -1 {
} else { args = append(args, "--args", strconv.Itoa(argsFd))
return []string{"--args", strconv.Itoa(argsFD), "--fd", strconv.Itoa(statFD)}
} }
if statFd > -1 {
args = append(args, "--fd", strconv.Itoa(statFd))
}
return
} }
// this function tests an implementation of the helper.Helper interface // this function tests an implementation of the helper.Helper interface
@ -45,66 +51,42 @@ func testHelper(t *testing.T, createHelper func() helper.Helper) {
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) {
h := createHelper() h := createHelper()
ready := make(chan error, 1)
cmd := h.Unwrap()
stdout, stderr := new(strings.Builder), new(strings.Builder) stdout, stderr := new(strings.Builder), new(strings.Builder)
cmd.Stdout, cmd.Stderr = stdout, stderr h.Stdout(stdout).Stderr(stderr)
t.Run("wait not yet started helper", func(t *testing.T) { t.Run("wait not yet started helper", func(t *testing.T) {
wantErr := "exec: not started" defer func() {
if err := h.Wait(); err != nil && err.Error() != wantErr { r := recover()
t.Errorf("Wait(%v) error = %v, wantErr %v", if r == nil {
ready, t.Fatalf("Wait did not panic")
err, wantErr)
return
} }
}()
panic(fmt.Sprintf("unreachable: %v", h.Wait()))
}) })
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
t.Log("starting helper stub") t.Log("starting helper stub")
if err := h.StartNotify(ready); err != nil { if err := h.Start(ctx, true); err != nil {
t.Errorf("StartNotify(%v) error = %v", t.Errorf("Start: error = %v", err)
ready, cancel()
err)
return return
} }
t.Log("cancelling context")
cancel()
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.StartNotify(ready); err != nil && err.Error() != wantErr { if err := h.Start(ctx, true); err != nil && err.Error() != wantErr {
t.Errorf("StartNotify(%v) error = %v, wantErr %v", t.Errorf("Start: error = %v, wantErr %v",
ready,
err, wantErr) err, wantErr)
return return
} }
}) })
t.Log("waiting on status channel with timeout")
select {
case <-time.NewTimer(5 * time.Second).C:
t.Errorf("never got a ready response")
t.Errorf("stdout:\n%s", stdout.String())
t.Errorf("stderr:\n%s", stderr.String())
if err := cmd.Process.Kill(); err != nil {
panic(err.Error())
}
return
case err := <-ready:
if err != nil {
t.Errorf("StartNotify(%v) latent error = %v",
ready,
err)
}
}
t.Log("closing status pipe")
if err := h.Close(); err != nil {
t.Errorf("Close() error = %v",
err)
}
t.Log("waiting on helper") t.Log("waiting on helper")
if err := h.Wait(); err != nil { if err := h.Wait(); !errors.Is(err, context.Canceled) {
t.Errorf("Wait() err = %v stderr = %s", t.Errorf("Wait() err = %v stderr = %s",
err, stderr) err, stderr)
} }
@ -112,51 +94,35 @@ func testHelper(t *testing.T, createHelper func() helper.Helper) {
t.Run("wait already finalised helper", func(t *testing.T) { t.Run("wait already finalised helper", func(t *testing.T) {
wantErr := "exec: Wait was already called" wantErr := "exec: Wait was already called"
if err := h.Wait(); err != nil && err.Error() != wantErr { if err := h.Wait(); err != nil && err.Error() != wantErr {
t.Errorf("Wait(%v) error = %v, wantErr %v", t.Errorf("Wait: error = %v, wantErr %v",
ready,
err, wantErr) err, wantErr)
return return
} }
}) })
if got := stdout.String(); !strings.HasPrefix(got, wantPayload) { if got := stdout.String(); !strings.HasPrefix(got, wantPayload) {
t.Errorf("StartNotify(%v) stdout = %v, want %v", t.Errorf("Start: stdout = %v, want %v",
ready,
got, wantPayload) got, wantPayload)
} }
}) })
t.Run("start helper and wait", func(t *testing.T) { t.Run("start helper and wait", func(t *testing.T) {
h := createHelper() h := createHelper()
cmd := h.Unwrap()
stdout, stderr := new(strings.Builder), new(strings.Builder) stdout, stderr := new(strings.Builder), new(strings.Builder)
cmd.Stdout, cmd.Stderr = stdout, stderr h.Stdout(stdout).Stderr(stderr)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := h.Start(); err != nil { if err := h.Start(ctx, false); err != nil {
t.Errorf("Start() error = %v", t.Errorf("Start() error = %v",
err) err)
return return
} }
t.Run("close helper without status pipe", func(t *testing.T) {
defer func() {
wantPanic := "attempted to close helper with no status pipe"
if r := recover(); r != wantPanic {
t.Errorf("Close() panic = %v, wantPanic %v",
r, wantPanic)
}
}()
if err := h.Close(); err != nil {
t.Errorf("Close() error = %v",
err)
return
}
})
if err := h.Wait(); err != nil { if err := h.Wait(); err != nil {
t.Errorf("Wait() err = %v stderr = %s", t.Errorf("Wait() err = %v stdout = %s stderr = %s",
err, stderr) err, stdout, stderr)
} }
if got := stdout.String(); !strings.HasPrefix(got, wantPayload) { if got := stdout.String(); !strings.HasPrefix(got, wantPayload) {

View File

@ -1,149 +0,0 @@
package helper
import (
"errors"
"io"
"os"
"os/exec"
"git.gensokyo.uk/security/fortify/internal/proc"
)
type pipes struct {
args io.WriterTo
statP [2]*os.File
argsP [2]*os.File
ready chan error
cmd *exec.Cmd
}
func (p *pipes) pipe() error {
if p.statP[0] != nil || p.statP[1] != nil ||
p.argsP[0] != nil || p.argsP[1] != nil {
panic("attempted to pipe twice")
}
if p.args == nil {
panic("attempted to pipe without args")
}
// create pipes
if pr, pw, err := os.Pipe(); err != nil {
return err
} else {
p.argsP[0], p.argsP[1] = pr, pw
}
// create status pipes if ready signal is requested
if p.ready != nil {
if pr, pw, err := os.Pipe(); err != nil {
return err
} else {
p.statP[0], p.statP[1] = pr, pw
}
}
return nil
}
// calls pipe to create pipes and sets them up as ExtraFiles, returning their fd
func (p *pipes) prepareCmd(cmd *exec.Cmd) (argsFd, statFd int, err error) {
argsFd, statFd = -1, -1
if err = p.pipe(); err != nil {
return
}
// save a reference of cmd for future use
p.cmd = cmd
argsFd = int(proc.ExtraFile(cmd, p.argsP[0]))
if p.ready != nil {
statFd = int(proc.ExtraFile(cmd, p.statP[1]))
}
return
}
func (p *pipes) readyWriteArgs() error {
statsP, argsP := p.statP[0], p.argsP[1]
// write arguments and close args pipe
if _, err := p.args.WriteTo(argsP); err != nil {
if err1 := p.cmd.Process.Kill(); err1 != nil {
// should be unreachable
panic(err1.Error())
}
return err
} else {
if err = argsP.Close(); err != nil {
if err1 := p.cmd.Process.Kill(); err1 != nil {
// should be unreachable
panic(err1.Error())
}
return err
}
}
if p.ready != nil {
// monitor stat pipe
go func() {
n, err := statsP.Read(make([]byte, 1))
switch n {
case -1:
if err1 := p.cmd.Process.Kill(); err1 != nil {
// should be unreachable
panic(err1.Error())
}
// ensure error is not nil
if err == nil {
err = ErrStatusFault
}
p.ready <- err
case 0:
// ensure error is not nil
if err == nil {
err = ErrStatusRead
}
p.ready <- err
case 1:
p.ready <- nil
default:
panic("unreachable") // unexpected read count
}
}()
}
return nil
}
func (p *pipes) mustClosePipes() {
if err := p.argsP[0].Close(); err != nil && !errors.Is(err, os.ErrClosed) {
// unreachable
panic(err.Error())
}
if err := p.argsP[1].Close(); err != nil && !errors.Is(err, os.ErrClosed) {
// unreachable
panic(err.Error())
}
if p.ready != nil {
if err := p.statP[0].Close(); err != nil && !errors.Is(err, os.ErrClosed) {
// unreachable
panic(err.Error())
}
if err := p.statP[1].Close(); err != nil && !errors.Is(err, os.ErrClosed) {
// unreachable
panic(err.Error())
}
}
}
func (p *pipes) closeStatus() error {
if p.ready == nil {
panic("attempted to close helper with no status pipe")
}
return p.statP[0].Close()
}

View File

@ -1,42 +0,0 @@
package helper
import (
"testing"
)
func Test_pipes_pipe_mustClosePipes(t *testing.T) {
p := new(pipes)
t.Run("pipe without args", func(t *testing.T) {
defer func() {
wantPanic := "attempted to pipe without args"
if r := recover(); r != wantPanic {
t.Errorf("pipe() panic = %v, wantPanic %v",
r, wantPanic)
}
}()
_ = p.pipe()
})
p.args = MustNewCheckedArgs(make([]string, 0))
t.Run("obtain pipes", func(t *testing.T) {
if err := p.pipe(); err != nil {
t.Errorf("pipe() error = %v",
err)
return
}
})
t.Run("pipe twice", func(t *testing.T) {
defer func() {
wantPanic := "attempted to pipe twice"
if r := recover(); r != wantPanic {
t.Errorf("pipe() panic = %v, wantPanic %v",
r, wantPanic)
}
}()
_ = p.pipe()
})
p.mustClosePipes()
}

152
helper/proc/files.go Normal file
View File

@ -0,0 +1,152 @@
package proc
import (
"context"
"os"
"os/exec"
"sync/atomic"
"syscall"
"time"
)
var FulfillmentTimeout = 2 * time.Second
// A File is an extra file with deferred initialisation.
type File interface {
// Init initialises File state. Init must not be called more than once.
Init(fd uintptr, v **os.File) uintptr
// Fd returns the fd value set on initialisation.
Fd() uintptr
// ErrCount returns count of error values emitted during fulfillment.
ErrCount() int
// Fulfill is called prior to process creation and must populate its corresponding file address.
// Calls to dispatchErr must match the return value of ErrCount.
// Fulfill must not be called more than once.
Fulfill(ctx context.Context, dispatchErr func(error)) error
}
// ExtraFilesPre is a linked list storing addresses of [os.File].
type ExtraFilesPre struct {
n *ExtraFilesPre
v *os.File
}
// Append grows the list by one entry and returns an address of the address of [os.File] stored in the new entry.
func (f *ExtraFilesPre) Append() (uintptr, **os.File) { return f.append(3) }
// Files returns a slice pointing to a continuous segment of memory containing all addresses stored in f in order.
func (f *ExtraFilesPre) Files() []*os.File { return f.copy(make([]*os.File, 0, f.len())) }
func (f *ExtraFilesPre) append(i uintptr) (uintptr, **os.File) {
if f.n == nil {
f.n = new(ExtraFilesPre)
return i, &f.v
}
return f.n.append(i + 1)
}
func (f *ExtraFilesPre) len() uintptr {
if f == nil {
return 0
}
return f.n.len() + 1
}
func (f *ExtraFilesPre) copy(e []*os.File) []*os.File {
if f == nil {
// the public methods ensure the first call is never nil;
// the last element is unused, slice it off here
return e[:len(e)-1]
}
return f.n.copy(append(e, f.v))
}
// Fulfill calls the [File.Fulfill] method on all files, starts cmd and blocks until all fulfillment completes.
func Fulfill(ctx context.Context, cmd *exec.Cmd, files []File, extraFiles *ExtraFilesPre) (err error) {
var ecs int
for _, o := range files {
ecs += o.ErrCount()
}
ec := make(chan error, ecs)
c, cancel := context.WithTimeout(ctx, FulfillmentTimeout)
defer cancel()
for _, f := range files {
err = f.Fulfill(c, makeDispatchErr(f, ec))
if err != nil {
return
}
}
cmd.ExtraFiles = extraFiles.Files()
if err = cmd.Start(); err != nil {
return
}
for ecs > 0 {
select {
case err = <-ec:
ecs--
if err != nil {
break
}
case <-ctx.Done():
err = syscall.ECANCELED
break
}
}
return
}
// InitFile initialises f as part of the slice extraFiles points to,
// and returns its final fd value.
func InitFile(f File, extraFiles *ExtraFilesPre) (fd uintptr) { return f.Init(extraFiles.Append()) }
// BaseFile implements the Init method of the File interface and provides indirect access to extra file state.
type BaseFile struct {
fd uintptr
v **os.File
}
func (f *BaseFile) Init(fd uintptr, v **os.File) uintptr {
if v == nil || fd < 3 {
panic("invalid extra file initial state")
}
if f.v != nil {
panic("extra file initialised twice")
}
f.fd, f.v = fd, v
return fd
}
func (f *BaseFile) Fd() uintptr {
if f.v == nil {
panic("use of uninitialised extra file")
}
return f.fd
}
func (f *BaseFile) Set(v *os.File) {
*f.v = v // runtime guards against use before init
}
func makeDispatchErr(f File, ec chan<- error) func(error) {
c := new(atomic.Int32)
c.Store(int32(f.ErrCount()))
return func(err error) {
if c.Add(-1) < 0 {
panic("unexpected error dispatches")
}
ec <- err
}
}
func ExtraFile(cmd *exec.Cmd, f *os.File) (fd uintptr) {
return ExtraFileSlice(&cmd.ExtraFiles, f)
}
func ExtraFileSlice(extraFiles *[]*os.File, f *os.File) (fd uintptr) {
// ExtraFiles: If non-nil, entry i becomes file descriptor 3+i.
fd = uintptr(3 + len(*extraFiles))
*extraFiles = append(*extraFiles, f)
return
}

100
helper/proc/pipe.go Normal file
View File

@ -0,0 +1,100 @@
package proc
import (
"context"
"errors"
"io"
"os"
)
// NewWriterTo returns a [File] that receives content from wt on fulfillment.
func NewWriterTo(wt io.WriterTo) File { return &writeToFile{wt: wt} }
// writeToFile exports the read end of a pipe with data written by an [io.WriterTo].
type writeToFile struct {
wt io.WriterTo
BaseFile
}
func (f *writeToFile) ErrCount() int { return 3 }
func (f *writeToFile) Fulfill(ctx context.Context, dispatchErr func(error)) error {
r, w, err := os.Pipe()
if err != nil {
return err
}
f.Set(r)
done := make(chan struct{})
go func() { _, err = f.wt.WriteTo(w); dispatchErr(err); dispatchErr(w.Close()); close(done) }()
go func() {
select {
case <-done:
dispatchErr(nil)
case <-ctx.Done():
dispatchErr(w.Close()) // this aborts WriteTo with file already closed
}
}()
return nil
}
// NewStat returns a [File] implementing the behaviour
// of the receiving end of xdg-dbus-proxy stat fd.
func NewStat(s *io.Closer) File { return &statFile{s: s} }
var (
ErrStatFault = errors.New("generic stat fd fault")
ErrStatRead = errors.New("unexpected stat behaviour")
)
// statFile implements xdg-dbus-proxy stat fd behaviour.
type statFile struct {
s *io.Closer
BaseFile
}
func (f *statFile) ErrCount() int { return 2 }
func (f *statFile) Fulfill(ctx context.Context, dispatchErr func(error)) error {
r, w, err := os.Pipe()
if err != nil {
return err
}
f.Set(w)
done := make(chan struct{})
go func() {
defer close(done)
var n int
n, err = r.Read(make([]byte, 1))
switch n {
case -1:
if err == nil {
err = ErrStatFault
}
dispatchErr(err)
case 0:
if err == nil {
err = ErrStatRead
}
dispatchErr(err)
case 1:
dispatchErr(err)
default:
panic("unreachable")
}
}()
go func() {
select {
case <-done:
dispatchErr(nil)
case <-ctx.Done():
dispatchErr(r.Close()) // this aborts Read with file already closed
}
}()
// this gets closed by the caller
*f.s = r
return nil
}

68
helper/seccomp/api.go Normal file
View File

@ -0,0 +1,68 @@
package seccomp
import (
"context"
"errors"
"syscall"
"git.gensokyo.uk/security/fortify/helper/proc"
)
// New returns an inactive Encoder instance.
func New(opts SyscallOpts) *Encoder { return &Encoder{newExporter(opts)} }
/*
An Encoder writes a BPF program to an output stream.
Methods of Encoder are not safe for concurrent use.
An Encoder must not be copied after first use.
*/
type Encoder struct {
*exporter
}
func (e *Encoder) Read(p []byte) (n int, err error) {
if err = e.prepare(); err != nil {
return
}
return e.r.Read(p)
}
func (e *Encoder) Close() error {
if e.r == nil {
return syscall.EINVAL
}
// this hangs if the cgo thread fails to exit
return errors.Join(e.closeWrite(), <-e.exportErr)
}
// NewFile returns an instance of exporter implementing [proc.File].
func NewFile(opts SyscallOpts) proc.File { return &File{opts: opts} }
// File implements [proc.File] and provides access to the read end of exporter pipe.
type File struct {
opts SyscallOpts
proc.BaseFile
}
func (f *File) ErrCount() int { return 2 }
func (f *File) Fulfill(ctx context.Context, dispatchErr func(error)) error {
e := newExporter(f.opts)
if err := e.prepare(); err != nil {
return err
}
f.Set(e.r)
go func() {
select {
case err := <-e.exportErr:
dispatchErr(nil)
dispatchErr(err)
case <-ctx.Done():
dispatchErr(e.closeWrite())
dispatchErr(<-e.exportErr)
}
}()
return nil
}

View File

@ -1,17 +1,53 @@
package seccomp package seccomp
import ( import (
"io"
"os" "os"
"runtime"
"sync"
) )
func Export(opts SyscallOpts) (f *os.File, err error) { type exporter struct {
if f, err = tmpfile(); err != nil { opts SyscallOpts
return r, w *os.File
prepareOnce sync.Once
prepareErr error
closeOnce sync.Once
closeErr error
exportErr <-chan error
} }
if err = exportFilter(f.Fd(), opts); err != nil {
func (e *exporter) prepare() error {
e.prepareOnce.Do(func() {
if r, w, err := os.Pipe(); err != nil {
e.prepareErr = err
return return
} else {
e.r, e.w = r, w
} }
_, err = f.Seek(0, io.SeekStart)
return ec := make(chan error, 1)
go func(fd uintptr) { ec <- exportFilter(fd, e.opts); close(ec); _ = e.closeWrite() }(e.w.Fd())
e.exportErr = ec
runtime.SetFinalizer(e, (*exporter).closeWrite)
})
return e.prepareErr
}
func (e *exporter) closeWrite() error {
e.closeOnce.Do(func() {
if e.w == nil {
panic("closeWrite called on invalid exporter")
}
e.closeErr = e.w.Close()
// no need for a finalizer anymore
runtime.SetFinalizer(e, nil)
})
return e.closeErr
}
func newExporter(opts SyscallOpts) *exporter {
return &exporter{opts: opts}
} }

View File

@ -0,0 +1,139 @@
package seccomp_test
import (
"crypto/sha512"
"errors"
"io"
"slices"
"syscall"
"testing"
"git.gensokyo.uk/security/fortify/helper/seccomp"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
func TestExport(t *testing.T) {
testCases := []struct {
name string
opts seccomp.SyscallOpts
want []byte
wantErr bool
}{
{"compat", 0, []byte{
0x95, 0xec, 0x69, 0xd0, 0x17, 0x73, 0x3e, 0x07,
0x21, 0x60, 0xe0, 0xda, 0x80, 0xfd, 0xeb, 0xec,
0xdf, 0x27, 0xae, 0x81, 0x66, 0xf5, 0xe2, 0xa7,
0x31, 0x27, 0x0c, 0x98, 0xea, 0x2d, 0x29, 0x46,
0xcb, 0x52, 0x31, 0x02, 0x90, 0x63, 0x66, 0x8a,
0xf2, 0x15, 0x87, 0x91, 0x55, 0xda, 0x21, 0xac,
0xa7, 0x9b, 0x07, 0x0e, 0x04, 0xc0, 0xee, 0x9a,
0xcd, 0xf5, 0x8f, 0x55, 0xcf, 0xa8, 0x15, 0xa5,
}, false},
{"base", seccomp.FlagExt, []byte{
0xdc, 0x7f, 0x2e, 0x1c, 0x5e, 0x82, 0x9b, 0x79,
0xeb, 0xb7, 0xef, 0xc7, 0x59, 0x15, 0x0f, 0x54,
0xa8, 0x3a, 0x75, 0xc8, 0xdf, 0x6f, 0xee, 0x4d,
0xce, 0x5d, 0xad, 0xc4, 0x73, 0x6c, 0x58, 0x5d,
0x4d, 0xee, 0xbf, 0xeb, 0x3c, 0x79, 0x69, 0xaf,
0x3a, 0x07, 0x7e, 0x90, 0xb7, 0x7b, 0xb4, 0x74,
0x1d, 0xb0, 0x5d, 0x90, 0x99, 0x7c, 0x86, 0x59,
0xb9, 0x58, 0x91, 0x20, 0x6a, 0xc9, 0x95, 0x2d,
}, false},
{"everything", seccomp.FlagExt |
seccomp.FlagDenyNS | seccomp.FlagDenyTTY | seccomp.FlagDenyDevel |
seccomp.FlagMultiarch | seccomp.FlagLinux32 | seccomp.FlagCan |
seccomp.FlagBluetooth, []byte{
0xe9, 0x9d, 0xd3, 0x45, 0xe1, 0x95, 0x41, 0x34,
0x73, 0xd3, 0xcb, 0xee, 0x07, 0xb4, 0xed, 0x57,
0xb9, 0x08, 0xbf, 0xa8, 0x9e, 0xa2, 0x07, 0x2f,
0xe9, 0x34, 0x82, 0x84, 0x7f, 0x50, 0xb5, 0xb7,
0x58, 0xda, 0x17, 0xe7, 0x4c, 0xa2, 0xbb, 0xc0,
0x08, 0x13, 0xde, 0x49, 0xa2, 0xb9, 0xbf, 0x83,
0x4c, 0x02, 0x4e, 0xd4, 0x88, 0x50, 0xbe, 0x69,
0xb6, 0x8a, 0x9a, 0x4c, 0x5f, 0x53, 0xa9, 0xdb,
}, false},
{"strict", seccomp.FlagExt |
seccomp.FlagDenyNS | seccomp.FlagDenyTTY | seccomp.FlagDenyDevel, []byte{
0xe8, 0x80, 0x29, 0x8d, 0xf2, 0xbd, 0x67, 0x51,
0xd0, 0x04, 0x0f, 0xc2, 0x1b, 0xc0, 0xed, 0x4c,
0x00, 0xf9, 0x5d, 0xc0, 0xd7, 0xba, 0x50, 0x6c,
0x24, 0x4d, 0x8b, 0x8c, 0xf6, 0x86, 0x6d, 0xba,
0x8e, 0xf4, 0xa3, 0x32, 0x96, 0xf2, 0x87, 0xb6,
0x6c, 0xcc, 0xc1, 0xd7, 0x8e, 0x97, 0x02, 0x65,
0x97, 0xf8, 0x4c, 0xc7, 0xde, 0xc1, 0x57, 0x3e,
0x14, 0x89, 0x60, 0xfb, 0xd3, 0x5c, 0xd7, 0x35,
}, false},
{"strict compat", 0 |
seccomp.FlagDenyNS | seccomp.FlagDenyTTY | seccomp.FlagDenyDevel, []byte{
0x39, 0x87, 0x1b, 0x93, 0xff, 0xaf, 0xc8, 0xb9,
0x79, 0xfc, 0xed, 0xc0, 0xb0, 0xc3, 0x7b, 0x9e,
0x03, 0x92, 0x2f, 0x5b, 0x02, 0x74, 0x8d, 0xc5,
0xc3, 0xc1, 0x7c, 0x92, 0x52, 0x7f, 0x6e, 0x02,
0x2e, 0xde, 0x1f, 0x48, 0xbf, 0xf5, 0x92, 0x46,
0xea, 0x45, 0x2c, 0x0d, 0x1d, 0xe5, 0x48, 0x27,
0x80, 0x8b, 0x1a, 0x6f, 0x84, 0xf3, 0x2b, 0xbd,
0xe1, 0xaa, 0x02, 0xae, 0x30, 0xee, 0xdc, 0xfa,
}, false},
}
buf := make([]byte, 8)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
seccomp.CPrintln = fmsg.Println
t.Cleanup(func() { seccomp.CPrintln = nil })
e := seccomp.New(tc.opts)
digest := sha512.New()
if _, err := io.CopyBuffer(digest, e, buf); (err != nil) != tc.wantErr {
t.Errorf("Exporter: error = %v, wantErr %v", err, tc.wantErr)
return
}
if err := e.Close(); err != nil {
t.Errorf("Close: error = %v", err)
return
}
if got := digest.Sum(nil); slices.Compare(got, tc.want) != 0 {
t.Fatalf("Export() hash = %x, want %x",
got, tc.want)
return
}
})
}
t.Run("close without use", func(t *testing.T) {
e := seccomp.New(0)
if err := e.Close(); !errors.Is(err, syscall.EINVAL) {
t.Errorf("Close: error = %v", err)
return
}
})
t.Run("close partial read", func(t *testing.T) {
e := seccomp.New(0)
if _, err := e.Read(make([]byte, 0)); err != nil {
t.Errorf("Read: error = %v", err)
return
}
if err := e.Close(); err == nil || err.Error() != "seccomp_export_bpf failed: operation canceled" {
t.Errorf("Close: error = %v", err)
return
}
})
}
func BenchmarkExport(b *testing.B) {
buf := make([]byte, 8)
for i := 0; i < b.N; i++ {
e := seccomp.New(seccomp.FlagExt |
seccomp.FlagDenyNS | seccomp.FlagDenyTTY | seccomp.FlagDenyDevel |
seccomp.FlagMultiarch | seccomp.FlagLinux32 | seccomp.FlagCan |
seccomp.FlagBluetooth)
if _, err := io.CopyBuffer(io.Discard, e, buf); err != nil {
b.Fatalf("cannot export: %v", err)
}
if err := e.Close(); err != nil {
b.Fatalf("cannot close exporter: %v", err)
}
}
}

View File

@ -48,14 +48,6 @@ struct f_syscall_act {
} \ } \
} while (0) } while (0)
int f_tmpfile_fd() {
FILE *f = tmpfile();
if (f == NULL)
return -1;
return fileno(f);
}
int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts) { int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts) {
int32_t res = 0; // refer to resErr for meaning int32_t res = 0; // refer to resErr for meaning
int allow_multiarch = opts & F_MULTIARCH; int allow_multiarch = opts & F_MULTIARCH;

View File

@ -20,5 +20,4 @@ typedef enum {
} f_syscall_opts; } f_syscall_opts;
extern void F_println(char *v); extern void F_println(char *v);
int f_tmpfile_fd();
int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts); int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts);

View File

@ -9,7 +9,6 @@ import "C"
import ( import (
"errors" "errors"
"fmt" "fmt"
"os"
"runtime" "runtime"
) )
@ -29,24 +28,24 @@ type SyscallOpts = C.f_syscall_opts
const ( const (
flagVerbose SyscallOpts = C.F_VERBOSE flagVerbose SyscallOpts = C.F_VERBOSE
// FlagExt are project-specific extensions.
FlagExt SyscallOpts = C.F_EXT FlagExt SyscallOpts = C.F_EXT
// FlagDenyNS denies namespace setup syscalls.
FlagDenyNS SyscallOpts = C.F_DENY_NS FlagDenyNS SyscallOpts = C.F_DENY_NS
// FlagDenyTTY denies faking input.
FlagDenyTTY SyscallOpts = C.F_DENY_TTY FlagDenyTTY SyscallOpts = C.F_DENY_TTY
// FlagDenyDevel denies development-related syscalls.
FlagDenyDevel SyscallOpts = C.F_DENY_DEVEL FlagDenyDevel SyscallOpts = C.F_DENY_DEVEL
// FlagMultiarch allows multiarch/emulation.
FlagMultiarch SyscallOpts = C.F_MULTIARCH FlagMultiarch SyscallOpts = C.F_MULTIARCH
// FlagLinux32 sets PER_LINUX32.
FlagLinux32 SyscallOpts = C.F_LINUX32 FlagLinux32 SyscallOpts = C.F_LINUX32
// FlagCan allows AF_CAN.
FlagCan SyscallOpts = C.F_CAN FlagCan SyscallOpts = C.F_CAN
// FlagBluetooth allows AF_BLUETOOTH.
FlagBluetooth SyscallOpts = C.F_BLUETOOTH FlagBluetooth SyscallOpts = C.F_BLUETOOTH
) )
func tmpfile() (*os.File, error) {
fd, err := C.f_tmpfile_fd()
if err != nil {
return nil, err
}
return os.NewFile(uintptr(fd), "tmpfile"), err
}
func exportFilter(fd uintptr, opts SyscallOpts) error { func exportFilter(fd uintptr, opts SyscallOpts) error {
var ( var (
arch C.uint32_t = 0 arch C.uint32_t = 0

View File

@ -1,7 +1,9 @@
package helper package helper
import ( import (
"context"
"flag" "flag"
"fmt"
"io" "io"
"os" "os"
"os/exec" "os/exec"
@ -11,6 +13,7 @@ import (
"testing" "testing"
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
) )
@ -18,20 +21,23 @@ import (
// it is part of the implementation of the helper stub. // it is part of the implementation of the helper stub.
func InternalChildStub() { func InternalChildStub() {
// this test mocks the helper process // this test mocks the helper process
if os.Getenv(FortifyHelper) != "1" || var ap, sp string
os.Getenv(FortifyStatus) == "-1" { // this indicates the stub is being invoked as a bwrap child without pipes if v, ok := os.LookupEnv(FortifyHelper); !ok {
return return
} else {
ap = v
}
if v, ok := os.LookupEnv(FortifyStatus); !ok {
panic(FortifyStatus)
} else {
sp = v
} }
argsFD := flag.Int("args", -1, "")
statFD := flag.Int("fd", -1, "")
_ = flag.CommandLine.Parse(os.Args[4:])
switch os.Args[3] { switch os.Args[3] {
case "bwrap": case "bwrap":
bwrapStub(argsFD, statFD) bwrapStub()
default: default:
genericStub(argsFD, statFD) genericStub(flagRestoreFiles(4, ap, sp))
} }
fmsg.Exit(0) fmsg.Exit(0)
@ -40,57 +46,65 @@ func InternalChildStub() {
// InternalReplaceExecCommand is an internal function but exported because it is cross-package; // InternalReplaceExecCommand is an internal function but exported because it is cross-package;
// it is part of the implementation of the helper stub. // it is part of the implementation of the helper stub.
func InternalReplaceExecCommand(t *testing.T) { func InternalReplaceExecCommand(t *testing.T) {
t.Cleanup(func() { t.Cleanup(func() { commandContext = exec.CommandContext })
execCommand = exec.Command
})
// replace execCommand to have the resulting *exec.Cmd launch TestHelperChildStub // replace execCommand to have the resulting *exec.Cmd launch TestHelperChildStub
execCommand = func(name string, arg ...string) *exec.Cmd { commandContext = func(ctx context.Context, name string, arg ...string) *exec.Cmd {
// pass through nonexistent path // pass through nonexistent path
if name == "/nonexistent" && len(arg) == 0 { if name == "/nonexistent" && len(arg) == 0 {
return exec.Command(name) return exec.CommandContext(ctx, name)
} }
return exec.Command(os.Args[0], append([]string{"-test.run=TestHelperChildStub", "--", name}, arg...)...) return exec.CommandContext(ctx, os.Args[0], append([]string{"-test.run=TestHelperChildStub", "--", name}, arg...)...)
} }
} }
func genericStub(argsFD, statFD *int) { func newFile(fd int, name, p string) *os.File {
// simulate args pipe behaviour present := false
func() { switch p {
if *argsFD == -1 { case "0":
panic("attempted to start helper without passing args pipe fd") case "1":
present = true
default:
panic(fmt.Sprintf("%s fd has unexpected presence value %q", name, p))
} }
f := os.NewFile(uintptr(*argsFD), "|0") f := os.NewFile(uintptr(fd), name)
if f == nil { if !present && f != nil {
panic("attempted to start helper without args pipe") panic(fmt.Sprintf("%s fd set but not present", name))
}
if present && f == nil {
panic(fmt.Sprintf("%s fd preset but unset", name))
} }
if _, err := io.Copy(os.Stdout, f); err != nil { return f
}
func flagRestoreFiles(offset int, ap, sp string) (argsFile, statFile *os.File) {
argsFd := flag.Int("args", -1, "")
statFd := flag.Int("fd", -1, "")
_ = flag.CommandLine.Parse(os.Args[offset:])
argsFile = newFile(*argsFd, "args", ap)
statFile = newFile(*statFd, "stat", sp)
return
}
func genericStub(argsFile, statFile *os.File) {
if argsFile != nil {
// this output is checked by parent
if _, err := io.Copy(os.Stdout, argsFile); err != nil {
panic("cannot read args: " + err.Error()) panic("cannot read args: " + err.Error())
} }
}() }
var wait chan struct{}
// simulate status pipe behaviour // simulate status pipe behaviour
if os.Getenv(FortifyStatus) == "1" { if statFile != nil {
if *statFD == -1 { if _, err := statFile.Write([]byte{'x'}); err != nil {
panic("attempted to start helper with status reporting without passing status pipe fd")
}
wait = make(chan struct{})
go func() {
f := os.NewFile(uintptr(*statFD), "|1")
if f == nil {
panic("attempted to start with status reporting without status pipe")
}
if _, err := f.Write([]byte{'x'}); err != nil {
panic("cannot write to status pipe: " + err.Error()) panic("cannot write to status pipe: " + err.Error())
} }
done := make(chan struct{})
go func() {
// wait for status pipe close // wait for status pipe close
var epoll int var epoll int
if fd, err := syscall.EpollCreate1(0); err != nil { if fd, err := syscall.EpollCreate1(0); err != nil {
@ -103,7 +117,7 @@ func genericStub(argsFD, statFD *int) {
}() }()
epoll = fd epoll = fd
} }
if err := syscall.EpollCtl(epoll, syscall.EPOLL_CTL_ADD, int(f.Fd()), &syscall.EpollEvent{}); err != nil { if err := syscall.EpollCtl(epoll, syscall.EPOLL_CTL_ADD, int(statFile.Fd()), &syscall.EpollEvent{}); err != nil {
panic("cannot add status pipe to epoll: " + err.Error()) panic("cannot add status pipe to epoll: " + err.Error())
} }
events := make([]syscall.EpollEvent, 1) events := make([]syscall.EpollEvent, 1)
@ -114,50 +128,36 @@ func genericStub(argsFD, statFD *int) {
panic(strconv.Itoa(int(events[0].Events))) panic(strconv.Itoa(int(events[0].Events)))
} }
close(wait) close(done)
}() }()
} <-done
if wait != nil {
<-wait
} }
} }
func bwrapStub(argsFD, statFD *int) { func bwrapStub() {
// the bwrap launcher does not ever launch with sync fd // the bwrap launcher does not launch with a typical sync fd
if *statFD != -1 { argsFile, _ := flagRestoreFiles(4, "1", "0")
panic("attempted to launch bwrap with status monitoring")
}
// test args pipe behaviour // test args pipe behaviour
func() { func() {
if *argsFD == -1 {
panic("attempted to start bwrap without passing args pipe fd")
}
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) got, want := new(strings.Builder), new(strings.Builder)
if _, err := io.Copy(got, argsFile); err != nil {
if _, err := io.Copy(got, f); err != nil { panic("cannot read bwrap args: " + err.Error())
panic("cannot read args: " + err.Error())
} }
// hardcoded bwrap configuration used by test // hardcoded bwrap configuration used by test
if _, err := MustNewCheckedArgs((&bwrap.Config{ sc := &bwrap.Config{
Unshare: nil,
Net: true, Net: true,
UserNS: false,
Hostname: "localhost", Hostname: "localhost",
Chdir: "/nonexistent", Chdir: "/nonexistent",
Clearenv: true, Clearenv: true,
NewSession: true, NewSession: true,
DieWithParent: true, DieWithParent: true,
AsInit: true, AsInit: true,
}).Args()).WriteTo(want); err != nil { }
args := sc.Args()
sc.FDArgs(nil, &args, new(proc.ExtraFilesPre), new([]proc.File))
if _, err := MustNewCheckedArgs(args).WriteTo(want); err != nil {
panic("cannot read want: " + err.Error()) panic("cannot read want: " + err.Error())
} }

View File

@ -6,7 +6,7 @@ import (
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/linux" "git.gensokyo.uk/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/proc/priv/shim" "git.gensokyo.uk/security/fortify/internal/priv/shim"
) )
type App interface { type App interface {

View File

@ -34,8 +34,8 @@ var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z
type appSeal struct { type appSeal struct {
// app unique ID string representation // app unique ID string representation
id string id string
// dbus proxy message buffer retriever // dump dbus proxy message buffer
dbusMsg func(f func(msgbuf []string)) dbusMsg func()
// freedesktop application ID // freedesktop application ID
fid string fid string

View File

@ -11,7 +11,7 @@ import (
"git.gensokyo.uk/security/fortify/helper" "git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/proc/priv/shim" "git.gensokyo.uk/security/fortify/internal/priv/shim"
"git.gensokyo.uk/security/fortify/internal/state" "git.gensokyo.uk/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/internal/system" "git.gensokyo.uk/security/fortify/internal/system"
) )
@ -46,7 +46,7 @@ func (a *app) Run(ctx context.Context, rs *RunState) error {
} }
// startup will go ahead, commit system setup // startup will go ahead, commit system setup
if err := a.seal.sys.Commit(); err != nil { if err := a.seal.sys.Commit(ctx); err != nil {
return err return err
} }
a.seal.sys.needRevert = true a.seal.sys.needRevert = true
@ -140,11 +140,7 @@ func (a *app) Run(ctx context.Context, rs *RunState) error {
// print queued up dbus messages // print queued up dbus messages
if a.seal.dbusMsg != nil { if a.seal.dbusMsg != nil {
a.seal.dbusMsg(func(msgbuf []string) { a.seal.dbusMsg()
for _, msg := range msgbuf {
fmsg.Println(msg)
}
})
} }
// update store and revert app setup transaction // update store and revert app setup transaction

View File

@ -8,9 +8,9 @@ import (
"syscall" "syscall"
"time" "time"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/proc"
) )
const ( const (

View File

@ -1,18 +1,22 @@
package shim package shim
import ( import (
"context"
"errors" "errors"
"os" "os"
"os/exec"
"os/signal"
"path" "path"
"strconv" "strconv"
"syscall"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper" "git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/helper/seccomp" "git.gensokyo.uk/security/fortify/helper/seccomp"
"git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/proc" init0 "git.gensokyo.uk/security/fortify/internal/priv/init"
init0 "git.gensokyo.uk/security/fortify/internal/proc/priv/init"
) )
// everything beyond this point runs as unconstrained target user // everything beyond this point runs as unconstrained target user
@ -138,19 +142,22 @@ func Main() {
); err != nil { ); err != nil {
fmsg.Fatalf("malformed sandbox config: %v", err) fmsg.Fatalf("malformed sandbox config: %v", err)
} else { } else {
cmd := b.Unwrap() b.Stdin(os.Stdin).Stdout(os.Stdout).Stderr(os.Stderr)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop() // unreachable
// run and pass through exit code // run and pass through exit code
if err = b.Start(); err != nil { if err = b.Start(ctx, false); err != nil {
fmsg.Fatalf("cannot start target process: %v", err) fmsg.Fatalf("cannot start target process: %v", err)
} else if err = b.Wait(); err != nil { } else if err = b.Wait(); err != nil {
fmsg.VPrintln("wait:", err) var exitError *exec.ExitError
} if !errors.As(err, &exitError) {
if b.Unwrap().ProcessState != nil { fmsg.Println("wait:", err)
fmsg.Exit(b.Unwrap().ProcessState.ExitCode())
} else {
fmsg.Exit(127) fmsg.Exit(127)
panic("unreachable")
}
fmsg.Exit(exitError.ExitCode())
panic("unreachable")
} }
} }
} }

View File

@ -10,9 +10,9 @@ import (
"strings" "strings"
"time" "time"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/proc"
) )
// used by the parent process // used by the parent process

View File

@ -1,17 +0,0 @@
package proc
import (
"os"
"os/exec"
)
func ExtraFile(cmd *exec.Cmd, f *os.File) (fd uintptr) {
return ExtraFileSlice(&cmd.ExtraFiles, f)
}
func ExtraFileSlice(extraFiles *[]*os.File, f *os.File) (fd uintptr) {
// ExtraFiles: If non-nil, entry i becomes file descriptor 3+i.
fd = uintptr(3 + len(*extraFiles))
*extraFiles = append(*extraFiles, f)
return
}

View File

@ -3,7 +3,6 @@ package system
import ( import (
"bytes" "bytes"
"errors" "errors"
"os"
"strings" "strings"
"sync" "sync"
@ -23,12 +22,9 @@ func (sys *I) MustProxyDBus(sessionPath string, session *dbus.Config, systemPath
} }
} }
func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath string) (func(f func(msgbuf []string)), error) { func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath string) (func(), error) {
d := new(DBus) d := new(DBus)
// used by waiting goroutine to notify process exit
d.done = make(chan struct{})
// session bus is mandatory // session bus is mandatory
if session == nil { if session == nil {
return nil, fmsg.WrapError(ErrDBusConfig, return nil, fmsg.WrapError(ErrDBusConfig,
@ -65,7 +61,7 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st
// seal dbus proxy // seal dbus proxy
d.out = &scanToFmsg{msg: new(strings.Builder)} d.out = &scanToFmsg{msg: new(strings.Builder)}
return d.out.F, fmsg.WrapErrorSuffix(d.proxy.Seal(session, system), return d.out.Dump, fmsg.WrapErrorSuffix(d.proxy.Seal(session, system),
"cannot seal message bus proxy:") "cannot seal message bus proxy:")
} }
@ -75,84 +71,34 @@ type DBus struct {
out *scanToFmsg out *scanToFmsg
// whether system bus proxy is enabled // whether system bus proxy is enabled
system bool system bool
// notification from goroutine waiting for dbus.Proxy
done chan struct{}
} }
func (d *DBus) Type() Enablement { func (d *DBus) Type() Enablement {
return Process return Process
} }
func (d *DBus) apply(_ *I) error { func (d *DBus) apply(sys *I) error {
fmsg.VPrintf("session bus proxy on %q for upstream %q", d.proxy.Session()[1], d.proxy.Session()[0]) fmsg.VPrintf("session bus proxy on %q for upstream %q", d.proxy.Session()[1], d.proxy.Session()[0])
if d.system { if d.system {
fmsg.VPrintf("system bus proxy on %q for upstream %q", d.proxy.System()[1], d.proxy.System()[0]) fmsg.VPrintf("system bus proxy on %q for upstream %q", d.proxy.System()[1], d.proxy.System()[0])
} }
// ready channel passed to dbus package // this starts the process and blocks until ready
ready := make(chan error, 1) if err := d.proxy.Start(sys.ctx, d.out, true); err != nil {
d.out.Dump()
// background dbus proxy start
if err := d.proxy.Start(ready, d.out, true, true); err != nil {
return fmsg.WrapErrorSuffix(err, return fmsg.WrapErrorSuffix(err,
"cannot start message bus proxy:") "cannot start message bus proxy:")
} }
fmsg.VPrintln("starting message bus proxy:", d.proxy) fmsg.VPrintln("starting message bus proxy:", d.proxy)
if fmsg.Verbose() { // save the extra bwrap arg build when verbose logging is off
fmsg.VPrintln("message bus proxy bwrap args:", d.proxy.BwrapStatic())
}
// background wait for proxy instance and notify completion
go func() {
if err := d.proxy.Wait(); err != nil {
fmsg.Println("message bus proxy exited with error:", err)
go func() { ready <- err }()
} else {
fmsg.VPrintln("message bus proxy exit")
}
// ensure socket removal so ephemeral directory is empty at revert
if err := os.Remove(d.proxy.Session()[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
fmsg.Println("cannot remove dangling session bus socket:", err)
}
if d.system {
if err := os.Remove(d.proxy.System()[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
fmsg.Println("cannot remove dangling system bus socket:", err)
}
}
// notify proxy completion
close(d.done)
}()
// ready is not nil if the proxy process faulted
if err := <-ready; err != nil {
// note that err here is either an I/O error or a predetermined unexpected behaviour error
return fmsg.WrapErrorSuffix(err,
"message bus proxy fault after start:")
}
fmsg.VPrintln("message bus proxy ready")
return nil return nil
} }
func (d *DBus) revert(_ *I, _ *Criteria) error { func (d *DBus) revert(_ *I, _ *Criteria) error {
// criteria ignored here since dbus is always process-scoped // criteria ignored here since dbus is always process-scoped
fmsg.VPrintln("terminating message bus proxy") fmsg.VPrintln("terminating message bus proxy")
d.proxy.Close()
if err := d.proxy.Close(); err != nil { defer fmsg.VPrintln("message bus proxy exit")
if errors.Is(err, os.ErrClosed) { return fmsg.WrapErrorSuffix(d.proxy.Wait(), "message bus proxy error:")
return fmsg.WrapError(err,
"message bus proxy already closed")
} else {
return fmsg.WrapErrorSuffix(err,
"cannot stop message bus proxy:")
}
}
// block until proxy wait returns
<-d.done
return nil
} }
func (d *DBus) Is(o Op) bool { func (d *DBus) Is(o Op) bool {
@ -195,8 +141,10 @@ func (s *scanToFmsg) write(p []byte, a int) (int, error) {
} }
} }
func (s *scanToFmsg) F(f func(msgbuf []string)) { func (s *scanToFmsg) Dump() {
s.mu.RLock() s.mu.RLock()
f(s.msgbuf) for _, msg := range s.msgbuf {
fmsg.Println(msg)
}
s.mu.RUnlock() s.mu.RUnlock()
} }

View File

@ -1,6 +1,7 @@
package system package system
import ( import (
"context"
"errors" "errors"
"os" "os"
"sync" "sync"
@ -57,9 +58,13 @@ func TypeString(e Enablement) string {
type I struct { type I struct {
uid int uid int
ops []Op ops []Op
ctx context.Context
// sync fd passed to bwrap
sp *os.File sp *os.File
state [2]bool // whether sys has been reverted
state bool
lock sync.Mutex lock sync.Mutex
} }
@ -85,14 +90,14 @@ func (sys *I) Equal(v *I) bool {
return true return true
} }
func (sys *I) Commit() error { func (sys *I) Commit(ctx context.Context) error {
sys.lock.Lock() sys.lock.Lock()
defer sys.lock.Unlock() defer sys.lock.Unlock()
if sys.state[0] { if sys.ctx != nil {
panic("sys instance committed twice") panic("sys instance committed twice")
} }
sys.state[0] = true sys.ctx = ctx
sp := New(sys.uid) sp := New(sys.uid)
sp.ops = make([]Op, 0, len(sys.ops)) // prevent copies during commits sp.ops = make([]Op, 0, len(sys.ops)) // prevent copies during commits
@ -125,10 +130,10 @@ func (sys *I) Revert(ec *Criteria) error {
sys.lock.Lock() sys.lock.Lock()
defer sys.lock.Unlock() defer sys.lock.Unlock()
if sys.state[1] { if sys.state {
panic("sys instance reverted twice") panic("sys instance reverted twice")
} }
sys.state[1] = true sys.state = true
// collect errors // collect errors
errs := make([]error, len(sys.ops)) errs := make([]error, len(sys.ops))

View File

@ -1,20 +1,19 @@
package ldd package ldd
import ( import (
"fmt" "context"
"os" "os"
"os/exec"
"strings" "strings"
"time"
"git.gensokyo.uk/security/fortify/helper" "git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.gensokyo.uk/security/fortify/helper/bwrap"
) )
func Exec(p string) ([]*Entry, error) { const lddTimeout = 2 * time.Second
var (
h helper.Helper func Exec(ctx context.Context, p string) ([]*Entry, error) {
cmd *exec.Cmd var h helper.Helper
)
if b, err := helper.NewBwrap( if b, err := helper.NewBwrap(
(&bwrap.Config{ (&bwrap.Config{
@ -29,17 +28,20 @@ func Exec(p string) ([]*Entry, error) {
); err != nil { ); err != nil {
return nil, err return nil, err
} else { } else {
cmd = b.Unwrap()
h = b h = b
} }
cmd.Stdout, cmd.Stderr = new(strings.Builder), os.Stderr stdout := new(strings.Builder)
if err := h.Start(); err != nil { h.Stdout(stdout).Stderr(os.Stderr)
c, cancel := context.WithTimeout(ctx, lddTimeout)
defer cancel()
if err := h.Start(c, false); err != nil {
return nil, err return nil, err
} }
if err := h.Wait(); err != nil { if err := h.Wait(); err != nil {
return nil, err return nil, err
} }
return Parse(cmd.Stdout.(fmt.Stringer)) return Parse(stdout)
} }

18
main.go
View File

@ -21,8 +21,8 @@ import (
"git.gensokyo.uk/security/fortify/internal/app" "git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/linux" "git.gensokyo.uk/security/fortify/internal/linux"
init0 "git.gensokyo.uk/security/fortify/internal/proc/priv/init" init0 "git.gensokyo.uk/security/fortify/internal/priv/init"
"git.gensokyo.uk/security/fortify/internal/proc/priv/shim" "git.gensokyo.uk/security/fortify/internal/priv/shim"
"git.gensokyo.uk/security/fortify/internal/system" "git.gensokyo.uk/security/fortify/internal/system"
) )
@ -307,22 +307,14 @@ func main() {
func runApp(config *fst.Config) { func runApp(config *fst.Config) {
rs := new(app.RunState) rs := new(app.RunState)
ctx, cancel := context.WithCancel(context.Background()) ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable
if fmsg.Verbose() { if fmsg.Verbose() {
seccomp.CPrintln = fmsg.Println seccomp.CPrintln = fmsg.Println
} }
// handle signals for graceful shutdown
sig := make(chan os.Signal, 2)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
go func() {
v := <-sig
fmsg.Printf("got %s after program start", v)
cancel()
signal.Ignore(syscall.SIGINT, syscall.SIGTERM)
}()
if a, err := app.New(sys); err != nil { if a, err := app.New(sys); err != nil {
fmsg.Fatalf("cannot create app: %s\n", err) fmsg.Fatalf("cannot create app: %s\n", err)
} else if err = a.Seal(config); err != nil { } else if err = a.Seal(config); err != nil {

View File

@ -36,7 +36,7 @@ package
*Default:* *Default:*
` <derivation fortify-0.2.12> ` ` <derivation fortify-0.2.13> `

View File

@ -16,7 +16,7 @@
buildGoModule rec { buildGoModule rec {
pname = "fortify"; pname = "fortify";
version = "0.2.12"; version = "0.2.13";
src = builtins.path { src = builtins.path {
name = "fortify-src"; name = "fortify-src";

View File

@ -147,6 +147,18 @@ nixosTest {
pulse = false; pulse = false;
}; };
} }
{
name = "strace-failure";
verbose = true;
share = pkgs.strace;
command = "strace true";
capability = {
wayland = false;
x11 = false;
dbus = false;
pulse = false;
};
}
]; ];
}; };
@ -248,9 +260,6 @@ nixosTest {
# Deny unmapped uid: # Deny unmapped uid:
print(machine.fail("sudo -u untrusted -i ${self.packages.${system}.fortify}/bin/fortify -v run")) print(machine.fail("sudo -u untrusted -i ${self.packages.${system}.fortify}/bin/fortify -v run"))
# Create fortify uid 0 state directory:
machine.succeed("install -dm 0755 -o u0_a0 -g users /var/lib/fortify/u0")
# Start fortify permissive defaults outside Wayland session: # Start fortify permissive defaults outside Wayland session:
print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare")) print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare"))
machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-bare") machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-bare")
@ -313,6 +322,9 @@ nixosTest {
machine.send_chars("exit\n") machine.send_chars("exit\n")
machine.wait_until_fails("pgrep alacritty") machine.wait_until_fails("pgrep alacritty")
# Test syscall filter:
print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure"))
# Exit Sway and verify process exit status 0: # Exit Sway and verify process exit status 0:
swaymsg("exit", succeed=False) swaymsg("exit", succeed=False)
machine.wait_until_fails("pgrep -x sway") machine.wait_until_fails("pgrep -x sway")