dbus/run: support running xdg-dbus-proxy in a restrictive bubblewrap sandbox

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
This commit is contained in:
Ophestra Umiker 2024-10-09 20:41:42 +09:00
parent 6232291cae
commit 753c5191b1
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
5 changed files with 228 additions and 98 deletions

View File

@ -1,84 +1,39 @@
package dbus
import (
"errors"
"fmt"
"io"
"os"
"sync"
"git.ophivana.moe/cat/fortify/helper"
)
// ProxyName is the file name or path to the proxy program.
// 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.
type Proxy struct {
helper helper.Helper
name string
session [2]string
system [2]string
seal io.WriterTo
lock sync.RWMutex
}
const (
SessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
SystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
)
var (
ErrConfig = errors.New("no configuration to seal")
addresses [2]string
addressOnce sync.Once
)
func (p *Proxy) String() string {
if p == nil {
return "(invalid dbus proxy)"
}
func Address() (session, system string) {
addressOnce.Do(func() {
// resolve upstream session bus address
if addr, ok := os.LookupEnv(SessionBusAddress); !ok {
// fall back to default format
addresses[0] = fmt.Sprintf("unix:path=/run/user/%d/bus", os.Getuid())
} else {
addresses[0] = addr
}
p.lock.RLock()
defer p.lock.RUnlock()
// resolve upstream system bus address
if addr, ok := os.LookupEnv(SystemBusAddress); !ok {
// fall back to default hardcoded value
addresses[1] = "unix:path=/run/dbus/system_bus_socket"
} else {
addresses[1] = addr
}
})
if p.helper != nil {
return p.helper.Unwrap().String()
}
if p.seal != nil {
return p.seal.(fmt.Stringer).String()
}
return "(unsealed 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")
}
if session == nil && system == nil {
return ErrConfig
}
var args []string
if session != nil {
args = append(args, session.Args(p.session)...)
}
if system != nil {
args = append(args, system.Args(p.system)...)
}
if seal, err := helper.NewCheckedArgs(args); err != nil {
return err
} else {
p.seal = seal
}
return nil
}
// New returns a reference to a new unsealed Proxy.
func New(session, system [2]string) *Proxy {
return &Proxy{name: ProxyName, session: session, system: system}
return addresses[0], addresses[1]
}

View File

@ -98,6 +98,15 @@ func TestProxy_Seal(t *testing.T) {
}
func TestProxy_Start_Wait_Close_String(t *testing.T) {
t.Run("sandboxed", func(t *testing.T) {
testProxyStartWaitCloseString(t, true)
})
t.Run("direct", func(t *testing.T) {
testProxyStartWaitCloseString(t, false)
})
}
func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
for id, tc := range testCasePairs() {
// this test does not test errors
if tc[0].wantErr {
@ -116,6 +125,7 @@ func TestProxy_Start_Wait_Close_String(t *testing.T) {
t.Run("proxy for "+id, func(t *testing.T) {
helper.InternalReplaceExecCommand(t)
p := dbus.New(tc[0].bus, tc[1].bus)
output := new(strings.Builder)
t.Run("unsealed behaviour of "+id, func(t *testing.T) {
t.Run("unsealed string of "+id, func(t *testing.T) {
@ -154,13 +164,13 @@ func TestProxy_Start_Wait_Close_String(t *testing.T) {
}
t.Run("sealed start of "+id, func(t *testing.T) {
if err := p.Start(nil, nil); err != nil {
if err := p.Start(nil, output, sandbox); err != nil {
t.Errorf("Start(nil, nil) error = %v",
err)
}
t.Run("started string of "+id, func(t *testing.T) {
wantSubstr := dbus.ProxyName + " --args=3"
wantSubstr := dbus.ProxyName + " --args="
if got := p.String(); !strings.Contains(got, wantSubstr) {
t.Errorf("String() = %v, want %v",
p.String(), wantSubstr)
@ -185,8 +195,8 @@ func TestProxy_Start_Wait_Close_String(t *testing.T) {
t.Run("started wait of "+id, func(t *testing.T) {
if err := p.Wait(); err != nil {
t.Errorf("Wait() error = %v",
err)
t.Errorf("Wait() error = %v\noutput: %s",
err, output.String())
}
})
})

90
dbus/proxy.go Normal file
View File

@ -0,0 +1,90 @@
package dbus
import (
"errors"
"fmt"
"io"
"sync"
"git.ophivana.moe/cat/fortify/helper"
"git.ophivana.moe/cat/fortify/helper/bwrap"
)
// ProxyName is the file name or path to the proxy program.
// 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.
type Proxy struct {
helper helper.Helper
bwrap *bwrap.Config
name string
session [2]string
system [2]string
seal io.WriterTo
lock sync.RWMutex
}
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()
if p.helper != nil {
return p.helper.Unwrap().String()
}
if p.seal != nil {
return p.seal.(fmt.Stringer).String()
}
return "(unsealed dbus proxy)"
}
func (p *Proxy) Bwrap() []string {
return p.bwrap.Args()
}
// 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")
}
if session == nil && system == nil {
return ErrConfig
}
var args []string
if session != nil {
args = append(args, session.Args(p.session)...)
}
if system != nil {
args = append(args, system.Args(p.system)...)
}
if seal, err := helper.NewCheckedArgs(args); err != nil {
return err
} else {
p.seal = seal
}
return nil
}
// New returns a reference to a new unsealed Proxy.
func New(session, system [2]string) *Proxy {
return &Proxy{name: ProxyName, session: session, system: system}
}

View File

@ -3,14 +3,20 @@ package dbus
import (
"errors"
"io"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
"git.ophivana.moe/cat/fortify/helper"
"git.ophivana.moe/cat/fortify/helper/bwrap"
"git.ophivana.moe/cat/fortify/ldd"
)
// Start launches the D-Bus proxy and sets up the Wait method.
// ready should be buffered and should only be received from once.
func (p *Proxy) Start(ready chan error, output io.Writer) error {
// ready should be buffered and must only be received from once.
func (p *Proxy) Start(ready chan error, output io.Writer, sandbox bool) error {
p.lock.Lock()
defer p.lock.Unlock()
@ -18,18 +24,98 @@ func (p *Proxy) Start(ready chan error, output io.Writer) error {
return errors.New("proxy not sealed")
}
h := helper.New(p.seal, p.name,
func(argsFD, statFD int) []string {
var (
h helper.Helper
cmd *exec.Cmd
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)}
}
},
}
)
cmd := h.Unwrap()
// xdg-dbus-proxy does not need to inherit the environment
cmd.Env = []string{}
if !sandbox {
h = helper.New(p.seal, p.name, argF)
cmd = h.Unwrap()
// xdg-dbus-proxy does not need to inherit the environment
cmd.Env = []string{}
} 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 {
toolPath = s
}
}
// resolve libraries by parsing ldd output
var proxyDeps []*ldd.Entry
if path.IsAbs(toolPath) {
if l, err := ldd.Exec(toolPath); err != nil {
return err
} else {
proxyDeps = l
}
}
bc := &bwrap.Config{
Unshare: nil,
Hostname: "fortify-dbus",
Chdir: "/",
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{}{}
}
}
}
bindTargetDedup := make([][2]string, 0, len(bindTarget))
for k := range bindTarget {
bindTargetDedup = append(bindTargetDedup, [2]string{k, k})
}
bc.Bind = append(bc.Bind, bindTargetDedup...)
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 ent == nil {
continue
}
if path.IsAbs(ent.Path) {
roBindTarget[path.Dir(ent.Path)] = 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{}{}
}
}
roBindTargetDedup := make([][2]string, 0, len(roBindTarget))
for k := range roBindTarget {
roBindTargetDedup = append(roBindTargetDedup, [2]string{k, k})
}
bc.ROBind = append(bc.ROBind, roBindTargetDedup...)
h = helper.MustNewBwrap(bc, p.seal, toolPath, argF)
cmd = h.Unwrap()
p.bwrap = bc
}
if output != nil {
cmd.Stdout = output

View File

@ -48,21 +48,8 @@ func (seal *appSeal) shareDBus(config [2]*dbus.Config) error {
sessionBus[1] = path.Join(seal.share, "bus")
systemBus[1] = path.Join(seal.share, "system_bus_socket")
// resolve upstream session bus address
if addr, ok := os.LookupEnv(dbusSessionBusAddress); !ok {
// fall back to default format
sessionBus[0] = fmt.Sprintf("unix:path=/run/user/%d/bus", os.Getuid())
} else {
sessionBus[0] = addr
}
// resolve upstream system bus address
if addr, ok := os.LookupEnv(dbusSystemBusAddress); !ok {
// fall back to default hardcoded value
systemBus[0] = "unix:path=/run/dbus/system_bus_socket"
} else {
systemBus[0] = addr
}
// resolve upstream bus addresses
sessionBus[0], systemBus[0] = dbus.Address()
// create proxy instance
seal.sys.dbus = dbus.New(sessionBus, systemBus)
@ -93,15 +80,17 @@ func (tx *appSealTx) startDBus() error {
tx.dbusWait = make(chan struct{})
// background dbus proxy start
if err := tx.dbus.Start(ready, os.Stderr); err != nil {
if err := tx.dbus.Start(ready, os.Stderr, true); err != nil {
return (*StartDBusError)(wrapError(err, "cannot start message bus proxy:", err))
}
verbose.Println("starting message bus proxy:", tx.dbus)
verbose.Println("message bus proxy bwrap args:", tx.dbus.Bwrap())
// background wait for proxy instance and notify completion
go func() {
if err := tx.dbus.Wait(); err != nil {
fmt.Println("fortify: warn: message bus proxy returned error:", err)
go func() { ready <- err }()
} else {
verbose.Println("message bus proxy exit")
}