cmd: shim and init into separate binaries
test / test (push) Successful in 19s Details

This change also fixes a deadlock when shim fails to connect and complete the setup.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
This commit is contained in:
Ophestra Umiker 2024-11-02 03:03:44 +09:00
parent 4b7b899bb3
commit 584732f80a
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
27 changed files with 350 additions and 218 deletions

View File

@ -1,6 +1,6 @@
package init0 package init0
const EnvInit = "FORTIFY_INIT" const Env = "FORTIFY_INIT"
type Payload struct { type Payload struct {
// target full exec path // target full exec path

View File

@ -1,9 +1,8 @@
package init0 package main
import ( import (
"encoding/gob" "encoding/gob"
"errors" "errors"
"flag"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
@ -12,58 +11,80 @@ import (
"syscall" "syscall"
"time" "time"
init0 "git.ophivana.moe/security/fortify/cmd/finit/ipc"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
const ( 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 residualProcessTimeout = 5 * time.Second
) )
// everything beyond this point runs within pid namespace // everything beyond this point runs within pid namespace
// proceed with caution! // proceed with caution!
func doInit(fd uintptr) { func main() {
// sharing stdout with shim
// USE WITH CAUTION
fmsg.SetPrefix("init") 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 // re-exec
if len(os.Args) > 0 && os.Args[0] != "fortify" && path.IsAbs(os.Args[0]) { 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{"fortify", "init"}, os.Environ()); err != nil { if err := syscall.Exec(os.Args[0], []string{"finit"}, os.Environ()); err != nil {
fmsg.Println("cannot re-exec self:", err) fmsg.Println("cannot re-exec self:", err)
// continue anyway // continue anyway
} }
} }
var payload Payload // setup pipe fd from environment
p := os.NewFile(fd, "config-stream") var setup *os.File
if p == nil { if s, ok := os.LookupEnv(init0.Env); !ok {
fmsg.Fatal("invalid config descriptor") fmsg.Fatal("FORTIFY_INIT not set")
} panic("unreachable")
if err := gob.NewDecoder(p).Decode(&payload); err != nil { } else {
fmsg.Fatal("cannot decode init payload:", err) 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 { } else {
// sharing stdout with parent
// USE WITH CAUTION
fmsg.SetVerbose(payload.Verbose) fmsg.SetVerbose(payload.Verbose)
// child does not need to see this // child does not need to see this
if err = os.Unsetenv(EnvInit); err != nil { if err = os.Unsetenv(init0.Env); err != nil {
fmsg.Println("cannot unset", EnvInit+":", err) fmsg.Printf("cannot unset %s: %v", init0.Env, err)
// not fatal // not fatal
} else { } else {
fmsg.VPrintln("received configuration") 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 // die with parent
if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGKILL), 0); errno != 0 { if err := internal.PR_SET_PDEATHSIG__SIGKILL(); err != nil {
fmsg.Fatal("prctl(PR_SET_PDEATHSIG, SIGKILL):", errno.Error()) fmsg.Fatalf("prctl(PR_SET_PDEATHSIG, SIGKILL): %v", err)
} }
cmd := exec.Command(payload.Argv0) cmd := exec.Command(payload.Argv0)
@ -82,6 +103,13 @@ func doInit(fd uintptr) {
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
fmsg.Fatalf("cannot start %q: %v", payload.Argv0, err) 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) sig := make(chan os.Signal, 2)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
@ -122,6 +150,7 @@ func doInit(fd uintptr) {
close(done) close(done)
}() }()
// closed after residualProcessTimeout has elapsed after initial process death
timeout := make(chan struct{}) timeout := make(chan struct{})
r := 2 r := 2
@ -129,9 +158,13 @@ func doInit(fd uintptr) {
select { select {
case s := <-sig: case s := <-sig:
fmsg.VPrintln("received", s.String()) fmsg.VPrintln("received", s.String())
fmsg.Resume() // output could still be withheld at this point, so resume is called
fmsg.Exit(0) fmsg.Exit(0)
case w := <-info: case w := <-info:
if w.wpid == cmd.Process.Pid { if w.wpid == cmd.Process.Pid {
// initial process exited, output is most likely available again
fmsg.Resume()
switch { switch {
case w.wstatus.Exited(): case w.wstatus.Exited():
r = w.wstatus.ExitStatus() 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")
}
}
}

View File

@ -1,4 +1,4 @@
package shim package shim0
import ( import (
"encoding/gob" "encoding/gob"
@ -9,13 +9,13 @@ import (
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
const EnvShim = "FORTIFY_SHIM" const Env = "FORTIFY_SHIM"
type Payload struct { type Payload struct {
// child full argv // child full argv
Argv []string Argv []string
// fortify, bwrap, target full exec path // bwrap, target full exec path
Exec [3]string Exec [2]string
// bwrap config // bwrap config
Bwrap *bwrap.Config Bwrap *bwrap.Config
// whether to pass wayland fd // whether to pass wayland fd
@ -25,7 +25,7 @@ type Payload struct {
Verbose bool 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 { if err := gob.NewEncoder(conn).Encode(*p); err != nil {
return fmsg.WrapErrorSuffix(err, return fmsg.WrapErrorSuffix(err,
"cannot stream shim payload:") "cannot stream shim payload:")

View File

@ -11,9 +11,12 @@ import (
"time" "time"
"git.ophivana.moe/security/fortify/acl" "git.ophivana.moe/security/fortify/acl"
shim0 "git.ophivana.moe/security/fortify/cmd/fshim/ipc"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
const shimSetupTimeout = 5 * time.Second
// used by the parent process // used by the parent process
type Shim struct { type Shim struct {
@ -32,12 +35,12 @@ type Shim struct {
abortErr atomic.Pointer[error] abortErr atomic.Pointer[error]
abortOnce sync.Once abortOnce sync.Once
// wayland mediation, nil if disabled // wayland mediation, nil if disabled
wl *Wayland wl *shim0.Wayland
// shim setup payload // 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} 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 // 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.Env = []string{}
s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
s.cmd.Dir = "/" s.cmd.Dir = "/"
@ -105,9 +108,18 @@ func (s *Shim) Start(f CommandBuilder) (*time.Time, error) {
defer func() { killShim() }() defer func() { killShim() }()
accept() accept()
conn := <-cf var conn *net.UnixConn
if conn == nil { select {
return &startTime, fmsg.WrapErrorSuffix(*s.abortErr.Load(), "cannot accept call on setup socket:") 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 // 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 // serve payload and wayland fd if enabled
// this also closes the connection // this also closes the connection
err := s.payload.serve(conn, s.wl) err := s.payload.Serve(conn, s.wl)
if err == nil { if err == nil {
killShim = func() {} killShim = func() {}
} }
@ -158,6 +170,7 @@ func (s *Shim) serve() (chan *net.UnixConn, func(), error) {
} }
go func() { go func() {
cfWg := new(sync.WaitGroup)
for { for {
select { select {
case err = <-s.abort: case err = <-s.abort:
@ -168,15 +181,24 @@ func (s *Shim) serve() (chan *net.UnixConn, func(), error) {
fmsg.Println("cannot close setup socket:", err) fmsg.Println("cannot close setup socket:", err)
} }
close(s.abort) close(s.abort)
close(cf) go func() {
cfWg.Wait()
close(cf)
}()
return return
case <-accept: case <-accept:
if conn, err0 := l.AcceptUnix(); err0 != nil { cfWg.Add(1)
s.Abort(err0) // does not block, breaks loop go func() {
cf <- nil // receiver sees nil value and loads err0 stored during abort defer cfWg.Done()
} else { if conn, err0 := l.AcceptUnix(); err0 != nil {
cf <- conn // breaks loop
} s.Abort(err0)
// receiver sees nil value and loads err0 stored during abort
cf <- nil
} else {
cf <- conn
}
}()
} }
} }
}() }()

View File

@ -1,4 +1,4 @@
package shim package shim0
import ( import (
"fmt" "fmt"

View File

@ -1,37 +1,63 @@
package shim package main
import ( import (
"encoding/gob" "encoding/gob"
"errors" "errors"
"flag"
"net" "net"
"os" "os"
"path" "path"
"strconv" "strconv"
"syscall" "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/helper"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg" "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! // proceed with caution!
func doShim(socket string) { func main() {
// sharing stdout with fortify
// USE WITH CAUTION
fmsg.SetPrefix("shim") 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 // re-exec
if len(os.Args) > 0 && os.Args[0] != "fortify" && path.IsAbs(os.Args[0]) { 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{"fortify", "shim"}, os.Environ()); err != nil { if err := syscall.Exec(os.Args[0], []string{"fshim"}, os.Environ()); err != nil {
fmsg.Println("cannot re-exec self:", err) fmsg.Println("cannot re-exec self:", err)
// continue anyway // 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 // dial setup socket
var conn *net.UnixConn 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) fmsg.Fatal("cannot dial setup socket:", err)
panic("unreachable") panic("unreachable")
} else { } else {
@ -39,12 +65,10 @@ func doShim(socket string) {
} }
// decode payload gob stream // decode payload gob stream
var payload Payload var payload shim.Payload
if err := gob.NewDecoder(conn).Decode(&payload); err != nil { if err := gob.NewDecoder(conn).Decode(&payload); err != nil {
fmsg.Fatal("cannot decode shim payload:", err) fmsg.Fatal("cannot decode shim payload:", err)
} else { } else {
// sharing stdout with parent
// USE WITH CAUTION
fmsg.SetVerbose(payload.Verbose) fmsg.SetVerbose(payload.Verbose)
} }
@ -74,7 +98,7 @@ func doShim(socket string) {
ic.Argv = payload.Argv ic.Argv = payload.Argv
if len(ic.Argv) > 0 { if len(ic.Argv) > 0 {
// looked up from $PATH by parent // looked up from $PATH by parent
ic.Argv0 = payload.Exec[2] ic.Argv0 = payload.Exec[1]
} else { } else {
// no argv, look up shell instead // no argv, look up shell instead
var ok bool var ok bool
@ -103,7 +127,7 @@ func doShim(socket string) {
if r, w, err := os.Pipe(); err != nil { if r, w, err := os.Pipe(); err != nil {
fmsg.Fatal("cannot pipe:", err) fmsg.Fatal("cannot pipe:", err)
} else { } else {
conf.SetEnv[init0.EnvInit] = strconv.Itoa(3 + len(extraFiles)) conf.SetEnv[init0.Env] = strconv.Itoa(3 + len(extraFiles))
extraFiles = append(extraFiles, r) extraFiles = append(extraFiles, r)
fmsg.VPrintln("transmitting config to init") fmsg.VPrintln("transmitting config to init")
@ -115,8 +139,9 @@ func doShim(socket string) {
}() }()
} }
helper.BubblewrapName = payload.Exec[1] // resolved bwrap path by parent helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
if b, err := helper.NewBwrap(conf, nil, payload.Exec[0], func(int, int) []string { return []string{"init"} }); err != nil { 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.Fatal("malformed sandbox config:", err)
} else { } else {
cmd := b.Unwrap() cmd := b.Unwrap()
@ -167,13 +192,3 @@ func receiveWLfd(conn *net.UnixConn) (int, error) {
return fds[0], nil 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")
}
}
}

View File

@ -8,19 +8,16 @@ import (
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
"git.ophivana.moe/security/fortify/internal"
) )
const ( const (
fsuConfFile = "/etc/fsurc" fsuConfFile = "/etc/fsurc"
envShim = "FORTIFY_SHIM" envShim = "FORTIFY_SHIM"
envAID = "FORTIFY_APP_ID" envAID = "FORTIFY_APP_ID"
fpPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
) )
// FortifyPath is the path to fortify, set at compile time.
var FortifyPath = fpPoison
func main() { func main() {
log.SetFlags(0) log.SetFlags(0)
log.SetPrefix("fsu: ") log.SetPrefix("fsu: ")
@ -35,9 +32,11 @@ func main() {
log.Fatal("this program must not be started by root") log.Fatal("this program must not be started by root")
} }
// validate compiled in fortify path var fmain string
if FortifyPath == fpPoison || !path.IsAbs(FortifyPath) { if p, ok := internal.Path(internal.Fmain); !ok {
log.Fatal("invalid fortify path, this copy of fsu is not compiled correctly") 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") pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
@ -45,7 +44,7 @@ func main() {
log.Fatalf("cannot read parent executable path: %v", err) log.Fatalf("cannot read parent executable path: %v", err)
} else if strings.HasSuffix(p, " (deleted)") { } else if strings.HasSuffix(p, " (deleted)") {
log.Fatal("fortify executable has been 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") log.Fatal("this program must be started by fortify")
} }
@ -86,7 +85,7 @@ func main() {
if err := syscall.Setresuid(uid, uid, uid); err != nil { if err := syscall.Setresuid(uid, uid, uid); err != nil {
log.Fatalf("cannot set uid: %v", err) 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) log.Fatalf("cannot start shim: %v", err)
} }

View File

@ -3,8 +3,8 @@ package app
import ( import (
"sync" "sync"
"git.ophivana.moe/security/fortify/internal" "git.ophivana.moe/security/fortify/cmd/fshim/ipc/shim"
"git.ophivana.moe/security/fortify/internal/shim" "git.ophivana.moe/security/fortify/internal/linux"
) )
type App interface { type App interface {
@ -25,7 +25,7 @@ type app struct {
// application unique identifier // application unique identifier
id *ID id *ID
// operating system interface // operating system interface
os internal.System os linux.System
// shim process manager // shim process manager
shim *shim.Shim shim *shim.Shim
// child process related information // child process related information
@ -63,7 +63,7 @@ func (a *app) WaitErr() error {
return a.waitErr return a.waitErr
} }
func New(os internal.System) (App, error) { func New(os linux.System) (App, error) {
a := new(app) a := new(app)
a.id = new(ID) a.id = new(ID)
a.os = os a.os = os

View File

@ -9,8 +9,8 @@ import (
"git.ophivana.moe/security/fortify/acl" "git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/helper/bwrap" "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/app"
"git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/system" "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)) panic("called exit on stub with code " + strconv.Itoa(code))
} }
func (s *stubNixOS) Paths() internal.Paths { func (s *stubNixOS) FshimPath() string {
return internal.Paths{ return "/nix/store/00000000000000000000000000000000-fortify-0.0.10/bin/.fshim"
}
func (s *stubNixOS) Paths() linux.Paths {
return linux.Paths{
SharePath: "/tmp/fortify.1971", SharePath: "/tmp/fortify.1971",
RuntimePath: "/run/user/1971", RuntimePath: "/run/user/1971",
RunDirPath: "/run/user/1971/fortify", RunDirPath: "/run/user/1971/fortify",

View File

@ -7,14 +7,14 @@ import (
"time" "time"
"git.ophivana.moe/security/fortify/helper/bwrap" "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/app"
"git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
type sealTestCase struct { type sealTestCase struct {
name string name string
os internal.System os linux.System
config *app.Config config *app.Config
id app.ID id app.ID
wantSys *system.I wantSys *system.I

View File

@ -2,11 +2,11 @@ package app
import ( import (
"git.ophivana.moe/security/fortify/helper/bwrap" "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" "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 := new(app)
a.id = &id a.id = &id
a.os = os a.os = os

View File

@ -47,8 +47,8 @@ func (a *app) commandBuilderMachineCtl(shimEnv string) (args []string) {
} }
innerCommand.WriteString("; ") innerCommand.WriteString("; ")
// launch fortify as shim // launch fortify shim
innerCommand.WriteString("exec " + a.seal.sys.executable + " shim") innerCommand.WriteString("exec " + a.os.FshimPath())
// append inner command // append inner command
args = append(args, innerCommand.String()) args = append(args, innerCommand.String())

View File

@ -24,7 +24,7 @@ func (a *app) commandBuilderSudo(shimEnv string) (args []string) {
args = append(args, shimEnv) args = append(args, shimEnv)
// -- $@ // -- $@
args = append(args, "--", a.seal.sys.executable, "shim") args = append(args, "--", a.os.FshimPath())
return return
} }

View File

@ -7,10 +7,10 @@ import (
"path" "path"
"strconv" "strconv"
shim "git.ophivana.moe/security/fortify/cmd/fshim/ipc"
"git.ophivana.moe/security/fortify/dbus" "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/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/state"
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
@ -66,7 +66,7 @@ type appSeal struct {
// seal system-level component // seal system-level component
sys *appSealSys sys *appSealSys
internal.Paths linux.Paths
// protected by upstream mutex // protected by upstream mutex
} }
@ -127,13 +127,6 @@ func (a *app) Seal(config *Config) error {
// create seal system component // create seal system component
seal.sys = new(appSealSys) 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 // look up user from system
if u, err := a.os.Lookup(config.User); err != nil { if u, err := a.os.Lookup(config.User); err != nil {
if errors.As(err, new(user.UnknownUserError)) { if errors.As(err, new(user.UnknownUserError)) {

View File

@ -5,8 +5,8 @@ import (
"path" "path"
"git.ophivana.moe/security/fortify/acl" "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/fmsg"
"git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
@ -23,7 +23,7 @@ var (
ErrXDisplay = errors.New(display + " unset") 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 // pass $TERM to launcher
if t, ok := os.LookupEnv(term); ok { if t, ok := os.LookupEnv(term); ok {
seal.sys.bwrap.SetEnv[term] = t seal.sys.bwrap.SetEnv[term] = t

View File

@ -6,8 +6,8 @@ import (
"io/fs" "io/fs"
"path" "path"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
@ -25,7 +25,7 @@ var (
ErrPulseMode = errors.New("unexpected pulse socket mode") 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) { if !seal.et.Has(system.EPulse) {
return nil 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 // 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 { if p, ok := os.LookupEnv(pulseCookie); ok {
return p, nil return p, nil
} }

View File

@ -4,7 +4,7 @@ import (
"path" "path"
"git.ophivana.moe/security/fortify/acl" "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" "git.ophivana.moe/security/fortify/internal/system"
) )
@ -38,7 +38,7 @@ func (seal *appSeal) shareSystem() {
seal.sys.bwrap.Tmpfs(seal.SharePath, 1*1024*1024) 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 // look up shell
sh := "/bin/sh" sh := "/bin/sh"
if s, ok := os.LookupEnv(shell); ok { if s, ok := os.LookupEnv(shell); ok {

View File

@ -8,9 +8,10 @@ import (
"path/filepath" "path/filepath"
"strings" "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/helper"
"git.ophivana.moe/security/fortify/internal/fmsg" "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/state"
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
@ -22,9 +23,9 @@ func (a *app) Start() error {
defer a.lock.Unlock() defer a.lock.Unlock()
// resolve exec paths // resolve exec paths
shimExec := [3]string{a.seal.sys.executable, helper.BubblewrapName} shimExec := [2]string{helper.BubblewrapName}
if len(a.seal.command) > 0 { if len(a.seal.command) > 0 {
shimExec[2] = a.seal.command[0] shimExec[1] = a.seal.command[0]
} }
for i, n := range shimExec { for i, n := range shimExec {
if len(n) == 0 { if len(n) == 0 {
@ -53,7 +54,7 @@ func (a *app) Start() error {
// construct shim manager // 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(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, Argv: a.seal.command,
Exec: shimExec, Exec: shimExec,
Bwrap: a.seal.sys.bwrap, Bwrap: a.seal.sys.bwrap,

View File

@ -5,7 +5,7 @@ import (
"git.ophivana.moe/security/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/helper/bwrap" "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" "git.ophivana.moe/security/fortify/internal/system"
) )
@ -17,8 +17,6 @@ type appSealSys struct {
// default formatted XDG_RUNTIME_DIR of User // default formatted XDG_RUNTIME_DIR of User
runtime string runtime string
// sealed path to fortify executable, used by shim
executable string
// target user sealed from config // target user sealed from config
user *user.User user *user.User
@ -30,7 +28,7 @@ type appSealSys struct {
} }
// shareAll calls all share methods in sequence // 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 { if seal.shared {
panic("seal shared twice") panic("seal shared twice")
} }

12
internal/comp.go Normal file
View File

@ -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 != ""
}

View File

@ -1,14 +1,10 @@
package internal package linux
import ( import (
"errors"
"io/fs" "io/fs"
"os"
"os/exec"
"os/user" "os/user"
"path" "path"
"strconv" "strconv"
"sync"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
@ -36,6 +32,8 @@ type System interface {
// Exit provides [os.Exit]. // Exit provides [os.Exit].
Exit(code int) Exit(code int)
// FshimPath returns an absolute path to the fshim binary.
FshimPath() string
// Paths returns a populated [Paths] struct. // Paths returns a populated [Paths] struct.
Paths() Paths Paths() Paths
// SdBooted implements https://www.freedesktop.org/software/systemd/man/sd_booted.html // 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) 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
}

83
internal/linux/std.go Normal file
View File

@ -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
}

14
internal/path.go Normal file
View File

@ -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)
}

20
internal/prctl.go Normal file
View File

@ -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
}

12
main.go
View File

@ -4,11 +4,9 @@ import (
"flag" "flag"
"syscall" "syscall"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/app" "git.ophivana.moe/security/fortify/internal/app"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
init0 "git.ophivana.moe/security/fortify/internal/init" "git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/shim"
) )
var ( var (
@ -19,12 +17,12 @@ func init() {
flag.BoolVar(&flagVerbose, "v", false, "Verbose output") flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
} }
var os = new(internal.Std) var os = new(linux.Std)
func main() { func main() {
// linux/sched/coredump.h // linux/sched/coredump.h
if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, 0, 0); errno != 0 { 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() flag.Parse()
@ -34,10 +32,6 @@ func main() {
fmsg.VPrintln("system booted with systemd as init system") fmsg.VPrintln("system booted with systemd as init system")
} }
// shim/init early exit
init0.Try()
shim.Try()
// root check // root check
if os.Geteuid() == 0 { if os.Geteuid() == 0 {
fmsg.Fatal("this program must not run as root") fmsg.Fatal("this program must not run as root")

View File

@ -15,14 +15,27 @@ buildGoModule rec {
src = ./.; src = ./.;
vendorHash = null; vendorHash = null;
ldflags = [ ldflags =
"-s" lib.attrsets.foldlAttrs
"-w" (
"-X" ldflags: name: value:
"main.Version=v${version}" ldflags
"-X" ++ [
"main.FortifyPath=${placeholder "out"}/bin/.fortify-wrapped" "-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 = [ buildInputs = [
acl acl
@ -40,5 +53,7 @@ buildGoModule rec {
} }
mv $out/bin/fsu $out/bin/.fsu mv $out/bin/fsu $out/bin/.fsu
mv $out/bin/fshim $out/bin/.fshim
mv $out/bin/finit $out/bin/.finit
''; '';
} }

View File

@ -3,11 +3,11 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"git.ophivana.moe/security/fortify/internal"
) )
var ( var (
Version = "impure"
printVersion bool printVersion bool
) )
@ -17,7 +17,11 @@ func init() {
func tryVersion() { func tryVersion() {
if printVersion { if printVersion {
fmt.Println(Version) if v, ok := internal.Check(internal.Version); ok {
fmt.Println(v)
} else {
fmt.Println("impure")
}
os.Exit(0) os.Exit(0)
} }
} }