diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index b7b3e0f..625de0d 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 70e51d3..f04ddf3 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 e92af81..da3eaea 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 e1205b0..a03b600 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 43248f6..2322984 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 0ba5a13..1ec3a4b 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++ - - // - 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 0000000..d029835 --- /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++ + + // + 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 28564eb..3260dea 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 a154486..4a350ca 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 96c51ce..dd45b02 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 3c196b8..0000000 --- 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 dbb4931..0000000 --- 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 0846a7f..f01061a 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) - } else { - // unreachable - panic(err) - } + // 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 = u + 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 { + 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 + } + } } // 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 7e814a8..073a29f 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 08b006c..7c97c48 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 67a091c..0feb0ae 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 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 - } + wait := make(chan error, 1) + go func() { wait <- cmd.Wait() }() - // store non-zero return code - r = exitError.ExitCode() - } else { - r = cmd.ProcessState.ExitCode() + select { + // wait for process and resolve exit code + 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) + + // 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 47bbd7e..6a03b92 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 ed4a773..0f3f5ad 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 36f61b1..a920492 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 e0a0994..4ea5b90 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 3bc2821..5138853 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 8ca3529..174f069 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 81df965..ee5a95f 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 1e3e2ee..75dfcb0 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 "" + } + 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 cdc3dbe..ddc9d48 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"; };