diff options
| author | Ophestra Umiker <cat@ophivana.moe> | 2024-11-02 03:03:44 +0900 |
|---|---|---|
| committer | Ophestra Umiker <cat@ophivana.moe> | 2024-11-02 03:13:57 +0900 |
| commit | 584732f80ab91afb349720cfb8e9979ed2ba173e (patch) | |
| tree | 8ef6ab9f8c9d3b8197682a53fb2c4a7b2ce8da55 /internal | |
| parent | 4b7b899bb35fb4ea218dabe49a674f4d2f80e7f8 (diff) | |
cmd: shim and init into separate binaries
This change also fixes a deadlock when shim fails to connect and complete the setup.
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/app/app.go | 8 | ||||
| -rw-r--r-- | internal/app/app_nixos_test.go | 10 | ||||
| -rw-r--r-- | internal/app/app_test.go | 4 | ||||
| -rw-r--r-- | internal/app/export_test.go | 4 | ||||
| -rw-r--r-- | internal/app/launch.machinectl.go | 4 | ||||
| -rw-r--r-- | internal/app/launch.sudo.go | 2 | ||||
| -rw-r--r-- | internal/app/seal.go | 13 | ||||
| -rw-r--r-- | internal/app/share.display.go | 4 | ||||
| -rw-r--r-- | internal/app/share.pulse.go | 6 | ||||
| -rw-r--r-- | internal/app/share.system.go | 4 | ||||
| -rw-r--r-- | internal/app/start.go | 9 | ||||
| -rw-r--r-- | internal/app/system.go | 6 | ||||
| -rw-r--r-- | internal/comp.go | 12 | ||||
| -rw-r--r-- | internal/init/main.go | 174 | ||||
| -rw-r--r-- | internal/init/payload.go | 15 | ||||
| -rw-r--r-- | internal/linux/interface.go (renamed from internal/system.go) | 63 | ||||
| -rw-r--r-- | internal/linux/std.go | 83 | ||||
| -rw-r--r-- | internal/path.go | 14 | ||||
| -rw-r--r-- | internal/prctl.go | 20 | ||||
| -rw-r--r-- | internal/shim/main.go | 179 | ||||
| -rw-r--r-- | internal/shim/parent.go | 200 | ||||
| -rw-r--r-- | internal/shim/payload.go | 42 | ||||
| -rw-r--r-- | internal/shim/wayland.go | 75 |
23 files changed, 167 insertions, 784 deletions
diff --git a/internal/app/app.go b/internal/app/app.go index 264975c5..28564ebf 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -3,8 +3,8 @@ package app import ( "sync" - "git.ophivana.moe/security/fortify/internal" - "git.ophivana.moe/security/fortify/internal/shim" + "git.ophivana.moe/security/fortify/cmd/fshim/ipc/shim" + "git.ophivana.moe/security/fortify/internal/linux" ) type App interface { @@ -25,7 +25,7 @@ type app struct { // application unique identifier id *ID // operating system interface - os internal.System + os linux.System // shim process manager shim *shim.Shim // child process related information @@ -63,7 +63,7 @@ func (a *app) WaitErr() error { return a.waitErr } -func New(os internal.System) (App, error) { +func New(os linux.System) (App, error) { a := new(app) a.id = new(ID) a.os = os diff --git a/internal/app/app_nixos_test.go b/internal/app/app_nixos_test.go index 11079215..f7eb4c0e 100644 --- a/internal/app/app_nixos_test.go +++ b/internal/app/app_nixos_test.go @@ -9,8 +9,8 @@ import ( "git.ophivana.moe/security/fortify/acl" "git.ophivana.moe/security/fortify/dbus" "git.ophivana.moe/security/fortify/helper/bwrap" - "git.ophivana.moe/security/fortify/internal" "git.ophivana.moe/security/fortify/internal/app" + "git.ophivana.moe/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/system" ) @@ -579,8 +579,12 @@ func (s *stubNixOS) Exit(code int) { panic("called exit on stub with code " + strconv.Itoa(code)) } -func (s *stubNixOS) Paths() internal.Paths { - return internal.Paths{ +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", RuntimePath: "/run/user/1971", RunDirPath: "/run/user/1971/fortify", diff --git a/internal/app/app_test.go b/internal/app/app_test.go index fd223afb..de68e531 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -7,14 +7,14 @@ import ( "time" "git.ophivana.moe/security/fortify/helper/bwrap" - "git.ophivana.moe/security/fortify/internal" "git.ophivana.moe/security/fortify/internal/app" + "git.ophivana.moe/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/system" ) type sealTestCase struct { name string - os internal.System + os linux.System config *app.Config id app.ID wantSys *system.I diff --git a/internal/app/export_test.go b/internal/app/export_test.go index 8e524681..56eb4a31 100644 --- a/internal/app/export_test.go +++ b/internal/app/export_test.go @@ -2,11 +2,11 @@ package app import ( "git.ophivana.moe/security/fortify/helper/bwrap" - "git.ophivana.moe/security/fortify/internal" + "git.ophivana.moe/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/system" ) -func NewWithID(id ID, os internal.System) App { +func NewWithID(id ID, os linux.System) App { a := new(app) a.id = &id a.os = os diff --git a/internal/app/launch.machinectl.go b/internal/app/launch.machinectl.go index c1d26101..3c196b84 100644 --- a/internal/app/launch.machinectl.go +++ b/internal/app/launch.machinectl.go @@ -47,8 +47,8 @@ func (a *app) commandBuilderMachineCtl(shimEnv string) (args []string) { } innerCommand.WriteString("; ") - // launch fortify as shim - innerCommand.WriteString("exec " + a.seal.sys.executable + " shim") + // launch fortify shim + innerCommand.WriteString("exec " + a.os.FshimPath()) // append inner command args = append(args, innerCommand.String()) diff --git a/internal/app/launch.sudo.go b/internal/app/launch.sudo.go index e7a20672..dbb49318 100644 --- a/internal/app/launch.sudo.go +++ b/internal/app/launch.sudo.go @@ -24,7 +24,7 @@ func (a *app) commandBuilderSudo(shimEnv string) (args []string) { args = append(args, shimEnv) // -- $@ - args = append(args, "--", a.seal.sys.executable, "shim") + args = append(args, "--", a.os.FshimPath()) return } diff --git a/internal/app/seal.go b/internal/app/seal.go index 80c3ec29..502ec6e2 100644 --- a/internal/app/seal.go +++ b/internal/app/seal.go @@ -7,10 +7,10 @@ import ( "path" "strconv" + shim "git.ophivana.moe/security/fortify/cmd/fshim/ipc" "git.ophivana.moe/security/fortify/dbus" - "git.ophivana.moe/security/fortify/internal" "git.ophivana.moe/security/fortify/internal/fmsg" - "git.ophivana.moe/security/fortify/internal/shim" + "git.ophivana.moe/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/state" "git.ophivana.moe/security/fortify/internal/system" ) @@ -66,7 +66,7 @@ type appSeal struct { // seal system-level component sys *appSealSys - internal.Paths + linux.Paths // protected by upstream mutex } @@ -127,13 +127,6 @@ func (a *app) Seal(config *Config) error { // create seal system component seal.sys = new(appSealSys) - // look up fortify executable path - if p, err := a.os.Executable(); err != nil { - return fmsg.WrapErrorSuffix(err, "cannot look up fortify executable path:") - } else { - seal.sys.executable = p - } - // look up user from system if u, err := a.os.Lookup(config.User); err != nil { if errors.As(err, new(user.UnknownUserError)) { diff --git a/internal/app/share.display.go b/internal/app/share.display.go index 95132119..7e814a83 100644 --- a/internal/app/share.display.go +++ b/internal/app/share.display.go @@ -5,8 +5,8 @@ import ( "path" "git.ophivana.moe/security/fortify/acl" - "git.ophivana.moe/security/fortify/internal" "git.ophivana.moe/security/fortify/internal/fmsg" + "git.ophivana.moe/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/system" ) @@ -23,7 +23,7 @@ var ( ErrXDisplay = errors.New(display + " unset") ) -func (seal *appSeal) shareDisplay(os internal.System) error { +func (seal *appSeal) shareDisplay(os linux.System) error { // pass $TERM to launcher if t, ok := os.LookupEnv(term); ok { seal.sys.bwrap.SetEnv[term] = t diff --git a/internal/app/share.pulse.go b/internal/app/share.pulse.go index fa5c0a7a..14315342 100644 --- a/internal/app/share.pulse.go +++ b/internal/app/share.pulse.go @@ -6,8 +6,8 @@ import ( "io/fs" "path" - "git.ophivana.moe/security/fortify/internal" "git.ophivana.moe/security/fortify/internal/fmsg" + "git.ophivana.moe/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/system" ) @@ -25,7 +25,7 @@ var ( ErrPulseMode = errors.New("unexpected pulse socket mode") ) -func (seal *appSeal) sharePulse(os internal.System) error { +func (seal *appSeal) sharePulse(os linux.System) error { if !seal.et.Has(system.EPulse) { return nil } @@ -78,7 +78,7 @@ func (seal *appSeal) sharePulse(os internal.System) error { } // discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie -func discoverPulseCookie(os internal.System) (string, error) { +func discoverPulseCookie(os linux.System) (string, error) { if p, ok := os.LookupEnv(pulseCookie); ok { return p, nil } diff --git a/internal/app/share.system.go b/internal/app/share.system.go index 600119ee..cf2df947 100644 --- a/internal/app/share.system.go +++ b/internal/app/share.system.go @@ -4,7 +4,7 @@ import ( "path" "git.ophivana.moe/security/fortify/acl" - "git.ophivana.moe/security/fortify/internal" + "git.ophivana.moe/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/system" ) @@ -38,7 +38,7 @@ func (seal *appSeal) shareSystem() { seal.sys.bwrap.Tmpfs(seal.SharePath, 1*1024*1024) } -func (seal *appSeal) sharePasswd(os internal.System) { +func (seal *appSeal) sharePasswd(os linux.System) { // look up shell sh := "/bin/sh" if s, ok := os.LookupEnv(shell); ok { diff --git a/internal/app/start.go b/internal/app/start.go index a50be6ea..376d3067 100644 --- a/internal/app/start.go +++ b/internal/app/start.go @@ -8,9 +8,10 @@ import ( "path/filepath" "strings" + shim0 "git.ophivana.moe/security/fortify/cmd/fshim/ipc" + "git.ophivana.moe/security/fortify/cmd/fshim/ipc/shim" "git.ophivana.moe/security/fortify/helper" "git.ophivana.moe/security/fortify/internal/fmsg" - "git.ophivana.moe/security/fortify/internal/shim" "git.ophivana.moe/security/fortify/internal/state" "git.ophivana.moe/security/fortify/internal/system" ) @@ -22,9 +23,9 @@ func (a *app) Start() error { defer a.lock.Unlock() // resolve exec paths - shimExec := [3]string{a.seal.sys.executable, helper.BubblewrapName} + shimExec := [2]string{helper.BubblewrapName} if len(a.seal.command) > 0 { - shimExec[2] = a.seal.command[0] + shimExec[1] = a.seal.command[0] } for i, n := range shimExec { if len(n) == 0 { @@ -53,7 +54,7 @@ func (a *app) Start() error { // construct shim manager a.shim = shim.New(a.seal.toolPath, uint32(a.seal.sys.UID()), path.Join(a.seal.share, "shim"), a.seal.wl, - &shim.Payload{ + &shim0.Payload{ Argv: a.seal.command, Exec: shimExec, Bwrap: a.seal.sys.bwrap, diff --git a/internal/app/system.go b/internal/app/system.go index 74b8e611..3c17cf41 100644 --- a/internal/app/system.go +++ b/internal/app/system.go @@ -5,7 +5,7 @@ import ( "git.ophivana.moe/security/fortify/dbus" "git.ophivana.moe/security/fortify/helper/bwrap" - "git.ophivana.moe/security/fortify/internal" + "git.ophivana.moe/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/system" ) @@ -17,8 +17,6 @@ type appSealSys struct { // default formatted XDG_RUNTIME_DIR of User runtime string - // sealed path to fortify executable, used by shim - executable string // target user sealed from config user *user.User @@ -30,7 +28,7 @@ type appSealSys struct { } // shareAll calls all share methods in sequence -func (seal *appSeal) shareAll(bus [2]*dbus.Config, os internal.System) error { +func (seal *appSeal) shareAll(bus [2]*dbus.Config, os linux.System) error { if seal.shared { panic("seal shared twice") } diff --git a/internal/comp.go b/internal/comp.go new file mode 100644 index 00000000..e7064db0 --- /dev/null +++ b/internal/comp.go @@ -0,0 +1,12 @@ +package internal + +const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID" + +var ( + Version = compPoison +) + +// Check validates string value set at compile time. +func Check(s string) (string, bool) { + return s, s != compPoison && s != "" +} diff --git a/internal/init/main.go b/internal/init/main.go deleted file mode 100644 index f2927728..00000000 --- a/internal/init/main.go +++ /dev/null @@ -1,174 +0,0 @@ -package init0 - -import ( - "encoding/gob" - "errors" - "flag" - "os" - "os/exec" - "os/signal" - "path" - "strconv" - "syscall" - "time" - - "git.ophivana.moe/security/fortify/internal/fmsg" -) - -const ( - // time to wait for linger processes after death initial process - residualProcessTimeout = 5 * time.Second -) - -// everything beyond this point runs within pid namespace -// proceed with caution! - -func doInit(fd uintptr) { - fmsg.SetPrefix("init") - - // re-exec - if len(os.Args) > 0 && os.Args[0] != "fortify" && path.IsAbs(os.Args[0]) { - if err := syscall.Exec(os.Args[0], []string{"fortify", "init"}, os.Environ()); err != nil { - fmsg.Println("cannot re-exec self:", err) - // continue anyway - } - } - - var payload Payload - p := os.NewFile(fd, "config-stream") - if p == nil { - fmsg.Fatal("invalid config descriptor") - } - if err := gob.NewDecoder(p).Decode(&payload); err != nil { - fmsg.Fatal("cannot decode init payload:", err) - } else { - // sharing stdout with parent - // USE WITH CAUTION - fmsg.SetVerbose(payload.Verbose) - - // child does not need to see this - if err = os.Unsetenv(EnvInit); err != nil { - fmsg.Println("cannot unset", EnvInit+":", err) - // not fatal - } else { - fmsg.VPrintln("received configuration") - } - } - - // close config fd - if err := p.Close(); err != nil { - fmsg.Println("cannot close config fd:", err) - // not fatal - } - - // die with parent - if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGKILL), 0); errno != 0 { - fmsg.Fatal("prctl(PR_SET_PDEATHSIG, SIGKILL):", errno.Error()) - } - - cmd := exec.Command(payload.Argv0) - cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr - cmd.Args = payload.Argv - cmd.Env = os.Environ() - - // pass wayland fd - if payload.WL != -1 { - if f := os.NewFile(uintptr(payload.WL), "wayland"); f != nil { - cmd.Env = append(cmd.Env, "WAYLAND_SOCKET="+strconv.Itoa(3+len(cmd.ExtraFiles))) - cmd.ExtraFiles = append(cmd.ExtraFiles, f) - } - } - - if err := cmd.Start(); err != nil { - fmsg.Fatalf("cannot start %q: %v", payload.Argv0, err) - } - - sig := make(chan os.Signal, 2) - signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) - - type winfo struct { - wpid int - wstatus syscall.WaitStatus - } - info := make(chan winfo, 1) - done := make(chan struct{}) - - go func() { - var ( - err error - wpid = -2 - wstatus syscall.WaitStatus - ) - - // keep going until no child process is left - for wpid != -1 { - if err != nil { - break - } - - if wpid != -2 { - info <- winfo{wpid, wstatus} - } - - err = syscall.EINTR - for errors.Is(err, syscall.EINTR) { - wpid, err = syscall.Wait4(-1, &wstatus, 0, nil) - } - } - if !errors.Is(err, syscall.ECHILD) { - fmsg.Println("unexpected wait4 response:", err) - } - - close(done) - }() - - timeout := make(chan struct{}) - - r := 2 - for { - select { - case s := <-sig: - fmsg.VPrintln("received", s.String()) - fmsg.Exit(0) - case w := <-info: - if w.wpid == cmd.Process.Pid { - switch { - case w.wstatus.Exited(): - r = w.wstatus.ExitStatus() - case w.wstatus.Signaled(): - r = 128 + int(w.wstatus.Signal()) - default: - r = 255 - } - - go func() { - time.Sleep(residualProcessTimeout) - close(timeout) - }() - } - case <-done: - fmsg.Exit(r) - case <-timeout: - fmsg.Println("timeout exceeded waiting for lingering processes") - fmsg.Exit(r) - } - } -} - -// Try runs init and stops execution if FORTIFY_INIT is set. -func Try() { - if os.Getpid() != 1 { - return - } - - if args := flag.Args(); len(args) == 1 && args[0] == "init" { - if s, ok := os.LookupEnv(EnvInit); ok { - if fd, err := strconv.Atoi(s); err != nil { - fmsg.Fatalf("cannot parse %q: %v", s, err) - } else { - doInit(uintptr(fd)) - } - panic("unreachable") - } - } -} diff --git a/internal/init/payload.go b/internal/init/payload.go deleted file mode 100644 index 6f4b2781..00000000 --- a/internal/init/payload.go +++ /dev/null @@ -1,15 +0,0 @@ -package init0 - -const EnvInit = "FORTIFY_INIT" - -type Payload struct { - // target full exec path - Argv0 string - // child full argv - Argv []string - // wayland fd, -1 to disable - WL int - - // verbosity pass through - Verbose bool -} diff --git a/internal/system.go b/internal/linux/interface.go index 3e973b56..cf984a32 100644 --- a/internal/system.go +++ b/internal/linux/interface.go @@ -1,14 +1,10 @@ -package internal +package linux import ( - "errors" "io/fs" - "os" - "os/exec" "os/user" "path" "strconv" - "sync" "git.ophivana.moe/security/fortify/internal/fmsg" ) @@ -36,6 +32,8 @@ type System interface { // Exit provides [os.Exit]. Exit(code int) + // FshimPath returns an absolute path to the fshim binary. + FshimPath() string // Paths returns a populated [Paths] struct. Paths() Paths // SdBooted implements https://www.freedesktop.org/software/systemd/man/sd_booted.html @@ -69,58 +67,3 @@ func CopyPaths(os System, v *Paths) { fmsg.VPrintf("runtime directory at %q", v.RunDirPath) } - -// Std implements System using the standard library. -type Std struct { - paths Paths - pathsOnce sync.Once - - sdBooted bool - sdBootedOnce sync.Once -} - -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) } - -const xdgRuntimeDir = "XDG_RUNTIME_DIR" - -func (s *Std) Paths() Paths { - s.pathsOnce.Do(func() { CopyPaths(s, &s.paths) }) - return s.paths -} - -func (s *Std) SdBooted() bool { - s.sdBootedOnce.Do(func() { s.sdBooted = copySdBooted() }) - return s.sdBooted -} - -const systemdCheckPath = "/run/systemd/system" - -func copySdBooted() bool { - if v, err := sdBooted(); err != nil { - fmsg.Println("cannot read systemd marker:", err) - return false - } else { - return v - } -} - -func sdBooted() (bool, error) { - _, err := os.Stat(systemdCheckPath) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - err = nil - } - return false, err - } - - return true, nil -} diff --git a/internal/linux/std.go b/internal/linux/std.go new file mode 100644 index 00000000..704ba162 --- /dev/null +++ b/internal/linux/std.go @@ -0,0 +1,83 @@ +package linux + +import ( + "errors" + "io/fs" + "os" + "os/exec" + "os/user" + "sync" + + "git.ophivana.moe/security/fortify/internal" + "git.ophivana.moe/security/fortify/internal/fmsg" +) + +// Std implements System using the standard library. +type Std struct { + paths Paths + pathsOnce sync.Once + + sdBooted bool + sdBootedOnce sync.Once + + fshim string + fshimOnce sync.Once +} + +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) } + +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) SdBooted() bool { + s.sdBootedOnce.Do(func() { s.sdBooted = copySdBooted() }) + return s.sdBooted +} + +const systemdCheckPath = "/run/systemd/system" + +func copySdBooted() bool { + if v, err := sdBooted(); err != nil { + fmsg.Println("cannot read systemd marker:", err) + return false + } else { + return v + } +} + +func sdBooted() (bool, error) { + _, err := os.Stat(systemdCheckPath) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + err = nil + } + return false, err + } + + return true, nil +} diff --git a/internal/path.go b/internal/path.go new file mode 100644 index 00000000..1332276e --- /dev/null +++ b/internal/path.go @@ -0,0 +1,14 @@ +package internal + +import "path" + +var ( + Fmain = compPoison + Fsu = compPoison + Fshim = compPoison + Finit = compPoison +) + +func Path(p string) (string, bool) { + return p, p != compPoison && p != "" && path.IsAbs(p) +} diff --git a/internal/prctl.go b/internal/prctl.go new file mode 100644 index 00000000..1c03edd9 --- /dev/null +++ b/internal/prctl.go @@ -0,0 +1,20 @@ +package internal + +import "syscall" + +func PR_SET_DUMPABLE__SUID_DUMP_DISABLE() error { + // linux/sched/coredump.h + if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, 0, 0); errno != 0 { + return errno + } + + return nil +} + +func PR_SET_PDEATHSIG__SIGKILL() error { + if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGKILL), 0); errno != 0 { + return errno + } + + return nil +} diff --git a/internal/shim/main.go b/internal/shim/main.go deleted file mode 100644 index 52a8fa9e..00000000 --- a/internal/shim/main.go +++ /dev/null @@ -1,179 +0,0 @@ -package shim - -import ( - "encoding/gob" - "errors" - "flag" - "net" - "os" - "path" - "strconv" - "syscall" - - "git.ophivana.moe/security/fortify/helper" - "git.ophivana.moe/security/fortify/internal/fmsg" - init0 "git.ophivana.moe/security/fortify/internal/init" -) - -// everything beyond this point runs as target user -// proceed with caution! - -func doShim(socket string) { - fmsg.SetPrefix("shim") - - // re-exec - if len(os.Args) > 0 && os.Args[0] != "fortify" && path.IsAbs(os.Args[0]) { - if err := syscall.Exec(os.Args[0], []string{"fortify", "shim"}, os.Environ()); err != nil { - fmsg.Println("cannot re-exec self:", err) - // continue anyway - } - } - - // dial setup socket - var conn *net.UnixConn - if c, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: socket, Net: "unix"}); err != nil { - fmsg.Fatal("cannot dial setup socket:", err) - panic("unreachable") - } else { - conn = c - } - - // decode payload gob stream - var payload Payload - if err := gob.NewDecoder(conn).Decode(&payload); err != nil { - fmsg.Fatal("cannot decode shim payload:", err) - } else { - // sharing stdout with parent - // USE WITH CAUTION - fmsg.SetVerbose(payload.Verbose) - } - - if payload.Bwrap == nil { - fmsg.Fatal("bwrap config not supplied") - } - - // receive wayland fd over socket - wfd := -1 - if payload.WL { - if fd, err := receiveWLfd(conn); err != nil { - fmsg.Fatal("cannot receive wayland fd:", err) - } else { - wfd = fd - } - } - - // close setup socket - if err := conn.Close(); err != nil { - fmsg.Println("cannot close setup socket:", err) - // not fatal - } - - var ic init0.Payload - - // resolve argv0 - ic.Argv = payload.Argv - if len(ic.Argv) > 0 { - // looked up from $PATH by parent - ic.Argv0 = payload.Exec[2] - } else { - // no argv, look up shell instead - var ok bool - if ic.Argv0, ok = os.LookupEnv("SHELL"); !ok { - fmsg.Fatal("no command was specified and $SHELL was unset") - } - - ic.Argv = []string{ic.Argv0} - } - - conf := payload.Bwrap - - var extraFiles []*os.File - - // pass wayland fd - if wfd != -1 { - if f := os.NewFile(uintptr(wfd), "wayland"); f != nil { - ic.WL = 3 + len(extraFiles) - extraFiles = append(extraFiles, f) - } - } else { - ic.WL = -1 - } - - // share config pipe - if r, w, err := os.Pipe(); err != nil { - fmsg.Fatal("cannot pipe:", err) - } else { - conf.SetEnv[init0.EnvInit] = strconv.Itoa(3 + len(extraFiles)) - extraFiles = append(extraFiles, r) - - fmsg.VPrintln("transmitting config to init") - go func() { - // stream config to pipe - if err = gob.NewEncoder(w).Encode(&ic); err != nil { - fmsg.Fatal("cannot transmit init config:", err) - } - }() - } - - helper.BubblewrapName = payload.Exec[1] // resolved bwrap path by parent - if b, err := helper.NewBwrap(conf, nil, payload.Exec[0], func(int, int) []string { return []string{"init"} }); err != nil { - fmsg.Fatal("malformed sandbox config:", err) - } else { - cmd := b.Unwrap() - cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr - cmd.ExtraFiles = extraFiles - - if fmsg.Verbose() { - fmsg.VPrintln("bwrap args:", conf.Args()) - } - - // run and pass through exit code - if err = b.Start(); err != nil { - fmsg.Fatal("cannot start target process:", err) - } else if err = b.Wait(); err != nil { - fmsg.VPrintln("wait:", err) - } - if b.Unwrap().ProcessState != nil { - fmsg.Exit(b.Unwrap().ProcessState.ExitCode()) - } else { - fmsg.Exit(127) - } - } -} - -func receiveWLfd(conn *net.UnixConn) (int, error) { - oob := make([]byte, syscall.CmsgSpace(4)) // single fd - - if _, oobn, _, _, err := conn.ReadMsgUnix(nil, oob); err != nil { - return -1, err - } else if len(oob) != oobn { - return -1, errors.New("invalid message length") - } - - var msg syscall.SocketControlMessage - if messages, err := syscall.ParseSocketControlMessage(oob); err != nil { - return -1, err - } else if len(messages) != 1 { - return -1, errors.New("unexpected message count") - } else { - msg = messages[0] - } - - if fds, err := syscall.ParseUnixRights(&msg); err != nil { - return -1, err - } else if len(fds) != 1 { - return -1, errors.New("unexpected fd count") - } else { - return fds[0], nil - } -} - -// Try runs shim and stops execution if FORTIFY_SHIM is set. -func Try() { - if args := flag.Args(); len(args) == 1 && args[0] == "shim" { - if s, ok := os.LookupEnv(EnvShim); ok { - doShim(s) - panic("unreachable") - } - } -} diff --git a/internal/shim/parent.go b/internal/shim/parent.go deleted file mode 100644 index e0696671..00000000 --- a/internal/shim/parent.go +++ /dev/null @@ -1,200 +0,0 @@ -package shim - -import ( - "errors" - "net" - "os" - "os/exec" - "sync" - "sync/atomic" - "syscall" - "time" - - "git.ophivana.moe/security/fortify/acl" - "git.ophivana.moe/security/fortify/internal/fmsg" -) - -// used by the parent process - -type Shim struct { - // user switcher process - cmd *exec.Cmd - // uid of shim target user - uid uint32 - // whether to check shim pid - checkPid bool - // user switcher executable path - executable string - // path to setup socket - socket string - // shim setup abort reason and completion - abort chan error - abortErr atomic.Pointer[error] - abortOnce sync.Once - // wayland mediation, nil if disabled - wl *Wayland - // shim setup payload - payload *Payload -} - -func New(executable string, uid uint32, socket string, wl *Wayland, payload *Payload, checkPid bool) *Shim { - return &Shim{uid: uid, executable: executable, socket: socket, wl: wl, payload: payload, checkPid: checkPid} -} - -func (s *Shim) String() string { - if s.cmd == nil { - return "(unused shim manager)" - } - return s.cmd.String() -} - -func (s *Shim) Unwrap() *exec.Cmd { - return s.cmd -} - -func (s *Shim) Abort(err error) { - s.abortOnce.Do(func() { - s.abortErr.Store(&err) - // s.abort is buffered so this will never block - s.abort <- err - }) -} - -func (s *Shim) AbortWait(err error) { - s.Abort(err) - <-s.abort -} - -type CommandBuilder func(shimEnv string) (args []string) - -func (s *Shim) Start(f CommandBuilder) (*time.Time, error) { - var ( - cf chan *net.UnixConn - accept func() - ) - - // listen on setup socket - if c, a, err := s.serve(); err != nil { - return nil, fmsg.WrapErrorSuffix(err, - "cannot listen on shim setup socket:") - } else { - // accepts a connection after each call to accept - // connections are sent to the channel cf - cf, accept = c, a - } - - // start user switcher process and save time - s.cmd = exec.Command(s.executable, f(EnvShim+"="+s.socket)...) - s.cmd.Env = []string{} - 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 - if err := s.cmd.Start(); err != nil { - return nil, fmsg.WrapErrorSuffix(err, - "cannot start user switcher:") - } - startTime := time.Now().UTC() - - // kill shim if something goes wrong and an error is returned - killShim := func() { - if err := s.cmd.Process.Signal(os.Interrupt); err != nil { - fmsg.Println("cannot terminate shim on faulted setup:", err) - } - } - defer func() { killShim() }() - - accept() - conn := <-cf - if conn == nil { - return &startTime, fmsg.WrapErrorSuffix(*s.abortErr.Load(), "cannot accept call on setup socket:") - } - - // authenticate against called provided uid and shim pid - if cred, err := peerCred(conn); err != nil { - return &startTime, fmsg.WrapErrorSuffix(*s.abortErr.Load(), "cannot retrieve shim credentials:") - } else if cred.Uid != s.uid { - fmsg.Printf("process %d owned by user %d tried to connect, expecting %d", - cred.Pid, cred.Uid, s.uid) - err = errors.New("compromised fortify build") - s.Abort(err) - return &startTime, err - } else if s.checkPid && 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") - s.Abort(err) - return &startTime, err - } - - // serve payload and wayland fd if enabled - // this also closes the connection - err := s.payload.serve(conn, s.wl) - if err == nil { - killShim = func() {} - } - s.Abort(err) // aborting with nil indicates success - return &startTime, err -} - -func (s *Shim) serve() (chan *net.UnixConn, func(), error) { - if s.abort != nil { - panic("attempted to serve shim setup twice") - } - s.abort = make(chan error, 1) - - cf := make(chan *net.UnixConn) - accept := make(chan struct{}, 1) - - if l, err := net.ListenUnix("unix", &net.UnixAddr{Name: s.socket, Net: "unix"}); err != nil { - return nil, nil, err - } else { - l.SetUnlinkOnClose(true) - - fmsg.VPrintf("listening on shim setup socket %q", s.socket) - if err = acl.UpdatePerm(s.socket, int(s.uid), acl.Read, acl.Write, acl.Execute); err != nil { - fmsg.Println("cannot append ACL entry to shim setup socket:", err) - s.Abort(err) // ensures setup socket cleanup - } - - go func() { - for { - select { - case err = <-s.abort: - if err != nil { - fmsg.VPrintln("aborting shim setup, reason:", err) - } - if err = l.Close(); err != nil { - fmsg.Println("cannot close setup socket:", err) - } - close(s.abort) - close(cf) - return - case <-accept: - if conn, err0 := l.AcceptUnix(); err0 != nil { - s.Abort(err0) // does not block, breaks loop - cf <- nil // receiver sees nil value and loads err0 stored during abort - } else { - cf <- conn - } - } - } - }() - } - - return cf, func() { accept <- struct{}{} }, nil -} - -// peerCred fetches peer credentials of conn -func peerCred(conn *net.UnixConn) (ucred *syscall.Ucred, err error) { - var raw syscall.RawConn - if raw, err = conn.SyscallConn(); err != nil { - return - } - - err0 := raw.Control(func(fd uintptr) { - ucred, err = syscall.GetsockoptUcred(int(fd), syscall.SOL_SOCKET, syscall.SO_PEERCRED) - }) - err = errors.Join(err, err0) - return -} diff --git a/internal/shim/payload.go b/internal/shim/payload.go deleted file mode 100644 index ba39ec04..00000000 --- a/internal/shim/payload.go +++ /dev/null @@ -1,42 +0,0 @@ -package shim - -import ( - "encoding/gob" - "errors" - "net" - - "git.ophivana.moe/security/fortify/helper/bwrap" - "git.ophivana.moe/security/fortify/internal/fmsg" -) - -const EnvShim = "FORTIFY_SHIM" - -type Payload struct { - // child full argv - Argv []string - // fortify, bwrap, target full exec path - Exec [3]string - // bwrap config - Bwrap *bwrap.Config - // whether to pass wayland fd - WL bool - - // verbosity pass through - Verbose bool -} - -func (p *Payload) serve(conn *net.UnixConn, wl *Wayland) error { - if err := gob.NewEncoder(conn).Encode(*p); err != nil { - return fmsg.WrapErrorSuffix(err, - "cannot stream shim payload:") - } - - if wl != nil { - if err := wl.WriteUnix(conn); err != nil { - return errors.Join(err, conn.Close()) - } - } - - return fmsg.WrapErrorSuffix(conn.Close(), - "cannot close setup connection:") -} diff --git a/internal/shim/wayland.go b/internal/shim/wayland.go deleted file mode 100644 index 3bac55bc..00000000 --- a/internal/shim/wayland.go +++ /dev/null @@ -1,75 +0,0 @@ -package shim - -import ( - "fmt" - "net" - "sync" - "syscall" - - "git.ophivana.moe/security/fortify/internal/fmsg" -) - -// Wayland implements wayland mediation. -type Wayland struct { - // wayland socket path - Path string - - // wayland connection - conn *net.UnixConn - - connErr error - sync.Once - // wait for wayland client to exit - done chan struct{} -} - -func (wl *Wayland) WriteUnix(conn *net.UnixConn) error { - // connect to host wayland socket - if f, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: wl.Path, Net: "unix"}); err != nil { - return fmsg.WrapErrorSuffix(err, - fmt.Sprintf("cannot connect to wayland at %q:", wl.Path)) - } else { - fmsg.VPrintf("connected to wayland at %q", wl.Path) - wl.conn = f - } - - // set up for passing wayland socket - if rc, err := wl.conn.SyscallConn(); err != nil { - return fmsg.WrapErrorSuffix(err, "cannot obtain raw wayland connection:") - } else { - ec := make(chan error) - go func() { - // pass wayland connection fd - if err = rc.Control(func(fd uintptr) { - if _, _, err = conn.WriteMsgUnix(nil, syscall.UnixRights(int(fd)), nil); err != nil { - ec <- fmsg.WrapErrorSuffix(err, "cannot pass wayland connection to shim:") - return - } - ec <- nil - - // block until shim exits - <-wl.done - fmsg.VPrintln("releasing wayland connection") - }); err != nil { - ec <- fmsg.WrapErrorSuffix(err, "cannot obtain wayland connection fd:") - return - } - }() - return <-ec - } -} - -func (wl *Wayland) Close() error { - wl.Do(func() { - close(wl.done) - wl.connErr = wl.conn.Close() - }) - - return wl.connErr -} - -func NewWayland() *Wayland { - wl := new(Wayland) - wl.done = make(chan struct{}) - return wl -} |
