Compare commits
51 Commits
ea853e21d9
...
24618ab9a1
Author | SHA1 | Date | |
---|---|---|---|
24618ab9a1 | |||
9ce4706a07 | |||
9a1f8e129f | |||
ee10860357 | |||
44277dc0f1 | |||
bc54db54d2 | |||
bf07b7cd9e | |||
5d3c8dcc92 | |||
48feca800f | |||
42de09e896 | |||
1576fea8a3 | |||
ae522ab364 | |||
273d97af85 | |||
891316d924 | |||
9f5dad1998 | |||
6e7ddb2d2e | |||
bac4e67867 | |||
4230281194 | |||
e64e7608ca | |||
10a21ce3ef | |||
0f1f0e4364 | |||
f9bf20a3c7 | |||
73c1a83032 | |||
f443d315ad | |||
9e18d1de77 | |||
2647a71be1 | |||
7c60a4d8e8 | |||
4bb5d9780f | |||
f41fd94628 | |||
94895bbacb | |||
f332200ca4 | |||
2eff470091 | |||
a092b042ab | |||
e94b09d337 | |||
5d9e669d97 | |||
f1002157a5 | |||
4133b555ba | |||
9b1a60b5c9 | |||
beb3918809 | |||
2871426df2 | |||
e048f31baa | |||
6af8b8859f | |||
f38ba7e923 | |||
d22145a392 | |||
29c3f8becb | |||
be16970e77 | |||
df266527f1 | |||
c8ed7aae6e | |||
61e58aa14d | |||
9e15898c8f | |||
f7bd6a5a41 |
@ -13,12 +13,12 @@ import (
|
|||||||
"git.gensokyo.uk/security/fortify/command"
|
"git.gensokyo.uk/security/fortify/command"
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||||
"git.gensokyo.uk/security/fortify/helper/seccomp"
|
|
||||||
"git.gensokyo.uk/security/fortify/internal"
|
"git.gensokyo.uk/security/fortify/internal"
|
||||||
init0 "git.gensokyo.uk/security/fortify/internal/app/init"
|
"git.gensokyo.uk/security/fortify/internal/app/init0"
|
||||||
"git.gensokyo.uk/security/fortify/internal/app/shim"
|
"git.gensokyo.uk/security/fortify/internal/app/shim"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
"git.gensokyo.uk/security/fortify/internal/sys"
|
"git.gensokyo.uk/security/fortify/internal/sys"
|
||||||
|
"git.gensokyo.uk/security/fortify/sandbox"
|
||||||
)
|
)
|
||||||
|
|
||||||
const shellPath = "/run/current-system/sw/bin/bash"
|
const shellPath = "/run/current-system/sw/bin/bash"
|
||||||
@ -37,10 +37,11 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// early init argv0 check, skips root check and duplicate PR_SET_DUMPABLE
|
// early init path, skips root check and duplicate PR_SET_DUMPABLE
|
||||||
|
sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
|
||||||
init0.TryArgv0()
|
init0.TryArgv0()
|
||||||
|
|
||||||
if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
|
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
|
||||||
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
|
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
|
||||||
// not fatal: this program runs as the privileged user
|
// not fatal: this program runs as the privileged user
|
||||||
}
|
}
|
||||||
@ -58,10 +59,7 @@ func main() {
|
|||||||
flagDropShell bool
|
flagDropShell bool
|
||||||
)
|
)
|
||||||
c := command.New(os.Stderr, log.Printf, "fpkg", func([]string) error {
|
c := command.New(os.Stderr, log.Printf, "fpkg", func([]string) error {
|
||||||
fmsg.Store(flagVerbose)
|
internal.InstallFmsg(flagVerbose)
|
||||||
if flagVerbose {
|
|
||||||
seccomp.CPrintln = log.Println
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}).
|
}).
|
||||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
|
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
echo 'output Virtual-1 res 1680x1050') > ~/.config/sway/config
|
echo 'output Virtual-1 res 1680x1050') > ~/.config/sway/config
|
||||||
|
|
||||||
sway --validate
|
sway --validate
|
||||||
systemd-cat --identifier=sway sway && touch /tmp/sway-exit-ok
|
systemd-cat --identifier=session sway && touch /tmp/sway-exit-ok
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
@ -1,14 +1,21 @@
|
|||||||
package dbus_test
|
package dbus_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
|
"git.gensokyo.uk/security/fortify/sandbox"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
func TestNew(t *testing.T) {
|
||||||
@ -100,15 +107,20 @@ func TestProxy_Seal(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestProxy_Start_Wait_Close_String(t *testing.T) {
|
func TestProxy_Start_Wait_Close_String(t *testing.T) {
|
||||||
t.Run("sandboxed", func(t *testing.T) {
|
oldWaitDelay := helper.WaitDelay
|
||||||
|
helper.WaitDelay = 16 * time.Second
|
||||||
|
t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
|
||||||
|
|
||||||
|
t.Run("sandbox", func(t *testing.T) {
|
||||||
|
proxyName := dbus.ProxyName
|
||||||
|
dbus.ProxyName = os.Args[0]
|
||||||
|
t.Cleanup(func() { dbus.ProxyName = proxyName })
|
||||||
testProxyStartWaitCloseString(t, true)
|
testProxyStartWaitCloseString(t, true)
|
||||||
})
|
})
|
||||||
t.Run("direct", func(t *testing.T) {
|
t.Run("direct", func(t *testing.T) { testProxyStartWaitCloseString(t, false) })
|
||||||
testProxyStartWaitCloseString(t, false)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
|
func testProxyStartWaitCloseString(t *testing.T, useSandbox bool) {
|
||||||
for id, tc := range testCasePairs() {
|
for id, tc := range testCasePairs() {
|
||||||
// this test does not test errors
|
// this test does not test errors
|
||||||
if tc[0].wantErr {
|
if tc[0].wantErr {
|
||||||
@ -125,14 +137,33 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("proxy for "+id, func(t *testing.T) {
|
t.Run("proxy for "+id, func(t *testing.T) {
|
||||||
helper.InternalReplaceExecCommand(t)
|
|
||||||
overridePath(t)
|
|
||||||
|
|
||||||
p := dbus.New(tc[0].bus, tc[1].bus)
|
p := dbus.New(tc[0].bus, tc[1].bus)
|
||||||
|
p.CommandContext = func(ctx context.Context) (cmd *exec.Cmd) {
|
||||||
|
return exec.CommandContext(ctx, os.Args[0], "-test.v",
|
||||||
|
"-test.run=TestHelperInit", "--", "init")
|
||||||
|
}
|
||||||
|
p.CmdF = func(v any) {
|
||||||
|
if useSandbox {
|
||||||
|
container := v.(*sandbox.Container)
|
||||||
|
if container.Args[0] != dbus.ProxyName {
|
||||||
|
panic(fmt.Sprintf("unexpected argv0 %q", os.Args[0]))
|
||||||
|
}
|
||||||
|
container.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, container.Args[1:]...)
|
||||||
|
} else {
|
||||||
|
cmd := v.(*exec.Cmd)
|
||||||
|
if cmd.Args[0] != dbus.ProxyName {
|
||||||
|
panic(fmt.Sprintf("unexpected argv0 %q", os.Args[0]))
|
||||||
|
}
|
||||||
|
cmd.Err = nil
|
||||||
|
cmd.Path = os.Args[0]
|
||||||
|
cmd.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, cmd.Args[1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.FilterF = func(v []byte) []byte { return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1] }
|
||||||
output := new(strings.Builder)
|
output := new(strings.Builder)
|
||||||
|
|
||||||
t.Run("unsealed behaviour of "+id, func(t *testing.T) {
|
t.Run("unsealed", func(t *testing.T) {
|
||||||
t.Run("unsealed string of "+id, func(t *testing.T) {
|
t.Run("string", func(t *testing.T) {
|
||||||
want := "(unsealed dbus proxy)"
|
want := "(unsealed dbus proxy)"
|
||||||
if got := p.String(); got != want {
|
if got := p.String(); got != want {
|
||||||
t.Errorf("String() = %v, want %v",
|
t.Errorf("String() = %v, want %v",
|
||||||
@ -141,16 +172,16 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unsealed start of "+id, func(t *testing.T) {
|
t.Run("start", func(t *testing.T) {
|
||||||
want := "proxy not sealed"
|
want := "proxy not sealed"
|
||||||
if err := p.Start(context.Background(), nil, sandbox); err == nil || err.Error() != want {
|
if err := p.Start(context.Background(), nil, useSandbox); 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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unsealed wait of "+id, func(t *testing.T) {
|
t.Run("wait", func(t *testing.T) {
|
||||||
wantErr := "dbus: 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",
|
||||||
@ -168,7 +199,7 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("sealed behaviour of "+id, func(t *testing.T) {
|
t.Run("sealed", func(t *testing.T) {
|
||||||
want := strings.Join(append(tc[0].want, tc[1].want...), " ")
|
want := strings.Join(append(tc[0].want, tc[1].want...), " ")
|
||||||
if got := p.String(); got != want {
|
if got := p.String(); got != want {
|
||||||
t.Errorf("String() = %v, want %v",
|
t.Errorf("String() = %v, want %v",
|
||||||
@ -176,17 +207,20 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("sealed start of "+id, func(t *testing.T) {
|
t.Run("start", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if err := p.Start(ctx, output, sandbox); err != nil {
|
if err := p.Start(ctx, output, useSandbox); err != nil {
|
||||||
t.Fatalf("Start(nil, nil) error = %v",
|
t.Fatalf("Start(nil, nil) error = %v",
|
||||||
err)
|
err)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("started string of "+id, func(t *testing.T) {
|
t.Run("string", func(t *testing.T) {
|
||||||
wantSubstr := dbus.ProxyName + " --args="
|
wantSubstr := fmt.Sprintf("%s -test.run=TestHelperStub -- --args=3 --fd=4", os.Args[0])
|
||||||
|
if useSandbox {
|
||||||
|
wantSubstr = fmt.Sprintf(`argv: ["%s" "-test.run=TestHelperStub" "--" "--args=3" "--fd=4"], flags: 0x0, seccomp: 0x3e`, os.Args[0])
|
||||||
|
}
|
||||||
if got := p.String(); !strings.Contains(got, wantSubstr) {
|
if got := p.String(); !strings.Contains(got, wantSubstr) {
|
||||||
t.Errorf("String() = %v, want %v",
|
t.Errorf("String() = %v, want %v",
|
||||||
p.String(), wantSubstr)
|
p.String(), wantSubstr)
|
||||||
@ -194,7 +228,7 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("started wait of "+id, func(t *testing.T) {
|
t.Run("wait", func(t *testing.T) {
|
||||||
p.Close()
|
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",
|
||||||
@ -207,10 +241,10 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func overridePath(t *testing.T) {
|
func TestHelperInit(t *testing.T) {
|
||||||
proxyName := dbus.ProxyName
|
if len(os.Args) != 5 || os.Args[4] != "init" {
|
||||||
dbus.ProxyName = "/nonexistent-xdg-dbus-proxy"
|
return
|
||||||
t.Cleanup(func() {
|
}
|
||||||
dbus.ProxyName = proxyName
|
sandbox.SetOutput(fmsg.Output{})
|
||||||
})
|
sandbox.Init(fmsg.Prepare, internal.InstallFmsg)
|
||||||
}
|
}
|
||||||
|
178
dbus/proc.go
Normal file
178
dbus/proc.go
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
package dbus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/helper"
|
||||||
|
"git.gensokyo.uk/security/fortify/ldd"
|
||||||
|
"git.gensokyo.uk/security/fortify/sandbox"
|
||||||
|
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Start launches the D-Bus proxy.
|
||||||
|
func (p *Proxy) Start(ctx context.Context, output io.Writer, useSandbox bool) error {
|
||||||
|
p.lock.Lock()
|
||||||
|
defer p.lock.Unlock()
|
||||||
|
|
||||||
|
if p.seal == nil {
|
||||||
|
return errors.New("proxy not sealed")
|
||||||
|
}
|
||||||
|
|
||||||
|
var h helper.Helper
|
||||||
|
|
||||||
|
c, cancel := context.WithCancelCause(ctx)
|
||||||
|
if !useSandbox {
|
||||||
|
h = helper.NewDirect(c, p.name, p.seal, true, argF, func(cmd *exec.Cmd) {
|
||||||
|
if p.CmdF != nil {
|
||||||
|
p.CmdF(cmd)
|
||||||
|
}
|
||||||
|
if output != nil {
|
||||||
|
cmd.Stdout, cmd.Stderr = output, output
|
||||||
|
}
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
|
cmd.Env = make([]string, 0)
|
||||||
|
}, nil)
|
||||||
|
} else {
|
||||||
|
toolPath := p.name
|
||||||
|
if filepath.Base(p.name) == p.name {
|
||||||
|
if s, err := exec.LookPath(p.name); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
toolPath = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var libPaths []string
|
||||||
|
if entries, err := ldd.ExecFilter(ctx, p.CommandContext, p.FilterF, toolPath); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
libPaths = ldd.Path(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
h = helper.New(
|
||||||
|
c, toolPath,
|
||||||
|
p.seal, true,
|
||||||
|
argF, func(container *sandbox.Container) {
|
||||||
|
container.Seccomp |= seccomp.FlagMultiarch
|
||||||
|
container.Hostname = "fortify-dbus"
|
||||||
|
container.CommandContext = p.CommandContext
|
||||||
|
if output != nil {
|
||||||
|
container.Stdout, container.Stderr = output, output
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.CmdF != nil {
|
||||||
|
p.CmdF(container)
|
||||||
|
}
|
||||||
|
|
||||||
|
// these lib paths are unpredictable, so mount them first so they cannot cover anything
|
||||||
|
for _, name := range libPaths {
|
||||||
|
container.Bind(name, name, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// upstream bus directories
|
||||||
|
upstreamPaths := make([]string, 0, 2)
|
||||||
|
for _, as := range []string{p.session[0], p.system[0]} {
|
||||||
|
if len(as) > 0 && strings.HasPrefix(as, "unix:path=/") {
|
||||||
|
// leave / intact
|
||||||
|
upstreamPaths = append(upstreamPaths, path.Dir(as[10:]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.Sort(upstreamPaths)
|
||||||
|
upstreamPaths = slices.Compact(upstreamPaths)
|
||||||
|
for _, name := range upstreamPaths {
|
||||||
|
container.Bind(name, name, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parent directories of bind paths
|
||||||
|
sockDirPaths := make([]string, 0, 2)
|
||||||
|
if d := path.Dir(p.session[1]); path.IsAbs(d) {
|
||||||
|
sockDirPaths = append(sockDirPaths, d)
|
||||||
|
}
|
||||||
|
if d := path.Dir(p.system[1]); path.IsAbs(d) {
|
||||||
|
sockDirPaths = append(sockDirPaths, d)
|
||||||
|
}
|
||||||
|
slices.Sort(sockDirPaths)
|
||||||
|
sockDirPaths = slices.Compact(sockDirPaths)
|
||||||
|
for _, name := range sockDirPaths {
|
||||||
|
container.Bind(name, name, sandbox.BindWritable)
|
||||||
|
}
|
||||||
|
|
||||||
|
// xdg-dbus-proxy bin path
|
||||||
|
binPath := path.Dir(toolPath)
|
||||||
|
container.Bind(binPath, binPath, 0)
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.Start(); err != nil {
|
||||||
|
cancel(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.helper = h
|
||||||
|
p.ctx = c
|
||||||
|
p.cancel = cancel
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var proxyClosed = errors.New("proxy closed")
|
||||||
|
|
||||||
|
// Wait blocks until xdg-dbus-proxy exits and releases resources.
|
||||||
|
func (p *Proxy) Wait() error {
|
||||||
|
p.lock.RLock()
|
||||||
|
defer p.lock.RUnlock()
|
||||||
|
|
||||||
|
if p.helper == nil {
|
||||||
|
return errors.New("dbus: not started")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure socket removal so ephemeral directory is empty at revert
|
||||||
|
if err := os.Remove(p.session[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func argF(argsFd, statFd int) []string {
|
||||||
|
if statFd == -1 {
|
||||||
|
return []string{"--args=" + strconv.Itoa(argsFd)}
|
||||||
|
} else {
|
||||||
|
return []string{"--args=" + strconv.Itoa(argsFd), "--fd=" + strconv.Itoa(statFd)}
|
||||||
|
}
|
||||||
|
}
|
@ -5,10 +5,10 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"os/exec"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper"
|
"git.gensokyo.uk/security/fortify/helper"
|
||||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProxyName is the file name or path to the proxy program.
|
// ProxyName is the file name or path to the proxy program.
|
||||||
@ -19,15 +19,18 @@ var ProxyName = "xdg-dbus-proxy"
|
|||||||
// Once sealed, configuration changes will no longer be possible and attempting to do so will result in a panic.
|
// Once sealed, configuration changes will no longer be possible and attempting to do so will result in a panic.
|
||||||
type Proxy struct {
|
type Proxy struct {
|
||||||
helper helper.Helper
|
helper helper.Helper
|
||||||
bwrap *bwrap.Config
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelCauseFunc
|
cancel context.CancelCauseFunc
|
||||||
|
|
||||||
name string
|
name string
|
||||||
session [2]string
|
session [2]string
|
||||||
system [2]string
|
system [2]string
|
||||||
|
CmdF func(any)
|
||||||
sysP bool
|
sysP bool
|
||||||
|
|
||||||
|
CommandContext func(ctx context.Context) (cmd *exec.Cmd)
|
||||||
|
FilterF func([]byte) []byte
|
||||||
|
|
||||||
seal io.WriterTo
|
seal io.WriterTo
|
||||||
lock sync.RWMutex
|
lock sync.RWMutex
|
||||||
}
|
}
|
||||||
|
175
dbus/run.go
175
dbus/run.go
@ -1,175 +0,0 @@
|
|||||||
package dbus
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper"
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
|
||||||
"git.gensokyo.uk/security/fortify/ldd"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Start launches the D-Bus proxy.
|
|
||||||
func (p *Proxy) Start(ctx context.Context, output io.Writer, sandbox bool) error {
|
|
||||||
p.lock.Lock()
|
|
||||||
defer p.lock.Unlock()
|
|
||||||
|
|
||||||
if p.seal == nil {
|
|
||||||
return errors.New("proxy not sealed")
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
h helper.Helper
|
|
||||||
|
|
||||||
argF = func(argsFD, statFD int) []string {
|
|
||||||
if statFD == -1 {
|
|
||||||
return []string{"--args=" + strconv.Itoa(argsFD)}
|
|
||||||
} else {
|
|
||||||
return []string{"--args=" + strconv.Itoa(argsFD), "--fd=" + strconv.Itoa(statFD)}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if !sandbox {
|
|
||||||
h = helper.New(p.seal, p.name, argF)
|
|
||||||
// xdg-dbus-proxy does not need to inherit the environment
|
|
||||||
h.SetEnv(make([]string, 0))
|
|
||||||
} else {
|
|
||||||
// look up absolute path if name is just a file name
|
|
||||||
toolPath := p.name
|
|
||||||
if filepath.Base(p.name) == p.name {
|
|
||||||
if s, err := exec.LookPath(p.name); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
toolPath = s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolve libraries by parsing ldd output
|
|
||||||
var proxyDeps []*ldd.Entry
|
|
||||||
if toolPath != "/nonexistent-xdg-dbus-proxy" {
|
|
||||||
if l, err := ldd.Exec(ctx, toolPath); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
proxyDeps = l
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bc := &bwrap.Config{
|
|
||||||
Unshare: nil,
|
|
||||||
Hostname: "fortify-dbus",
|
|
||||||
Chdir: "/",
|
|
||||||
Syscall: &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
|
|
||||||
Clearenv: true,
|
|
||||||
NewSession: true,
|
|
||||||
DieWithParent: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolve proxy socket directories
|
|
||||||
bindTarget := make(map[string]struct{}, 2)
|
|
||||||
for _, ps := range []string{p.session[1], p.system[1]} {
|
|
||||||
if pd := path.Dir(ps); len(pd) > 0 {
|
|
||||||
if pd[0] == '/' {
|
|
||||||
bindTarget[pd] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for k := range bindTarget {
|
|
||||||
bc.Bind(k, k, false, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
roBindTarget := make(map[string]struct{}, 2+1+len(proxyDeps))
|
|
||||||
|
|
||||||
// xdb-dbus-proxy bin and dependencies
|
|
||||||
roBindTarget[path.Dir(toolPath)] = struct{}{}
|
|
||||||
for _, ent := range proxyDeps {
|
|
||||||
if path.IsAbs(ent.Path) {
|
|
||||||
roBindTarget[path.Dir(ent.Path)] = struct{}{}
|
|
||||||
}
|
|
||||||
if path.IsAbs(ent.Name) {
|
|
||||||
roBindTarget[path.Dir(ent.Name)] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolve upstream bus directories
|
|
||||||
for _, as := range []string{p.session[0], p.system[0]} {
|
|
||||||
if len(as) > 0 && strings.HasPrefix(as, "unix:path=/") {
|
|
||||||
// leave / intact
|
|
||||||
roBindTarget[path.Dir(as[10:])] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for k := range roBindTarget {
|
|
||||||
bc.Bind(k, k)
|
|
||||||
}
|
|
||||||
|
|
||||||
h = helper.MustNewBwrap(bc, toolPath, true, p.seal, argF, nil, nil)
|
|
||||||
p.bwrap = bc
|
|
||||||
}
|
|
||||||
|
|
||||||
if output != nil {
|
|
||||||
h.Stdout(output).Stderr(output)
|
|
||||||
}
|
|
||||||
c, cancel := context.WithCancelCause(ctx)
|
|
||||||
if err := h.Start(c, true); err != nil {
|
|
||||||
cancel(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
p.helper = h
|
|
||||||
p.ctx = c
|
|
||||||
p.cancel = cancel
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var proxyClosed = errors.New("proxy closed")
|
|
||||||
|
|
||||||
// Wait blocks until xdg-dbus-proxy exits and releases resources.
|
|
||||||
func (p *Proxy) Wait() error {
|
|
||||||
p.lock.RLock()
|
|
||||||
defer p.lock.RUnlock()
|
|
||||||
|
|
||||||
if p.helper == nil {
|
|
||||||
return errors.New("dbus: not started")
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure socket removal so ephemeral directory is empty at revert
|
|
||||||
if err := os.Remove(p.session[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
||||||
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,6 +6,12 @@ import (
|
|||||||
"git.gensokyo.uk/security/fortify/dbus"
|
"git.gensokyo.uk/security/fortify/dbus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sampleHostPath = "/tmp/bus"
|
||||||
|
sampleHostAddr = "unix:path=" + sampleHostPath
|
||||||
|
sampleBindPath = "/tmp/proxied_bus"
|
||||||
|
)
|
||||||
|
|
||||||
var samples = []dbusTestCase{
|
var samples = []dbusTestCase{
|
||||||
{
|
{
|
||||||
"org.chromium.Chromium", &dbus.Config{
|
"org.chromium.Chromium", &dbus.Config{
|
||||||
@ -19,10 +25,10 @@ var samples = []dbusTestCase{
|
|||||||
Log: false,
|
Log: false,
|
||||||
Filter: true,
|
Filter: true,
|
||||||
}, false, false,
|
}, false, false,
|
||||||
[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/bus"},
|
[2]string{sampleHostAddr, sampleBindPath},
|
||||||
[]string{
|
[]string{
|
||||||
"unix:path=/run/user/1971/bus",
|
sampleHostAddr,
|
||||||
"/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/bus",
|
sampleBindPath,
|
||||||
"--filter",
|
"--filter",
|
||||||
"--talk=org.freedesktop.Notifications",
|
"--talk=org.freedesktop.Notifications",
|
||||||
"--talk=org.freedesktop.FileManager1",
|
"--talk=org.freedesktop.FileManager1",
|
||||||
@ -48,9 +54,10 @@ var samples = []dbusTestCase{
|
|||||||
Log: false,
|
Log: false,
|
||||||
Filter: true,
|
Filter: true,
|
||||||
}, false, false,
|
}, false, false,
|
||||||
[2]string{"unix:path=/run/dbus/system_bus_socket", "/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/system_bus_socket"},
|
[2]string{sampleHostAddr, sampleBindPath},
|
||||||
[]string{"unix:path=/run/dbus/system_bus_socket",
|
[]string{
|
||||||
"/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/system_bus_socket",
|
sampleHostAddr,
|
||||||
|
sampleBindPath,
|
||||||
"--filter",
|
"--filter",
|
||||||
"--talk=org.bluez",
|
"--talk=org.bluez",
|
||||||
"--talk=org.freedesktop.Avahi",
|
"--talk=org.freedesktop.Avahi",
|
||||||
@ -68,10 +75,10 @@ var samples = []dbusTestCase{
|
|||||||
Log: false,
|
Log: false,
|
||||||
Filter: true,
|
Filter: true,
|
||||||
}, false, false,
|
}, false, false,
|
||||||
[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/34c24f16a0d791d28835ededaf446033/bus"},
|
[2]string{sampleHostAddr, sampleBindPath},
|
||||||
[]string{
|
[]string{
|
||||||
"unix:path=/run/user/1971/bus",
|
sampleHostAddr,
|
||||||
"/tmp/fortify.1971/34c24f16a0d791d28835ededaf446033/bus",
|
sampleBindPath,
|
||||||
"--filter",
|
"--filter",
|
||||||
"--talk=org.freedesktop.Notifications",
|
"--talk=org.freedesktop.Notifications",
|
||||||
"--talk=org.kde.StatusNotifierWatcher",
|
"--talk=org.kde.StatusNotifierWatcher",
|
||||||
@ -91,10 +98,10 @@ var samples = []dbusTestCase{
|
|||||||
Log: true,
|
Log: true,
|
||||||
Filter: true,
|
Filter: true,
|
||||||
}, false, false,
|
}, false, false,
|
||||||
[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus"},
|
[2]string{sampleHostAddr, sampleBindPath},
|
||||||
[]string{
|
[]string{
|
||||||
"unix:path=/run/user/1971/bus",
|
sampleHostAddr,
|
||||||
"/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus",
|
sampleBindPath,
|
||||||
"--filter",
|
"--filter",
|
||||||
"--see=uk.gensokyo.CrashTestDummy1",
|
"--see=uk.gensokyo.CrashTestDummy1",
|
||||||
"--talk=org.freedesktop.Notifications",
|
"--talk=org.freedesktop.Notifications",
|
||||||
@ -114,10 +121,10 @@ var samples = []dbusTestCase{
|
|||||||
Log: true,
|
Log: true,
|
||||||
Filter: true,
|
Filter: true,
|
||||||
}, false, true,
|
}, false, true,
|
||||||
[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus"},
|
[2]string{sampleHostAddr, sampleBindPath},
|
||||||
[]string{
|
[]string{
|
||||||
"unix:path=/run/user/1971/bus",
|
sampleHostAddr,
|
||||||
"/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus",
|
sampleBindPath,
|
||||||
"--filter",
|
"--filter",
|
||||||
"--see=uk.gensokyo.CrashTestDummy",
|
"--see=uk.gensokyo.CrashTestDummy",
|
||||||
"--talk=org.freedesktop.Notifications",
|
"--talk=org.freedesktop.Notifications",
|
||||||
|
@ -6,6 +6,4 @@ import (
|
|||||||
"git.gensokyo.uk/security/fortify/helper"
|
"git.gensokyo.uk/security/fortify/helper"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHelperChildStub(t *testing.T) {
|
func TestHelperStub(t *testing.T) { helper.InternalHelperStub() }
|
||||||
helper.InternalChildStub()
|
|
||||||
}
|
|
||||||
|
14
flake.lock
generated
14
flake.lock
generated
@ -7,11 +7,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1736373539,
|
"lastModified": 1739757849,
|
||||||
"narHash": "sha256-dinzAqCjenWDxuy+MqUQq0I4zUSfaCvN9rzuCmgMZJY=",
|
"narHash": "sha256-Gs076ot1YuAAsYVcyidLKUMIc4ooOaRGO0PqTY7sBzA=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "home-manager",
|
"repo": "home-manager",
|
||||||
"rev": "bd65bc3cde04c16755955630b344bc9e35272c56",
|
"rev": "9d3d080aec2a35e05a15cedd281c2384767c2cfe",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -23,16 +23,16 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1739333913,
|
"lastModified": 1741445498,
|
||||||
"narHash": "sha256-JXt5FtySR+yBm5ny8zG/hX1IybF/7R66jZfXxXSb6wY=",
|
"narHash": "sha256-F5Em0iv/CxkN5mZ9hRn3vPknpoWdcdCyR0e4WklHwiE=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "7d83f668aee9e41d574c398a9bb569047e8a3f5d",
|
"rev": "52e3095f6d812b91b22fb7ad0bfc1ab416453634",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"ref": "nixos-24.11-small",
|
"ref": "nixos-24.11",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
description = "fortify sandbox tool and nixos module";
|
description = "fortify sandbox tool and nixos module";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11-small";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
|
||||||
|
|
||||||
home-manager = {
|
home-manager = {
|
||||||
url = "github:nix-community/home-manager/release-24.11";
|
url = "github:nix-community/home-manager/release-24.11";
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
"git.gensokyo.uk/security/fortify/helper"
|
"git.gensokyo.uk/security/fortify/helper"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_argsFD_String(t *testing.T) {
|
func Test_argsFd_String(t *testing.T) {
|
||||||
wantString := strings.Join(wantArgs, " ")
|
wantString := strings.Join(wantArgs, " ")
|
||||||
if got := argsWt.(fmt.Stringer).String(); got != wantString {
|
if got := argsWt.(fmt.Stringer).String(); got != wantString {
|
||||||
t.Errorf("String(): got %v; want %v",
|
t.Errorf("String(): got %v; want %v",
|
||||||
|
@ -2,13 +2,11 @@ package helper
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"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/helper/proc"
|
||||||
@ -17,51 +15,21 @@ import (
|
|||||||
// 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 {
|
|
||||||
// final args fd of bwrap process
|
|
||||||
argsFd uintptr
|
|
||||||
|
|
||||||
// name of the command to run in bwrap
|
|
||||||
name string
|
|
||||||
|
|
||||||
// whether to set process group id
|
|
||||||
setpgid bool
|
|
||||||
|
|
||||||
lock sync.RWMutex
|
|
||||||
*helperCmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *bubblewrap) Start(ctx context.Context, stat bool) error {
|
|
||||||
b.lock.Lock()
|
|
||||||
defer b.lock.Unlock()
|
|
||||||
|
|
||||||
// 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 != nil && b.Cmd.Process != nil {
|
|
||||||
return errors.New("exec: already started")
|
|
||||||
}
|
|
||||||
|
|
||||||
args := b.finalise(ctx, stat)
|
|
||||||
if b.setpgid {
|
|
||||||
b.Cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.Cmd.Args = slices.Grow(b.Cmd.Args, 4+len(args))
|
|
||||||
b.Cmd.Args = append(b.Cmd.Args, "--args", strconv.Itoa(int(b.argsFd)), "--", b.name)
|
|
||||||
b.Cmd.Args = append(b.Cmd.Args, args...)
|
|
||||||
return proc.Fulfill(ctx, b.Cmd, b.files, b.extraFiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
||||||
// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
|
// 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.
|
// Function argF returns an array of arguments passed directly to the child process.
|
||||||
func MustNewBwrap(
|
func MustNewBwrap(
|
||||||
conf *bwrap.Config, name string, setpgid bool,
|
ctx context.Context,
|
||||||
wt io.WriterTo, argF func(argsFD, statFD int) []string,
|
name string,
|
||||||
|
wt io.WriterTo,
|
||||||
|
stat bool,
|
||||||
|
argF func(argsFd, statFd int) []string,
|
||||||
|
cmdF func(cmd *exec.Cmd),
|
||||||
extraFiles []*os.File,
|
extraFiles []*os.File,
|
||||||
|
conf *bwrap.Config,
|
||||||
syncFd *os.File,
|
syncFd *os.File,
|
||||||
) Helper {
|
) Helper {
|
||||||
b, err := NewBwrap(conf, name, setpgid, wt, argF, extraFiles, syncFd)
|
b, err := NewBwrap(ctx, name, wt, stat, argF, cmdF, extraFiles, conf, syncFd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err.Error())
|
panic(err.Error())
|
||||||
} else {
|
} else {
|
||||||
@ -73,24 +41,32 @@ func MustNewBwrap(
|
|||||||
// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
|
// 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.
|
// Function argF returns an array of arguments passed directly to the child process.
|
||||||
func NewBwrap(
|
func NewBwrap(
|
||||||
conf *bwrap.Config, name string, setpgid bool,
|
ctx context.Context,
|
||||||
wt io.WriterTo, argF func(argsFd, statFd int) []string,
|
name string,
|
||||||
|
wt io.WriterTo,
|
||||||
|
stat bool,
|
||||||
|
argF func(argsFd, statFd int) []string,
|
||||||
|
cmdF func(cmd *exec.Cmd),
|
||||||
extraFiles []*os.File,
|
extraFiles []*os.File,
|
||||||
|
conf *bwrap.Config,
|
||||||
syncFd *os.File,
|
syncFd *os.File,
|
||||||
) (Helper, error) {
|
) (Helper, error) {
|
||||||
b := new(bubblewrap)
|
b, args := newHelperCmd(ctx, BubblewrapName, wt, stat, argF, extraFiles)
|
||||||
|
|
||||||
b.name = name
|
|
||||||
b.setpgid = setpgid
|
|
||||||
b.helperCmd = newHelperCmd(b, BubblewrapName, wt, argF, extraFiles)
|
|
||||||
|
|
||||||
|
var argsFd uintptr
|
||||||
if v, err := NewCheckedArgs(conf.Args(syncFd, b.extraFiles, &b.files)); err != nil {
|
if v, err := NewCheckedArgs(conf.Args(syncFd, b.extraFiles, &b.files)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
f := proc.NewWriterTo(v)
|
f := proc.NewWriterTo(v)
|
||||||
b.argsFd = proc.InitFile(f, b.extraFiles)
|
argsFd = proc.InitFile(f, b.extraFiles)
|
||||||
b.files = append(b.files, f)
|
b.files = append(b.files, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.Args = slices.Grow(b.Args, 4+len(args))
|
||||||
|
b.Args = append(b.Args, "--args", strconv.Itoa(int(argsFd)), "--", name)
|
||||||
|
b.Args = append(b.Args, args...)
|
||||||
|
if cmdF != nil {
|
||||||
|
cmdF(b.Cmd)
|
||||||
|
}
|
||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
package bwrap_test
|
package bwrap_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"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/helper/proc"
|
||||||
"git.gensokyo.uk/security/fortify/helper/seccomp"
|
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfig_Args(t *testing.T) {
|
func TestConfig_Args(t *testing.T) {
|
||||||
seccomp.CPrintln = log.Println
|
oldF := seccomp.GetOutput()
|
||||||
t.Cleanup(func() { seccomp.CPrintln = nil })
|
seccomp.SetOutput(t.Log)
|
||||||
|
t.Cleanup(func() { seccomp.SetOutput(oldF) })
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
"git.gensokyo.uk/security/fortify/helper/proc"
|
||||||
"git.gensokyo.uk/security/fortify/helper/seccomp"
|
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SyscallPolicy struct {
|
type SyscallPolicy struct {
|
||||||
@ -48,20 +48,21 @@ func (c *Config) seccompArgs() FDBuilder {
|
|||||||
{c.Syscall.Bluetooth, seccomp.FlagBluetooth, "bluetooth"},
|
{c.Syscall.Bluetooth, seccomp.FlagBluetooth, "bluetooth"},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if seccomp.CPrintln != nil {
|
scmpPrintln := seccomp.GetOutput()
|
||||||
|
if scmpPrintln != nil {
|
||||||
optd = make([]string, 1, len(optCond)+1)
|
optd = make([]string, 1, len(optCond)+1)
|
||||||
optd[0] = "common"
|
optd[0] = "common"
|
||||||
}
|
}
|
||||||
for _, opt := range optCond {
|
for _, opt := range optCond {
|
||||||
if opt.v {
|
if opt.v {
|
||||||
opts |= opt.o
|
opts |= opt.o
|
||||||
if seccomp.CPrintln != nil {
|
if scmpPrintln != nil {
|
||||||
optd = append(optd, opt.d)
|
optd = append(optd, opt.d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if seccomp.CPrintln != nil {
|
if scmpPrintln != nil {
|
||||||
seccomp.CPrintln(fmt.Sprintf("seccomp flags: %s", optd))
|
scmpPrintln(fmt.Sprintf("seccomp flags: %s", optd))
|
||||||
}
|
}
|
||||||
|
|
||||||
return &seccompBuilder{seccomp.NewFile(opts)}
|
return &seccompBuilder{seccomp.NewFile(opts)}
|
||||||
|
@ -3,7 +3,10 @@ package helper_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -16,7 +19,7 @@ func TestBwrap(t *testing.T) {
|
|||||||
sc := &bwrap.Config{
|
sc := &bwrap.Config{
|
||||||
Net: true,
|
Net: true,
|
||||||
Hostname: "localhost",
|
Hostname: "localhost",
|
||||||
Chdir: "/nonexistent",
|
Chdir: "/proc/nonexistent",
|
||||||
Clearenv: true,
|
Clearenv: true,
|
||||||
NewSession: true,
|
NewSession: true,
|
||||||
DieWithParent: true,
|
DieWithParent: true,
|
||||||
@ -25,18 +28,19 @@ func TestBwrap(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("nonexistent bwrap name", func(t *testing.T) {
|
t.Run("nonexistent bwrap name", func(t *testing.T) {
|
||||||
bubblewrapName := helper.BubblewrapName
|
bubblewrapName := helper.BubblewrapName
|
||||||
helper.BubblewrapName = "/nonexistent"
|
helper.BubblewrapName = "/proc/nonexistent"
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() { helper.BubblewrapName = bubblewrapName })
|
||||||
helper.BubblewrapName = bubblewrapName
|
|
||||||
})
|
|
||||||
|
|
||||||
h := helper.MustNewBwrap(
|
h := helper.MustNewBwrap(
|
||||||
sc, "fortify", false,
|
context.Background(),
|
||||||
argsWt, argF,
|
"false",
|
||||||
nil, nil,
|
argsWt, false,
|
||||||
|
argF, nil,
|
||||||
|
nil,
|
||||||
|
sc, nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := h.Start(context.Background(), false); !errors.Is(err, os.ErrNotExist) {
|
if err := h.Start(); !errors.Is(err, os.ErrNotExist) {
|
||||||
t.Errorf("Start: error = %v, wantErr %v",
|
t.Errorf("Start: error = %v, wantErr %v",
|
||||||
err, os.ErrNotExist)
|
err, os.ErrNotExist)
|
||||||
}
|
}
|
||||||
@ -44,12 +48,15 @@ func TestBwrap(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("valid new helper nil check", func(t *testing.T) {
|
t.Run("valid new helper nil check", func(t *testing.T) {
|
||||||
if got := helper.MustNewBwrap(
|
if got := helper.MustNewBwrap(
|
||||||
sc, "fortify", false,
|
context.TODO(),
|
||||||
argsWt, argF,
|
"false",
|
||||||
nil, nil,
|
argsWt, false,
|
||||||
|
argF, nil,
|
||||||
|
nil,
|
||||||
|
sc, nil,
|
||||||
); got == nil {
|
); got == nil {
|
||||||
t.Errorf("MustNewBwrap(%#v, %#v, %#v) got nil",
|
t.Errorf("MustNewBwrap(%#v, %#v, %#v) got nil",
|
||||||
sc, argsWt, "fortify")
|
sc, argsWt, "false")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -64,28 +71,28 @@ func TestBwrap(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
helper.MustNewBwrap(
|
helper.MustNewBwrap(
|
||||||
&bwrap.Config{Hostname: "\x00"}, "fortify", false,
|
context.TODO(),
|
||||||
nil, argF,
|
"false",
|
||||||
nil, nil,
|
argsWt, false,
|
||||||
|
argF, nil,
|
||||||
|
nil,
|
||||||
|
&bwrap.Config{Hostname: "\x00"}, nil,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("start without pipes", func(t *testing.T) {
|
t.Run("start without pipes", func(t *testing.T) {
|
||||||
helper.InternalReplaceExecCommand(t)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
stdout, stderr := new(strings.Builder), new(strings.Builder)
|
||||||
h := helper.MustNewBwrap(
|
h := helper.MustNewBwrap(
|
||||||
sc, "crash-test-dummy", false,
|
ctx, os.Args[0],
|
||||||
nil, argFChecked,
|
nil, false,
|
||||||
nil, nil,
|
argFChecked, func(cmd *exec.Cmd) { cmd.Stdout, cmd.Stderr = stdout, stderr; hijackBwrap(cmd) },
|
||||||
|
nil,
|
||||||
|
sc, nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
stdout, stderr := new(strings.Builder), new(strings.Builder)
|
if err := h.Start(); err != nil {
|
||||||
h.Stdout(stdout).Stderr(stderr)
|
|
||||||
|
|
||||||
c, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if err := h.Start(c, false); err != nil {
|
|
||||||
t.Errorf("Start: error = %v",
|
t.Errorf("Start: error = %v",
|
||||||
err)
|
err)
|
||||||
return
|
return
|
||||||
@ -98,11 +105,23 @@ func TestBwrap(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("implementation compliance", func(t *testing.T) {
|
t.Run("implementation compliance", func(t *testing.T) {
|
||||||
testHelper(t, func() helper.Helper {
|
testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper {
|
||||||
return helper.MustNewBwrap(
|
return helper.MustNewBwrap(
|
||||||
sc, "crash-test-dummy", false,
|
ctx, os.Args[0],
|
||||||
argsWt, argF, nil, nil,
|
argsWt, stat,
|
||||||
|
argF, func(cmd *exec.Cmd) { setOutput(&cmd.Stdout, &cmd.Stderr); hijackBwrap(cmd) },
|
||||||
|
nil,
|
||||||
|
sc, nil,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hijackBwrap(cmd *exec.Cmd) {
|
||||||
|
if cmd.Args[0] != "bwrap" {
|
||||||
|
panic(fmt.Sprintf("unexpected argv0 %q", cmd.Args[0]))
|
||||||
|
}
|
||||||
|
cmd.Err = nil
|
||||||
|
cmd.Path = os.Args[0]
|
||||||
|
cmd.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, cmd.Args...)
|
||||||
|
}
|
||||||
|
84
helper/cmd.go
Normal file
84
helper/cmd.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/helper/proc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewDirect initialises a new direct Helper instance with wt as the null-terminated argument writer.
|
||||||
|
// Function argF returns an array of arguments passed directly to the child process.
|
||||||
|
func NewDirect(
|
||||||
|
ctx context.Context,
|
||||||
|
name string,
|
||||||
|
wt io.WriterTo,
|
||||||
|
stat bool,
|
||||||
|
argF func(argsFd, statFd int) []string,
|
||||||
|
cmdF func(cmd *exec.Cmd),
|
||||||
|
extraFiles []*os.File,
|
||||||
|
) Helper {
|
||||||
|
d, args := newHelperCmd(ctx, name, wt, stat, argF, extraFiles)
|
||||||
|
d.Args = append(d.Args, args...)
|
||||||
|
if cmdF != nil {
|
||||||
|
cmdF(d.Cmd)
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHelperCmd(
|
||||||
|
ctx context.Context,
|
||||||
|
name string,
|
||||||
|
wt io.WriterTo,
|
||||||
|
stat bool,
|
||||||
|
argF func(argsFd, statFd int) []string,
|
||||||
|
extraFiles []*os.File,
|
||||||
|
) (cmd *helperCmd, args []string) {
|
||||||
|
cmd = new(helperCmd)
|
||||||
|
cmd.helperFiles, args = newHelperFiles(ctx, wt, stat, argF, extraFiles)
|
||||||
|
cmd.Cmd = exec.CommandContext(ctx, name)
|
||||||
|
cmd.Cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGTERM) }
|
||||||
|
cmd.WaitDelay = WaitDelay
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// helperCmd provides a [exec.Cmd] wrapper around helper ipc.
|
||||||
|
type helperCmd struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
*helperFiles
|
||||||
|
*exec.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *helperCmd) Start() error {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
// 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 h.Cmd != nil && h.Cmd.Process != nil {
|
||||||
|
return errors.New("helper: already started")
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Env = slices.Grow(h.Env, 2)
|
||||||
|
if h.useArgsFd {
|
||||||
|
h.Env = append(h.Env, FortifyHelper+"=1")
|
||||||
|
} else {
|
||||||
|
h.Env = append(h.Env, FortifyHelper+"=0")
|
||||||
|
}
|
||||||
|
if h.useStatFd {
|
||||||
|
h.Env = append(h.Env, FortifyStatus+"=1")
|
||||||
|
|
||||||
|
// stat is populated on fulfill
|
||||||
|
h.Cancel = func() error { return h.stat.Close() }
|
||||||
|
} else {
|
||||||
|
h.Env = append(h.Env, FortifyStatus+"=0")
|
||||||
|
}
|
||||||
|
|
||||||
|
return proc.Fulfill(h.helperFiles.ctx, &h.ExtraFiles, h.Cmd.Start, h.files, h.extraFiles)
|
||||||
|
}
|
39
helper/cmd_test.go
Normal file
39
helper/cmd_test.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package helper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/helper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCmd(t *testing.T) {
|
||||||
|
t.Run("start non-existent helper path", func(t *testing.T) {
|
||||||
|
h := helper.NewDirect(context.Background(), "/proc/nonexistent", argsWt, false, argF, nil, nil)
|
||||||
|
|
||||||
|
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.NewDirect(context.TODO(), "fortify", argsWt, false, argF, nil, nil); got == nil {
|
||||||
|
t.Errorf("NewDirect(%q, %q) got nil",
|
||||||
|
argsWt, "fortify")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("implementation compliance", func(t *testing.T) {
|
||||||
|
testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper {
|
||||||
|
return helper.NewDirect(ctx, os.Args[0], argsWt, stat, argF, func(cmd *exec.Cmd) {
|
||||||
|
setOutput(&cmd.Stdout, &cmd.Stderr)
|
||||||
|
}, nil)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
75
helper/container.go
Normal file
75
helper/container.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/helper/proc"
|
||||||
|
"git.gensokyo.uk/security/fortify/sandbox"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New initialises a Helper instance with wt as the null-terminated argument writer.
|
||||||
|
func New(
|
||||||
|
ctx context.Context,
|
||||||
|
name string,
|
||||||
|
wt io.WriterTo,
|
||||||
|
stat bool,
|
||||||
|
argF func(argsFd, statFd int) []string,
|
||||||
|
cmdF func(container *sandbox.Container),
|
||||||
|
extraFiles []*os.File,
|
||||||
|
) Helper {
|
||||||
|
var args []string
|
||||||
|
h := new(helperContainer)
|
||||||
|
h.helperFiles, args = newHelperFiles(ctx, wt, stat, argF, extraFiles)
|
||||||
|
h.Container = sandbox.New(ctx, name, args...)
|
||||||
|
h.WaitDelay = WaitDelay
|
||||||
|
if cmdF != nil {
|
||||||
|
cmdF(h.Container)
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// helperContainer provides a [sandbox.Container] wrapper around helper ipc.
|
||||||
|
type helperContainer struct {
|
||||||
|
started bool
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
*helperFiles
|
||||||
|
*sandbox.Container
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *helperContainer) Start() error {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
if h.started {
|
||||||
|
return errors.New("helper: already started")
|
||||||
|
}
|
||||||
|
h.started = true
|
||||||
|
|
||||||
|
h.Env = slices.Grow(h.Env, 2)
|
||||||
|
if h.useArgsFd {
|
||||||
|
h.Env = append(h.Env, FortifyHelper+"=1")
|
||||||
|
} else {
|
||||||
|
h.Env = append(h.Env, FortifyHelper+"=0")
|
||||||
|
}
|
||||||
|
if h.useStatFd {
|
||||||
|
h.Env = append(h.Env, FortifyStatus+"=1")
|
||||||
|
|
||||||
|
// stat is populated on fulfill
|
||||||
|
h.Cancel = func() error { return h.stat.Close() }
|
||||||
|
} else {
|
||||||
|
h.Env = append(h.Env, FortifyStatus+"=0")
|
||||||
|
}
|
||||||
|
|
||||||
|
return proc.Fulfill(h.helperFiles.ctx, &h.ExtraFiles, func() error {
|
||||||
|
if err := h.Container.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return h.Container.Serve()
|
||||||
|
}, h.files, h.extraFiles)
|
||||||
|
}
|
57
helper/container_test.go
Normal file
57
helper/container_test.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package helper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/helper"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
|
"git.gensokyo.uk/security/fortify/sandbox"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestContainer(t *testing.T) {
|
||||||
|
t.Run("start empty container", func(t *testing.T) {
|
||||||
|
h := helper.New(context.Background(), "/nonexistent", argsWt, false, argF, nil, nil)
|
||||||
|
|
||||||
|
wantErr := "sandbox: starting an empty container"
|
||||||
|
if err := h.Start(); err == nil || err.Error() != wantErr {
|
||||||
|
t.Errorf("Start: error = %v, wantErr %q",
|
||||||
|
err, wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid new helper nil check", func(t *testing.T) {
|
||||||
|
if got := helper.New(context.TODO(), "fortify", argsWt, false, argF, nil, nil); got == nil {
|
||||||
|
t.Errorf("New(%q, %q) got nil",
|
||||||
|
argsWt, "fortify")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("implementation compliance", func(t *testing.T) {
|
||||||
|
testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper {
|
||||||
|
return helper.New(ctx, os.Args[0], argsWt, stat, argF, func(container *sandbox.Container) {
|
||||||
|
setOutput(&container.Stdout, &container.Stderr)
|
||||||
|
container.CommandContext = func(ctx context.Context) (cmd *exec.Cmd) {
|
||||||
|
return exec.CommandContext(ctx, os.Args[0], "-test.v",
|
||||||
|
"-test.run=TestHelperInit", "--", "init")
|
||||||
|
}
|
||||||
|
container.Bind("/", "/", 0)
|
||||||
|
container.Proc("/proc")
|
||||||
|
container.Dev("/dev")
|
||||||
|
}, nil)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHelperInit(t *testing.T) {
|
||||||
|
if len(os.Args) != 5 || os.Args[4] != "init" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sandbox.SetOutput(fmsg.Output{})
|
||||||
|
sandbox.Init(fmsg.Prepare, func(bool) { internal.InstallFmsg(false) })
|
||||||
|
}
|
@ -1,40 +0,0 @@
|
|||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
|
||||||
)
|
|
||||||
|
|
||||||
// direct wraps *exec.Cmd and manages status and args fd.
|
|
||||||
// Args is always 3 and status if set is always 4.
|
|
||||||
type direct struct {
|
|
||||||
lock sync.RWMutex
|
|
||||||
*helperCmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *direct) Start(ctx context.Context, stat bool) error {
|
|
||||||
h.lock.Lock()
|
|
||||||
defer h.lock.Unlock()
|
|
||||||
|
|
||||||
// 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 h.Cmd != nil && h.Cmd.Process != nil {
|
|
||||||
return errors.New("exec: already started")
|
|
||||||
}
|
|
||||||
|
|
||||||
args := h.finalise(ctx, stat)
|
|
||||||
h.Cmd.Args = append(h.Cmd.Args, args...)
|
|
||||||
return proc.Fulfill(ctx, h.Cmd, h.files, h.extraFiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
func New(wt io.WriterTo, name string, argF func(argsFd, statFd int) []string) Helper {
|
|
||||||
d := new(direct)
|
|
||||||
d.helperCmd = newHelperCmd(d, name, wt, argF, nil)
|
|
||||||
return d
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
package helper_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDirect(t *testing.T) {
|
|
||||||
t.Run("start non-existent helper path", func(t *testing.T) {
|
|
||||||
h := helper.New(argsWt, "/nonexistent", argF)
|
|
||||||
|
|
||||||
if err := h.Start(context.Background(), false); !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.New(argsWt, "fortify", argF); got == nil {
|
|
||||||
t.Errorf("New(%q, %q) got nil",
|
|
||||||
argsWt, "fortify")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("implementation compliance", func(t *testing.T) {
|
|
||||||
testHelper(t, func() helper.Helper { return helper.New(argsWt, "crash-test-dummy", argF) })
|
|
||||||
})
|
|
||||||
}
|
|
117
helper/helper.go
117
helper/helper.go
@ -1,4 +1,4 @@
|
|||||||
// Package helper runs external helpers with optional sandboxing and manages their status/args pipes.
|
// Package helper runs external helpers with optional sandboxing.
|
||||||
package helper
|
package helper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -6,17 +6,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"slices"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
"git.gensokyo.uk/security/fortify/helper/proc"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var WaitDelay = 2 * time.Second
|
||||||
WaitDelay = 2 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// FortifyHelper is set to 1 when args fd is enabled and 0 otherwise.
|
// FortifyHelper is set to 1 when args fd is enabled and 0 otherwise.
|
||||||
@ -26,62 +21,56 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Helper interface {
|
type Helper interface {
|
||||||
// Stdin sets the standard input of Helper.
|
|
||||||
Stdin(r io.Reader) Helper
|
|
||||||
// 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.
|
||||||
// A status pipe is passed to the helper if stat is true.
|
Start() error
|
||||||
Start(ctx context.Context, stat bool) error
|
// Wait blocks until Helper exits.
|
||||||
// Wait blocks until Helper exits and releases all its resources.
|
|
||||||
Wait() error
|
Wait() error
|
||||||
|
|
||||||
fmt.Stringer
|
fmt.Stringer
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHelperCmd(
|
func newHelperFiles(
|
||||||
h Helper, name string,
|
ctx context.Context,
|
||||||
wt io.WriterTo, argF func(argsFd, statFd int) []string,
|
wt io.WriterTo,
|
||||||
|
stat bool,
|
||||||
|
argF func(argsFd, statFd int) []string,
|
||||||
extraFiles []*os.File,
|
extraFiles []*os.File,
|
||||||
) (cmd *helperCmd) {
|
) (hl *helperFiles, args []string) {
|
||||||
cmd = new(helperCmd)
|
hl = new(helperFiles)
|
||||||
|
hl.ctx = ctx
|
||||||
|
hl.useArgsFd = wt != nil
|
||||||
|
hl.useStatFd = stat
|
||||||
|
|
||||||
cmd.r = h
|
hl.extraFiles = new(proc.ExtraFilesPre)
|
||||||
cmd.name = name
|
|
||||||
|
|
||||||
cmd.extraFiles = new(proc.ExtraFilesPre)
|
|
||||||
for _, f := range extraFiles {
|
for _, f := range extraFiles {
|
||||||
_, v := cmd.extraFiles.Append()
|
_, v := hl.extraFiles.Append()
|
||||||
*v = f
|
*v = f
|
||||||
}
|
}
|
||||||
|
|
||||||
argsFd := -1
|
argsFd := -1
|
||||||
if wt != nil {
|
if hl.useArgsFd {
|
||||||
f := proc.NewWriterTo(wt)
|
f := proc.NewWriterTo(wt)
|
||||||
argsFd = int(proc.InitFile(f, cmd.extraFiles))
|
argsFd = int(proc.InitFile(f, hl.extraFiles))
|
||||||
cmd.files = append(cmd.files, f)
|
hl.files = append(hl.files, f)
|
||||||
cmd.hasArgsFd = true
|
|
||||||
}
|
}
|
||||||
cmd.argF = func(statFd int) []string { return argF(argsFd, statFd) }
|
|
||||||
|
|
||||||
|
statFd := -1
|
||||||
|
if hl.useStatFd {
|
||||||
|
f := proc.NewStat(&hl.stat)
|
||||||
|
statFd = int(proc.InitFile(f, hl.extraFiles))
|
||||||
|
hl.files = append(hl.files, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = argF(argsFd, statFd)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// helperCmd wraps Cmd and implements methods shared across all Helper implementations.
|
// helperFiles provides a generic wrapper around helper ipc.
|
||||||
type helperCmd struct {
|
type helperFiles 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
|
// whether argsFd is present
|
||||||
hasArgsFd bool
|
useArgsFd bool
|
||||||
|
// whether statFd is present
|
||||||
|
useStatFd bool
|
||||||
|
|
||||||
// closes statFd
|
// closes statFd
|
||||||
stat io.Closer
|
stat io.Closer
|
||||||
@ -90,45 +79,5 @@ type helperCmd struct {
|
|||||||
// passed through to [proc.Fulfill] and [proc.InitFile]
|
// passed through to [proc.Fulfill] and [proc.InitFile]
|
||||||
extraFiles *proc.ExtraFilesPre
|
extraFiles *proc.ExtraFilesPre
|
||||||
|
|
||||||
name string
|
ctx context.Context
|
||||||
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
|
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@ -35,7 +36,8 @@ func argF(argsFd, statFd int) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func argFChecked(argsFd, statFd int) (args []string) {
|
func argFChecked(argsFd, statFd int) (args []string) {
|
||||||
args = make([]string, 0, 4)
|
args = make([]string, 0, 6)
|
||||||
|
args = append(args, "-test.run=TestHelperStub", "--")
|
||||||
if argsFd > -1 {
|
if argsFd > -1 {
|
||||||
args = append(args, "--args", strconv.Itoa(argsFd))
|
args = append(args, "--args", strconv.Itoa(argsFd))
|
||||||
}
|
}
|
||||||
@ -46,14 +48,15 @@ func argFChecked(argsFd, statFd int) (args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// this function tests an implementation of the helper.Helper interface
|
// this function tests an implementation of the helper.Helper interface
|
||||||
func testHelper(t *testing.T, createHelper func() helper.Helper) {
|
func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper) {
|
||||||
helper.InternalReplaceExecCommand(t)
|
oldWaitDelay := helper.WaitDelay
|
||||||
|
helper.WaitDelay = 16 * time.Second
|
||||||
|
t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
|
||||||
|
|
||||||
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()
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
|
||||||
stdout, stderr := new(strings.Builder), new(strings.Builder)
|
stdout, stderr := new(strings.Builder), new(strings.Builder)
|
||||||
h.Stdout(stdout).Stderr(stderr)
|
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, stderr }, true)
|
||||||
|
|
||||||
t.Run("wait not yet started helper", func(t *testing.T) {
|
t.Run("wait not yet started helper", func(t *testing.T) {
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -65,10 +68,8 @@ func testHelper(t *testing.T, createHelper func() helper.Helper) {
|
|||||||
panic(fmt.Sprintf("unreachable: %v", h.Wait()))
|
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.Start(ctx, true); err != nil {
|
if err := h.Start(); err != nil {
|
||||||
t.Errorf("Start: error = %v", err)
|
t.Errorf("Start: error = %v", err)
|
||||||
cancel()
|
cancel()
|
||||||
return
|
return
|
||||||
@ -77,8 +78,8 @@ func testHelper(t *testing.T, createHelper func() helper.Helper) {
|
|||||||
cancel()
|
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 := "helper: already started"
|
||||||
if err := h.Start(ctx, true); err != nil && err.Error() != wantErr {
|
if err := h.Start(); err != nil && err.Error() != wantErr {
|
||||||
t.Errorf("Start: error = %v, wantErr %v",
|
t.Errorf("Start: error = %v, wantErr %v",
|
||||||
err, wantErr)
|
err, wantErr)
|
||||||
return
|
return
|
||||||
@ -100,21 +101,19 @@ func testHelper(t *testing.T, createHelper func() helper.Helper) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if got := stdout.String(); !strings.HasPrefix(got, wantPayload) {
|
if got := stderr.String(); got != wantPayload {
|
||||||
t.Errorf("Start: stdout = %v, want %v",
|
t.Errorf("Start: stderr = %v, want %v",
|
||||||
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()
|
|
||||||
|
|
||||||
stdout, stderr := new(strings.Builder), new(strings.Builder)
|
|
||||||
h.Stdout(stdout).Stderr(stderr)
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
stdout, stderr := new(strings.Builder), new(strings.Builder)
|
||||||
|
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, stderr }, false)
|
||||||
|
|
||||||
if err := h.Start(ctx, false); err != nil {
|
if err := h.Start(); err != nil {
|
||||||
t.Errorf("Start() error = %v",
|
t.Errorf("Start() error = %v",
|
||||||
err)
|
err)
|
||||||
return
|
return
|
||||||
@ -125,8 +124,8 @@ func testHelper(t *testing.T, createHelper func() helper.Helper) {
|
|||||||
err, stdout, stderr)
|
err, stdout, stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if got := stdout.String(); !strings.HasPrefix(got, wantPayload) {
|
if got := stderr.String(); got != wantPayload {
|
||||||
t.Errorf("Start() stdout = %v, want %v",
|
t.Errorf("Start() stderr = %v, want %v",
|
||||||
got, wantPayload)
|
got, wantPayload)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -60,7 +60,10 @@ func (f *ExtraFilesPre) copy(e []*os.File) []*os.File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fulfill calls the [File.Fulfill] method on all files, starts cmd and blocks until all fulfillment completes.
|
// 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) {
|
func Fulfill(ctx context.Context,
|
||||||
|
v *[]*os.File, start func() error,
|
||||||
|
files []File, extraFiles *ExtraFilesPre,
|
||||||
|
) (err error) {
|
||||||
var ecs int
|
var ecs int
|
||||||
for _, o := range files {
|
for _, o := range files {
|
||||||
ecs += o.ErrCount()
|
ecs += o.ErrCount()
|
||||||
@ -77,8 +80,8 @@ func Fulfill(ctx context.Context, cmd *exec.Cmd, files []File, extraFiles *Extra
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.ExtraFiles = extraFiles.Files()
|
*v = extraFiles.Files()
|
||||||
if err = cmd.Start(); err != nil {
|
if err = start(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,25 +1,21 @@
|
|||||||
package helper
|
package helper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"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/helper/proc"
|
||||||
"git.gensokyo.uk/security/fortify/internal"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// InternalChildStub is an internal function but exported because it is cross-package;
|
// InternalHelperStub 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 InternalChildStub() {
|
func InternalHelperStub() {
|
||||||
// this test mocks the helper process
|
// this test mocks the helper process
|
||||||
var ap, sp string
|
var ap, sp string
|
||||||
if v, ok := os.LookupEnv(FortifyHelper); !ok {
|
if v, ok := os.LookupEnv(FortifyHelper); !ok {
|
||||||
@ -33,30 +29,13 @@ func InternalChildStub() {
|
|||||||
sp = v
|
sp = v
|
||||||
}
|
}
|
||||||
|
|
||||||
switch os.Args[3] {
|
if len(os.Args) > 3 && os.Args[3] == "bwrap" {
|
||||||
case "bwrap":
|
|
||||||
bwrapStub()
|
bwrapStub()
|
||||||
default:
|
} else {
|
||||||
genericStub(flagRestoreFiles(4, ap, sp))
|
genericStub(flagRestoreFiles(3, ap, sp))
|
||||||
}
|
}
|
||||||
|
|
||||||
internal.Exit(0)
|
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() { commandContext = exec.CommandContext })
|
|
||||||
|
|
||||||
// replace execCommand to have the resulting *exec.Cmd launch TestHelperChildStub
|
|
||||||
commandContext = func(ctx context.Context, name string, arg ...string) *exec.Cmd {
|
|
||||||
// pass through nonexistent path
|
|
||||||
if name == "/nonexistent" && len(arg) == 0 {
|
|
||||||
return exec.CommandContext(ctx, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return exec.CommandContext(ctx, os.Args[0], append([]string{"-test.run=TestHelperChildStub", "--", name}, arg...)...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newFile(fd int, name, p string) *os.File {
|
func newFile(fd int, name, p string) *os.File {
|
||||||
@ -92,7 +71,7 @@ func flagRestoreFiles(offset int, ap, sp string) (argsFile, statFile *os.File) {
|
|||||||
func genericStub(argsFile, statFile *os.File) {
|
func genericStub(argsFile, statFile *os.File) {
|
||||||
if argsFile != nil {
|
if argsFile != nil {
|
||||||
// this output is checked by parent
|
// this output is checked by parent
|
||||||
if _, err := io.Copy(os.Stdout, argsFile); err != nil {
|
if _, err := io.Copy(os.Stderr, argsFile); err != nil {
|
||||||
panic("cannot read args: " + err.Error())
|
panic("cannot read args: " + err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -149,26 +128,27 @@ func bwrapStub() {
|
|||||||
sc := &bwrap.Config{
|
sc := &bwrap.Config{
|
||||||
Net: true,
|
Net: true,
|
||||||
Hostname: "localhost",
|
Hostname: "localhost",
|
||||||
Chdir: "/nonexistent",
|
Chdir: "/proc/nonexistent",
|
||||||
Clearenv: true,
|
Clearenv: true,
|
||||||
NewSession: true,
|
NewSession: true,
|
||||||
DieWithParent: true,
|
DieWithParent: true,
|
||||||
AsInit: true,
|
AsInit: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := MustNewCheckedArgs(sc.Args(nil, new(proc.ExtraFilesPre), new([]proc.File))).
|
if _, err := MustNewCheckedArgs(sc.Args(nil, new(proc.ExtraFilesPre), new([]proc.File))).
|
||||||
WriteTo(want); err != nil {
|
WriteTo(want); err != nil {
|
||||||
panic("cannot read want: " + err.Error())
|
panic("cannot read want: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(flag.CommandLine.Args()) > 0 && flag.CommandLine.Args()[0] == "crash-test-dummy" && got.String() != want.String() {
|
if got.String() != want.String() {
|
||||||
panic("bad bwrap args\ngot: " + got.String() + "\nwant: " + want.String())
|
panic("bad bwrap args\ngot: " + got.String() + "\nwant: " + want.String())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := syscall.Exec(
|
if err := syscall.Exec(
|
||||||
os.Args[0],
|
flag.CommandLine.Args()[0],
|
||||||
append([]string{os.Args[0], "-test.run=TestHelperChildStub", "--"}, flag.CommandLine.Args()...),
|
flag.CommandLine.Args(),
|
||||||
os.Environ()); err != nil {
|
os.Environ()); err != nil {
|
||||||
panic("cannot start general stub: " + err.Error())
|
panic("cannot start helper stub: " + err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,4 @@ import (
|
|||||||
"git.gensokyo.uk/security/fortify/helper"
|
"git.gensokyo.uk/security/fortify/helper"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHelperChildStub(t *testing.T) {
|
func TestHelperStub(t *testing.T) { helper.InternalHelperStub() }
|
||||||
helper.InternalChildStub()
|
|
||||||
}
|
|
||||||
|
@ -218,6 +218,6 @@ var testCasesNixos = []sealTestCase{
|
|||||||
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket").
|
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket").
|
||||||
Tmpfs("/var/run/nscd", 8192).
|
Tmpfs("/var/run/nscd", 8192).
|
||||||
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
|
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
|
||||||
Symlink("fortify", "/.fortify/sbin/init"),
|
Symlink("fortify", "/.fortify/sbin/init0"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -158,7 +158,7 @@ var testCasesPd = []sealTestCase{
|
|||||||
CopyBind("/etc/group", []byte("fortify:x:65534:\n")).
|
CopyBind("/etc/group", []byte("fortify:x:65534:\n")).
|
||||||
Tmpfs("/var/run/nscd", 8192).
|
Tmpfs("/var/run/nscd", 8192).
|
||||||
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
|
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
|
||||||
Symlink("fortify", "/.fortify/sbin/init"),
|
Symlink("fortify", "/.fortify/sbin/init0"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"nixos permissive defaults chromium", new(stubNixOS),
|
"nixos permissive defaults chromium", new(stubNixOS),
|
||||||
@ -389,6 +389,6 @@ var testCasesPd = []sealTestCase{
|
|||||||
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket").
|
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket").
|
||||||
Tmpfs("/var/run/nscd", 8192).
|
Tmpfs("/var/run/nscd", 8192).
|
||||||
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
|
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
|
||||||
Symlink("fortify", "/.fortify/sbin/init"),
|
Symlink("fortify", "/.fortify/sbin/init0"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -9,9 +9,9 @@ import (
|
|||||||
|
|
||||||
// used by the parent process
|
// used by the parent process
|
||||||
|
|
||||||
// TryArgv0 calls [Main] if argv0 indicates the process is started from a file named "init".
|
// TryArgv0 calls [Main] if the last element of argv0 is "init0".
|
||||||
func TryArgv0() {
|
func TryArgv0() {
|
||||||
if len(os.Args) > 0 && path.Base(os.Args[0]) == "init" {
|
if len(os.Args) > 0 && path.Base(os.Args[0]) == "init0" {
|
||||||
Main()
|
Main()
|
||||||
internal.Exit(0)
|
internal.Exit(0)
|
||||||
}
|
}
|
@ -9,9 +9,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/sandbox"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -25,10 +25,10 @@ const (
|
|||||||
func Main() {
|
func Main() {
|
||||||
// sharing stdout with shim
|
// sharing stdout with shim
|
||||||
// USE WITH CAUTION
|
// USE WITH CAUTION
|
||||||
fmsg.Prepare("init")
|
fmsg.Prepare("init0")
|
||||||
|
|
||||||
// setting this prevents ptrace
|
// setting this prevents ptrace
|
||||||
if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
|
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
|
||||||
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
|
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,11 +41,11 @@ func Main() {
|
|||||||
payload Payload
|
payload Payload
|
||||||
closeSetup func() error
|
closeSetup func() error
|
||||||
)
|
)
|
||||||
if f, err := proc.Receive(Env, &payload); err != nil {
|
if f, err := sandbox.Receive(Env, &payload, nil); err != nil {
|
||||||
if errors.Is(err, proc.ErrInvalid) {
|
if errors.Is(err, sandbox.ErrInvalid) {
|
||||||
log.Fatal("invalid config descriptor")
|
log.Fatal("invalid config descriptor")
|
||||||
}
|
}
|
||||||
if errors.Is(err, proc.ErrNotSet) {
|
if errors.Is(err, sandbox.ErrNotSet) {
|
||||||
log.Fatal("FORTIFY_INIT not set")
|
log.Fatal("FORTIFY_INIT not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ func Main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// die with parent
|
// die with parent
|
||||||
if err := internal.PR_SET_PDEATHSIG__SIGKILL(); err != nil {
|
if err := sandbox.SetPdeathsig(syscall.SIGKILL); err != nil {
|
||||||
log.Fatalf("prctl(PR_SET_PDEATHSIG, SIGKILL): %v", err)
|
log.Fatalf("prctl(PR_SET_PDEATHSIG, SIGKILL): %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -231,10 +231,6 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
|
|||||||
sc := sys.Paths()
|
sc := sys.Paths()
|
||||||
seal.runDirPath = sc.RunDirPath
|
seal.runDirPath = sc.RunDirPath
|
||||||
seal.sys = system.New(seal.user.uid.unwrap())
|
seal.sys = system.New(seal.user.uid.unwrap())
|
||||||
seal.sys.IsVerbose = fmsg.Load
|
|
||||||
seal.sys.Verbose = fmsg.Verbose
|
|
||||||
seal.sys.Verbosef = fmsg.Verbosef
|
|
||||||
seal.sys.WrapErr = fmsg.WrapError
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Work directories
|
Work directories
|
||||||
@ -486,7 +482,7 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
|
|||||||
|
|
||||||
// mount fortify in sandbox for init
|
// mount fortify in sandbox for init
|
||||||
seal.container.Bind(sys.MustExecutable(), path.Join(fst.Tmp, "sbin/fortify"))
|
seal.container.Bind(sys.MustExecutable(), path.Join(fst.Tmp, "sbin/fortify"))
|
||||||
seal.container.Symlink("fortify", path.Join(fst.Tmp, "sbin/init"))
|
seal.container.Symlink("fortify", path.Join(fst.Tmp, "sbin/init0"))
|
||||||
|
|
||||||
fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, command: %s",
|
fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, command: %s",
|
||||||
seal.user.uid, seal.user.username, config.Confinement.Groups, config.Command)
|
seal.user.uid, seal.user.username, config.Confinement.Groups, config.Command)
|
||||||
|
@ -13,11 +13,10 @@ import (
|
|||||||
|
|
||||||
"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/internal"
|
"git.gensokyo.uk/security/fortify/internal"
|
||||||
init0 "git.gensokyo.uk/security/fortify/internal/app/init"
|
"git.gensokyo.uk/security/fortify/internal/app/init0"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
|
"git.gensokyo.uk/security/fortify/sandbox"
|
||||||
)
|
)
|
||||||
|
|
||||||
// everything beyond this point runs as unconstrained target user
|
// everything beyond this point runs as unconstrained target user
|
||||||
@ -29,7 +28,7 @@ func Main() {
|
|||||||
fmsg.Prepare("shim")
|
fmsg.Prepare("shim")
|
||||||
|
|
||||||
// setting this prevents ptrace
|
// setting this prevents ptrace
|
||||||
if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
|
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
|
||||||
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
|
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,17 +37,17 @@ func Main() {
|
|||||||
payload Payload
|
payload Payload
|
||||||
closeSetup func() error
|
closeSetup func() error
|
||||||
)
|
)
|
||||||
if f, err := proc.Receive(Env, &payload); err != nil {
|
if f, err := sandbox.Receive(Env, &payload, nil); err != nil {
|
||||||
if errors.Is(err, proc.ErrInvalid) {
|
if errors.Is(err, sandbox.ErrInvalid) {
|
||||||
log.Fatal("invalid config descriptor")
|
log.Fatal("invalid config descriptor")
|
||||||
}
|
}
|
||||||
if errors.Is(err, proc.ErrNotSet) {
|
if errors.Is(err, sandbox.ErrNotSet) {
|
||||||
log.Fatal("FORTIFY_SHIM not set")
|
log.Fatal("FORTIFY_SHIM not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Fatalf("cannot decode shim setup payload: %v", err)
|
log.Fatalf("cannot decode shim setup payload: %v", err)
|
||||||
} else {
|
} else {
|
||||||
fmsg.Store(payload.Verbose)
|
internal.InstallFmsg(payload.Verbose)
|
||||||
closeSetup = f
|
closeSetup = f
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +107,7 @@ func Main() {
|
|||||||
var extraFiles []*os.File
|
var extraFiles []*os.File
|
||||||
|
|
||||||
// serve setup payload
|
// serve setup payload
|
||||||
if fd, encoder, err := proc.Setup(&extraFiles); err != nil {
|
if fd, encoder, err := sandbox.Setup(&extraFiles); err != nil {
|
||||||
log.Fatalf("cannot pipe: %v", err)
|
log.Fatalf("cannot pipe: %v", err)
|
||||||
} else {
|
} else {
|
||||||
conf.SetEnv[init0.Env] = strconv.Itoa(fd)
|
conf.SetEnv[init0.Env] = strconv.Itoa(fd)
|
||||||
@ -121,23 +120,21 @@ func Main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
|
helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
|
||||||
if fmsg.Load() {
|
|
||||||
seccomp.CPrintln = log.Println
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
}
|
defer stop() // unreachable
|
||||||
if b, err := helper.NewBwrap(
|
if b, err := helper.NewBwrap(
|
||||||
conf, path.Join(fst.Tmp, "sbin/init"), false,
|
ctx, path.Join(fst.Tmp, "sbin/init0"),
|
||||||
nil, func(int, int) []string { return make([]string, 0) },
|
nil, false,
|
||||||
|
func(int, int) []string { return make([]string, 0) },
|
||||||
|
func(cmd *exec.Cmd) { cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr },
|
||||||
extraFiles,
|
extraFiles,
|
||||||
syncFd,
|
conf, syncFd,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.Fatalf("malformed sandbox config: %v", err)
|
log.Fatalf("malformed sandbox config: %v", err)
|
||||||
} else {
|
} else {
|
||||||
b.Stdin(os.Stdin).Stdout(os.Stdout).Stderr(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(ctx, false); err != nil {
|
if err = b.Start(); err != nil {
|
||||||
log.Fatalf("cannot start target process: %v", err)
|
log.Fatalf("cannot start target process: %v", err)
|
||||||
} else if err = b.Wait(); err != nil {
|
} else if err = b.Wait(); err != nil {
|
||||||
var exitError *exec.ExitError
|
var exitError *exec.ExitError
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
"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/sandbox"
|
||||||
)
|
)
|
||||||
|
|
||||||
// used by the parent process
|
// used by the parent process
|
||||||
@ -56,7 +57,7 @@ func (s *Shim) Start(
|
|||||||
s.cmd = exec.Command(fsuPath)
|
s.cmd = exec.Command(fsuPath)
|
||||||
|
|
||||||
// pass shim setup pipe
|
// pass shim setup pipe
|
||||||
if fd, e, err := proc.Setup(&s.cmd.ExtraFiles); err != nil {
|
if fd, e, err := sandbox.Setup(&s.cmd.ExtraFiles); err != nil {
|
||||||
return nil, fmsg.WrapErrorSuffix(err,
|
return nil, fmsg.WrapErrorSuffix(err,
|
||||||
"cannot create shim setup pipe:")
|
"cannot create shim setup pipe:")
|
||||||
} else {
|
} else {
|
||||||
|
12
internal/fmsg/msg.go
Normal file
12
internal/fmsg/msg.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package fmsg
|
||||||
|
|
||||||
|
type Output struct{}
|
||||||
|
|
||||||
|
func (Output) IsVerbose() bool { return Load() }
|
||||||
|
func (Output) Verbose(v ...any) { Verbose(v...) }
|
||||||
|
func (Output) Verbosef(format string, v ...any) { Verbosef(format, v...) }
|
||||||
|
func (Output) WrapErr(err error, a ...any) error { return WrapError(err, a...) }
|
||||||
|
func (Output) PrintBaseErr(err error, fallback string) { PrintBaseError(err, fallback) }
|
||||||
|
func (Output) Suspend() { Suspend() }
|
||||||
|
func (Output) Resume() bool { return Resume() }
|
||||||
|
func (Output) BeforeExit() { BeforeExit() }
|
17
internal/output.go
Normal file
17
internal/output.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
|
"git.gensokyo.uk/security/fortify/sandbox"
|
||||||
|
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
||||||
|
"git.gensokyo.uk/security/fortify/system"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InstallFmsg(verbose bool) {
|
||||||
|
fmsg.Store(verbose)
|
||||||
|
sandbox.SetOutput(fmsg.Output{})
|
||||||
|
system.SetOutput(fmsg.Output{})
|
||||||
|
if verbose {
|
||||||
|
seccomp.SetOutput(fmsg.Verbose)
|
||||||
|
}
|
||||||
|
}
|
@ -1,20 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import "syscall"
|
|
||||||
|
|
||||||
func PR_SET_DUMPABLE__SUID_DUMP_DISABLE() error {
|
|
||||||
// linux/sched/coredump.h
|
|
||||||
if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, 0, 0); errno != 0 {
|
|
||||||
return errno
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func PR_SET_PDEATHSIG__SIGKILL() error {
|
|
||||||
if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGKILL), 0); errno != 0 {
|
|
||||||
return errno
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -15,6 +15,7 @@ import (
|
|||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
"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/sandbox"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Std implements System using the standard library.
|
// Std implements System using the standard library.
|
||||||
@ -34,7 +35,7 @@ func (s *Std) Geteuid() int { return os.Geteuid(
|
|||||||
func (s *Std) LookupEnv(key string) (string, bool) { return os.LookupEnv(key) }
|
func (s *Std) LookupEnv(key string) (string, bool) { return os.LookupEnv(key) }
|
||||||
func (s *Std) TempDir() string { return os.TempDir() }
|
func (s *Std) TempDir() string { return os.TempDir() }
|
||||||
func (s *Std) LookPath(file string) (string, error) { return exec.LookPath(file) }
|
func (s *Std) LookPath(file string) (string, error) { return exec.LookPath(file) }
|
||||||
func (s *Std) MustExecutable() string { return internal.MustExecutable() }
|
func (s *Std) MustExecutable() string { return sandbox.MustExecutable() }
|
||||||
func (s *Std) LookupGroup(name string) (*user.Group, error) { return user.LookupGroup(name) }
|
func (s *Std) LookupGroup(name string) (*user.Group, error) { return user.LookupGroup(name) }
|
||||||
func (s *Std) ReadDir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) }
|
func (s *Std) ReadDir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) }
|
||||||
func (s *Std) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) }
|
func (s *Std) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) }
|
||||||
|
58
ldd/exec.go
58
ldd/exec.go
@ -3,56 +3,56 @@ package ldd
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper"
|
"git.gensokyo.uk/security/fortify/sandbox"
|
||||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const lddTimeout = 2 * time.Second
|
const lddTimeout = 2 * time.Second
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
msgStatic = []byte("Not a valid dynamic program")
|
||||||
msgStaticGlibc = []byte("not a dynamic executable")
|
msgStaticGlibc = []byte("not a dynamic executable")
|
||||||
)
|
)
|
||||||
|
|
||||||
func Exec(ctx context.Context, p string) ([]*Entry, error) {
|
func Exec(ctx context.Context, p string) ([]*Entry, error) { return ExecFilter(ctx, nil, nil, p) }
|
||||||
var h helper.Helper
|
|
||||||
|
|
||||||
if toolPath, err := exec.LookPath("ldd"); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if h, err = helper.NewBwrap(
|
|
||||||
(&bwrap.Config{
|
|
||||||
Hostname: "fortify-ldd",
|
|
||||||
Chdir: "/",
|
|
||||||
Syscall: &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
|
|
||||||
NewSession: true,
|
|
||||||
DieWithParent: true,
|
|
||||||
}).Bind("/", "/").DevTmpfs("/dev"), toolPath, false,
|
|
||||||
nil, func(_, _ int) []string { return []string{p} },
|
|
||||||
nil, nil,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
|
|
||||||
h.Stdout(stdout).Stderr(stderr)
|
|
||||||
|
|
||||||
|
func ExecFilter(ctx context.Context,
|
||||||
|
commandContext func(context.Context) *exec.Cmd,
|
||||||
|
f func([]byte) []byte,
|
||||||
|
p string) ([]*Entry, error) {
|
||||||
c, cancel := context.WithTimeout(ctx, lddTimeout)
|
c, cancel := context.WithTimeout(ctx, lddTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := h.Start(c, false); err != nil {
|
container := sandbox.New(c, "ldd", p)
|
||||||
|
container.CommandContext = commandContext
|
||||||
|
container.Hostname = "fortify-ldd"
|
||||||
|
stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
|
||||||
|
container.Stdout = stdout
|
||||||
|
container.Stderr = stderr
|
||||||
|
container.Bind("/", "/", 0).Proc("/proc").Dev("/dev")
|
||||||
|
|
||||||
|
if err := container.Start(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := h.Wait(); err != nil {
|
defer func() { _, _ = io.Copy(os.Stderr, stderr) }()
|
||||||
|
if err := container.Serve(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := container.Wait(); err != nil {
|
||||||
m := stderr.Bytes()
|
m := stderr.Bytes()
|
||||||
if bytes.Contains(m, msgStaticGlibc) {
|
if bytes.Contains(m, append([]byte(p+": "), msgStatic...)) ||
|
||||||
|
bytes.Contains(m, msgStaticGlibc) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _ = os.Stderr.Write(m)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return Parse(stdout)
|
v := stdout.Bytes()
|
||||||
|
if f != nil {
|
||||||
|
v = f(v)
|
||||||
|
}
|
||||||
|
return Parse(v)
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
package ldd
|
package ldd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"math"
|
"math"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -15,8 +14,8 @@ type Entry struct {
|
|||||||
Location uint64 `json:"location"`
|
Location uint64 `json:"location"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Parse(stdout fmt.Stringer) ([]*Entry, error) {
|
func Parse(p []byte) ([]*Entry, error) {
|
||||||
payload := strings.Split(strings.TrimSpace(stdout.String()), "\n")
|
payload := strings.Split(strings.TrimSpace(string(p)), "\n")
|
||||||
result := make([]*Entry, len(payload))
|
result := make([]*Entry, len(payload))
|
||||||
|
|
||||||
for i, ent := range payload {
|
for i, ent := range payload {
|
||||||
|
@ -3,7 +3,6 @@ package ldd_test
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/ldd"
|
"git.gensokyo.uk/security/fortify/ldd"
|
||||||
@ -34,10 +33,7 @@ libzstd.so.1 => /usr/lib/libzstd.so.1 7ff71bfd2000
|
|||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
stdout := new(strings.Builder)
|
if _, err := ldd.Parse([]byte(tc.out)); !errors.Is(err, tc.wantErr) {
|
||||||
stdout.WriteString(tc.out)
|
|
||||||
|
|
||||||
if _, err := ldd.Parse(stdout); !errors.Is(err, tc.wantErr) {
|
|
||||||
t.Errorf("Parse() error = %v, wantErr %v", err, tc.wantErr)
|
t.Errorf("Parse() error = %v, wantErr %v", err, tc.wantErr)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -111,10 +107,7 @@ libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7ff71c0a4000)`,
|
|||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.file, func(t *testing.T) {
|
t.Run(tc.file, func(t *testing.T) {
|
||||||
stdout := new(strings.Builder)
|
if got, err := ldd.Parse([]byte(tc.out)); err != nil {
|
||||||
stdout.WriteString(tc.out)
|
|
||||||
|
|
||||||
if got, err := ldd.Parse(stdout); err != nil {
|
|
||||||
t.Errorf("Parse() error = %v", err)
|
t.Errorf("Parse() error = %v", err)
|
||||||
} else if !reflect.DeepEqual(got, tc.want) {
|
} else if !reflect.DeepEqual(got, tc.want) {
|
||||||
t.Errorf("Parse() got = %#v, want %#v", got, tc.want)
|
t.Errorf("Parse() got = %#v, want %#v", got, tc.want)
|
||||||
|
21
ldd/path.go
Normal file
21
ldd/path.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package ldd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Path returns a deterministic, deduplicated slice of absolute directory paths in entries.
|
||||||
|
func Path(entries []*Entry) []string {
|
||||||
|
p := make([]string, 0, len(entries)*2)
|
||||||
|
for _, entry := range entries {
|
||||||
|
if path.IsAbs(entry.Path) {
|
||||||
|
p = append(p, path.Dir(entry.Path))
|
||||||
|
}
|
||||||
|
if path.IsAbs(entry.Name) {
|
||||||
|
p = append(p, path.Dir(entry.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.Sort(p)
|
||||||
|
return slices.Compact(p)
|
||||||
|
}
|
14
main.go
14
main.go
@ -18,14 +18,14 @@ import (
|
|||||||
"git.gensokyo.uk/security/fortify/command"
|
"git.gensokyo.uk/security/fortify/command"
|
||||||
"git.gensokyo.uk/security/fortify/dbus"
|
"git.gensokyo.uk/security/fortify/dbus"
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
"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/app"
|
"git.gensokyo.uk/security/fortify/internal/app"
|
||||||
init0 "git.gensokyo.uk/security/fortify/internal/app/init"
|
"git.gensokyo.uk/security/fortify/internal/app/init0"
|
||||||
"git.gensokyo.uk/security/fortify/internal/app/shim"
|
"git.gensokyo.uk/security/fortify/internal/app/shim"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
"git.gensokyo.uk/security/fortify/internal/state"
|
"git.gensokyo.uk/security/fortify/internal/state"
|
||||||
"git.gensokyo.uk/security/fortify/internal/sys"
|
"git.gensokyo.uk/security/fortify/internal/sys"
|
||||||
|
"git.gensokyo.uk/security/fortify/sandbox"
|
||||||
"git.gensokyo.uk/security/fortify/system"
|
"git.gensokyo.uk/security/fortify/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -41,10 +41,11 @@ func init() { fmsg.Prepare("fortify") }
|
|||||||
var std sys.State = new(sys.Std)
|
var std sys.State = new(sys.Std)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// early init argv0 check, skips root check and duplicate PR_SET_DUMPABLE
|
// early init path, skips root check and duplicate PR_SET_DUMPABLE
|
||||||
|
sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
|
||||||
init0.TryArgv0()
|
init0.TryArgv0()
|
||||||
|
|
||||||
if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
|
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
|
||||||
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
|
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
|
||||||
// not fatal: this program runs as the privileged user
|
// not fatal: this program runs as the privileged user
|
||||||
}
|
}
|
||||||
@ -69,10 +70,7 @@ func buildCommand(out io.Writer) command.Command {
|
|||||||
flagJSON bool
|
flagJSON bool
|
||||||
)
|
)
|
||||||
c := command.New(out, log.Printf, "fortify", func([]string) error {
|
c := command.New(out, log.Printf, "fortify", func([]string) error {
|
||||||
fmsg.Store(flagVerbose)
|
internal.InstallFmsg(flagVerbose)
|
||||||
if flagVerbose {
|
|
||||||
seccomp.CPrintln = log.Println
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}).
|
}).
|
||||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
|
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
|
||||||
|
6
sandbox/const.go
Normal file
6
sandbox/const.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package sandbox
|
||||||
|
|
||||||
|
const (
|
||||||
|
PR_SET_NO_NEW_PRIVS = 0x26
|
||||||
|
CAP_SYS_ADMIN = 0x15
|
||||||
|
)
|
232
sandbox/container.go
Normal file
232
sandbox/container.go
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
package sandbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/gob"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HardeningFlags uintptr
|
||||||
|
|
||||||
|
const (
|
||||||
|
FSyscallCompat HardeningFlags = 1 << iota
|
||||||
|
FAllowDevel
|
||||||
|
FAllowUserns
|
||||||
|
FAllowTTY
|
||||||
|
FAllowNet
|
||||||
|
)
|
||||||
|
|
||||||
|
func (flags HardeningFlags) seccomp(opts seccomp.SyscallOpts) seccomp.SyscallOpts {
|
||||||
|
if flags&FSyscallCompat == 0 {
|
||||||
|
opts |= seccomp.FlagExt
|
||||||
|
}
|
||||||
|
if flags&FAllowDevel == 0 {
|
||||||
|
opts |= seccomp.FlagDenyDevel
|
||||||
|
}
|
||||||
|
if flags&FAllowUserns == 0 {
|
||||||
|
opts |= seccomp.FlagDenyNS
|
||||||
|
}
|
||||||
|
if flags&FAllowTTY == 0 {
|
||||||
|
opts |= seccomp.FlagDenyTTY
|
||||||
|
}
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Container represents a container environment being prepared or run.
|
||||||
|
// None of [Container] methods are safe for concurrent use.
|
||||||
|
Container struct {
|
||||||
|
// Name of initial process in the container.
|
||||||
|
name string
|
||||||
|
// Cgroup fd, nil to disable.
|
||||||
|
Cgroup *int
|
||||||
|
// ExtraFiles passed through to initial process in the container,
|
||||||
|
// with behaviour identical to its [exec.Cmd] counterpart.
|
||||||
|
ExtraFiles []*os.File
|
||||||
|
|
||||||
|
InitParams
|
||||||
|
// Custom [exec.Cmd] initialisation function.
|
||||||
|
CommandContext func(ctx context.Context) (cmd *exec.Cmd)
|
||||||
|
|
||||||
|
// param encoder for shim and init
|
||||||
|
setup *gob.Encoder
|
||||||
|
// cancels cmd
|
||||||
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
Stdin io.Reader
|
||||||
|
Stdout io.Writer
|
||||||
|
Stderr io.Writer
|
||||||
|
|
||||||
|
Cancel func() error
|
||||||
|
WaitDelay time.Duration
|
||||||
|
|
||||||
|
cmd *exec.Cmd
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
InitParams struct {
|
||||||
|
// Working directory in the container.
|
||||||
|
Dir string
|
||||||
|
// Initial process environment.
|
||||||
|
Env []string
|
||||||
|
// Absolute path of initial process in the container. Overrides name.
|
||||||
|
Path string
|
||||||
|
// Initial process argv.
|
||||||
|
Args []string
|
||||||
|
|
||||||
|
// Mapped Uid in user namespace.
|
||||||
|
Uid int
|
||||||
|
// Mapped Gid in user namespace.
|
||||||
|
Gid int
|
||||||
|
// Hostname value in UTS namespace.
|
||||||
|
Hostname string
|
||||||
|
// Sequential container setup ops.
|
||||||
|
*Ops
|
||||||
|
// Extra seccomp options.
|
||||||
|
Seccomp seccomp.SyscallOpts
|
||||||
|
|
||||||
|
Flags HardeningFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
Ops []Op
|
||||||
|
Op interface {
|
||||||
|
apply(params *InitParams) error
|
||||||
|
|
||||||
|
Is(op Op) bool
|
||||||
|
fmt.Stringer
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *Container) Start() error {
|
||||||
|
if p.cmd != nil {
|
||||||
|
return errors.New("sandbox: already started")
|
||||||
|
}
|
||||||
|
if p.Ops == nil || len(*p.Ops) == 0 {
|
||||||
|
return errors.New("sandbox: starting an empty container")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(p.ctx)
|
||||||
|
p.cancel = cancel
|
||||||
|
|
||||||
|
var cloneFlags uintptr = syscall.CLONE_NEWIPC |
|
||||||
|
syscall.CLONE_NEWUTS |
|
||||||
|
syscall.CLONE_NEWCGROUP
|
||||||
|
if p.Flags&FAllowNet == 0 {
|
||||||
|
cloneFlags |= syscall.CLONE_NEWNET
|
||||||
|
}
|
||||||
|
|
||||||
|
// map to overflow id to work around ownership checks
|
||||||
|
if p.Uid < 1 {
|
||||||
|
p.Uid = OverflowUid()
|
||||||
|
}
|
||||||
|
if p.Gid < 1 {
|
||||||
|
p.Gid = OverflowGid()
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.CommandContext != nil {
|
||||||
|
p.cmd = p.CommandContext(ctx)
|
||||||
|
} else {
|
||||||
|
p.cmd = exec.CommandContext(ctx, MustExecutable())
|
||||||
|
p.cmd.Args = []string{"init"}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
|
||||||
|
p.cmd.Cancel, p.cmd.WaitDelay = p.Cancel, p.WaitDelay
|
||||||
|
p.cmd.Dir = "/"
|
||||||
|
p.cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
Setsid: p.Flags&FAllowTTY == 0,
|
||||||
|
Pdeathsig: syscall.SIGKILL,
|
||||||
|
|
||||||
|
Cloneflags: cloneFlags |
|
||||||
|
syscall.CLONE_NEWUSER |
|
||||||
|
syscall.CLONE_NEWPID |
|
||||||
|
syscall.CLONE_NEWNS,
|
||||||
|
|
||||||
|
// remain privileged for setup
|
||||||
|
AmbientCaps: []uintptr{CAP_SYS_ADMIN},
|
||||||
|
|
||||||
|
UseCgroupFD: p.Cgroup != nil,
|
||||||
|
}
|
||||||
|
if p.cmd.SysProcAttr.UseCgroupFD {
|
||||||
|
p.cmd.SysProcAttr.CgroupFD = *p.Cgroup
|
||||||
|
}
|
||||||
|
|
||||||
|
// place setup pipe before user supplied extra files, this is later restored by init
|
||||||
|
if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil {
|
||||||
|
return wrapErrSuffix(err,
|
||||||
|
"cannot create shim setup pipe:")
|
||||||
|
} else {
|
||||||
|
p.setup = e
|
||||||
|
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
|
||||||
|
}
|
||||||
|
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
|
||||||
|
|
||||||
|
msg.Verbose("starting container init")
|
||||||
|
if err := p.cmd.Start(); err != nil {
|
||||||
|
return msg.WrapErr(err, err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Container) Serve() error {
|
||||||
|
if p.setup == nil {
|
||||||
|
panic("invalid serve")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Path != "" && !path.IsAbs(p.Path) {
|
||||||
|
return msg.WrapErr(syscall.EINVAL,
|
||||||
|
fmt.Sprintf("invalid executable path %q", p.Path))
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Path == "" {
|
||||||
|
if p.name == "" {
|
||||||
|
p.Path = os.Getenv("SHELL")
|
||||||
|
if !path.IsAbs(p.Path) {
|
||||||
|
return msg.WrapErr(syscall.EBADE,
|
||||||
|
"no command specified and $SHELL is invalid")
|
||||||
|
}
|
||||||
|
p.name = path.Base(p.Path)
|
||||||
|
} else if path.IsAbs(p.name) {
|
||||||
|
p.Path = p.name
|
||||||
|
} else if v, err := exec.LookPath(p.name); err != nil {
|
||||||
|
return msg.WrapErr(err, err.Error())
|
||||||
|
} else {
|
||||||
|
p.Path = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setup := p.setup
|
||||||
|
p.setup = nil
|
||||||
|
return setup.Encode(
|
||||||
|
&initParams{
|
||||||
|
p.InitParams,
|
||||||
|
syscall.Getuid(),
|
||||||
|
syscall.Getgid(),
|
||||||
|
len(p.ExtraFiles),
|
||||||
|
msg.IsVerbose(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() }
|
||||||
|
|
||||||
|
func (p *Container) String() string {
|
||||||
|
return fmt.Sprintf("argv: %q, flags: %#x, seccomp: %#x",
|
||||||
|
p.Args, p.Flags, int(p.Flags.seccomp(p.Seccomp)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context, name string, args ...string) *Container {
|
||||||
|
return &Container{name: name, ctx: ctx,
|
||||||
|
InitParams: InitParams{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)},
|
||||||
|
}
|
||||||
|
}
|
182
sandbox/container_test.go
Normal file
182
sandbox/container_test.go
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
package sandbox_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
|
"git.gensokyo.uk/security/fortify/ldd"
|
||||||
|
"git.gensokyo.uk/security/fortify/sandbox"
|
||||||
|
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
||||||
|
check "git.gensokyo.uk/security/fortify/test/sandbox"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestContainer(t *testing.T) {
|
||||||
|
{
|
||||||
|
oldVerbose := fmsg.Load()
|
||||||
|
oldOutput := sandbox.GetOutput()
|
||||||
|
internal.InstallFmsg(true)
|
||||||
|
t.Cleanup(func() { fmsg.Store(oldVerbose) })
|
||||||
|
t.Cleanup(func() { sandbox.SetOutput(oldOutput) })
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
flags sandbox.HardeningFlags
|
||||||
|
ops *sandbox.Ops
|
||||||
|
mnt []*check.Mntent
|
||||||
|
host string
|
||||||
|
}{
|
||||||
|
{"minimal", 0, new(sandbox.Ops), nil, "test-minimal"},
|
||||||
|
{"allow", sandbox.FAllowUserns | sandbox.FAllowNet | sandbox.FAllowTTY,
|
||||||
|
new(sandbox.Ops), nil, "test-minimal"},
|
||||||
|
{"tmpfs", 0,
|
||||||
|
new(sandbox.Ops).
|
||||||
|
Tmpfs(fst.Tmp, 0, 0755),
|
||||||
|
[]*check.Mntent{
|
||||||
|
{FSName: "tmpfs", Dir: fst.Tmp, Type: "tmpfs", Opts: "\x00"},
|
||||||
|
}, "test-tmpfs"},
|
||||||
|
{"dev", sandbox.FAllowTTY, // go test output is not a tty
|
||||||
|
new(sandbox.Ops).
|
||||||
|
Dev("/dev"),
|
||||||
|
[]*check.Mntent{
|
||||||
|
{FSName: "devtmpfs", Dir: "/dev", Type: "tmpfs", Opts: "\x00"},
|
||||||
|
{FSName: "devtmpfs", Dir: "/dev/null", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
|
||||||
|
{FSName: "devtmpfs", Dir: "/dev/zero", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
|
||||||
|
{FSName: "devtmpfs", Dir: "/dev/full", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
|
||||||
|
{FSName: "devtmpfs", Dir: "/dev/random", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
|
||||||
|
{FSName: "devtmpfs", Dir: "/dev/urandom", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
|
||||||
|
{FSName: "devtmpfs", Dir: "/dev/tty", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
|
||||||
|
{FSName: "devpts", Dir: "/dev/pts", Type: "devpts", Opts: "rw,nosuid,noexec,relatime,mode=620,ptmxmode=666", Freq: 0, Passno: 0},
|
||||||
|
}, ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
container := sandbox.New(ctx, os.Args[0], "-test.v",
|
||||||
|
"-test.run=TestHelperCheckContainer", "--", "check", tc.host)
|
||||||
|
container.Uid = 1000
|
||||||
|
container.Gid = 100
|
||||||
|
container.Hostname = tc.host
|
||||||
|
container.CommandContext = commandContext
|
||||||
|
container.Flags |= tc.flags
|
||||||
|
container.Stdout, container.Stderr = os.Stdout, os.Stderr
|
||||||
|
container.Ops = tc.ops
|
||||||
|
if container.Args[5] == "" {
|
||||||
|
if name, err := os.Hostname(); err != nil {
|
||||||
|
t.Fatalf("cannot get hostname: %v", err)
|
||||||
|
} else {
|
||||||
|
container.Args[5] = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
container.
|
||||||
|
Tmpfs("/tmp", 0, 0755).
|
||||||
|
Bind(os.Args[0], os.Args[0], 0)
|
||||||
|
// in case test has cgo enabled
|
||||||
|
var libPaths []string
|
||||||
|
if entries, err := ldd.ExecFilter(ctx,
|
||||||
|
commandContext,
|
||||||
|
func(v []byte) []byte {
|
||||||
|
return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1]
|
||||||
|
}, os.Args[0]); err != nil {
|
||||||
|
log.Fatalf("ldd: %v", err)
|
||||||
|
} else {
|
||||||
|
libPaths = ldd.Path(entries)
|
||||||
|
}
|
||||||
|
for _, name := range libPaths {
|
||||||
|
container.Bind(name, name, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
mnt := make([]*check.Mntent, 0, 3+len(libPaths))
|
||||||
|
mnt = append(mnt, &check.Mntent{FSName: "rootfs", Dir: "/", Type: "tmpfs", Opts: "host_passthrough"})
|
||||||
|
mnt = append(mnt, tc.mnt...)
|
||||||
|
mnt = append(mnt,
|
||||||
|
&check.Mntent{FSName: "tmpfs", Dir: "/tmp", Type: "tmpfs", Opts: "host_passthrough"},
|
||||||
|
&check.Mntent{FSName: "\x00", Dir: os.Args[0], Type: "\x00", Opts: "\x00"})
|
||||||
|
for _, name := range libPaths {
|
||||||
|
mnt = append(mnt, &check.Mntent{FSName: "\x00", Dir: name, Type: "\x00", Opts: "\x00", Freq: -1, Passno: -1})
|
||||||
|
}
|
||||||
|
mnt = append(mnt, &check.Mntent{FSName: "proc", Dir: "/proc", Type: "proc", Opts: "rw,nosuid,nodev,noexec,relatime"})
|
||||||
|
mntentWant := new(bytes.Buffer)
|
||||||
|
if err := json.NewEncoder(mntentWant).Encode(mnt); err != nil {
|
||||||
|
t.Fatalf("cannot serialise mntent: %v", err)
|
||||||
|
}
|
||||||
|
container.Stdin = mntentWant
|
||||||
|
|
||||||
|
// needs /proc to check mntent
|
||||||
|
container.Proc("/proc")
|
||||||
|
|
||||||
|
if err := container.Start(); err != nil {
|
||||||
|
fmsg.PrintBaseError(err, "start:")
|
||||||
|
t.Fatalf("cannot start container: %v", err)
|
||||||
|
} else if err = container.Serve(); err != nil {
|
||||||
|
fmsg.PrintBaseError(err, "serve:")
|
||||||
|
t.Errorf("cannot serve setup params: %v", err)
|
||||||
|
}
|
||||||
|
if err := container.Wait(); err != nil {
|
||||||
|
fmsg.PrintBaseError(err, "wait:")
|
||||||
|
t.Fatalf("wait: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainerString(t *testing.T) {
|
||||||
|
container := sandbox.New(context.TODO(), "ldd", "/usr/bin/env")
|
||||||
|
container.Flags |= sandbox.FAllowDevel
|
||||||
|
container.Seccomp |= seccomp.FlagMultiarch
|
||||||
|
want := `argv: ["ldd" "/usr/bin/env"], flags: 0x2, seccomp: 0x2e`
|
||||||
|
if got := container.String(); got != want {
|
||||||
|
t.Errorf("String: %s, want %s", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHelperInit(t *testing.T) {
|
||||||
|
if len(os.Args) != 5 || os.Args[4] != "init" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sandbox.SetOutput(fmsg.Output{})
|
||||||
|
sandbox.Init(fmsg.Prepare, internal.InstallFmsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHelperCheckContainer(t *testing.T) {
|
||||||
|
if len(os.Args) != 6 || os.Args[4] != "check" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("user", func(t *testing.T) {
|
||||||
|
if uid := syscall.Getuid(); uid != 1000 {
|
||||||
|
t.Errorf("Getuid: %d, want 1000", uid)
|
||||||
|
}
|
||||||
|
if gid := syscall.Getgid(); gid != 100 {
|
||||||
|
t.Errorf("Getgid: %d, want 100", gid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("hostname", func(t *testing.T) {
|
||||||
|
if name, err := os.Hostname(); err != nil {
|
||||||
|
t.Fatalf("cannot get hostname: %v", err)
|
||||||
|
} else if name != os.Args[5] {
|
||||||
|
t.Errorf("Hostname: %q, want %q", name, os.Args[5])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("seccomp", func(t *testing.T) { check.MustAssertSeccomp() })
|
||||||
|
t.Run("mntent", func(t *testing.T) { check.MustAssertMounts("", "/proc/mounts", "/proc/self/fd/0") })
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandContext(ctx context.Context) *exec.Cmd {
|
||||||
|
return exec.CommandContext(ctx, os.Args[0], "-test.v",
|
||||||
|
"-test.run=TestHelperInit", "--", "init")
|
||||||
|
}
|
@ -1,11 +1,9 @@
|
|||||||
package internal
|
package sandbox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -15,7 +13,7 @@ var (
|
|||||||
|
|
||||||
func copyExecutable() {
|
func copyExecutable() {
|
||||||
if name, err := os.Executable(); err != nil {
|
if name, err := os.Executable(); err != nil {
|
||||||
fmsg.BeforeExit()
|
msg.BeforeExit()
|
||||||
log.Fatalf("cannot read executable path: %v", err)
|
log.Fatalf("cannot read executable path: %v", err)
|
||||||
} else {
|
} else {
|
||||||
executable = name
|
executable = name
|
17
sandbox/executable_test.go
Normal file
17
sandbox/executable_test.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package sandbox_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/sandbox"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExecutable(t *testing.T) {
|
||||||
|
for i := 0; i < 16; i++ {
|
||||||
|
if got := sandbox.MustExecutable(); got != os.Args[0] {
|
||||||
|
t.Errorf("MustExecutable: %q, want %q",
|
||||||
|
got, os.Args[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
341
sandbox/init.go
Normal file
341
sandbox/init.go
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
package sandbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// time to wait for linger processes after death of initial process
|
||||||
|
residualProcessTimeout = 5 * time.Second
|
||||||
|
|
||||||
|
// intermediate tmpfs mount point
|
||||||
|
basePath = "/tmp"
|
||||||
|
|
||||||
|
// setup params file descriptor
|
||||||
|
setupEnv = "FORTIFY_SETUP"
|
||||||
|
)
|
||||||
|
|
||||||
|
type initParams struct {
|
||||||
|
InitParams
|
||||||
|
|
||||||
|
HostUid, HostGid int
|
||||||
|
// extra files count
|
||||||
|
Count int
|
||||||
|
// verbosity pass through
|
||||||
|
Verbose bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
||||||
|
runtime.LockOSThread()
|
||||||
|
prepare("init")
|
||||||
|
|
||||||
|
if os.Getpid() != 1 {
|
||||||
|
log.Fatal("this process must run as pid 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
receive setup payload
|
||||||
|
*/
|
||||||
|
|
||||||
|
var (
|
||||||
|
params initParams
|
||||||
|
closeSetup func() error
|
||||||
|
setupFile *os.File
|
||||||
|
offsetSetup int
|
||||||
|
)
|
||||||
|
if f, err := Receive(setupEnv, ¶ms, &setupFile); err != nil {
|
||||||
|
if errors.Is(err, ErrInvalid) {
|
||||||
|
log.Fatal("invalid setup descriptor")
|
||||||
|
}
|
||||||
|
if errors.Is(err, ErrNotSet) {
|
||||||
|
log.Fatal("FORTIFY_SETUP not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Fatalf("cannot decode init setup payload: %v", err)
|
||||||
|
} else {
|
||||||
|
if params.Ops == nil {
|
||||||
|
log.Fatal("invalid setup parameters")
|
||||||
|
}
|
||||||
|
|
||||||
|
setVerbose(params.Verbose)
|
||||||
|
msg.Verbose("received setup parameters")
|
||||||
|
closeSetup = f
|
||||||
|
offsetSetup = int(setupFile.Fd() + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write uid/gid map here so parent does not need to set dumpable
|
||||||
|
if err := SetDumpable(SUID_DUMP_USER); err != nil {
|
||||||
|
log.Fatalf("cannot set SUID_DUMP_USER: %s", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile("/proc/self/uid_map",
|
||||||
|
append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
|
||||||
|
0); err != nil {
|
||||||
|
log.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile("/proc/self/setgroups",
|
||||||
|
[]byte("deny\n"),
|
||||||
|
0); err != nil && !os.IsNotExist(err) {
|
||||||
|
log.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile("/proc/self/gid_map",
|
||||||
|
append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
|
||||||
|
0); err != nil {
|
||||||
|
log.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
if err := SetDumpable(SUID_DUMP_DISABLE); err != nil {
|
||||||
|
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Hostname != "" {
|
||||||
|
if err := syscall.Sethostname([]byte(params.Hostname)); err != nil {
|
||||||
|
log.Fatalf("cannot set hostname: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
set up mount points from intermediate root
|
||||||
|
*/
|
||||||
|
|
||||||
|
if err := syscall.Mount("", "/", "",
|
||||||
|
syscall.MS_SILENT|syscall.MS_SLAVE|syscall.MS_REC,
|
||||||
|
""); err != nil {
|
||||||
|
log.Fatalf("cannot make / rslave: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := syscall.Mount("rootfs", basePath, "tmpfs",
|
||||||
|
syscall.MS_NODEV|syscall.MS_NOSUID,
|
||||||
|
""); err != nil {
|
||||||
|
log.Fatalf("cannot mount intermediate root: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.Chdir(basePath); err != nil {
|
||||||
|
log.Fatalf("cannot enter base path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Mkdir(sysrootDir, 0755); err != nil {
|
||||||
|
log.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
if err := syscall.Mount(sysrootDir, sysrootDir, "",
|
||||||
|
syscall.MS_SILENT|syscall.MS_MGC_VAL|syscall.MS_BIND|syscall.MS_REC,
|
||||||
|
""); err != nil {
|
||||||
|
log.Fatalf("cannot bind sysroot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Mkdir(hostDir, 0755); err != nil {
|
||||||
|
log.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
if err := syscall.PivotRoot(basePath, hostDir); err != nil {
|
||||||
|
log.Fatalf("cannot pivot into intermediate root: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.Chdir("/"); err != nil {
|
||||||
|
log.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, op := range *params.Ops {
|
||||||
|
msg.Verbosef("mounting %s", op)
|
||||||
|
if err := op.apply(¶ms.InitParams); err != nil {
|
||||||
|
msg.PrintBaseErr(err,
|
||||||
|
fmt.Sprintf("cannot apply op %d:", i))
|
||||||
|
msg.BeforeExit()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
pivot to sysroot
|
||||||
|
*/
|
||||||
|
|
||||||
|
if err := syscall.Mount(hostDir, hostDir, "",
|
||||||
|
syscall.MS_SILENT|syscall.MS_REC|syscall.MS_PRIVATE,
|
||||||
|
""); err != nil {
|
||||||
|
log.Fatalf("cannot make host root rprivate: %v", err)
|
||||||
|
}
|
||||||
|
if err := syscall.Unmount(hostDir, syscall.MNT_DETACH); err != nil {
|
||||||
|
log.Fatalf("cannot unmount host root: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var fd int
|
||||||
|
if err := IgnoringEINTR(func() (err error) {
|
||||||
|
fd, err = syscall.Open("/", syscall.O_DIRECTORY|syscall.O_RDONLY, 0)
|
||||||
|
return
|
||||||
|
}); err != nil {
|
||||||
|
log.Fatalf("cannot open intermediate root: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.Chdir(sysrootPath); err != nil {
|
||||||
|
log.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := syscall.PivotRoot(".", "."); err != nil {
|
||||||
|
log.Fatalf("cannot pivot into sysroot: %v", err)
|
||||||
|
}
|
||||||
|
if err := syscall.Fchdir(fd); err != nil {
|
||||||
|
log.Fatalf("cannot re-enter intermediate root: %v", err)
|
||||||
|
}
|
||||||
|
if err := syscall.Unmount(".", syscall.MNT_DETACH); err != nil {
|
||||||
|
log.Fatalf("cannot unmount intemediate root: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.Chdir("/"); err != nil {
|
||||||
|
log.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := syscall.Close(fd); err != nil {
|
||||||
|
log.Fatalf("cannot close intermediate root: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
load seccomp filter
|
||||||
|
*/
|
||||||
|
|
||||||
|
if _, _, err := syscall.Syscall(PR_SET_NO_NEW_PRIVS, 1, 0, 0); err != 0 {
|
||||||
|
log.Fatalf("prctl(PR_SET_NO_NEW_PRIVS): %v", err)
|
||||||
|
}
|
||||||
|
if err := seccomp.Load(params.Flags.seccomp(params.Seccomp)); err != nil {
|
||||||
|
log.Fatalf("cannot load syscall filter: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* at this point CAP_SYS_ADMIN can be dropped, however it is kept for now as it does not increase attack surface */
|
||||||
|
|
||||||
|
/*
|
||||||
|
pass through extra files
|
||||||
|
*/
|
||||||
|
|
||||||
|
extraFiles := make([]*os.File, params.Count)
|
||||||
|
for i := range extraFiles {
|
||||||
|
extraFiles[i] = os.NewFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
prepare initial process
|
||||||
|
*/
|
||||||
|
|
||||||
|
cmd := exec.Command(params.Path)
|
||||||
|
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
|
cmd.Args = params.Args
|
||||||
|
cmd.Env = params.Env
|
||||||
|
cmd.ExtraFiles = extraFiles
|
||||||
|
cmd.Dir = params.Dir
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
log.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
msg.Suspend()
|
||||||
|
|
||||||
|
/*
|
||||||
|
close setup pipe
|
||||||
|
*/
|
||||||
|
|
||||||
|
if err := closeSetup(); err != nil {
|
||||||
|
log.Println("cannot close setup pipe:", err)
|
||||||
|
// not fatal
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
perform init duties
|
||||||
|
*/
|
||||||
|
|
||||||
|
sig := make(chan os.Signal, 2)
|
||||||
|
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
type winfo struct {
|
||||||
|
wpid int
|
||||||
|
wstatus syscall.WaitStatus
|
||||||
|
}
|
||||||
|
info := make(chan winfo, 1)
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
wpid = -2
|
||||||
|
wstatus syscall.WaitStatus
|
||||||
|
)
|
||||||
|
|
||||||
|
// keep going until no child process is left
|
||||||
|
for wpid != -1 {
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if wpid != -2 {
|
||||||
|
info <- winfo{wpid, wstatus}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = syscall.EINTR
|
||||||
|
for errors.Is(err, syscall.EINTR) {
|
||||||
|
wpid, err = syscall.Wait4(-1, &wstatus, 0, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !errors.Is(err, syscall.ECHILD) {
|
||||||
|
log.Println("unexpected wait4 response:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// closed after residualProcessTimeout has elapsed after initial process death
|
||||||
|
timeout := make(chan struct{})
|
||||||
|
|
||||||
|
r := 2
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case s := <-sig:
|
||||||
|
if msg.Resume() {
|
||||||
|
msg.Verbosef("terminating on %s after process start", s.String())
|
||||||
|
} else {
|
||||||
|
msg.Verbosef("terminating on %s", s.String())
|
||||||
|
}
|
||||||
|
msg.BeforeExit()
|
||||||
|
os.Exit(0)
|
||||||
|
case w := <-info:
|
||||||
|
if w.wpid == cmd.Process.Pid {
|
||||||
|
// initial process exited, output is most likely available again
|
||||||
|
msg.Resume()
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case w.wstatus.Exited():
|
||||||
|
r = w.wstatus.ExitStatus()
|
||||||
|
case w.wstatus.Signaled():
|
||||||
|
r = 128 + int(w.wstatus.Signal())
|
||||||
|
default:
|
||||||
|
r = 255
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
time.Sleep(residualProcessTimeout)
|
||||||
|
close(timeout)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
case <-done:
|
||||||
|
msg.BeforeExit()
|
||||||
|
os.Exit(r)
|
||||||
|
case <-timeout:
|
||||||
|
log.Println("timeout exceeded waiting for lingering processes")
|
||||||
|
msg.BeforeExit()
|
||||||
|
os.Exit(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TryArgv0 calls [Init] if the last element of argv0 is "init".
|
||||||
|
func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) {
|
||||||
|
if len(os.Args) > 0 && path.Base(os.Args[0]) == "init" {
|
||||||
|
msg = v
|
||||||
|
Init(prepare, setVerbose)
|
||||||
|
msg.BeforeExit()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
95
sandbox/mount.go
Normal file
95
sandbox/mount.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package sandbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
BindOptional = 1 << iota
|
||||||
|
BindSource
|
||||||
|
BindRecursive
|
||||||
|
BindWritable
|
||||||
|
BindDevices
|
||||||
|
)
|
||||||
|
|
||||||
|
func bindMount(src, dest string, flags int) error {
|
||||||
|
target := toSysroot(dest)
|
||||||
|
var source string
|
||||||
|
|
||||||
|
if flags&BindSource == 0 {
|
||||||
|
// this is what bwrap does, so the behaviour is kept for now,
|
||||||
|
// however recursively resolving links might improve user experience
|
||||||
|
if rp, err := realpathHost(src); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
if flags&BindOptional != 0 {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return msg.WrapErr(err,
|
||||||
|
fmt.Sprintf("path %q does not exist", src))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return msg.WrapErr(err, err.Error())
|
||||||
|
} else {
|
||||||
|
source = toHost(rp)
|
||||||
|
}
|
||||||
|
} else if flags&BindOptional != 0 {
|
||||||
|
return msg.WrapErr(syscall.EINVAL,
|
||||||
|
"flag source excludes optional")
|
||||||
|
} else {
|
||||||
|
source = toHost(src)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi, err := os.Stat(source); err != nil {
|
||||||
|
return msg.WrapErr(err, err.Error())
|
||||||
|
} else if fi.IsDir() {
|
||||||
|
if err = os.MkdirAll(target, 0755); err != nil {
|
||||||
|
return wrapErrSuffix(err,
|
||||||
|
fmt.Sprintf("cannot create directory %q:", dest))
|
||||||
|
}
|
||||||
|
} else if err = ensureFile(target, 0444); err != nil {
|
||||||
|
if errors.Is(err, syscall.EISDIR) {
|
||||||
|
return msg.WrapErr(err,
|
||||||
|
fmt.Sprintf("path %q is a directory", dest))
|
||||||
|
}
|
||||||
|
return wrapErrSuffix(err,
|
||||||
|
fmt.Sprintf("cannot create %q:", dest))
|
||||||
|
}
|
||||||
|
|
||||||
|
var mf uintptr = syscall.MS_SILENT | syscall.MS_BIND
|
||||||
|
if flags&BindRecursive != 0 {
|
||||||
|
mf |= syscall.MS_REC
|
||||||
|
}
|
||||||
|
if flags&BindWritable == 0 {
|
||||||
|
mf |= syscall.MS_RDONLY
|
||||||
|
}
|
||||||
|
if flags&BindDevices == 0 {
|
||||||
|
mf |= syscall.MS_NODEV
|
||||||
|
}
|
||||||
|
if msg.IsVerbose() {
|
||||||
|
if strings.TrimPrefix(source, hostPath) == strings.TrimPrefix(target, sysrootPath) {
|
||||||
|
msg.Verbosef("resolved %q flags %#x", target, mf)
|
||||||
|
} else {
|
||||||
|
msg.Verbosef("resolved %q on %q flags %#x", source, target, mf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wrapErrSuffix(syscall.Mount(source, target, "", mf, ""),
|
||||||
|
fmt.Sprintf("cannot bind %q on %q:", src, dest))
|
||||||
|
}
|
||||||
|
|
||||||
|
func mountTmpfs(fsname, name string, size int, perm os.FileMode) error {
|
||||||
|
target := toSysroot(name)
|
||||||
|
if err := os.MkdirAll(target, perm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
opt := fmt.Sprintf("mode=%#o", perm)
|
||||||
|
if size > 0 {
|
||||||
|
opt += fmt.Sprintf(",size=%d", size)
|
||||||
|
}
|
||||||
|
return wrapErrSuffix(syscall.Mount(fsname, target, "tmpfs",
|
||||||
|
syscall.MS_NOSUID|syscall.MS_NODEV, opt),
|
||||||
|
fmt.Sprintf("cannot mount tmpfs on %q:", name))
|
||||||
|
}
|
43
sandbox/msg.go
Normal file
43
sandbox/msg.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package sandbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Msg interface {
|
||||||
|
IsVerbose() bool
|
||||||
|
Verbose(v ...any)
|
||||||
|
Verbosef(format string, v ...any)
|
||||||
|
WrapErr(err error, a ...any) error
|
||||||
|
PrintBaseErr(err error, fallback string)
|
||||||
|
|
||||||
|
Suspend()
|
||||||
|
Resume() bool
|
||||||
|
|
||||||
|
BeforeExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
type DefaultMsg struct{ inactive atomic.Bool }
|
||||||
|
|
||||||
|
func (msg *DefaultMsg) IsVerbose() bool { return true }
|
||||||
|
func (msg *DefaultMsg) Verbose(v ...any) {
|
||||||
|
if !msg.inactive.Load() {
|
||||||
|
log.Println(v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (msg *DefaultMsg) Verbosef(format string, v ...any) {
|
||||||
|
if !msg.inactive.Load() {
|
||||||
|
log.Printf(format, v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *DefaultMsg) WrapErr(err error, a ...any) error {
|
||||||
|
log.Println(a...)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
func (msg *DefaultMsg) PrintBaseErr(err error, fallback string) { log.Println(fallback, err) }
|
||||||
|
|
||||||
|
func (msg *DefaultMsg) Suspend() { msg.inactive.Store(true) }
|
||||||
|
func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) }
|
||||||
|
func (msg *DefaultMsg) BeforeExit() {}
|
19
sandbox/output.go
Normal file
19
sandbox/output.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package sandbox
|
||||||
|
|
||||||
|
var msg Msg = new(DefaultMsg)
|
||||||
|
|
||||||
|
func GetOutput() Msg { return msg }
|
||||||
|
func SetOutput(v Msg) {
|
||||||
|
if v == nil {
|
||||||
|
msg = new(DefaultMsg)
|
||||||
|
} else {
|
||||||
|
msg = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapErrSuffix(err error, a ...any) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return msg.WrapErr(err, append(a, err)...)
|
||||||
|
}
|
37
sandbox/overflow.go
Normal file
37
sandbox/overflow.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package sandbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ofUid int
|
||||||
|
ofGid int
|
||||||
|
ofOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ofUidPath = "/proc/sys/kernel/overflowuid"
|
||||||
|
ofGidPath = "/proc/sys/kernel/overflowgid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustReadOverflow() {
|
||||||
|
if v, err := os.ReadFile(ofUidPath); err != nil {
|
||||||
|
log.Fatalf("cannot read %q: %v", ofUidPath, err)
|
||||||
|
} else if ofUid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
|
||||||
|
log.Fatalf("cannot interpret %q: %v", ofUidPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := os.ReadFile(ofGidPath); err != nil {
|
||||||
|
log.Fatalf("cannot read %q: %v", ofGidPath, err)
|
||||||
|
} else if ofGid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
|
||||||
|
log.Fatalf("cannot interpret %q: %v", ofGidPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func OverflowUid() int { ofOnce.Do(mustReadOverflow); return ofUid }
|
||||||
|
func OverflowGid() int { ofOnce.Do(mustReadOverflow); return ofGid }
|
@ -1,4 +1,4 @@
|
|||||||
package proc
|
package sandbox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
@ -12,7 +12,7 @@ var (
|
|||||||
ErrInvalid = errors.New("bad file descriptor")
|
ErrInvalid = errors.New("bad file descriptor")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Setup appends the read end of a pipe for payload transmission and returns its fd.
|
// Setup appends the read end of a pipe for setup params transmission and returns its fd.
|
||||||
func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
|
func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
|
||||||
if r, w, err := os.Pipe(); err != nil {
|
if r, w, err := os.Pipe(); err != nil {
|
||||||
return -1, nil, err
|
return -1, nil, err
|
||||||
@ -23,9 +23,8 @@ func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Receive retrieves payload pipe fd from the environment,
|
// Receive retrieves setup fd from the environment and receives params.
|
||||||
// receives its payload and returns the Close method of the pipe.
|
func Receive(key string, e any, v **os.File) (func() error, error) {
|
||||||
func Receive(key string, e any) (func() error, error) {
|
|
||||||
var setup *os.File
|
var setup *os.File
|
||||||
|
|
||||||
if s, ok := os.LookupEnv(key); !ok {
|
if s, ok := os.LookupEnv(key); !ok {
|
||||||
@ -38,8 +37,11 @@ func Receive(key string, e any) (func() error, error) {
|
|||||||
if setup == nil {
|
if setup == nil {
|
||||||
return nil, ErrInvalid
|
return nil, ErrInvalid
|
||||||
}
|
}
|
||||||
|
if v != nil {
|
||||||
|
*v = setup
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return func() error { return setup.Close() }, gob.NewDecoder(setup).Decode(e)
|
return setup.Close, gob.NewDecoder(setup).Decode(e)
|
||||||
}
|
}
|
75
sandbox/path.go
Normal file
75
sandbox/path.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package sandbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
hostPath = "/" + hostDir
|
||||||
|
hostDir = "host"
|
||||||
|
sysrootPath = "/" + sysrootDir
|
||||||
|
sysrootDir = "sysroot"
|
||||||
|
)
|
||||||
|
|
||||||
|
func toSysroot(name string) string {
|
||||||
|
name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' })
|
||||||
|
return path.Join(sysrootPath, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toHost(name string) string {
|
||||||
|
name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' })
|
||||||
|
return path.Join(hostPath, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func realpathHost(name string) (string, error) {
|
||||||
|
source := toHost(name)
|
||||||
|
rp, err := os.Readlink(source)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, syscall.EINVAL) {
|
||||||
|
// not a symlink
|
||||||
|
return name, nil
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !path.IsAbs(rp) {
|
||||||
|
return name, nil
|
||||||
|
}
|
||||||
|
msg.Verbosef("path %q resolves to %q", name, rp)
|
||||||
|
return rp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createFile(name string, perm os.FileMode, content []byte) error {
|
||||||
|
if err := os.MkdirAll(path.Dir(name), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if content != nil {
|
||||||
|
_, err = f.Write(content)
|
||||||
|
}
|
||||||
|
return errors.Join(f.Close(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureFile(name string, perm os.FileMode) error {
|
||||||
|
fi, err := os.Stat(name)
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return createFile(name, perm, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 {
|
||||||
|
err = syscall.EISDIR
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
@ -11,6 +11,9 @@ import (
|
|||||||
// New returns an inactive Encoder instance.
|
// New returns an inactive Encoder instance.
|
||||||
func New(opts SyscallOpts) *Encoder { return &Encoder{newExporter(opts)} }
|
func New(opts SyscallOpts) *Encoder { return &Encoder{newExporter(opts)} }
|
||||||
|
|
||||||
|
// Load loads a filter into the kernel.
|
||||||
|
func Load(opts SyscallOpts) error { return buildFilter(-1, opts) }
|
||||||
|
|
||||||
/*
|
/*
|
||||||
An Encoder writes a BPF program to an output stream.
|
An Encoder writes a BPF program to an output stream.
|
||||||
|
|
@ -28,7 +28,7 @@ func (e *exporter) prepare() error {
|
|||||||
|
|
||||||
ec := make(chan error, 1)
|
ec := make(chan error, 1)
|
||||||
go func(fd uintptr) {
|
go func(fd uintptr) {
|
||||||
ec <- exportFilter(fd, e.opts)
|
ec <- buildFilter(int(fd), e.opts)
|
||||||
close(ec)
|
close(ec)
|
||||||
_ = e.closeWrite()
|
_ = e.closeWrite()
|
||||||
runtime.KeepAlive(e.w)
|
runtime.KeepAlive(e.w)
|
@ -4,12 +4,11 @@ import (
|
|||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"slices"
|
"slices"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/seccomp"
|
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestExport(t *testing.T) {
|
func TestExport(t *testing.T) {
|
||||||
@ -79,8 +78,9 @@ func TestExport(t *testing.T) {
|
|||||||
buf := make([]byte, 8)
|
buf := make([]byte, 8)
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
seccomp.CPrintln = log.Println
|
oldF := seccomp.GetOutput()
|
||||||
t.Cleanup(func() { seccomp.CPrintln = nil })
|
seccomp.SetOutput(t.Log)
|
||||||
|
t.Cleanup(func() { seccomp.SetOutput(oldF) })
|
||||||
|
|
||||||
e := seccomp.New(tc.opts)
|
e := seccomp.New(tc.opts)
|
||||||
digest := sha512.New()
|
digest := sha512.New()
|
||||||
@ -115,7 +115,7 @@ func TestExport(t *testing.T) {
|
|||||||
t.Errorf("Read: error = %v", err)
|
t.Errorf("Read: error = %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := e.Close(); err == nil || err.Error() != "seccomp_export_bpf failed: operation canceled" {
|
if err := e.Close(); err == nil || !errors.Is(err, syscall.ECANCELED) || !errors.Is(err, syscall.EBADF) {
|
||||||
t.Errorf("Close: error = %v", err)
|
t.Errorf("Close: error = %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
30
sandbox/seccomp/output.go
Normal file
30
sandbox/seccomp/output.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package seccomp
|
||||||
|
|
||||||
|
import "C"
|
||||||
|
import "sync/atomic"
|
||||||
|
|
||||||
|
var printlnP atomic.Pointer[func(v ...any)]
|
||||||
|
|
||||||
|
func SetOutput(f func(v ...any)) {
|
||||||
|
if f == nil {
|
||||||
|
// avoid storing nil function
|
||||||
|
printlnP.Store(nil)
|
||||||
|
} else {
|
||||||
|
printlnP.Store(&f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOutput() func(v ...any) {
|
||||||
|
if fp := printlnP.Load(); fp == nil {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return *fp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//export F_println
|
||||||
|
func F_println(v *C.char) {
|
||||||
|
if fp := printlnP.Load(); fp != nil {
|
||||||
|
(*fp)(C.GoString(v))
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@
|
|||||||
#define _GNU_SOURCE // CLONE_NEWUSER
|
#define _GNU_SOURCE // CLONE_NEWUSER
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "seccomp-export.h"
|
#include "seccomp-build.h"
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
@ -27,28 +27,27 @@ struct f_syscall_act {
|
|||||||
|
|
||||||
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
|
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
|
||||||
|
|
||||||
#define SECCOMP_RULESET_ADD(ruleset) do { \
|
#define SECCOMP_RULESET_ADD(ruleset) do { \
|
||||||
if (opts & F_VERBOSE) F_println("adding seccomp ruleset \"" #ruleset "\""); \
|
if (opts & F_VERBOSE) F_println("adding seccomp ruleset \"" #ruleset "\""); \
|
||||||
for (int i = 0; i < LEN(ruleset); i++) { \
|
for (int i = 0; i < LEN(ruleset); i++) { \
|
||||||
assert(ruleset[i].m_errno == EPERM || ruleset[i].m_errno == ENOSYS); \
|
assert(ruleset[i].m_errno == EPERM || ruleset[i].m_errno == ENOSYS); \
|
||||||
\
|
\
|
||||||
if (ruleset[i].arg) \
|
if (ruleset[i].arg) \
|
||||||
ret = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ruleset[i].m_errno), ruleset[i].syscall, 1, *ruleset[i].arg); \
|
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ruleset[i].m_errno), ruleset[i].syscall, 1, *ruleset[i].arg); \
|
||||||
else \
|
else \
|
||||||
ret = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ruleset[i].m_errno), ruleset[i].syscall, 0); \
|
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ruleset[i].m_errno), ruleset[i].syscall, 0); \
|
||||||
\
|
\
|
||||||
if (ret == -EFAULT) { \
|
if (*ret_p == -EFAULT) { \
|
||||||
res = 4; \
|
res = 4; \
|
||||||
goto out; \
|
goto out; \
|
||||||
} else if (ret < 0) { \
|
} else if (*ret_p < 0) { \
|
||||||
res = 5; \
|
res = 5; \
|
||||||
errno = -ret; \
|
goto out; \
|
||||||
goto out; \
|
} \
|
||||||
} \
|
} \
|
||||||
} \
|
|
||||||
} while (0)
|
} while (0)
|
||||||
|
|
||||||
int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts) {
|
int32_t f_build_filter(int *ret_p, 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;
|
||||||
int allowed_personality = PER_LINUX;
|
int allowed_personality = PER_LINUX;
|
||||||
@ -229,8 +228,6 @@ int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts o
|
|||||||
} else
|
} else
|
||||||
errno = 0;
|
errno = 0;
|
||||||
|
|
||||||
int ret;
|
|
||||||
|
|
||||||
// We only really need to handle arches on multiarch systems.
|
// We only really need to handle arches on multiarch systems.
|
||||||
// If only one arch is supported the default is fine
|
// If only one arch is supported the default is fine
|
||||||
if (arch != 0) {
|
if (arch != 0) {
|
||||||
@ -239,18 +236,16 @@ int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts o
|
|||||||
// allow the target arch, but we can't really disallow the
|
// allow the target arch, but we can't really disallow the
|
||||||
// native arch at this point, because then bubblewrap
|
// native arch at this point, because then bubblewrap
|
||||||
// couldn't continue running.
|
// couldn't continue running.
|
||||||
ret = seccomp_arch_add(ctx, arch);
|
*ret_p = seccomp_arch_add(ctx, arch);
|
||||||
if (ret < 0 && ret != -EEXIST) {
|
if (*ret_p < 0 && *ret_p != -EEXIST) {
|
||||||
res = 2;
|
res = 2;
|
||||||
errno = -ret;
|
|
||||||
goto out;
|
goto out;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allow_multiarch && multiarch != 0) {
|
if (allow_multiarch && multiarch != 0) {
|
||||||
ret = seccomp_arch_add(ctx, multiarch);
|
*ret_p = seccomp_arch_add(ctx, multiarch);
|
||||||
if (ret < 0 && ret != -EEXIST) {
|
if (*ret_p < 0 && *ret_p != -EEXIST) {
|
||||||
res = 3;
|
res = 3;
|
||||||
errno = -ret;
|
|
||||||
goto out;
|
goto out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -285,11 +280,18 @@ int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts o
|
|||||||
// Blocklist the rest
|
// Blocklist the rest
|
||||||
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1));
|
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1));
|
||||||
|
|
||||||
ret = seccomp_export_bpf(ctx, fd);
|
if (fd < 0) {
|
||||||
if (ret != 0) {
|
*ret_p = seccomp_load(ctx);
|
||||||
res = 6;
|
if (*ret_p != 0) {
|
||||||
errno = -ret;
|
res = 7;
|
||||||
goto out;
|
goto out;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
*ret_p = seccomp_export_bpf(ctx, fd);
|
||||||
|
if (*ret_p != 0) {
|
||||||
|
res = 6;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
out:
|
out:
|
@ -20,4 +20,4 @@ typedef enum {
|
|||||||
} f_syscall_opts;
|
} f_syscall_opts;
|
||||||
|
|
||||||
extern void F_println(char *v);
|
extern void F_println(char *v);
|
||||||
int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts);
|
int32_t f_build_filter(int *ret_p, int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts);
|
@ -3,25 +3,56 @@ package seccomp
|
|||||||
/*
|
/*
|
||||||
#cgo linux pkg-config: --static libseccomp
|
#cgo linux pkg-config: --static libseccomp
|
||||||
|
|
||||||
#include "seccomp-export.h"
|
#include "seccomp-build.h"
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
var CPrintln func(v ...any)
|
// LibraryError represents a libseccomp error.
|
||||||
|
type LibraryError struct {
|
||||||
|
Prefix string
|
||||||
|
Seccomp syscall.Errno
|
||||||
|
Errno error
|
||||||
|
}
|
||||||
|
|
||||||
var resErr = [...]error{
|
func (e *LibraryError) Error() string {
|
||||||
0: nil,
|
if e.Seccomp == 0 {
|
||||||
1: errors.New("seccomp_init failed"),
|
if e.Errno == nil {
|
||||||
2: errors.New("seccomp_arch_add failed"),
|
panic("invalid libseccomp error")
|
||||||
3: errors.New("seccomp_arch_add failed (multiarch)"),
|
}
|
||||||
4: errors.New("internal libseccomp failure"),
|
return fmt.Sprintf("%s: %s", e.Prefix, e.Errno)
|
||||||
5: errors.New("seccomp_rule_add failed"),
|
}
|
||||||
6: errors.New("seccomp_export_bpf failed"),
|
if e.Errno == nil {
|
||||||
|
return fmt.Sprintf("%s: %s", e.Prefix, e.Seccomp)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s: %s (%s)", e.Prefix, e.Seccomp, e.Errno)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LibraryError) Is(err error) bool {
|
||||||
|
if e == nil {
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
if ef, ok := err.(*LibraryError); ok {
|
||||||
|
return *e == *ef
|
||||||
|
}
|
||||||
|
return (e.Seccomp != 0 && errors.Is(err, e.Seccomp)) ||
|
||||||
|
(e.Errno != nil && errors.Is(err, e.Errno))
|
||||||
|
}
|
||||||
|
|
||||||
|
var resPrefix = [...]string{
|
||||||
|
0: "",
|
||||||
|
1: "seccomp_init failed",
|
||||||
|
2: "seccomp_arch_add failed",
|
||||||
|
3: "seccomp_arch_add failed (multiarch)",
|
||||||
|
4: "internal libseccomp failure",
|
||||||
|
5: "seccomp_rule_add failed",
|
||||||
|
6: "seccomp_export_bpf failed",
|
||||||
|
7: "seccomp_load failed",
|
||||||
}
|
}
|
||||||
|
|
||||||
type SyscallOpts = C.f_syscall_opts
|
type SyscallOpts = C.f_syscall_opts
|
||||||
@ -46,7 +77,7 @@ const (
|
|||||||
FlagBluetooth SyscallOpts = C.F_BLUETOOTH
|
FlagBluetooth SyscallOpts = C.F_BLUETOOTH
|
||||||
)
|
)
|
||||||
|
|
||||||
func exportFilter(fd uintptr, opts SyscallOpts) error {
|
func buildFilter(fd int, opts SyscallOpts) error {
|
||||||
var (
|
var (
|
||||||
arch C.uint32_t = 0
|
arch C.uint32_t = 0
|
||||||
multiarch C.uint32_t = 0
|
multiarch C.uint32_t = 0
|
||||||
@ -66,23 +97,18 @@ func exportFilter(fd uintptr, opts SyscallOpts) error {
|
|||||||
|
|
||||||
// this removes repeated transitions between C and Go execution
|
// this removes repeated transitions between C and Go execution
|
||||||
// when producing log output via F_println and CPrintln is nil
|
// when producing log output via F_println and CPrintln is nil
|
||||||
if CPrintln != nil {
|
if fp := printlnP.Load(); fp != nil {
|
||||||
opts |= flagVerbose
|
opts |= flagVerbose
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := C.f_export_bpf(C.int(fd), arch, multiarch, opts)
|
var ret C.int
|
||||||
if re := resErr[res]; re != nil {
|
res, err := C.f_build_filter(&ret, C.int(fd), arch, multiarch, opts)
|
||||||
if err == nil {
|
if prefix := resPrefix[res]; prefix != "" {
|
||||||
return re
|
return &LibraryError{
|
||||||
|
prefix,
|
||||||
|
-syscall.Errno(ret),
|
||||||
|
err,
|
||||||
}
|
}
|
||||||
return fmt.Errorf("%s: %v", re.Error(), err)
|
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
//export F_println
|
|
||||||
func F_println(v *C.char) {
|
|
||||||
if CPrintln != nil {
|
|
||||||
CPrintln(C.GoString(v))
|
|
||||||
}
|
|
||||||
}
|
|
65
sandbox/seccomp/seccomp_test.go
Normal file
65
sandbox/seccomp/seccomp_test.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package seccomp_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLibraryError(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
sample *seccomp.LibraryError
|
||||||
|
want string
|
||||||
|
wantIs bool
|
||||||
|
compare error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"full",
|
||||||
|
&seccomp.LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF},
|
||||||
|
"seccomp_export_bpf failed: operation canceled (bad file descriptor)",
|
||||||
|
true,
|
||||||
|
&seccomp.LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"errno only",
|
||||||
|
&seccomp.LibraryError{Prefix: "seccomp_init failed", Errno: syscall.ENOMEM},
|
||||||
|
"seccomp_init failed: cannot allocate memory",
|
||||||
|
false,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"seccomp only",
|
||||||
|
&seccomp.LibraryError{Prefix: "internal libseccomp failure", Seccomp: syscall.EFAULT},
|
||||||
|
"internal libseccomp failure: bad address",
|
||||||
|
true,
|
||||||
|
syscall.EFAULT,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if errors.Is(tc.sample, tc.compare) != tc.wantIs {
|
||||||
|
t.Errorf("errors.Is(%#v, %#v) did not return %v",
|
||||||
|
tc.sample, tc.compare, tc.wantIs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := tc.sample.Error(); got != tc.want {
|
||||||
|
t.Errorf("Error: %q, want %q",
|
||||||
|
got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("invalid", func(t *testing.T) {
|
||||||
|
wantPanic := "invalid libseccomp error"
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != wantPanic {
|
||||||
|
t.Errorf("panic: %q, want %q", r, wantPanic)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
runtime.KeepAlive(new(seccomp.LibraryError).Error())
|
||||||
|
})
|
||||||
|
}
|
180
sandbox/sequential.go
Normal file
180
sandbox/sequential.go
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
package sandbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() { gob.Register(new(BindMount)) }
|
||||||
|
|
||||||
|
// BindMount bind mounts host path Source on container path Target.
|
||||||
|
type BindMount struct {
|
||||||
|
Source, Target string
|
||||||
|
|
||||||
|
Flags int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BindMount) apply(*InitParams) error {
|
||||||
|
if !path.IsAbs(b.Source) || !path.IsAbs(b.Target) {
|
||||||
|
return msg.WrapErr(syscall.EBADE,
|
||||||
|
"path is not absolute")
|
||||||
|
}
|
||||||
|
return bindMount(b.Source, b.Target, b.Flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BindMount) Is(op Op) bool { vb, ok := op.(*BindMount); return ok && *b == *vb }
|
||||||
|
func (b *BindMount) String() string {
|
||||||
|
if b.Source == b.Target {
|
||||||
|
return fmt.Sprintf("%q flags %#x", b.Source, b.Flags)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags&BindWritable)
|
||||||
|
}
|
||||||
|
func (f *Ops) Bind(source, target string, flags int) *Ops {
|
||||||
|
*f = append(*f, &BindMount{source, target, flags | BindRecursive})
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { gob.Register(new(MountProc)) }
|
||||||
|
|
||||||
|
// MountProc mounts a private proc instance on container Path.
|
||||||
|
type MountProc struct {
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MountProc) apply(*InitParams) error {
|
||||||
|
if !path.IsAbs(p.Path) {
|
||||||
|
return msg.WrapErr(syscall.EBADE,
|
||||||
|
fmt.Sprintf("path %q is not absolute", p.Path))
|
||||||
|
}
|
||||||
|
|
||||||
|
target := toSysroot(p.Path)
|
||||||
|
if err := os.MkdirAll(target, 0755); err != nil {
|
||||||
|
return msg.WrapErr(err, err.Error())
|
||||||
|
}
|
||||||
|
return wrapErrSuffix(syscall.Mount("proc", target, "proc",
|
||||||
|
syscall.MS_NOSUID|syscall.MS_NOEXEC|syscall.MS_NODEV, ""),
|
||||||
|
fmt.Sprintf("cannot mount proc on %q:", p.Path))
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { gob.Register(new(MountDev)) }
|
||||||
|
|
||||||
|
// MountDev mounts dev on container Path.
|
||||||
|
type MountDev struct {
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MountDev) apply(params *InitParams) error {
|
||||||
|
if !path.IsAbs(d.Path) {
|
||||||
|
return msg.WrapErr(syscall.EBADE,
|
||||||
|
fmt.Sprintf("path %q is not absolute", d.Path))
|
||||||
|
}
|
||||||
|
target := toSysroot(d.Path)
|
||||||
|
|
||||||
|
if err := mountTmpfs("devtmpfs", d.Path, 0, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
|
||||||
|
if err := bindMount(
|
||||||
|
"/dev/"+name, path.Join(d.Path, name),
|
||||||
|
BindSource|BindDevices,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, name := range []string{"stdin", "stdout", "stderr"} {
|
||||||
|
if err := os.Symlink(
|
||||||
|
"/proc/self/fd/"+string(rune(i+'0')),
|
||||||
|
path.Join(target, name),
|
||||||
|
); err != nil {
|
||||||
|
return msg.WrapErr(err, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, pair := range [][2]string{
|
||||||
|
{"/proc/self/fd", "fd"},
|
||||||
|
{"/proc/kcore", "core"},
|
||||||
|
{"pts/ptmx", "ptmx"},
|
||||||
|
} {
|
||||||
|
if err := os.Symlink(pair[0], path.Join(target, pair[1])); err != nil {
|
||||||
|
return msg.WrapErr(err, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
devPtsPath := path.Join(target, "pts")
|
||||||
|
for _, name := range []string{path.Join(target, "shm"), devPtsPath} {
|
||||||
|
if err := os.Mkdir(name, 0755); err != nil {
|
||||||
|
return msg.WrapErr(err, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := syscall.Mount("devpts", devPtsPath, "devpts",
|
||||||
|
syscall.MS_NOSUID|syscall.MS_NOEXEC,
|
||||||
|
"newinstance,ptmxmode=0666,mode=620"); err != nil {
|
||||||
|
return wrapErrSuffix(err,
|
||||||
|
fmt.Sprintf("cannot mount devpts on %q:", devPtsPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Flags&FAllowTTY != 0 {
|
||||||
|
var buf [8]byte
|
||||||
|
if _, _, errno := syscall.Syscall(
|
||||||
|
syscall.SYS_IOCTL, 1, syscall.TIOCGWINSZ,
|
||||||
|
uintptr(unsafe.Pointer(&buf[0])),
|
||||||
|
); errno == 0 {
|
||||||
|
if err := bindMount(
|
||||||
|
"/proc/self/fd/1", path.Join(d.Path, "console"),
|
||||||
|
BindDevices,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MountDev) Is(op Op) bool { vd, ok := op.(*MountDev); return ok && *d == *vd }
|
||||||
|
func (d *MountDev) String() string { return fmt.Sprintf("dev on %q", d.Path) }
|
||||||
|
func (f *Ops) Dev(dest string) *Ops {
|
||||||
|
*f = append(*f, &MountDev{dest})
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MountProc) Is(op Op) bool { vp, ok := op.(*MountProc); return ok && *p == *vp }
|
||||||
|
func (p *MountProc) String() string { return fmt.Sprintf("proc on %q", p.Path) }
|
||||||
|
func (f *Ops) Proc(dest string) *Ops {
|
||||||
|
*f = append(*f, &MountProc{dest})
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { gob.Register(new(MountTmpfs)) }
|
||||||
|
|
||||||
|
// MountTmpfs mounts tmpfs on container Path.
|
||||||
|
type MountTmpfs struct {
|
||||||
|
Path string
|
||||||
|
Size int
|
||||||
|
Perm os.FileMode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *MountTmpfs) apply(*InitParams) error {
|
||||||
|
if !path.IsAbs(t.Path) {
|
||||||
|
return msg.WrapErr(syscall.EBADE,
|
||||||
|
fmt.Sprintf("path %q is not absolute", t.Path))
|
||||||
|
}
|
||||||
|
if t.Size < 0 || t.Size > math.MaxUint>>1 {
|
||||||
|
return msg.WrapErr(syscall.EBADE,
|
||||||
|
fmt.Sprintf("size %d out of bounds", t.Size))
|
||||||
|
}
|
||||||
|
return mountTmpfs("tmpfs", t.Path, t.Size, t.Perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *MountTmpfs) Is(op Op) bool { vt, ok := op.(*MountTmpfs); return ok && *t == *vt }
|
||||||
|
func (t *MountTmpfs) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
|
||||||
|
func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops {
|
||||||
|
*f = append(*f, &MountTmpfs{dest, size, perm})
|
||||||
|
return f
|
||||||
|
}
|
41
sandbox/syscall.go
Normal file
41
sandbox/syscall.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package sandbox
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
const (
|
||||||
|
SUID_DUMP_DISABLE = iota
|
||||||
|
SUID_DUMP_USER
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetDumpable(dumpable uintptr) error {
|
||||||
|
// linux/sched/coredump.h
|
||||||
|
if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, dumpable, 0); errno != 0 {
|
||||||
|
return errno
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetPdeathsig(sig syscall.Signal) error {
|
||||||
|
if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(sig), 0); errno != 0 {
|
||||||
|
return errno
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IgnoringEINTR makes a function call and repeats it if it returns an
|
||||||
|
// EINTR error. This appears to be required even though we install all
|
||||||
|
// signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846.
|
||||||
|
// Also #20400 and #36644 are issues in which a signal handler is
|
||||||
|
// installed without setting SA_RESTART. None of these are the common case,
|
||||||
|
// but there are enough of them that it seems that we can't avoid
|
||||||
|
// an EINTR loop.
|
||||||
|
func IgnoringEINTR(fn func() error) error {
|
||||||
|
for {
|
||||||
|
err := fn()
|
||||||
|
if err != syscall.EINTR {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -35,24 +35,24 @@ type ACL struct {
|
|||||||
func (a *ACL) Type() Enablement { return a.et }
|
func (a *ACL) Type() Enablement { return a.et }
|
||||||
|
|
||||||
func (a *ACL) apply(sys *I) error {
|
func (a *ACL) apply(sys *I) error {
|
||||||
sys.println("applying ACL", a)
|
msg.Verbose("applying ACL", a)
|
||||||
return sys.wrapErrSuffix(acl.Update(a.path, sys.uid, a.perms...),
|
return wrapErrSuffix(acl.Update(a.path, sys.uid, a.perms...),
|
||||||
fmt.Sprintf("cannot apply ACL entry to %q:", a.path))
|
fmt.Sprintf("cannot apply ACL entry to %q:", a.path))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ACL) revert(sys *I, ec *Criteria) error {
|
func (a *ACL) revert(sys *I, ec *Criteria) error {
|
||||||
if ec.hasType(a) {
|
if ec.hasType(a) {
|
||||||
sys.println("stripping ACL", a)
|
msg.Verbose("stripping ACL", a)
|
||||||
err := acl.Update(a.path, sys.uid)
|
err := acl.Update(a.path, sys.uid)
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
// the ACL is effectively stripped if the file no longer exists
|
// the ACL is effectively stripped if the file no longer exists
|
||||||
sys.printf("target of ACL %s no longer exists", a)
|
msg.Verbosef("target of ACL %s no longer exists", a)
|
||||||
err = nil
|
err = nil
|
||||||
}
|
}
|
||||||
return sys.wrapErrSuffix(err,
|
return wrapErrSuffix(err,
|
||||||
fmt.Sprintf("cannot strip ACL entry from %q:", a.path))
|
fmt.Sprintf("cannot strip ACL entry from %q:", a.path))
|
||||||
} else {
|
} else {
|
||||||
sys.println("skipping ACL", a)
|
msg.Verbose("skipping ACL", a)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st
|
|||||||
|
|
||||||
// session bus is mandatory
|
// session bus is mandatory
|
||||||
if session == nil {
|
if session == nil {
|
||||||
return nil, sys.wrapErr(ErrDBusConfig,
|
return nil, msg.WrapErr(ErrDBusConfig,
|
||||||
"attempted to seal message bus proxy without session bus config")
|
"attempted to seal message bus proxy without session bus config")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,12 +48,12 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st
|
|||||||
d.proxy = dbus.New(sessionBus, systemBus)
|
d.proxy = dbus.New(sessionBus, systemBus)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if sys.IsVerbose() && d.proxy.Sealed() {
|
if msg.IsVerbose() && d.proxy.Sealed() {
|
||||||
sys.println("sealed session proxy", session.Args(sessionBus))
|
msg.Verbose("sealed session proxy", session.Args(sessionBus))
|
||||||
if system != nil {
|
if system != nil {
|
||||||
sys.println("sealed system proxy", system.Args(systemBus))
|
msg.Verbose("sealed system proxy", system.Args(systemBus))
|
||||||
}
|
}
|
||||||
sys.println("message bus proxy final args:", d.proxy)
|
msg.Verbose("message bus proxy final args:", d.proxy)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -62,7 +62,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.Dump, sys.wrapErrSuffix(d.proxy.Seal(session, system),
|
return d.out.Dump, wrapErrSuffix(d.proxy.Seal(session, system),
|
||||||
"cannot seal message bus proxy:")
|
"cannot seal message bus proxy:")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,32 +77,32 @@ type DBus struct {
|
|||||||
func (d *DBus) Type() Enablement { return Process }
|
func (d *DBus) Type() Enablement { return Process }
|
||||||
|
|
||||||
func (d *DBus) apply(sys *I) error {
|
func (d *DBus) apply(sys *I) error {
|
||||||
sys.printf("session bus proxy on %q for upstream %q", d.proxy.Session()[1], d.proxy.Session()[0])
|
msg.Verbosef("session bus proxy on %q for upstream %q", d.proxy.Session()[1], d.proxy.Session()[0])
|
||||||
if d.system {
|
if d.system {
|
||||||
sys.printf("system bus proxy on %q for upstream %q", d.proxy.System()[1], d.proxy.System()[0])
|
msg.Verbosef("system bus proxy on %q for upstream %q", d.proxy.System()[1], d.proxy.System()[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// this starts the process and blocks until ready
|
// this starts the process and blocks until ready
|
||||||
if err := d.proxy.Start(sys.ctx, d.out, true); err != nil {
|
if err := d.proxy.Start(sys.ctx, d.out, true); err != nil {
|
||||||
d.out.Dump()
|
d.out.Dump()
|
||||||
return sys.wrapErrSuffix(err,
|
return wrapErrSuffix(err,
|
||||||
"cannot start message bus proxy:")
|
"cannot start message bus proxy:")
|
||||||
}
|
}
|
||||||
sys.println("starting message bus proxy:", d.proxy)
|
msg.Verbose("starting message bus proxy", d.proxy)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DBus) revert(sys *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
|
||||||
sys.println("terminating message bus proxy")
|
msg.Verbose("terminating message bus proxy")
|
||||||
d.proxy.Close()
|
d.proxy.Close()
|
||||||
defer sys.println("message bus proxy exit")
|
defer msg.Verbose("message bus proxy exit")
|
||||||
err := d.proxy.Wait()
|
err := d.proxy.Wait()
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
sys.println("message bus proxy canceled upstream")
|
msg.Verbose("message bus proxy canceled upstream")
|
||||||
err = nil
|
err = nil
|
||||||
}
|
}
|
||||||
return sys.wrapErrSuffix(err, "message bus proxy error:")
|
return wrapErrSuffix(err, "message bus proxy error:")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DBus) Is(o Op) bool {
|
func (d *DBus) Is(o Op) bool {
|
||||||
@ -139,7 +139,15 @@ func (s *scanToFmsg) write(p []byte, a int) (int, error) {
|
|||||||
return a + n, nil
|
return a + n, nil
|
||||||
} else {
|
} else {
|
||||||
n, _ := s.msg.Write(p[:i])
|
n, _ := s.msg.Write(p[:i])
|
||||||
s.msgbuf = append(s.msgbuf, s.msg.String())
|
|
||||||
|
// allow container init messages through
|
||||||
|
v := s.msg.String()
|
||||||
|
if strings.HasPrefix(v, "init: ") {
|
||||||
|
log.Println("(dbus) " + v)
|
||||||
|
} else {
|
||||||
|
s.msgbuf = append(s.msgbuf, v)
|
||||||
|
}
|
||||||
|
|
||||||
s.msg.Reset()
|
s.msg.Reset()
|
||||||
return s.write(p[i+1:], a+n+1)
|
return s.write(p[i+1:], a+n+1)
|
||||||
}
|
}
|
||||||
@ -148,7 +156,7 @@ func (s *scanToFmsg) write(p []byte, a int) (int, error) {
|
|||||||
func (s *scanToFmsg) Dump() {
|
func (s *scanToFmsg) Dump() {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
for _, msg := range s.msgbuf {
|
for _, msg := range s.msgbuf {
|
||||||
log.Println(msg)
|
log.Println("(dbus) " + msg)
|
||||||
}
|
}
|
||||||
s.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
}
|
}
|
||||||
|
@ -25,19 +25,19 @@ type Hardlink struct {
|
|||||||
|
|
||||||
func (l *Hardlink) Type() Enablement { return l.et }
|
func (l *Hardlink) Type() Enablement { return l.et }
|
||||||
|
|
||||||
func (l *Hardlink) apply(sys *I) error {
|
func (l *Hardlink) apply(*I) error {
|
||||||
sys.println("linking", l)
|
msg.Verbose("linking", l)
|
||||||
return sys.wrapErrSuffix(os.Link(l.src, l.dst),
|
return wrapErrSuffix(os.Link(l.src, l.dst),
|
||||||
fmt.Sprintf("cannot link %q:", l.dst))
|
fmt.Sprintf("cannot link %q:", l.dst))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Hardlink) revert(sys *I, ec *Criteria) error {
|
func (l *Hardlink) revert(_ *I, ec *Criteria) error {
|
||||||
if ec.hasType(l) {
|
if ec.hasType(l) {
|
||||||
sys.printf("removing hard link %q", l.dst)
|
msg.Verbosef("removing hard link %q", l.dst)
|
||||||
return sys.wrapErrSuffix(os.Remove(l.dst),
|
return wrapErrSuffix(os.Remove(l.dst),
|
||||||
fmt.Sprintf("cannot remove hard link %q:", l.dst))
|
fmt.Sprintf("cannot remove hard link %q:", l.dst))
|
||||||
} else {
|
} else {
|
||||||
sys.printf("skipping hard link %q", l.dst)
|
msg.Verbosef("skipping hard link %q", l.dst)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,33 +37,33 @@ func (m *Mkdir) Type() Enablement {
|
|||||||
return m.et
|
return m.et
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Mkdir) apply(sys *I) error {
|
func (m *Mkdir) apply(*I) error {
|
||||||
sys.println("ensuring directory", m)
|
msg.Verbose("ensuring directory", m)
|
||||||
|
|
||||||
// create directory
|
// create directory
|
||||||
err := os.Mkdir(m.path, m.perm)
|
err := os.Mkdir(m.path, m.perm)
|
||||||
if !errors.Is(err, os.ErrExist) {
|
if !errors.Is(err, os.ErrExist) {
|
||||||
return sys.wrapErrSuffix(err,
|
return wrapErrSuffix(err,
|
||||||
fmt.Sprintf("cannot create directory %q:", m.path))
|
fmt.Sprintf("cannot create directory %q:", m.path))
|
||||||
}
|
}
|
||||||
|
|
||||||
// directory exists, ensure mode
|
// directory exists, ensure mode
|
||||||
return sys.wrapErrSuffix(os.Chmod(m.path, m.perm),
|
return wrapErrSuffix(os.Chmod(m.path, m.perm),
|
||||||
fmt.Sprintf("cannot change mode of %q to %s:", m.path, m.perm))
|
fmt.Sprintf("cannot change mode of %q to %s:", m.path, m.perm))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Mkdir) revert(sys *I, ec *Criteria) error {
|
func (m *Mkdir) revert(_ *I, ec *Criteria) error {
|
||||||
if !m.ephemeral {
|
if !m.ephemeral {
|
||||||
// skip non-ephemeral dir and do not log anything
|
// skip non-ephemeral dir and do not log anything
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if ec.hasType(m) {
|
if ec.hasType(m) {
|
||||||
sys.println("destroying ephemeral directory", m)
|
msg.Verbose("destroying ephemeral directory", m)
|
||||||
return sys.wrapErrSuffix(os.Remove(m.path),
|
return wrapErrSuffix(os.Remove(m.path),
|
||||||
fmt.Sprintf("cannot remove ephemeral directory %q:", m.path))
|
fmt.Sprintf("cannot remove ephemeral directory %q:", m.path))
|
||||||
} else {
|
} else {
|
||||||
sys.println("skipping ephemeral directory", m)
|
msg.Verbose("skipping ephemeral directory", m)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
22
system/op.go
22
system/op.go
@ -60,10 +60,6 @@ func TypeString(e Enablement) string {
|
|||||||
func New(uid int) (sys *I) {
|
func New(uid int) (sys *I) {
|
||||||
sys = new(I)
|
sys = new(I)
|
||||||
sys.uid = uid
|
sys.uid = uid
|
||||||
sys.IsVerbose = func() bool { return false }
|
|
||||||
sys.Verbose = func(...any) {}
|
|
||||||
sys.Verbosef = func(string, ...any) {}
|
|
||||||
sys.WrapErr = func(err error, _ ...any) error { return err }
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,27 +69,13 @@ type I struct {
|
|||||||
ops []Op
|
ops []Op
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
|
||||||
IsVerbose func() bool
|
|
||||||
Verbose func(v ...any)
|
|
||||||
Verbosef func(format string, v ...any)
|
|
||||||
WrapErr func(err error, a ...any) error
|
|
||||||
|
|
||||||
// whether sys has been reverted
|
// whether sys has been reverted
|
||||||
state bool
|
state bool
|
||||||
|
|
||||||
lock sync.Mutex
|
lock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sys *I) UID() int { return sys.uid }
|
func (sys *I) UID() int { return sys.uid }
|
||||||
func (sys *I) println(v ...any) { sys.Verbose(v...) }
|
|
||||||
func (sys *I) printf(format string, v ...any) { sys.Verbosef(format, v...) }
|
|
||||||
func (sys *I) wrapErr(err error, a ...any) error { return sys.WrapErr(err, a...) }
|
|
||||||
func (sys *I) wrapErrSuffix(err error, a ...any) error {
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return sys.wrapErr(err, append(a, err)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Equal returns whether all [Op] instances held by v is identical to that of sys.
|
// Equal returns whether all [Op] instances held by v is identical to that of sys.
|
||||||
func (sys *I) Equal(v *I) bool {
|
func (sys *I) Equal(v *I) bool {
|
||||||
@ -127,7 +109,7 @@ func (sys *I) Commit(ctx context.Context) error {
|
|||||||
// sp is set to nil when all ops are applied
|
// sp is set to nil when all ops are applied
|
||||||
if sp != nil {
|
if sp != nil {
|
||||||
// rollback partial commit
|
// rollback partial commit
|
||||||
sys.printf("commit faulted after %d ops, rolling back partial commit", len(sp.ops))
|
msg.Verbosef("commit faulted after %d ops, rolling back partial commit", len(sp.ops))
|
||||||
if err := sp.Revert(&Criteria{nil}); err != nil {
|
if err := sp.Revert(&Criteria{nil}); err != nil {
|
||||||
log.Println("errors returned reverting partial commit:", err)
|
log.Println("errors returned reverting partial commit:", err)
|
||||||
}
|
}
|
||||||
|
20
system/output.go
Normal file
20
system/output.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import "git.gensokyo.uk/security/fortify/sandbox"
|
||||||
|
|
||||||
|
var msg sandbox.Msg = new(sandbox.DefaultMsg)
|
||||||
|
|
||||||
|
func SetOutput(v sandbox.Msg) {
|
||||||
|
if v == nil {
|
||||||
|
msg = new(sandbox.DefaultMsg)
|
||||||
|
} else {
|
||||||
|
msg = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapErrSuffix(err error, a ...any) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return msg.WrapErr(err, append(a, err)...)
|
||||||
|
}
|
@ -31,8 +31,8 @@ type Tmpfile struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tmpfile) Type() Enablement { return Process }
|
func (t *Tmpfile) Type() Enablement { return Process }
|
||||||
func (t *Tmpfile) apply(sys *I) error {
|
func (t *Tmpfile) apply(*I) error {
|
||||||
sys.println("copying", t)
|
msg.Verbose("copying", t)
|
||||||
|
|
||||||
if t.payload == nil {
|
if t.payload == nil {
|
||||||
// this is a misuse of the API; do not return an error message
|
// this is a misuse of the API; do not return an error message
|
||||||
@ -40,25 +40,25 @@ func (t *Tmpfile) apply(sys *I) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if b, err := os.Stat(t.src); err != nil {
|
if b, err := os.Stat(t.src); err != nil {
|
||||||
return sys.wrapErrSuffix(err,
|
return wrapErrSuffix(err,
|
||||||
fmt.Sprintf("cannot stat %q:", t.src))
|
fmt.Sprintf("cannot stat %q:", t.src))
|
||||||
} else {
|
} else {
|
||||||
if b.IsDir() {
|
if b.IsDir() {
|
||||||
return sys.wrapErrSuffix(syscall.EISDIR,
|
return wrapErrSuffix(syscall.EISDIR,
|
||||||
fmt.Sprintf("%q is a directory", t.src))
|
fmt.Sprintf("%q is a directory", t.src))
|
||||||
}
|
}
|
||||||
if s := b.Size(); s > t.n {
|
if s := b.Size(); s > t.n {
|
||||||
return sys.wrapErrSuffix(syscall.ENOMEM,
|
return wrapErrSuffix(syscall.ENOMEM,
|
||||||
fmt.Sprintf("file %q is too long: %d > %d",
|
fmt.Sprintf("file %q is too long: %d > %d",
|
||||||
t.src, s, t.n))
|
t.src, s, t.n))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if f, err := os.Open(t.src); err != nil {
|
if f, err := os.Open(t.src); err != nil {
|
||||||
return sys.wrapErrSuffix(err,
|
return wrapErrSuffix(err,
|
||||||
fmt.Sprintf("cannot open %q:", t.src))
|
fmt.Sprintf("cannot open %q:", t.src))
|
||||||
} else if _, err = io.CopyN(t.buf, f, t.n); err != nil {
|
} else if _, err = io.CopyN(t.buf, f, t.n); err != nil {
|
||||||
return sys.wrapErrSuffix(err,
|
return wrapErrSuffix(err,
|
||||||
fmt.Sprintf("cannot read from %q:", t.src))
|
fmt.Sprintf("cannot read from %q:", t.src))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,35 +46,35 @@ func (w *Wayland) apply(sys *I) error {
|
|||||||
if errors.Is(err, os.ErrNotExist) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
err = os.ErrNotExist
|
err = os.ErrNotExist
|
||||||
}
|
}
|
||||||
return sys.wrapErrSuffix(err,
|
return wrapErrSuffix(err,
|
||||||
fmt.Sprintf("cannot attach to wayland on %q:", w.src))
|
fmt.Sprintf("cannot attach to wayland on %q:", w.src))
|
||||||
} else {
|
} else {
|
||||||
sys.printf("wayland attached on %q", w.src)
|
msg.Verbosef("wayland attached on %q", w.src)
|
||||||
}
|
}
|
||||||
|
|
||||||
if sp, err := w.conn.Bind(w.dst, w.appID, w.instanceID); err != nil {
|
if sp, err := w.conn.Bind(w.dst, w.appID, w.instanceID); err != nil {
|
||||||
return sys.wrapErrSuffix(err,
|
return wrapErrSuffix(err,
|
||||||
fmt.Sprintf("cannot bind to socket on %q:", w.dst))
|
fmt.Sprintf("cannot bind to socket on %q:", w.dst))
|
||||||
} else {
|
} else {
|
||||||
*w.sync = sp
|
*w.sync = sp
|
||||||
sys.printf("wayland listening on %q", w.dst)
|
msg.Verbosef("wayland listening on %q", w.dst)
|
||||||
return sys.wrapErrSuffix(errors.Join(os.Chmod(w.dst, 0), acl.Update(w.dst, sys.uid, acl.Read, acl.Write, acl.Execute)),
|
return wrapErrSuffix(errors.Join(os.Chmod(w.dst, 0), acl.Update(w.dst, sys.uid, acl.Read, acl.Write, acl.Execute)),
|
||||||
fmt.Sprintf("cannot chmod socket on %q:", w.dst))
|
fmt.Sprintf("cannot chmod socket on %q:", w.dst))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Wayland) revert(sys *I, ec *Criteria) error {
|
func (w *Wayland) revert(_ *I, ec *Criteria) error {
|
||||||
if ec.hasType(w) {
|
if ec.hasType(w) {
|
||||||
sys.printf("removing wayland socket on %q", w.dst)
|
msg.Verbosef("removing wayland socket on %q", w.dst)
|
||||||
if err := os.Remove(w.dst); err != nil && !errors.Is(err, os.ErrNotExist) {
|
if err := os.Remove(w.dst); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
sys.printf("detaching from wayland on %q", w.src)
|
msg.Verbosef("detaching from wayland on %q", w.src)
|
||||||
return sys.wrapErrSuffix(w.conn.Close(),
|
return wrapErrSuffix(w.conn.Close(),
|
||||||
fmt.Sprintf("cannot detach from wayland on %q:", w.src))
|
fmt.Sprintf("cannot detach from wayland on %q:", w.src))
|
||||||
} else {
|
} else {
|
||||||
sys.printf("skipping wayland cleanup on %q", w.dst)
|
msg.Verbosef("skipping wayland cleanup on %q", w.dst)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,19 +22,19 @@ func (x XHost) Type() Enablement {
|
|||||||
return EX11
|
return EX11
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x XHost) apply(sys *I) error {
|
func (x XHost) apply(*I) error {
|
||||||
sys.printf("inserting entry %s to X11", x)
|
msg.Verbosef("inserting entry %s to X11", x)
|
||||||
return sys.wrapErrSuffix(xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)),
|
return wrapErrSuffix(xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)),
|
||||||
fmt.Sprintf("cannot insert entry %s to X11:", x))
|
fmt.Sprintf("cannot insert entry %s to X11:", x))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x XHost) revert(sys *I, ec *Criteria) error {
|
func (x XHost) revert(_ *I, ec *Criteria) error {
|
||||||
if ec.hasType(x) {
|
if ec.hasType(x) {
|
||||||
sys.printf("deleting entry %s from X11", x)
|
msg.Verbosef("deleting entry %s from X11", x)
|
||||||
return sys.wrapErrSuffix(xcb.ChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)),
|
return wrapErrSuffix(xcb.ChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)),
|
||||||
fmt.Sprintf("cannot delete entry %s from X11:", x))
|
fmt.Sprintf("cannot delete entry %s from X11:", x))
|
||||||
} else {
|
} else {
|
||||||
sys.printf("skipping entry %s in X11", x)
|
msg.Verbosef("skipping entry %s in X11", x)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,15 @@ nixosTest {
|
|||||||
{
|
{
|
||||||
environment.systemPackages = [
|
environment.systemPackages = [
|
||||||
# For go tests:
|
# For go tests:
|
||||||
self.packages.${system}.fhs
|
(writeShellScriptBin "fortify-go-test" ''
|
||||||
(writeShellScriptBin "fortify-src" "echo -n ${self.packages.${system}.fortify.src}")
|
set -e
|
||||||
|
WORK="$(mktemp -ud)"
|
||||||
|
cp -r "${self.packages.${system}.fortify.src}" "$WORK"
|
||||||
|
chmod -R +w "$WORK"
|
||||||
|
cd "$WORK"
|
||||||
|
${self.packages.${system}.fhs}/bin/fortify-fhs -c \
|
||||||
|
'go generate ./... && go test ${if withRace then "-race" else "-count 16"} ./... && touch /tmp/go-test-ok'
|
||||||
|
'')
|
||||||
];
|
];
|
||||||
|
|
||||||
# Run with Go race detector:
|
# Run with Go race detector:
|
||||||
|
@ -41,6 +41,11 @@ func MustAssertMounts(name, hostMountsFile, wantFile string) {
|
|||||||
if want[i].Opts == "host_passthrough" {
|
if want[i].Opts == "host_passthrough" {
|
||||||
for _, ent := range hostMounts {
|
for _, ent := range hostMounts {
|
||||||
if want[i].FSName == ent.FSName {
|
if want[i].FSName == ent.FSName {
|
||||||
|
// special case for tmpfs bind mounts
|
||||||
|
if want[i].FSName == "tmpfs" && want[i].Dir != ent.Dir {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
want[i].Opts = ent.Opts
|
want[i].Opts = ent.Opts
|
||||||
goto out
|
goto out
|
||||||
}
|
}
|
||||||
@ -55,7 +60,7 @@ func MustAssertMounts(name, hostMountsFile, wantFile string) {
|
|||||||
if i == len(want) {
|
if i == len(want) {
|
||||||
fatalf("got more than %d entries", i)
|
fatalf("got more than %d entries", i)
|
||||||
}
|
}
|
||||||
if *e != want[i] {
|
if !e.Is(&want[i]) {
|
||||||
fatalf("entry %d\n got: %s\nwant: %s", i,
|
fatalf("entry %d\n got: %s\nwant: %s", i,
|
||||||
e, &want[i])
|
e, &want[i])
|
||||||
}
|
}
|
||||||
@ -78,3 +83,9 @@ func MustAssertFS(e fs.FS, wantFile string) {
|
|||||||
fatalf("%v", err)
|
fatalf("%v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MustAssertSeccomp() {
|
||||||
|
if TrySyscalls() != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -8,6 +8,7 @@ writeShellScript "check-sandbox" ''
|
|||||||
set -e
|
set -e
|
||||||
${callPackage ./mount.nix { inherit version; }}/bin/test
|
${callPackage ./mount.nix { inherit version; }}/bin/test
|
||||||
${callPackage ./fs.nix { inherit version; }}/bin/test
|
${callPackage ./fs.nix { inherit version; }}/bin/test
|
||||||
|
${callPackage ./seccomp.nix { inherit version; }}/bin/test
|
||||||
|
|
||||||
touch /tmp/sandbox-ok
|
touch /tmp/sandbox-ok
|
||||||
''
|
''
|
||||||
|
@ -21,7 +21,7 @@ let
|
|||||||
etc = fs "800001ed" null null;
|
etc = fs "800001ed" null null;
|
||||||
sbin = fs "800001c0" {
|
sbin = fs "800001c0" {
|
||||||
fortify = fs "16d" null null;
|
fortify = fs "16d" null null;
|
||||||
init = fs "80001ff" null null;
|
init0 = fs "80001ff" null null;
|
||||||
} null;
|
} null;
|
||||||
host-mounts = fs "124" null null;
|
host-mounts = fs "124" null null;
|
||||||
} null;
|
} null;
|
||||||
|
@ -37,6 +37,18 @@ func (e *Mntent) String() string {
|
|||||||
e.FSName, e.Dir, e.Type, e.Opts, e.Freq, e.Passno)
|
e.FSName, e.Dir, e.Type, e.Opts, e.Freq, e.Passno)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Mntent) Is(want *Mntent) bool {
|
||||||
|
if want == nil {
|
||||||
|
return e == nil
|
||||||
|
}
|
||||||
|
return (e.FSName == want.FSName || want.FSName == "\x00") &&
|
||||||
|
(e.Dir == want.Dir || want.Dir == "\x00") &&
|
||||||
|
(e.Type == want.Type || want.Type == "\x00") &&
|
||||||
|
(e.Opts == want.Opts || want.Opts == "\x00") &&
|
||||||
|
(e.Freq == want.Freq || want.Freq == -1) &&
|
||||||
|
(e.Passno == want.Passno || want.Passno == -1)
|
||||||
|
}
|
||||||
|
|
||||||
func IterMounts(name string, f func(e *Mntent)) error {
|
func IterMounts(name string, f func(e *Mntent)) error {
|
||||||
m := new(mounts)
|
m := new(mounts)
|
||||||
m.p = name
|
m.p = name
|
||||||
|
@ -51,7 +51,7 @@ let
|
|||||||
(ent "tmpfs" "/etc/passwd" "tmpfs" "ro,nosuid,nodev,relatime,uid=1000001,gid=1000001" 0 0)
|
(ent "tmpfs" "/etc/passwd" "tmpfs" "ro,nosuid,nodev,relatime,uid=1000001,gid=1000001" 0 0)
|
||||||
(ent "tmpfs" "/etc/group" "tmpfs" "ro,nosuid,nodev,relatime,uid=1000001,gid=1000001" 0 0)
|
(ent "tmpfs" "/etc/group" "tmpfs" "ro,nosuid,nodev,relatime,uid=1000001,gid=1000001" 0 0)
|
||||||
(ent "/dev/disk/by-label/nixos" "/run/user/65534/wayland-0" "ext4" "ro,nosuid,nodev,relatime" 0 0)
|
(ent "/dev/disk/by-label/nixos" "/run/user/65534/wayland-0" "ext4" "ro,nosuid,nodev,relatime" 0 0)
|
||||||
(ent "tmpfs" "/run/user/65534/pulse/native" "tmpfs" "ro,nosuid,nodev,relatime,size=98784k,nr_inodes=24696,mode=700,uid=1000,gid=100" 0 0)
|
(ent "tmpfs" "/run/user/65534/pulse/native" "tmpfs" "host_passthrough" 0 0)
|
||||||
(ent "/dev/disk/by-label/nixos" "/run/user/65534/bus" "ext4" "ro,nosuid,nodev,relatime" 0 0)
|
(ent "/dev/disk/by-label/nixos" "/run/user/65534/bus" "ext4" "ro,nosuid,nodev,relatime" 0 0)
|
||||||
(ent "tmpfs" "/var/run/nscd" "tmpfs" "rw,nosuid,nodev,relatime,size=8k,mode=755,uid=1000001,gid=1000001" 0 0)
|
(ent "tmpfs" "/var/run/nscd" "tmpfs" "rw,nosuid,nodev,relatime,size=8k,mode=755,uid=1000001,gid=1000001" 0 0)
|
||||||
(ent "overlay" "/.fortify/sbin/fortify" "overlay" "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on" 0 0)
|
(ent "overlay" "/.fortify/sbin/fortify" "overlay" "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on" 0 0)
|
||||||
|
45
test/sandbox/seccomp.go
Normal file
45
test/sandbox/seccomp.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package sandbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
#include <sys/quota.h>
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
const NULL = 0
|
||||||
|
|
||||||
|
func TrySyscalls() error {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
errno syscall.Errno
|
||||||
|
|
||||||
|
trap, a1, a2, a3, a4, a5, a6 uintptr
|
||||||
|
}{
|
||||||
|
{"syslog", syscall.EPERM, syscall.SYS_SYSLOG, 0, NULL, NULL, NULL, NULL, NULL},
|
||||||
|
{"uselib", syscall.EPERM, syscall.SYS_USELIB, 0, NULL, NULL, NULL, NULL, NULL},
|
||||||
|
{"acct", syscall.EPERM, syscall.SYS_ACCT, 0, NULL, NULL, NULL, NULL, NULL},
|
||||||
|
{"quotactl", syscall.EPERM, syscall.SYS_QUOTACTL, C.Q_GETQUOTA, NULL, uintptr(os.Getuid()), NULL, NULL, NULL},
|
||||||
|
{"add_key", syscall.EPERM, syscall.SYS_ADD_KEY, NULL, NULL, NULL, NULL, NULL, NULL},
|
||||||
|
{"keyctl", syscall.EPERM, syscall.SYS_KEYCTL, NULL, NULL, NULL, NULL, NULL, NULL},
|
||||||
|
{"request_key", syscall.EPERM, syscall.SYS_REQUEST_KEY, NULL, NULL, NULL, NULL, NULL, NULL},
|
||||||
|
{"move_pages", syscall.EPERM, syscall.SYS_MOVE_PAGES, uintptr(os.Getpid()), NULL, NULL, NULL, NULL, NULL},
|
||||||
|
{"mbind", syscall.EPERM, syscall.SYS_MBIND, NULL, NULL, NULL, NULL, NULL, NULL},
|
||||||
|
{"get_mempolicy", syscall.EPERM, syscall.SYS_GET_MEMPOLICY, NULL, NULL, NULL, NULL, NULL, NULL},
|
||||||
|
{"set_mempolicy", syscall.EPERM, syscall.SYS_SET_MEMPOLICY, NULL, NULL, NULL, NULL, NULL, NULL},
|
||||||
|
{"migrate_pages", syscall.EPERM, syscall.SYS_MIGRATE_PAGES, NULL, NULL, NULL, NULL, NULL, NULL},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
if _, _, errno := syscall.Syscall6(tc.trap, tc.a1, tc.a2, tc.a3, tc.a4, tc.a5, tc.a6); errno != tc.errno {
|
||||||
|
printf("[FAIL] %s: %v, want %v", tc.name, errno, tc.errno)
|
||||||
|
return errno
|
||||||
|
}
|
||||||
|
printf("[ OK ] %s: %v", tc.name, tc.errno)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
27
test/sandbox/seccomp.nix
Normal file
27
test/sandbox/seccomp.nix
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
writeText,
|
||||||
|
buildGoModule,
|
||||||
|
|
||||||
|
version,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
mainFile = writeText "main.go" ''
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "git.gensokyo.uk/security/fortify/test/sandbox"
|
||||||
|
|
||||||
|
func main() { sandbox.MustAssertSeccomp() }
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
buildGoModule {
|
||||||
|
pname = "check-seccomp";
|
||||||
|
inherit version;
|
||||||
|
|
||||||
|
src = ../.;
|
||||||
|
vendorHash = null;
|
||||||
|
|
||||||
|
preBuild = ''
|
||||||
|
go mod init git.gensokyo.uk/security/fortify/test >& /dev/null
|
||||||
|
cp ${mainFile} main.go
|
||||||
|
'';
|
||||||
|
}
|
53
test/test.py
53
test/test.py
@ -78,9 +78,7 @@ start_all()
|
|||||||
machine.wait_for_unit("multi-user.target")
|
machine.wait_for_unit("multi-user.target")
|
||||||
|
|
||||||
# Run fortify Go tests outside of nix build in the background:
|
# Run fortify Go tests outside of nix build in the background:
|
||||||
machine.succeed("rm -rf /tmp/src && cp -a \"$(fortify-src)\" /tmp/src")
|
machine.succeed("sudo -u untrusted -i fortify-go-test &> /tmp/go-test &")
|
||||||
machine.succeed(
|
|
||||||
"fortify-fhs -c '(cd /tmp/src && go generate ./... && go test ./... && touch /tmp/success-gotest)' &> /tmp/gotest &")
|
|
||||||
|
|
||||||
# To check fortify's version:
|
# To check fortify's version:
|
||||||
print(machine.succeed("sudo -u alice -i fortify version"))
|
print(machine.succeed("sudo -u alice -i fortify version"))
|
||||||
@ -106,11 +104,11 @@ if denyOutputVerbose != "fsu: uid 1001 is not in the fsurc file\nfortify: *canno
|
|||||||
|
|
||||||
# Check sandbox state:
|
# Check sandbox state:
|
||||||
swaymsg("exec check-sandbox")
|
swaymsg("exec check-sandbox")
|
||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/sandbox-ok")
|
machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/sandbox-ok", timeout=15)
|
||||||
|
|
||||||
# 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", timeout=5)
|
||||||
|
|
||||||
# Verify silent output permissive defaults:
|
# Verify silent output permissive defaults:
|
||||||
output = machine.succeed("sudo -u alice -i fortify run -a 0 true &>/dev/stdout")
|
output = machine.succeed("sudo -u alice -i fortify run -a 0 true &>/dev/stdout")
|
||||||
@ -123,11 +121,11 @@ def silent_output_interrupt(flags):
|
|||||||
wait_for_window("alice@machine")
|
wait_for_window("alice@machine")
|
||||||
# aid 0 does not have home-manager
|
# aid 0 does not have home-manager
|
||||||
machine.send_chars(f"exec fortify run {flags}-a 0 sh -c 'export PATH=/run/current-system/sw/bin:$PATH && touch /tmp/pd-silent-ready && sleep infinity' &>/tmp/pd-silent\n")
|
machine.send_chars(f"exec fortify run {flags}-a 0 sh -c 'export PATH=/run/current-system/sw/bin:$PATH && touch /tmp/pd-silent-ready && sleep infinity' &>/tmp/pd-silent\n")
|
||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/pd-silent-ready")
|
machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/pd-silent-ready", timeout=10)
|
||||||
machine.succeed("rm /tmp/fortify.1000/tmpdir/0/pd-silent-ready")
|
machine.succeed("rm /tmp/fortify.1000/tmpdir/0/pd-silent-ready")
|
||||||
machine.send_key("ctrl-c")
|
machine.send_key("ctrl-c")
|
||||||
machine.wait_until_fails("pgrep foot")
|
machine.wait_until_fails("pgrep foot", timeout=5)
|
||||||
machine.wait_until_fails(f"pgrep -u alice -f 'fortify run {flags}-a 0 '")
|
machine.wait_until_fails(f"pgrep -u alice -f 'fortify run {flags}-a 0 '", timeout=5)
|
||||||
output = machine.succeed("cat /tmp/pd-silent && rm /tmp/pd-silent")
|
output = machine.succeed("cat /tmp/pd-silent && rm /tmp/pd-silent")
|
||||||
if output != "":
|
if output != "":
|
||||||
raise Exception(f"unexpected output\n{output}")
|
raise Exception(f"unexpected output\n{output}")
|
||||||
@ -141,8 +139,8 @@ silent_output_interrupt("--wayland -X --dbus --pulse ")
|
|||||||
print(machine.fail("sudo -u alice -i fortify -v run --wayland true"))
|
print(machine.fail("sudo -u alice -i fortify -v run --wayland true"))
|
||||||
|
|
||||||
# Start fortify permissive defaults within Wayland session:
|
# Start fortify permissive defaults within Wayland session:
|
||||||
fortify('-v run --wayland --dbus notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-done')
|
fortify('-v run --wayland --dbus notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-ok')
|
||||||
machine.wait_for_file("/tmp/dbus-done")
|
machine.wait_for_file("/tmp/dbus-ok", timeout=15)
|
||||||
collect_state_ui("dbus_notify_exited")
|
collect_state_ui("dbus_notify_exited")
|
||||||
machine.succeed("pkill -9 mako")
|
machine.succeed("pkill -9 mako")
|
||||||
|
|
||||||
@ -150,63 +148,62 @@ machine.succeed("pkill -9 mako")
|
|||||||
swaymsg("exec ne-foot")
|
swaymsg("exec ne-foot")
|
||||||
wait_for_window("u0_a2@machine")
|
wait_for_window("u0_a2@machine")
|
||||||
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
|
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
|
||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-client")
|
machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-client", timeout=10)
|
||||||
collect_state_ui("foot_wayland")
|
collect_state_ui("foot_wayland")
|
||||||
check_state("ne-foot", 1)
|
check_state("ne-foot", 1)
|
||||||
# Verify acl on XDG_RUNTIME_DIR:
|
# Verify acl on XDG_RUNTIME_DIR:
|
||||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002"))
|
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002"))
|
||||||
machine.send_chars("exit\n")
|
machine.send_chars("exit\n")
|
||||||
machine.wait_until_fails("pgrep foot")
|
machine.wait_until_fails("pgrep foot", timeout=5)
|
||||||
# Verify acl cleanup on XDG_RUNTIME_DIR:
|
# Verify acl cleanup on XDG_RUNTIME_DIR:
|
||||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002")
|
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002", timeout=5)
|
||||||
|
|
||||||
# Start app (foot) with Wayland enablement from a terminal:
|
# Start app (foot) with Wayland enablement from a terminal:
|
||||||
swaymsg(
|
swaymsg("exec foot $SHELL -c '(ne-foot) & sleep 1 && fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && cat'")
|
||||||
"exec foot $SHELL -c '(ne-foot) & sleep 1 && fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && cat'")
|
|
||||||
wait_for_window("u0_a2@machine")
|
wait_for_window("u0_a2@machine")
|
||||||
machine.send_chars("clear; wayland-info && touch /tmp/success-client-term\n")
|
machine.send_chars("clear; wayland-info && touch /tmp/success-client-term\n")
|
||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-client-term")
|
machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-client-term", timeout=10)
|
||||||
machine.wait_for_file("/tmp/ps-show-ok")
|
machine.wait_for_file("/tmp/ps-show-ok", timeout=5)
|
||||||
collect_state_ui("foot_wayland_term")
|
collect_state_ui("foot_wayland_term")
|
||||||
check_state("ne-foot", 1)
|
check_state("ne-foot", 1)
|
||||||
machine.send_chars("exit\n")
|
machine.send_chars("exit\n")
|
||||||
wait_for_window("foot")
|
wait_for_window("foot")
|
||||||
machine.send_key("ctrl-c")
|
machine.send_key("ctrl-c")
|
||||||
machine.wait_until_fails("pgrep foot")
|
machine.wait_until_fails("pgrep foot", timeout=5)
|
||||||
|
|
||||||
# Test PulseAudio (fortify does not support PipeWire yet):
|
# Test PulseAudio (fortify does not support PipeWire yet):
|
||||||
swaymsg("exec pa-foot")
|
swaymsg("exec pa-foot")
|
||||||
wait_for_window("u0_a3@machine")
|
wait_for_window("u0_a3@machine")
|
||||||
machine.send_chars("clear; pactl info && touch /tmp/success-pulse\n")
|
machine.send_chars("clear; pactl info && touch /tmp/success-pulse\n")
|
||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/3/success-pulse")
|
machine.wait_for_file("/tmp/fortify.1000/tmpdir/3/success-pulse", timeout=10)
|
||||||
collect_state_ui("pulse_wayland")
|
collect_state_ui("pulse_wayland")
|
||||||
check_state("pa-foot", 9)
|
check_state("pa-foot", 9)
|
||||||
machine.send_chars("exit\n")
|
machine.send_chars("exit\n")
|
||||||
machine.wait_until_fails("pgrep foot")
|
machine.wait_until_fails("pgrep foot", timeout=5)
|
||||||
|
|
||||||
# Test XWayland (foot does not support X):
|
# Test XWayland (foot does not support X):
|
||||||
swaymsg("exec x11-alacritty")
|
swaymsg("exec x11-alacritty")
|
||||||
wait_for_window("u0_a4@machine")
|
wait_for_window("u0_a4@machine")
|
||||||
machine.send_chars("clear; glinfo && touch /tmp/success-client-x11\n")
|
machine.send_chars("clear; glinfo && touch /tmp/success-client-x11\n")
|
||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/4/success-client-x11")
|
machine.wait_for_file("/tmp/fortify.1000/tmpdir/4/success-client-x11", timeout=10)
|
||||||
collect_state_ui("alacritty_x11")
|
collect_state_ui("alacritty_x11")
|
||||||
check_state("x11-alacritty", 2)
|
check_state("x11-alacritty", 2)
|
||||||
machine.send_chars("exit\n")
|
machine.send_chars("exit\n")
|
||||||
machine.wait_until_fails("pgrep alacritty")
|
machine.wait_until_fails("pgrep alacritty", timeout=5)
|
||||||
|
|
||||||
# Start app (foot) with direct Wayland access:
|
# Start app (foot) with direct Wayland access:
|
||||||
swaymsg("exec da-foot")
|
swaymsg("exec da-foot")
|
||||||
wait_for_window("u0_a5@machine")
|
wait_for_window("u0_a5@machine")
|
||||||
machine.send_chars("clear; wayland-info && touch /tmp/success-direct\n")
|
machine.send_chars("clear; wayland-info && touch /tmp/success-direct\n")
|
||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/5/success-direct")
|
machine.wait_for_file("/tmp/fortify.1000/tmpdir/5/success-direct", timeout=10)
|
||||||
collect_state_ui("foot_direct")
|
collect_state_ui("foot_direct")
|
||||||
check_state("da-foot", 1)
|
check_state("da-foot", 1)
|
||||||
# Verify acl on XDG_RUNTIME_DIR:
|
# Verify acl on XDG_RUNTIME_DIR:
|
||||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000005"))
|
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000005"))
|
||||||
machine.send_chars("exit\n")
|
machine.send_chars("exit\n")
|
||||||
machine.wait_until_fails("pgrep foot")
|
machine.wait_until_fails("pgrep foot", timeout=5)
|
||||||
# Verify acl cleanup on XDG_RUNTIME_DIR:
|
# Verify acl cleanup on XDG_RUNTIME_DIR:
|
||||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000005")
|
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000005", timeout=5)
|
||||||
|
|
||||||
# Test syscall filter:
|
# Test syscall filter:
|
||||||
print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure"))
|
print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure"))
|
||||||
@ -219,6 +216,6 @@ machine.wait_for_file("/tmp/sway-exit-ok")
|
|||||||
print(machine.succeed("find /run/user/1000/fortify"))
|
print(machine.succeed("find /run/user/1000/fortify"))
|
||||||
|
|
||||||
# Verify go test status:
|
# Verify go test status:
|
||||||
machine.wait_for_file("/tmp/gotest")
|
machine.wait_for_file("/tmp/go-test", timeout=5)
|
||||||
print(machine.succeed("cat /tmp/gotest"))
|
print(machine.succeed("cat /tmp/go-test"))
|
||||||
machine.wait_for_file("/tmp/success-gotest")
|
machine.wait_for_file("/tmp/go-test-ok", timeout=5)
|
||||||
|
Loading…
Reference in New Issue
Block a user