Compare commits

...

20 Commits

Author SHA1 Message Date
Ophestra Umiker 2f34627d37
release: 0.0.8
release / release (push) Successful in 31s Details
test / test (push) Successful in 20s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-27 00:49:50 +09:00
Ophestra Umiker 1d6ea81205
shim: user switcher process management struct
test / test (push) Successful in 19s Details
This change moves all user switcher and shim management to the shim package and withholds output while shim is alive. This also eliminated all exit scenarios where revert is skipped.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-27 00:46:15 +09:00
Ophestra Umiker ae1a102882
fmsg: support temporarily withholding output
test / test (push) Successful in 31s Details
Trying to print to a shared stdout is a terrible idea. This change makes it possible to withhold output for the lifetime of the sandbox.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-26 23:09:32 +09:00
Ophestra Umiker 093e99d062
app: separate nixos test cases from tests
test / test (push) Successful in 20s Details
Test cases are very long, separating them improves editor performance.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 17:44:29 +09:00
Ophestra Umiker ad7e389eee
app: test app permissive defaults sealing behaviour
test / test (push) Successful in 20s Details
This test seals App against a deterministic os stub and checks the resulting sys and bwrap values against known correct ones. The effects of sys and bwrap on the OS and sandbox is deterministic and tested in their own respective packages.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 17:12:13 +09:00
Ophestra Umiker 5b249e4a66
system: print number of ops completed at point of failure
test / test (push) Successful in 21s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 17:08:21 +09:00
Ophestra Umiker 2a348c7f91
system: include more info in ACL Stringer
test / test (push) Successful in 24s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 16:23:22 +09:00
Ophestra Umiker eb767e7642
app/start: cleaner command not found message
test / test (push) Successful in 27s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 16:12:18 +09:00
Ophestra Umiker 3bfe8dbf5d
internal: ReadDir wrapper return fs.DirEntry
test / test (push) Successful in 25s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 14:56:29 +09:00
Ophestra Umiker 8fa791a2f8
app/seal: symlink /etc entries in permissive default
test / test (push) Successful in 20s Details
Fortify overrides /etc/passwd and /etc/group in the sandbox. Bind mounting /etc results in them being replaced when the passwd database is updated on host.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 13:31:57 +09:00
Ophestra Umiker b932ac8260
app/config: support creating symlinks within sandbox
test / test (push) Successful in 21s Details
This is already supported by the underlying bwrap helper. This change exposes access to it in Config.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 13:29:01 +09:00
Ophestra Umiker 050ffceb27
helper/bwrap: register generic PermConfig types with gob
test / test (push) Successful in 21s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 13:26:01 +09:00
Ophestra Umiker 31350d74e5
shim: kill shim if setup becomes impossible
test / test (push) Successful in 23s Details
This prevents a hang when setup faults but the shim keeps waiting on the socket. Setup is automatically aborted when the shim is killed.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 13:19:37 +09:00
Ophestra Umiker 3b82cc55de
internal: use fallback paths when XDG_RUNTIME_DIR is not absolute
test / test (push) Successful in 24s Details
There are scenarios where XDG_RUNTIME_DIR is set to an empty string or garbage. This check tries to ensure reasonable behaviour in most of those cases.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 12:14:57 +09:00
Ophestra Umiker 6bc5be7e5a
internal: wrap calls to os standard library functions
test / test (push) Successful in 19s Details
This change helps tests stub out and simulate OS behaviour during the sealing process. This also removes dependency on XDG_RUNTIME_DIR as the internal.System implementation provided to App provides a compat directory inside the tmpdir-based share when XDG_RUNTIME_DIR is unavailable.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-23 21:46:21 +09:00
Ophestra Umiker e35c5fe3ed
system: sys comparison method
test / test (push) Successful in 24s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-23 14:15:13 +09:00
Ophestra Umiker 20195ece47
system: return sys in queueing methods
test / test (push) Successful in 54s Details
This enables building an instance in a single statement.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-23 12:34:16 +09:00
Ophestra Umiker cafed5f234
shim: abort setup on failed start and process exit
test / test (push) Successful in 25s Details
Shim setup listens on a socket in the process share, if shim setup hasn't happened on exit revert will fail. This change makes sure shim setup is aborted on a doomed launch.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-21 21:23:56 +09:00
Ophestra Umiker 42e0b168e3
fmsg: produce all output through fmsg
test / test (push) Successful in 17s Details
The behaviour of print functions from package fmt is not thread safe. Functions provided by fmsg wrap around Logger methods. This makes prefix much cleaner and makes it easy to deal with future changes to logging.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-21 20:47:02 +09:00
Ophestra Umiker 380d1f4585
app: move wayland mediation to shim package
test / test (push) Successful in 29s Details
Values used in the Wayland mediation implementation is stored in various struct fields strewn across multiple app structs and checks are messy and confusing. This commit unifies them into a single struct and access it using much better looking methods.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-21 18:46:06 +09:00
48 changed files with 1435 additions and 614 deletions

View File

@ -25,7 +25,25 @@ const (
Other = C.ACL_OTHER
)
type Perm C.acl_perm_t
type (
Perm C.acl_perm_t
Perms []Perm
)
func (ps Perms) String() string {
var s = []byte("---")
for _, p := range ps {
switch p {
case Read:
s[0] = 'r'
case Write:
s[1] = 'w'
case Execute:
s[2] = 'x'
}
}
return string(s)
}
func UpdatePerm(path string, uid int, perms ...Perm) error {
// read acl from file

View File

@ -4,11 +4,11 @@ import (
"encoding/json"
"flag"
"fmt"
"os"
"git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/app"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/system"
)
@ -60,12 +60,12 @@ func init() {
func tryTemplate() {
if printTemplate {
if s, err := json.MarshalIndent(app.Template(), "", " "); err != nil {
fatalf("cannot generate template: %v", err)
fmsg.Fatalf("cannot generate template: %v", err)
panic("unreachable")
} else {
fmt.Println(string(s))
}
os.Exit(0)
fmsg.Exit(0)
}
}
@ -77,10 +77,10 @@ func loadConfig() *app.Config {
// config from file
c := new(app.Config)
if f, err := os.Open(confPath); err != nil {
fatalf("cannot access config file '%s': %s\n", confPath, err)
fmsg.Fatalf("cannot access config file %q: %s", confPath, err)
panic("unreachable")
} else if err = json.NewDecoder(f).Decode(&c); err != nil {
fatalf("cannot parse config file '%s': %s\n", confPath, err)
fmsg.Fatalf("cannot parse config file %q: %s", confPath, err)
panic("unreachable")
} else {
return c
@ -110,7 +110,7 @@ func configFromFlags() (config *app.Config) {
config.Confinement.SessionBus = dbus.NewConfig(dbusID, true, mpris)
} else {
if c, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
fatalf("cannot load session bus proxy config from %q: %s\n", dbusConfigSession, err)
fmsg.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
} else {
config.Confinement.SessionBus = c
}
@ -119,7 +119,7 @@ func configFromFlags() (config *app.Config) {
// system bus proxy is optional
if dbusConfigSystem != "nil" {
if c, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
fatalf("cannot load system bus proxy config from %q: %s\n", dbusConfigSystem, err)
fmsg.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
} else {
config.Confinement.SystemBus = c
}

View File

@ -2,8 +2,6 @@ package main
import (
"errors"
"fmt"
"os"
"git.ophivana.moe/security/fortify/internal/app"
"git.ophivana.moe/security/fortify/internal/fmsg"
@ -12,13 +10,13 @@ import (
func logWaitError(err error) {
var e *fmsg.BaseError
if !fmsg.AsBaseError(err, &e) {
fmt.Println("fortify: wait failed:", err)
fmsg.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
fmt.Print("fortify: " + e.Message())
fmsg.Print(e.Message())
} else {
// inner error are either unwrapped store errors
// or joined errors returned by *appSealTx revert
@ -26,7 +24,7 @@ func logWaitError(err error) {
var ej app.RevertCompoundError
if !errors.As(se.InnerErr, &ej) {
// does not require special handling
fmt.Print("fortify: " + e.Message())
fmsg.Print(e.Message())
} else {
errs := ej.Unwrap()
@ -35,10 +33,10 @@ func logWaitError(err error) {
var eb *fmsg.BaseError
if !errors.As(ei, &eb) {
// unreachable
fmt.Println("fortify: invalid error type returned by revert:", ei)
fmsg.Println("invalid error type returned by revert:", ei)
} else {
// print inner *app.BaseError message
fmt.Print("fortify: " + eb.Message())
fmsg.Print(eb.Message())
}
}
}
@ -50,13 +48,8 @@ func logBaseError(err error, message string) {
var e *fmsg.BaseError
if fmsg.AsBaseError(err, &e) {
fmt.Print("fortify: " + e.Message())
fmsg.Print(e.Message())
} else {
fmt.Println(message, err)
fmsg.Println(message, err)
}
}
func fatalf(format string, a ...any) {
fmt.Printf("fortify: "+format, a...)
os.Exit(1)
}

View File

@ -1,10 +1,16 @@
package bwrap
import (
"encoding/gob"
"os"
"strconv"
)
func init() {
gob.Register(new(PermConfig[SymlinkConfig]))
gob.Register(new(PermConfig[*TmpfsConfig]))
}
type Config struct {
// unshare every namespace we support by default if nil
// (--unshare-all)

View File

@ -11,6 +11,7 @@ import (
"testing"
"git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
// InternalChildStub is an internal function but exported because it is cross-package;
@ -33,7 +34,7 @@ func InternalChildStub() {
genericStub(argsFD, statFD)
}
os.Exit(0)
fmsg.Exit(0)
}
// InternalReplaceExecCommand is an internal function but exported because it is cross-package;

View File

@ -1,9 +1,10 @@
package app
import (
"net"
"os/exec"
"sync"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/shim"
)
type App interface {
@ -23,13 +24,12 @@ type App interface {
type app struct {
// application unique identifier
id *ID
// underlying user switcher process
cmd *exec.Cmd
// operating system interface
os internal.System
// shim process manager
shim *shim.Shim
// child process related information
seal *appSeal
// wayland connection if wayland mediation is enabled
wayland *net.UnixConn
// error returned waiting for process
waitErr error
@ -48,8 +48,8 @@ func (a *app) String() string {
a.lock.RLock()
defer a.lock.RUnlock()
if a.cmd != nil {
return a.cmd.String()
if a.shim != nil {
return a.shim.String()
}
if a.seal != nil {
@ -63,8 +63,9 @@ func (a *app) WaitErr() error {
return a.waitErr
}
func New() (App, error) {
func New(os internal.System) (App, error) {
a := new(app)
a.id = new(ID)
a.os = os
return a, newAppID(a.id)
}

View File

@ -0,0 +1,313 @@
package app_test
import (
"fmt"
"io/fs"
"os/user"
"strconv"
"git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/app"
"git.ophivana.moe/security/fortify/internal/system"
)
var testCasesNixos = []sealTestCase{
{
"nixos permissive defaults no enablements", new(stubNixOS),
&app.Config{
User: "chronos",
Command: make([]string, 0),
Method: "sudo",
},
app.ID{
0x4a, 0x45, 0x0b, 0x65,
0x96, 0xd7, 0xbc, 0x15,
0xbd, 0x01, 0x78, 0x0e,
0xb9, 0xa6, 0x07, 0xac,
},
system.New(150).
Ensure("/tmp/fortify.1971", 0701).
Ephemeral(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac", 0701).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/150", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/150", acl.Read, acl.Write, acl.Execute).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute).
WriteType(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/passwd", "chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n").
WriteType(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/group", "fortify:x:65534:\n"),
(&bwrap.Config{
Net: true,
UserNS: true,
Clearenv: true,
SetEnv: map[string]string{
"HOME": "/home/chronos",
"SHELL": "/run/current-system/sw/bin/zsh",
"TERM": "xterm-256color",
"USER": "chronos",
"XDG_RUNTIME_DIR": "/run/user/150",
"XDG_SESSION_CLASS": "user",
"XDG_SESSION_TYPE": "tty"},
Chmod: make(bwrap.ChmodConfig),
DieWithParent: true,
AsInit: true,
}).SetUID(65534).SetGID(65534).
Procfs("/proc").DevTmpfs("/dev").Mqueue("/dev/mqueue").
Tmpfs("/dev/fortify", 4096).
Bind("/bin", "/bin", false, true).
Bind("/boot", "/boot", false, true).
Bind("/etc", "/dev/fortify/etc").
Bind("/home", "/home", false, true).
Bind("/lib", "/lib", false, true).
Bind("/lib64", "/lib64", false, true).
Bind("/nix", "/nix", false, true).
Bind("/root", "/root", false, true).
Bind("/srv", "/srv", false, true).
Bind("/sys", "/sys", false, true).
Bind("/usr", "/usr", false, true).
Bind("/var", "/var", false, true).
Bind("/run/agetty.reload", "/run/agetty.reload", false, true).
Bind("/run/binfmt", "/run/binfmt", false, true).
Bind("/run/booted-system", "/run/booted-system", false, true).
Bind("/run/credentials", "/run/credentials", false, true).
Bind("/run/cryptsetup", "/run/cryptsetup", false, true).
Bind("/run/current-system", "/run/current-system", false, true).
Bind("/run/host", "/run/host", false, true).
Bind("/run/keys", "/run/keys", false, true).
Bind("/run/libvirt", "/run/libvirt", false, true).
Bind("/run/libvirtd.pid", "/run/libvirtd.pid", false, true).
Bind("/run/lock", "/run/lock", false, true).
Bind("/run/log", "/run/log", false, true).
Bind("/run/lvm", "/run/lvm", false, true).
Bind("/run/mount", "/run/mount", false, true).
Bind("/run/NetworkManager", "/run/NetworkManager", false, true).
Bind("/run/nginx", "/run/nginx", false, true).
Bind("/run/nixos", "/run/nixos", false, true).
Bind("/run/nscd", "/run/nscd", false, true).
Bind("/run/opengl-driver", "/run/opengl-driver", false, true).
Bind("/run/pppd", "/run/pppd", false, true).
Bind("/run/resolvconf", "/run/resolvconf", false, true).
Bind("/run/sddm", "/run/sddm", false, true).
Bind("/run/store", "/run/store", false, true).
Bind("/run/syncoid", "/run/syncoid", false, true).
Bind("/run/system", "/run/system", false, true).
Bind("/run/systemd", "/run/systemd", false, true).
Bind("/run/tmpfiles.d", "/run/tmpfiles.d", false, true).
Bind("/run/udev", "/run/udev", false, true).
Bind("/run/udisks2", "/run/udisks2", false, true).
Bind("/run/utmp", "/run/utmp", false, true).
Bind("/run/virtlogd.pid", "/run/virtlogd.pid", false, true).
Bind("/run/wrappers", "/run/wrappers", false, true).
Bind("/run/zed.pid", "/run/zed.pid", false, true).
Bind("/run/zed.state", "/run/zed.state", false, true).
Symlink("/dev/fortify/etc/alsa", "/etc/alsa").
Symlink("/dev/fortify/etc/bashrc", "/etc/bashrc").
Symlink("/dev/fortify/etc/binfmt.d", "/etc/binfmt.d").
Symlink("/dev/fortify/etc/dbus-1", "/etc/dbus-1").
Symlink("/dev/fortify/etc/default", "/etc/default").
Symlink("/dev/fortify/etc/ethertypes", "/etc/ethertypes").
Symlink("/dev/fortify/etc/fonts", "/etc/fonts").
Symlink("/dev/fortify/etc/fstab", "/etc/fstab").
Symlink("/dev/fortify/etc/fuse.conf", "/etc/fuse.conf").
Symlink("/dev/fortify/etc/host.conf", "/etc/host.conf").
Symlink("/dev/fortify/etc/hostid", "/etc/hostid").
Symlink("/dev/fortify/etc/hostname", "/etc/hostname").
Symlink("/dev/fortify/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink("/dev/fortify/etc/hosts", "/etc/hosts").
Symlink("/dev/fortify/etc/inputrc", "/etc/inputrc").
Symlink("/dev/fortify/etc/ipsec.d", "/etc/ipsec.d").
Symlink("/dev/fortify/etc/issue", "/etc/issue").
Symlink("/dev/fortify/etc/kbd", "/etc/kbd").
Symlink("/dev/fortify/etc/libblockdev", "/etc/libblockdev").
Symlink("/dev/fortify/etc/locale.conf", "/etc/locale.conf").
Symlink("/dev/fortify/etc/localtime", "/etc/localtime").
Symlink("/dev/fortify/etc/login.defs", "/etc/login.defs").
Symlink("/dev/fortify/etc/lsb-release", "/etc/lsb-release").
Symlink("/dev/fortify/etc/lvm", "/etc/lvm").
Symlink("/dev/fortify/etc/machine-id", "/etc/machine-id").
Symlink("/dev/fortify/etc/man_db.conf", "/etc/man_db.conf").
Symlink("/dev/fortify/etc/modprobe.d", "/etc/modprobe.d").
Symlink("/dev/fortify/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab").
Symlink("/dev/fortify/etc/nanorc", "/etc/nanorc").
Symlink("/dev/fortify/etc/netgroup", "/etc/netgroup").
Symlink("/dev/fortify/etc/NetworkManager", "/etc/NetworkManager").
Symlink("/dev/fortify/etc/nix", "/etc/nix").
Symlink("/dev/fortify/etc/nixos", "/etc/nixos").
Symlink("/dev/fortify/etc/NIXOS", "/etc/NIXOS").
Symlink("/dev/fortify/etc/nscd.conf", "/etc/nscd.conf").
Symlink("/dev/fortify/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink("/dev/fortify/etc/opensnitchd", "/etc/opensnitchd").
Symlink("/dev/fortify/etc/os-release", "/etc/os-release").
Symlink("/dev/fortify/etc/pam", "/etc/pam").
Symlink("/dev/fortify/etc/pam.d", "/etc/pam.d").
Symlink("/dev/fortify/etc/pipewire", "/etc/pipewire").
Symlink("/dev/fortify/etc/pki", "/etc/pki").
Symlink("/dev/fortify/etc/polkit-1", "/etc/polkit-1").
Symlink("/dev/fortify/etc/profile", "/etc/profile").
Symlink("/dev/fortify/etc/protocols", "/etc/protocols").
Symlink("/dev/fortify/etc/qemu", "/etc/qemu").
Symlink("/dev/fortify/etc/resolv.conf", "/etc/resolv.conf").
Symlink("/dev/fortify/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink("/dev/fortify/etc/rpc", "/etc/rpc").
Symlink("/dev/fortify/etc/samba", "/etc/samba").
Symlink("/dev/fortify/etc/sddm.conf", "/etc/sddm.conf").
Symlink("/dev/fortify/etc/secureboot", "/etc/secureboot").
Symlink("/dev/fortify/etc/services", "/etc/services").
Symlink("/dev/fortify/etc/set-environment", "/etc/set-environment").
Symlink("/dev/fortify/etc/shadow", "/etc/shadow").
Symlink("/dev/fortify/etc/shells", "/etc/shells").
Symlink("/dev/fortify/etc/ssh", "/etc/ssh").
Symlink("/dev/fortify/etc/ssl", "/etc/ssl").
Symlink("/dev/fortify/etc/static", "/etc/static").
Symlink("/dev/fortify/etc/subgid", "/etc/subgid").
Symlink("/dev/fortify/etc/subuid", "/etc/subuid").
Symlink("/dev/fortify/etc/sudoers", "/etc/sudoers").
Symlink("/dev/fortify/etc/sysctl.d", "/etc/sysctl.d").
Symlink("/dev/fortify/etc/systemd", "/etc/systemd").
Symlink("/dev/fortify/etc/terminfo", "/etc/terminfo").
Symlink("/dev/fortify/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink("/dev/fortify/etc/udev", "/etc/udev").
Symlink("/dev/fortify/etc/udisks2", "/etc/udisks2").
Symlink("/dev/fortify/etc/UPower", "/etc/UPower").
Symlink("/dev/fortify/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink("/dev/fortify/etc/X11", "/etc/X11").
Symlink("/dev/fortify/etc/zfs", "/etc/zfs").
Symlink("/dev/fortify/etc/zinputrc", "/etc/zinputrc").
Symlink("/dev/fortify/etc/zoneinfo", "/etc/zoneinfo").
Symlink("/dev/fortify/etc/zprofile", "/etc/zprofile").
Symlink("/dev/fortify/etc/zshenv", "/etc/zshenv").
Symlink("/dev/fortify/etc/zshrc", "/etc/zshrc").
Bind("/tmp/fortify.1971/tmpdir/150", "/tmp", false, true).
Tmpfs("/tmp/fortify.1971", 1048576).
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/150", 8388608).
Bind("/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/passwd", "/etc/passwd").
Bind("/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/group", "/etc/group").
Tmpfs("/var/run/nscd", 8192),
},
}
// fs methods are not implemented using a real FS
// to help better understand filesystem access behaviour
type stubNixOS struct {
lookPathErr map[string]error
usernameErr map[string]error
}
func (s *stubNixOS) Geteuid() int {
return 1971
}
func (s *stubNixOS) LookupEnv(key string) (string, bool) {
switch key {
case "SHELL":
return "/run/current-system/sw/bin/zsh", true
case "TERM":
return "xterm-256color", true
default:
panic(fmt.Sprintf("attempted to access unexpected environment variable %q", key))
}
}
func (s *stubNixOS) TempDir() string {
return "/tmp"
}
func (s *stubNixOS) LookPath(file string) (string, error) {
if s.lookPathErr != nil {
if err, ok := s.lookPathErr[file]; ok {
return "", err
}
}
switch file {
case "sudo":
return "/run/wrappers/bin/sudo", nil
default:
panic(fmt.Sprintf("attempted to look up unexpected executable %q", file))
}
}
func (s *stubNixOS) Executable() (string, error) {
return "/home/ophestra/.nix-profile/bin/fortify", nil
}
func (s *stubNixOS) Lookup(username string) (*user.User, error) {
if s.usernameErr != nil {
if err, ok := s.usernameErr[username]; ok {
return nil, err
}
}
switch username {
case "chronos":
return &user.User{
Uid: "150",
Gid: "101",
Username: "chronos",
HomeDir: "/home/chronos",
}, nil
default:
return nil, user.UnknownUserError(username)
}
}
func (s *stubNixOS) ReadDir(name string) ([]fs.DirEntry, error) {
switch name {
case "/":
return stubDirEntries("bin", "boot", "dev", "etc", "home", "lib",
"lib64", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var")
case "/run":
return stubDirEntries("agetty.reload", "binfmt", "booted-system",
"credentials", "cryptsetup", "current-system", "dbus", "host", "keys",
"libvirt", "libvirtd.pid", "lock", "log", "lvm", "mount", "NetworkManager",
"nginx", "nixos", "nscd", "opengl-driver", "pppd", "resolvconf", "sddm",
"store", "syncoid", "system", "systemd", "tmpfiles.d", "udev", "udisks2",
"user", "utmp", "virtlogd.pid", "wrappers", "zed.pid", "zed.state")
case "/etc":
return stubDirEntries("alsa", "bashrc", "binfmt.d", "dbus-1", "default",
"ethertypes", "fonts", "fstab", "fuse.conf", "group", "host.conf", "hostid",
"hostname", "hostname.CHECKSUM", "hosts", "inputrc", "ipsec.d", "issue", "kbd",
"libblockdev", "locale.conf", "localtime", "login.defs", "lsb-release", "lvm",
"machine-id", "man_db.conf", "modprobe.d", "modules-load.d", "mtab", "nanorc",
"netgroup", "NetworkManager", "nix", "nixos", "NIXOS", "nscd.conf", "nsswitch.conf",
"opensnitchd", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1",
"profile", "protocols", "qemu", "resolv.conf", "resolvconf.conf", "rpc", "samba",
"sddm.conf", "secureboot", "services", "set-environment", "shadow", "shells", "ssh",
"ssl", "static", "subgid", "subuid", "sudoers", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "udisks2", "UPower", "vconsole.conf", "X11", "zfs", "zinputrc",
"zoneinfo", "zprofile", "zshenv", "zshrc")
default:
panic(fmt.Sprintf("attempted to read unexpected directory %q", name))
}
}
func (s *stubNixOS) Stat(name string) (fs.FileInfo, error) {
switch name {
case "/var/run/nscd":
return nil, nil
default:
panic(fmt.Sprintf("attempted to stat unexpected path %q", name))
}
}
func (s *stubNixOS) Open(name string) (fs.File, error) {
switch name {
default:
panic(fmt.Sprintf("attempted to open unexpected file %q", name))
}
}
func (s *stubNixOS) Exit(code int) {
panic("called exit on stub with code " + strconv.Itoa(code))
}
func (s *stubNixOS) Paths() internal.Paths {
return internal.Paths{
SharePath: "/tmp/fortify.1971",
RuntimePath: "/run/user/1971",
RunDirPath: "/run/user/1971/fortify",
}
}

81
internal/app/app_test.go Normal file
View File

@ -0,0 +1,81 @@
package app_test
import (
"io/fs"
"reflect"
"testing"
"git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/app"
"git.ophivana.moe/security/fortify/internal/system"
)
type sealTestCase struct {
name string
os internal.System
config *app.Config
id app.ID
wantSys *system.I
wantBwrap *bwrap.Config
}
func TestApp(t *testing.T) {
testCases := append(testCasesNixos)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
a := app.NewWithID(tc.id, tc.os)
if !t.Run("seal", func(t *testing.T) {
if err := a.Seal(tc.config); err != nil {
t.Errorf("Seal: error = %v", err)
}
}) {
return
}
gotSys, gotBwrap := app.AppSystemBwrap(a)
t.Run("compare sys", func(t *testing.T) {
if !gotSys.Equal(tc.wantSys) {
t.Errorf("Seal: sys = %#v, want %#v",
gotSys, tc.wantSys)
}
})
t.Run("compare bwrap", func(t *testing.T) {
if !reflect.DeepEqual(gotBwrap, tc.wantBwrap) {
t.Errorf("seal: bwrap = %#v, want %#v",
gotBwrap, tc.wantBwrap)
}
})
})
}
}
func stubDirEntries(names ...string) (e []fs.DirEntry, err error) {
e = make([]fs.DirEntry, len(names))
for i, name := range names {
e[i] = stubDirEntryPath(name)
}
return
}
type stubDirEntryPath string
func (p stubDirEntryPath) Name() string {
return string(p)
}
func (p stubDirEntryPath) IsDir() bool {
panic("attempted to call IsDir")
}
func (p stubDirEntryPath) Type() fs.FileMode {
panic("attempted to call Type")
}
func (p stubDirEntryPath) Info() (fs.FileInfo, error) {
panic("attempted to call Info")
}

View File

@ -1,7 +1,6 @@
package app
import (
"encoding/gob"
"os"
"git.ophivana.moe/security/fortify/dbus"
@ -9,10 +8,6 @@ import (
"git.ophivana.moe/security/fortify/internal/system"
)
func init() {
gob.Register(new(bwrap.PermConfig[*bwrap.TmpfsConfig]))
}
// Config is used to seal an *App
type Config struct {
// D-Bus application ID
@ -61,6 +56,8 @@ type SandboxConfig struct {
Env map[string]string `json:"env"`
// sandbox host filesystem access
Filesystem []*FilesystemConfig `json:"filesystem"`
// symlinks created inside the sandbox
Link [][2]string `json:"symlink"`
// paths to override by mounting tmpfs over them
Override []string `json:"override"`
}
@ -99,7 +96,8 @@ func (s *SandboxConfig) Bwrap() *bwrap.Config {
Chmod: make(map[string]os.FileMode),
}).
SetUID(65534).SetGID(65534).
Procfs("/proc").DevTmpfs("/dev").Mqueue("/dev/mqueue")
Procfs("/proc").DevTmpfs("/dev").Mqueue("/dev/mqueue").
Tmpfs("/dev/fortify", 4*1024)
for _, c := range s.Filesystem {
if c == nil {
@ -113,6 +111,10 @@ func (s *SandboxConfig) Bwrap() *bwrap.Config {
conf.Bind(src, dest, !c.Must, c.Write, c.Device)
}
for _, l := range s.Link {
conf.Symlink(l[0], l[1])
}
return conf
}
@ -149,6 +151,7 @@ func Template() *Config {
{Src: "/data/user/0", Dst: "/data/data", Write: true, Must: true},
{Src: "/var/tmp", Write: true},
},
Link: [][2]string{{"/dev/fortify/etc", "/etc"}},
Override: []string{"/var/run/nscd"},
},
SystemBus: &dbus.Config{

View File

@ -0,0 +1,19 @@
package app
import (
"git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/system"
)
func NewWithID(id ID, os internal.System) App {
a := new(app)
a.id = &id
a.os = os
return a
}
func AppSystemBwrap(a App) (*system.I, *bwrap.Config) {
v := a.(*app)
return v.seal.sys.I, v.seal.sys.bwrap
}

View File

@ -1,10 +1,9 @@
package app
import (
"os/exec"
"strings"
"git.ophivana.moe/security/fortify/internal/verbose"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
func (a *app) commandBuilderMachineCtl(shimEnv string) (args []string) {
@ -14,7 +13,7 @@ func (a *app) commandBuilderMachineCtl(shimEnv string) (args []string) {
args = append(args, "shell", "--uid="+a.seal.sys.user.Username)
// --quiet
if !verbose.Get() {
if !fmsg.Verbose() {
args = append(args, "--quiet")
}
@ -31,7 +30,7 @@ func (a *app) commandBuilderMachineCtl(shimEnv string) (args []string) {
args = append(args, "--", ".host")
// /bin/sh -c
if sh, err := exec.LookPath("sh"); err != nil {
if sh, err := a.os.LookPath("sh"); err != nil {
// hardcode /bin/sh path since it exists more often than not
args = append(args, "/bin/sh", "-c")
} else {

View File

@ -1,9 +1,7 @@
package app
import (
"os"
"git.ophivana.moe/security/fortify/internal/verbose"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
const (
@ -17,8 +15,8 @@ func (a *app) commandBuilderSudo(shimEnv string) (args []string) {
args = append(args, "-Hiu", a.seal.sys.user.Username)
// -A?
if _, ok := os.LookupEnv(sudoAskPass); ok {
verbose.Printf("%s set, adding askpass flag\n", sudoAskPass)
if _, ok := a.os.LookupEnv(sudoAskPass); ok {
fmsg.VPrintln(sudoAskPass, "set, adding askpass flag")
args = append(args, "-A")
}

View File

@ -2,8 +2,7 @@ package app
import (
"errors"
"os"
"os/exec"
"io/fs"
"os/user"
"path"
"strconv"
@ -11,9 +10,9 @@ import (
"git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/shim"
"git.ophivana.moe/security/fortify/internal/state"
"git.ophivana.moe/security/fortify/internal/system"
"git.ophivana.moe/security/fortify/internal/verbose"
)
const (
@ -36,6 +35,42 @@ var (
ErrMachineCtl = errors.New("machinectl not available")
)
// appSeal seals the application with child-related information
type appSeal struct {
// app unique ID string representation
id string
// wayland mediation, disabled if nil
wl *shim.Wayland
// freedesktop application ID
fid string
// argv to start process with in the final confined environment
command []string
// persistent process state store
store state.Store
// uint8 representation of launch method sealed from config
launchOption uint8
// process-specific share directory path
share string
// process-specific share directory path local to XDG_RUNTIME_DIR
shareLocal string
// path to launcher program
toolPath string
// pass-through enablement tracking from config
et system.Enablements
// prevents sharing from happening twice
shared bool
// seal system-level component
sys *appSealSys
internal.Paths
// protected by upstream mutex
}
// Seal seals the app launch context
func (a *app) Seal(config *Config) error {
a.lock.Lock()
@ -54,7 +89,7 @@ func (a *app) Seal(config *Config) error {
seal := new(appSeal)
// fetch system constants
seal.SystemConstants = internal.GetSC()
seal.Paths = a.os.Paths()
// pass through config values
seal.id = a.id.String()
@ -65,7 +100,7 @@ func (a *app) Seal(config *Config) error {
switch config.Method {
case method[LaunchMethodSudo]:
seal.launchOption = LaunchMethodSudo
if sudoPath, err := exec.LookPath("sudo"); err != nil {
if sudoPath, err := a.os.LookPath("sudo"); err != nil {
return fmsg.WrapError(ErrSudo,
"sudo not found")
} else {
@ -78,7 +113,7 @@ func (a *app) Seal(config *Config) error {
"system has not been booted with systemd as init system")
}
if machineCtlPath, err := exec.LookPath("machinectl"); err != nil {
if machineCtlPath, err := a.os.LookPath("machinectl"); err != nil {
return fmsg.WrapError(ErrMachineCtl,
"machinectl not found")
} else {
@ -93,14 +128,14 @@ func (a *app) Seal(config *Config) error {
seal.sys = new(appSealSys)
// look up fortify executable path
if p, err := os.Executable(); err != nil {
if p, err := a.os.Executable(); err != nil {
return fmsg.WrapErrorSuffix(err, "cannot look up fortify executable path:")
} else {
seal.sys.executable = p
}
// look up user from system
if u, err := user.Lookup(config.User); err != nil {
if u, err := a.os.Lookup(config.User); err != nil {
if errors.As(err, new(user.UnknownUserError)) {
return fmsg.WrapError(ErrUser, "unknown user", config.User)
} else {
@ -114,7 +149,7 @@ func (a *app) Seal(config *Config) error {
// map sandbox config to bwrap
if config.Confinement.Sandbox == nil {
verbose.Println("sandbox configuration not supplied, PROCEED WITH CAUTION")
fmsg.VPrintln("sandbox configuration not supplied, PROCEED WITH CAUTION")
// permissive defaults
conf := &SandboxConfig{
@ -123,27 +158,29 @@ func (a *app) Seal(config *Config) error {
NoNewSession: true,
}
// bind entries in /
if d, err := os.ReadDir("/"); err != nil {
if d, err := a.os.ReadDir("/"); err != nil {
return err
} else {
b := make([]*FilesystemConfig, 0, len(d))
for _, ent := range d {
name := ent.Name()
switch name {
case "proc":
case "dev":
case "run":
case "tmp":
case "mnt":
p := "/" + ent.Name()
switch p {
case "/proc":
case "/dev":
case "/run":
case "/tmp":
case "/mnt":
case "/etc":
b = append(b, &FilesystemConfig{Src: p, Dst: "/dev/fortify/etc", Write: false, Must: true})
default:
p := "/" + name
b = append(b, &FilesystemConfig{Src: p, Write: true, Must: true})
}
}
conf.Filesystem = append(conf.Filesystem, b...)
}
// bind entries in /run
if d, err := os.ReadDir("/run"); err != nil {
if d, err := a.os.ReadDir("/run"); err != nil {
return err
} else {
b := make([]*FilesystemConfig, 0, len(d))
@ -161,13 +198,39 @@ func (a *app) Seal(config *Config) error {
}
// hide nscd from sandbox if present
nscd := "/var/run/nscd"
if _, err := os.Stat(nscd); !errors.Is(err, os.ErrNotExist) {
if _, err := a.os.Stat(nscd); !errors.Is(err, fs.ErrNotExist) {
conf.Override = append(conf.Override, nscd)
}
// bind GPU stuff
if config.Confinement.Enablements.Has(system.EX11) || config.Confinement.Enablements.Has(system.EWayland) {
conf.Filesystem = append(conf.Filesystem, &FilesystemConfig{Src: "/dev/dri", Device: true})
}
// link host /etc to prevent passwd/group from being overwritten
if d, err := a.os.ReadDir("/etc"); err != nil {
return err
} else {
b := make([][2]string, 0, len(d))
for _, ent := range d {
name := ent.Name()
switch name {
case "passwd":
case "group":
case "mtab":
b = append(b, [2]string{
"/proc/mounts",
"/etc/" + name,
})
default:
b = append(b, [2]string{
"/dev/fortify/etc/" + name,
"/etc/" + name,
})
}
}
conf.Link = append(conf.Link, b...)
}
config.Confinement.Sandbox = conf
}
seal.sys.bwrap = config.Confinement.Sandbox.Bwrap()
@ -176,16 +239,16 @@ func (a *app) Seal(config *Config) error {
seal.sys.bwrap.SetEnv = make(map[string]string)
}
// create wayland client wait channel if mediated wayland is enabled
// this channel being set enables mediated wayland setup later on
// create wayland struct and client wait channel if mediated wayland is enabled
// this field being set enables mediated wayland setup later on
if config.Confinement.Sandbox.Wayland {
seal.wlDone = make(chan struct{})
seal.wl = shim.NewWayland()
}
// open process state store
// the simple store only starts holding an open file after first action
// store activity begins after Start is called and must end before Wait
seal.store = state.NewSimple(seal.SystemConstants.RunDirPath, seal.sys.user.Uid)
seal.store = state.NewSimple(seal.RunDirPath, seal.sys.user.Uid)
// parse string UID
if u, err := strconv.Atoi(seal.sys.user.Uid); err != nil {
@ -199,12 +262,12 @@ func (a *app) Seal(config *Config) error {
seal.et = config.Confinement.Enablements
// this method calls all share methods in sequence
if err := seal.shareAll([2]*dbus.Config{config.Confinement.SessionBus, config.Confinement.SystemBus}); err != nil {
if err := seal.shareAll([2]*dbus.Config{config.Confinement.SessionBus, config.Confinement.SystemBus}, a.os); err != nil {
return err
}
// verbose log seal information
verbose.Println("created application seal as user",
fmsg.VPrintln("created application seal as user",
seal.sys.user.Username, "("+seal.sys.user.Uid+"),",
"method:", config.Method+",",
"launcher:", seal.toolPath+",",

View File

@ -2,10 +2,10 @@ package app
import (
"errors"
"os"
"path"
"git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/system"
)
@ -23,7 +23,7 @@ var (
ErrXDisplay = errors.New(display + " unset")
)
func (seal *appSeal) shareDisplay() error {
func (seal *appSeal) shareDisplay(os internal.System) error {
// pass $TERM to launcher
if t, ok := os.LookupEnv(term); ok {
seal.sys.bwrap.SetEnv[term] = t
@ -34,7 +34,7 @@ func (seal *appSeal) shareDisplay() error {
if wd, ok := os.LookupEnv(waylandDisplay); !ok {
return fmsg.WrapError(ErrWayland,
"WAYLAND_DISPLAY is not set")
} else if seal.wlDone == nil {
} else if seal.wl == nil {
// hardlink wayland socket
wp := path.Join(seal.RuntimePath, wd)
wpi := path.Join(seal.shareLocal, "wayland")
@ -46,8 +46,8 @@ func (seal *appSeal) shareDisplay() error {
// ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
seal.sys.UpdatePermType(system.EWayland, wp, acl.Read, acl.Write, acl.Execute)
} else {
// set wayland socket path (e.g. `/run/user/%d/wayland-%d`)
seal.wl = path.Join(seal.RuntimePath, wd)
// set wayland socket path for mediation (e.g. `/run/user/%d/wayland-%d`)
seal.wl.Path = path.Join(seal.RuntimePath, wd)
}
}

View File

@ -4,9 +4,9 @@ import (
"errors"
"fmt"
"io/fs"
"os"
"path"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/system"
)
@ -25,7 +25,7 @@ var (
ErrPulseMode = errors.New("unexpected pulse socket mode")
)
func (seal *appSeal) sharePulse() error {
func (seal *appSeal) sharePulse(os internal.System) error {
if !seal.et.Has(system.EPulse) {
return nil
}
@ -65,7 +65,7 @@ func (seal *appSeal) sharePulse() error {
seal.sys.bwrap.SetEnv[pulseServer] = "unix:" + p
// publish current user's pulse cookie for target user
if src, err := discoverPulseCookie(); err != nil {
if src, err := discoverPulseCookie(os); err != nil {
return err
} else {
dst := path.Join(seal.share, "pulse-cookie")
@ -78,7 +78,7 @@ func (seal *appSeal) sharePulse() error {
}
// discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie
func discoverPulseCookie() (string, error) {
func discoverPulseCookie(os internal.System) (string, error) {
if p, ok := os.LookupEnv(pulseCookie); ok {
return p, nil
}

View File

@ -29,6 +29,7 @@ func (seal *appSeal) shareRuntime() {
seal.sys.UpdatePermType(system.User, seal.RunDirPath, acl.Execute)
// ensure runtime directory ACL (e.g. `/run/user/%d`)
seal.sys.Ensure(seal.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
seal.sys.UpdatePermType(system.User, seal.RuntimePath, acl.Execute)
// ensure process-specific share local to XDG_RUNTIME_DIR (e.g. `/run/user/%d/fortify/%s`)

View File

@ -1,10 +1,10 @@
package app
import (
"os"
"path"
"git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/system"
)
@ -38,7 +38,7 @@ func (seal *appSeal) shareSystem() {
seal.sys.bwrap.Tmpfs(seal.SharePath, 1*1024*1024)
}
func (seal *appSeal) sharePasswd() {
func (seal *appSeal) sharePasswd(os internal.System) {
// look up shell
sh := "/bin/sh"
if s, ok := os.LookupEnv(shell); ok {

View File

@ -3,23 +3,20 @@ package app
import (
"errors"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
"time"
"git.ophivana.moe/security/fortify/helper"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/shim"
"git.ophivana.moe/security/fortify/internal/state"
"git.ophivana.moe/security/fortify/internal/system"
"git.ophivana.moe/security/fortify/internal/verbose"
)
// Start starts the fortified child
// Start selects a user switcher and starts shim.
// Note that Wait must be called regardless of error returned by Start.
func (a *app) Start() error {
a.lock.Lock()
defer a.lock.Unlock()
@ -37,18 +34,14 @@ func (a *app) Start() error {
if s, err := exec.LookPath(n); err == nil {
shimExec[i] = s
} else {
return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot find %q:", n))
return fmsg.WrapError(err,
fmt.Sprintf("executable file %q not found in $PATH", n))
}
}
}
if err := a.seal.sys.Commit(); err != nil {
return err
}
// select command builder
var commandBuilder func(shimEnv string) (args []string)
var commandBuilder shim.CommandBuilder
switch a.seal.launchOption {
case LaunchMethodSudo:
commandBuilder = a.commandBuilderSudo
@ -58,53 +51,45 @@ func (a *app) Start() error {
panic("unreachable")
}
// configure child process
confSockPath := path.Join(a.seal.share, "shim")
a.cmd = exec.Command(a.seal.toolPath, commandBuilder(shim.EnvShim+"="+confSockPath)...)
a.cmd.Env = []string{}
a.cmd.Stdin = os.Stdin
a.cmd.Stdout = os.Stdout
a.cmd.Stderr = os.Stderr
a.cmd.Dir = a.seal.RunDirPath
if wls, err := shim.ServeConfig(confSockPath, a.seal.sys.UID(), &shim.Payload{
// construct shim manager
a.shim = shim.New(a.seal.toolPath, uint32(a.seal.sys.UID()), path.Join(a.seal.share, "shim"), a.seal.wl,
&shim.Payload{
Argv: a.seal.command,
Exec: shimExec,
Bwrap: a.seal.sys.bwrap,
WL: a.seal.wlDone != nil,
WL: a.seal.wl != nil,
Verbose: verbose.Get(),
}, a.seal.wl, a.seal.wlDone); err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot listen on shim socket:")
Verbose: fmsg.Verbose(),
},
)
// startup will go ahead, commit system setup
if err := a.seal.sys.Commit(); err != nil {
return err
}
a.seal.sys.needRevert = true
if startTime, err := a.shim.Start(commandBuilder); err != nil {
return err
} else {
a.wayland = wls
}
// start shim
verbose.Println("starting shim as target user:", a.cmd)
if err := a.cmd.Start(); err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot start process:")
}
startTime := time.Now().UTC()
// create process state
// shim start and setup success, create process state
sd := state.State{
PID: a.cmd.Process.Pid,
PID: a.shim.Unwrap().Process.Pid,
Command: a.seal.command,
Capability: a.seal.et,
Method: method[a.seal.launchOption],
Argv: a.cmd.Args,
Time: startTime,
Argv: a.shim.Unwrap().Args,
Time: *startTime,
}
// register process state
var err = new(StateStoreError)
err.Inner, err.DoErr = a.seal.store.Do(func(b state.Backend) {
err.InnerErr = b.Save(&sd)
var err0 = new(StateStoreError)
err0.Inner, err0.DoErr = a.seal.store.Do(func(b state.Backend) {
err0.InnerErr = b.Save(&sd)
})
return err.equiv("cannot save process state:")
a.seal.sys.saveState = true
return err0.equiv("cannot save process state:")
}
}
// StateStoreError is returned for a failed state save
@ -168,8 +153,12 @@ func (a *app) Wait() (int, error) {
var r int
if cmd := a.shim.Unwrap(); cmd == nil {
// failure prior to process start
r = 255
} else {
// wait for process and resolve exit code
if err := a.cmd.Wait(); err != nil {
if err := cmd.Wait(); err != nil {
var exitError *exec.ExitError
if !errors.As(err, &exitError) {
// should be unreachable
@ -179,16 +168,18 @@ func (a *app) Wait() (int, error) {
// store non-zero return code
r = exitError.ExitCode()
} else {
r = a.cmd.ProcessState.ExitCode()
r = cmd.ProcessState.ExitCode()
}
fmsg.VPrintf("process %d exited with exit code %d", cmd.Process.Pid, r)
}
verbose.Println("process", strconv.Itoa(a.cmd.Process.Pid), "exited with exit code", r)
// child process exited, resume output
fmsg.Resume()
// close wayland connection
if a.wayland != nil {
close(a.seal.wlDone)
if err := a.wayland.Close(); err != nil {
fmt.Println("fortify: cannot close wayland connection:", err)
if a.seal.wl != nil {
if err := a.seal.wl.Close(); err != nil {
fmsg.Println("cannot close wayland connection:", err)
}
}
@ -197,9 +188,11 @@ func (a *app) Wait() (int, error) {
e.Inner, e.DoErr = a.seal.store.Do(func(b state.Backend) {
e.InnerErr = func() error {
// destroy defunct state entry
if err := b.Destroy(a.cmd.Process.Pid); err != nil {
if cmd := a.shim.Unwrap(); cmd != nil && a.seal.sys.saveState {
if err := b.Destroy(cmd.Process.Pid); err != nil {
return err
}
}
// enablements of remaining launchers
rt, ec := new(system.Enablements), new(system.Criteria)
@ -210,10 +203,10 @@ func (a *app) Wait() (int, error) {
} else {
if l := len(states); l == 0 {
// cleanup globals as the final launcher
verbose.Println("no other launchers active, will clean up globals")
fmsg.VPrintln("no other launchers active, will clean up globals")
ec.Set(system.User)
} else {
verbose.Printf("found %d active launchers, cleaning up without globals\n", l)
fmsg.VPrintf("found %d active launchers, cleaning up without globals", l)
}
// accumulate capabilities of other launchers
@ -227,7 +220,7 @@ func (a *app) Wait() (int, error) {
ec.Set(i)
}
}
if verbose.Get() {
if fmsg.Verbose() {
labels := make([]string, 0, system.ELen+1)
for i := system.Enablement(0); i < system.Enablement(system.ELen+2); i++ {
if ec.Has(i) {
@ -235,10 +228,11 @@ func (a *app) Wait() (int, error) {
}
}
if len(labels) > 0 {
verbose.Println("reverting operations labelled", strings.Join(labels, ", "))
fmsg.VPrintln("reverting operations labelled", strings.Join(labels, ", "))
}
}
a.shim.AbortWait(errors.New("shim exited"))
if err := a.seal.sys.Revert(ec); err != nil {
return err.(RevertCompoundError)
}

View File

@ -6,50 +6,9 @@ import (
"git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/state"
"git.ophivana.moe/security/fortify/internal/system"
)
// appSeal seals the application with child-related information
type appSeal struct {
// wayland socket path if mediated wayland is enabled
wl string
// wait for wayland client to exit if mediated wayland is enabled,
// (wlDone == nil) determines whether mediated wayland setup is performed
wlDone chan struct{}
// app unique ID string representation
id string
// freedesktop application ID
fid string
// argv to start process with in the final confined environment
command []string
// persistent process state store
store state.Store
// uint8 representation of launch method sealed from config
launchOption uint8
// process-specific share directory path
share string
// process-specific share directory path local to XDG_RUNTIME_DIR
shareLocal string
// path to launcher program
toolPath string
// pass-through enablement tracking from config
et system.Enablements
// prevents sharing from happening twice
shared bool
// seal system-level component
sys *appSealSys
// used in various sealing operations
internal.SystemConstants
// protected by upstream mutex
}
// appSealSys encapsulates app seal behaviour with OS interactions
type appSealSys struct {
bwrap *bwrap.Config
@ -63,13 +22,15 @@ type appSealSys struct {
// target user sealed from config
user *user.User
needRevert bool
saveState bool
*system.I
// protected by upstream mutex
}
// shareAll calls all share methods in sequence
func (seal *appSeal) shareAll(bus [2]*dbus.Config) error {
func (seal *appSeal) shareAll(bus [2]*dbus.Config, os internal.System) error {
if seal.shared {
panic("seal shared twice")
}
@ -77,11 +38,11 @@ func (seal *appSeal) shareAll(bus [2]*dbus.Config) error {
seal.shareSystem()
seal.shareRuntime()
seal.sharePasswd()
if err := seal.shareDisplay(); err != nil {
seal.sharePasswd(os)
if err := seal.shareDisplay(os); err != nil {
return err
}
if err := seal.sharePulse(); err != nil {
if err := seal.sharePulse(os); err != nil {
return err
}

View File

@ -2,9 +2,10 @@ package internal
import (
"errors"
"fmt"
"io/fs"
"os"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
const (
@ -13,7 +14,7 @@ const (
var SdBootedV = func() bool {
if v, err := SdBooted(); err != nil {
fmt.Println("warn: read systemd marker:", err)
fmsg.Println("cannot read systemd marker:", err)
return false
} else {
return v

View File

@ -1,59 +0,0 @@
package internal
import (
"fmt"
"os"
"path"
"strconv"
"sync"
"git.ophivana.moe/security/fortify/internal/verbose"
)
// state that remain constant for the lifetime of the process
// fetched and cached here
const (
xdgRuntimeDir = "XDG_RUNTIME_DIR"
)
// SystemConstants contains state from the operating system
type SystemConstants struct {
// path to shared directory e.g. /tmp/fortify.%d
SharePath string `json:"share_path"`
// XDG_RUNTIME_DIR value e.g. /run/user/%d
RuntimePath string `json:"runtime_path"`
// application runtime directory e.g. /run/user/%d/fortify
RunDirPath string `json:"run_dir_path"`
}
var (
scVal SystemConstants
scOnce sync.Once
)
func copySC() {
sc := SystemConstants{
SharePath: path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Geteuid())),
}
verbose.Println("process share directory at", sc.SharePath)
// runtimePath, runDirPath
if r, ok := os.LookupEnv(xdgRuntimeDir); !ok {
fmt.Println("Env variable", xdgRuntimeDir, "unset")
os.Exit(1)
} else {
sc.RuntimePath = r
sc.RunDirPath = path.Join(sc.RuntimePath, "fortify")
verbose.Println("XDG runtime directory at", sc.RunDirPath)
}
scVal = sc
}
// GetSC returns a populated SystemConstants value
func GetSC() SystemConstants {
scOnce.Do(copySC)
return scVal
}

70
internal/fmsg/defer.go Normal file
View File

@ -0,0 +1,70 @@
package fmsg
import (
"os"
"sync"
"sync/atomic"
)
var (
wstate atomic.Bool
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
}
}
}()
}
type dOp interface{ Do() }
func Exit(code int) {
queueSync.Wait()
os.Exit(code)
}
func Withhold() {
if wstate.CompareAndSwap(false, true) {
withhold <- struct{}{}
}
}
func Resume() {
if wstate.CompareAndSwap(true, false) {
withhold <- struct{}{}
}
}
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

@ -1,2 +1,43 @@
// Package fmsg provides various functions for output messages.
package fmsg
import (
"log"
"os"
)
var std = log.New(os.Stderr, "fortify: ", 0)
func SetPrefix(prefix string) {
prefix += ": "
std.SetPrefix(prefix)
std.SetPrefix(prefix)
}
func Print(v ...any) {
dequeueOnce.Do(dequeue)
queueSync.Add(1)
msgbuf <- dPrint(v)
}
func Printf(format string, v ...any) {
dequeueOnce.Do(dequeue)
queueSync.Add(1)
msgbuf <- &dPrintf{format, v}
}
func Println(v ...any) {
dequeueOnce.Do(dequeue)
queueSync.Add(1)
msgbuf <- dPrintln(v)
}
func Fatal(v ...any) {
Print(v...)
Exit(1)
}
func Fatalf(format string, v ...any) {
Printf(format, v...)
Exit(1)
}

25
internal/fmsg/verbose.go Normal file
View File

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

View File

@ -4,7 +4,6 @@ import (
"encoding/gob"
"errors"
"flag"
"fmt"
"os"
"os/exec"
"os/signal"
@ -13,7 +12,7 @@ import (
"syscall"
"time"
"git.ophivana.moe/security/fortify/internal/verbose"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
const (
@ -25,49 +24,46 @@ const (
// proceed with caution!
func doInit(fd uintptr) {
fmsg.SetPrefix("init")
// re-exec
if len(os.Args) > 0 && os.Args[0] != "fortify" && path.IsAbs(os.Args[0]) {
if err := syscall.Exec(os.Args[0], []string{"fortify", "init"}, os.Environ()); err != nil {
fmt.Println("fortify-init: cannot re-exec self:", err)
fmsg.Println("cannot re-exec self:", err)
// continue anyway
}
}
verbose.Prefix = "fortify-init:"
var payload Payload
p := os.NewFile(fd, "config-stream")
if p == nil {
fmt.Println("fortify-init: invalid config descriptor")
os.Exit(1)
fmsg.Fatal("invalid config descriptor")
}
if err := gob.NewDecoder(p).Decode(&payload); err != nil {
fmt.Println("fortify-init: cannot decode init payload:", err)
os.Exit(1)
fmsg.Fatal("cannot decode init payload:", err)
} else {
// sharing stdout with parent
// USE WITH CAUTION
verbose.Set(payload.Verbose)
fmsg.SetVerbose(payload.Verbose)
// child does not need to see this
if err = os.Unsetenv(EnvInit); err != nil {
fmt.Println("fortify-init: cannot unset", EnvInit+":", err)
fmsg.Println("cannot unset", EnvInit+":", err)
// not fatal
} else {
verbose.Println("received configuration")
fmsg.VPrintln("received configuration")
}
}
// close config fd
if err := p.Close(); err != nil {
fmt.Println("fortify-init: cannot close config fd:", err)
fmsg.Println("cannot close config fd:", err)
// not fatal
}
// die with parent
if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGKILL), 0); errno != 0 {
fmt.Println("fortify-init: prctl(PR_SET_PDEATHSIG, SIGKILL):", errno.Error())
os.Exit(1)
fmsg.Fatal("prctl(PR_SET_PDEATHSIG, SIGKILL):", errno.Error())
}
cmd := exec.Command(payload.Argv0)
@ -84,8 +80,7 @@ func doInit(fd uintptr) {
}
if err := cmd.Start(); err != nil {
fmt.Printf("fortify-init: cannot start %q: %v", payload.Argv0, err)
os.Exit(1)
fmsg.Fatalf("cannot start %q: %v", payload.Argv0, err)
}
sig := make(chan os.Signal, 2)
@ -121,7 +116,7 @@ func doInit(fd uintptr) {
}
}
if !errors.Is(err, syscall.ECHILD) {
fmt.Println("fortify-init: unexpected wait4 response:", err)
fmsg.Println("unexpected wait4 response:", err)
}
close(done)
@ -133,8 +128,8 @@ func doInit(fd uintptr) {
for {
select {
case s := <-sig:
verbose.Println("received", s.String())
os.Exit(0)
fmsg.VPrintln("received", s.String())
fmsg.Exit(0)
case w := <-info:
if w.wpid == cmd.Process.Pid {
switch {
@ -152,10 +147,10 @@ func doInit(fd uintptr) {
}()
}
case <-done:
os.Exit(r)
fmsg.Exit(r)
case <-timeout:
fmt.Println("fortify-init: timeout exceeded waiting for lingering processes")
os.Exit(r)
fmsg.Println("timeout exceeded waiting for lingering processes")
fmsg.Exit(r)
}
}
}
@ -169,8 +164,7 @@ func Try() {
if args := flag.Args(); len(args) == 1 && args[0] == "init" {
if s, ok := os.LookupEnv(EnvInit); ok {
if fd, err := strconv.Atoi(s); err != nil {
fmt.Printf("fortify-init: cannot parse %q: %v", s, err)
os.Exit(1)
fmsg.Fatalf("cannot parse %q: %v", s, err)
} else {
doInit(uintptr(fd))
}

View File

@ -4,7 +4,6 @@ import (
"encoding/gob"
"errors"
"flag"
"fmt"
"net"
"os"
"path"
@ -12,29 +11,29 @@ import (
"syscall"
"git.ophivana.moe/security/fortify/helper"
"git.ophivana.moe/security/fortify/internal/fmsg"
init0 "git.ophivana.moe/security/fortify/internal/init"
"git.ophivana.moe/security/fortify/internal/verbose"
)
// everything beyond this point runs as target user
// proceed with caution!
func doShim(socket string) {
fmsg.SetPrefix("shim")
// re-exec
if len(os.Args) > 0 && os.Args[0] != "fortify" && path.IsAbs(os.Args[0]) {
if err := syscall.Exec(os.Args[0], []string{"fortify", "shim"}, os.Environ()); err != nil {
fmt.Println("fortify-shim: cannot re-exec self:", err)
fmsg.Println("cannot re-exec self:", err)
// continue anyway
}
}
verbose.Prefix = "fortify-shim:"
// dial setup socket
var conn *net.UnixConn
if c, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: socket, Net: "unix"}); err != nil {
fmt.Println("fortify-shim: cannot dial setup socket:", err)
os.Exit(1)
fmsg.Fatal("cannot dial setup socket:", err)
panic("unreachable")
} else {
conn = c
}
@ -42,25 +41,22 @@ func doShim(socket string) {
// decode payload gob stream
var payload Payload
if err := gob.NewDecoder(conn).Decode(&payload); err != nil {
fmt.Println("fortify-shim: cannot decode shim payload:", err)
os.Exit(1)
fmsg.Fatal("cannot decode shim payload:", err)
} else {
// sharing stdout with parent
// USE WITH CAUTION
verbose.Set(payload.Verbose)
fmsg.SetVerbose(payload.Verbose)
}
if payload.Bwrap == nil {
fmt.Println("fortify-shim: bwrap config not supplied")
os.Exit(1)
fmsg.Fatal("bwrap config not supplied")
}
// receive wayland fd over socket
wfd := -1
if payload.WL {
if fd, err := receiveWLfd(conn); err != nil {
fmt.Println("fortify-shim: cannot receive wayland fd:", err)
os.Exit(1)
fmsg.Fatal("cannot receive wayland fd:", err)
} else {
wfd = fd
}
@ -68,7 +64,7 @@ func doShim(socket string) {
// close setup socket
if err := conn.Close(); err != nil {
fmt.Println("fortify-shim: cannot close setup socket:", err)
fmsg.Println("cannot close setup socket:", err)
// not fatal
}
@ -83,8 +79,7 @@ func doShim(socket string) {
// no argv, look up shell instead
var ok bool
if ic.Argv0, ok = os.LookupEnv("SHELL"); !ok {
fmt.Println("fortify-shim: no command was specified and $SHELL was unset")
os.Exit(1)
fmsg.Fatal("no command was specified and $SHELL was unset")
}
ic.Argv = []string{ic.Argv0}
@ -106,46 +101,42 @@ func doShim(socket string) {
// share config pipe
if r, w, err := os.Pipe(); err != nil {
fmt.Println("fortify-shim: cannot pipe:", err)
os.Exit(1)
fmsg.Fatal("cannot pipe:", err)
} else {
conf.SetEnv[init0.EnvInit] = strconv.Itoa(3 + len(extraFiles))
extraFiles = append(extraFiles, r)
verbose.Println("transmitting config to init")
fmsg.VPrintln("transmitting config to init")
go func() {
// stream config to pipe
if err = gob.NewEncoder(w).Encode(&ic); err != nil {
fmt.Println("fortify-shim: cannot transmit init config:", err)
os.Exit(1)
fmsg.Fatal("cannot transmit init config:", err)
}
}()
}
helper.BubblewrapName = payload.Exec[1] // resolved bwrap path by parent
if b, err := helper.NewBwrap(conf, nil, payload.Exec[0], func(int, int) []string { return []string{"init"} }); err != nil {
fmt.Println("fortify-shim: malformed sandbox config:", err)
os.Exit(1)
fmsg.Fatal("malformed sandbox config:", err)
} else {
cmd := b.Unwrap()
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.ExtraFiles = extraFiles
if verbose.Get() {
verbose.Println("bwrap args:", conf.Args())
if fmsg.Verbose() {
fmsg.VPrintln("bwrap args:", conf.Args())
}
// run and pass through exit code
if err = b.Start(); err != nil {
fmt.Println("fortify-shim: cannot start target process:", err)
os.Exit(1)
fmsg.Fatal("cannot start target process:", err)
} else if err = b.Wait(); err != nil {
verbose.Println("wait:", err)
fmsg.VPrintln("wait:", err)
}
if b.Unwrap().ProcessState != nil {
os.Exit(b.Unwrap().ProcessState.ExitCode())
fmsg.Exit(b.Unwrap().ProcessState.ExitCode())
} else {
os.Exit(127)
fmsg.Exit(127)
}
}
}

View File

@ -1,84 +1,202 @@
package shim
import (
"encoding/gob"
"errors"
"fmt"
"net"
"os"
"os/exec"
"sync"
"sync/atomic"
"syscall"
"time"
"git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/internal/verbose"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
// called in the parent process
// used by the parent process
func ServeConfig(socket string, uid int, payload *Payload, wl string, done chan struct{}) (*net.UnixConn, error) {
var ws *net.UnixConn
if payload.WL {
if f, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: wl, Net: "unix"}); err != nil {
return nil, err
} else {
verbose.Println("connected to wayland at", wl)
ws = f
}
type Shim struct {
// user switcher process
cmd *exec.Cmd
// uid of shim target user
uid uint32
// whether to check shim pid
checkPid bool
// user switcher executable path
executable string
// path to setup socket
socket string
// shim setup abort reason and completion
abort chan error
abortErr atomic.Pointer[error]
abortOnce sync.Once
// wayland mediation, nil if disabled
wl *Wayland
// shim setup payload
payload *Payload
}
if c, err := net.ListenUnix("unix", &net.UnixAddr{Name: socket, Net: "unix"}); err != nil {
return nil, err
func New(executable string, uid uint32, socket string, wl *Wayland, payload *Payload) *Shim {
// checkPid is impossible at the moment since there is no way to obtain shim's pid
// this feature is disabled here until sudo is replaced by fortify suid wrapper
return &Shim{uid: uid, executable: executable, socket: socket, wl: wl, payload: payload}
}
func (s *Shim) String() string {
if s.cmd == nil {
return "(unused shim manager)"
}
return s.cmd.String()
}
func (s *Shim) Unwrap() *exec.Cmd {
return s.cmd
}
func (s *Shim) Abort(err error) {
s.abortOnce.Do(func() {
s.abortErr.Store(&err)
// s.abort is buffered so this will never block
s.abort <- err
})
}
func (s *Shim) AbortWait(err error) {
s.Abort(err)
<-s.abort
}
type CommandBuilder func(shimEnv string) (args []string)
func (s *Shim) Start(f CommandBuilder) (*time.Time, error) {
var (
cf chan *net.UnixConn
accept func()
)
// listen on setup socket
if c, a, err := s.serve(); err != nil {
return nil, fmsg.WrapErrorSuffix(err,
"cannot listen on shim setup socket:")
} else {
verbose.Println("configuring shim on socket", socket)
if err = acl.UpdatePerm(socket, uid, acl.Read, acl.Write, acl.Execute); err != nil {
fmt.Println("fortify: cannot change permissions of shim setup socket:", err)
// accepts a connection after each call to accept
// connections are sent to the channel cf
cf, accept = c, a
}
// start user switcher process and save time
s.cmd = exec.Command(s.executable, f(EnvShim+"="+s.socket)...)
s.cmd.Env = []string{}
s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
s.cmd.Dir = "/"
fmsg.VPrintln("starting shim via user switcher:", s.cmd)
fmsg.Withhold() // withhold messages to stderr
if err := s.cmd.Start(); err != nil {
return nil, fmsg.WrapErrorSuffix(err,
"cannot start user switcher:")
}
startTime := time.Now().UTC()
// kill shim if something goes wrong and an error is returned
killShim := func() {
if err := s.cmd.Process.Signal(os.Interrupt); err != nil {
fmsg.Println("cannot terminate shim on faulted setup:", err)
}
}
defer func() { killShim() }()
accept()
conn := <-cf
if conn == nil {
return &startTime, fmsg.WrapErrorSuffix(*s.abortErr.Load(), "cannot accept call on setup socket:")
}
// authenticate against called provided uid and shim pid
if cred, err := peerCred(conn); err != nil {
return &startTime, fmsg.WrapErrorSuffix(*s.abortErr.Load(), "cannot retrieve shim credentials:")
} else if cred.Uid != s.uid {
fmsg.Printf("process %d owned by user %d tried to connect, expecting %d",
cred.Pid, cred.Uid, s.uid)
err = errors.New("compromised fortify build")
s.Abort(err)
return &startTime, err
} else if s.checkPid && cred.Pid != int32(s.cmd.Process.Pid) {
fmsg.Printf("process %d tried to connect to shim setup socket, expecting shim %d",
cred.Pid, s.cmd.Process.Pid)
err = errors.New("compromised target user")
s.Abort(err)
return &startTime, err
}
// serve payload and wayland fd if enabled
// this also closes the connection
err := s.payload.serve(conn, s.wl)
if err == nil {
killShim = func() {}
}
s.Abort(err) // aborting with nil indicates success
return &startTime, err
}
func (s *Shim) serve() (chan *net.UnixConn, func(), error) {
if s.abort != nil {
panic("attempted to serve shim setup twice")
}
s.abort = make(chan error, 1)
cf := make(chan *net.UnixConn)
accept := make(chan struct{}, 1)
if l, err := net.ListenUnix("unix", &net.UnixAddr{Name: s.socket, Net: "unix"}); err != nil {
return nil, nil, err
} else {
l.SetUnlinkOnClose(true)
fmsg.VPrintf("listening on shim setup socket %q", s.socket)
if err = acl.UpdatePerm(s.socket, int(s.uid), acl.Read, acl.Write, acl.Execute); err != nil {
fmsg.Println("cannot append ACL entry to shim setup socket:", err)
s.Abort(err) // ensures setup socket cleanup
}
go func() {
var conn *net.UnixConn
if conn, err = c.AcceptUnix(); err != nil {
fmt.Println("fortify: cannot accept connection from shim:", err)
} else {
if err = gob.NewEncoder(conn).Encode(*payload); err != nil {
fmt.Println("fortify: cannot stream shim payload:", err)
_ = os.Remove(socket)
return
for {
select {
case err = <-s.abort:
if err != nil {
fmsg.VPrintln("aborting shim setup, reason:", err)
}
if err = l.Close(); err != nil {
fmsg.Println("cannot close setup socket:", err)
}
close(s.abort)
close(cf)
return
case <-accept:
if conn, err0 := l.AcceptUnix(); err0 != nil {
s.Abort(err0) // does not block, breaks loop
cf <- nil // receiver sees nil value and loads err0 stored during abort
} else {
cf <- conn
}
if payload.WL {
// get raw connection
var rc syscall.RawConn
if rc, err = ws.SyscallConn(); err != nil {
fmt.Println("fortify: cannot obtain raw wayland connection:", err)
return
} else {
go func() {
// pass wayland socket fd
if err = rc.Control(func(fd uintptr) {
if _, _, err = conn.WriteMsgUnix(nil, syscall.UnixRights(int(fd)), nil); err != nil {
fmt.Println("fortify: cannot pass wayland connection to shim:", err)
return
}
_ = conn.Close()
// block until shim exits
<-done
verbose.Println("releasing wayland connection")
}); err != nil {
fmt.Println("fortify: cannot obtain wayland connection fd:", err)
}
}()
}
} else {
_ = conn.Close()
return cf, func() { accept <- struct{}{} }, nil
}
// peerCred fetches peer credentials of conn
func peerCred(conn *net.UnixConn) (ucred *syscall.Ucred, err error) {
var raw syscall.RawConn
if raw, err = conn.SyscallConn(); err != nil {
return
}
if err = c.Close(); err != nil {
fmt.Println("fortify: cannot close shim socket:", err)
}
if err = os.Remove(socket); err != nil && !errors.Is(err, os.ErrNotExist) {
fmt.Println("fortify: cannot remove dangling shim socket:", err)
}
}()
return ws, nil
}
err0 := raw.Control(func(fd uintptr) {
ucred, err = syscall.GetsockoptUcred(int(fd), syscall.SOL_SOCKET, syscall.SO_PEERCRED)
})
err = errors.Join(err, err0)
return
}

View File

@ -1,6 +1,13 @@
package shim
import "git.ophivana.moe/security/fortify/helper/bwrap"
import (
"encoding/gob"
"errors"
"net"
"git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
const EnvShim = "FORTIFY_SHIM"
@ -17,3 +24,19 @@ type Payload struct {
// verbosity pass through
Verbose bool
}
func (p *Payload) serve(conn *net.UnixConn, wl *Wayland) error {
if err := gob.NewEncoder(conn).Encode(*p); err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot stream shim payload:")
}
if wl != nil {
if err := wl.WriteUnix(conn); err != nil {
return errors.Join(err, conn.Close())
}
}
return fmsg.WrapErrorSuffix(conn.Close(),
"cannot close setup connection:")
}

75
internal/shim/wayland.go Normal file
View File

@ -0,0 +1,75 @@
package shim
import (
"fmt"
"net"
"sync"
"syscall"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
// Wayland implements wayland mediation.
type Wayland struct {
// wayland socket path
Path string
// wayland connection
conn *net.UnixConn
connErr error
sync.Once
// wait for wayland client to exit
done chan struct{}
}
func (wl *Wayland) WriteUnix(conn *net.UnixConn) error {
// connect to host wayland socket
if f, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: wl.Path, Net: "unix"}); err != nil {
return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot connect to wayland at %q:", wl.Path))
} else {
fmsg.VPrintf("connected to wayland at %q", wl.Path)
wl.conn = f
}
// set up for passing wayland socket
if rc, err := wl.conn.SyscallConn(); err != nil {
return fmsg.WrapErrorSuffix(err, "cannot obtain raw wayland connection:")
} else {
ec := make(chan error)
go func() {
// pass wayland connection fd
if err = rc.Control(func(fd uintptr) {
if _, _, err = conn.WriteMsgUnix(nil, syscall.UnixRights(int(fd)), nil); err != nil {
ec <- fmsg.WrapErrorSuffix(err, "cannot pass wayland connection to shim:")
return
}
ec <- nil
// block until shim exits
<-wl.done
fmsg.VPrintln("releasing wayland connection")
}); err != nil {
ec <- fmsg.WrapErrorSuffix(err, "cannot obtain wayland connection fd:")
return
}
}()
return <-ec
}
}
func (wl *Wayland) Close() error {
wl.Do(func() {
close(wl.done)
wl.connErr = wl.conn.Close()
})
return wl.connErr
}
func NewWayland() *Wayland {
wl := new(Wayland)
wl.done = make(chan struct{})
return wl
}

View File

@ -10,8 +10,8 @@ import (
"text/tabwriter"
"time"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/system"
"git.ophivana.moe/security/fortify/internal/verbose"
)
// MustPrintLauncherStateSimpleGlobal prints active launcher states of all simple stores
@ -21,19 +21,18 @@ func MustPrintLauncherStateSimpleGlobal(w **tabwriter.Writer, runDir string) {
// read runtime directory to get all UIDs
if dirs, err := os.ReadDir(path.Join(runDir, "state")); err != nil && !errors.Is(err, os.ErrNotExist) {
fmt.Println("cannot read runtime directory:", err)
os.Exit(1)
fmsg.Fatal("cannot read runtime directory:", err)
} else {
for _, e := range dirs {
// skip non-directories
if !e.IsDir() {
verbose.Println("skipped non-directory entry", e.Name())
fmsg.VPrintf("skipped non-directory entry %q", e.Name())
continue
}
// skip non-numerical names
if _, err = strconv.Atoi(e.Name()); err != nil {
verbose.Println("skipped non-uid entry", e.Name())
fmsg.VPrintf("skipped non-uid entry %q", e.Name())
continue
}
@ -45,7 +44,7 @@ func MustPrintLauncherStateSimpleGlobal(w **tabwriter.Writer, runDir string) {
// mustPrintLauncherState causes store activity so store needs to be closed
if err = s.Close(); err != nil {
fmt.Printf("warn: error closing store for user %s: %s\n", e.Name(), err)
fmsg.Printf("cannot close store for user %q: %s", e.Name(), err)
}
}
}
@ -67,7 +66,7 @@ func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time
*w = tabwriter.NewWriter(os.Stdout, 0, 1, 4, ' ', 0)
// write header when initialising
if !verbose.Get() {
if !fmsg.Verbose() {
_, _ = fmt.Fprintln(*w, "\tUID\tPID\tUptime\tEnablements\tMethod\tCommand")
} else {
// argv is emitted in body when verbose
@ -96,7 +95,7 @@ func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time
ets.WriteString("(No enablements)")
}
if !verbose.Get() {
if !fmsg.Verbose() {
_, _ = fmt.Fprintf(*w, "\t%s\t%d\t%s\t%s\t%s\t%s\n",
s.path[len(s.path)-1], state.PID, now.Sub(state.Time).Round(time.Second).String(), strings.TrimPrefix(ets.String(), ", "), state.Method,
state.Command)
@ -110,15 +109,13 @@ func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time
return nil
}()
}); err != nil {
fmt.Printf("cannot perform action on store '%s': %s\n", path.Join(s.path...), err)
fmsg.Printf("cannot perform action on store %q: %s", path.Join(s.path...), err)
if !ok {
fmt.Println("warn: store faulted before printing")
os.Exit(1)
fmsg.Fatal("store faulted before printing")
}
}
if innerErr != nil {
fmt.Printf("cannot print launcher state for store '%s': %s\n", path.Join(s.path...), innerErr)
os.Exit(1)
fmsg.Fatalf("cannot print launcher state for store %q: %s", path.Join(s.path...), innerErr)
}
}

120
internal/system.go Normal file
View File

@ -0,0 +1,120 @@
package internal
import (
"io/fs"
"os"
"os/exec"
"os/user"
"path"
"strconv"
"sync"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
// System provides safe access to operating system resources.
type System interface {
// Geteuid provides [os.Geteuid].
Geteuid() int
// LookupEnv provides [os.LookupEnv].
LookupEnv(key string) (string, bool)
// TempDir provides [os.TempDir].
TempDir() string
// LookPath provides [exec.LookPath].
LookPath(file string) (string, error)
// Executable provides [os.Executable].
Executable() (string, error)
// Lookup provides [user.Lookup].
Lookup(username string) (*user.User, error)
// ReadDir provides [os.ReadDir].
ReadDir(name string) ([]fs.DirEntry, error)
// Stat provides [os.Stat].
Stat(name string) (fs.FileInfo, error)
// Open provides [os.Open]
Open(name string) (fs.File, error)
// Exit provides [os.Exit].
Exit(code int)
// Paths returns a populated [Paths] struct.
Paths() Paths
}
// Paths contains environment dependent paths used by fortify.
type Paths struct {
// path to shared directory e.g. /tmp/fortify.%d
SharePath string `json:"share_path"`
// XDG_RUNTIME_DIR value e.g. /run/user/%d
RuntimePath string `json:"runtime_path"`
// application runtime directory e.g. /run/user/%d/fortify
RunDirPath string `json:"run_dir_path"`
}
// CopyPaths is a generic implementation of [System.Paths].
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)
if r, ok := os.LookupEnv(xdgRuntimeDir); !ok || r == "" || !path.IsAbs(r) {
// fall back to path in share since fortify has no hard XDG dependency
v.RunDirPath = path.Join(v.SharePath, "run")
v.RuntimePath = path.Join(v.RunDirPath, "compat")
} else {
v.RuntimePath = r
v.RunDirPath = path.Join(v.RuntimePath, "fortify")
}
fmsg.VPrintf("runtime directory at %q", v.RunDirPath)
}
// Std implements System using the standard library.
type Std struct {
paths Paths
pathsOnce sync.Once
}
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) Executable() (string, error) {
return os.Executable()
}
func (s *Std) Lookup(username string) (*user.User, error) {
return user.Lookup(username)
}
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) Exit(code int) {
fmsg.Exit(code)
}
const xdgRuntimeDir = "XDG_RUNTIME_DIR"
func (s *Std) Paths() Paths {
s.pathsOnce.Do(func() { CopyPaths(s, &s.paths) })
return s.paths
}

View File

@ -6,26 +6,29 @@ import (
"git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/verbose"
)
// UpdatePerm appends an ephemeral acl update Op.
func (sys *I) UpdatePerm(path string, perms ...acl.Perm) {
func (sys *I) UpdatePerm(path string, perms ...acl.Perm) *I {
sys.UpdatePermType(Process, path, perms...)
return sys
}
// UpdatePermType appends an acl update Op.
func (sys *I) UpdatePermType(et Enablement, path string, perms ...acl.Perm) {
func (sys *I) UpdatePermType(et Enablement, path string, perms ...acl.Perm) *I {
sys.lock.Lock()
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &ACL{et, path, perms})
return sys
}
type ACL struct {
et Enablement
path string
perms []acl.Perm
perms acl.Perms
}
func (a *ACL) Type() Enablement {
@ -33,18 +36,18 @@ func (a *ACL) Type() Enablement {
}
func (a *ACL) apply(sys *I) error {
verbose.Println("applying ACL", a, "uid:", sys.uid, "type:", TypeString(a.et), "path:", a.path)
fmsg.VPrintln("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) {
verbose.Println("stripping ACL", a, "uid:", sys.uid, "type:", TypeString(a.et), "path:", a.path)
fmsg.VPrintln("stripping ACL", a)
return fmsg.WrapErrorSuffix(acl.UpdatePerm(a.path, sys.uid),
fmt.Sprintf("cannot strip ACL entry from %q:", a.path))
} else {
verbose.Println("skipping ACL", a, "uid:", sys.uid, "tag:", TypeString(a.et), "path:", a.path)
fmsg.VPrintln("skipping ACL", a)
return nil
}
}
@ -62,16 +65,6 @@ func (a *ACL) Path() string {
}
func (a *ACL) String() string {
var s = []byte("---")
for _, p := range a.perms {
switch p {
case acl.Read:
s[0] = 'r'
case acl.Write:
s[1] = 'w'
case acl.Execute:
s[2] = 'x'
}
}
return string(s)
return fmt.Sprintf("%s type: %s path: %q",
a.perms, TypeString(a.et), a.path)
}

View File

@ -49,21 +49,22 @@ func TestUpdatePermType(t *testing.T) {
func TestACL_String(t *testing.T) {
testCases := []struct {
want string
et Enablement
perms []acl.Perm
}{
{"---", []acl.Perm{}},
{"r--", []acl.Perm{acl.Read}},
{"-w-", []acl.Perm{acl.Write}},
{"--x", []acl.Perm{acl.Execute}},
{"rw-", []acl.Perm{acl.Read, acl.Write}},
{"r-x", []acl.Perm{acl.Read, acl.Execute}},
{"rwx", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
{"rwx", []acl.Perm{acl.Read, acl.Write, acl.Write, acl.Execute}},
{`--- type: Process path: "/nonexistent"`, Process, []acl.Perm{}},
{`r-- type: User path: "/nonexistent"`, User, []acl.Perm{acl.Read}},
{`-w- type: Wayland path: "/nonexistent"`, EWayland, []acl.Perm{acl.Write}},
{`--x type: X11 path: "/nonexistent"`, EX11, []acl.Perm{acl.Execute}},
{`rw- type: D-Bus path: "/nonexistent"`, EDBus, []acl.Perm{acl.Read, acl.Write}},
{`r-x type: PulseAudio path: "/nonexistent"`, EPulse, []acl.Perm{acl.Read, acl.Execute}},
{`rwx type: User path: "/nonexistent"`, User, []acl.Perm{acl.Read, acl.Write, acl.Execute}},
{`rwx type: Process path: "/nonexistent"`, Process, []acl.Perm{acl.Read, acl.Write, acl.Write, acl.Execute}},
}
for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) {
a := &ACL{perms: tc.perms}
a := &ACL{et: tc.et, perms: tc.perms, path: "/nonexistent"}
if got := a.String(); got != tc.want {
t.Errorf("String() = %v, want %v",
got, tc.want)

View File

@ -2,12 +2,10 @@ package system
import (
"errors"
"fmt"
"os"
"git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/verbose"
)
var (
@ -42,12 +40,12 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st
d.proxy = dbus.New(sessionBus, systemBus)
defer func() {
if verbose.Get() && d.proxy.Sealed() {
verbose.Println("sealed session proxy", session.Args(sessionBus))
if fmsg.Verbose() && d.proxy.Sealed() {
fmsg.VPrintln("sealed session proxy", session.Args(sessionBus))
if system != nil {
verbose.Println("sealed system proxy", system.Args(systemBus))
fmsg.VPrintln("sealed system proxy", system.Args(systemBus))
}
verbose.Println("message bus proxy final args:", d.proxy)
fmsg.VPrintln("message bus proxy final args:", d.proxy)
}
}()
@ -73,9 +71,9 @@ func (d *DBus) Type() Enablement {
}
func (d *DBus) apply(_ *I) error {
verbose.Printf("session bus proxy on %q for upstream %q\n", d.proxy.Session()[1], d.proxy.Session()[0])
fmsg.VPrintf("session bus proxy on %q for upstream %q", d.proxy.Session()[1], d.proxy.Session()[0])
if d.system {
verbose.Printf("system bus proxy on %q for upstream %q\n", d.proxy.System()[1], d.proxy.System()[0])
fmsg.VPrintf("system bus proxy on %q for upstream %q", d.proxy.System()[1], d.proxy.System()[0])
}
// ready channel passed to dbus package
@ -86,27 +84,27 @@ func (d *DBus) apply(_ *I) error {
return fmsg.WrapErrorSuffix(err,
"cannot start message bus proxy:")
}
verbose.Println("starting message bus proxy:", d.proxy)
if verbose.Get() { // save the extra bwrap arg build when verbose logging is off
verbose.Println("message bus proxy bwrap args:", d.proxy.Bwrap())
fmsg.VPrintln("starting message bus proxy:", d.proxy)
if fmsg.Verbose() { // save the extra bwrap arg build when verbose logging is off
fmsg.VPrintln("message bus proxy bwrap args:", d.proxy.Bwrap())
}
// background wait for proxy instance and notify completion
go func() {
if err := d.proxy.Wait(); err != nil {
fmt.Println("fortify: message bus proxy exited with error:", err)
fmsg.Println("message bus proxy exited with error:", err)
go func() { ready <- err }()
} else {
verbose.Println("message bus proxy exit")
fmsg.VPrintln("message bus proxy exit")
}
// ensure socket removal so ephemeral directory is empty at revert
if err := os.Remove(d.proxy.Session()[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
fmt.Println("fortify: cannot remove dangling session bus socket:", err)
fmsg.Println("cannot remove dangling session bus socket:", err)
}
if d.system {
if err := os.Remove(d.proxy.System()[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
fmt.Println("fortify: cannot remove dangling system bus socket:", err)
fmsg.Println("cannot remove dangling system bus socket:", err)
}
}
@ -120,14 +118,14 @@ func (d *DBus) apply(_ *I) error {
return fmsg.WrapErrorSuffix(err,
"message bus proxy fault after start:")
}
verbose.Println("message bus proxy ready")
fmsg.VPrintln("message bus proxy ready")
return nil
}
func (d *DBus) revert(_ *I, _ *Criteria) error {
// criteria ignored here since dbus is always process-scoped
verbose.Println("terminating message bus proxy")
fmsg.VPrintln("terminating message bus proxy")
if err := d.proxy.Close(); err != nil {
if errors.Is(err, os.ErrClosed) {

View File

@ -6,23 +6,26 @@ import (
"os"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/verbose"
)
// Ensure the existence and mode of a directory.
func (sys *I) Ensure(name string, perm os.FileMode) {
func (sys *I) Ensure(name string, perm os.FileMode) *I {
sys.lock.Lock()
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &Mkdir{User, name, perm, false})
return sys
}
// Ephemeral ensures the temporary existence and mode of a directory through the life of et.
func (sys *I) Ephemeral(et Enablement, name string, perm os.FileMode) {
func (sys *I) Ephemeral(et Enablement, name string, perm os.FileMode) *I {
sys.lock.Lock()
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &Mkdir{et, name, perm, true})
return sys
}
type Mkdir struct {
@ -37,7 +40,7 @@ func (m *Mkdir) Type() Enablement {
}
func (m *Mkdir) apply(_ *I) error {
verbose.Println("ensuring directory", m)
fmsg.VPrintln("ensuring directory", m)
// create directory
err := os.Mkdir(m.path, m.perm)
@ -58,11 +61,11 @@ func (m *Mkdir) revert(_ *I, ec *Criteria) error {
}
if ec.hasType(m) {
verbose.Println("destroying ephemeral directory", m)
fmsg.VPrintln("destroying ephemeral directory", m)
return fmsg.WrapErrorSuffix(os.Remove(m.path),
fmt.Sprintf("cannot remove ephemeral directory %q:", m.path))
} else {
verbose.Println("skipping ephemeral directory", m)
fmsg.VPrintln("skipping ephemeral directory", m)
return nil
}
}

View File

@ -2,8 +2,9 @@ package system
import (
"errors"
"fmt"
"sync"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
const (
@ -64,6 +65,20 @@ func (sys *I) UID() int {
return sys.uid
}
func (sys *I) Equal(v *I) bool {
if v == nil || sys.uid != v.uid || len(sys.ops) != len(v.ops) {
return false
}
for i, o := range sys.ops {
if !o.Is(v.ops[i]) {
return false
}
}
return true
}
func (sys *I) Commit() error {
sys.lock.Lock()
defer sys.lock.Unlock()
@ -79,8 +94,9 @@ func (sys *I) Commit() 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))
if err := sp.Revert(&Criteria{nil}); err != nil {
fmt.Println("fortify: errors returned reverting partial commit:", err)
fmsg.Println("errors returned reverting partial commit:", err)
}
}
}()

View File

@ -51,3 +51,79 @@ func TestTypeString(t *testing.T) {
})
}
}
func TestI_Equal(t *testing.T) {
testCases := []struct {
name string
sys *system.I
v *system.I
want bool
}{
{
"simple UID",
system.New(150),
system.New(150),
true,
},
{
"simple UID differ",
system.New(150),
system.New(151),
false,
},
{
"simple UID nil",
system.New(150),
nil,
false,
},
{
"op length mismatch",
system.New(150).
ChangeHosts("chronos"),
system.New(150).
ChangeHosts("chronos").
Ensure("/run", 0755),
false,
},
{
"op value mismatch",
system.New(150).
ChangeHosts("chronos").
Ensure("/run", 0644),
system.New(150).
ChangeHosts("chronos").
Ensure("/run", 0755),
false,
},
{
"op type mismatch",
system.New(150).
ChangeHosts("chronos").
CopyFile("/tmp/fortify.1971/30c9543e0a2c9621a8bfecb9d874c347/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie"),
system.New(150).
ChangeHosts("chronos").
Ensure("/run", 0755),
false,
},
{
"op equals",
system.New(150).
ChangeHosts("chronos").
Ensure("/run", 0755),
system.New(150).
ChangeHosts("chronos").
Ensure("/run", 0755),
true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.sys.Equal(tc.v) != tc.want {
t.Errorf("Equal: got %v; want %v",
!tc.want, tc.want)
}
})
}
}

View File

@ -9,48 +9,53 @@ import (
"git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/verbose"
)
// CopyFile registers an Op that copies path dst from src.
func (sys *I) CopyFile(dst, src string) {
sys.CopyFileType(Process, dst, src)
func (sys *I) CopyFile(dst, src string) *I {
return sys.CopyFileType(Process, dst, src)
}
// CopyFileType registers a file copying Op labelled with type et.
func (sys *I) CopyFileType(et Enablement, dst, src string) {
func (sys *I) CopyFileType(et Enablement, dst, src string) *I {
sys.lock.Lock()
sys.ops = append(sys.ops, &Tmpfile{et, tmpfileCopy, dst, src})
sys.lock.Unlock()
sys.UpdatePermType(et, dst, acl.Read)
return sys
}
// Link registers an Op that links dst to src.
func (sys *I) Link(oldname, newname string) {
sys.LinkFileType(Process, oldname, newname)
func (sys *I) Link(oldname, newname string) *I {
return sys.LinkFileType(Process, oldname, newname)
}
// LinkFileType registers a file linking Op labelled with type et.
func (sys *I) LinkFileType(et Enablement, oldname, newname string) {
func (sys *I) LinkFileType(et Enablement, oldname, newname string) *I {
sys.lock.Lock()
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &Tmpfile{et, tmpfileLink, newname, oldname})
return sys
}
// Write registers an Op that writes dst with the contents of src.
func (sys *I) Write(dst, src string) {
sys.WriteType(Process, dst, src)
func (sys *I) Write(dst, src string) *I {
return sys.WriteType(Process, dst, src)
}
// WriteType registers a file writing Op labelled with type et.
func (sys *I) WriteType(et Enablement, dst, src string) {
func (sys *I) WriteType(et Enablement, dst, src string) *I {
sys.lock.Lock()
sys.ops = append(sys.ops, &Tmpfile{et, tmpfileWrite, dst, src})
sys.lock.Unlock()
sys.UpdatePermType(et, dst, acl.Read)
return sys
}
const (
@ -72,15 +77,15 @@ func (t *Tmpfile) Type() Enablement {
func (t *Tmpfile) apply(_ *I) error {
switch t.method {
case tmpfileCopy:
verbose.Printf("publishing tmpfile %s\n", t)
fmsg.VPrintln("publishing tmpfile", t)
return fmsg.WrapErrorSuffix(copyFile(t.dst, t.src),
fmt.Sprintf("cannot copy tmpfile %q:", t.dst))
case tmpfileLink:
verbose.Printf("linking tmpfile %s\n", t)
fmsg.VPrintln("linking tmpfile", t)
return fmsg.WrapErrorSuffix(os.Link(t.src, t.dst),
fmt.Sprintf("cannot link tmpfile %q:", t.dst))
case tmpfileWrite:
verbose.Printf("writing %s\n", t)
fmsg.VPrintln("writing", t)
return fmsg.WrapErrorSuffix(os.WriteFile(t.dst, []byte(t.src), 0600),
fmt.Sprintf("cannot write tmpfile %q:", t.dst))
default:
@ -90,11 +95,11 @@ func (t *Tmpfile) apply(_ *I) error {
func (t *Tmpfile) revert(_ *I, ec *Criteria) error {
if ec.hasType(t) {
verbose.Printf("removing tmpfile %q\n", t.dst)
fmsg.VPrintf("removing tmpfile %q", t.dst)
return fmsg.WrapErrorSuffix(os.Remove(t.dst),
fmt.Sprintf("cannot remove tmpfile %q:", t.dst))
} else {
verbose.Printf("skipping tmpfile %q\n", t.dst)
fmsg.VPrintf("skipping tmpfile %q", t.dst)
return nil
}
}

View File

@ -4,16 +4,17 @@ import (
"fmt"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/verbose"
"git.ophivana.moe/security/fortify/xcb"
)
// ChangeHosts appends an X11 ChangeHosts command Op.
func (sys *I) ChangeHosts(username string) {
func (sys *I) ChangeHosts(username string) *I {
sys.lock.Lock()
defer sys.lock.Unlock()
sys.ops = append(sys.ops, XHost(username))
return sys
}
type XHost string
@ -23,18 +24,18 @@ func (x XHost) Type() Enablement {
}
func (x XHost) apply(_ *I) error {
verbose.Printf("inserting entry %s to X11\n", x)
fmsg.VPrintf("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) {
verbose.Printf("deleting entry %s from X11\n", x)
fmsg.VPrintf("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 {
verbose.Printf("skipping entry %s in X11\n", x)
fmsg.VPrintf("skipping entry %s in X11", x)
return nil
}
}

View File

@ -1,19 +0,0 @@
package verbose
import (
"fmt"
)
var Prefix = "fortify:"
func Println(a ...any) {
if verbose.Load() {
fmt.Println(append([]any{Prefix}, a...)...)
}
}
func Printf(format string, a ...any) {
if verbose.Load() {
fmt.Printf(Prefix+" "+format, a...)
}
}

View File

@ -1,67 +0,0 @@
package verbose_test
import (
"os"
"os/exec"
"strconv"
"strings"
"testing"
"git.ophivana.moe/security/fortify/internal/verbose"
)
const (
testVerbose = "GO_TEST_VERBOSE"
wantStdout = "fortify: println\nfortify: printf"
)
func TestPrinter(t *testing.T) {
switch os.Getenv(testVerbose) {
case "0":
verbose.Set(false)
case "1":
verbose.Set(true)
default:
return
}
verbose.Println("println")
verbose.Printf("%s", "printf")
}
func TestPrintf_Println(t *testing.T) {
testPrintfPrintln(t, false)
testPrintfPrintln(t, true)
// make -cover happy
stdout := os.Stdout
t.Cleanup(func() {
os.Stdout = stdout
})
os.Stdout = nil
verbose.Set(true)
verbose.Printf("")
verbose.Println()
}
func testPrintfPrintln(t *testing.T, v bool) {
t.Run("start verbose printer with verbose "+strconv.FormatBool(v), func(t *testing.T) {
stdout, stderr := new(strings.Builder), new(strings.Builder)
stdout.Grow(len(wantStdout))
cmd := exec.Command(os.Args[0], "-test.run=TestPrinter")
cmd.Stdout, cmd.Stderr = stdout, stderr
if v {
cmd.Env = append(cmd.Env, testVerbose+"=1")
} else {
cmd.Env = append(cmd.Env, testVerbose+"=0")
}
if err := cmd.Run(); err != nil {
panic("cannot run printer process: " + err.Error() + " stderr: " + stderr.String())
}
if got := stdout.String(); strings.Contains(got, wantStdout) != v {
t.Errorf("Print: got %v; want %t",
got, v)
}
})
}

View File

@ -1,13 +0,0 @@
package verbose
import "sync/atomic"
var verbose = new(atomic.Bool)
func Get() bool {
return verbose.Load()
}
func Set(v bool) {
verbose.Store(v)
}

View File

@ -1,20 +0,0 @@
package verbose_test
import (
"testing"
"git.ophivana.moe/security/fortify/internal/verbose"
)
func TestGetSet(t *testing.T) {
verbose.Set(false)
if verbose.Get() {
t.Errorf("Get() = true, want false")
}
verbose.Set(true)
if !verbose.Get() {
t.Errorf("Get() = false, want true")
}
}

View File

@ -4,7 +4,6 @@ import (
_ "embed"
"flag"
"fmt"
"os"
)
var (

37
main.go
View File

@ -2,15 +2,13 @@ package main
import (
"flag"
"fmt"
"os"
"syscall"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/app"
"git.ophivana.moe/security/fortify/internal/fmsg"
init0 "git.ophivana.moe/security/fortify/internal/init"
"git.ophivana.moe/security/fortify/internal/shim"
"git.ophivana.moe/security/fortify/internal/verbose"
)
var (
@ -21,17 +19,19 @@ func init() {
flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
}
var os = new(internal.Std)
func main() {
// linux/sched/coredump.h
if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, 0, 0); errno != 0 {
fmt.Printf("fortify: cannot set SUID_DUMP_DISABLE: %s", errno.Error())
fmsg.Printf("fortify: cannot set SUID_DUMP_DISABLE: %s", errno.Error())
}
flag.Parse()
verbose.Set(flagVerbose)
fmsg.SetVerbose(flagVerbose)
if internal.SdBootedV {
verbose.Println("system booted with systemd as init system")
fmsg.VPrintln("system booted with systemd as init system")
}
// shim/init early exit
@ -39,9 +39,9 @@ func main() {
shim.Try()
// root check
if os.Getuid() == 0 {
fmt.Println("fortify: this program must not run as root")
os.Exit(1)
if os.Geteuid() == 0 {
fmsg.Fatal("this program must not run as root")
panic("unreachable")
}
// version/license/template command early exit
@ -53,22 +53,25 @@ func main() {
tryState()
// invoke app
r := 1
a, err := app.New()
a, err := app.New(os)
if err != nil {
fatalf("cannot create app: %s\n", err)
fmsg.Fatalf("cannot create app: %s\n", err)
} else if err = a.Seal(loadConfig()); err != nil {
logBaseError(err, "fortify: cannot seal app:")
logBaseError(err, "cannot seal app:")
} else if err = a.Start(); err != nil {
logBaseError(err, "fortify: cannot start app:")
} else if r, err = a.Wait(); err != nil {
logBaseError(err, "cannot start app:")
}
var r int
// wait must be called regardless of result of start
if r, err = a.Wait(); err != nil {
if r < 1 {
r = 1
}
logWaitError(err)
}
if err = a.WaitErr(); err != nil {
fmt.Println("fortify: inner wait failed:", err)
fmsg.Println("inner wait failed:", err)
}
os.Exit(r)
fmsg.Exit(r)
}

View File

@ -10,7 +10,7 @@
buildGoModule rec {
pname = "fortify";
version = "0.0.7";
version = "0.0.8";
src = ./.;
vendorHash = null;

View File

@ -3,10 +3,9 @@ package main
import (
"flag"
"fmt"
"os"
"text/tabwriter"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/state"
)
@ -22,15 +21,15 @@ func init() {
func tryState() {
if stateActionEarly {
var w *tabwriter.Writer
state.MustPrintLauncherStateSimpleGlobal(&w, internal.GetSC().RunDirPath)
state.MustPrintLauncherStateSimpleGlobal(&w, os.Paths().RunDirPath)
if w != nil {
if err := w.Flush(); err != nil {
fmt.Println("warn: error formatting output:", err)
fmsg.Println("cannot format output:", err)
}
} else {
fmt.Println("No information available")
}
os.Exit(0)
fmsg.Exit(0)
}
}

View File

@ -3,7 +3,6 @@ package main
import (
"flag"
"fmt"
"os"
)
var (