aboutsummaryrefslogtreecommitdiffhomepage
path: root/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'cmd')
-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
5 files changed, 182 insertions, 81 deletions
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
+ }
+}