2024-07-09 15:39:40 +09:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2024-11-04 23:08:29 +09:00
|
|
|
_ "embed"
|
|
|
|
"encoding/json"
|
2024-07-09 15:39:40 +09:00
|
|
|
"flag"
|
2024-11-04 23:08:29 +09:00
|
|
|
"fmt"
|
2024-11-18 13:01:07 +09:00
|
|
|
"os/user"
|
|
|
|
"strconv"
|
2024-11-16 21:19:45 +09:00
|
|
|
"strings"
|
2024-11-19 21:03:09 +09:00
|
|
|
"sync"
|
2024-11-04 23:08:29 +09:00
|
|
|
"text/tabwriter"
|
2024-09-04 01:20:12 +09:00
|
|
|
|
2024-12-20 00:20:02 +09:00
|
|
|
"git.gensokyo.uk/security/fortify/dbus"
|
|
|
|
"git.gensokyo.uk/security/fortify/fst"
|
|
|
|
"git.gensokyo.uk/security/fortify/internal"
|
|
|
|
"git.gensokyo.uk/security/fortify/internal/app"
|
|
|
|
"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"
|
2024-07-09 15:39:40 +09:00
|
|
|
)
|
|
|
|
|
2024-09-04 01:20:12 +09:00
|
|
|
var (
|
2024-10-12 01:28:22 +09:00
|
|
|
flagVerbose bool
|
2024-12-21 00:32:34 +09:00
|
|
|
flagJSON bool
|
2024-11-04 23:08:29 +09:00
|
|
|
|
|
|
|
//go:embed LICENSE
|
|
|
|
license string
|
2024-09-04 01:20:12 +09:00
|
|
|
)
|
2024-07-09 15:39:40 +09:00
|
|
|
|
2024-10-12 01:28:22 +09:00
|
|
|
func init() {
|
|
|
|
flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
|
2024-12-21 00:32:34 +09:00
|
|
|
flag.BoolVar(&flagJSON, "json", false, "Format output in JSON when applicable")
|
2024-07-16 22:07:40 +09:00
|
|
|
}
|
|
|
|
|
2024-11-02 03:03:44 +09:00
|
|
|
var os = new(linux.Std)
|
2024-10-23 21:46:21 +09:00
|
|
|
|
2024-11-16 21:19:45 +09:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-07-09 15:39:40 +09:00
|
|
|
func main() {
|
2024-11-02 17:00:25 +09:00
|
|
|
if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
|
|
|
|
fmsg.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
|
|
|
|
// not fatal: this program runs as the privileged user
|
2024-10-13 00:09:14 +09:00
|
|
|
}
|
|
|
|
|
2024-11-05 12:57:03 +09:00
|
|
|
if os.Geteuid() == 0 {
|
|
|
|
fmsg.Fatal("this program must not run as root")
|
|
|
|
panic("unreachable")
|
|
|
|
}
|
|
|
|
|
2024-11-05 00:12:31 +09:00
|
|
|
flag.CommandLine.Usage = func() {
|
2024-11-04 23:08:29 +09:00
|
|
|
fmt.Println()
|
2024-12-21 00:32:34 +09:00
|
|
|
fmt.Println("Usage:\tfortify [-v] [--json] COMMAND [OPTIONS]")
|
2024-11-04 23:08:29 +09:00
|
|
|
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"},
|
2024-12-21 00:32:34 +09:00
|
|
|
{"show", "Show the contents of an app configuration"},
|
2024-11-04 23:08:29 +09:00
|
|
|
{"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 {
|
2024-11-05 12:57:03 +09:00
|
|
|
fmt.Printf("fortify: cannot write command list: %v\n", err)
|
2024-11-04 23:08:29 +09:00
|
|
|
}
|
|
|
|
fmt.Println()
|
|
|
|
}
|
2024-11-05 00:12:31 +09:00
|
|
|
flag.Parse()
|
|
|
|
fmsg.SetVerbose(flagVerbose)
|
|
|
|
|
2024-11-04 23:08:29 +09:00
|
|
|
args := flag.Args()
|
|
|
|
if len(args) == 0 {
|
2024-11-05 00:12:31 +09:00
|
|
|
flag.CommandLine.Usage()
|
2024-11-04 23:08:29 +09:00
|
|
|
fmsg.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")
|
|
|
|
}
|
|
|
|
fmsg.Exit(0)
|
|
|
|
case "license": // print embedded license
|
|
|
|
fmt.Println(license)
|
|
|
|
fmsg.Exit(0)
|
|
|
|
case "template": // print full template configuration
|
2024-12-21 18:51:59 +09:00
|
|
|
printJSON(fst.Template())
|
2024-11-04 23:08:29 +09:00
|
|
|
fmsg.Exit(0)
|
|
|
|
case "help": // print help message
|
2024-11-05 00:12:31 +09:00
|
|
|
flag.CommandLine.Usage()
|
2024-11-04 23:08:29 +09:00
|
|
|
fmsg.Exit(0)
|
|
|
|
case "ps": // print all state info
|
|
|
|
var w *tabwriter.Writer
|
|
|
|
state.MustPrintLauncherStateSimpleGlobal(&w, os.Paths().RunDirPath)
|
|
|
|
if w != nil {
|
|
|
|
if err := w.Flush(); err != nil {
|
|
|
|
fmsg.Println("cannot format output:", err)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
fmt.Println("No information available")
|
|
|
|
}
|
|
|
|
|
2024-12-21 00:32:34 +09:00
|
|
|
fmsg.Exit(0)
|
|
|
|
case "show": // pretty-print app info
|
|
|
|
if len(args) != 2 {
|
|
|
|
fmsg.Fatal("show requires 1 argument")
|
|
|
|
}
|
|
|
|
|
|
|
|
likePrefix := false
|
|
|
|
if len(args[1]) <= 32 {
|
|
|
|
likePrefix = true
|
|
|
|
for _, c := range args[1] {
|
|
|
|
if c >= '0' && c <= '9' {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if c >= 'a' && c <= 'f' {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
likePrefix = false
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
config *fst.Config
|
|
|
|
instance *state.State
|
|
|
|
)
|
|
|
|
|
|
|
|
// try to match from state store
|
|
|
|
if likePrefix && len(args[1]) >= 8 {
|
|
|
|
fmsg.VPrintln("argument looks like prefix")
|
|
|
|
|
|
|
|
s := state.NewMulti(os.Paths().RunDirPath)
|
|
|
|
if entries, err := state.Join(s); err != nil {
|
|
|
|
fmsg.Printf("cannot join store: %v", err)
|
|
|
|
// drop to fetch from file
|
|
|
|
} else {
|
|
|
|
for id := range entries {
|
|
|
|
v := id.String()
|
|
|
|
if strings.HasPrefix(v, args[1]) {
|
|
|
|
// match, use config from this state entry
|
|
|
|
instance = entries[id]
|
|
|
|
config = instance.Config
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
fmsg.VPrintf("instance %s skipped", v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if config == nil {
|
|
|
|
fmsg.VPrintf("reading from file")
|
|
|
|
|
|
|
|
config = new(fst.Config)
|
|
|
|
if f, err := os.Open(args[1]); err != nil {
|
|
|
|
fmsg.Fatalf("cannot access config file %q: %s", args[1], err)
|
|
|
|
panic("unreachable")
|
|
|
|
} else if err = json.NewDecoder(f).Decode(&config); err != nil {
|
|
|
|
fmsg.Fatalf("cannot parse config file %q: %s", args[1], err)
|
|
|
|
panic("unreachable")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-21 18:16:31 +09:00
|
|
|
printShow(instance, config)
|
2024-11-04 23:08:29 +09:00
|
|
|
fmsg.Exit(0)
|
|
|
|
case "app": // launch app from configuration file
|
|
|
|
if len(args) < 2 {
|
|
|
|
fmsg.Fatal("app requires at least 1 argument")
|
|
|
|
}
|
|
|
|
|
2024-12-18 15:50:46 +09:00
|
|
|
config := new(fst.Config)
|
2024-11-04 23:08:29 +09:00
|
|
|
if f, err := os.Open(args[1]); err != nil {
|
|
|
|
fmsg.Fatalf("cannot access config file %q: %s", args[1], err)
|
|
|
|
panic("unreachable")
|
|
|
|
} else if err = json.NewDecoder(f).Decode(&config); err != nil {
|
|
|
|
fmsg.Fatalf("cannot parse config file %q: %s", args[1], err)
|
|
|
|
panic("unreachable")
|
|
|
|
}
|
|
|
|
|
|
|
|
// append extra args
|
|
|
|
config.Command = append(config.Command, args[2:]...)
|
|
|
|
|
|
|
|
// invoke app
|
|
|
|
runApp(config)
|
|
|
|
case "run": // run app in permissive defaults usage pattern
|
|
|
|
set := flag.NewFlagSet("run", flag.ExitOnError)
|
|
|
|
|
|
|
|
var (
|
|
|
|
dbusConfigSession string
|
|
|
|
dbusConfigSystem string
|
|
|
|
mpris bool
|
|
|
|
dbusVerbose bool
|
|
|
|
|
2024-12-06 03:26:09 +09:00
|
|
|
fid string
|
2024-11-16 21:19:45 +09:00
|
|
|
aid int
|
|
|
|
groups gl
|
|
|
|
homeDir string
|
2024-11-04 23:08:29 +09:00
|
|
|
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")
|
|
|
|
|
2024-12-06 03:26:09 +09:00
|
|
|
set.StringVar(&fid, "id", "", "App ID, leave empty to disable security context app_id")
|
2024-11-16 21:19:45 +09:00
|
|
|
set.IntVar(&aid, "a", 0, "Fortify application ID")
|
|
|
|
set.Var(&groups, "g", "Groups inherited by the app process")
|
2024-11-18 13:01:07 +09:00
|
|
|
set.StringVar(&homeDir, "d", "os", "Application home directory")
|
2024-11-16 21:19:45 +09:00
|
|
|
set.StringVar(&userName, "u", "chronos", "Passwd name within sandbox")
|
2024-12-06 03:26:09 +09:00
|
|
|
set.BoolVar(&enablements[system.EWayland], "wayland", false, "Allow Wayland connections")
|
2024-11-04 23:08:29 +09:00
|
|
|
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
|
2024-12-18 15:50:46 +09:00
|
|
|
config := &fst.Config{
|
2024-12-06 03:26:09 +09:00
|
|
|
ID: fid,
|
2024-11-04 23:08:29 +09:00
|
|
|
Command: set.Args(),
|
|
|
|
}
|
|
|
|
|
2024-11-16 21:19:45 +09:00
|
|
|
if aid < 0 || aid > 9999 {
|
|
|
|
fmsg.Fatalf("aid %d out of range", aid)
|
|
|
|
panic("unreachable")
|
|
|
|
}
|
|
|
|
|
2024-11-19 21:03:09 +09:00
|
|
|
// resolve home/username from os when flag is unset
|
|
|
|
var (
|
|
|
|
passwd *user.User
|
|
|
|
passwdOnce sync.Once
|
|
|
|
passwdFunc = func() {
|
|
|
|
var us string
|
|
|
|
if uid, err := os.Uid(aid); err != nil {
|
|
|
|
fmsg.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)
|
|
|
|
passwd = &user.User{
|
|
|
|
Uid: us,
|
|
|
|
Gid: us,
|
|
|
|
Username: "chronos",
|
|
|
|
Name: "Fortify",
|
|
|
|
HomeDir: "/var/empty",
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
passwd = u
|
|
|
|
}
|
2024-11-18 13:01:07 +09:00
|
|
|
}
|
2024-11-19 21:03:09 +09:00
|
|
|
)
|
|
|
|
|
|
|
|
if homeDir == "os" {
|
|
|
|
passwdOnce.Do(passwdFunc)
|
|
|
|
homeDir = passwd.HomeDir
|
|
|
|
}
|
|
|
|
|
|
|
|
if userName == "chronos" {
|
|
|
|
passwdOnce.Do(passwdFunc)
|
|
|
|
userName = passwd.Username
|
2024-11-18 13:01:07 +09:00
|
|
|
}
|
|
|
|
|
2024-11-16 21:19:45 +09:00
|
|
|
config.Confinement.AppID = aid
|
|
|
|
config.Confinement.Groups = groups
|
2024-11-18 00:18:21 +09:00
|
|
|
config.Confinement.Outer = homeDir
|
2024-11-16 21:19:45 +09:00
|
|
|
config.Confinement.Username = userName
|
|
|
|
|
2024-11-04 23:08:29 +09:00
|
|
|
// 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" {
|
2024-12-06 03:26:09 +09:00
|
|
|
config.Confinement.SessionBus = dbus.NewConfig(fid, true, mpris)
|
2024-11-04 23:08:29 +09:00
|
|
|
} else {
|
|
|
|
if c, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
|
|
|
|
fmsg.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 {
|
|
|
|
fmsg.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)
|
|
|
|
default:
|
|
|
|
fmsg.Fatalf("%q is not a valid command", args[0])
|
|
|
|
}
|
|
|
|
|
|
|
|
panic("unreachable")
|
|
|
|
}
|
2024-09-04 01:20:12 +09:00
|
|
|
|
2024-12-18 15:50:46 +09:00
|
|
|
func runApp(config *fst.Config) {
|
2024-10-23 21:46:21 +09:00
|
|
|
a, err := app.New(os)
|
2024-10-20 00:07:48 +09:00
|
|
|
if err != nil {
|
2024-10-21 20:47:02 +09:00
|
|
|
fmsg.Fatalf("cannot create app: %s\n", err)
|
2024-11-04 23:08:29 +09:00
|
|
|
} else if err = a.Seal(config); err != nil {
|
2024-10-27 00:46:15 +09:00
|
|
|
logBaseError(err, "cannot seal app:")
|
2024-10-27 23:18:16 +09:00
|
|
|
fmsg.Exit(1)
|
2024-09-22 00:29:36 +09:00
|
|
|
} else if err = a.Start(); err != nil {
|
2024-10-27 00:46:15 +09:00
|
|
|
logBaseError(err, "cannot start app:")
|
|
|
|
}
|
|
|
|
|
|
|
|
var r int
|
|
|
|
// wait must be called regardless of result of start
|
|
|
|
if r, err = a.Wait(); err != nil {
|
2024-10-12 02:11:43 +09:00
|
|
|
if r < 1 {
|
|
|
|
r = 1
|
2024-09-09 03:16:54 +09:00
|
|
|
}
|
2024-10-12 02:11:43 +09:00
|
|
|
logWaitError(err)
|
2024-09-09 03:16:54 +09:00
|
|
|
}
|
2024-10-20 00:07:48 +09:00
|
|
|
if err = a.WaitErr(); err != nil {
|
2024-10-21 20:47:02 +09:00
|
|
|
fmsg.Println("inner wait failed:", err)
|
2024-09-22 00:29:36 +09:00
|
|
|
}
|
2024-10-27 00:46:15 +09:00
|
|
|
fmsg.Exit(r)
|
2024-11-04 23:08:29 +09:00
|
|
|
panic("unreachable")
|
2024-09-22 00:29:36 +09:00
|
|
|
}
|