From 9a1f8e129fe0f9919981b8a1d345a9b455bd76de Mon Sep 17 00:00:00 2001 From: Ophestra Date: Mon, 17 Mar 2025 02:31:46 +0900 Subject: [PATCH] sandbox: wrap fmsg interface Signed-off-by: Ophestra --- cmd/fpkg/main.go | 4 +- dbus/dbus_test.go | 4 +- helper/container_test.go | 4 +- helper/stub.go | 3 +- internal/app/init0/main.go | 5 ++- internal/app/seal.go | 4 -- internal/app/shim/main.go | 3 +- internal/fmsg/msg.go | 12 ++++++ internal/output.go | 4 ++ internal/prctl.go | 25 ------------ internal/sandbox/container.go | 18 ++++---- internal/sandbox/container_test.go | 7 +++- internal/{ => sandbox}/executable.go | 6 +-- internal/{ => sandbox}/executable_test.go | 6 +-- internal/sandbox/init.go | 50 ++++++++++++----------- internal/sandbox/mount.go | 26 ++++++------ internal/sandbox/msg.go | 43 +++++++++++++++++++ internal/sandbox/output.go | 19 +++++++++ internal/sandbox/path.go | 4 +- internal/sandbox/sequential.go | 24 +++++------ internal/{file.go => sandbox/syscall.go} | 24 ++++++++++- internal/sys/std.go | 3 +- main.go | 4 +- system/acl.go | 12 +++--- system/dbus.go | 30 +++++++------- system/link.go | 14 +++---- system/mkdir.go | 16 ++++---- system/op.go | 22 +--------- system/output.go | 20 +++++++++ system/tmpfiles.go | 14 +++---- system/wayland.go | 20 ++++----- system/xhost.go | 14 +++---- 32 files changed, 270 insertions(+), 194 deletions(-) create mode 100644 internal/fmsg/msg.go delete mode 100644 internal/prctl.go rename internal/{ => sandbox}/executable.go (79%) rename internal/{ => sandbox}/executable_test.go (55%) create mode 100644 internal/sandbox/msg.go create mode 100644 internal/sandbox/output.go rename internal/{file.go => sandbox/syscall.go} (54%) create mode 100644 system/output.go diff --git a/cmd/fpkg/main.go b/cmd/fpkg/main.go index 2fbefdb..63a3f77 100644 --- a/cmd/fpkg/main.go +++ b/cmd/fpkg/main.go @@ -38,10 +38,10 @@ func init() { func main() { // early init path, skips root check and duplicate PR_SET_DUMPABLE - sandbox.TryArgv0() + sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg) init0.TryArgv0() - if err := internal.SetDumpable(internal.SUID_DUMP_DISABLE); err != nil { + if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil { log.Printf("cannot set SUID_DUMP_DISABLE: %s", err) // not fatal: this program runs as the privileged user } diff --git a/dbus/dbus_test.go b/dbus/dbus_test.go index b8391c3..bba06f6 100644 --- a/dbus/dbus_test.go +++ b/dbus/dbus_test.go @@ -14,6 +14,7 @@ import ( "git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/helper" "git.gensokyo.uk/security/fortify/internal" + "git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/sandbox" ) @@ -244,5 +245,6 @@ func TestHelperInit(t *testing.T) { if len(os.Args) != 5 || os.Args[4] != "init" { return } - sandbox.Init(internal.Exit) + sandbox.SetOutput(fmsg.Output{}) + sandbox.Init(fmsg.Prepare, internal.InstallFmsg) } diff --git a/helper/container_test.go b/helper/container_test.go index 4dc88da..cb145bf 100644 --- a/helper/container_test.go +++ b/helper/container_test.go @@ -9,6 +9,7 @@ import ( "git.gensokyo.uk/security/fortify/helper" "git.gensokyo.uk/security/fortify/internal" + "git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/sandbox" ) @@ -51,5 +52,6 @@ func TestHelperInit(t *testing.T) { if len(os.Args) != 5 || os.Args[4] != "init" { return } - sandbox.Init(internal.Exit) + sandbox.SetOutput(fmsg.Output{}) + sandbox.Init(fmsg.Prepare, func(bool) { internal.InstallFmsg(false) }) } diff --git a/helper/stub.go b/helper/stub.go index 21ab5f4..cfd4a3f 100644 --- a/helper/stub.go +++ b/helper/stub.go @@ -11,7 +11,6 @@ import ( "git.gensokyo.uk/security/fortify/helper/bwrap" "git.gensokyo.uk/security/fortify/helper/proc" - "git.gensokyo.uk/security/fortify/internal" ) // InternalHelperStub is an internal function but exported because it is cross-package; @@ -36,7 +35,7 @@ func InternalHelperStub() { genericStub(flagRestoreFiles(3, ap, sp)) } - internal.Exit(0) + os.Exit(0) } func newFile(fd int, name, p string) *os.File { diff --git a/internal/app/init0/main.go b/internal/app/init0/main.go index fe2f9ae..8303313 100644 --- a/internal/app/init0/main.go +++ b/internal/app/init0/main.go @@ -12,6 +12,7 @@ import ( "git.gensokyo.uk/security/fortify/helper/proc" "git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal/fmsg" + "git.gensokyo.uk/security/fortify/internal/sandbox" ) const ( @@ -28,7 +29,7 @@ func Main() { fmsg.Prepare("init0") // setting this prevents ptrace - if err := internal.SetDumpable(internal.SUID_DUMP_DISABLE); err != nil { + if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil { log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err) } @@ -64,7 +65,7 @@ func Main() { } // die with parent - if err := internal.SetPdeathsig(syscall.SIGKILL); err != nil { + if err := sandbox.SetPdeathsig(syscall.SIGKILL); err != nil { log.Fatalf("prctl(PR_SET_PDEATHSIG, SIGKILL): %v", err) } diff --git a/internal/app/seal.go b/internal/app/seal.go index 7c4ba4b..3070212 100644 --- a/internal/app/seal.go +++ b/internal/app/seal.go @@ -231,10 +231,6 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error { sc := sys.Paths() seal.runDirPath = sc.RunDirPath seal.sys = system.New(seal.user.uid.unwrap()) - seal.sys.IsVerbose = fmsg.Load - seal.sys.Verbose = fmsg.Verbose - seal.sys.Verbosef = fmsg.Verbosef - seal.sys.WrapErr = fmsg.WrapError /* Work directories diff --git a/internal/app/shim/main.go b/internal/app/shim/main.go index 64df53d..effa6b8 100644 --- a/internal/app/shim/main.go +++ b/internal/app/shim/main.go @@ -17,6 +17,7 @@ import ( "git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal/app/init0" "git.gensokyo.uk/security/fortify/internal/fmsg" + "git.gensokyo.uk/security/fortify/internal/sandbox" ) // everything beyond this point runs as unconstrained target user @@ -28,7 +29,7 @@ func Main() { fmsg.Prepare("shim") // setting this prevents ptrace - if err := internal.SetDumpable(internal.SUID_DUMP_DISABLE); err != nil { + if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil { log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err) } diff --git a/internal/fmsg/msg.go b/internal/fmsg/msg.go new file mode 100644 index 0000000..7ff42b7 --- /dev/null +++ b/internal/fmsg/msg.go @@ -0,0 +1,12 @@ +package fmsg + +type Output struct{} + +func (Output) IsVerbose() bool { return Load() } +func (Output) Verbose(v ...any) { Verbose(v...) } +func (Output) Verbosef(format string, v ...any) { Verbosef(format, v...) } +func (Output) WrapErr(err error, a ...any) error { return WrapError(err, a...) } +func (Output) PrintBaseErr(err error, fallback string) { PrintBaseError(err, fallback) } +func (Output) Suspend() { Suspend() } +func (Output) Resume() bool { return Resume() } +func (Output) BeforeExit() { BeforeExit() } diff --git a/internal/output.go b/internal/output.go index db37f55..c85f1b5 100644 --- a/internal/output.go +++ b/internal/output.go @@ -2,11 +2,15 @@ package internal import ( "git.gensokyo.uk/security/fortify/internal/fmsg" + "git.gensokyo.uk/security/fortify/internal/sandbox" "git.gensokyo.uk/security/fortify/seccomp" + "git.gensokyo.uk/security/fortify/system" ) func InstallFmsg(verbose bool) { fmsg.Store(verbose) + sandbox.SetOutput(fmsg.Output{}) + system.SetOutput(fmsg.Output{}) if verbose { seccomp.SetOutput(fmsg.Verbose) } diff --git a/internal/prctl.go b/internal/prctl.go deleted file mode 100644 index ae8392a..0000000 --- a/internal/prctl.go +++ /dev/null @@ -1,25 +0,0 @@ -package internal - -import "syscall" - -const ( - SUID_DUMP_DISABLE = iota - SUID_DUMP_USER -) - -func SetDumpable(dumpable uintptr) error { - // linux/sched/coredump.h - if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, dumpable, 0); errno != 0 { - return errno - } - - return nil -} - -func SetPdeathsig(sig syscall.Signal) error { - if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(sig), 0); errno != 0 { - return errno - } - - return nil -} diff --git a/internal/sandbox/container.go b/internal/sandbox/container.go index 2cd64eb..f243ffd 100644 --- a/internal/sandbox/container.go +++ b/internal/sandbox/container.go @@ -14,8 +14,6 @@ import ( "time" "git.gensokyo.uk/security/fortify/helper/proc" - "git.gensokyo.uk/security/fortify/internal" - "git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/seccomp" ) @@ -139,7 +137,7 @@ func (p *Container) Start() error { if p.CommandContext != nil { p.cmd = p.CommandContext(ctx) } else { - p.cmd = exec.CommandContext(ctx, internal.MustExecutable()) + p.cmd = exec.CommandContext(ctx, MustExecutable()) p.cmd.Args = []string{"init"} } @@ -166,7 +164,7 @@ func (p *Container) Start() error { // place setup pipe before user supplied extra files, this is later restored by init if fd, e, err := proc.Setup(&p.cmd.ExtraFiles); err != nil { - return fmsg.WrapErrorSuffix(err, + return wrapErrSuffix(err, "cannot create shim setup pipe:") } else { p.setup = e @@ -174,9 +172,9 @@ func (p *Container) Start() error { } p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...) - fmsg.Verbose("starting container init") + msg.Verbose("starting container init") if err := p.cmd.Start(); err != nil { - return fmsg.WrapError(err, err.Error()) + return msg.WrapErr(err, err.Error()) } return nil } @@ -187,7 +185,7 @@ func (p *Container) Serve() error { } if p.Path != "" && !path.IsAbs(p.Path) { - return fmsg.WrapError(syscall.EINVAL, + return msg.WrapErr(syscall.EINVAL, fmt.Sprintf("invalid executable path %q", p.Path)) } @@ -195,14 +193,14 @@ func (p *Container) Serve() error { if p.name == "" { p.Path = os.Getenv("SHELL") if !path.IsAbs(p.Path) { - return fmsg.WrapError(syscall.EBADE, + return msg.WrapErr(syscall.EBADE, "no command specified and $SHELL is invalid") } p.name = path.Base(p.Path) } else if path.IsAbs(p.name) { p.Path = p.name } else if v, err := exec.LookPath(p.name); err != nil { - return fmsg.WrapError(err, err.Error()) + return msg.WrapErr(err, err.Error()) } else { p.Path = v } @@ -216,7 +214,7 @@ func (p *Container) Serve() error { syscall.Getuid(), syscall.Getgid(), len(p.ExtraFiles), - fmsg.Load(), + msg.IsVerbose(), }, ) } diff --git a/internal/sandbox/container_test.go b/internal/sandbox/container_test.go index 1a629d9..55df60b 100644 --- a/internal/sandbox/container_test.go +++ b/internal/sandbox/container_test.go @@ -23,8 +23,10 @@ import ( func TestContainer(t *testing.T) { { oldVerbose := fmsg.Load() - fmsg.Store(true) + oldOutput := sandbox.GetOutput() + internal.InstallFmsg(true) t.Cleanup(func() { fmsg.Store(oldVerbose) }) + t.Cleanup(func() { sandbox.SetOutput(oldOutput) }) } testCases := []struct { @@ -146,7 +148,8 @@ func TestHelperInit(t *testing.T) { if len(os.Args) != 5 || os.Args[4] != "init" { return } - sandbox.Init(internal.Exit) + sandbox.SetOutput(fmsg.Output{}) + sandbox.Init(fmsg.Prepare, internal.InstallFmsg) } func TestHelperCheckContainer(t *testing.T) { diff --git a/internal/executable.go b/internal/sandbox/executable.go similarity index 79% rename from internal/executable.go rename to internal/sandbox/executable.go index 381a09b..5f73d23 100644 --- a/internal/executable.go +++ b/internal/sandbox/executable.go @@ -1,11 +1,9 @@ -package internal +package sandbox import ( "log" "os" "sync" - - "git.gensokyo.uk/security/fortify/internal/fmsg" ) var ( @@ -15,7 +13,7 @@ var ( func copyExecutable() { if name, err := os.Executable(); err != nil { - fmsg.BeforeExit() + msg.BeforeExit() log.Fatalf("cannot read executable path: %v", err) } else { executable = name diff --git a/internal/executable_test.go b/internal/sandbox/executable_test.go similarity index 55% rename from internal/executable_test.go rename to internal/sandbox/executable_test.go index 1fdd40a..b11908c 100644 --- a/internal/executable_test.go +++ b/internal/sandbox/executable_test.go @@ -1,15 +1,15 @@ -package internal_test +package sandbox_test import ( "os" "testing" - "git.gensokyo.uk/security/fortify/internal" + "git.gensokyo.uk/security/fortify/internal/sandbox" ) func TestExecutable(t *testing.T) { for i := 0; i < 16; i++ { - if got := internal.MustExecutable(); got != os.Args[0] { + if got := sandbox.MustExecutable(); got != os.Args[0] { t.Errorf("MustExecutable: %q, want %q", got, os.Args[0]) } diff --git a/internal/sandbox/init.go b/internal/sandbox/init.go index 85fcb13..058fe9a 100644 --- a/internal/sandbox/init.go +++ b/internal/sandbox/init.go @@ -14,8 +14,6 @@ import ( "time" "git.gensokyo.uk/security/fortify/helper/proc" - "git.gensokyo.uk/security/fortify/internal" - "git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/seccomp" ) @@ -40,9 +38,9 @@ type initParams struct { Verbose bool } -func Init(exit func(code int)) { +func Init(prepare func(prefix string), setVerbose func(verbose bool)) { runtime.LockOSThread() - fmsg.Prepare("init") + prepare("init") if os.Getpid() != 1 { log.Fatal("this process must run as pid 1") @@ -72,14 +70,14 @@ func Init(exit func(code int)) { log.Fatal("invalid setup parameters") } - internal.InstallFmsg(params.Verbose) - fmsg.Verbose("received setup parameters") + setVerbose(params.Verbose) + msg.Verbose("received setup parameters") closeSetup = f offsetSetup = int(setupFile.Fd() + 1) } // write uid/gid map here so parent does not need to set dumpable - if err := internal.SetDumpable(internal.SUID_DUMP_USER); err != nil { + if err := SetDumpable(SUID_DUMP_USER); err != nil { log.Fatalf("cannot set SUID_DUMP_USER: %s", err) } if err := os.WriteFile("/proc/self/uid_map", @@ -97,7 +95,7 @@ func Init(exit func(code int)) { 0); err != nil { log.Fatalf("%v", err) } - if err := internal.SetDumpable(internal.SUID_DUMP_DISABLE); err != nil { + if err := SetDumpable(SUID_DUMP_DISABLE); err != nil { log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err) } @@ -146,11 +144,12 @@ func Init(exit func(code int)) { } for i, op := range *params.Ops { - fmsg.Verbosef("mounting %s", op) + msg.Verbosef("mounting %s", op) if err := op.apply(¶ms.InitParams); err != nil { - fmsg.PrintBaseError(err, + msg.PrintBaseErr(err, fmt.Sprintf("cannot apply op %d:", i)) - exit(1) + msg.BeforeExit() + os.Exit(1) } } @@ -169,7 +168,7 @@ func Init(exit func(code int)) { { var fd int - if err := internal.IgnoringEINTR(func() (err error) { + if err := IgnoringEINTR(func() (err error) { fd, err = syscall.Open("/", syscall.O_DIRECTORY|syscall.O_RDONLY, 0) return }); err != nil { @@ -234,7 +233,7 @@ func Init(exit func(code int)) { if err := cmd.Start(); err != nil { log.Fatalf("%v", err) } - fmsg.Suspend() + msg.Suspend() /* close setup pipe @@ -295,16 +294,17 @@ func Init(exit func(code int)) { for { select { case s := <-sig: - if fmsg.Resume() { - fmsg.Verbosef("terminating on %s after process start", s.String()) + if msg.Resume() { + msg.Verbosef("terminating on %s after process start", s.String()) } else { - fmsg.Verbosef("terminating on %s", s.String()) + msg.Verbosef("terminating on %s", s.String()) } - exit(0) + msg.BeforeExit() + os.Exit(0) case w := <-info: if w.wpid == cmd.Process.Pid { // initial process exited, output is most likely available again - fmsg.Resume() + msg.Resume() switch { case w.wstatus.Exited(): @@ -321,18 +321,22 @@ func Init(exit func(code int)) { }() } case <-done: - exit(r) + msg.BeforeExit() + os.Exit(r) case <-timeout: log.Println("timeout exceeded waiting for lingering processes") - exit(r) + msg.BeforeExit() + os.Exit(r) } } } // TryArgv0 calls [Init] if the last element of argv0 is "init". -func TryArgv0() { +func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) { if len(os.Args) > 0 && path.Base(os.Args[0]) == "init" { - Init(internal.Exit) - internal.Exit(0) + msg = v + Init(prepare, setVerbose) + msg.BeforeExit() + os.Exit(0) } } diff --git a/internal/sandbox/mount.go b/internal/sandbox/mount.go index eaede7b..e47e7f5 100644 --- a/internal/sandbox/mount.go +++ b/internal/sandbox/mount.go @@ -6,8 +6,6 @@ import ( "os" "strings" "syscall" - - "git.gensokyo.uk/security/fortify/internal/fmsg" ) const ( @@ -30,34 +28,34 @@ func bindMount(src, dest string, flags int) error { if flags&BindOptional != 0 { return nil } else { - return fmsg.WrapError(err, + return msg.WrapErr(err, fmt.Sprintf("path %q does not exist", src)) } } - return fmsg.WrapError(err, err.Error()) + return msg.WrapErr(err, err.Error()) } else { source = toHost(rp) } } else if flags&BindOptional != 0 { - return fmsg.WrapError(syscall.EINVAL, + return msg.WrapErr(syscall.EINVAL, "flag source excludes optional") } else { source = toHost(src) } if fi, err := os.Stat(source); err != nil { - return fmsg.WrapError(err, err.Error()) + return msg.WrapErr(err, err.Error()) } else if fi.IsDir() { if err = os.MkdirAll(target, 0755); err != nil { - return fmsg.WrapErrorSuffix(err, + return wrapErrSuffix(err, fmt.Sprintf("cannot create directory %q:", dest)) } } else if err = ensureFile(target, 0444); err != nil { if errors.Is(err, syscall.EISDIR) { - return fmsg.WrapError(err, + return msg.WrapErr(err, fmt.Sprintf("path %q is a directory", dest)) } - return fmsg.WrapErrorSuffix(err, + return wrapErrSuffix(err, fmt.Sprintf("cannot create %q:", dest)) } @@ -71,14 +69,14 @@ func bindMount(src, dest string, flags int) error { if flags&BindDevices == 0 { mf |= syscall.MS_NODEV } - if fmsg.Load() { + if msg.IsVerbose() { if strings.TrimPrefix(source, hostPath) == strings.TrimPrefix(target, sysrootPath) { - fmsg.Verbosef("resolved %q flags %#x", target, mf) + msg.Verbosef("resolved %q flags %#x", target, mf) } else { - fmsg.Verbosef("resolved %q on %q flags %#x", source, target, mf) + msg.Verbosef("resolved %q on %q flags %#x", source, target, mf) } } - return fmsg.WrapErrorSuffix(syscall.Mount(source, target, "", mf, ""), + return wrapErrSuffix(syscall.Mount(source, target, "", mf, ""), fmt.Sprintf("cannot bind %q on %q:", src, dest)) } @@ -91,7 +89,7 @@ func mountTmpfs(fsname, name string, size int, perm os.FileMode) error { if size > 0 { opt += fmt.Sprintf(",size=%d", size) } - return fmsg.WrapErrorSuffix(syscall.Mount(fsname, target, "tmpfs", + return wrapErrSuffix(syscall.Mount(fsname, target, "tmpfs", syscall.MS_NOSUID|syscall.MS_NODEV, opt), fmt.Sprintf("cannot mount tmpfs on %q:", name)) } diff --git a/internal/sandbox/msg.go b/internal/sandbox/msg.go new file mode 100644 index 0000000..5ee1850 --- /dev/null +++ b/internal/sandbox/msg.go @@ -0,0 +1,43 @@ +package sandbox + +import ( + "log" + "sync/atomic" +) + +type Msg interface { + IsVerbose() bool + Verbose(v ...any) + Verbosef(format string, v ...any) + WrapErr(err error, a ...any) error + PrintBaseErr(err error, fallback string) + + Suspend() + Resume() bool + + BeforeExit() +} + +type DefaultMsg struct{ inactive atomic.Bool } + +func (msg *DefaultMsg) IsVerbose() bool { return true } +func (msg *DefaultMsg) Verbose(v ...any) { + if !msg.inactive.Load() { + log.Println(v...) + } +} +func (msg *DefaultMsg) Verbosef(format string, v ...any) { + if !msg.inactive.Load() { + log.Printf(format, v...) + } +} + +func (msg *DefaultMsg) WrapErr(err error, a ...any) error { + log.Println(a...) + return err +} +func (msg *DefaultMsg) PrintBaseErr(err error, fallback string) { log.Println(fallback, err) } + +func (msg *DefaultMsg) Suspend() { msg.inactive.Store(true) } +func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) } +func (msg *DefaultMsg) BeforeExit() {} diff --git a/internal/sandbox/output.go b/internal/sandbox/output.go new file mode 100644 index 0000000..de4a104 --- /dev/null +++ b/internal/sandbox/output.go @@ -0,0 +1,19 @@ +package sandbox + +var msg Msg = new(DefaultMsg) + +func GetOutput() Msg { return msg } +func SetOutput(v Msg) { + if v == nil { + msg = new(DefaultMsg) + } else { + msg = v + } +} + +func wrapErrSuffix(err error, a ...any) error { + if err == nil { + return nil + } + return msg.WrapErr(err, append(a, err)...) +} diff --git a/internal/sandbox/path.go b/internal/sandbox/path.go index 0d49afa..6c5a6e2 100644 --- a/internal/sandbox/path.go +++ b/internal/sandbox/path.go @@ -7,8 +7,6 @@ import ( "path" "strings" "syscall" - - "git.gensokyo.uk/security/fortify/internal/fmsg" ) const ( @@ -43,7 +41,7 @@ func realpathHost(name string) (string, error) { if !path.IsAbs(rp) { return name, nil } - fmsg.Verbosef("path %q resolves to %q", name, rp) + msg.Verbosef("path %q resolves to %q", name, rp) return rp, nil } diff --git a/internal/sandbox/sequential.go b/internal/sandbox/sequential.go index 93cc74c..aa4bb18 100644 --- a/internal/sandbox/sequential.go +++ b/internal/sandbox/sequential.go @@ -8,8 +8,6 @@ import ( "path" "syscall" "unsafe" - - "git.gensokyo.uk/security/fortify/internal/fmsg" ) func init() { gob.Register(new(BindMount)) } @@ -23,7 +21,7 @@ type BindMount struct { func (b *BindMount) apply(*InitParams) error { if !path.IsAbs(b.Source) || !path.IsAbs(b.Target) { - return fmsg.WrapError(syscall.EBADE, + return msg.WrapErr(syscall.EBADE, "path is not absolute") } return bindMount(b.Source, b.Target, b.Flags) @@ -50,15 +48,15 @@ type MountProc struct { func (p *MountProc) apply(*InitParams) error { if !path.IsAbs(p.Path) { - return fmsg.WrapError(syscall.EBADE, + return msg.WrapErr(syscall.EBADE, fmt.Sprintf("path %q is not absolute", p.Path)) } target := toSysroot(p.Path) if err := os.MkdirAll(target, 0755); err != nil { - return fmsg.WrapError(err, err.Error()) + return msg.WrapErr(err, err.Error()) } - return fmsg.WrapErrorSuffix(syscall.Mount("proc", target, "proc", + return wrapErrSuffix(syscall.Mount("proc", target, "proc", syscall.MS_NOSUID|syscall.MS_NOEXEC|syscall.MS_NODEV, ""), fmt.Sprintf("cannot mount proc on %q:", p.Path)) } @@ -72,7 +70,7 @@ type MountDev struct { func (d *MountDev) apply(params *InitParams) error { if !path.IsAbs(d.Path) { - return fmsg.WrapError(syscall.EBADE, + return msg.WrapErr(syscall.EBADE, fmt.Sprintf("path %q is not absolute", d.Path)) } target := toSysroot(d.Path) @@ -94,7 +92,7 @@ func (d *MountDev) apply(params *InitParams) error { "/proc/self/fd/"+string(rune(i+'0')), path.Join(target, name), ); err != nil { - return fmsg.WrapError(err, err.Error()) + return msg.WrapErr(err, err.Error()) } } for _, pair := range [][2]string{ @@ -103,21 +101,21 @@ func (d *MountDev) apply(params *InitParams) error { {"pts/ptmx", "ptmx"}, } { if err := os.Symlink(pair[0], path.Join(target, pair[1])); err != nil { - return fmsg.WrapError(err, err.Error()) + return msg.WrapErr(err, err.Error()) } } devPtsPath := path.Join(target, "pts") for _, name := range []string{path.Join(target, "shm"), devPtsPath} { if err := os.Mkdir(name, 0755); err != nil { - return fmsg.WrapError(err, err.Error()) + return msg.WrapErr(err, err.Error()) } } if err := syscall.Mount("devpts", devPtsPath, "devpts", syscall.MS_NOSUID|syscall.MS_NOEXEC, "newinstance,ptmxmode=0666,mode=620"); err != nil { - return fmsg.WrapErrorSuffix(err, + return wrapErrSuffix(err, fmt.Sprintf("cannot mount devpts on %q:", devPtsPath)) } @@ -164,11 +162,11 @@ type MountTmpfs struct { func (t *MountTmpfs) apply(*InitParams) error { if !path.IsAbs(t.Path) { - return fmsg.WrapError(syscall.EBADE, + return msg.WrapErr(syscall.EBADE, fmt.Sprintf("path %q is not absolute", t.Path)) } if t.Size < 0 || t.Size > math.MaxUint>>1 { - return fmsg.WrapError(syscall.EBADE, + return msg.WrapErr(syscall.EBADE, fmt.Sprintf("size %d out of bounds", t.Size)) } return mountTmpfs("tmpfs", t.Path, t.Size, t.Perm) diff --git a/internal/file.go b/internal/sandbox/syscall.go similarity index 54% rename from internal/file.go rename to internal/sandbox/syscall.go index 153f0f9..a17928f 100644 --- a/internal/file.go +++ b/internal/sandbox/syscall.go @@ -1,7 +1,29 @@ -package internal +package sandbox import "syscall" +const ( + SUID_DUMP_DISABLE = iota + SUID_DUMP_USER +) + +func SetDumpable(dumpable uintptr) error { + // linux/sched/coredump.h + if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, dumpable, 0); errno != 0 { + return errno + } + + return nil +} + +func SetPdeathsig(sig syscall.Signal) error { + if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(sig), 0); errno != 0 { + return errno + } + + return nil +} + // IgnoringEINTR makes a function call and repeats it if it returns an // EINTR error. This appears to be required even though we install all // signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846. diff --git a/internal/sys/std.go b/internal/sys/std.go index d447f58..0145eb2 100644 --- a/internal/sys/std.go +++ b/internal/sys/std.go @@ -15,6 +15,7 @@ import ( "git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal/fmsg" + "git.gensokyo.uk/security/fortify/internal/sandbox" ) // Std implements System using the standard library. @@ -34,7 +35,7 @@ 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) MustExecutable() string { return internal.MustExecutable() } +func (s *Std) MustExecutable() string { return sandbox.MustExecutable() } func (s *Std) LookupGroup(name string) (*user.Group, error) { return user.LookupGroup(name) } func (s *Std) ReadDir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) } func (s *Std) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) } diff --git a/main.go b/main.go index 1a7e853..7a650d6 100644 --- a/main.go +++ b/main.go @@ -42,10 +42,10 @@ var std sys.State = new(sys.Std) func main() { // early init path, skips root check and duplicate PR_SET_DUMPABLE - sandbox.TryArgv0() + sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg) init0.TryArgv0() - if err := internal.SetDumpable(internal.SUID_DUMP_DISABLE); err != nil { + if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil { log.Printf("cannot set SUID_DUMP_DISABLE: %s", err) // not fatal: this program runs as the privileged user } diff --git a/system/acl.go b/system/acl.go index e6ae7d2..381d1eb 100644 --- a/system/acl.go +++ b/system/acl.go @@ -35,24 +35,24 @@ type ACL struct { func (a *ACL) Type() Enablement { return a.et } func (a *ACL) apply(sys *I) error { - sys.println("applying ACL", a) - return sys.wrapErrSuffix(acl.Update(a.path, sys.uid, a.perms...), + msg.Verbose("applying ACL", a) + return wrapErrSuffix(acl.Update(a.path, sys.uid, a.perms...), fmt.Sprintf("cannot apply ACL entry to %q:", a.path)) } func (a *ACL) revert(sys *I, ec *Criteria) error { if ec.hasType(a) { - sys.println("stripping ACL", a) + msg.Verbose("stripping ACL", a) err := acl.Update(a.path, sys.uid) if errors.Is(err, os.ErrNotExist) { // the ACL is effectively stripped if the file no longer exists - sys.printf("target of ACL %s no longer exists", a) + msg.Verbosef("target of ACL %s no longer exists", a) err = nil } - return sys.wrapErrSuffix(err, + return wrapErrSuffix(err, fmt.Sprintf("cannot strip ACL entry from %q:", a.path)) } else { - sys.println("skipping ACL", a) + msg.Verbose("skipping ACL", a) return nil } } diff --git a/system/dbus.go b/system/dbus.go index b44936c..dddff73 100644 --- a/system/dbus.go +++ b/system/dbus.go @@ -28,7 +28,7 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st // session bus is mandatory if session == nil { - return nil, sys.wrapErr(ErrDBusConfig, + return nil, msg.WrapErr(ErrDBusConfig, "attempted to seal message bus proxy without session bus config") } @@ -48,12 +48,12 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st d.proxy = dbus.New(sessionBus, systemBus) defer func() { - if sys.IsVerbose() && d.proxy.Sealed() { - sys.println("sealed session proxy", session.Args(sessionBus)) + if msg.IsVerbose() && d.proxy.Sealed() { + msg.Verbose("sealed session proxy", session.Args(sessionBus)) if system != nil { - sys.println("sealed system proxy", system.Args(systemBus)) + msg.Verbose("sealed system proxy", system.Args(systemBus)) } - sys.println("message bus proxy final args:", d.proxy) + msg.Verbose("message bus proxy final args:", d.proxy) } }() @@ -62,7 +62,7 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st // seal dbus proxy d.out = &scanToFmsg{msg: new(strings.Builder)} - return d.out.Dump, sys.wrapErrSuffix(d.proxy.Seal(session, system), + return d.out.Dump, wrapErrSuffix(d.proxy.Seal(session, system), "cannot seal message bus proxy:") } @@ -77,32 +77,32 @@ type DBus struct { func (d *DBus) Type() Enablement { return Process } func (d *DBus) apply(sys *I) error { - sys.printf("session bus proxy on %q for upstream %q", d.proxy.Session()[1], d.proxy.Session()[0]) + msg.Verbosef("session bus proxy on %q for upstream %q", d.proxy.Session()[1], d.proxy.Session()[0]) if d.system { - sys.printf("system bus proxy on %q for upstream %q", d.proxy.System()[1], d.proxy.System()[0]) + msg.Verbosef("system bus proxy on %q for upstream %q", d.proxy.System()[1], d.proxy.System()[0]) } // this starts the process and blocks until ready if err := d.proxy.Start(sys.ctx, d.out, true); err != nil { d.out.Dump() - return sys.wrapErrSuffix(err, + return wrapErrSuffix(err, "cannot start message bus proxy:") } - sys.println("starting message bus proxy", d.proxy) + msg.Verbose("starting message bus proxy", d.proxy) return nil } -func (d *DBus) revert(sys *I, _ *Criteria) error { +func (d *DBus) revert(*I, *Criteria) error { // criteria ignored here since dbus is always process-scoped - sys.println("terminating message bus proxy") + msg.Verbose("terminating message bus proxy") d.proxy.Close() - defer sys.println("message bus proxy exit") + defer msg.Verbose("message bus proxy exit") err := d.proxy.Wait() if errors.Is(err, context.Canceled) { - sys.println("message bus proxy canceled upstream") + msg.Verbose("message bus proxy canceled upstream") err = nil } - return sys.wrapErrSuffix(err, "message bus proxy error:") + return wrapErrSuffix(err, "message bus proxy error:") } func (d *DBus) Is(o Op) bool { diff --git a/system/link.go b/system/link.go index 4182d56..f307e67 100644 --- a/system/link.go +++ b/system/link.go @@ -25,19 +25,19 @@ type Hardlink struct { func (l *Hardlink) Type() Enablement { return l.et } -func (l *Hardlink) apply(sys *I) error { - sys.println("linking", l) - return sys.wrapErrSuffix(os.Link(l.src, l.dst), +func (l *Hardlink) apply(*I) error { + msg.Verbose("linking", l) + return wrapErrSuffix(os.Link(l.src, l.dst), fmt.Sprintf("cannot link %q:", l.dst)) } -func (l *Hardlink) revert(sys *I, ec *Criteria) error { +func (l *Hardlink) revert(_ *I, ec *Criteria) error { if ec.hasType(l) { - sys.printf("removing hard link %q", l.dst) - return sys.wrapErrSuffix(os.Remove(l.dst), + msg.Verbosef("removing hard link %q", l.dst) + return wrapErrSuffix(os.Remove(l.dst), fmt.Sprintf("cannot remove hard link %q:", l.dst)) } else { - sys.printf("skipping hard link %q", l.dst) + msg.Verbosef("skipping hard link %q", l.dst) return nil } } diff --git a/system/mkdir.go b/system/mkdir.go index 524289e..bc67997 100644 --- a/system/mkdir.go +++ b/system/mkdir.go @@ -37,33 +37,33 @@ func (m *Mkdir) Type() Enablement { return m.et } -func (m *Mkdir) apply(sys *I) error { - sys.println("ensuring directory", m) +func (m *Mkdir) apply(*I) error { + msg.Verbose("ensuring directory", m) // create directory err := os.Mkdir(m.path, m.perm) if !errors.Is(err, os.ErrExist) { - return sys.wrapErrSuffix(err, + return wrapErrSuffix(err, fmt.Sprintf("cannot create directory %q:", m.path)) } // directory exists, ensure mode - return sys.wrapErrSuffix(os.Chmod(m.path, m.perm), + return wrapErrSuffix(os.Chmod(m.path, m.perm), fmt.Sprintf("cannot change mode of %q to %s:", m.path, m.perm)) } -func (m *Mkdir) revert(sys *I, ec *Criteria) error { +func (m *Mkdir) revert(_ *I, ec *Criteria) error { if !m.ephemeral { // skip non-ephemeral dir and do not log anything return nil } if ec.hasType(m) { - sys.println("destroying ephemeral directory", m) - return sys.wrapErrSuffix(os.Remove(m.path), + msg.Verbose("destroying ephemeral directory", m) + return wrapErrSuffix(os.Remove(m.path), fmt.Sprintf("cannot remove ephemeral directory %q:", m.path)) } else { - sys.println("skipping ephemeral directory", m) + msg.Verbose("skipping ephemeral directory", m) return nil } } diff --git a/system/op.go b/system/op.go index e252c59..3c334b0 100644 --- a/system/op.go +++ b/system/op.go @@ -60,10 +60,6 @@ func TypeString(e Enablement) string { func New(uid int) (sys *I) { sys = new(I) sys.uid = uid - sys.IsVerbose = func() bool { return false } - sys.Verbose = func(...any) {} - sys.Verbosef = func(string, ...any) {} - sys.WrapErr = func(err error, _ ...any) error { return err } return } @@ -73,27 +69,13 @@ type I struct { ops []Op ctx context.Context - IsVerbose func() bool - Verbose func(v ...any) - Verbosef func(format string, v ...any) - WrapErr func(err error, a ...any) error - // whether sys has been reverted state bool lock sync.Mutex } -func (sys *I) UID() int { return sys.uid } -func (sys *I) println(v ...any) { sys.Verbose(v...) } -func (sys *I) printf(format string, v ...any) { sys.Verbosef(format, v...) } -func (sys *I) wrapErr(err error, a ...any) error { return sys.WrapErr(err, a...) } -func (sys *I) wrapErrSuffix(err error, a ...any) error { - if err == nil { - return nil - } - return sys.wrapErr(err, append(a, err)...) -} +func (sys *I) UID() int { return sys.uid } // Equal returns whether all [Op] instances held by v is identical to that of sys. func (sys *I) Equal(v *I) bool { @@ -127,7 +109,7 @@ func (sys *I) Commit(ctx context.Context) error { // sp is set to nil when all ops are applied if sp != nil { // rollback partial commit - sys.printf("commit faulted after %d ops, rolling back partial commit", len(sp.ops)) + msg.Verbosef("commit faulted after %d ops, rolling back partial commit", len(sp.ops)) if err := sp.Revert(&Criteria{nil}); err != nil { log.Println("errors returned reverting partial commit:", err) } diff --git a/system/output.go b/system/output.go new file mode 100644 index 0000000..daf4386 --- /dev/null +++ b/system/output.go @@ -0,0 +1,20 @@ +package system + +import "git.gensokyo.uk/security/fortify/internal/sandbox" + +var msg sandbox.Msg = new(sandbox.DefaultMsg) + +func SetOutput(v sandbox.Msg) { + if v == nil { + msg = new(sandbox.DefaultMsg) + } else { + msg = v + } +} + +func wrapErrSuffix(err error, a ...any) error { + if err == nil { + return nil + } + return msg.WrapErr(err, append(a, err)...) +} diff --git a/system/tmpfiles.go b/system/tmpfiles.go index 636943b..6ed6e05 100644 --- a/system/tmpfiles.go +++ b/system/tmpfiles.go @@ -31,8 +31,8 @@ type Tmpfile struct { } func (t *Tmpfile) Type() Enablement { return Process } -func (t *Tmpfile) apply(sys *I) error { - sys.println("copying", t) +func (t *Tmpfile) apply(*I) error { + msg.Verbose("copying", t) if t.payload == nil { // this is a misuse of the API; do not return an error message @@ -40,25 +40,25 @@ func (t *Tmpfile) apply(sys *I) error { } if b, err := os.Stat(t.src); err != nil { - return sys.wrapErrSuffix(err, + return wrapErrSuffix(err, fmt.Sprintf("cannot stat %q:", t.src)) } else { if b.IsDir() { - return sys.wrapErrSuffix(syscall.EISDIR, + return wrapErrSuffix(syscall.EISDIR, fmt.Sprintf("%q is a directory", t.src)) } if s := b.Size(); s > t.n { - return sys.wrapErrSuffix(syscall.ENOMEM, + return wrapErrSuffix(syscall.ENOMEM, fmt.Sprintf("file %q is too long: %d > %d", t.src, s, t.n)) } } if f, err := os.Open(t.src); err != nil { - return sys.wrapErrSuffix(err, + return wrapErrSuffix(err, fmt.Sprintf("cannot open %q:", t.src)) } else if _, err = io.CopyN(t.buf, f, t.n); err != nil { - return sys.wrapErrSuffix(err, + return wrapErrSuffix(err, fmt.Sprintf("cannot read from %q:", t.src)) } diff --git a/system/wayland.go b/system/wayland.go index 70b7dbd..12c7a28 100644 --- a/system/wayland.go +++ b/system/wayland.go @@ -46,35 +46,35 @@ func (w *Wayland) apply(sys *I) error { if errors.Is(err, os.ErrNotExist) { err = os.ErrNotExist } - return sys.wrapErrSuffix(err, + return wrapErrSuffix(err, fmt.Sprintf("cannot attach to wayland on %q:", w.src)) } else { - sys.printf("wayland attached on %q", w.src) + msg.Verbosef("wayland attached on %q", w.src) } if sp, err := w.conn.Bind(w.dst, w.appID, w.instanceID); err != nil { - return sys.wrapErrSuffix(err, + return wrapErrSuffix(err, fmt.Sprintf("cannot bind to socket on %q:", w.dst)) } else { *w.sync = sp - sys.printf("wayland listening on %q", w.dst) - return sys.wrapErrSuffix(errors.Join(os.Chmod(w.dst, 0), acl.Update(w.dst, sys.uid, acl.Read, acl.Write, acl.Execute)), + msg.Verbosef("wayland listening on %q", w.dst) + return wrapErrSuffix(errors.Join(os.Chmod(w.dst, 0), acl.Update(w.dst, sys.uid, acl.Read, acl.Write, acl.Execute)), fmt.Sprintf("cannot chmod socket on %q:", w.dst)) } } -func (w *Wayland) revert(sys *I, ec *Criteria) error { +func (w *Wayland) revert(_ *I, ec *Criteria) error { if ec.hasType(w) { - sys.printf("removing wayland socket on %q", w.dst) + msg.Verbosef("removing wayland socket on %q", w.dst) if err := os.Remove(w.dst); err != nil && !errors.Is(err, os.ErrNotExist) { return err } - sys.printf("detaching from wayland on %q", w.src) - return sys.wrapErrSuffix(w.conn.Close(), + msg.Verbosef("detaching from wayland on %q", w.src) + return wrapErrSuffix(w.conn.Close(), fmt.Sprintf("cannot detach from wayland on %q:", w.src)) } else { - sys.printf("skipping wayland cleanup on %q", w.dst) + msg.Verbosef("skipping wayland cleanup on %q", w.dst) return nil } } diff --git a/system/xhost.go b/system/xhost.go index a20e199..480cacc 100644 --- a/system/xhost.go +++ b/system/xhost.go @@ -22,19 +22,19 @@ func (x XHost) Type() Enablement { return EX11 } -func (x XHost) apply(sys *I) error { - sys.printf("inserting entry %s to X11", x) - return sys.wrapErrSuffix(xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), +func (x XHost) apply(*I) error { + msg.Verbosef("inserting entry %s to X11", x) + return wrapErrSuffix(xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), fmt.Sprintf("cannot insert entry %s to X11:", x)) } -func (x XHost) revert(sys *I, ec *Criteria) error { +func (x XHost) revert(_ *I, ec *Criteria) error { if ec.hasType(x) { - sys.printf("deleting entry %s from X11", x) - return sys.wrapErrSuffix(xcb.ChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), + msg.Verbosef("deleting entry %s from X11", x) + return wrapErrSuffix(xcb.ChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), fmt.Sprintf("cannot delete entry %s from X11:", x)) } else { - sys.printf("skipping entry %s in X11", x) + msg.Verbosef("skipping entry %s in X11", x) return nil } }