Compare commits

...

6 Commits

Author SHA1 Message Date
f30a439bcd
nix: improve common usability
All checks were successful
Test / Create distribution (push) Successful in 19s
Test / Sandbox (push) Successful in 31s
Test / Fortify (push) Successful in 35s
Test / Sandbox (race detector) (push) Successful in 31s
Test / Fortify (race detector) (push) Successful in 35s
Test / Fpkg (push) Successful in 33s
Test / Flake checks (push) Successful in 1m7s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-05-16 04:40:12 +09:00
008e9e7fc5
nix: update flake lock
All checks were successful
Test / Create distribution (push) Successful in 28s
Test / Fortify (push) Successful in 38s
Test / Fortify (race detector) (push) Successful in 37s
Test / Fpkg (push) Successful in 35s
Test / Sandbox (push) Successful in 1m18s
Test / Sandbox (race detector) (push) Successful in 1m27s
Test / Flake checks (push) Successful in 2m47s
2025-05-07 21:35:37 +09:00
23aefcd759
fortify: update help strings
All checks were successful
Test / Create distribution (push) Successful in 30s
Test / Sandbox (push) Successful in 1m58s
Test / Sandbox (race detector) (push) Successful in 3m11s
Test / Fpkg (push) Successful in 4m24s
Test / Fortify (race detector) (push) Successful in 4m58s
Test / Fortify (push) Successful in 3m44s
Test / Flake checks (push) Successful in 1m34s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-05-07 19:06:36 +09:00
cb8b886446
nix: update flake lock
All checks were successful
Test / Create distribution (push) Successful in 1m28s
Test / Fortify (push) Successful in 49m23s
Test / Fortify (race detector) (push) Successful in 49m56s
Test / Fpkg (push) Successful in 50m14s
Test / Sandbox (push) Successful in 1m18s
Test / Sandbox (race detector) (push) Successful in 1m20s
Test / Flake checks (push) Successful in 3m0s
2025-04-22 22:23:21 +09:00
5979d8b1e0
dbus: clean up wrapper implementation
All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Sandbox (push) Successful in 1m50s
Test / Fortify (push) Successful in 2m49s
Test / Sandbox (race detector) (push) Successful in 3m4s
Test / Fpkg (push) Successful in 3m35s
Test / Fortify (race detector) (push) Successful in 4m13s
Test / Flake checks (push) Successful in 1m3s
The dbus proxy wrapper haven't been updated much ever since the helper interface was introduced.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-16 23:35:17 +09:00
e587112e63
test: check xdg-dbus-proxy termination
All checks were successful
Test / Sandbox (race detector) (push) Successful in 31s
Test / Sandbox (push) Successful in 33s
Test / Create distribution (push) Successful in 28s
Test / Fpkg (push) Successful in 35s
Test / Fortify (push) Successful in 2m9s
Test / Fortify (race detector) (push) Successful in 2m37s
Test / Flake checks (push) Successful in 1m2s
This process runs outside the application container's pid namespace, so it is a good idea to check whether its lifecycle becomes decoupled from the application.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-15 20:45:31 +09:00
21 changed files with 427 additions and 346 deletions

View File

@ -55,6 +55,8 @@
stateDir = "/var/lib/fortify";
users.alice = 0;
home-manager = _: _: { home.stateVersion = "23.05"; };
extraHomeConfig = {
home.stateVersion = "23.05";
};
};
}

View File

@ -7,24 +7,24 @@ _fortify_app() {
_fortify_run() {
_arguments \
'--id[App ID, leave empty to disable security context app_id]:id' \
'-a[Fortify application ID]: :_numbers' \
'-g[Groups inherited by the app process]: :_groups' \
'-d[Application home directory]: :_files -/' \
'-u[Passwd name within sandbox]: :_users' \
'--wayland[Share Wayland socket]' \
'-X[Share X11 socket and allow connection]' \
'--dbus[Proxy D-Bus connection]' \
'--pulse[Share PulseAudio socket and cookie]' \
'--dbus-config[Path to D-Bus proxy config file]: :_files -g "*.json"' \
'--dbus-system[Path to system D-Bus proxy config file]: :_files -g "*.json"' \
'--id[Reverse-DNS style Application identifier, leave empty to inherit instance identifier]:id' \
'-a[Application identity]: :_numbers' \
'-g[Groups inherited by all container processes]: :_groups' \
'-d[Container home directory]: :_files -/' \
'-u[Passwd user name within sandbox]: :_users' \
'--wayland[Enable connection to Wayland via security-context-v1]' \
'-X[Enable direct connection to X11]' \
'--dbus[Enable proxied connection to D-Bus]' \
'--pulse[Enable direct connection to PulseAudio]' \
'--dbus-config[Path to session bus proxy config file]: :_files -g "*.json"' \
'--dbus-system[Path to system bus proxy config file]: :_files -g "*.json"' \
'--mpris[Allow owning MPRIS D-Bus path]' \
'--dbus-log[Force logging in the D-Bus proxy]'
'--dbus-log[Force buffered logging in the D-Bus proxy]'
}
_fortify_ps() {
_arguments \
'--short[Print instance id]'
'--short[List instances only]'
}
_fortify_show() {
@ -78,5 +78,5 @@ __fortify_instances() {
_arguments -C \
'-v[Verbose output]' \
'--json[Format output in JSON when applicable]' \
'--json[Serialise output in JSON when applicable]' \
'*::fortify command:_fortify_commands'

View File

@ -7,6 +7,9 @@ import (
"os"
)
// ProxyPair is an upstream dbus address and a downstream socket path.
type ProxyPair [2]string
type Config struct {
// See set 'see' policy for NAME (--see=NAME)
See []string `json:"see"`
@ -24,7 +27,7 @@ type Config struct {
Filter bool `json:"filter"`
}
func (c *Config) Args(bus [2]string) (args []string) {
func (c *Config) Args(bus ProxyPair) (args []string) {
argc := 2 + len(c.See) + len(c.Talk) + len(c.Own) + len(c.Call) + len(c.Broadcast)
if c.Log {
argc++

View File

@ -5,6 +5,7 @@ import (
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"strings"
@ -19,64 +20,21 @@ import (
"git.gensokyo.uk/security/fortify/sandbox"
)
func TestNew(t *testing.T) {
for _, tc := range [][2][2]string{
{
{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/1ca5d183ef4c99e74c3e544715f32702/bus"},
{"unix:path=/run/dbus/system_bus_socket", "/tmp/fortify.1971/1ca5d183ef4c99e74c3e544715f32702/system_bus_socket"},
},
{
{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/881ac3796ff3f3bf0a773824383187a0/bus"},
{"unix:path=/run/dbus/system_bus_socket", "/tmp/fortify.1971/881ac3796ff3f3bf0a773824383187a0/system_bus_socket"},
},
{
{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/3d1a5084520ef79c0c6a49a675bac701/bus"},
{"unix:path=/run/dbus/system_bus_socket", "/tmp/fortify.1971/3d1a5084520ef79c0c6a49a675bac701/system_bus_socket"},
},
{
{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/2a1639bab712799788ea0ff7aa280c35/bus"},
{"unix:path=/run/dbus/system_bus_socket", "/tmp/fortify.1971/2a1639bab712799788ea0ff7aa280c35/system_bus_socket"},
},
} {
t.Run("create instance for "+tc[0][0]+" and "+tc[1][0], func(t *testing.T) {
if got := dbus.New(tc[0], tc[1]); !got.CompareTestNew(tc[0], tc[1]) {
t.Errorf("New(%q, %q) = %v",
tc[0], tc[1],
got)
}
})
}
}
func TestProxy_Seal(t *testing.T) {
t.Run("double seal panic", func(t *testing.T) {
defer func() {
want := "dbus proxy sealed twice"
if r := recover(); r != want {
t.Errorf("Seal: panic = %q, want %q",
r, want)
}
}()
p := dbus.New([2]string{}, [2]string{})
_ = p.Seal(dbus.NewConfig("", true, false), nil)
_ = p.Seal(dbus.NewConfig("", true, false), nil)
})
ep := dbus.New([2]string{}, [2]string{})
if err := ep.Seal(nil, nil); !errors.Is(err, dbus.ErrConfig) {
t.Errorf("Seal(nil, nil) error = %v, want %v",
err, dbus.ErrConfig)
func TestFinalise(t *testing.T) {
if _, err := dbus.Finalise(dbus.ProxyPair{}, dbus.ProxyPair{}, nil, nil); !errors.Is(err, syscall.EBADE) {
t.Errorf("Finalise: error = %v, want %v",
err, syscall.EBADE)
}
for id, tc := range testCasePairs() {
t.Run("create seal for "+id, func(t *testing.T) {
p := dbus.New(tc[0].bus, tc[1].bus)
if err := p.Seal(tc[0].c, tc[1].c); (errors.Is(err, syscall.EINVAL)) != tc[0].wantErr {
t.Errorf("Seal(%p, %p) error = %v, wantErr %v",
tc[0].c, tc[1].c,
t.Run("create final for "+id, func(t *testing.T) {
var wt io.WriterTo
if v, err := dbus.Finalise(tc[0].bus, tc[1].bus, tc[0].c, tc[1].c); (errors.Is(err, syscall.EINVAL)) != tc[0].wantErr {
t.Errorf("Finalise: error = %v, wantErr %v",
err, tc[0].wantErr)
return
} else {
wt = v
}
// rest of the tests happen for sealed instances
@ -89,25 +47,23 @@ func TestProxy_Seal(t *testing.T) {
args := append(tc[0].want, tc[1].want...)
for _, arg := range args {
want.WriteString(arg)
want.WriteByte('\x00')
want.WriteByte(0)
}
wt := p.AccessTestProxySeal()
got := new(strings.Builder)
if _, err := wt.WriteTo(got); err != nil {
t.Errorf("p.seal.WriteTo(): %v", err)
t.Errorf("WriteTo: error = %v", err)
}
if want.String() != got.String() {
t.Errorf("Seal(%p, %p) seal = %v, want %v",
tc[0].c, tc[1].c,
t.Errorf("Seal: %q, want %q",
got.String(), want.String())
}
})
}
}
func TestProxy_Start_Wait_Close_String(t *testing.T) {
func TestProxyStartWaitCloseString(t *testing.T) {
oldWaitDelay := helper.WaitDelay
helper.WaitDelay = 16 * time.Second
t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
@ -116,29 +72,62 @@ func TestProxy_Start_Wait_Close_String(t *testing.T) {
proxyName := dbus.ProxyName
dbus.ProxyName = os.Args[0]
t.Cleanup(func() { dbus.ProxyName = proxyName })
testProxyStartWaitCloseString(t, true)
testProxyFinaliseStartWaitCloseString(t, true)
})
t.Run("direct", func(t *testing.T) { testProxyStartWaitCloseString(t, false) })
t.Run("direct", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, false) })
}
func testProxyStartWaitCloseString(t *testing.T, useSandbox bool) {
func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
var p *dbus.Proxy
t.Run("string for nil proxy", func(t *testing.T) {
want := "(invalid dbus proxy)"
if got := p.String(); got != want {
t.Errorf("String: %q, want %q",
got, want)
}
})
t.Run("invalid start", func(t *testing.T) {
if !useSandbox {
p = dbus.NewDirect(context.TODO(), nil, nil)
} else {
p = dbus.New(context.TODO(), nil, nil)
}
if err := p.Start(); !errors.Is(err, syscall.ENOTRECOVERABLE) {
t.Errorf("Start: error = %q, wantErr %q",
err, syscall.ENOTRECOVERABLE)
return
}
})
for id, tc := range testCasePairs() {
// this test does not test errors
if tc[0].wantErr {
continue
}
t.Run("string for nil proxy", func(t *testing.T) {
var p *dbus.Proxy
want := "(invalid dbus proxy)"
if got := p.String(); got != want {
t.Errorf("String() = %v, want %v",
got, want)
}
})
t.Run("proxy for "+id, func(t *testing.T) {
p := dbus.New(tc[0].bus, tc[1].bus)
var final *dbus.Final
t.Run("finalise", func(t *testing.T) {
if v, err := dbus.Finalise(tc[0].bus, tc[1].bus, tc[0].c, tc[1].c); err != nil {
t.Errorf("Finalise: error = %v, wantErr %v",
err, tc[0].wantErr)
return
} else {
final = v
}
})
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
defer cancel()
if !useSandbox {
p = dbus.NewDirect(ctx, final, nil)
} else {
p = dbus.New(ctx, final, nil)
}
p.CommandContext = func(ctx context.Context) (cmd *exec.Cmd) {
return exec.CommandContext(ctx, os.Args[0], "-test.v",
"-test.run=TestHelperInit", "--", "init")
@ -163,79 +152,52 @@ func testProxyStartWaitCloseString(t *testing.T, useSandbox bool) {
p.FilterF = func(v []byte) []byte { return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1] }
output := new(strings.Builder)
t.Run("unsealed", func(t *testing.T) {
t.Run("string", func(t *testing.T) {
want := "(unsealed dbus proxy)"
if got := p.String(); got != want {
t.Errorf("String() = %v, want %v",
got, want)
return
}
})
t.Run("invalid wait", func(t *testing.T) {
wantErr := "dbus: not started"
if err := p.Wait(); err == nil || err.Error() != wantErr {
t.Errorf("Wait: error = %v, wantErr %v",
err, wantErr)
}
})
t.Run("start", func(t *testing.T) {
want := "proxy not sealed"
if err := p.Start(context.Background(), nil, useSandbox); err == nil || err.Error() != want {
t.Errorf("Start() error = %v, wantErr %q",
err, errors.New(want))
t.Run("string", func(t *testing.T) {
want := "(unused dbus proxy)"
if got := p.String(); got != want {
t.Errorf("String: %q, want %q",
got, want)
return
}
})
t.Run("start", func(t *testing.T) {
if err := p.Start(); err != nil {
t.Fatalf("Start: error = %v",
err)
}
t.Run("string", func(t *testing.T) {
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) {
t.Errorf("String: %q, want %q",
got, wantSubstr)
return
}
})
t.Run("wait", func(t *testing.T) {
wantErr := "dbus: not started"
if err := p.Wait(); err == nil || err.Error() != wantErr {
t.Errorf("Wait() error = %v, wantErr %v",
err, wantErr)
}
})
})
t.Run("seal with "+id, func(t *testing.T) {
if err := p.Seal(tc[0].c, tc[1].c); err != nil {
t.Errorf("Seal(%p, %p) error = %v, wantErr %v",
tc[0].c, tc[1].c,
err, tc[0].wantErr)
return
}
})
t.Run("sealed", func(t *testing.T) {
want := strings.Join(append(tc[0].want, tc[1].want...), " ")
if got := p.String(); got != want {
t.Errorf("String() = %v, want %v",
got, want)
return
}
t.Run("start", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := p.Start(ctx, output, useSandbox); err != nil {
t.Fatalf("Start(nil, nil) error = %v",
err)
}
t.Run("string", func(t *testing.T) {
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) {
t.Errorf("String() = %v, want %v",
p.String(), wantSubstr)
return
}
})
t.Run("wait", func(t *testing.T) {
p.Close()
done := make(chan struct{})
go func() {
if err := p.Wait(); err != nil {
t.Errorf("Wait() error = %v\noutput: %s",
t.Errorf("Wait: error = %v\noutput: %s",
err, output.String())
}
})
close(done)
}()
p.Close()
<-done
})
})
})

View File

@ -1,13 +1,13 @@
package dbus
import "io"
import (
"context"
"io"
)
// CompareTestNew provides TestNew with comparison access to unexported Proxy fields.
func (p *Proxy) CompareTestNew(session, system [2]string) bool {
return session == p.session && system == p.system
}
// AccessTestProxySeal provides TestProxy_Seal with access to unexported Proxy seal field.
func (p *Proxy) AccessTestProxySeal() io.WriterTo {
return p.seal
// NewDirect returns a new instance of [Proxy] with its sandbox disabled.
func NewDirect(ctx context.Context, final *Final, output io.Writer) *Proxy {
p := New(ctx, final, output)
p.useSandbox = false
return p
}

View File

@ -3,14 +3,12 @@ package dbus
import (
"context"
"errors"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"slices"
"strconv"
"strings"
"syscall"
"git.gensokyo.uk/security/fortify/helper"
@ -19,25 +17,30 @@ import (
"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")
// Start starts and configures a D-Bus proxy process.
func (p *Proxy) Start() error {
if p.final == nil || p.final.WriterTo == nil {
return syscall.ENOTRECOVERABLE
}
var h helper.Helper
p.mu.Lock()
defer p.mu.Unlock()
p.pmu.Lock()
defer p.pmu.Unlock()
c, cancel := context.WithCancelCause(ctx)
if !useSandbox {
h = helper.NewDirect(c, p.name, p.seal, true, argF, func(cmd *exec.Cmd) {
if p.cancel != nil || p.cause != nil {
return errors.New("dbus: already started")
}
ctx, cancel := context.WithCancelCause(p.ctx)
if !p.useSandbox {
p.helper = helper.NewDirect(ctx, p.name, p.final, true, argF, func(cmd *exec.Cmd) {
if p.CmdF != nil {
p.CmdF(cmd)
}
if output != nil {
cmd.Stdout, cmd.Stderr = output, output
if p.output != nil {
cmd.Stdout, cmd.Stderr = p.output, p.output
}
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
cmd.Env = make([]string, 0)
@ -59,15 +62,15 @@ func (p *Proxy) Start(ctx context.Context, output io.Writer, useSandbox bool) er
libPaths = ldd.Path(entries)
}
h = helper.New(
c, toolPath,
p.seal, true,
p.helper = helper.New(
ctx, toolPath,
p.final, true,
argF, func(container *sandbox.Container) {
container.Seccomp |= seccomp.FilterMultiarch
container.Hostname = "fortify-dbus"
container.CommandContext = p.CommandContext
if output != nil {
container.Stdout, container.Stderr = output, output
if p.output != nil {
container.Stdout, container.Stderr = p.output, p.output
}
if p.CmdF != nil {
@ -81,10 +84,17 @@ func (p *Proxy) Start(ctx context.Context, output io.Writer, useSandbox bool) er
// 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:]))
for _, addr := range [][]AddrEntry{p.final.SessionUpstream, p.final.SystemUpstream} {
for _, ent := range addr {
if ent.Method != "unix" {
continue
}
for _, pair := range ent.Values {
if pair[0] != "path" || !path.IsAbs(pair[1]) {
continue
}
upstreamPaths = append(upstreamPaths, path.Dir(pair[1]))
}
}
}
slices.Sort(upstreamPaths)
@ -95,10 +105,10 @@ func (p *Proxy) Start(ctx context.Context, output io.Writer, useSandbox bool) er
// parent directories of bind paths
sockDirPaths := make([]string, 0, 2)
if d := path.Dir(p.session[1]); path.IsAbs(d) {
if d := path.Dir(p.final.Session[1]); path.IsAbs(d) {
sockDirPaths = append(sockDirPaths, d)
}
if d := path.Dir(p.system[1]); path.IsAbs(d) {
if d := path.Dir(p.final.System[1]); path.IsAbs(d) {
sockDirPaths = append(sockDirPaths, d)
}
slices.Sort(sockDirPaths)
@ -113,14 +123,13 @@ func (p *Proxy) Start(ctx context.Context, output io.Writer, useSandbox bool) er
}, nil)
}
if err := h.Start(); err != nil {
if err := p.helper.Start(); err != nil {
cancel(err)
p.helper = nil
return err
}
p.helper = h
p.ctx = c
p.cancel = cancel
p.cancel, p.cause = cancel, func() error { return context.Cause(ctx) }
return nil
}
@ -128,28 +137,30 @@ 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()
p.mu.RLock()
defer p.mu.RUnlock()
if p.helper == nil {
p.pmu.RLock()
if p.helper == nil || p.cancel == nil || p.cause == nil {
p.pmu.RUnlock()
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) {
if errors.Is(errs[0], context.Canceled) &&
errors.Is(p.cause(), proxyClosed) {
errs[0] = nil
}
p.pmu.RUnlock()
// ensure socket removal so ephemeral directory is empty at revert
if err := os.Remove(p.session[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
if err := os.Remove(p.final.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) {
if p.final.System[1] != "" {
if err := os.Remove(p.final.System[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
errs[2] = err
}
}
@ -159,14 +170,13 @@ func (p *Proxy) Wait() error {
// 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()
p.pmu.Lock()
defer p.pmu.Unlock()
if p.cancel == nil {
panic("dbus: not started")
}
p.cancel(proxyClosed)
p.cancel = nil
}
func argF(argsFd, statFd int) []string {

View File

@ -2,11 +2,10 @@ package dbus
import (
"context"
"errors"
"fmt"
"io"
"os/exec"
"sync"
"syscall"
"git.gensokyo.uk/security/fortify/helper"
)
@ -15,84 +14,88 @@ import (
// Overriding ProxyName will only affect Proxy instance created after the change.
var ProxyName = "xdg-dbus-proxy"
// Proxy holds references to a xdg-dbus-proxy process, and should never be copied.
// Once sealed, configuration changes will no longer be possible and attempting to do so will result in a panic.
// Proxy holds the state of a xdg-dbus-proxy process, and should never be copied.
type Proxy struct {
helper helper.Helper
ctx context.Context
cancel context.CancelCauseFunc
name string
session [2]string
system [2]string
CmdF func(any)
sysP bool
cancel context.CancelCauseFunc
cause func() error
final *Final
output io.Writer
useSandbox bool
name string
CmdF func(any)
CommandContext func(ctx context.Context) (cmd *exec.Cmd)
FilterF func([]byte) []byte
seal io.WriterTo
lock sync.RWMutex
mu, pmu sync.RWMutex
}
func (p *Proxy) Session() [2]string { return p.session }
func (p *Proxy) System() [2]string { return p.system }
func (p *Proxy) Sealed() bool { p.lock.RLock(); defer p.lock.RUnlock(); return p.seal != nil }
var (
ErrConfig = errors.New("no configuration to seal")
)
func (p *Proxy) String() string {
if p == nil {
return "(invalid dbus proxy)"
}
p.lock.RLock()
defer p.lock.RUnlock()
p.mu.RLock()
defer p.mu.RUnlock()
if p.helper != nil {
return p.helper.String()
}
if p.seal != nil {
return p.seal.(fmt.Stringer).String()
}
return "(unsealed dbus proxy)"
return "(unused dbus proxy)"
}
// Seal seals the Proxy instance.
func (p *Proxy) Seal(session, system *Config) error {
p.lock.Lock()
defer p.lock.Unlock()
if p.seal != nil {
panic("dbus proxy sealed twice")
}
// Final describes the outcome of a proxy configuration.
type Final struct {
Session, System ProxyPair
// parsed upstream address
SessionUpstream, SystemUpstream []AddrEntry
io.WriterTo
}
// Finalise creates a checked argument writer for [Proxy].
func Finalise(sessionBus, systemBus ProxyPair, session, system *Config) (final *Final, err error) {
if session == nil && system == nil {
return ErrConfig
return nil, syscall.EBADE
}
var args []string
if session != nil {
args = append(args, session.Args(p.session)...)
args = append(args, session.Args(sessionBus)...)
}
if system != nil {
args = append(args, system.Args(p.system)...)
p.sysP = true
}
if seal, err := helper.NewCheckedArgs(args); err != nil {
return err
} else {
p.seal = seal
args = append(args, system.Args(systemBus)...)
}
return nil
final = &Final{Session: sessionBus, System: systemBus}
final.WriterTo, err = helper.NewCheckedArgs(args)
if err != nil {
return
}
if session != nil {
final.SessionUpstream, err = Parse([]byte(final.Session[0]))
if err != nil {
return
}
}
if system != nil {
final.SystemUpstream, err = Parse([]byte(final.System[0]))
if err != nil {
return
}
}
return
}
// New returns a reference to a new unsealed Proxy.
func New(session, system [2]string) *Proxy {
return &Proxy{name: ProxyName, session: session, system: system}
// New returns a new instance of [Proxy].
func New(ctx context.Context, final *Final, output io.Writer) *Proxy {
return &Proxy{name: ProxyName, ctx: ctx, final: final, output: output, useSandbox: true}
}

12
flake.lock generated
View File

@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1742655702,
"narHash": "sha256-jbqlw4sPArFtNtA1s3kLg7/A4fzP4GLk9bGbtUJg0JQ=",
"lastModified": 1746171682,
"narHash": "sha256-EyXUNSa+H+YvGVuQJP1nZskXAowxKYp79RNUsNdQTj4=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "0948aeedc296f964140d9429223c7e4a0702a1ff",
"rev": "50eee705bbdbac942074a8c120e8194185633675",
"type": "github"
},
"original": {
@ -23,11 +23,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1743231893,
"narHash": "sha256-tpJsHMUPEhEnzySoQxx7+kA+KUtgWqvlcUBqROYNNt0=",
"lastModified": 1746557022,
"narHash": "sha256-QkNoyEf6TbaTW5UZYX0OkwIJ/ZMeKSSoOMnSDPQuol0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c570c1f5304493cafe133b8d843c7c1c4a10d3a6",
"rev": "1d3aeb5a193b9ff13f63f4d9cc169fb88129f860",
"type": "github"
},
"original": {

26
main.go
View File

@ -72,7 +72,7 @@ func buildCommand(out io.Writer) command.Command {
return nil
}).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable")
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
c.Command("shim", command.UsageInternal, func([]string) error { instance.ShimMain(); return errSuccess })
@ -205,31 +205,31 @@ func buildCommand(out io.Writer) command.Command {
panic("unreachable")
}).
Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
"Path to D-Bus proxy config file, or \"builtin\" for defaults").
"Path to session bus proxy config file, or \"builtin\" for defaults").
Flag(&dbusConfigSystem, "dbus-system", command.StringFlag("nil"),
"Path to system D-Bus proxy config file, or \"nil\" to disable").
"Path to system bus proxy config file, or \"nil\" to disable").
Flag(&mpris, "mpris", command.BoolFlag(false),
"Allow owning MPRIS D-Bus path, has no effect if custom config is available").
Flag(&dbusVerbose, "dbus-log", command.BoolFlag(false),
"Force logging in the D-Bus proxy").
"Force buffered logging in the D-Bus proxy").
Flag(&fid, "id", command.StringFlag(""),
"App ID, leave empty to disable security context app_id").
"Reverse-DNS style Application identifier, leave empty to inherit instance identifier").
Flag(&aid, "a", command.IntFlag(0),
"Fortify application ID").
"Application identity").
Flag(nil, "g", &groups,
"Groups inherited by the app process").
"Groups inherited by all container processes").
Flag(&homeDir, "d", command.StringFlag("os"),
"Application home directory").
"Container home directory").
Flag(&userName, "u", command.StringFlag("chronos"),
"Passwd name within sandbox").
"Passwd user name within sandbox").
Flag(&wayland, "wayland", command.BoolFlag(false),
"Allow Wayland connections").
"Enable connection to Wayland via security-context-v1").
Flag(&x11, "X", command.BoolFlag(false),
"Share X11 socket and allow connection").
"Enable direct connection to X11").
Flag(&dBus, "dbus", command.BoolFlag(false),
"Proxy D-Bus connection").
"Enable proxied connection to D-Bus").
Flag(&pulse, "pulse", command.BoolFlag(false),
"Share PulseAudio socket and cookie")
"Enable direct connection to PulseAudio")
}
var showFlagShort bool

View File

@ -36,31 +36,31 @@ Commands:
Usage: fortify run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--wayland] [-X] [--dbus] [--pulse] COMMAND [OPTIONS]
Flags:
-X Share X11 socket and allow connection
-X Enable direct connection to X11
-a int
Fortify application ID
Application identity
-d string
Application home directory (default "os")
Container home directory (default "os")
-dbus
Proxy D-Bus connection
Enable proxied connection to D-Bus
-dbus-config string
Path to D-Bus proxy config file, or "builtin" for defaults (default "builtin")
Path to session bus proxy config file, or "builtin" for defaults (default "builtin")
-dbus-log
Force logging in the D-Bus proxy
Force buffered logging in the D-Bus proxy
-dbus-system string
Path to system D-Bus proxy config file, or "nil" to disable (default "nil")
Path to system bus proxy config file, or "nil" to disable (default "nil")
-g value
Groups inherited by the app process
Groups inherited by all container processes
-id string
App ID, leave empty to disable security context app_id
Reverse-DNS style Application identifier, leave empty to inherit instance identifier
-mpris
Allow owning MPRIS D-Bus path, has no effect if custom config is available
-pulse
Share PulseAudio socket and cookie
Enable direct connection to PulseAudio
-u string
Passwd name within sandbox (default "chronos")
Passwd user name within sandbox (default "chronos")
-wayland
Allow Wayland connections
Enable connection to Wayland via security-context-v1
`,
},

View File

@ -146,7 +146,6 @@ in
]
++ optionals app.nix [
(mustBind "/nix/var")
(bind "/var/db/nix-channels")
]
++ optionals isGraphical [
(devBind "/dev/dri")
@ -156,6 +155,7 @@ in
(devBind "/dev/nvidia-uvm-tools")
(devBind "/dev/nvidia0")
]
++ optionals app.useCommonPaths cfg.commonPaths
++ app.extraPaths;
auto_etc = true;
cover = [ "/var/run/nscd" ];
@ -225,13 +225,13 @@ in
# aid 0 is reserved
imap1 (aid: app: {
${getsubname fid aid} = mkMerge [
(cfg.home-manager (getsubname fid aid) (getsubuid fid aid))
cfg.extraHomeConfig
app.extraConfig
{ home.packages = app.packages; }
];
}) cfg.apps
))
{ ${getsubname fid 0} = cfg.home-manager (getsubname fid 0) (getsubuid fid 0); }
{ ${getsubname fid 0} = cfg.extraHomeConfig; }
acc
]
) privPackages cfg.users;

View File

@ -3,6 +3,38 @@ packages:
let
inherit (lib) types mkOption mkEnableOption;
mountPoint =
let
inherit (types)
str
submodule
nullOr
listOf
;
in
listOf (submodule {
options = {
dst = mkOption {
type = nullOr str;
default = null;
description = ''
Mount point in container, same as src if null.
'';
};
src = mkOption {
type = str;
description = ''
Host filesystem path to make available to the container.
'';
};
write = mkEnableOption "mounting path as writable";
dev = mkEnableOption "use of device files";
require = mkEnableOption "start failure if the bind mount cannot be established for any reason";
};
});
in
{
@ -33,14 +65,10 @@ in
'';
};
home-manager = mkOption {
type =
let
inherit (types) functionTo attrsOf anything;
in
functionTo (functionTo (attrsOf anything));
extraHomeConfig = mkOption {
type = types.anything;
description = ''
Target user shared home-manager configuration.
Extra home-manager configuration to merge with all target users.
'';
};
@ -189,11 +217,15 @@ in
'';
};
useCommonPaths = mkEnableOption "common extra paths" // {
default = true;
};
extraPaths = mkOption {
type = listOf anything;
type = mountPoint;
default = [ ];
description = ''
Extra paths to make available to the sandbox.
Extra paths to make available to the container.
'';
};
@ -242,7 +274,17 @@ in
};
});
default = [ ];
description = "Declarative fortify apps.";
description = ''
Declaratively configured fortify apps.
'';
};
commonPaths = mkOption {
type = mountPoint;
default = [ ];
description = ''
Common extra paths to make available to the container.
'';
};
stateDir = mkOption {

View File

@ -7,6 +7,7 @@ import (
"log"
"strings"
"sync"
"syscall"
"git.gensokyo.uk/security/fortify/dbus"
)
@ -26,64 +27,62 @@ func (sys *I) MustProxyDBus(sessionPath string, session *dbus.Config, systemPath
func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath string) (func(), error) {
d := new(DBus)
// session bus is mandatory
// session bus is required as otherwise this is effectively a very expensive noop
if session == nil {
return nil, msg.WrapErr(ErrDBusConfig,
"attempted to seal message bus proxy without session bus config")
"attempted to create message bus proxy args without session bus config")
}
// system bus is optional
d.system = system != nil
// upstream address, downstream socket path
var sessionBus, systemBus [2]string
// resolve upstream bus addresses
sessionBus[0], systemBus[0] = dbus.Address()
// set paths from caller
sessionBus[1], systemBus[1] = sessionPath, systemPath
// create proxy instance
d.proxy = dbus.New(sessionBus, systemBus)
defer func() {
if msg.IsVerbose() && d.proxy.Sealed() {
msg.Verbose("sealed session proxy", session.Args(sessionBus))
if system != nil {
msg.Verbose("sealed system proxy", system.Args(systemBus))
}
msg.Verbose("message bus proxy final args:", d.proxy)
}
}()
// queue operation
sys.ops = append(sys.ops, d)
// seal dbus proxy
d.sessionBus[0], d.systemBus[0] = dbus.Address()
d.sessionBus[1], d.systemBus[1] = sessionPath, systemPath
d.out = &scanToFmsg{msg: new(strings.Builder)}
return d.out.Dump, wrapErrSuffix(d.proxy.Seal(session, system),
"cannot seal message bus proxy:")
if final, err := dbus.Finalise(d.sessionBus, d.systemBus, session, system); err != nil {
if errors.Is(err, syscall.EINVAL) {
return nil, msg.WrapErr(err, "message bus proxy configuration contains NUL byte")
}
return nil, wrapErrSuffix(err, "cannot finalise message bus proxy:")
} else {
if msg.IsVerbose() {
msg.Verbose("session bus proxy:", session.Args(d.sessionBus))
if system != nil {
msg.Verbose("system bus proxy:", system.Args(d.systemBus))
}
// this calls the argsWt String method
msg.Verbose("message bus proxy final args:", final.WriterTo)
}
d.final = final
}
sys.ops = append(sys.ops, d)
return d.out.Dump, nil
}
type DBus struct {
proxy *dbus.Proxy
proxy *dbus.Proxy // populated during apply
out *scanToFmsg
final *dbus.Final
out *scanToFmsg
// whether system bus proxy is enabled
system bool
sessionBus, systemBus dbus.ProxyPair
}
func (d *DBus) Type() Enablement { return Process }
func (d *DBus) apply(sys *I) error {
msg.Verbosef("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.sessionBus[1], d.sessionBus[0])
if d.system {
msg.Verbosef("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.systemBus[1], d.systemBus[0])
}
// this starts the process and blocks until ready
if err := d.proxy.Start(sys.ctx, d.out, true); err != nil {
d.proxy = dbus.New(sys.ctx, d.final, d.out)
if err := d.proxy.Start(); err != nil {
d.out.Dump()
return wrapErrSuffix(err,
"cannot start message bus proxy:")

View File

@ -30,13 +30,9 @@
environment = {
systemPackages = with pkgs; [
# For glinfo and wayland-info:
mesa-demos
wayland-utils
# For D-Bus tests:
libnotify
mako
libnotify
];
variables = {
@ -99,14 +95,21 @@
stateDir = "/var/lib/fortify";
users.alice = 0;
home-manager = _: _: { home.stateVersion = "23.05"; };
extraHomeConfig = {
home.stateVersion = "23.05";
};
apps = [
{
name = "ne-foot";
verbose = true;
share = pkgs.foot;
packages = [ pkgs.foot ];
packages = with pkgs; [
foot
# For wayland-info:
wayland-utils
];
command = "foot";
capability = {
dbus = false;
@ -125,7 +128,13 @@
name = "x11-alacritty";
verbose = true;
share = pkgs.alacritty;
packages = [ pkgs.alacritty ];
packages = with pkgs; [
# For X11 terminal emulator:
alacritty
# For glinfo:
mesa-demos
];
command = "alacritty";
capability = {
wayland = false;
@ -139,7 +148,12 @@
verbose = true;
insecureWayland = true;
share = pkgs.foot;
packages = [ pkgs.foot ];
packages = with pkgs; [
foot
# For wayland-info:
wayland-utils
];
command = "foot";
capability = {
dbus = false;

View File

@ -37,7 +37,12 @@ let
{
name = "check-sandbox-${tc.name}";
verbose = true;
inherit (tc) tty device mapRealUid;
inherit (tc)
tty
device
mapRealUid
useCommonPaths
;
share = testProgram;
packages = [ ];
path = "${testProgram}/bin/fortify-test";

View File

@ -8,6 +8,7 @@
tty = false;
device = true;
mapRealUid = false;
useCommonPaths = true;
want = {
env = [
@ -140,7 +141,12 @@
u0 = fs "800001c0" {
a4 = fs "800001c0" {
".cache" = fs "800001ed" { ".keep" = fs "80001ff" null ""; } null;
".config" = fs "800001ed" { "environment.d" = fs "800001ed" { "10-home-manager.conf" = fs "80001ff" null null; } null; } null;
".config" = fs "800001ed" {
"environment.d" = fs "800001ed" { "10-home-manager.conf" = fs "80001ff" null null; } null;
systemd = fs "800001ed" {
user = fs "800001ed" { "tray.target" = fs "80001ff" null null; } null;
} null;
} null;
".local" = fs "800001ed" {
state = fs "800001ed" {
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
@ -164,6 +170,7 @@
} null;
} null;
run = fs "800001ed" { nscd = fs "800001ed" { } null; } null;
cache = fs "800001ed" { private = fs "800001c0" null null; } null;
} null;
} null;
@ -185,6 +192,7 @@
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000004,gid=1000004")
(ent "/" "/run/user/65534" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8192k,mode=700,uid=1000004,gid=1000004")

View File

@ -8,6 +8,7 @@
tty = false;
device = false;
mapRealUid = true;
useCommonPaths = true;
want = {
env = [
@ -164,7 +165,12 @@
u0 = fs "800001c0" {
a3 = fs "800001c0" {
".cache" = fs "800001ed" { ".keep" = fs "80001ff" null ""; } null;
".config" = fs "800001ed" { "environment.d" = fs "800001ed" { "10-home-manager.conf" = fs "80001ff" null null; } null; } null;
".config" = fs "800001ed" {
"environment.d" = fs "800001ed" { "10-home-manager.conf" = fs "80001ff" null null; } null;
systemd = fs "800001ed" {
user = fs "800001ed" { "tray.target" = fs "80001ff" null null; } null;
} null;
} null;
".local" = fs "800001ed" {
state = fs "800001ed" {
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
@ -188,6 +194,7 @@
} null;
} null;
run = fs "800001ed" { nscd = fs "800001ed" { } null; } null;
cache = fs "800001ed" { private = fs "800001c0" null null; } null;
} null;
} null;
@ -213,6 +220,7 @@
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000003,gid=1000003")
(ent "/" "/run/user/1000" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8192k,mode=700,uid=1000003,gid=1000003")

View File

@ -8,6 +8,7 @@
tty = false;
device = false;
mapRealUid = false;
useCommonPaths = false;
want = {
env = [
@ -164,7 +165,12 @@
u0 = fs "800001c0" {
a1 = fs "800001c0" {
".cache" = fs "800001ed" { ".keep" = fs "80001ff" null ""; } null;
".config" = fs "800001ed" { "environment.d" = fs "800001ed" { "10-home-manager.conf" = fs "80001ff" null null; } null; } null;
".config" = fs "800001ed" {
"environment.d" = fs "800001ed" { "10-home-manager.conf" = fs "80001ff" null null; } null;
systemd = fs "800001ed" {
user = fs "800001ed" { "tray.target" = fs "80001ff" null null; } null;
} null;
} null;
".local" = fs "800001ed" {
state = fs "800001ed" {
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;

View File

@ -8,6 +8,7 @@
tty = true;
device = false;
mapRealUid = false;
useCommonPaths = true;
want = {
env = [
@ -165,7 +166,12 @@
u0 = fs "800001c0" {
a2 = fs "800001c0" {
".cache" = fs "800001ed" { ".keep" = fs "80001ff" null ""; } null;
".config" = fs "800001ed" { "environment.d" = fs "800001ed" { "10-home-manager.conf" = fs "80001ff" null null; } null; } null;
".config" = fs "800001ed" {
"environment.d" = fs "800001ed" { "10-home-manager.conf" = fs "80001ff" null null; } null;
systemd = fs "800001ed" {
user = fs "800001ed" { "tray.target" = fs "80001ff" null null; } null;
} null;
} null;
".local" = fs "800001ed" {
state = fs "800001ed" {
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
@ -189,6 +195,7 @@
} null;
} null;
run = fs "800001ed" { nscd = fs "800001ed" { } null; } null;
cache = fs "800001ed" { private = fs "800001c0" null null; } null;
} null;
} null;
@ -215,6 +222,7 @@
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000002,gid=1000002")
(ent "/" "/run/user/65534" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8192k,mode=700,uid=1000002,gid=1000002")

View File

@ -65,7 +65,16 @@ in
stateDir = "/var/lib/fortify";
users.alice = 0;
home-manager = _: _: { home.stateVersion = "23.05"; };
extraHomeConfig = {
home.stateVersion = "23.05";
};
commonPaths = [
{
src = "/var/cache";
write = true;
}
];
apps = with testCases; [
preset

View File

@ -152,6 +152,8 @@ print(machine.fail("sudo -u alice -i fortify -v run --wayland true"))
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-ok", timeout=15)
collect_state_ui("dbus_notify_exited")
# not in pid namespace, verify termination
machine.wait_until_fails("pgrep xdg-dbus-proxy")
machine.succeed("pkill -9 mako")
# Check revert type selection: