diff --git a/dbus/dbus.go b/dbus/dbus.go index 775dfb5..50d2ec4 100644 --- a/dbus/dbus.go +++ b/dbus/dbus.go @@ -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] } diff --git a/dbus/dbus_test.go b/dbus/dbus_test.go index c20f9be..b1d4669 100644 --- a/dbus/dbus_test.go +++ b/dbus/dbus_test.go @@ -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()) } }) }) diff --git a/dbus/proxy.go b/dbus/proxy.go new file mode 100644 index 0000000..7e7d9a6 --- /dev/null +++ b/dbus/proxy.go @@ -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} +} diff --git a/dbus/run.go b/dbus/run.go index ac1c161..345f12f 100644 --- a/dbus/run.go +++ b/dbus/run.go @@ -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 diff --git a/internal/app/share.dbus.go b/internal/app/share.dbus.go index d26534a..468a8af 100644 --- a/internal/app/share.dbus.go +++ b/internal/app/share.dbus.go @@ -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") }