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