aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.gitea/workflows/release.yml4
-rw-r--r--.gitea/workflows/test.yml4
-rw-r--r--cmd/finit/main.go2
-rw-r--r--cmd/fshim/ipc/shim/shim.go51
-rw-r--r--cmd/fshim/main.go19
-rw-r--r--cmd/fsu/main.go114
-rw-r--r--cmd/fsu/parse.go77
-rw-r--r--internal/app/app.go2
-rw-r--r--internal/app/app_nixos_test.go66
-rw-r--r--internal/app/config.go20
-rw-r--r--internal/app/launch.machinectl.go57
-rw-r--r--internal/app/launch.sudo.go30
-rw-r--r--internal/app/seal.go114
-rw-r--r--internal/app/share.display.go2
-rw-r--r--internal/app/share.system.go24
-rw-r--r--internal/app/start.go55
-rw-r--r--internal/app/system.go24
-rw-r--r--internal/fmsg/defer.go3
-rw-r--r--internal/linux/interface.go8
-rw-r--r--internal/linux/std.go85
-rw-r--r--internal/path.go1
-rw-r--r--internal/state/print.go12
-rw-r--r--internal/state/state.go2
-rw-r--r--main.go43
-rw-r--r--package.nix3
25 files changed, 447 insertions, 375 deletions
diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml
index b7b3e0f3..625de0d3 100644
--- a/.gitea/workflows/release.yml
+++ b/.gitea/workflows/release.yml
@@ -33,9 +33,9 @@ jobs:
go build -v -ldflags '-s -w
-X git.ophivana.moe/security/fortify/internal.Version=${{ github.ref_name }}
-X git.ophivana.moe/security/fortify/internal.Fsu=/usr/bin/fsu
- -X git.ophivana.moe/security/fortify/internal.Fshim=/usr/libexec/fortify/fshim
-X git.ophivana.moe/security/fortify/internal.Finit=/usr/libexec/fortify/finit
- -X main.Fmain=/usr/bin/fortify'
+ -X main.Fmain=/usr/bin/fortify
+ -X main.Fshim=/usr/libexec/fortify/fshim'
-o bin/ ./... &&
(cd bin && sha512sum --tag -b * > sha512sums)
diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml
index 70e51d37..f04ddf3e 100644
--- a/.gitea/workflows/test.yml
+++ b/.gitea/workflows/test.yml
@@ -36,8 +36,8 @@ jobs:
go build -v -ldflags '-s -w
-X git.ophivana.moe/security/fortify/internal.Version=${{ github.ref_name }}
-X git.ophivana.moe/security/fortify/internal.Fsu=/usr/bin/fsu
- -X git.ophivana.moe/security/fortify/internal.Fshim=/usr/libexec/fortify/fshim
-X git.ophivana.moe/security/fortify/internal.Finit=/usr/libexec/fortify/finit
- -X main.Fmain=/usr/bin/fortify'
+ -X main.Fmain=/usr/bin/fortify
+ -X main.Fshim=/usr/libexec/fortify/fshim'
-o bin/ ./... &&
(cd bin && sha512sum --tag -b * > sha512sums)
diff --git a/cmd/finit/main.go b/cmd/finit/main.go
index e92af811..da3eaea3 100644
--- a/cmd/finit/main.go
+++ b/cmd/finit/main.go
@@ -103,7 +103,7 @@ func main() {
if err := cmd.Start(); err != nil {
fmsg.Fatalf("cannot start %q: %v", payload.Argv0, err)
}
- fmsg.Withhold()
+ fmsg.Suspend()
// close setup pipe as setup is now complete
if err := setup.Close(); err != nil {
diff --git a/cmd/fshim/ipc/shim/shim.go b/cmd/fshim/ipc/shim/shim.go
index e1205b08..a03b6003 100644
--- a/cmd/fshim/ipc/shim/shim.go
+++ b/cmd/fshim/ipc/shim/shim.go
@@ -5,6 +5,7 @@ import (
"net"
"os"
"os/exec"
+ "strings"
"sync"
"sync/atomic"
"syscall"
@@ -12,6 +13,7 @@ import (
"git.ophivana.moe/security/fortify/acl"
shim0 "git.ophivana.moe/security/fortify/cmd/fshim/ipc"
+ "git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
@@ -24,24 +26,26 @@ type Shim struct {
cmd *exec.Cmd
// uid of shim target user
uid uint32
- // whether to check shim pid
- checkPid bool
- // user switcher executable path
- executable string
+ // string representation of application id
+ aid string
+ // string representation of supplementary group ids
+ supp []string
// path to setup socket
socket string
// shim setup abort reason and completion
abort chan error
abortErr atomic.Pointer[error]
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(executable string, uid uint32, socket string, wl *shim0.Wayland, payload *shim0.Payload, checkPid bool) *Shim {
- return &Shim{uid: uid, executable: executable, socket: socket, wl: wl, payload: payload, checkPid: checkPid}
+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 {
@@ -68,9 +72,11 @@ func (s *Shim) AbortWait(err error) {
<-s.abort
}
-type CommandBuilder func(shimEnv string) (args []string)
+func (s *Shim) WaitFallback() chan error {
+ return s.killFallback
+}
-func (s *Shim) Start(f CommandBuilder) (*time.Time, error) {
+func (s *Shim) Start() (*time.Time, error) {
var (
cf chan *net.UnixConn
accept func()
@@ -87,22 +93,37 @@ func (s *Shim) Start(f CommandBuilder) (*time.Time, error) {
}
// start user switcher process and save time
- s.cmd = exec.Command(s.executable, f(shim0.Env+"="+s.socket)...)
- s.cmd.Env = []string{}
+ var fsu string
+ if p, ok := internal.Check(internal.Fsu); !ok {
+ fmsg.Fatal("invalid fsu path, this copy of fshim is not compiled correctly")
+ panic("unreachable")
+ } else {
+ fsu = p
+ }
+ s.cmd = exec.Command(fsu)
+ s.cmd.Env = []string{
+ shim0.Env + "=" + s.socket,
+ "FORTIFY_APP_ID=" + s.aid,
+ }
+ if len(s.supp) > 0 {
+ fmsg.VPrintf("attaching supplementary group ids %s", s.supp)
+ s.cmd.Env = append(s.cmd.Env, "FORTIFY_GROUPS="+strings.Join(s.supp, " "))
+ }
s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
s.cmd.Dir = "/"
- fmsg.VPrintln("starting shim via user switcher:", s.cmd)
- fmsg.Withhold() // withhold messages to stderr
+ fmsg.VPrintln("starting shim via fsu:", s.cmd)
+ fmsg.Suspend() // withhold messages to stderr
if err := s.cmd.Start(); err != nil {
return nil, fmsg.WrapErrorSuffix(err,
- "cannot start user switcher:")
+ "cannot start fsu:")
}
startTime := time.Now().UTC()
// kill shim if something goes wrong and an error is returned
+ s.killFallback = make(chan error, 1)
killShim := func() {
if err := s.cmd.Process.Signal(os.Interrupt); err != nil {
- fmsg.Println("cannot terminate shim on faulted setup:", err)
+ s.killFallback <- err
}
}
defer func() { killShim() }()
@@ -132,7 +153,7 @@ func (s *Shim) Start(f CommandBuilder) (*time.Time, error) {
err = errors.New("compromised fortify build")
s.Abort(err)
return &startTime, err
- } else if s.checkPid && cred.Pid != int32(s.cmd.Process.Pid) {
+ } else if cred.Pid != int32(s.cmd.Process.Pid) {
fmsg.Printf("process %d tried to connect to shim setup socket, expecting shim %d",
cred.Pid, s.cmd.Process.Pid)
err = errors.New("compromised target user")
diff --git a/cmd/fshim/main.go b/cmd/fshim/main.go
index 43248f69..2322984a 100644
--- a/cmd/fshim/main.go
+++ b/cmd/fshim/main.go
@@ -58,7 +58,7 @@ func main() {
// dial setup socket
var conn *net.UnixConn
if c, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: socketPath, Net: "unix"}); err != nil {
- fmsg.Fatal("cannot dial setup socket:", err)
+ fmsg.Fatal(err.Error())
panic("unreachable")
} else {
conn = c
@@ -67,7 +67,7 @@ func main() {
// decode payload gob stream
var payload shim.Payload
if err := gob.NewDecoder(conn).Decode(&payload); err != nil {
- fmsg.Fatal("cannot decode shim payload:", err)
+ fmsg.Fatalf("cannot decode shim payload: %v", err)
} else {
fmsg.SetVerbose(payload.Verbose)
}
@@ -80,7 +80,7 @@ func main() {
wfd := -1
if payload.WL {
if fd, err := receiveWLfd(conn); err != nil {
- fmsg.Fatal("cannot receive wayland fd:", err)
+ fmsg.Fatalf("cannot receive wayland fd: %v", err)
} else {
wfd = fd
}
@@ -102,7 +102,10 @@ func main() {
} else {
// no argv, look up shell instead
var ok bool
- if ic.Argv0, ok = os.LookupEnv("SHELL"); !ok {
+ if payload.Bwrap.SetEnv == nil {
+ fmsg.Fatal("no command was specified and environment is unset")
+ }
+ if ic.Argv0, ok = payload.Bwrap.SetEnv["SHELL"]; !ok {
fmsg.Fatal("no command was specified and $SHELL was unset")
}
@@ -125,7 +128,7 @@ func main() {
// share config pipe
if r, w, err := os.Pipe(); err != nil {
- fmsg.Fatal("cannot pipe:", err)
+ fmsg.Fatalf("cannot pipe: %v", err)
} else {
conf.SetEnv[init0.Env] = strconv.Itoa(3 + len(extraFiles))
extraFiles = append(extraFiles, r)
@@ -134,7 +137,7 @@ func main() {
go func() {
// stream config to pipe
if err = gob.NewEncoder(w).Encode(&ic); err != nil {
- fmsg.Fatal("cannot transmit init config:", err)
+ fmsg.Fatalf("cannot transmit init config: %v", err)
}
}()
}
@@ -142,7 +145,7 @@ func main() {
helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
if b, err := helper.NewBwrap(conf, nil, finitPath,
func(int, int) []string { return make([]string, 0) }); err != nil {
- fmsg.Fatal("malformed sandbox config:", err)
+ fmsg.Fatalf("malformed sandbox config: %v", err)
} else {
cmd := b.Unwrap()
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
@@ -154,7 +157,7 @@ func main() {
// run and pass through exit code
if err = b.Start(); err != nil {
- fmsg.Fatal("cannot start target process:", err)
+ fmsg.Fatalf("cannot start target process: %v", err)
} else if err = b.Wait(); err != nil {
fmsg.VPrintln("wait:", err)
}
diff --git a/cmd/fsu/main.go b/cmd/fsu/main.go
index 0ba5a134..1ec3a4bc 100644
--- a/cmd/fsu/main.go
+++ b/cmd/fsu/main.go
@@ -1,10 +1,12 @@
package main
import (
- "bufio"
+ "bytes"
+ "fmt"
"log"
"os"
"path"
+ "slices"
"strconv"
"strings"
"syscall"
@@ -15,11 +17,15 @@ const (
fsuConfFile = "/etc/fsurc"
envShim = "FORTIFY_SHIM"
envAID = "FORTIFY_APP_ID"
+ envGroups = "FORTIFY_GROUPS"
PR_SET_NO_NEW_PRIVS = 0x26
)
-var Fmain = compPoison
+var (
+ Fmain = compPoison
+ Fshim = compPoison
+)
func main() {
log.SetFlags(0)
@@ -35,12 +41,17 @@ func main() {
log.Fatal("this program must not be started by root")
}
- var fmain string
+ var fmain, fshim string
if p, ok := checkPath(Fmain); !ok {
log.Fatal("invalid fortify path, this copy of fsu is not compiled correctly")
} else {
fmain = p
}
+ if p, ok := checkPath(Fshim); !ok {
+ log.Fatal("invalid fshim path, this copy of fsu is not compiled correctly")
+ } else {
+ fshim = p
+ }
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
if p, err := os.Readlink(pexe); err != nil {
@@ -63,87 +74,76 @@ func main() {
uid += fid * 10000
}
+ // allowed aid range 0 to 9999
+ if as, ok := os.LookupEnv(envAID); !ok {
+ log.Fatal("FORTIFY_APP_ID not set")
+ } else if aid, err := parseUint32Fast(as); err != nil || aid < 0 || aid > 9999 {
+ log.Fatal("invalid aid")
+ } else {
+ uid += aid
+ }
+
// pass through setup path to shim
var shimSetupPath string
if s, ok := os.LookupEnv(envShim); !ok {
- log.Fatal("FORTIFY_SHIM not set")
+ // fortify requests target uid
+ // print resolved uid and exit
+ fmt.Print(uid)
+ os.Exit(0)
} else if !path.IsAbs(s) {
log.Fatal("FORTIFY_SHIM is not absolute")
} else {
shimSetupPath = s
}
- // allowed aid range 0 to 9999
- if as, ok := os.LookupEnv(envAID); !ok {
- log.Fatal("FORTIFY_APP_ID not set")
- } else if aid, err := strconv.Atoi(as); err != nil || aid < 0 || aid > 9999 {
- log.Fatal("invalid aid")
+ // supplementary groups
+ var suppGroups, suppCurrent []int
+
+ if gs, ok := os.LookupEnv(envGroups); ok {
+ if cur, err := os.Getgroups(); err != nil {
+ log.Fatalf("cannot get groups: %v", err)
+ } else {
+ suppCurrent = cur
+ }
+
+ // parse space-separated list of group ids
+ gss := bytes.Split([]byte(gs), []byte{' '})
+ suppGroups = make([]int, len(gss)+1)
+ for i, s := range gss {
+ if gid, err := strconv.Atoi(string(s)); err != nil {
+ log.Fatalf("cannot parse %q: %v", string(s), err)
+ } else if gid > 0 && gid != uid && gid != os.Getgid() && slices.Contains(suppCurrent, gid) {
+ suppGroups[i] = gid
+ } else {
+ log.Fatalf("invalid gid %d", gid)
+ }
+ }
+ suppGroups[len(suppGroups)-1] = uid
} else {
- uid += aid
+ suppGroups = []int{uid}
}
+ // careful! users in the allowlist is effectively allowed to drop groups via fsu
+
if err := syscall.Setresgid(uid, uid, uid); err != nil {
log.Fatalf("cannot set gid: %v", err)
}
+ if err := syscall.Setgroups(suppGroups); err != nil {
+ log.Fatalf("cannot set supplementary groups: %v", err)
+ }
if err := syscall.Setresuid(uid, uid, uid); err != nil {
log.Fatalf("cannot set uid: %v", err)
}
if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
}
- if err := syscall.Exec(fmain, []string{"fortify", "shim"}, []string{envShim + "=" + shimSetupPath}); err != nil {
+ if err := syscall.Exec(fshim, []string{"fshim"}, []string{envShim + "=" + shimSetupPath}); err != nil {
log.Fatalf("cannot start shim: %v", err)
}
panic("unreachable")
}
-func parseConfig(p string, puid int) (fid int, ok bool) {
- // refuse to run if fsurc is not protected correctly
- if s, err := os.Stat(p); err != nil {
- log.Fatal(err)
- } else if s.Mode().Perm() != 0400 {
- log.Fatal("bad fsurc perm")
- } else if st := s.Sys().(*syscall.Stat_t); st.Uid != 0 || st.Gid != 0 {
- log.Fatal("fsurc must be owned by uid 0")
- }
-
- if r, err := os.Open(p); err != nil {
- log.Fatal(err)
- return -1, false
- } else {
- s := bufio.NewScanner(r)
- var line int
- for s.Scan() {
- line++
-
- // <puid> <fid>
- lf := strings.SplitN(s.Text(), " ", 2)
- if len(lf) != 2 {
- log.Fatalf("invalid entry on line %d", line)
- }
-
- var puid0 int
- if puid0, err = strconv.Atoi(lf[0]); err != nil || puid0 < 1 {
- log.Fatalf("invalid parent uid on line %d", line)
- }
-
- ok = puid0 == puid
- if ok {
- // allowed fid range 0 to 99
- if fid, err = strconv.Atoi(lf[1]); err != nil || fid < 0 || fid > 99 {
- log.Fatalf("invalid fortify uid on line %d", line)
- }
- return
- }
- }
- if err = s.Err(); err != nil {
- log.Fatalf("cannot read fsurc: %v", err)
- }
- return -1, false
- }
-}
-
func checkPath(p string) (string, bool) {
return p, p != compPoison && p != "" && path.IsAbs(p)
}
diff --git a/cmd/fsu/parse.go b/cmd/fsu/parse.go
new file mode 100644
index 00000000..d0298357
--- /dev/null
+++ b/cmd/fsu/parse.go
@@ -0,0 +1,77 @@
+package main
+
+import (
+ "bufio"
+ "errors"
+ "fmt"
+ "log"
+ "os"
+ "strings"
+ "syscall"
+)
+
+func parseUint32Fast(s string) (int, error) {
+ sLen := len(s)
+ if sLen < 1 {
+ return -1, errors.New("zero length string")
+ }
+ if sLen > 10 {
+ return -1, errors.New("string too long")
+ }
+
+ n := 0
+ for i, ch := range []byte(s) {
+ ch -= '0'
+ if ch > 9 {
+ return -1, fmt.Errorf("invalid character '%s' at index %d", string([]byte{ch}), i)
+ }
+ n = n*10 + int(ch)
+ }
+ return n, nil
+}
+
+func parseConfig(p string, puid int) (fid int, ok bool) {
+ // refuse to run if fsurc is not protected correctly
+ if s, err := os.Stat(p); err != nil {
+ log.Fatal(err)
+ } else if s.Mode().Perm() != 0400 {
+ log.Fatal("bad fsurc perm")
+ } else if st := s.Sys().(*syscall.Stat_t); st.Uid != 0 || st.Gid != 0 {
+ log.Fatal("fsurc must be owned by uid 0")
+ }
+
+ if r, err := os.Open(p); err != nil {
+ log.Fatal(err)
+ return -1, false
+ } else {
+ s := bufio.NewScanner(r)
+ var line int
+ for s.Scan() {
+ line++
+
+ // <puid> <fid>
+ lf := strings.SplitN(s.Text(), " ", 2)
+ if len(lf) != 2 {
+ log.Fatalf("invalid entry on line %d", line)
+ }
+
+ var puid0 int
+ if puid0, err = parseUint32Fast(lf[0]); err != nil || puid0 < 1 {
+ log.Fatalf("invalid parent uid on line %d", line)
+ }
+
+ ok = puid0 == puid
+ if ok {
+ // allowed fid range 0 to 99
+ if fid, err = parseUint32Fast(lf[1]); err != nil || fid < 0 || fid > 99 {
+ log.Fatalf("invalid fortify uid on line %d", line)
+ }
+ return
+ }
+ }
+ if err = s.Err(); err != nil {
+ log.Fatalf("cannot read fsurc: %v", err)
+ }
+ return -1, false
+ }
+}
diff --git a/internal/app/app.go b/internal/app/app.go
index 28564ebf..3260deab 100644
--- a/internal/app/app.go
+++ b/internal/app/app.go
@@ -53,7 +53,7 @@ func (a *app) String() string {
}
if a.seal != nil {
- return "(sealed fortified app as uid " + a.seal.sys.user.Uid + ")"
+ return "(sealed fortified app as uid " + a.seal.sys.user.us + ")"
}
return "(unsealed fortified app)"
diff --git a/internal/app/app_nixos_test.go b/internal/app/app_nixos_test.go
index a154486d..4a350caf 100644
--- a/internal/app/app_nixos_test.go
+++ b/internal/app/app_nixos_test.go
@@ -19,9 +19,12 @@ var testCasesNixos = []sealTestCase{
{
"nixos permissive defaults no enablements", new(stubNixOS),
&app.Config{
- User: "chronos",
Command: make([]string, 0),
- Method: "sudo",
+ Confinement: app.ConfinementConfig{
+ AppID: 0,
+ Username: "chronos",
+ Home: "/home/chronos",
+ },
},
app.ID{
0x4a, 0x45, 0x0b, 0x65,
@@ -29,11 +32,11 @@ var testCasesNixos = []sealTestCase{
0xbd, 0x01, 0x78, 0x0e,
0xb9, 0xa6, 0x07, 0xac,
},
- system.New(150).
- Ensure("/tmp/fortify.1971", 0701).
- Ephemeral(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac", 0701).
+ system.New(1000000).
+ Ensure("/tmp/fortify.1971", 0711).
+ Ephemeral(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac", 0711).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
- Ensure("/tmp/fortify.1971/tmpdir/150", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/150", acl.Read, acl.Write, acl.Execute).
+ Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute).
@@ -43,6 +46,7 @@ var testCasesNixos = []sealTestCase{
Net: true,
UserNS: true,
Clearenv: true,
+ Chdir: "/home/chronos",
SetEnv: map[string]string{
"HOME": "/home/chronos",
"SHELL": "/run/current-system/sw/bin/zsh",
@@ -182,10 +186,11 @@ var testCasesNixos = []sealTestCase{
Symlink("/fortify/etc/zprofile", "/etc/zprofile").
Symlink("/fortify/etc/zshenv", "/etc/zshenv").
Symlink("/fortify/etc/zshrc", "/etc/zshrc").
- Bind("/tmp/fortify.1971/tmpdir/150", "/tmp", false, true).
+ Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", false, true).
Tmpfs("/tmp/fortify.1971", 1048576).
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/65534", 8388608).
+ Bind("/home/chronos", "/home/chronos", false, true).
Bind("/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/passwd", "/etc/passwd").
Bind("/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/group", "/etc/group").
Tmpfs("/var/run/nscd", 8192),
@@ -194,9 +199,12 @@ var testCasesNixos = []sealTestCase{
"nixos permissive defaults chromium", new(stubNixOS),
&app.Config{
ID: "org.chromium.Chromium",
- User: "chronos",
Command: []string{"/run/current-system/sw/bin/zsh", "-c", "exec chromium "},
Confinement: app.ConfinementConfig{
+ AppID: 9,
+ Groups: []string{"video"},
+ Username: "chronos",
+ Home: "/home/chronos",
SessionBus: &dbus.Config{
Talk: []string{
"org.freedesktop.Notifications",
@@ -230,7 +238,6 @@ var testCasesNixos = []sealTestCase{
},
Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(),
},
- Method: "systemd",
},
app.ID{
0xeb, 0xf0, 0x83, 0xd1,
@@ -238,11 +245,11 @@ var testCasesNixos = []sealTestCase{
0x82, 0xd4, 0x13, 0x36,
0x9b, 0x64, 0xce, 0x7c,
},
- system.New(150).
- Ensure("/tmp/fortify.1971", 0701).
- Ephemeral(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0701).
+ system.New(1000009).
+ Ensure("/tmp/fortify.1971", 0711).
+ Ephemeral(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0711).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
- Ensure("/tmp/fortify.1971/tmpdir/150", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/150", acl.Read, acl.Write, acl.Execute).
+ Ensure("/tmp/fortify.1971/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/9", acl.Read, acl.Write, acl.Execute).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", acl.Execute).
@@ -287,6 +294,7 @@ var testCasesNixos = []sealTestCase{
(&bwrap.Config{
Net: true,
UserNS: true,
+ Chdir: "/home/chronos",
Clearenv: true,
SetEnv: map[string]string{
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/65534/bus",
@@ -434,10 +442,11 @@ var testCasesNixos = []sealTestCase{
Symlink("/fortify/etc/zprofile", "/etc/zprofile").
Symlink("/fortify/etc/zshenv", "/etc/zshenv").
Symlink("/fortify/etc/zshrc", "/etc/zshrc").
- Bind("/tmp/fortify.1971/tmpdir/150", "/tmp", false, true).
+ Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", false, true).
Tmpfs("/tmp/fortify.1971", 1048576).
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/65534", 8388608).
+ Bind("/home/chronos", "/home/chronos", false, true).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/passwd", "/etc/passwd").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/group", "/etc/group").
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/65534/wayland-0").
@@ -504,23 +513,12 @@ func (s *stubNixOS) Executable() (string, error) {
return "/home/ophestra/.nix-profile/bin/fortify", nil
}
-func (s *stubNixOS) Lookup(username string) (*user.User, error) {
- if s.usernameErr != nil {
- if err, ok := s.usernameErr[username]; ok {
- return nil, err
- }
- }
-
- switch username {
- case "chronos":
- return &user.User{
- Uid: "150",
- Gid: "101",
- Username: "chronos",
- HomeDir: "/home/chronos",
- }, nil
+func (s *stubNixOS) LookupGroup(name string) (*user.Group, error) {
+ switch name {
+ case "video":
+ return &user.Group{Gid: "26", Name: "video"}, nil
default:
- return nil, user.UnknownUserError(username)
+ return nil, user.UnknownGroupError(name)
}
}
@@ -586,10 +584,6 @@ func (s *stubNixOS) Stdout() io.Writer {
panic("requested stdout")
}
-func (s *stubNixOS) FshimPath() string {
- return "/nix/store/00000000000000000000000000000000-fortify-0.0.10/bin/.fshim"
-}
-
func (s *stubNixOS) Paths() linux.Paths {
return linux.Paths{
SharePath: "/tmp/fortify.1971",
@@ -598,6 +592,10 @@ func (s *stubNixOS) Paths() linux.Paths {
}
}
+func (s *stubNixOS) Uid(aid int) (int, error) {
+ return 1000000 + 0*10000 + aid, nil
+}
+
func (s *stubNixOS) SdBooted() bool {
return true
}
diff --git a/internal/app/config.go b/internal/app/config.go
index 96c51ce7..dd45b023 100644
--- a/internal/app/config.go
+++ b/internal/app/config.go
@@ -15,12 +15,8 @@ const fTmp = "/fortify"
type Config struct {
// D-Bus application ID
ID string `json:"id"`
- // username of the target user to switch to
- User string `json:"user"`
// value passed through to the child process as its argv
Command []string `json:"command"`
- // string representation of the child's launch method
- Method string `json:"method"`
// child confinement configuration
Confinement ConfinementConfig `json:"confinement"`
@@ -28,6 +24,14 @@ type Config struct {
// ConfinementConfig defines fortified child's confinement
type ConfinementConfig struct {
+ // numerical application id, determines uid in the init namespace
+ AppID int `json:"app_id"`
+ // list of supplementary groups to inherit
+ Groups []string `json:"groups"`
+ // passwd username in the sandbox, defaults to chronos
+ Username string `json:"username,omitempty"`
+ // home directory in sandbox
+ Home string `json:"home"`
// bwrap sandbox confinement configuration
Sandbox *SandboxConfig `json:"sandbox"`
@@ -169,8 +173,7 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
// Template returns a fully populated instance of Config.
func Template() *Config {
return &Config{
- ID: "org.chromium.Chromium",
- User: "chronos",
+ ID: "org.chromium.Chromium",
Command: []string{
"chromium",
"--ignore-gpu-blocklist",
@@ -178,8 +181,11 @@ func Template() *Config {
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland",
},
- Method: "sudo",
Confinement: ConfinementConfig{
+ AppID: 9,
+ Groups: []string{"video"},
+ Username: "chronos",
+ Home: "/var/lib/persist/home/org.chromium.Chromium",
Sandbox: &SandboxConfig{
Hostname: "localhost",
UserNS: true,
diff --git a/internal/app/launch.machinectl.go b/internal/app/launch.machinectl.go
deleted file mode 100644
index 3c196b84..00000000
--- a/internal/app/launch.machinectl.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package app
-
-import (
- "strings"
-
- "git.ophivana.moe/security/fortify/internal/fmsg"
-)
-
-func (a *app) commandBuilderMachineCtl(shimEnv string) (args []string) {
- args = make([]string, 0, 9+len(a.seal.sys.bwrap.SetEnv))
-
- // shell --uid=$USER
- args = append(args, "shell", "--uid="+a.seal.sys.user.Username)
-
- // --quiet
- if !fmsg.Verbose() {
- args = append(args, "--quiet")
- }
-
- // environ
- envQ := make([]string, 0, len(a.seal.sys.bwrap.SetEnv)+1)
- for k, v := range a.seal.sys.bwrap.SetEnv {
- envQ = append(envQ, "-E"+k+"="+v)
- }
- // add shim payload to environment for shim path
- envQ = append(envQ, "-E"+shimEnv)
- args = append(args, envQ...)
-
- // -- .host
- args = append(args, "--", ".host")
-
- // /bin/sh -c
- if sh, err := a.os.LookPath("sh"); err != nil {
- // hardcode /bin/sh path since it exists more often than not
- args = append(args, "/bin/sh", "-c")
- } else {
- args = append(args, sh, "-c")
- }
-
- // build inner command expression ran as target user
- innerCommand := strings.Builder{}
-
- // apply custom environment variables to activation environment
- innerCommand.WriteString("dbus-update-activation-environment --systemd")
- for k := range a.seal.sys.bwrap.SetEnv {
- innerCommand.WriteString(" " + k)
- }
- innerCommand.WriteString("; ")
-
- // launch fortify shim
- innerCommand.WriteString("exec " + a.os.FshimPath())
-
- // append inner command
- args = append(args, innerCommand.String())
-
- return
-}
diff --git a/internal/app/launch.sudo.go b/internal/app/launch.sudo.go
deleted file mode 100644
index dbb49318..00000000
--- a/internal/app/launch.sudo.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package app
-
-import (
- "git.ophivana.moe/security/fortify/internal/fmsg"
-)
-
-const (
- sudoAskPass = "SUDO_ASKPASS"
-)
-
-func (a *app) commandBuilderSudo(shimEnv string) (args []string) {
- args = make([]string, 0, 8)
-
- // -Hiu $USER
- args = append(args, "-Hiu", a.seal.sys.user.Username)
-
- // -A?
- if _, ok := a.os.LookupEnv(sudoAskPass); ok {
- fmsg.VPrintln(sudoAskPass, "set, adding askpass flag")
- args = append(args, "-A")
- }
-
- // shim payload
- args = append(args, shimEnv)
-
- // -- $@
- args = append(args, "--", a.os.FshimPath())
-
- return
-}
diff --git a/internal/app/seal.go b/internal/app/seal.go
index 0846a7f8..f01061af 100644
--- a/internal/app/seal.go
+++ b/internal/app/seal.go
@@ -2,8 +2,8 @@ package app
import (
"errors"
+ "fmt"
"io/fs"
- "os/user"
"path"
"strconv"
@@ -15,24 +15,10 @@ import (
"git.ophivana.moe/security/fortify/internal/system"
)
-const (
- LaunchMethodSudo uint8 = iota
- LaunchMethodMachineCtl
-)
-
-var method = [...]string{
- LaunchMethodSudo: "sudo",
- LaunchMethodMachineCtl: "systemd",
-}
-
var (
ErrConfig = errors.New("no configuration to seal")
- ErrUser = errors.New("unknown user")
- ErrLaunch = errors.New("invalid launch method")
-
- ErrSudo = errors.New("sudo not available")
- ErrSystemd = errors.New("systemd not available")
- ErrMachineCtl = errors.New("machinectl not available")
+ ErrUser = errors.New("invalid aid")
+ ErrHome = errors.New("invalid home directory")
)
// appSeal seals the application with child-related information
@@ -51,15 +37,11 @@ type appSeal struct {
// persistent process state store
store state.Store
- // uint8 representation of launch method sealed from config
- launchOption uint8
// process-specific share directory path
share string
// process-specific share directory path local to XDG_RUNTIME_DIR
shareLocal string
- // path to launcher program
- toolPath string
// pass-through enablement tracking from config
et system.Enablements
@@ -98,34 +80,6 @@ func (a *app) Seal(config *Config) error {
seal.fid = config.ID
seal.command = config.Command
- // parses launch method text and looks up tool path
- switch config.Method {
- case method[LaunchMethodSudo]:
- seal.launchOption = LaunchMethodSudo
- if sudoPath, err := a.os.LookPath("sudo"); err != nil {
- return fmsg.WrapError(ErrSudo,
- "sudo not found")
- } else {
- seal.toolPath = sudoPath
- }
- case method[LaunchMethodMachineCtl]:
- seal.launchOption = LaunchMethodMachineCtl
- if !a.os.SdBooted() {
- return fmsg.WrapError(ErrSystemd,
- "system has not been booted with systemd as init system")
- }
-
- if machineCtlPath, err := a.os.LookPath("machinectl"); err != nil {
- return fmsg.WrapError(ErrMachineCtl,
- "machinectl not found")
- } else {
- seal.toolPath = machineCtlPath
- }
- default:
- return fmsg.WrapError(ErrLaunch,
- "invalid launch method")
- }
-
// create seal system component
seal.sys = new(appSealSys)
@@ -138,16 +92,44 @@ func (a *app) Seal(config *Config) error {
seal.sys.mappedIDString = strconv.Itoa(seal.sys.mappedID)
seal.sys.runtime = path.Join("/run/user", seal.sys.mappedIDString)
- // look up user from system
- if u, err := a.os.Lookup(config.User); err != nil {
- if errors.As(err, new(user.UnknownUserError)) {
- return fmsg.WrapError(ErrUser, "unknown user", config.User)
+ // validate uid and set user info
+ if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 {
+ return fmsg.WrapError(ErrUser,
+ fmt.Sprintf("aid %d out of range", config.Confinement.AppID))
+ } else {
+ seal.sys.user = appUser{
+ aid: config.Confinement.AppID,
+ as: strconv.Itoa(config.Confinement.AppID),
+ home: config.Confinement.Home,
+ username: config.Confinement.Username,
+ }
+ if seal.sys.user.username == "" {
+ seal.sys.user.username = "chronos"
+ }
+ if seal.sys.user.home == "" || !path.IsAbs(seal.sys.user.home) {
+ return fmsg.WrapError(ErrHome,
+ fmt.Sprintf("invalid home directory %q", seal.sys.user.home))
+ }
+
+ // invoke fsu for full uid
+ if u, err := a.os.Uid(seal.sys.user.aid); err != nil {
+ return fmsg.WrapErrorSuffix(err,
+ "cannot obtain uid from fsu:")
} else {
- // unreachable
- panic(err)
+ seal.sys.user.uid = u
+ seal.sys.user.us = strconv.Itoa(u)
+ }
+
+ // resolve supplementary group ids from names
+ seal.sys.user.supp = make([]string, len(config.Confinement.Groups))
+ for i, name := range config.Confinement.Groups {
+ if g, err := a.os.LookupGroup(name); err != nil {
+ return fmsg.WrapError(err,
+ fmt.Sprintf("unknown group %q", name))
+ } else {
+ seal.sys.user.supp[i] = g.Gid
+ }
}
- } else {
- seal.sys.user = u
}
// map sandbox config to bwrap
@@ -230,15 +212,10 @@ func (a *app) Seal(config *Config) error {
// 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
- seal.store = state.NewSimple(seal.RunDirPath, seal.sys.user.Uid)
+ seal.store = state.NewSimple(seal.RunDirPath, seal.sys.user.as)
- // parse string UID
- if u, err := strconv.Atoi(seal.sys.user.Uid); err != nil {
- // unreachable unless kernel bug
- panic("uid parse")
- } else {
- seal.sys.I = system.New(u)
- }
+ // initialise system interface with full uid
+ seal.sys.I = system.New(seal.sys.user.uid)
// pass through enablements
seal.et = config.Confinement.Enablements
@@ -249,11 +226,8 @@ func (a *app) Seal(config *Config) error {
}
// verbose log seal information
- fmsg.VPrintln("created application seal as user",
- seal.sys.user.Username, "("+seal.sys.user.Uid+"),",
- "method:", config.Method+",",
- "launcher:", seal.toolPath+",",
- "command:", config.Command)
+ fmsg.VPrintf("created application seal for uid %s (%s) groups: %v, command: %s",
+ seal.sys.user.us, seal.sys.user.username, config.Confinement.Groups, config.Command)
// seal app and release lock
a.seal = seal
diff --git a/internal/app/share.display.go b/internal/app/share.display.go
index 7e814a83..073a29fe 100644
--- a/internal/app/share.display.go
+++ b/internal/app/share.display.go
@@ -58,7 +58,7 @@ func (seal *appSeal) shareDisplay(os linux.System) error {
return fmsg.WrapError(ErrXDisplay,
"DISPLAY is not set")
} else {
- seal.sys.ChangeHosts(seal.sys.user.Username)
+ seal.sys.ChangeHosts(seal.sys.user.us)
seal.sys.bwrap.SetEnv[display] = d
seal.sys.bwrap.Bind("/tmp/.X11-unix", "/tmp/.X11-unix")
}
diff --git a/internal/app/share.system.go b/internal/app/share.system.go
index 08b006c3..7c97c48a 100644
--- a/internal/app/share.system.go
+++ b/internal/app/share.system.go
@@ -16,12 +16,12 @@ const (
func (seal *appSeal) shareSystem() {
// ensure Share (e.g. `/tmp/fortify.%d`)
// acl is unnecessary as this directory is world executable
- seal.sys.Ensure(seal.SharePath, 0701)
+ seal.sys.Ensure(seal.SharePath, 0711)
// ensure process-specific share (e.g. `/tmp/fortify.%d/%s`)
// acl is unnecessary as this directory is world executable
seal.share = path.Join(seal.SharePath, seal.id)
- seal.sys.Ephemeral(system.Process, seal.share, 0701)
+ seal.sys.Ephemeral(system.Process, seal.share, 0711)
// ensure child tmpdir parent directory (e.g. `/tmp/fortify.%d/tmpdir`)
targetTmpdirParent := path.Join(seal.SharePath, "tmpdir")
@@ -29,7 +29,7 @@ func (seal *appSeal) shareSystem() {
seal.sys.UpdatePermType(system.User, targetTmpdirParent, acl.Execute)
// ensure child tmpdir (e.g. `/tmp/fortify.%d/tmpdir/%d`)
- targetTmpdir := path.Join(targetTmpdirParent, seal.sys.user.Uid)
+ targetTmpdir := path.Join(targetTmpdirParent, seal.sys.user.as)
seal.sys.Ensure(targetTmpdir, 01700)
seal.sys.UpdatePermType(system.User, targetTmpdir, acl.Read, acl.Write, acl.Execute)
seal.sys.bwrap.Bind(targetTmpdir, "/tmp", false, true)
@@ -49,15 +49,21 @@ func (seal *appSeal) sharePasswd(os linux.System) {
// generate /etc/passwd
passwdPath := path.Join(seal.share, "passwd")
username := "chronos"
- if seal.sys.user.Username != "" {
- username = seal.sys.user.Username
- seal.sys.bwrap.SetEnv["USER"] = seal.sys.user.Username
+ if seal.sys.user.username != "" {
+ username = seal.sys.user.username
}
homeDir := "/var/empty"
- if seal.sys.user.HomeDir != "" {
- homeDir = seal.sys.user.HomeDir
- seal.sys.bwrap.SetEnv["HOME"] = seal.sys.user.HomeDir
+ if seal.sys.user.home != "" {
+ homeDir = seal.sys.user.home
}
+
+ // bind home directory
+ seal.sys.bwrap.Bind(homeDir, homeDir, false, true)
+ seal.sys.bwrap.Chdir = homeDir
+
+ seal.sys.bwrap.SetEnv["USER"] = username
+ seal.sys.bwrap.SetEnv["HOME"] = homeDir
+
passwd := username + ":x:" + seal.sys.mappedIDString + ":" + seal.sys.mappedIDString + ":Fortify:" + homeDir + ":" + sh + "\n"
seal.sys.Write(passwdPath, passwd)
diff --git a/internal/app/start.go b/internal/app/start.go
index 67a091c6..0feb0ae6 100644
--- a/internal/app/start.go
+++ b/internal/app/start.go
@@ -41,19 +41,13 @@ func (a *app) Start() error {
}
}
- // select command builder
- var commandBuilder shim.CommandBuilder
- switch a.seal.launchOption {
- case LaunchMethodSudo:
- commandBuilder = a.commandBuilderSudo
- case LaunchMethodMachineCtl:
- commandBuilder = a.commandBuilderMachineCtl
- default:
- panic("unreachable")
- }
-
// construct shim manager
- a.shim = shim.New(a.seal.toolPath, uint32(a.seal.sys.UID()), path.Join(a.seal.share, "shim"), a.seal.wl,
+ a.shim = shim.New(
+ uint32(a.seal.sys.UID()),
+ 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,
@@ -62,9 +56,6 @@ func (a *app) Start() error {
Verbose: fmsg.Verbose(),
},
- // checkPid is impossible at the moment since there is no reliable way to obtain shim's pid
- // this feature is disabled here until sudo is replaced by fortify suid wrapper
- false,
)
// startup will go ahead, commit system setup
@@ -73,7 +64,7 @@ func (a *app) Start() error {
}
a.seal.sys.needRevert = true
- if startTime, err := a.shim.Start(commandBuilder); err != nil {
+ if startTime, err := a.shim.Start(); err != nil {
return err
} else {
// shim start and setup success, create process state
@@ -81,7 +72,6 @@ func (a *app) Start() error {
PID: a.shim.Unwrap().Process.Pid,
Command: a.seal.command,
Capability: a.seal.et,
- Method: method[a.seal.launchOption],
Argv: a.shim.Unwrap().Args,
Time: *startTime,
}
@@ -166,20 +156,31 @@ func (a *app) Wait() (int, error) {
// failure prior to process start
r = 255
} else {
+ wait := make(chan error, 1)
+ go func() { wait <- cmd.Wait() }()
+
+ select {
// wait for process and resolve exit code
- if err := cmd.Wait(); err != nil {
- var exitError *exec.ExitError
- if !errors.As(err, &exitError) {
- // should be unreachable
- a.waitErr = err
+ case err := <-wait:
+ if err != nil {
+ var exitError *exec.ExitError
+ if !errors.As(err, &exitError) {
+ // should be unreachable
+ a.waitErr = err
+ }
+
+ // store non-zero return code
+ r = exitError.ExitCode()
+ } else {
+ r = cmd.ProcessState.ExitCode()
}
+ fmsg.VPrintf("process %d exited with exit code %d", cmd.Process.Pid, r)
- // store non-zero return code
- r = exitError.ExitCode()
- } else {
- r = cmd.ProcessState.ExitCode()
+ // alternative exit path when kill was unsuccessful
+ case err := <-a.shim.WaitFallback():
+ r = 255
+ fmsg.Printf("cannot terminate shim on faulted setup: %v", err)
}
- fmsg.VPrintf("process %d exited with exit code %d", cmd.Process.Pid, r)
}
// child process exited, resume output
diff --git a/internal/app/system.go b/internal/app/system.go
index 47bbd7ed..6a03b92f 100644
--- a/internal/app/system.go
+++ b/internal/app/system.go
@@ -1,8 +1,6 @@
package app
import (
- "os/user"
-
"git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal/linux"
@@ -18,7 +16,7 @@ type appSealSys struct {
// default formatted XDG_RUNTIME_DIR of User
runtime string
// target user sealed from config
- user *user.User
+ user appUser
// mapped uid and gid in user namespace
mappedID int
@@ -32,6 +30,26 @@ type appSealSys struct {
// protected by upstream mutex
}
+type appUser struct {
+ // full uid resolved by fsu
+ uid int
+ // string representation of uid
+ us string
+
+ // supplementary group ids
+ supp []string
+
+ // application id
+ aid int
+ // string representation of aid
+ as string
+
+ // app user home directory
+ home string
+ // passwd database username
+ username string
+}
+
// shareAll calls all share methods in sequence
func (seal *appSeal) shareAll(bus [2]*dbus.Config, os linux.System) error {
if seal.shared {
diff --git a/internal/fmsg/defer.go b/internal/fmsg/defer.go
index ed4a7735..0f3f5ad5 100644
--- a/internal/fmsg/defer.go
+++ b/internal/fmsg/defer.go
@@ -56,9 +56,10 @@ func Exit(code int) {
os.Exit(code)
}
-func Withhold() {
+func Suspend() {
dequeueOnce.Do(dequeue)
if wstate.CompareAndSwap(false, true) {
+ queueSync.Wait()
withhold <- struct{}{}
}
}
diff --git a/internal/linux/interface.go b/internal/linux/interface.go
index 36f61b1a..a920492f 100644
--- a/internal/linux/interface.go
+++ b/internal/linux/interface.go
@@ -22,8 +22,8 @@ type System interface {
LookPath(file string) (string, error)
// Executable provides [os.Executable].
Executable() (string, error)
- // Lookup provides [user.Lookup].
- Lookup(username string) (*user.User, error)
+ // LookupGroup provides [user.LookupGroup].
+ LookupGroup(name string) (*user.Group, error)
// ReadDir provides [os.ReadDir].
ReadDir(name string) ([]fs.DirEntry, error)
// Stat provides [os.Stat].
@@ -35,10 +35,10 @@ type System interface {
// Stdout provides [os.Stdout].
Stdout() io.Writer
- // FshimPath returns an absolute path to the fshim binary.
- FshimPath() string
// Paths returns a populated [Paths] struct.
Paths() Paths
+ // Uid invokes fsu and returns target uid.
+ Uid(aid int) (int, error)
// SdBooted implements https://www.freedesktop.org/software/systemd/man/sd_booted.html
SdBooted() bool
}
diff --git a/internal/linux/std.go b/internal/linux/std.go
index e0a0994c..4ea5b903 100644
--- a/internal/linux/std.go
+++ b/internal/linux/std.go
@@ -7,6 +7,7 @@ import (
"os"
"os/exec"
"os/user"
+ "strconv"
"sync"
"git.ophivana.moe/security/fortify/internal"
@@ -21,41 +22,75 @@ type Std struct {
sdBooted bool
sdBootedOnce sync.Once
- fshim string
- fshimOnce sync.Once
+ uidOnce sync.Once
+ uidCopy map[int]struct {
+ uid int
+ err error
+ }
+ uidMu sync.RWMutex
}
-func (s *Std) Geteuid() int { return os.Geteuid() }
-func (s *Std) LookupEnv(key string) (string, bool) { return os.LookupEnv(key) }
-func (s *Std) TempDir() string { return os.TempDir() }
-func (s *Std) LookPath(file string) (string, error) { return exec.LookPath(file) }
-func (s *Std) Executable() (string, error) { return os.Executable() }
-func (s *Std) Lookup(username string) (*user.User, error) { return user.Lookup(username) }
-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) Open(name string) (fs.File, error) { return os.Open(name) }
-func (s *Std) Exit(code int) { fmsg.Exit(code) }
-func (s *Std) Stdout() io.Writer { return os.Stdout }
+func (s *Std) Geteuid() int { return os.Geteuid() }
+func (s *Std) LookupEnv(key string) (string, bool) { return os.LookupEnv(key) }
+func (s *Std) TempDir() string { return os.TempDir() }
+func (s *Std) LookPath(file string) (string, error) { return exec.LookPath(file) }
+func (s *Std) Executable() (string, error) { return os.Executable() }
+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) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) }
+func (s *Std) Open(name string) (fs.File, error) { return os.Open(name) }
+func (s *Std) Exit(code int) { fmsg.Exit(code) }
+func (s *Std) Stdout() io.Writer { return os.Stdout }
const xdgRuntimeDir = "XDG_RUNTIME_DIR"
-func (s *Std) FshimPath() string {
- s.fshimOnce.Do(func() {
- p, ok := internal.Path(internal.Fshim)
- if !ok {
- fmsg.Fatal("invalid fshim path, this copy of fortify is not compiled correctly")
- }
- s.fshim = p
- })
-
- return s.fshim
-}
-
func (s *Std) Paths() Paths {
s.pathsOnce.Do(func() { CopyPaths(s, &s.paths) })
return s.paths
}
+func (s *Std) Uid(aid int) (int, error) {
+ s.uidOnce.Do(func() {
+ s.uidCopy = make(map[int]struct {
+ uid int
+ err error
+ })
+ })
+
+ s.uidMu.RLock()
+ if u, ok := s.uidCopy[aid]; ok {
+ s.uidMu.RUnlock()
+ return u.uid, u.err
+ }
+
+ s.uidMu.RUnlock()
+ s.uidMu.Lock()
+ defer s.uidMu.Unlock()
+
+ u := struct {
+ uid int
+ err error
+ }{}
+ defer func() { s.uidCopy[aid] = u }()
+
+ u.uid = -1
+ if fsu, ok := internal.Check(internal.Fsu); !ok {
+ fmsg.Fatal("invalid fsu path, this copy of fshim is not compiled correctly")
+ panic("unreachable")
+ } else {
+ cmd := exec.Command(fsu)
+ cmd.Path = fsu
+ cmd.Stderr = os.Stderr // pass through fatal messages
+ cmd.Env = []string{"FORTIFY_APP_ID=" + strconv.Itoa(aid)}
+ cmd.Dir = "/"
+ var p []byte
+ if p, u.err = cmd.Output(); u.err == nil {
+ u.uid, u.err = strconv.Atoi(string(p))
+ }
+ return u.uid, u.err
+ }
+}
+
func (s *Std) SdBooted() bool {
s.sdBootedOnce.Do(func() { s.sdBooted = copySdBooted() })
return s.sdBooted
diff --git a/internal/path.go b/internal/path.go
index 3bc28216..51388536 100644
--- a/internal/path.go
+++ b/internal/path.go
@@ -4,7 +4,6 @@ import "path"
var (
Fsu = compPoison
- Fshim = compPoison
Finit = compPoison
)
diff --git a/internal/state/print.go b/internal/state/print.go
index 8ca3529e..174f0693 100644
--- a/internal/state/print.go
+++ b/internal/state/print.go
@@ -67,10 +67,10 @@ func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time
// write header when initialising
if !fmsg.Verbose() {
- _, _ = fmt.Fprintln(*w, "\tUID\tPID\tUptime\tEnablements\tMethod\tCommand")
+ _, _ = fmt.Fprintln(*w, "\tPID\tApp\tUptime\tEnablements\tCommand")
} else {
// argv is emitted in body when verbose
- _, _ = fmt.Fprintln(*w, "\tUID\tPID\tArgv")
+ _, _ = fmt.Fprintln(*w, "\tPID\tApp\tArgv")
}
}
@@ -96,13 +96,13 @@ func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time
}
if !fmsg.Verbose() {
- _, _ = fmt.Fprintf(*w, "\t%s\t%d\t%s\t%s\t%s\t%s\n",
- s.path[len(s.path)-1], state.PID, now.Sub(state.Time).Round(time.Second).String(), strings.TrimPrefix(ets.String(), ", "), state.Method,
+ _, _ = fmt.Fprintf(*w, "\t%d\t%s\t%s\t%s\t%s\n",
+ state.PID, s.path[len(s.path)-1], now.Sub(state.Time).Round(time.Second).String(), strings.TrimPrefix(ets.String(), ", "),
state.Command)
} else {
// emit argv instead when verbose
- _, _ = fmt.Fprintf(*w, "\t%s\t%d\t%s\n",
- s.path[len(s.path)-1], state.PID, state.Argv)
+ _, _ = fmt.Fprintf(*w, "\t%d\t%s\t%s\n",
+ state.PID, s.path[len(s.path)-1], state.Argv)
}
}
diff --git a/internal/state/state.go b/internal/state/state.go
index 81df9655..ee5a95f7 100644
--- a/internal/state/state.go
+++ b/internal/state/state.go
@@ -33,8 +33,6 @@ type State struct {
// capability enablements applied to child
Capability system.Enablements
- // user switch method
- Method string
// full argv whe launching
Argv []string
// process start time
diff --git a/main.go b/main.go
index 1e3e2ee3..75dfcb08 100644
--- a/main.go
+++ b/main.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"flag"
"fmt"
+ "strings"
"text/tabwriter"
"git.ophivana.moe/security/fortify/dbus"
@@ -29,6 +30,20 @@ func init() {
var os = new(linux.Std)
+type gl []string
+
+func (g *gl) String() string {
+ if g == nil {
+ return "<nil>"
+ }
+ return strings.Join(*g, " ")
+}
+
+func (g *gl) Set(v string) error {
+ *g = append(*g, v)
+ return nil
+}
+
func main() {
if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
fmsg.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
@@ -135,10 +150,11 @@ func main() {
mpris bool
dbusVerbose bool
+ aid int
+ groups gl
+ homeDir string
userName string
enablements [system.ELen]bool
-
- launchMethodText string
)
set.StringVar(&dbusConfigSession, "dbus-config", "builtin", "Path to D-Bus proxy config file, or \"builtin\" for defaults")
@@ -147,29 +163,34 @@ func main() {
set.BoolVar(&mpris, "mpris", false, "Allow owning MPRIS D-Bus path, has no effect if custom config is available")
set.BoolVar(&dbusVerbose, "dbus-log", false, "Force logging in the D-Bus proxy")
- set.StringVar(&userName, "u", "chronos", "Passwd name of user to run as")
+ set.IntVar(&aid, "a", 0, "Fortify application ID")
+ set.Var(&groups, "g", "Groups inherited by the app process")
+ set.StringVar(&homeDir, "d", "/var/empty", "Application home directory")
+ set.StringVar(&userName, "u", "chronos", "Passwd name within sandbox")
set.BoolVar(&enablements[system.EWayland], "wayland", false, "Share Wayland socket")
set.BoolVar(&enablements[system.EX11], "X", false, "Share X11 socket and allow connection")
set.BoolVar(&enablements[system.EDBus], "dbus", false, "Proxy D-Bus connection")
set.BoolVar(&enablements[system.EPulse], "pulse", false, "Share PulseAudio socket and cookie")
- methodHelpString := "Method of launching the child process, can be one of \"sudo\""
- if os.SdBooted() {
- methodHelpString += ", \"systemd\""
- }
- set.StringVar(&launchMethodText, "method", "sudo", methodHelpString)
-
// Ignore errors; set is set for ExitOnError.
_ = set.Parse(args[1:])
// initialise config from flags
config := &app.Config{
ID: dbusID,
- User: userName,
Command: set.Args(),
- Method: launchMethodText,
}
+ if aid < 0 || aid > 9999 {
+ fmsg.Fatalf("aid %d out of range", aid)
+ panic("unreachable")
+ }
+
+ config.Confinement.AppID = aid
+ config.Confinement.Groups = groups
+ config.Confinement.Home = homeDir
+ config.Confinement.Username = userName
+
// enablements from flags
for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
if enablements[i] {
diff --git a/package.nix b/package.nix
index cdc3dbeb..ddc9d480 100644
--- a/package.nix
+++ b/package.nix
@@ -30,11 +30,12 @@ buildGoModule rec {
"-w"
"-X"
"main.Fmain=${placeholder "out"}/bin/.fortify-wrapped"
+ "-X"
+ "main.Fshim=${placeholder "out"}/bin/fshim"
]
{
Version = "v${version}";
Fsu = "/run/wrappers/bin/fsu";
- Fshim = "${placeholder "out"}/bin/fshim";
Finit = "${placeholder "out"}/bin/finit";
};