From 584732f80ab91afb349720cfb8e9979ed2ba173e Mon Sep 17 00:00:00 2001 From: Ophestra Umiker Date: Sat, 2 Nov 2024 03:03:44 +0900 Subject: [PATCH] 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 --- {internal/init => cmd/finit/ipc}/payload.go | 2 +- {internal/init => cmd/finit}/main.go | 101 ++++++++++-------- {internal/shim => cmd/fshim/ipc}/payload.go | 10 +- .../parent.go => cmd/fshim/ipc/shim/shim.go | 52 ++++++--- {internal/shim => cmd/fshim/ipc}/wayland.go | 2 +- {internal/shim => cmd/fshim}/main.go | 65 ++++++----- cmd/fsu/main.go | 17 ++- internal/app/app.go | 8 +- internal/app/app_nixos_test.go | 10 +- internal/app/app_test.go | 4 +- internal/app/export_test.go | 4 +- internal/app/launch.machinectl.go | 4 +- internal/app/launch.sudo.go | 2 +- internal/app/seal.go | 13 +-- internal/app/share.display.go | 4 +- internal/app/share.pulse.go | 6 +- internal/app/share.system.go | 4 +- internal/app/start.go | 9 +- internal/app/system.go | 6 +- internal/comp.go | 12 +++ internal/{system.go => linux/interface.go} | 63 +---------- internal/linux/std.go | 83 ++++++++++++++ internal/path.go | 14 +++ internal/prctl.go | 20 ++++ main.go | 12 +-- package.nix | 31 ++++-- version.go | 10 +- 27 files changed, 350 insertions(+), 218 deletions(-) rename {internal/init => cmd/finit/ipc}/payload.go (86%) rename {internal/init => cmd/finit}/main.go (56%) rename {internal/shim => cmd/fshim/ipc}/payload.go (79%) rename internal/shim/parent.go => cmd/fshim/ipc/shim/shim.go (81%) rename {internal/shim => cmd/fshim/ipc}/wayland.go (99%) rename {internal/shim => cmd/fshim}/main.go (70%) create mode 100644 internal/comp.go rename internal/{system.go => linux/interface.go} (54%) create mode 100644 internal/linux/std.go create mode 100644 internal/path.go create mode 100644 internal/prctl.go diff --git a/internal/init/payload.go b/cmd/finit/ipc/payload.go similarity index 86% rename from internal/init/payload.go rename to cmd/finit/ipc/payload.go index 6f4b278..535a0e7 100644 --- a/internal/init/payload.go +++ b/cmd/finit/ipc/payload.go @@ -1,6 +1,6 @@ package init0 -const EnvInit = "FORTIFY_INIT" +const Env = "FORTIFY_INIT" type Payload struct { // target full exec path diff --git a/internal/init/main.go b/cmd/finit/main.go similarity index 56% rename from internal/init/main.go rename to cmd/finit/main.go index f292772..e92af81 100644 --- a/internal/init/main.go +++ b/cmd/finit/main.go @@ -1,9 +1,8 @@ -package init0 +package main import ( "encoding/gob" "errors" - "flag" "os" "os/exec" "os/signal" @@ -12,58 +11,80 @@ import ( "syscall" "time" + init0 "git.ophivana.moe/security/fortify/cmd/finit/ipc" + "git.ophivana.moe/security/fortify/internal" "git.ophivana.moe/security/fortify/internal/fmsg" ) const ( - // time to wait for linger processes after death initial process + // time to wait for linger processes after death of initial process residualProcessTimeout = 5 * time.Second ) // everything beyond this point runs within pid namespace // proceed with caution! -func doInit(fd uintptr) { +func main() { + // sharing stdout with shim + // USE WITH CAUTION fmsg.SetPrefix("init") + // setting this prevents ptrace + if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil { + fmsg.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err) + panic("unreachable") + } + + if os.Getpid() != 1 { + fmsg.Fatal("this process must run as pid 1") + panic("unreachable") + } + // 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 { + if len(os.Args) > 0 && (os.Args[0] != "finit" || len(os.Args) != 1) && path.IsAbs(os.Args[0]) { + if err := syscall.Exec(os.Args[0], []string{"finit"}, 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) + // setup pipe fd from environment + var setup *os.File + if s, ok := os.LookupEnv(init0.Env); !ok { + fmsg.Fatal("FORTIFY_INIT not set") + panic("unreachable") + } else { + if fd, err := strconv.Atoi(s); err != nil { + fmsg.Fatalf("cannot parse %q: %v", s, err) + panic("unreachable") + } else { + setup = os.NewFile(uintptr(fd), "setup") + if setup == nil { + fmsg.Fatal("invalid config descriptor") + panic("unreachable") + } + } + } + + var payload init0.Payload + if err := gob.NewDecoder(setup).Decode(&payload); err != nil { + fmsg.Fatal("cannot decode init setup payload:", err) + panic("unreachable") } 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) + if err = os.Unsetenv(init0.Env); err != nil { + fmsg.Printf("cannot unset %s: %v", init0.Env, 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()) + if err := internal.PR_SET_PDEATHSIG__SIGKILL(); err != nil { + fmsg.Fatalf("prctl(PR_SET_PDEATHSIG, SIGKILL): %v", err) } cmd := exec.Command(payload.Argv0) @@ -82,6 +103,13 @@ func doInit(fd uintptr) { if err := cmd.Start(); err != nil { fmsg.Fatalf("cannot start %q: %v", payload.Argv0, err) } + fmsg.Withhold() + + // close setup pipe as setup is now complete + if err := setup.Close(); err != nil { + fmsg.Println("cannot close setup pipe:", err) + // not fatal + } sig := make(chan os.Signal, 2) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) @@ -122,6 +150,7 @@ func doInit(fd uintptr) { close(done) }() + // closed after residualProcessTimeout has elapsed after initial process death timeout := make(chan struct{}) r := 2 @@ -129,9 +158,13 @@ func doInit(fd uintptr) { select { case s := <-sig: fmsg.VPrintln("received", s.String()) + fmsg.Resume() // output could still be withheld at this point, so resume is called fmsg.Exit(0) case w := <-info: if w.wpid == cmd.Process.Pid { + // initial process exited, output is most likely available again + fmsg.Resume() + switch { case w.wstatus.Exited(): r = w.wstatus.ExitStatus() @@ -154,21 +187,3 @@ func doInit(fd uintptr) { } } } - -// 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/shim/payload.go b/cmd/fshim/ipc/payload.go similarity index 79% rename from internal/shim/payload.go rename to cmd/fshim/ipc/payload.go index ba39ec0..1aaddb1 100644 --- a/internal/shim/payload.go +++ b/cmd/fshim/ipc/payload.go @@ -1,4 +1,4 @@ -package shim +package shim0 import ( "encoding/gob" @@ -9,13 +9,13 @@ import ( "git.ophivana.moe/security/fortify/internal/fmsg" ) -const EnvShim = "FORTIFY_SHIM" +const Env = "FORTIFY_SHIM" type Payload struct { // child full argv Argv []string - // fortify, bwrap, target full exec path - Exec [3]string + // bwrap, target full exec path + Exec [2]string // bwrap config Bwrap *bwrap.Config // whether to pass wayland fd @@ -25,7 +25,7 @@ type Payload struct { Verbose bool } -func (p *Payload) serve(conn *net.UnixConn, wl *Wayland) error { +func (p *Payload) Serve(conn *net.UnixConn, wl *Wayland) error { if err := gob.NewEncoder(conn).Encode(*p); err != nil { return fmsg.WrapErrorSuffix(err, "cannot stream shim payload:") diff --git a/internal/shim/parent.go b/cmd/fshim/ipc/shim/shim.go similarity index 81% rename from internal/shim/parent.go rename to cmd/fshim/ipc/shim/shim.go index e069667..5e1606e 100644 --- a/internal/shim/parent.go +++ b/cmd/fshim/ipc/shim/shim.go @@ -11,9 +11,12 @@ import ( "time" "git.ophivana.moe/security/fortify/acl" + shim0 "git.ophivana.moe/security/fortify/cmd/fshim/ipc" "git.ophivana.moe/security/fortify/internal/fmsg" ) +const shimSetupTimeout = 5 * time.Second + // used by the parent process type Shim struct { @@ -32,12 +35,12 @@ type Shim struct { abortErr atomic.Pointer[error] abortOnce sync.Once // wayland mediation, nil if disabled - wl *Wayland + wl *shim0.Wayland // shim setup payload - payload *Payload + payload *shim0.Payload } -func New(executable string, uid uint32, socket string, wl *Wayland, payload *Payload, checkPid bool) *Shim { +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} } @@ -84,7 +87,7 @@ func (s *Shim) Start(f CommandBuilder) (*time.Time, error) { } // start user switcher process and save time - s.cmd = exec.Command(s.executable, f(EnvShim+"="+s.socket)...) + s.cmd = exec.Command(s.executable, f(shim0.Env+"="+s.socket)...) s.cmd.Env = []string{} s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr s.cmd.Dir = "/" @@ -105,9 +108,18 @@ func (s *Shim) Start(f CommandBuilder) (*time.Time, error) { defer func() { killShim() }() accept() - conn := <-cf - if conn == nil { - return &startTime, fmsg.WrapErrorSuffix(*s.abortErr.Load(), "cannot accept call on setup socket:") + var conn *net.UnixConn + select { + case c := <-cf: + if c == nil { + return &startTime, fmsg.WrapErrorSuffix(*s.abortErr.Load(), "cannot accept call on setup socket:") + } else { + conn = c + } + case <-time.After(shimSetupTimeout): + err := errors.New("timed out waiting for shim") + s.AbortWait(err) + return &startTime, err } // authenticate against called provided uid and shim pid @@ -129,7 +141,7 @@ func (s *Shim) Start(f CommandBuilder) (*time.Time, error) { // serve payload and wayland fd if enabled // this also closes the connection - err := s.payload.serve(conn, s.wl) + err := s.payload.Serve(conn, s.wl) if err == nil { killShim = func() {} } @@ -158,6 +170,7 @@ func (s *Shim) serve() (chan *net.UnixConn, func(), error) { } go func() { + cfWg := new(sync.WaitGroup) for { select { case err = <-s.abort: @@ -168,15 +181,24 @@ func (s *Shim) serve() (chan *net.UnixConn, func(), error) { fmsg.Println("cannot close setup socket:", err) } close(s.abort) - close(cf) + go func() { + cfWg.Wait() + 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 - } + cfWg.Add(1) + go func() { + defer cfWg.Done() + if conn, err0 := l.AcceptUnix(); err0 != nil { + // breaks loop + s.Abort(err0) + // receiver sees nil value and loads err0 stored during abort + cf <- nil + } else { + cf <- conn + } + }() } } }() diff --git a/internal/shim/wayland.go b/cmd/fshim/ipc/wayland.go similarity index 99% rename from internal/shim/wayland.go rename to cmd/fshim/ipc/wayland.go index 3bac55b..132e74f 100644 --- a/internal/shim/wayland.go +++ b/cmd/fshim/ipc/wayland.go @@ -1,4 +1,4 @@ -package shim +package shim0 import ( "fmt" diff --git a/internal/shim/main.go b/cmd/fshim/main.go similarity index 70% rename from internal/shim/main.go rename to cmd/fshim/main.go index 52a8fa9..43248f6 100644 --- a/internal/shim/main.go +++ b/cmd/fshim/main.go @@ -1,37 +1,63 @@ -package shim +package main import ( "encoding/gob" "errors" - "flag" "net" "os" "path" "strconv" "syscall" + init0 "git.ophivana.moe/security/fortify/cmd/finit/ipc" + shim "git.ophivana.moe/security/fortify/cmd/fshim/ipc" "git.ophivana.moe/security/fortify/helper" + "git.ophivana.moe/security/fortify/internal" "git.ophivana.moe/security/fortify/internal/fmsg" - init0 "git.ophivana.moe/security/fortify/internal/init" ) -// everything beyond this point runs as target user +// everything beyond this point runs as unconstrained target user // proceed with caution! -func doShim(socket string) { +func main() { + // sharing stdout with fortify + // USE WITH CAUTION fmsg.SetPrefix("shim") + // setting this prevents ptrace + if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil { + fmsg.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err) + panic("unreachable") + } + // 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 { + if len(os.Args) > 0 && (os.Args[0] != "fshim" || len(os.Args) != 1) && path.IsAbs(os.Args[0]) { + if err := syscall.Exec(os.Args[0], []string{"fshim"}, os.Environ()); err != nil { fmsg.Println("cannot re-exec self:", err) // continue anyway } } + // lookup socket path from environment + var socketPath string + if s, ok := os.LookupEnv(shim.Env); !ok { + fmsg.Fatal("FORTIFY_SHIM not set") + panic("unreachable") + } else { + socketPath = s + } + + // check path to finit + var finitPath string + if p, ok := internal.Path(internal.Finit); !ok { + fmsg.Fatal("invalid finit path, this copy of fshim is not compiled correctly") + } else { + finitPath = p + } + // dial setup socket var conn *net.UnixConn - if c, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: socket, Net: "unix"}); err != nil { + if c, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: socketPath, Net: "unix"}); err != nil { fmsg.Fatal("cannot dial setup socket:", err) panic("unreachable") } else { @@ -39,12 +65,10 @@ func doShim(socket string) { } // decode payload gob stream - var payload Payload + var payload shim.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) } @@ -74,7 +98,7 @@ func doShim(socket string) { ic.Argv = payload.Argv if len(ic.Argv) > 0 { // looked up from $PATH by parent - ic.Argv0 = payload.Exec[2] + ic.Argv0 = payload.Exec[1] } else { // no argv, look up shell instead var ok bool @@ -103,7 +127,7 @@ func doShim(socket string) { if r, w, err := os.Pipe(); err != nil { fmsg.Fatal("cannot pipe:", err) } else { - conf.SetEnv[init0.EnvInit] = strconv.Itoa(3 + len(extraFiles)) + conf.SetEnv[init0.Env] = strconv.Itoa(3 + len(extraFiles)) extraFiles = append(extraFiles, r) fmsg.VPrintln("transmitting config to init") @@ -115,8 +139,9 @@ func doShim(socket string) { }() } - 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 { + 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) } else { cmd := b.Unwrap() @@ -167,13 +192,3 @@ func receiveWLfd(conn *net.UnixConn) (int, error) { 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/cmd/fsu/main.go b/cmd/fsu/main.go index af0315e..2d3ad8b 100644 --- a/cmd/fsu/main.go +++ b/cmd/fsu/main.go @@ -8,19 +8,16 @@ import ( "strconv" "strings" "syscall" + + "git.ophivana.moe/security/fortify/internal" ) const ( fsuConfFile = "/etc/fsurc" envShim = "FORTIFY_SHIM" envAID = "FORTIFY_APP_ID" - - fpPoison = "INVALIDINVALIDINVALIDINVALIDINVALID" ) -// FortifyPath is the path to fortify, set at compile time. -var FortifyPath = fpPoison - func main() { log.SetFlags(0) log.SetPrefix("fsu: ") @@ -35,9 +32,11 @@ func main() { log.Fatal("this program must not be started by root") } - // validate compiled in fortify path - if FortifyPath == fpPoison || !path.IsAbs(FortifyPath) { + var fmain string + if p, ok := internal.Path(internal.Fmain); !ok { log.Fatal("invalid fortify path, this copy of fsu is not compiled correctly") + } else { + fmain = p } pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe") @@ -45,7 +44,7 @@ func main() { log.Fatalf("cannot read parent executable path: %v", err) } else if strings.HasSuffix(p, " (deleted)") { log.Fatal("fortify executable has been deleted") - } else if p != FortifyPath { + } else if p != fmain { log.Fatal("this program must be started by fortify") } @@ -86,7 +85,7 @@ func main() { if err := syscall.Setresuid(uid, uid, uid); err != nil { log.Fatalf("cannot set uid: %v", err) } - if err := syscall.Exec(FortifyPath, []string{"fortify", "shim"}, []string{envShim + "=" + shimSetupPath}); err != nil { + if err := syscall.Exec(fmain, []string{"fortify", "shim"}, []string{envShim + "=" + shimSetupPath}); err != nil { log.Fatalf("cannot start shim: %v", err) } diff --git a/internal/app/app.go b/internal/app/app.go index 264975c..28564eb 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 1107921..f7eb4c0 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 fd223af..de68e53 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 8e52468..56eb4a3 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 c1d2610..3c196b8 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 e7a2067..dbb4931 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 80c3ec2..502ec6e 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 9513211..7e814a8 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 fa5c0a7..1431534 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 600119e..cf2df94 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 a50be6e..376d306 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 74b8e61..3c17cf4 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 0000000..e7064db --- /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/system.go b/internal/linux/interface.go similarity index 54% rename from internal/system.go rename to internal/linux/interface.go index 3e973b5..cf984a3 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 0000000..704ba16 --- /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 0000000..1332276 --- /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 0000000..1c03edd --- /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/main.go b/main.go index 746a233..4ffca31 100644 --- a/main.go +++ b/main.go @@ -4,11 +4,9 @@ import ( "flag" "syscall" - "git.ophivana.moe/security/fortify/internal" "git.ophivana.moe/security/fortify/internal/app" "git.ophivana.moe/security/fortify/internal/fmsg" - init0 "git.ophivana.moe/security/fortify/internal/init" - "git.ophivana.moe/security/fortify/internal/shim" + "git.ophivana.moe/security/fortify/internal/linux" ) var ( @@ -19,12 +17,12 @@ func init() { flag.BoolVar(&flagVerbose, "v", false, "Verbose output") } -var os = new(internal.Std) +var os = new(linux.Std) func main() { // linux/sched/coredump.h if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, 0, 0); errno != 0 { - fmsg.Printf("fortify: cannot set SUID_DUMP_DISABLE: %s", errno.Error()) + fmsg.Printf("cannot set SUID_DUMP_DISABLE: %s", errno.Error()) } flag.Parse() @@ -34,10 +32,6 @@ func main() { fmsg.VPrintln("system booted with systemd as init system") } - // shim/init early exit - init0.Try() - shim.Try() - // root check if os.Geteuid() == 0 { fmsg.Fatal("this program must not run as root") diff --git a/package.nix b/package.nix index e96a60c..39dcff9 100644 --- a/package.nix +++ b/package.nix @@ -15,14 +15,27 @@ buildGoModule rec { src = ./.; vendorHash = null; - ldflags = [ - "-s" - "-w" - "-X" - "main.Version=v${version}" - "-X" - "main.FortifyPath=${placeholder "out"}/bin/.fortify-wrapped" - ]; + ldflags = + lib.attrsets.foldlAttrs + ( + ldflags: name: value: + ldflags + ++ [ + "-X" + "git.ophivana.moe/security/fortify/internal.${name}=${value}" + ] + ) + [ + "-s" + "-w" + ] + { + Version = "v${version}"; + Fmain = "${placeholder "out"}/bin/.fortify-wrapped"; + Fsu = "/run/wrappers/bin/fsu"; + Fshim = "${placeholder "out"}/bin/.fshim"; + Finit = "${placeholder "out"}/bin/.finit"; + }; buildInputs = [ acl @@ -40,5 +53,7 @@ buildGoModule rec { } mv $out/bin/fsu $out/bin/.fsu + mv $out/bin/fshim $out/bin/.fshim + mv $out/bin/finit $out/bin/.finit ''; } diff --git a/version.go b/version.go index d3136cd..4028b16 100644 --- a/version.go +++ b/version.go @@ -3,11 +3,11 @@ package main import ( "flag" "fmt" + + "git.ophivana.moe/security/fortify/internal" ) var ( - Version = "impure" - printVersion bool ) @@ -17,7 +17,11 @@ func init() { func tryVersion() { if printVersion { - fmt.Println(Version) + if v, ok := internal.Check(internal.Version); ok { + fmt.Println(v) + } else { + fmt.Println("impure") + } os.Exit(0) } }