helper/proc: move package out of internal
All checks were successful
Test / Create distribution (push) Successful in 1m32s
Test / Run NixOS test (push) Successful in 4m6s

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2025-02-08 13:03:45 +09:00
parent 7aff3ead3a
commit e14923ae53
14 changed files with 10 additions and 10 deletions

View File

@@ -0,0 +1,18 @@
package init0
import (
"os"
"path"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
// used by the parent process
// TryArgv0 calls [Main] if argv0 indicates the process is started from a file named "init".
func TryArgv0() {
if len(os.Args) > 0 && path.Base(os.Args[0]) == "init" {
Main()
fmsg.Exit(0)
}
}

164
internal/priv/init/main.go Normal file
View File

@@ -0,0 +1,164 @@
package init0
import (
"errors"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
const (
// 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 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")
}
// receive setup payload
var (
payload Payload
closeSetup func() error
)
if f, err := proc.Receive(Env, &payload); err != nil {
if errors.Is(err, proc.ErrInvalid) {
fmsg.Fatal("invalid config descriptor")
}
if errors.Is(err, proc.ErrNotSet) {
fmsg.Fatal("FORTIFY_INIT not set")
}
fmsg.Fatalf("cannot decode init setup payload: %v", err)
panic("unreachable")
} else {
fmsg.SetVerbose(payload.Verbose)
closeSetup = f
// child does not need to see this
if err = os.Unsetenv(Env); err != nil {
fmsg.Printf("cannot unset %s: %v", Env, err)
// not fatal
} else {
fmsg.VPrintln("received configuration")
}
}
// die with parent
if err := internal.PR_SET_PDEATHSIG__SIGKILL(); err != nil {
fmsg.Fatalf("prctl(PR_SET_PDEATHSIG, SIGKILL): %v", err)
}
cmd := exec.Command(payload.Argv0)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Args = payload.Argv
cmd.Env = os.Environ()
if err := cmd.Start(); err != nil {
fmsg.Fatalf("cannot start %q: %v", payload.Argv0, err)
}
fmsg.Suspend()
// close setup pipe as setup is now complete
if err := closeSetup(); err != nil {
fmsg.Println("cannot close setup pipe:", err)
// not fatal
}
sig := make(chan os.Signal, 2)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
type winfo struct {
wpid int
wstatus syscall.WaitStatus
}
info := make(chan winfo, 1)
done := make(chan struct{})
go func() {
var (
err error
wpid = -2
wstatus syscall.WaitStatus
)
// keep going until no child process is left
for wpid != -1 {
if err != nil {
break
}
if wpid != -2 {
info <- winfo{wpid, wstatus}
}
err = syscall.EINTR
for errors.Is(err, syscall.EINTR) {
wpid, err = syscall.Wait4(-1, &wstatus, 0, nil)
}
}
if !errors.Is(err, syscall.ECHILD) {
fmsg.Println("unexpected wait4 response:", err)
}
close(done)
}()
// closed after residualProcessTimeout has elapsed after initial process death
timeout := make(chan struct{})
r := 2
for {
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()
case w.wstatus.Signaled():
r = 128 + int(w.wstatus.Signal())
default:
r = 255
}
go func() {
time.Sleep(residualProcessTimeout)
close(timeout)
}()
}
case <-done:
fmsg.Exit(r)
case <-timeout:
fmsg.Println("timeout exceeded waiting for lingering processes")
fmsg.Exit(r)
}
}
}

View File

@@ -0,0 +1,13 @@
package init0
const Env = "FORTIFY_INIT"
type Payload struct {
// target full exec path
Argv0 string
// child full argv
Argv []string
// verbosity pass through
Verbose bool
}

156
internal/priv/shim/main.go Normal file
View File

@@ -0,0 +1,156 @@
package shim
import (
"errors"
"os"
"path"
"strconv"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/helper/seccomp"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
init0 "git.gensokyo.uk/security/fortify/internal/priv/init"
)
// everything beyond this point runs as unconstrained target user
// proceed with caution!
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")
}
// receive setup payload
var (
payload Payload
closeSetup func() error
)
if f, err := proc.Receive(Env, &payload); err != nil {
if errors.Is(err, proc.ErrInvalid) {
fmsg.Fatal("invalid config descriptor")
}
if errors.Is(err, proc.ErrNotSet) {
fmsg.Fatal("FORTIFY_SHIM not set")
}
fmsg.Fatalf("cannot decode shim setup payload: %v", err)
panic("unreachable")
} else {
fmsg.SetVerbose(payload.Verbose)
closeSetup = f
}
if payload.Bwrap == nil {
fmsg.Fatal("bwrap config not supplied")
}
// restore bwrap sync fd
var syncFd *os.File
if payload.Sync != nil {
syncFd = os.NewFile(*payload.Sync, "sync")
}
// close setup socket
if err := closeSetup(); err != nil {
fmsg.Println("cannot close setup pipe:", err)
// not fatal
}
// ensure home directory as target user
if s, err := os.Stat(payload.Home); err != nil {
if os.IsNotExist(err) {
if err = os.Mkdir(payload.Home, 0700); err != nil {
fmsg.Fatalf("cannot create home directory: %v", err)
}
} else {
fmsg.Fatalf("cannot access home directory: %v", err)
}
// home directory is created, proceed
} else if !s.IsDir() {
fmsg.Fatalf("data path %q is not a directory", payload.Home)
}
var ic init0.Payload
// resolve argv0
ic.Argv = payload.Argv
if len(ic.Argv) > 0 {
// looked up from $PATH by parent
ic.Argv0 = payload.Exec[1]
} else {
// no argv, look up shell instead
var ok bool
if payload.Bwrap.SetEnv == nil {
fmsg.Fatal("no command was specified and environment is unset")
}
if ic.Argv0, ok = payload.Bwrap.SetEnv["SHELL"]; !ok {
fmsg.Fatal("no command was specified and $SHELL was unset")
}
ic.Argv = []string{ic.Argv0}
}
conf := payload.Bwrap
var extraFiles []*os.File
// serve setup payload
if fd, encoder, err := proc.Setup(&extraFiles); err != nil {
fmsg.Fatalf("cannot pipe: %v", err)
} else {
conf.SetEnv[init0.Env] = strconv.Itoa(fd)
go func() {
fmsg.VPrintln("transmitting config to init")
if err = encoder.Encode(&ic); err != nil {
fmsg.Fatalf("cannot transmit init config: %v", err)
}
}()
}
// bind fortify inside sandbox
var (
innerSbin = path.Join(fst.Tmp, "sbin")
innerFortify = path.Join(innerSbin, "fortify")
innerInit = path.Join(innerSbin, "init")
)
conf.Bind(proc.MustExecutable(), innerFortify)
conf.Symlink("fortify", innerInit)
helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
if fmsg.Verbose() {
seccomp.CPrintln = fmsg.Println
}
if b, err := helper.NewBwrap(
conf, innerInit,
nil, func(int, int) []string { return make([]string, 0) },
extraFiles,
syncFd,
); err != nil {
fmsg.Fatalf("malformed sandbox config: %v", err)
} else {
cmd := b.Unwrap()
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
// run and pass through exit code
if err = b.Start(); err != nil {
fmsg.Fatalf("cannot start target process: %v", err)
} else if err = b.Wait(); err != nil {
fmsg.VPrintln("wait:", err)
}
if b.Unwrap().ProcessState != nil {
fmsg.Exit(b.Unwrap().ProcessState.ExitCode())
} else {
fmsg.Exit(127)
}
}
}

View File

@@ -0,0 +1,139 @@
package shim
import (
"context"
"encoding/gob"
"errors"
"os"
"os/exec"
"strconv"
"strings"
"time"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
// used by the parent process
type Shim struct {
// user switcher process
cmd *exec.Cmd
// fallback exit notifier with error returned killing the process
killFallback chan error
// monitor to shim encoder
encoder *gob.Encoder
// bwrap --sync-fd value
sync *uintptr
}
func (s *Shim) String() string {
if s.cmd == nil {
return "(unused shim manager)"
}
return s.cmd.String()
}
func (s *Shim) Unwrap() *exec.Cmd {
return s.cmd
}
func (s *Shim) WaitFallback() chan error {
return s.killFallback
}
func (s *Shim) Start(
// string representation of application id
aid string,
// string representation of supplementary group ids
supp []string,
// bwrap --sync-fd
syncFd *os.File,
) (*time.Time, error) {
// prepare user switcher invocation
var fsu string
if p, ok := internal.Path(internal.Fsu); !ok {
fmsg.Fatal("invalid fsu path, this copy of fortify is not compiled correctly")
panic("unreachable")
} else {
fsu = p
}
s.cmd = exec.Command(fsu)
// pass shim setup pipe
if fd, e, err := proc.Setup(&s.cmd.ExtraFiles); err != nil {
return nil, fmsg.WrapErrorSuffix(err,
"cannot create shim setup pipe:")
} else {
s.encoder = e
s.cmd.Env = []string{
Env + "=" + strconv.Itoa(fd),
"FORTIFY_APP_ID=" + aid,
}
}
// format fsu supplementary groups
if len(supp) > 0 {
fmsg.VPrintf("attaching supplementary group ids %s", supp)
s.cmd.Env = append(s.cmd.Env, "FORTIFY_GROUPS="+strings.Join(supp, " "))
}
s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
s.cmd.Dir = "/"
// pass sync fd if set
if syncFd != nil {
fd := proc.ExtraFile(s.cmd, syncFd)
s.sync = &fd
}
fmsg.VPrintln("starting shim via fsu:", s.cmd)
// withhold messages to stderr
fmsg.Suspend()
if err := s.cmd.Start(); err != nil {
return nil, fmsg.WrapErrorSuffix(err,
"cannot start fsu:")
}
startTime := time.Now().UTC()
return &startTime, nil
}
func (s *Shim) Serve(ctx context.Context, payload *Payload) error {
// kill shim if something goes wrong and an error is returned
s.killFallback = make(chan error, 1)
killShim := func() {
if err := s.cmd.Process.Signal(os.Interrupt); err != nil {
s.killFallback <- err
}
}
defer func() { killShim() }()
payload.Sync = s.sync
encodeErr := make(chan error)
go func() { encodeErr <- s.encoder.Encode(payload) }()
select {
// encode return indicates setup completion
case err := <-encodeErr:
if err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot transmit shim config:")
}
killShim = func() {}
return nil
// setup canceled before payload was accepted
case <-ctx.Done():
err := ctx.Err()
if errors.Is(err, context.Canceled) {
return fmsg.WrapError(errors.New("shim setup canceled"),
"shim setup canceled")
}
if errors.Is(err, context.DeadlineExceeded) {
return fmsg.WrapError(errors.New("deadline exceeded waiting for shim"),
"deadline exceeded waiting for shim")
}
// unreachable
return err
}
}

View File

@@ -0,0 +1,23 @@
package shim
import (
"git.gensokyo.uk/security/fortify/helper/bwrap"
)
const Env = "FORTIFY_SHIM"
type Payload struct {
// child full argv
Argv []string
// bwrap, target full exec path
Exec [2]string
// bwrap config
Bwrap *bwrap.Config
// path to outer home directory
Home string
// sync fd
Sync *uintptr
// verbosity pass through
Verbose bool
}