Compare commits

..

No commits in common. "b3ef53b193bdf764d8f04e19ea47901b71eec10b" and "2d606b1f4b92fc9c037aaf5718bc3e24ef05a532" have entirely different histories.

17 changed files with 186 additions and 169 deletions

View File

@ -7,6 +7,8 @@ type Payload struct {
Argv0 string
// child full argv
Argv []string
// wayland fd, -1 to disable
WL int
// verbosity pass through
Verbose bool

View File

@ -92,6 +92,14 @@ func main() {
cmd.Args = payload.Argv
cmd.Env = os.Environ()
// pass wayland fd
if payload.WL != -1 {
if f := os.NewFile(uintptr(payload.WL), "wayland"); f != nil {
cmd.Env = append(cmd.Env, "WAYLAND_SOCKET="+strconv.Itoa(3+len(cmd.ExtraFiles)))
cmd.ExtraFiles = append(cmd.ExtraFiles, f)
}
}
if err := cmd.Start(); err != nil {
fmsg.Fatalf("cannot start %q: %v", payload.Argv0, err)
}

View File

@ -2,6 +2,7 @@ package shim0
import (
"encoding/gob"
"errors"
"net"
"git.ophivana.moe/security/fortify/helper/bwrap"
@ -17,19 +18,25 @@ type Payload struct {
Exec [2]string
// bwrap config
Bwrap *bwrap.Config
// sync fd
Sync *uintptr
// whether to pass wayland fd
WL bool
// verbosity pass through
Verbose bool
}
func (p *Payload) Serve(conn *net.UnixConn) error {
func (p *Payload) Serve(conn *net.UnixConn, wl *Wayland) error {
if err := gob.NewEncoder(conn).Encode(*p); err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot stream shim payload:")
}
if wl != nil {
if err := wl.WriteUnix(conn); err != nil {
return errors.Join(err, conn.Close())
}
}
return fmsg.WrapErrorSuffix(conn.Close(),
"cannot close setup connection:")
}

View File

@ -39,12 +39,14 @@ type Shim struct {
abortOnce sync.Once
// fallback exit notifier with error returned killing the process
killFallback chan error
// wayland mediation, nil if disabled
wl *shim0.Wayland
// shim setup payload
payload *shim0.Payload
}
func New(uid uint32, aid string, supp []string, socket string, payload *shim0.Payload) *Shim {
return &Shim{uid: uid, aid: aid, supp: supp, socket: socket, payload: payload}
func New(uid uint32, aid string, supp []string, socket string, wl *shim0.Wayland, payload *shim0.Payload) *Shim {
return &Shim{uid: uid, aid: aid, supp: supp, socket: socket, wl: wl, payload: payload}
}
func (s *Shim) String() string {
@ -110,14 +112,6 @@ func (s *Shim) Start() (*time.Time, error) {
}
s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
s.cmd.Dir = "/"
// pass sync fd if set
if s.payload.Bwrap.Sync() != nil {
fd := uintptr(3 + len(s.cmd.ExtraFiles))
s.payload.Sync = &fd
s.cmd.ExtraFiles = append(s.cmd.ExtraFiles, s.payload.Bwrap.Sync())
}
fmsg.VPrintln("starting shim via fsu:", s.cmd)
fmsg.Suspend() // withhold messages to stderr
if err := s.cmd.Start(); err != nil {
@ -178,9 +172,9 @@ func (s *Shim) Start() (*time.Time, error) {
return &startTime, err
}
// serve payload
// serve payload and wayland fd if enabled
// this also closes the connection
err := s.payload.Serve(conn)
err := s.payload.Serve(conn, s.wl)
if err == nil {
killShim = func() {}
}

75
cmd/fshim/ipc/wayland.go Normal file
View File

@ -0,0 +1,75 @@
package shim0
import (
"fmt"
"net"
"sync"
"syscall"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
// Wayland implements wayland mediation.
type Wayland struct {
// wayland socket path
Path string
// wayland connection
conn *net.UnixConn
connErr error
sync.Once
// wait for wayland client to exit
done chan struct{}
}
func (wl *Wayland) WriteUnix(conn *net.UnixConn) error {
// connect to host wayland socket
if f, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: wl.Path, Net: "unix"}); err != nil {
return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot connect to wayland at %q:", wl.Path))
} else {
fmsg.VPrintf("connected to wayland at %q", wl.Path)
wl.conn = f
}
// set up for passing wayland socket
if rc, err := wl.conn.SyscallConn(); err != nil {
return fmsg.WrapErrorSuffix(err, "cannot obtain raw wayland connection:")
} else {
ec := make(chan error)
go func() {
// pass wayland connection fd
if err = rc.Control(func(fd uintptr) {
if _, _, err = conn.WriteMsgUnix(nil, syscall.UnixRights(int(fd)), nil); err != nil {
ec <- fmsg.WrapErrorSuffix(err, "cannot pass wayland connection to shim:")
return
}
ec <- nil
// block until shim exits
<-wl.done
fmsg.VPrintln("releasing wayland connection")
}); err != nil {
ec <- fmsg.WrapErrorSuffix(err, "cannot obtain wayland connection fd:")
return
}
}()
return <-ec
}
}
func (wl *Wayland) Close() error {
wl.Do(func() {
close(wl.done)
wl.connErr = wl.conn.Close()
})
return wl.connErr
}
func NewWayland() *Wayland {
wl := new(Wayland)
wl.done = make(chan struct{})
return wl
}

View File

@ -2,6 +2,7 @@ package main
import (
"encoding/gob"
"errors"
"net"
"os"
"path"
@ -75,9 +76,14 @@ func main() {
fmsg.Fatal("bwrap config not supplied")
}
// restore bwrap sync fd
if payload.Sync != nil {
payload.Bwrap.SetSync(os.NewFile(*payload.Sync, "sync"))
// receive wayland fd over socket
wfd := -1
if payload.WL {
if fd, err := receiveWLfd(conn); err != nil {
fmsg.Fatalf("cannot receive wayland fd: %v", err)
} else {
wfd = fd
}
}
// close setup socket
@ -110,6 +116,16 @@ func main() {
var extraFiles []*os.File
// pass wayland fd
if wfd != -1 {
if f := os.NewFile(uintptr(wfd), "wayland"); f != nil {
ic.WL = 3 + len(extraFiles)
extraFiles = append(extraFiles, f)
}
} else {
ic.WL = -1
}
// share config pipe
if r, w, err := os.Pipe(); err != nil {
fmsg.Fatalf("cannot pipe: %v", err)
@ -152,3 +168,30 @@ func main() {
}
}
}
func receiveWLfd(conn *net.UnixConn) (int, error) {
oob := make([]byte, syscall.CmsgSpace(4)) // single fd
if _, oobn, _, _, err := conn.ReadMsgUnix(nil, oob); err != nil {
return -1, err
} else if len(oob) != oobn {
return -1, errors.New("invalid message length")
}
var msg syscall.SocketControlMessage
if messages, err := syscall.ParseSocketControlMessage(oob); err != nil {
return -1, err
} else if len(messages) != 1 {
return -1, errors.New("unexpected message count")
} else {
msg = messages[0]
}
if fds, err := syscall.ParseUnixRights(&msg); err != nil {
return -1, err
} else if len(fds) != 1 {
return -1, errors.New("unexpected fd count")
} else {
return fds[0], nil
}
}

View File

@ -3,7 +3,6 @@ package helper
import (
"errors"
"io"
"os"
"os/exec"
"strconv"
"sync"
@ -20,8 +19,6 @@ type bubblewrap struct {
// bwrap pipes
p *pipes
// sync pipe
sync *os.File
// returns an array of arguments passed directly
// to the child process spawned by bwrap
argF func(argsFD, statFD int) []string
@ -75,11 +72,6 @@ func (b *bubblewrap) StartNotify(ready chan error) error {
b.Cmd.Env = append(b.Cmd.Env, FortifyHelper+"=1", FortifyStatus+"=-1")
}
if b.sync != nil {
b.Cmd.Args = append(b.Cmd.Args, "--sync-fd", strconv.Itoa(3+len(b.Cmd.ExtraFiles)))
b.Cmd.ExtraFiles = append(b.Cmd.ExtraFiles, b.sync)
}
if err := b.Cmd.Start(); err != nil {
return err
}
@ -139,7 +131,6 @@ func NewBwrap(conf *bwrap.Config, wt io.WriterTo, name string, argF func(argsFD,
b.p = &pipes{args: args}
}
b.sync = conf.Sync()
b.argF = argF
b.name = name
if wt != nil {

View File

@ -68,16 +68,13 @@ type Config struct {
// (--as-pid-1)
AsInit bool `json:"as_init"`
// keep this fd open while sandbox is running
// (--sync-fd FD)
sync *os.File
/* unmapped options include:
--unshare-user-try Create new user namespace if possible else continue by skipping it
--unshare-cgroup-try Create new cgroup namespace if possible else continue by skipping it
--userns FD Use this user namespace (cannot combine with --unshare-user)
--userns2 FD After setup switch to this user namespace, only useful with --userns
--pidns FD Use this pid namespace (as parent namespace if using --unshare-pid)
--sync-fd FD Keep this fd open while sandbox is running
--exec-label LABEL Exec label for the sandbox
--file-label LABEL File label for temporary sandbox content
--file FD DEST Copy from FD to destination DEST
@ -95,12 +92,6 @@ type Config struct {
among which --args is used internally for passing arguments */
}
// Sync keep this fd open while sandbox is running
// (--sync-fd FD)
func (c *Config) Sync() *os.File {
return c.sync
}
type UnshareConfig struct {
// (--unshare-user)
// create new user namespace

View File

@ -136,10 +136,3 @@ func (c *Config) SetGID(gid int) *Config {
}
return c
}
// SetSync sets the sync pipe kept open while sandbox is running
// (--sync-fd FD)
func (c *Config) SetSync(s *os.File) *Config {
c.sync = s
return c
}

View File

@ -10,7 +10,7 @@ import (
var testCasesNixos = []sealTestCase{
{
"nixos chromium direct wayland", new(stubNixOS),
"nixos chromium", new(stubNixOS),
&app.Config{
ID: "org.chromium.Chromium",
Command: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
@ -18,7 +18,7 @@ var testCasesNixos = []sealTestCase{
AppID: 1, Groups: []string{}, Username: "u0_a1",
Outer: "/var/lib/persist/module/fortify/0/1",
Sandbox: &app.SandboxConfig{
UserNS: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil,
UserNS: true, Net: true, MapRealUID: true, Env: nil,
Filesystem: []*app.FilesystemConfig{
{Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true},
{Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true},

View File

@ -248,8 +248,8 @@ var testCasesPd = []sealTestCase{
Ephemeral(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", acl.Execute).
WriteType(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/passwd", "chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n").
WriteType(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/group", "fortify:x:65534:\n").
Ensure("/tmp/fortify.1971/wayland", 0711).
Wayland("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
Link("/run/user/1971/wayland-0", "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/wayland").
UpdatePermType(system.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute).
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse").
CopyFile("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie").
MustProxyDBus("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", &dbus.Config{
@ -442,7 +442,7 @@ var testCasesPd = []sealTestCase{
Bind("/home/chronos", "/home/chronos", false, true).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/passwd", "/etc/passwd").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/group", "/etc/group").
Bind("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/65534/wayland-0").
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/65534/wayland-0").
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie", "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus").

View File

@ -62,8 +62,8 @@ type SandboxConfig struct {
NoNewSession bool `json:"no_new_session,omitempty"`
// map target user uid to privileged user uid in the user namespace
MapRealUID bool `json:"map_real_uid"`
// direct access to wayland socket
DirectWayland bool `json:"direct_wayland,omitempty"`
// mediated access to wayland socket
Wayland bool `json:"wayland,omitempty"`
// final environment variables
Env map[string]string `json:"env"`
@ -190,13 +190,13 @@ func Template() *Config {
Outer: "/var/lib/persist/home/org.chromium.Chromium",
Inner: "/var/lib/fortify",
Sandbox: &SandboxConfig{
Hostname: "localhost",
UserNS: true,
Net: true,
NoNewSession: true,
MapRealUID: true,
Dev: true,
DirectWayland: false,
Hostname: "localhost",
UserNS: true,
Net: true,
NoNewSession: true,
MapRealUID: true,
Dev: true,
Wayland: false,
// example API credentials pulled from Google Chrome
// DO NOT USE THESE IN A REAL BROWSER
Env: map[string]string{

View File

@ -8,6 +8,7 @@ import (
"regexp"
"strconv"
shim "git.ophivana.moe/security/fortify/cmd/fshim/ipc"
"git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/linux"
@ -28,6 +29,8 @@ var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z
type appSeal struct {
// app unique ID string representation
id string
// wayland mediation, disabled if nil
wl *shim.Wayland
// dbus proxy message buffer retriever
dbusMsg func(f func(msgbuf []string))
@ -45,8 +48,6 @@ type appSeal struct {
// pass-through enablement tracking from config
et system.Enablements
// wayland socket direct access
directWayland bool
// prevents sharing from happening twice
shared bool
@ -203,7 +204,6 @@ func (a *app) Seal(config *Config) error {
config.Confinement.Sandbox = conf
}
seal.directWayland = config.Confinement.Sandbox.DirectWayland
if b, err := config.Confinement.Sandbox.Bwrap(a.os); err != nil {
return err
} else {
@ -214,6 +214,12 @@ func (a *app) Seal(config *Config) error {
seal.sys.bwrap.SetEnv = make(map[string]string)
}
// create wayland struct and client wait channel if mediated wayland is enabled
// this field being set enables mediated wayland setup later on
if config.Confinement.Sandbox.Wayland {
seal.wl = shim.NewWayland()
}
// open process state store
// the simple store only starts holding an open file after first action
// store activity begins after Start is called and must end before Wait

View File

@ -31,36 +31,23 @@ func (seal *appSeal) shareDisplay(os linux.System) error {
// set up wayland
if seal.et.Has(system.EWayland) {
var wp string
if wd, ok := os.LookupEnv(waylandDisplay); !ok {
return fmsg.WrapError(ErrWayland,
"WAYLAND_DISPLAY is not set")
} else {
wp = path.Join(seal.RuntimePath, wd)
}
w := path.Join(seal.sys.runtime, "wayland-0")
seal.sys.bwrap.SetEnv[waylandDisplay] = w
if seal.directWayland {
} else if seal.wl == nil {
// hardlink wayland socket
wp := path.Join(seal.RuntimePath, wd)
wpi := path.Join(seal.shareLocal, "wayland")
w := path.Join(seal.sys.runtime, "wayland-0")
seal.sys.Link(wp, wpi)
seal.sys.bwrap.SetEnv[waylandDisplay] = w
seal.sys.bwrap.Bind(wpi, w)
// ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
seal.sys.UpdatePermType(system.EWayland, wp, acl.Read, acl.Write, acl.Execute)
} else {
wc := path.Join(seal.SharePath, "wayland")
wt := path.Join(wc, seal.id)
seal.sys.Ensure(wc, 0711)
appID := seal.fid
if appID == "" {
// use instance ID in case app id is not set
appID = "moe.ophivana.fortify." + seal.id
}
seal.sys.Wayland(wt, wp, appID, seal.id)
seal.sys.bwrap.Bind(wt, w)
// set wayland socket path for mediation (e.g. `/run/user/%d/wayland-%d`)
seal.wl.Path = path.Join(seal.RuntimePath, wd)
}
}

View File

@ -47,10 +47,12 @@ func (a *app) Start() error {
a.seal.sys.user.as,
a.seal.sys.user.supp,
path.Join(a.seal.share, "shim"),
a.seal.wl,
&shim0.Payload{
Argv: a.seal.command,
Exec: shimExec,
Bwrap: a.seal.sys.bwrap,
WL: a.seal.wl != nil,
Verbose: fmsg.Verbose(),
},
@ -62,9 +64,6 @@ func (a *app) Start() error {
}
a.seal.sys.needRevert = true
// export sync pipe from sys
a.seal.sys.bwrap.SetSync(a.seal.sys.Sync())
if startTime, err := a.shim.Start(); err != nil {
return err
} else {
@ -200,6 +199,13 @@ func (a *app) Wait() (int, error) {
})
}
// close wayland connection
if a.seal.wl != nil {
if err := a.seal.wl.Close(); err != nil {
fmsg.Println("cannot close wayland connection:", err)
}
}
// update store and revert app setup transaction
e := new(StateStoreError)
e.Inner, e.DoErr = a.seal.store.Do(func(b state.Backend) {

View File

@ -2,7 +2,6 @@ package system
import (
"errors"
"os"
"sync"
"git.ophivana.moe/security/fortify/internal/fmsg"
@ -57,7 +56,6 @@ func TypeString(e Enablement) string {
type I struct {
uid int
ops []Op
sp *os.File
state [2]bool
lock sync.Mutex
@ -67,10 +65,6 @@ func (sys *I) UID() int {
return sys.uid
}
func (sys *I) Sync() *os.File {
return sys.sp
}
func (sys *I) Equal(v *I) bool {
if v == nil || sys.uid != v.uid || len(sys.ops) != len(v.ops) {
return false

View File

@ -1,80 +0,0 @@
package system
import (
"errors"
"fmt"
"os"
"git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/wl"
)
// Wayland sets up a wayland socket with a security context attached.
func (sys *I) Wayland(dst, src, appID, instanceID string) *I {
sys.lock.Lock()
defer sys.lock.Unlock()
sys.ops = append(sys.ops, Wayland{[2]string{dst, src}, new(wl.Conn), appID, instanceID})
return sys
}
type Wayland struct {
pair [2]string
conn *wl.Conn
appID, instanceID string
}
func (w Wayland) Type() Enablement {
return Process
}
func (w Wayland) apply(sys *I) error {
if err := w.conn.Attach(w.pair[1]); err != nil {
return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot attach to wayland on %q:", w.pair[1]))
} else {
fmsg.VPrintf("wayland attached on %q", w.pair[1])
}
if sp, err := w.conn.Bind(w.pair[0], w.appID, w.instanceID); err != nil {
return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot bind to socket on %q:", w.pair[0]))
} else {
sys.sp = sp
fmsg.VPrintf("wayland listening on %q", w.pair[0])
return fmsg.WrapErrorSuffix(errors.Join(os.Chmod(w.pair[0], 0), acl.UpdatePerm(w.pair[0], sys.uid, acl.Read, acl.Write, acl.Execute)),
fmt.Sprintf("cannot chmod socket on %q:", w.pair[0]))
}
}
func (w Wayland) revert(_ *I, ec *Criteria) error {
if ec.hasType(w) {
fmsg.VPrintf("removing wayland socket on %q", w.pair[0])
if err := os.Remove(w.pair[0]); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
fmsg.VPrintf("detaching from wayland on %q", w.pair[1])
return fmsg.WrapErrorSuffix(w.conn.Close(),
fmt.Sprintf("cannot detach from wayland on %q:", w.pair[1]))
} else {
fmsg.VPrintf("skipping wayland cleanup on %q", w.pair[0])
return nil
}
}
func (w Wayland) Is(o Op) bool {
w0, ok := o.(Wayland)
return ok && w.pair == w0.pair
}
func (w Wayland) Path() string {
return w.pair[0]
}
func (w Wayland) String() string {
return fmt.Sprintf("wayland socket at %q", w.pair[0])
}