fmsg: implement suspend in writer
All checks were successful
Test / Create distribution (push) Successful in 24s
Test / Run NixOS test (push) Successful in 2m18s

This removes the requirement to call fmsg.Exit on every exit path, and enables direct use of the "log" package. However, fmsg.BeforeExit is still encouraged when possible to catch exit on suspended output.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-02-16 17:26:09 +09:00
parent 33a4ab11c2
commit e599b5583d
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
38 changed files with 336 additions and 382 deletions

View File

@ -2,10 +2,10 @@ package main
import (
"encoding/json"
"log"
"os"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/system"
)
@ -63,18 +63,18 @@ func loadBundleInfo(name string, beforeFail func()) *bundleInfo {
bundle := new(bundleInfo)
if f, err := os.Open(name); err != nil {
beforeFail()
fmsg.Fatalf("cannot open bundle: %v", err)
log.Fatalf("cannot open bundle: %v", err)
} else if err = json.NewDecoder(f).Decode(&bundle); err != nil {
beforeFail()
fmsg.Fatalf("cannot parse bundle metadata: %v", err)
log.Fatalf("cannot parse bundle metadata: %v", err)
} else if err = f.Close(); err != nil {
fmsg.Printf("cannot close bundle metadata: %v", err)
log.Printf("cannot close bundle metadata: %v", err)
// not fatal
}
if bundle.ID == "" {
beforeFail()
fmsg.Fatal("application identifier must not be empty")
log.Fatal("application identifier must not be empty")
}
return bundle
@ -82,7 +82,7 @@ func loadBundleInfo(name string, beforeFail func()) *bundleInfo {
func formatHostname(name string) string {
if h, err := os.Hostname(); err != nil {
fmsg.Printf("cannot get hostname: %v", err)
log.Printf("cannot get hostname: %v", err)
return "fortify-" + name
} else {
return h + "-" + name

View File

@ -3,10 +3,12 @@ package main
import (
"encoding/json"
"flag"
"log"
"os"
"path"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
@ -25,12 +27,12 @@ func actionInstall(args []string) {
args = set.Args()
if len(args) != 1 {
fmsg.Fatal("invalid argument")
log.Fatal("invalid argument")
}
pkgPath := args[0]
if !path.IsAbs(pkgPath) {
if dir, err := os.Getwd(); err != nil {
fmsg.Fatalf("cannot get current directory: %v", err)
log.Fatalf("cannot get current directory: %v", err)
} else {
pkgPath = path.Join(dir, pkgPath)
}
@ -54,7 +56,7 @@ func actionInstall(args []string) {
var workDir string
if p, err := os.MkdirTemp("", "fpkg.*"); err != nil {
fmsg.Fatalf("cannot create temporary directory: %v", err)
log.Fatalf("cannot create temporary directory: %v", err)
} else {
workDir = p
}
@ -78,19 +80,17 @@ func actionInstall(args []string) {
if s, err := os.Stat(pathSet.metaPath); err != nil {
if !os.IsNotExist(err) {
cleanup()
fmsg.Fatalf("cannot access %q: %v", pathSet.metaPath, err)
panic("unreachable")
log.Fatalf("cannot access %q: %v", pathSet.metaPath, err)
}
// did not modify app, clean installation condition met later
} else if s.IsDir() {
cleanup()
fmsg.Fatalf("metadata path %q is not a file", pathSet.metaPath)
panic("unreachable")
log.Fatalf("metadata path %q is not a file", pathSet.metaPath)
} else {
app = loadBundleInfo(pathSet.metaPath, cleanup)
if app.ID != bundle.ID {
cleanup()
fmsg.Fatalf("app %q claims to have identifier %q", bundle.ID, app.ID)
log.Fatalf("app %q claims to have identifier %q", bundle.ID, app.ID)
}
// sec: should verify credentials
}
@ -102,21 +102,20 @@ func actionInstall(args []string) {
app.Launcher == bundle.Launcher &&
app.ActivationPackage == bundle.ActivationPackage {
cleanup()
fmsg.Printf("package %q is identical to local application %q", pkgPath, app.ID)
fmsg.Exit(0)
log.Printf("package %q is identical to local application %q", pkgPath, app.ID)
internal.Exit(0)
}
// AppID determines uid
if app.AppID != bundle.AppID {
cleanup()
fmsg.Fatalf("package %q app id %d differs from installed %d", pkgPath, bundle.AppID, app.AppID)
panic("unreachable")
log.Fatalf("package %q app id %d differs from installed %d", pkgPath, bundle.AppID, app.AppID)
}
// sec: should compare version string
fmsg.VPrintf("installing application %q version %q over local %q", bundle.ID, bundle.Version, app.Version)
fmsg.Verbosef("installing application %q version %q over local %q", bundle.ID, bundle.Version, app.Version)
} else {
fmsg.VPrintf("application %q clean installation", bundle.ID)
fmsg.Verbosef("application %q clean installation", bundle.ID)
// sec: should install credentials
}
@ -174,21 +173,18 @@ func actionInstall(args []string) {
// serialise metadata to ensure consistency
if f, err := os.OpenFile(pathSet.metaPath+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
cleanup()
fmsg.Fatalf("cannot create metadata file: %v", err)
panic("unreachable")
log.Fatalf("cannot create metadata file: %v", err)
} else if err = json.NewEncoder(f).Encode(bundle); err != nil {
cleanup()
fmsg.Fatalf("cannot write metadata: %v", err)
panic("unreachable")
log.Fatalf("cannot write metadata: %v", err)
} else if err = f.Close(); err != nil {
fmsg.Printf("cannot close metadata file: %v", err)
log.Printf("cannot close metadata file: %v", err)
// not fatal
}
if err := os.Rename(pathSet.metaPath+"~", pathSet.metaPath); err != nil {
cleanup()
fmsg.Fatalf("cannot rename metadata file: %v", err)
panic("unreachable")
log.Fatalf("cannot rename metadata file: %v", err)
}
cleanup()

View File

@ -2,8 +2,10 @@ package main
import (
"flag"
"log"
"os"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
@ -11,7 +13,7 @@ const shell = "/run/current-system/sw/bin/bash"
func init() {
if err := os.Setenv("SHELL", shell); err != nil {
fmsg.Fatalf("cannot set $SHELL: %v", err)
log.Fatalf("cannot set $SHELL: %v", err)
}
}
@ -24,14 +26,14 @@ func init() {
}
func main() {
fmsg.SetPrefix("fpkg")
fmsg.Prepare("fpkg")
flag.Parse()
fmsg.SetVerbose(flagVerbose)
fmsg.Store(flagVerbose)
args := flag.Args()
if len(args) < 1 {
fmsg.Fatal("invalid argument")
log.Fatal("invalid argument")
}
switch args[0] {
@ -41,8 +43,8 @@ func main() {
actionStart(args[1:])
default:
fmsg.Fatal("invalid argument")
log.Fatal("invalid argument")
}
fmsg.Exit(0)
internal.Exit(0)
}

View File

@ -1,6 +1,7 @@
package main
import (
"log"
"os"
"os/exec"
"path"
@ -25,8 +26,8 @@ func init() {
func lookPath(file string) string {
if p, err := exec.LookPath(file); err != nil {
fmsg.Fatalf("%s: command not found", file)
panic("unreachable")
log.Fatalf("%s: command not found", file)
return ""
} else {
return p
}
@ -35,15 +36,14 @@ func lookPath(file string) string {
var beforeRunFail = new(atomic.Pointer[func()])
func mustRun(name string, arg ...string) {
fmsg.VPrintf("spawning process: %q %q", name, arg)
fmsg.Verbosef("spawning process: %q %q", name, arg)
cmd := exec.Command(name, arg...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
if f := beforeRunFail.Swap(nil); f != nil {
(*f)()
}
fmsg.Fatalf("%s: %v", name, err)
panic("unreachable")
log.Fatalf("%s: %v", name, err)
}
}

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"io"
"log"
"os"
"os/exec"
@ -25,14 +26,12 @@ func fortifyApp(config *fst.Config, beforeFail func()) {
)
if p, ok := internal.Path(Fmain); !ok {
beforeFail()
fmsg.Fatal("invalid fortify path, this copy of fpkg is not compiled correctly")
panic("unreachable")
log.Fatal("invalid fortify path, this copy of fpkg is not compiled correctly")
} else if r, w, err := os.Pipe(); err != nil {
beforeFail()
fmsg.Fatalf("cannot pipe: %v", err)
panic("unreachable")
log.Fatalf("cannot pipe: %v", err)
} else {
if fmsg.Verbose() {
if fmsg.Load() {
cmd = exec.Command(p, "-v", "app", "3")
} else {
cmd = exec.Command(p, "app", "3")
@ -45,26 +44,22 @@ func fortifyApp(config *fst.Config, beforeFail func()) {
go func() {
if err := json.NewEncoder(st).Encode(config); err != nil {
beforeFail()
fmsg.Fatalf("cannot send configuration: %v", err)
panic("unreachable")
log.Fatalf("cannot send configuration: %v", err)
}
}()
if err := cmd.Start(); err != nil {
beforeFail()
fmsg.Fatalf("cannot start fortify: %v", err)
panic("unreachable")
log.Fatalf("cannot start fortify: %v", err)
}
if err := cmd.Wait(); err != nil {
var exitError *exec.ExitError
if errors.As(err, &exitError) {
beforeFail()
fmsg.Exit(exitError.ExitCode())
panic("unreachable")
internal.Exit(exitError.ExitCode())
} else {
beforeFail()
fmsg.Fatalf("cannot wait: %v", err)
panic("unreachable")
log.Fatalf("cannot wait: %v", err)
}
}
}

View File

@ -2,11 +2,12 @@ package main
import (
"flag"
"log"
"path"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal"
)
func actionStart(args []string) {
@ -26,7 +27,7 @@ func actionStart(args []string) {
args = set.Args()
if len(args) < 1 {
fmsg.Fatal("invalid argument")
log.Fatal("invalid argument")
}
/*
@ -37,7 +38,7 @@ func actionStart(args []string) {
pathSet := pathSetByApp(id)
app := loadBundleInfo(pathSet.metaPath, func() {})
if app.ID != id {
fmsg.Fatalf("app %q claims to have identifier %q", id, app.ID)
log.Fatalf("app %q claims to have identifier %q", id, app.ID)
}
/*
@ -144,7 +145,7 @@ func actionStart(args []string) {
*/
fortifyApp(config, func() {})
fmsg.Exit(0)
internal.Exit(0)
}
func appendGPUFilesystem(config *fst.Config) {

View File

@ -6,7 +6,7 @@ import (
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal"
)
func withNixDaemon(
@ -95,7 +95,7 @@ func fortifyAppDropShell(config *fst.Config, dropShell bool, beforeFail func())
config.Command = []string{shell, "-l"}
fortifyApp(config, beforeFail)
beforeFail()
fmsg.Exit(0)
internal.Exit(0)
}
fortifyApp(config, beforeFail)
}

View File

@ -2,6 +2,7 @@ package main
import (
"errors"
"log"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/fmsg"
@ -10,13 +11,13 @@ import (
func logWaitError(err error) {
var e *fmsg.BaseError
if !fmsg.AsBaseError(err, &e) {
fmsg.Println("wait failed:", err)
log.Println("wait failed:", err)
} else {
// Wait only returns either *app.ProcessError or *app.StateStoreError wrapped in a *app.BaseError
var se *app.StateStoreError
if !errors.As(err, &se) {
// does not need special handling
fmsg.Print(e.Message())
log.Print(e.Message())
} else {
// inner error are either unwrapped store errors
// or joined errors returned by *appSealTx revert
@ -24,7 +25,7 @@ func logWaitError(err error) {
var ej app.RevertCompoundError
if !errors.As(se.InnerErr, &ej) {
// does not require special handling
fmsg.Print(e.Message())
log.Print(e.Message())
} else {
errs := ej.Unwrap()
@ -33,10 +34,10 @@ func logWaitError(err error) {
var eb *fmsg.BaseError
if !errors.As(ei, &eb) {
// unreachable
fmsg.Println("invalid error type returned by revert:", ei)
log.Println("invalid error type returned by revert:", ei)
} else {
// print inner *app.BaseError message
fmsg.Print(eb.Message())
log.Print(eb.Message())
}
}
}
@ -48,8 +49,8 @@ func logBaseError(err error, message string) {
var e *fmsg.BaseError
if fmsg.AsBaseError(err, &e) {
fmsg.Print(e.Message())
log.Print(e.Message())
} else {
fmsg.Println(message, err)
log.Println(message, err)
}
}

View File

@ -53,7 +53,7 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
}
if s.Syscall == nil {
fmsg.VPrintln("syscall filter not configured, PROCEED WITH CAUTION")
fmsg.Verbose("syscall filter not configured, PROCEED WITH CAUTION")
}
var uid int
@ -121,11 +121,11 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
// get parent dir of socket
dir := path.Dir(pair[1])
if dir == "." || dir == "/" {
fmsg.VPrintf("dbus socket %q is in an unusual location", pair[1])
fmsg.Verbosef("dbus socket %q is in an unusual location", pair[1])
}
hidePaths = append(hidePaths, dir)
} else {
fmsg.VPrintf("dbus socket %q is not absolute", pair[1])
fmsg.Verbosef("dbus socket %q is not absolute", pair[1])
}
}
}
@ -169,7 +169,7 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
return nil, err
} else if ok {
hidePathMatch[i] = true
fmsg.VPrintf("hiding paths from %q", c.Src)
fmsg.Verbosef("hiding paths from %q", c.Src)
}
}
@ -221,7 +221,7 @@ func evalSymlinks(os linux.System, v *string) error {
if !errors.Is(err, fs.ErrNotExist) {
return err
}
fmsg.VPrintf("path %q does not yet exist", *v)
fmsg.Verbosef("path %q does not yet exist", *v)
} else {
*v = p
}

View File

@ -1,6 +1,7 @@
package bwrap_test
import (
"log"
"os"
"slices"
"testing"
@ -8,11 +9,10 @@ import (
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/helper/seccomp"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
func TestConfig_Args(t *testing.T) {
seccomp.CPrintln = fmsg.Println
seccomp.CPrintln = log.Println
t.Cleanup(func() { seccomp.CPrintln = nil })
testCases := []struct {

View File

@ -4,12 +4,12 @@ import (
"crypto/sha512"
"errors"
"io"
"log"
"slices"
"syscall"
"testing"
"git.gensokyo.uk/security/fortify/helper/seccomp"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
func TestExport(t *testing.T) {
@ -79,7 +79,7 @@ func TestExport(t *testing.T) {
buf := make([]byte, 8)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
seccomp.CPrintln = fmsg.Println
seccomp.CPrintln = log.Println
t.Cleanup(func() { seccomp.CPrintln = nil })
e := seccomp.New(tc.opts)

View File

@ -14,7 +14,7 @@ import (
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal"
)
// InternalChildStub is an internal function but exported because it is cross-package;
@ -40,7 +40,7 @@ func InternalChildStub() {
genericStub(flagRestoreFiles(4, ap, sp))
}
fmsg.Exit(0)
internal.Exit(0)
}
// InternalReplaceExecCommand is an internal function but exported because it is cross-package;

View File

@ -4,7 +4,7 @@ import (
"os"
"path"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal"
)
// used by the parent process
@ -13,6 +13,6 @@ import (
func TryArgv0() {
if len(os.Args) > 0 && path.Base(os.Args[0]) == "init" {
Main()
fmsg.Exit(0)
internal.Exit(0)
}
}

View File

@ -2,6 +2,7 @@ package init0
import (
"errors"
"log"
"os"
"os/exec"
"os/signal"
@ -24,17 +25,15 @@ const (
func Main() {
// sharing stdout with shim
// USE WITH CAUTION
fmsg.SetPrefix("init")
fmsg.Prepare("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")
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
}
if os.Getpid() != 1 {
fmsg.Fatal("this process must run as pid 1")
panic("unreachable")
log.Fatal("this process must run as pid 1")
}
// receive setup payload
@ -44,30 +43,29 @@ func Main() {
)
if f, err := proc.Receive(Env, &payload); err != nil {
if errors.Is(err, proc.ErrInvalid) {
fmsg.Fatal("invalid config descriptor")
log.Fatal("invalid config descriptor")
}
if errors.Is(err, proc.ErrNotSet) {
fmsg.Fatal("FORTIFY_INIT not set")
log.Fatal("FORTIFY_INIT not set")
}
fmsg.Fatalf("cannot decode init setup payload: %v", err)
panic("unreachable")
log.Fatalf("cannot decode init setup payload: %v", err)
} else {
fmsg.SetVerbose(payload.Verbose)
fmsg.Store(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)
log.Printf("cannot unset %s: %v", Env, err)
// not fatal
} else {
fmsg.VPrintln("received configuration")
fmsg.Verbose("received configuration")
}
}
// die with parent
if err := internal.PR_SET_PDEATHSIG__SIGKILL(); err != nil {
fmsg.Fatalf("prctl(PR_SET_PDEATHSIG, SIGKILL): %v", err)
log.Fatalf("prctl(PR_SET_PDEATHSIG, SIGKILL): %v", err)
}
cmd := exec.Command(payload.Argv0)
@ -76,13 +74,13 @@ func Main() {
cmd.Env = os.Environ()
if err := cmd.Start(); err != nil {
fmsg.Fatalf("cannot start %q: %v", payload.Argv0, err)
log.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)
log.Println("cannot close setup pipe:", err)
// not fatal
}
@ -119,7 +117,7 @@ func Main() {
}
}
if !errors.Is(err, syscall.ECHILD) {
fmsg.Println("unexpected wait4 response:", err)
log.Println("unexpected wait4 response:", err)
}
close(done)
@ -132,9 +130,12 @@ func Main() {
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)
if fmsg.Resume() {
fmsg.Verbosef("terminating on %s after process start", s.String())
} else {
fmsg.Verbosef("terminating on %s", s.String())
}
internal.Exit(0)
case w := <-info:
if w.wpid == cmd.Process.Pid {
// initial process exited, output is most likely available again
@ -155,10 +156,10 @@ func Main() {
}()
}
case <-done:
fmsg.Exit(r)
internal.Exit(r)
case <-timeout:
fmsg.Println("timeout exceeded waiting for lingering processes")
fmsg.Exit(r)
log.Println("timeout exceeded waiting for lingering processes")
internal.Exit(r)
}
}
}

View File

@ -191,7 +191,7 @@ func (a *app) Seal(config *fst.Config) error {
// map sandbox config to bwrap
if config.Confinement.Sandbox == nil {
fmsg.VPrintln("sandbox configuration not supplied, PROCEED WITH CAUTION")
fmsg.Verbose("sandbox configuration not supplied, PROCEED WITH CAUTION")
// permissive defaults
conf := &fst.SandboxConfig{
@ -264,7 +264,7 @@ func (a *app) Seal(config *fst.Config) error {
}
// verbose log seal information
fmsg.VPrintf("created application seal for uid %s (%s) groups: %v, command: %s",
fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, command: %s",
seal.sys.user.us, seal.sys.user.username, config.Confinement.Groups, config.Command)
// seal app and release lock

View File

@ -143,7 +143,7 @@ func (seal *appSeal) setupShares(bus [2]*dbus.Config, os linux.System) error {
if seal.et.Has(system.EWayland) {
var socketPath string
if name, ok := os.LookupEnv(wl.WaylandDisplay); !ok {
fmsg.VPrintln(wl.WaylandDisplay + " is not set, assuming " + wl.FallbackName)
fmsg.Verbose(wl.WaylandDisplay + " is not set, assuming " + wl.FallbackName)
socketPath = path.Join(seal.RuntimePath, wl.FallbackName)
} else if !path.IsAbs(name) {
socketPath = path.Join(seal.RuntimePath, name)
@ -166,7 +166,7 @@ func (seal *appSeal) setupShares(bus [2]*dbus.Config, os linux.System) error {
seal.sys.Wayland(outerPath, socketPath, appID, seal.id)
seal.sys.bwrap.Bind(outerPath, innerPath)
} else { // bind mount wayland socket (insecure)
fmsg.VPrintln("direct wayland access, PROCEED WITH CAUTION")
fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION")
seal.sys.bwrap.Bind(socketPath, innerPath)
// ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
@ -229,7 +229,7 @@ func (seal *appSeal) setupShares(bus [2]*dbus.Config, os linux.System) error {
// publish current user's pulse cookie for target user
if src, err := discoverPulseCookie(os); err != nil {
// not fatal
fmsg.VPrintln(strings.TrimSpace(err.(*fmsg.BaseError).Message()))
fmsg.Verbose(strings.TrimSpace(err.(*fmsg.BaseError).Message()))
} else {
dst := path.Join(seal.share, "pulse-cookie")
innerDst := fst.Tmp + "/pulse-cookie"

View File

@ -3,6 +3,7 @@ package shim
import (
"context"
"errors"
"log"
"os"
"os/exec"
"os/signal"
@ -25,12 +26,11 @@ import (
func Main() {
// sharing stdout with fortify
// USE WITH CAUTION
fmsg.SetPrefix("shim")
fmsg.Prepare("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")
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
}
// receive setup payload
@ -40,21 +40,20 @@ func Main() {
)
if f, err := proc.Receive(Env, &payload); err != nil {
if errors.Is(err, proc.ErrInvalid) {
fmsg.Fatal("invalid config descriptor")
log.Fatal("invalid config descriptor")
}
if errors.Is(err, proc.ErrNotSet) {
fmsg.Fatal("FORTIFY_SHIM not set")
log.Fatal("FORTIFY_SHIM not set")
}
fmsg.Fatalf("cannot decode shim setup payload: %v", err)
panic("unreachable")
log.Fatalf("cannot decode shim setup payload: %v", err)
} else {
fmsg.SetVerbose(payload.Verbose)
fmsg.Store(payload.Verbose)
closeSetup = f
}
if payload.Bwrap == nil {
fmsg.Fatal("bwrap config not supplied")
log.Fatal("bwrap config not supplied")
}
// restore bwrap sync fd
@ -65,7 +64,7 @@ func Main() {
// close setup socket
if err := closeSetup(); err != nil {
fmsg.Println("cannot close setup pipe:", err)
log.Println("cannot close setup pipe:", err)
// not fatal
}
@ -73,15 +72,15 @@ func Main() {
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)
log.Fatalf("cannot create home directory: %v", err)
}
} else {
fmsg.Fatalf("cannot access home directory: %v", err)
log.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)
log.Fatalf("data path %q is not a directory", payload.Home)
}
var ic init0.Payload
@ -95,10 +94,10 @@ func Main() {
// no argv, look up shell instead
var ok bool
if payload.Bwrap.SetEnv == nil {
fmsg.Fatal("no command was specified and environment is unset")
log.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")
log.Fatal("no command was specified and $SHELL was unset")
}
ic.Argv = []string{ic.Argv0}
@ -110,20 +109,20 @@ func Main() {
// serve setup payload
if fd, encoder, err := proc.Setup(&extraFiles); err != nil {
fmsg.Fatalf("cannot pipe: %v", err)
log.Fatalf("cannot pipe: %v", err)
} else {
conf.SetEnv[init0.Env] = strconv.Itoa(fd)
go func() {
fmsg.VPrintln("transmitting config to init")
fmsg.Verbose("transmitting config to init")
if err = encoder.Encode(&ic); err != nil {
fmsg.Fatalf("cannot transmit init config: %v", err)
log.Fatalf("cannot transmit init config: %v", err)
}
}()
}
helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
if fmsg.Verbose() {
seccomp.CPrintln = fmsg.Println
if fmsg.Load() {
seccomp.CPrintln = log.Println
}
if b, err := helper.NewBwrap(
conf, path.Join(fst.Tmp, "sbin/init"),
@ -131,7 +130,7 @@ func Main() {
extraFiles,
syncFd,
); err != nil {
fmsg.Fatalf("malformed sandbox config: %v", err)
log.Fatalf("malformed sandbox config: %v", err)
} else {
b.Stdin(os.Stdin).Stdout(os.Stdout).Stderr(os.Stderr)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
@ -139,15 +138,15 @@ func Main() {
// run and pass through exit code
if err = b.Start(ctx, false); err != nil {
fmsg.Fatalf("cannot start target process: %v", err)
log.Fatalf("cannot start target process: %v", err)
} else if err = b.Wait(); err != nil {
var exitError *exec.ExitError
if !errors.As(err, &exitError) {
fmsg.Println("wait:", err)
fmsg.Exit(127)
log.Printf("wait: %v", err)
internal.Exit(127)
panic("unreachable")
}
fmsg.Exit(exitError.ExitCode())
internal.Exit(exitError.ExitCode())
panic("unreachable")
}
}

View File

@ -4,6 +4,7 @@ import (
"context"
"encoding/gob"
"errors"
"log"
"os"
"os/exec"
"strconv"
@ -54,8 +55,7 @@ func (s *Shim) Start(
// 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")
log.Fatal("invalid fsu path, this copy of fortify is not compiled correctly")
} else {
fsu = p
}
@ -75,7 +75,7 @@ func (s *Shim) Start(
// format fsu supplementary groups
if len(supp) > 0 {
fmsg.VPrintf("attaching supplementary group ids %s", supp)
fmsg.Verbosef("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
@ -87,7 +87,7 @@ func (s *Shim) Start(
s.sync = &fd
}
fmsg.VPrintln("starting shim via fsu:", s.cmd)
fmsg.Verbose("starting shim via fsu:", s.cmd)
// withhold messages to stderr
fmsg.Suspend()
if err := s.cmd.Start(); err != nil {

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"log"
"os/exec"
"path/filepath"
"strings"
@ -81,7 +82,7 @@ func (a *app) Run(ctx context.Context, rs *RunState) error {
Bwrap: a.seal.sys.bwrap,
Home: a.seal.sys.user.data,
Verbose: fmsg.Verbose(),
Verbose: fmsg.Load(),
}); err != nil {
return err
}
@ -119,8 +120,8 @@ func (a *app) Run(ctx context.Context, rs *RunState) error {
} else {
rs.ExitCode = a.shim.Unwrap().ProcessState.ExitCode()
}
if fmsg.Verbose() {
fmsg.VPrintf("process %d exited with exit code %d", a.shim.Unwrap().Process.Pid, rs.ExitCode)
if fmsg.Load() {
fmsg.Verbosef("process %d exited with exit code %d", a.shim.Unwrap().Process.Pid, rs.ExitCode)
}
// this is reached when a fault makes an already running shim impossible to continue execution
@ -128,11 +129,11 @@ func (a *app) Run(ctx context.Context, rs *RunState) error {
// the effects of this is similar to the alternative exit path and ensures shim death
case err := <-a.shim.WaitFallback():
rs.ExitCode = 255
fmsg.Printf("cannot terminate shim on faulted setup: %v", err)
log.Printf("cannot terminate shim on faulted setup: %v", err)
// alternative exit path relying on shim behaviour on monitor process exit
case <-ctx.Done():
fmsg.VPrintln("alternative exit path selected")
fmsg.Verbose("alternative exit path selected")
}
// child process exited, resume output
@ -163,10 +164,10 @@ func (a *app) Run(ctx context.Context, rs *RunState) error {
} else {
if l := len(states); l == 0 {
// cleanup globals as the final launcher
fmsg.VPrintln("no other launchers active, will clean up globals")
fmsg.Verbose("no other launchers active, will clean up globals")
ec.Set(system.User)
} else {
fmsg.VPrintf("found %d active launchers, cleaning up without globals", l)
fmsg.Verbosef("found %d active launchers, cleaning up without globals", l)
}
// accumulate capabilities of other launchers
@ -174,7 +175,7 @@ func (a *app) Run(ctx context.Context, rs *RunState) error {
if s.Config != nil {
*rt |= s.Config.Confinement.Enablements
} else {
fmsg.Printf("state entry %d does not contain config", i)
log.Printf("state entry %d does not contain config", i)
}
}
}
@ -184,7 +185,7 @@ func (a *app) Run(ctx context.Context, rs *RunState) error {
ec.Set(i)
}
}
if fmsg.Verbose() {
if fmsg.Load() {
labels := make([]string, 0, system.ELen+1)
for i := system.Enablement(0); i < system.Enablement(system.ELen+2); i++ {
if ec.Has(i) {
@ -192,7 +193,7 @@ func (a *app) Run(ctx context.Context, rs *RunState) error {
}
}
if len(labels) > 0 {
fmsg.VPrintln("reverting operations labelled", strings.Join(labels, ", "))
fmsg.Verbose("reverting operations labelled", strings.Join(labels, ", "))
}
}

View File

@ -1,10 +1,9 @@
package proc
package internal
import (
"log"
"os"
"sync"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
var (
@ -14,7 +13,7 @@ var (
func copyExecutable() {
if name, err := os.Executable(); err != nil {
fmsg.Fatalf("cannot read executable path: %v", err)
log.Fatalf("cannot read executable path: %v", err)
} else {
executable = name
}

9
internal/exit.go Normal file
View File

@ -0,0 +1,9 @@
package internal
import (
"os"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
func Exit(code int) { fmsg.BeforeExit(); os.Exit(code) }

View File

@ -1,98 +0,0 @@
package fmsg
import (
"os"
"sync"
"sync/atomic"
)
var (
wstate atomic.Bool
dropped atomic.Uint64
withhold = make(chan struct{}, 1)
msgbuf = make(chan dOp, 64) // these ops are tiny so a large buffer is allocated for withholding output
dequeueOnce sync.Once
queueSync sync.WaitGroup
)
func dequeue() {
go func() {
for {
select {
case op := <-msgbuf:
op.Do()
queueSync.Done()
case <-withhold:
<-withhold
}
}
}()
}
// queue submits ops to msgbuf but drops messages
// when the buffer is full and dequeue is withholding
func queue(op dOp) {
queueSync.Add(1)
select {
case msgbuf <- op:
default:
// send the op anyway if not withholding
// as dequeue will get to it eventually
if !wstate.Load() {
msgbuf <- op
} else {
queueSync.Done()
// increment dropped message count
dropped.Add(1)
}
}
}
type dOp interface{ Do() }
func Exit(code int) {
Resume() // resume here to avoid deadlock
queueSync.Wait()
os.Exit(code)
}
func Suspend() {
dequeueOnce.Do(dequeue)
if wstate.CompareAndSwap(false, true) {
queueSync.Wait()
withhold <- struct{}{}
}
}
func Resume() {
dequeueOnce.Do(dequeue)
if wstate.CompareAndSwap(true, false) {
withhold <- struct{}{}
if d := dropped.Swap(0); d != 0 {
Printf("dropped %d messages during withhold", d)
}
}
}
type dPrint []any
func (v dPrint) Do() {
std.Print(v...)
}
type dPrintf struct {
format string
v []any
}
func (d *dPrintf) Do() {
std.Printf(d.format, d.v...)
}
type dPrintln []any
func (v dPrintln) Do() {
std.Println(v...)
}

View File

@ -2,39 +2,85 @@
package fmsg
import (
"bytes"
"io"
"log"
"os"
"sync"
"sync/atomic"
"syscall"
)
var std = log.New(os.Stderr, "fortify: ", 0)
const (
bufSize = 4 * 1024
bufSizeMax = 16 * 1024 * 1024
)
func SetPrefix(prefix string) {
prefix += ": "
std.SetPrefix(prefix)
std.SetPrefix(prefix)
var o = &suspendable{w: os.Stderr}
// Prepare configures the system logger for [Suspend] and [Resume] to take effect.
func Prepare(prefix string) { log.SetPrefix(prefix + ": "); log.SetFlags(0); log.SetOutput(o) }
type suspendable struct {
w io.Writer
s atomic.Bool
buf bytes.Buffer
bufOnce sync.Once
bufMu sync.Mutex
dropped int
}
func Print(v ...any) {
dequeueOnce.Do(dequeue)
queue(dPrint(v))
func (s *suspendable) Write(p []byte) (n int, err error) {
if !s.s.Load() {
return s.w.Write(p)
}
s.bufOnce.Do(func() { s.prepareBuf() })
s.bufMu.Lock()
defer s.bufMu.Unlock()
if l := len(p); s.buf.Len()+l > bufSizeMax {
s.dropped += l
return 0, syscall.ENOMEM
}
return s.buf.Write(p)
}
func Printf(format string, v ...any) {
dequeueOnce.Do(dequeue)
queue(&dPrintf{format, v})
func (s *suspendable) prepareBuf() { s.buf.Grow(bufSize) }
func (s *suspendable) Suspend() bool { return o.s.CompareAndSwap(false, true) }
func (s *suspendable) Resume() (resumed bool, dropped uintptr, n int64, err error) {
if o.s.CompareAndSwap(true, false) {
o.bufMu.Lock()
defer o.bufMu.Unlock()
resumed = true
dropped = uintptr(o.dropped)
o.dropped = 0
n, err = io.Copy(s.w, &s.buf)
s.buf = bytes.Buffer{}
s.prepareBuf()
}
return
}
func Println(v ...any) {
dequeueOnce.Do(dequeue)
queue(dPrintln(v))
func Suspend() bool { return o.Suspend() }
func Resume() bool {
resumed, dropped, _, err := o.Resume()
if err != nil {
// probably going to result in an error as well,
// so this call is as good as unreachable
log.Printf("cannot dump buffer on resume: %v", err)
}
if resumed && dropped > 0 {
log.Fatalf("dropped %d bytes while output is suspended", dropped)
}
return resumed
}
func Fatal(v ...any) {
Print(v...)
Exit(1)
}
func Fatalf(format string, v ...any) {
Printf(format, v...)
Exit(1)
func BeforeExit() {
if Resume() {
log.Printf("beforeExit reached on suspended output")
}
}

View File

@ -1,25 +1,23 @@
package fmsg
import "sync/atomic"
import (
"log"
"sync/atomic"
)
var verbose = new(atomic.Bool)
func Verbose() bool {
return verbose.Load()
}
func Load() bool { return verbose.Load() }
func Store(v bool) { verbose.Store(v) }
func SetVerbose(v bool) {
verbose.Store(v)
}
func VPrintf(format string, v ...any) {
func Verbosef(format string, v ...any) {
if verbose.Load() {
Printf(format, v...)
log.Printf(format, v...)
}
}
func VPrintln(v ...any) {
func Verbose(v ...any) {
if verbose.Load() {
Println(v...)
log.Println(v...)
}
}

View File

@ -54,7 +54,7 @@ type Paths struct {
func CopyPaths(os System, v *Paths) {
v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Geteuid()))
fmsg.VPrintf("process share directory at %q", v.SharePath)
fmsg.Verbosef("process share directory at %q", v.SharePath)
if r, ok := os.LookupEnv(xdgRuntimeDir); !ok || r == "" || !path.IsAbs(r) {
// fall back to path in share since fortify has no hard XDG dependency
@ -65,5 +65,5 @@ func CopyPaths(os System, v *Paths) {
v.RunDirPath = path.Join(v.RuntimePath, "fortify")
}
fmsg.VPrintf("runtime directory at %q", v.RunDirPath)
fmsg.Verbosef("runtime directory at %q", v.RunDirPath)
}

View File

@ -3,6 +3,7 @@ package linux
import (
"errors"
"io/fs"
"log"
"os"
"os/exec"
"os/user"
@ -11,9 +12,7 @@ import (
"sync"
"syscall"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
// Std implements System using the standard library.
@ -33,13 +32,13 @@ 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 proc.MustExecutable() }
func (s *Std) MustExecutable() string { return internal.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) }
func (s *Std) Open(name string) (fs.File, error) { return os.Open(name) }
func (s *Std) EvalSymlinks(path string) (string, error) { return filepath.EvalSymlinks(path) }
func (s *Std) Exit(code int) { fmsg.Exit(code) }
func (s *Std) Exit(code int) { internal.Exit(code) }
const xdgRuntimeDir = "XDG_RUNTIME_DIR"
@ -74,8 +73,9 @@ func (s *Std) Uid(aid int) (int, error) {
u.uid = -1
if fsu, ok := internal.Check(internal.Fsu); !ok {
fmsg.Fatal("invalid fsu path, this copy of fortify is not compiled correctly")
panic("unreachable")
log.Fatal("invalid fsu path, this copy of fortify is not compiled correctly")
// unreachable
return 0, syscall.EBADE
} else {
cmd := exec.Command(fsu)
cmd.Path = fsu

View File

@ -85,17 +85,17 @@ func (s *multiStore) List() ([]int, error) {
for _, e := range entries {
// skip non-directories
if !e.IsDir() {
fmsg.VPrintf("skipped non-directory entry %q", e.Name())
fmsg.Verbosef("skipped non-directory entry %q", e.Name())
continue
}
// skip non-numerical names
if v, err := strconv.Atoi(e.Name()); err != nil {
fmsg.VPrintf("skipped non-aid entry %q", e.Name())
fmsg.Verbosef("skipped non-aid entry %q", e.Name())
continue
} else {
if v < 0 || v > 9999 {
fmsg.VPrintf("skipped out of bounds entry %q", e.Name())
fmsg.Verbosef("skipped out of bounds entry %q", e.Name())
continue
}

View File

@ -36,18 +36,18 @@ func (a *ACL) Type() Enablement {
}
func (a *ACL) apply(sys *I) error {
fmsg.VPrintln("applying ACL", a)
fmsg.Verbose("applying ACL", a)
return fmsg.WrapErrorSuffix(acl.UpdatePerm(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) {
fmsg.VPrintln("stripping ACL", a)
fmsg.Verbose("stripping ACL", a)
return fmsg.WrapErrorSuffix(acl.UpdatePerm(a.path, sys.uid),
fmt.Sprintf("cannot strip ACL entry from %q:", a.path))
} else {
fmsg.VPrintln("skipping ACL", a)
fmsg.Verbose("skipping ACL", a)
return nil
}
}

View File

@ -3,6 +3,7 @@ package system
import (
"bytes"
"errors"
"log"
"strings"
"sync"
@ -47,12 +48,12 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st
d.proxy = dbus.New(sessionBus, systemBus)
defer func() {
if fmsg.Verbose() && d.proxy.Sealed() {
fmsg.VPrintln("sealed session proxy", session.Args(sessionBus))
if fmsg.Load() && d.proxy.Sealed() {
fmsg.Verbose("sealed session proxy", session.Args(sessionBus))
if system != nil {
fmsg.VPrintln("sealed system proxy", system.Args(systemBus))
fmsg.Verbose("sealed system proxy", system.Args(systemBus))
}
fmsg.VPrintln("message bus proxy final args:", d.proxy)
fmsg.Verbose("message bus proxy final args:", d.proxy)
}
}()
@ -78,9 +79,9 @@ func (d *DBus) Type() Enablement {
}
func (d *DBus) apply(sys *I) error {
fmsg.VPrintf("session bus proxy on %q for upstream %q", d.proxy.Session()[1], d.proxy.Session()[0])
fmsg.Verbosef("session bus proxy on %q for upstream %q", d.proxy.Session()[1], d.proxy.Session()[0])
if d.system {
fmsg.VPrintf("system bus proxy on %q for upstream %q", d.proxy.System()[1], d.proxy.System()[0])
fmsg.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
@ -89,15 +90,15 @@ func (d *DBus) apply(sys *I) error {
return fmsg.WrapErrorSuffix(err,
"cannot start message bus proxy:")
}
fmsg.VPrintln("starting message bus proxy:", d.proxy)
fmsg.Verbose("starting message bus proxy:", d.proxy)
return nil
}
func (d *DBus) revert(_ *I, _ *Criteria) error {
// criteria ignored here since dbus is always process-scoped
fmsg.VPrintln("terminating message bus proxy")
fmsg.Verbose("terminating message bus proxy")
d.proxy.Close()
defer fmsg.VPrintln("message bus proxy exit")
defer fmsg.Verbose("message bus proxy exit")
return fmsg.WrapErrorSuffix(d.proxy.Wait(), "message bus proxy error:")
}
@ -144,7 +145,7 @@ func (s *scanToFmsg) write(p []byte, a int) (int, error) {
func (s *scanToFmsg) Dump() {
s.mu.RLock()
for _, msg := range s.msgbuf {
fmsg.Println(msg)
log.Println(msg)
}
s.mu.RUnlock()
}

View File

@ -28,18 +28,18 @@ type Hardlink struct {
func (l *Hardlink) Type() Enablement { return l.et }
func (l *Hardlink) apply(_ *I) error {
fmsg.VPrintln("linking ", l)
fmsg.Verbose("linking ", l)
return fmsg.WrapErrorSuffix(os.Link(l.src, l.dst),
fmt.Sprintf("cannot link %q:", l.dst))
}
func (l *Hardlink) revert(_ *I, ec *Criteria) error {
if ec.hasType(l) {
fmsg.VPrintf("removing hard link %q", l.dst)
fmsg.Verbosef("removing hard link %q", l.dst)
return fmsg.WrapErrorSuffix(os.Remove(l.dst),
fmt.Sprintf("cannot remove hard link %q:", l.dst))
} else {
fmsg.VPrintf("skipping hard link %q", l.dst)
fmsg.Verbosef("skipping hard link %q", l.dst)
return nil
}
}

View File

@ -40,7 +40,7 @@ func (m *Mkdir) Type() Enablement {
}
func (m *Mkdir) apply(_ *I) error {
fmsg.VPrintln("ensuring directory", m)
fmsg.Verbose("ensuring directory", m)
// create directory
err := os.Mkdir(m.path, m.perm)
@ -61,11 +61,11 @@ func (m *Mkdir) revert(_ *I, ec *Criteria) error {
}
if ec.hasType(m) {
fmsg.VPrintln("destroying ephemeral directory", m)
fmsg.Verbose("destroying ephemeral directory", m)
return fmsg.WrapErrorSuffix(os.Remove(m.path),
fmt.Sprintf("cannot remove ephemeral directory %q:", m.path))
} else {
fmsg.VPrintln("skipping ephemeral directory", m)
fmsg.Verbose("skipping ephemeral directory", m)
return nil
}
}

View File

@ -3,6 +3,7 @@ package system
import (
"context"
"errors"
"log"
"os"
"sync"
@ -105,9 +106,9 @@ func (sys *I) Commit(ctx context.Context) error {
// sp is set to nil when all ops are applied
if sp != nil {
// rollback partial commit
fmsg.VPrintf("commit faulted after %d ops, rolling back partial commit", len(sp.ops))
fmsg.Verbosef("commit faulted after %d ops, rolling back partial commit", len(sp.ops))
if err := sp.Revert(&Criteria{nil}); err != nil {
fmsg.Println("errors returned reverting partial commit:", err)
log.Println("errors returned reverting partial commit:", err)
}
}
}()

View File

@ -44,7 +44,7 @@ func (t *Tmpfile) Type() Enablement {
func (t *Tmpfile) apply(_ *I) error {
switch t.method {
case tmpfileCopy:
fmsg.VPrintln("publishing tmpfile", t)
fmsg.Verbose("publishing tmpfile", t)
return fmsg.WrapErrorSuffix(copyFile(t.dst, t.src),
fmt.Sprintf("cannot copy tmpfile %q:", t.dst))
default:
@ -54,11 +54,11 @@ func (t *Tmpfile) apply(_ *I) error {
func (t *Tmpfile) revert(_ *I, ec *Criteria) error {
if ec.hasType(t) {
fmsg.VPrintf("removing tmpfile %q", t.dst)
fmsg.Verbosef("removing tmpfile %q", t.dst)
return fmsg.WrapErrorSuffix(os.Remove(t.dst),
fmt.Sprintf("cannot remove tmpfile %q:", t.dst))
} else {
fmsg.VPrintf("skipping tmpfile %q", t.dst)
fmsg.Verbosef("skipping tmpfile %q", t.dst)
return nil
}
}

View File

@ -45,7 +45,7 @@ func (w Wayland) apply(sys *I) error {
return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot attach to wayland on %q:", w.pair[1]))
} else {
fmsg.VPrintf("wayland attached on %q", w.pair[1])
fmsg.Verbosef("wayland attached on %q", w.pair[1])
}
if sp, err := w.conn.Bind(w.pair[0], w.appID, w.instanceID); err != nil {
@ -53,7 +53,7 @@ func (w Wayland) apply(sys *I) error {
fmt.Sprintf("cannot bind to socket on %q:", w.pair[0]))
} else {
sys.sp = sp
fmsg.VPrintf("wayland listening on %q", w.pair[0])
fmsg.Verbosef("wayland listening on %q", w.pair[0])
return fmsg.WrapErrorSuffix(errors.Join(os.Chmod(w.pair[0], 0), acl.UpdatePerm(w.pair[0], sys.uid, acl.Read, acl.Write, acl.Execute)),
fmt.Sprintf("cannot chmod socket on %q:", w.pair[0]))
}
@ -61,16 +61,16 @@ func (w Wayland) apply(sys *I) error {
func (w Wayland) revert(_ *I, ec *Criteria) error {
if ec.hasType(w) {
fmsg.VPrintf("removing wayland socket on %q", w.pair[0])
fmsg.Verbosef("removing wayland socket on %q", w.pair[0])
if err := os.Remove(w.pair[0]); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
fmsg.VPrintf("detaching from wayland on %q", w.pair[1])
fmsg.Verbosef("detaching from wayland on %q", w.pair[1])
return fmsg.WrapErrorSuffix(w.conn.Close(),
fmt.Sprintf("cannot detach from wayland on %q:", w.pair[1]))
} else {
fmsg.VPrintf("skipping wayland cleanup on %q", w.pair[0])
fmsg.Verbosef("skipping wayland cleanup on %q", w.pair[0])
return nil
}
}

View File

@ -24,18 +24,18 @@ func (x XHost) Type() Enablement {
}
func (x XHost) apply(_ *I) error {
fmsg.VPrintf("inserting entry %s to X11", x)
fmsg.Verbosef("inserting entry %s to X11", x)
return fmsg.WrapErrorSuffix(xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)),
fmt.Sprintf("cannot insert entry %s to X11:", x))
}
func (x XHost) revert(_ *I, ec *Criteria) error {
if ec.hasType(x) {
fmsg.VPrintf("deleting entry %s from X11", x)
fmsg.Verbosef("deleting entry %s from X11", x)
return fmsg.WrapErrorSuffix(xcb.ChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)),
fmt.Sprintf("cannot delete entry %s from X11:", x))
} else {
fmsg.VPrintf("skipping entry %s in X11", x)
fmsg.Verbosef("skipping entry %s in X11", x)
return nil
}
}

71
main.go
View File

@ -5,6 +5,7 @@ import (
_ "embed"
"flag"
"fmt"
"log"
"os"
"os/signal"
"os/user"
@ -37,6 +38,8 @@ var (
)
func init() {
fmsg.Prepare("fortify")
flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
flag.BoolVar(&flagJSON, "json", false, "Format output in JSON when applicable")
}
@ -62,13 +65,12 @@ func main() {
init0.TryArgv0()
if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
fmsg.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
// not fatal: this program runs as the privileged user
}
if os.Geteuid() == 0 {
fmsg.Fatal("this program must not run as root")
panic("unreachable")
log.Fatal("this program must not run as root")
}
flag.CommandLine.Usage = func() {
@ -96,12 +98,12 @@ func main() {
fmt.Println()
}
flag.Parse()
fmsg.SetVerbose(flagVerbose)
fmsg.Store(flagVerbose)
args := flag.Args()
if len(args) == 0 {
flag.CommandLine.Usage()
fmsg.Exit(0)
internal.Exit(0)
}
switch args[0] {
@ -111,16 +113,20 @@ func main() {
} else {
fmt.Println("impure")
}
fmsg.Exit(0)
internal.Exit(0)
case "license": // print embedded license
fmt.Println(license)
fmsg.Exit(0)
internal.Exit(0)
case "template": // print full template configuration
printJSON(os.Stdout, false, fst.Template())
fmsg.Exit(0)
internal.Exit(0)
case "help": // print help message
flag.CommandLine.Usage()
fmsg.Exit(0)
internal.Exit(0)
case "ps": // print all state info
set := flag.NewFlagSet("ps", flag.ExitOnError)
var short bool
@ -130,7 +136,8 @@ func main() {
_ = set.Parse(args[1:])
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(sys.Paths().RunDirPath), short)
fmsg.Exit(0)
internal.Exit(0)
case "show": // pretty-print app info
set := flag.NewFlagSet("show", flag.ExitOnError)
var short bool
@ -142,6 +149,7 @@ func main() {
switch len(set.Args()) {
case 0: // system
printShowSystem(os.Stdout, short)
case 1: // instance
name := set.Args()[0]
config, instance := tryShort(name)
@ -149,14 +157,15 @@ func main() {
config = tryPath(name)
}
printShowInstance(os.Stdout, time.Now().UTC(), instance, config, short)
default:
fmsg.Fatal("show requires 1 argument")
}
fmsg.Exit(0)
default:
log.Fatal("show requires 1 argument")
}
internal.Exit(0)
case "app": // launch app from configuration file
if len(args) < 2 {
fmsg.Fatal("app requires at least 1 argument")
log.Fatal("app requires at least 1 argument")
}
// config extraArgs...
@ -166,6 +175,7 @@ func main() {
// invoke app
runApp(config)
panic("unreachable")
case "run": // run app in permissive defaults usage pattern
set := flag.NewFlagSet("run", flag.ExitOnError)
@ -208,8 +218,7 @@ func main() {
}
if aid < 0 || aid > 9999 {
fmsg.Fatalf("aid %d out of range", aid)
panic("unreachable")
log.Fatalf("aid %d out of range", aid)
}
// resolve home/username from os when flag is unset
@ -219,13 +228,13 @@ func main() {
passwdFunc = func() {
var us string
if uid, err := sys.Uid(aid); err != nil {
fmsg.Fatalf("cannot obtain uid from fsu: %v", err)
log.Fatalf("cannot obtain uid from fsu: %v", err)
} else {
us = strconv.Itoa(uid)
}
if u, err := user.LookupId(us); err != nil {
fmsg.VPrintf("cannot look up uid %s", us)
fmsg.Verbosef("cannot look up uid %s", us)
passwd = &user.User{
Uid: us,
Gid: us,
@ -267,7 +276,7 @@ func main() {
config.Confinement.SessionBus = dbus.NewConfig(fid, true, mpris)
} else {
if c, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
fmsg.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
log.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
} else {
config.Confinement.SessionBus = c
}
@ -276,7 +285,7 @@ func main() {
// system bus proxy is optional
if dbusConfigSystem != "nil" {
if c, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
fmsg.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
log.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
} else {
config.Confinement.SystemBus = c
}
@ -291,17 +300,18 @@ func main() {
// invoke app
runApp(config)
panic("unreachable")
// internal commands
case "shim":
shim.Main()
fmsg.Exit(0)
internal.Exit(0)
case "init":
init0.Main()
fmsg.Exit(0)
internal.Exit(0)
default:
fmsg.Fatalf("%q is not a valid command", args[0])
log.Fatalf("%q is not a valid command", args[0])
}
panic("unreachable")
@ -313,15 +323,15 @@ func runApp(config *fst.Config) {
syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable
if fmsg.Verbose() {
seccomp.CPrintln = fmsg.Println
if fmsg.Load() {
seccomp.CPrintln = log.Println
}
if a, err := app.New(sys); err != nil {
fmsg.Fatalf("cannot create app: %s\n", err)
log.Fatalf("cannot create app: %s", err)
} else if err = a.Seal(config); err != nil {
logBaseError(err, "cannot seal app:")
fmsg.Exit(1)
internal.Exit(1)
} else if err = a.Run(ctx, rs); err != nil {
if !rs.Start {
logBaseError(err, "cannot start app:")
@ -334,8 +344,7 @@ func runApp(config *fst.Config) {
}
}
if rs.WaitErr != nil {
fmsg.Println("inner wait failed:", rs.WaitErr)
log.Println("inner wait failed:", rs.WaitErr)
}
fmsg.Exit(rs.ExitCode)
panic("unreachable")
internal.Exit(rs.ExitCode)
}

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"io"
"log"
"os"
"strconv"
"strings"
@ -21,11 +22,10 @@ func tryPath(name string) (config *fst.Config) {
if name != "-" {
r = tryFd(name)
if r == nil {
fmsg.VPrintln("load configuration from file")
fmsg.Verbose("load configuration from file")
if f, err := os.Open(name); err != nil {
fmsg.Fatalf("cannot access configuration file %q: %s", name, err)
panic("unreachable")
log.Fatalf("cannot access configuration file %q: %s", name, err)
} else {
// finalizer closes f
r = f
@ -33,7 +33,7 @@ func tryPath(name string) (config *fst.Config) {
} else {
defer func() {
if err := r.(io.ReadCloser).Close(); err != nil {
fmsg.Printf("cannot close config fd: %v", err)
log.Printf("cannot close config fd: %v", err)
}
}()
}
@ -42,8 +42,7 @@ func tryPath(name string) (config *fst.Config) {
}
if err := json.NewDecoder(r).Decode(&config); err != nil {
fmsg.Fatalf("cannot load configuration: %v", err)
panic("unreachable")
log.Fatalf("cannot load configuration: %v", err)
}
return
@ -51,7 +50,7 @@ func tryPath(name string) (config *fst.Config) {
func tryFd(name string) io.ReadCloser {
if v, err := strconv.Atoi(name); err != nil {
fmsg.VPrintf("name cannot be interpreted as int64: %v", err)
fmsg.Verbosef("name cannot be interpreted as int64: %v", err)
return nil
} else {
fd := uintptr(v)
@ -59,7 +58,7 @@ func tryFd(name string) io.ReadCloser {
if errors.Is(errno, syscall.EBADF) {
return nil
}
fmsg.Fatalf("cannot get fd %d: %v", fd, errno)
log.Fatalf("cannot get fd %d: %v", fd, errno)
}
return os.NewFile(fd, strconv.Itoa(v))
}
@ -83,11 +82,11 @@ func tryShort(name string) (config *fst.Config, instance *state.State) {
// try to match from state store
if likePrefix && len(name) >= 8 {
fmsg.VPrintln("argument looks like prefix")
fmsg.Verbose("argument looks like prefix")
s := state.NewMulti(sys.Paths().RunDirPath)
if entries, err := state.Join(s); err != nil {
fmsg.Printf("cannot join store: %v", err)
log.Printf("cannot join store: %v", err)
// drop to fetch from file
} else {
for id := range entries {
@ -99,7 +98,7 @@ func tryShort(name string) (config *fst.Config, instance *state.State) {
break
}
fmsg.VPrintf("instance %s skipped", v)
fmsg.Verbosef("instance %s skipped", v)
}
}
}

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"slices"
"strconv"
"strings"
@ -12,7 +13,6 @@ import (
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/state"
)
@ -24,7 +24,7 @@ func printShowSystem(output io.Writer, short bool) {
// get fid by querying uid of aid 0
if uid, err := sys.Uid(0); err != nil {
fmsg.Fatalf("cannot obtain uid from fsu: %v", err)
log.Fatalf("cannot obtain uid from fsu: %v", err)
} else {
info.User = (uid / 10000) - 100
}
@ -190,12 +190,12 @@ func printShowInstance(
func printPs(output io.Writer, now time.Time, s state.Store, short bool) {
var entries state.Entries
if e, err := state.Join(s); err != nil {
fmsg.Fatalf("cannot join store: %v", err)
log.Fatalf("cannot join store: %v", err)
} else {
entries = e
}
if err := s.Close(); err != nil {
fmsg.Printf("cannot close store: %v", err)
log.Printf("cannot close store: %v", err)
}
if !short && flagJSON {
@ -212,13 +212,13 @@ func printPs(output io.Writer, now time.Time, s state.Store, short bool) {
for id, instance := range entries {
// gracefully skip nil states
if instance == nil {
fmsg.Printf("got invalid state entry %s", id.String())
log.Printf("got invalid state entry %s", id.String())
continue
}
// gracefully skip inconsistent states
if id != instance.ID {
fmsg.Printf("possible store corruption: entry %s has id %s",
log.Printf("possible store corruption: entry %s has id %s",
id.String(), instance.ID.String())
continue
}
@ -273,8 +273,7 @@ func printJSON(output io.Writer, short bool, v any) {
encoder.SetIndent("", " ")
}
if err := encoder.Encode(v); err != nil {
fmsg.Fatalf("cannot serialise: %v", err)
panic("unreachable")
log.Fatalf("cannot serialise: %v", err)
}
}
@ -284,31 +283,26 @@ type tp struct{ *tabwriter.Writer }
func (p *tp) Printf(format string, a ...any) {
if _, err := fmt.Fprintf(p, format, a...); err != nil {
fmsg.Fatalf("cannot write to tabwriter: %v", err)
panic("unreachable")
log.Fatalf("cannot write to tabwriter: %v", err)
}
}
func (p *tp) Println(a ...any) {
if _, err := fmt.Fprintln(p, a...); err != nil {
fmsg.Fatalf("cannot write to tabwriter: %v", err)
panic("unreachable")
log.Fatalf("cannot write to tabwriter: %v", err)
}
}
func (p *tp) MustFlush() {
if err := p.Writer.Flush(); err != nil {
fmsg.Fatalf("cannot flush tabwriter: %v", err)
panic("unreachable")
log.Fatalf("cannot flush tabwriter: %v", err)
}
}
func mustPrint(output io.Writer, a ...any) {
if _, err := fmt.Fprint(output, a...); err != nil {
fmsg.Fatalf("cannot print: %v", err)
panic("unreachable")
log.Fatalf("cannot print: %v", err)
}
}
func mustPrintln(output io.Writer, a ...any) {
if _, err := fmt.Fprintln(output, a...); err != nil {
fmsg.Fatalf("cannot print: %v", err)
panic("unreachable")
log.Fatalf("cannot print: %v", err)
}
}