aboutsummaryrefslogtreecommitdiffhomepage
path: root/main.go
blob: 197dd1e7f6abbadf3dba74b73c2fde764d815927 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
package main

import (
	"context"
	_ "embed"
	"flag"
	"fmt"
	"os"
	"os/signal"
	"os/user"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"text/tabwriter"

	"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/system"
)

var (
	flagVerbose bool
	flagJSON    bool

	//go:embed LICENSE
	license string
)

func init() {
	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() {
	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
	}

	if os.Geteuid() == 0 {
		fmsg.Fatal("this program must not run as root")
		panic("unreachable")
	}

	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.SetVerbose(flagVerbose)

	args := flag.Args()
	if len(args) == 0 {
		flag.CommandLine.Usage()
		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
		printJSON(fst.Template())
		fmsg.Exit(0)
	case "help": // print help message
		flag.CommandLine.Usage()
		fmsg.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(short)
		fmsg.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(short)
		case 1: // instance
			name := set.Args()[0]
			config, instance := tryShort(name)
			if config == nil {
				config = tryPath(name)
			}
			printShowInstance(instance, config, short)
		default:
			fmsg.Fatal("show requires 1 argument")
		}

		fmsg.Exit(0)
	case "app": // launch app from configuration file
		if len(args) < 2 {
			fmsg.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 {
			fmsg.Fatalf("aid %d out of range", aid)
			panic("unreachable")
		}

		// 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 {
					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
				}
			}
		)

		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 {
					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")
}

func runApp(config *fst.Config) {
	rs := new(app.RunState)
	ctx, cancel := context.WithCancel(context.Background())

	// handle signals for graceful shutdown
	sig := make(chan os.Signal, 2)
	signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		v := <-sig
		fmsg.Printf("got %s after program start", v)
		cancel()
		signal.Ignore(syscall.SIGINT, syscall.SIGTERM)
	}()

	if a, err := app.New(sys); err != nil {
		fmsg.Fatalf("cannot create app: %s\n", err)
	} else if err = a.Seal(config); err != nil {
		logBaseError(err, "cannot seal app:")
		fmsg.Exit(1)
	} else if err = a.Run(ctx, rs); err != nil {
		if !rs.Start {
			logBaseError(err, "cannot start app:")
		} else {
			logWaitError(err)
		}
	}
	if rs.WaitErr != nil {
		fmsg.Println("inner wait failed:", rs.WaitErr)
	}
	if rs.ExitCode < 0 {
		fmsg.VPrintf("got negative exit %v", rs.ExitCode)
		fmsg.Exit(1)
	}
	fmsg.Exit(rs.ExitCode)
	panic("unreachable")
}