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>
351 lines
8.9 KiB
Go
351 lines
8.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"os/user"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"text/tabwriter"
|
|
"time"
|
|
|
|
"git.gensokyo.uk/security/fortify/dbus"
|
|
"git.gensokyo.uk/security/fortify/fst"
|
|
"git.gensokyo.uk/security/fortify/helper/seccomp"
|
|
"git.gensokyo.uk/security/fortify/internal"
|
|
"git.gensokyo.uk/security/fortify/internal/app"
|
|
init0 "git.gensokyo.uk/security/fortify/internal/app/init"
|
|
"git.gensokyo.uk/security/fortify/internal/app/shim"
|
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
|
"git.gensokyo.uk/security/fortify/internal/linux"
|
|
"git.gensokyo.uk/security/fortify/internal/state"
|
|
"git.gensokyo.uk/security/fortify/internal/system"
|
|
)
|
|
|
|
var (
|
|
flagVerbose bool
|
|
flagJSON bool
|
|
|
|
//go:embed LICENSE
|
|
license string
|
|
)
|
|
|
|
func init() {
|
|
fmsg.Prepare("fortify")
|
|
|
|
flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
|
|
flag.BoolVar(&flagJSON, "json", false, "Format output in JSON when applicable")
|
|
}
|
|
|
|
var sys linux.System = new(linux.Std)
|
|
|
|
type gl []string
|
|
|
|
func (g *gl) String() string {
|
|
if g == nil {
|
|
return "<nil>"
|
|
}
|
|
return strings.Join(*g, " ")
|
|
}
|
|
|
|
func (g *gl) Set(v string) error {
|
|
*g = append(*g, v)
|
|
return nil
|
|
}
|
|
|
|
func main() {
|
|
// early init argv0 check, skips root check and duplicate PR_SET_DUMPABLE
|
|
init0.TryArgv0()
|
|
|
|
if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
|
|
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
|
|
// not fatal: this program runs as the privileged user
|
|
}
|
|
|
|
if os.Geteuid() == 0 {
|
|
log.Fatal("this program must not run as root")
|
|
}
|
|
|
|
flag.CommandLine.Usage = func() {
|
|
fmt.Println()
|
|
fmt.Println("Usage:\tfortify [-v] [--json] COMMAND [OPTIONS]")
|
|
fmt.Println()
|
|
fmt.Println("Commands:")
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 1, 4, ' ', 0)
|
|
commands := [][2]string{
|
|
{"app", "Launch app defined by the specified config file"},
|
|
{"run", "Configure and start a permissive default sandbox"},
|
|
{"show", "Show the contents of an app configuration"},
|
|
{"ps", "List active apps and their state"},
|
|
{"version", "Show fortify version"},
|
|
{"license", "Show full license text"},
|
|
{"template", "Produce a config template"},
|
|
{"help", "Show this help message"},
|
|
}
|
|
for _, c := range commands {
|
|
_, _ = fmt.Fprintf(w, "\t%s\t%s\n", c[0], c[1])
|
|
}
|
|
if err := w.Flush(); err != nil {
|
|
fmt.Printf("fortify: cannot write command list: %v\n", err)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
flag.Parse()
|
|
fmsg.Store(flagVerbose)
|
|
|
|
args := flag.Args()
|
|
if len(args) == 0 {
|
|
flag.CommandLine.Usage()
|
|
internal.Exit(0)
|
|
}
|
|
|
|
switch args[0] {
|
|
case "version": // print version string
|
|
if v, ok := internal.Check(internal.Version); ok {
|
|
fmt.Println(v)
|
|
} else {
|
|
fmt.Println("impure")
|
|
}
|
|
internal.Exit(0)
|
|
|
|
case "license": // print embedded license
|
|
fmt.Println(license)
|
|
internal.Exit(0)
|
|
|
|
case "template": // print full template configuration
|
|
printJSON(os.Stdout, false, fst.Template())
|
|
internal.Exit(0)
|
|
|
|
case "help": // print help message
|
|
flag.CommandLine.Usage()
|
|
internal.Exit(0)
|
|
|
|
case "ps": // print all state info
|
|
set := flag.NewFlagSet("ps", flag.ExitOnError)
|
|
var short bool
|
|
set.BoolVar(&short, "short", false, "Print instance id")
|
|
|
|
// Ignore errors; set is set for ExitOnError.
|
|
_ = set.Parse(args[1:])
|
|
|
|
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(sys.Paths().RunDirPath), short)
|
|
internal.Exit(0)
|
|
|
|
case "show": // pretty-print app info
|
|
set := flag.NewFlagSet("show", flag.ExitOnError)
|
|
var short bool
|
|
set.BoolVar(&short, "short", false, "Omit filesystem information")
|
|
|
|
// Ignore errors; set is set for ExitOnError.
|
|
_ = set.Parse(args[1:])
|
|
|
|
switch len(set.Args()) {
|
|
case 0: // system
|
|
printShowSystem(os.Stdout, short)
|
|
|
|
case 1: // instance
|
|
name := set.Args()[0]
|
|
config, instance := tryShort(name)
|
|
if config == nil {
|
|
config = tryPath(name)
|
|
}
|
|
printShowInstance(os.Stdout, time.Now().UTC(), instance, config, short)
|
|
|
|
default:
|
|
log.Fatal("show requires 1 argument")
|
|
}
|
|
internal.Exit(0)
|
|
|
|
case "app": // launch app from configuration file
|
|
if len(args) < 2 {
|
|
log.Fatal("app requires at least 1 argument")
|
|
}
|
|
|
|
// config extraArgs...
|
|
config := tryPath(args[1])
|
|
config.Command = append(config.Command, args[2:]...)
|
|
|
|
// invoke app
|
|
runApp(config)
|
|
panic("unreachable")
|
|
|
|
case "run": // run app in permissive defaults usage pattern
|
|
set := flag.NewFlagSet("run", flag.ExitOnError)
|
|
|
|
var (
|
|
dbusConfigSession string
|
|
dbusConfigSystem string
|
|
mpris bool
|
|
dbusVerbose bool
|
|
|
|
fid string
|
|
aid int
|
|
groups gl
|
|
homeDir string
|
|
userName string
|
|
enablements [system.ELen]bool
|
|
)
|
|
|
|
set.StringVar(&dbusConfigSession, "dbus-config", "builtin", "Path to D-Bus proxy config file, or \"builtin\" for defaults")
|
|
set.StringVar(&dbusConfigSystem, "dbus-system", "nil", "Path to system D-Bus proxy config file, or \"nil\" to disable")
|
|
set.BoolVar(&mpris, "mpris", false, "Allow owning MPRIS D-Bus path, has no effect if custom config is available")
|
|
set.BoolVar(&dbusVerbose, "dbus-log", false, "Force logging in the D-Bus proxy")
|
|
|
|
set.StringVar(&fid, "id", "", "App ID, leave empty to disable security context app_id")
|
|
set.IntVar(&aid, "a", 0, "Fortify application ID")
|
|
set.Var(&groups, "g", "Groups inherited by the app process")
|
|
set.StringVar(&homeDir, "d", "os", "Application home directory")
|
|
set.StringVar(&userName, "u", "chronos", "Passwd name within sandbox")
|
|
set.BoolVar(&enablements[system.EWayland], "wayland", false, "Allow Wayland connections")
|
|
set.BoolVar(&enablements[system.EX11], "X", false, "Share X11 socket and allow connection")
|
|
set.BoolVar(&enablements[system.EDBus], "dbus", false, "Proxy D-Bus connection")
|
|
set.BoolVar(&enablements[system.EPulse], "pulse", false, "Share PulseAudio socket and cookie")
|
|
|
|
// Ignore errors; set is set for ExitOnError.
|
|
_ = set.Parse(args[1:])
|
|
|
|
// initialise config from flags
|
|
config := &fst.Config{
|
|
ID: fid,
|
|
Command: set.Args(),
|
|
}
|
|
|
|
if aid < 0 || aid > 9999 {
|
|
log.Fatalf("aid %d out of range", aid)
|
|
}
|
|
|
|
// resolve home/username from os when flag is unset
|
|
var (
|
|
passwd *user.User
|
|
passwdOnce sync.Once
|
|
passwdFunc = func() {
|
|
var us string
|
|
if uid, err := sys.Uid(aid); err != nil {
|
|
log.Fatalf("cannot obtain uid from fsu: %v", err)
|
|
} else {
|
|
us = strconv.Itoa(uid)
|
|
}
|
|
|
|
if u, err := user.LookupId(us); err != nil {
|
|
fmsg.Verbosef("cannot look up uid %s", us)
|
|
passwd = &user.User{
|
|
Uid: us,
|
|
Gid: us,
|
|
Username: "chronos",
|
|
Name: "Fortify",
|
|
HomeDir: "/var/empty",
|
|
}
|
|
} else {
|
|
passwd = u
|
|
}
|
|
}
|
|
)
|
|
|
|
if homeDir == "os" {
|
|
passwdOnce.Do(passwdFunc)
|
|
homeDir = passwd.HomeDir
|
|
}
|
|
|
|
if userName == "chronos" {
|
|
passwdOnce.Do(passwdFunc)
|
|
userName = passwd.Username
|
|
}
|
|
|
|
config.Confinement.AppID = aid
|
|
config.Confinement.Groups = groups
|
|
config.Confinement.Outer = homeDir
|
|
config.Confinement.Username = userName
|
|
|
|
// enablements from flags
|
|
for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
|
|
if enablements[i] {
|
|
config.Confinement.Enablements.Set(i)
|
|
}
|
|
}
|
|
|
|
// parse D-Bus config file from flags if applicable
|
|
if enablements[system.EDBus] {
|
|
if dbusConfigSession == "builtin" {
|
|
config.Confinement.SessionBus = dbus.NewConfig(fid, true, mpris)
|
|
} else {
|
|
if c, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
|
|
log.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
|
|
} else {
|
|
config.Confinement.SessionBus = c
|
|
}
|
|
}
|
|
|
|
// system bus proxy is optional
|
|
if dbusConfigSystem != "nil" {
|
|
if c, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
|
|
log.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
|
|
} else {
|
|
config.Confinement.SystemBus = c
|
|
}
|
|
}
|
|
|
|
// override log from configuration
|
|
if dbusVerbose {
|
|
config.Confinement.SessionBus.Log = true
|
|
config.Confinement.SystemBus.Log = true
|
|
}
|
|
}
|
|
|
|
// invoke app
|
|
runApp(config)
|
|
panic("unreachable")
|
|
|
|
// internal commands
|
|
case "shim":
|
|
shim.Main()
|
|
internal.Exit(0)
|
|
case "init":
|
|
init0.Main()
|
|
internal.Exit(0)
|
|
|
|
default:
|
|
log.Fatalf("%q is not a valid command", args[0])
|
|
}
|
|
|
|
panic("unreachable")
|
|
}
|
|
|
|
func runApp(config *fst.Config) {
|
|
rs := new(app.RunState)
|
|
ctx, stop := signal.NotifyContext(context.Background(),
|
|
syscall.SIGINT, syscall.SIGTERM)
|
|
defer stop() // unreachable
|
|
|
|
if fmsg.Load() {
|
|
seccomp.CPrintln = log.Println
|
|
}
|
|
|
|
if a, err := app.New(sys); err != nil {
|
|
log.Fatalf("cannot create app: %s", err)
|
|
} else if err = a.Seal(config); err != nil {
|
|
logBaseError(err, "cannot seal app:")
|
|
internal.Exit(1)
|
|
} else if err = a.Run(ctx, rs); err != nil {
|
|
if !rs.Start {
|
|
logBaseError(err, "cannot start app:")
|
|
} else {
|
|
logWaitError(err)
|
|
}
|
|
|
|
if rs.ExitCode == 0 {
|
|
rs.ExitCode = 126
|
|
}
|
|
}
|
|
if rs.WaitErr != nil {
|
|
log.Println("inner wait failed:", rs.WaitErr)
|
|
}
|
|
internal.Exit(rs.ExitCode)
|
|
}
|