Compare commits

..

No commits in common. "master" and "v0.3.1" have entirely different histories.

87 changed files with 2534 additions and 3345 deletions

View File

@ -22,57 +22,6 @@ jobs:
path: result/*
retention-days: 1
race:
name: Fortify (race detector)
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run NixOS test
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.race
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "fortify-race-vm-output"
path: result/*
retention-days: 1
sandbox:
name: Sandbox
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run NixOS test
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sandbox
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "sandbox-vm-output"
path: result/*
retention-days: 1
sandbox-race:
name: Sandbox (race detector)
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run NixOS test
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sandbox-race
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "sandbox-race-vm-output"
path: result/*
retention-days: 1
fpkg:
name: Fpkg
runs-on: nix
@ -90,14 +39,29 @@ jobs:
path: result/*
retention-days: 1
race:
name: Data race detector
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run NixOS test
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.race
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "fortify-race-vm-output"
path: result/*
retention-days: 1
check:
name: Flake checks
needs:
- fortify
- race
- sandbox
- sandbox-race
- fpkg
- race
runs-on: nix
steps:
- name: Checkout

View File

@ -19,7 +19,7 @@ type appInfo struct {
// passed through to [fst.Config]
ID string `json:"id"`
// passed through to [fst.Config]
Identity int `json:"identity"`
AppID int `json:"app_id"`
// passed through to [fst.Config]
Groups []string `json:"groups,omitempty"`
// passed through to [fst.Config]
@ -29,7 +29,7 @@ type appInfo struct {
// passed through to [fst.Config]
Net bool `json:"net,omitempty"`
// passed through to [fst.Config]
Device bool `json:"dev,omitempty"`
Dev bool `json:"dev,omitempty"`
// passed through to [fst.Config]
Tty bool `json:"tty,omitempty"`
// passed through to [fst.Config]
@ -65,32 +65,23 @@ type appInfo struct {
func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool) *fst.Config {
config := &fst.Config{
ID: app.ID,
Path: argv[0],
Args: argv,
Enablements: app.Enablements,
SystemBus: app.SystemBus,
SessionBus: app.SessionBus,
DirectWayland: app.DirectWayland,
Username: "fortify",
Shell: shellPath,
Data: pathSet.homeDir,
Dir: path.Join("/data/data", app.ID),
Identity: app.Identity,
Confinement: fst.ConfinementConfig{
AppID: app.AppID,
Groups: app.Groups,
Container: &fst.ContainerConfig{
Username: "fortify",
Inner: path.Join("/data/data", app.ID),
Outer: pathSet.homeDir,
Sandbox: &fst.SandboxConfig{
Hostname: formatHostname(app.Name),
Devel: app.Devel,
Userns: app.Userns,
Net: app.Net,
Device: app.Device,
Dev: app.Dev,
Tty: app.Tty || flagDropShell,
MapRealUID: app.MapRealUID,
DirectWayland: app.DirectWayland,
Filesystem: []*fst.FilesystemConfig{
{Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true},
{Src: pathSet.metaPath, Dst: path.Join(fst.Tmp, "app"), Must: true},
@ -113,12 +104,16 @@ func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
},
SystemBus: app.SystemBus,
SessionBus: app.SessionBus,
Enablements: app.Enablements,
},
}
if app.Multiarch {
config.Container.Seccomp |= seccomp.FilterMultiarch
config.Confinement.Sandbox.Seccomp |= seccomp.FlagMultiarch
}
if app.Bluetooth {
config.Container.Seccomp |= seccomp.FilterBluetooth
config.Confinement.Sandbox.Seccomp |= seccomp.FlagBluetooth
}
return config
}

View File

@ -31,7 +31,7 @@
'',
id ? name,
identity ? throw "identity is required",
app_id ? throw "app_id is required",
groups ? [ ],
userns ? false,
net ? true,
@ -147,7 +147,7 @@ let
name
version
id
identity
app_id
launcher
groups
userns

View File

@ -13,7 +13,7 @@ import (
"git.gensokyo.uk/security/fortify/command"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/app/instance"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox"
@ -62,7 +62,7 @@ func main() {
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next fortify action")
c.Command("shim", command.UsageInternal, func([]string) error { instance.ShimMain(); return errSuccess })
c.Command("shim", command.UsageInternal, func([]string) error { app.ShimMain(); return errSuccess })
{
var (
@ -157,11 +157,11 @@ func main() {
return errSuccess
}
// identity determines uid
if a.Identity != bundle.Identity {
// AppID determines uid
if a.AppID != bundle.AppID {
cleanup()
log.Printf("package %q identity %d differs from installed %d",
pkgPath, bundle.Identity, a.Identity)
log.Printf("package %q app id %d differs from installed %d",
pkgPath, bundle.AppID, a.AppID)
return syscall.EBADE
}
@ -292,7 +292,7 @@ func main() {
"--override-input nixpkgs path:/etc/nixpkgs " +
"path:" + a.NixGL + "#nixVulkanNvidia",
}, true, func(config *fst.Config) *fst.Config {
config.Container.Filesystem = append(config.Container.Filesystem, []*fst.FilesystemConfig{
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
{Src: "/etc/resolv.conf"},
{Src: "/sys/block"},
{Src: "/sys/bus"},
@ -324,7 +324,7 @@ func main() {
*/
if a.GPU {
config.Container.Filesystem = append(config.Container.Filesystem,
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem,
&fst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(fst.Tmp, "nixGL")})
appendGPUFilesystem(config)
}

View File

@ -72,7 +72,7 @@ func pathSetByApp(id string) *appPathSet {
}
func appendGPUFilesystem(config *fst.Config) {
config.Container.Filesystem = append(config.Container.Filesystem, []*fst.FilesystemConfig{
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
{Src: "/dev/dri", Device: true},
// mali

View File

@ -6,24 +6,23 @@ import (
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/app/instance"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
func mustRunApp(ctx context.Context, config *fst.Config, beforeFail func()) {
rs := new(app.RunState)
a := instance.MustNew(instance.ISetuid, ctx, std)
rs := new(fst.RunState)
a := app.MustNew(ctx, std)
var code int
if sa, err := a.Seal(config); err != nil {
fmsg.PrintBaseError(err, "cannot seal app:")
code = 1
rs.ExitCode = 1
} else {
code = instance.PrintRunStateErr(instance.ISetuid, rs, sa.Run(rs))
// this updates ExitCode
app.PrintRunStateErr(rs, sa.Run(rs))
}
if code != 0 {
if rs.ExitCode != 0 {
beforeFail()
os.Exit(code)
os.Exit(rs.ExitCode)
}
}

View File

@ -10,7 +10,7 @@ buildPackage {
name = "foot";
inherit (foot) version;
identity = 2;
app_id = 2;
id = "org.codeberg.dnkl.foot";
modules = [

View File

@ -65,8 +65,8 @@ def check_state(name, enablements):
if len(config['args']) != 1 or not (config['args'][0].startswith("/nix/store/")) or f"fortify-{name}-" not in (config['args'][0]):
raise Exception(f"unexpected args {instance['config']['args']}")
if config['enablements'] != enablements:
raise Exception(f"unexpected enablements {instance['config']['enablements']}")
if config['confinement']['enablements'] != enablements:
raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
start_all()

View File

@ -17,7 +17,6 @@ func withNixDaemon(
) {
mustRunAppDropShell(ctx, updateConfig(&fst.Config{
ID: app.ID,
Path: shellPath,
Args: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
// start nix-daemon
@ -30,23 +29,16 @@ func withNixDaemon(
// terminate nix-daemon
" && pkill nix-daemon",
},
Confinement: fst.ConfinementConfig{
AppID: app.AppID,
Username: "fortify",
Shell: shellPath,
Data: pathSet.homeDir,
Dir: path.Join("/data/data", app.ID),
ExtraPerms: []*fst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
},
Identity: app.Identity,
Container: &fst.ContainerConfig{
Inner: path.Join("/data/data", app.ID),
Outer: pathSet.homeDir,
Sandbox: &fst.SandboxConfig{
Hostname: formatHostname(app.Name) + "-" + action,
Userns: true, // nix sandbox requires userns
Net: net,
Seccomp: seccomp.FilterMultiarch,
Seccomp: seccomp.FlagMultiarch,
Tty: dropShell,
Filesystem: []*fst.FilesystemConfig{
{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
@ -59,6 +51,11 @@ func withNixDaemon(
Etc: path.Join(pathSet.cacheDir, "etc"),
AutoEtc: true,
},
ExtraPerms: []*fst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
},
},
}), dropShell, beforeFail)
}
@ -68,25 +65,16 @@ func withCacheDir(
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
mustRunAppDropShell(ctx, &fst.Config{
ID: app.ID,
Path: shellPath,
Args: []string{shellPath, "-lc", strings.Join(command, " && ")},
Confinement: fst.ConfinementConfig{
AppID: app.AppID,
Username: "nixos",
Shell: shellPath,
Data: pathSet.cacheDir, // this also ensures cacheDir via shim
Dir: path.Join("/data/data", app.ID, "cache"),
ExtraPerms: []*fst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
{Path: workDir, Execute: true},
},
Identity: app.Identity,
Container: &fst.ContainerConfig{
Inner: path.Join("/data/data", app.ID, "cache"),
Outer: pathSet.cacheDir, // this also ensures cacheDir via shim
Sandbox: &fst.SandboxConfig{
Hostname: formatHostname(app.Name) + "-" + action,
Seccomp: seccomp.FilterMultiarch,
Seccomp: seccomp.FlagMultiarch,
Tty: dropShell,
Filesystem: []*fst.FilesystemConfig{
{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
@ -100,6 +88,12 @@ func withCacheDir(
Etc: path.Join(workDir, "etc"),
AutoEtc: true,
},
ExtraPerms: []*fst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
{Path: workDir, Execute: true},
},
},
}, dropShell, beforeFail)
}

View File

@ -8,7 +8,6 @@ import (
"os"
"os/exec"
"strings"
"syscall"
"testing"
"time"
@ -72,7 +71,7 @@ func TestProxy_Seal(t *testing.T) {
for id, tc := range testCasePairs() {
t.Run("create seal for "+id, func(t *testing.T) {
p := dbus.New(tc[0].bus, tc[1].bus)
if err := p.Seal(tc[0].c, tc[1].c); (errors.Is(err, syscall.EINVAL)) != tc[0].wantErr {
if err := p.Seal(tc[0].c, tc[1].c); (errors.Is(err, helper.ErrContainsNull)) != tc[0].wantErr {
t.Errorf("Seal(%p, %p) error = %v, wantErr %v",
tc[0].c, tc[1].c,
err, tc[0].wantErr)

View File

@ -63,7 +63,7 @@ func (p *Proxy) Start(ctx context.Context, output io.Writer, useSandbox bool) er
c, toolPath,
p.seal, true,
argF, func(container *sandbox.Container) {
container.Seccomp |= seccomp.FilterMultiarch
container.Seccomp |= seccomp.FlagMultiarch
container.Hostname = "fortify-dbus"
container.CommandContext = p.CommandContext
if output != nil {

12
flake.lock generated
View File

@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1742655702,
"narHash": "sha256-jbqlw4sPArFtNtA1s3kLg7/A4fzP4GLk9bGbtUJg0JQ=",
"lastModified": 1742234739,
"narHash": "sha256-zFL6zsf/5OztR1NSNQF33dvS1fL/BzVUjabZq4qrtY4=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "0948aeedc296f964140d9429223c7e4a0702a1ff",
"rev": "f6af7280a3390e65c2ad8fd059cdc303426cbd59",
"type": "github"
},
"original": {
@ -23,11 +23,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1743231893,
"narHash": "sha256-tpJsHMUPEhEnzySoQxx7+kA+KUtgWqvlcUBqROYNNt0=",
"lastModified": 1742512142,
"narHash": "sha256-8XfURTDxOm6+33swQJu/hx6xw1Tznl8vJJN5HwVqckg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c570c1f5304493cafe133b8d843c7c1c4a10d3a6",
"rev": "7105ae3957700a9646cc4b766f5815b23ed0c682",
"type": "github"
},
"original": {

View File

@ -58,19 +58,12 @@
in
{
fortify = callPackage ./test { inherit system self; };
fpkg = callPackage ./cmd/fpkg/test { inherit system self; };
race = callPackage ./test {
inherit system self;
withRace = true;
};
sandbox = callPackage ./test/sandbox { inherit self; };
sandbox-race = callPackage ./test/sandbox {
inherit self;
withRace = true;
};
fpkg = callPackage ./cmd/fpkg/test { inherit system self; };
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
cd ${./.}

47
fst/app.go Normal file
View File

@ -0,0 +1,47 @@
// Package fst exports shared fortify types.
package fst
import (
"time"
)
type App interface {
// ID returns a copy of [fst.ID] held by App.
ID() ID
// Seal determines the outcome of config as a [SealedApp].
// The value of config might be overwritten and must not be used again.
Seal(config *Config) (SealedApp, error)
String() string
}
type SealedApp interface {
// Run commits sealed system setup and starts the app process.
Run(rs *RunState) error
}
// RunState stores the outcome of a call to [SealedApp.Run].
type RunState struct {
// Time is the exact point in time where the process was created.
// Location must be set to UTC.
//
// Time is nil if no process was ever created.
Time *time.Time
// ExitCode is the value returned by shim.
ExitCode int
// RevertErr is stored by the deferred revert call.
RevertErr error
// WaitErr is error returned by the underlying wait syscall.
WaitErr error
}
// Paths contains environment-dependent paths used by fortify.
type Paths struct {
// path to shared directory (usually `/tmp/fortify.%d`)
SharePath string `json:"share_path"`
// XDG_RUNTIME_DIR value (usually `/run/user/%d`)
RuntimePath string `json:"runtime_path"`
// application runtime directory (usually `/run/user/%d/fortify`)
RunDirPath string `json:"run_dir_path"`
}

View File

@ -1,14 +1,14 @@
// Package fst exports shared fortify types.
package fst
import (
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
"git.gensokyo.uk/security/fortify/system"
)
const Tmp = "/.fortify"
// Config is used to seal an app implementation.
// Config is used to seal an app
type Config struct {
// reverse-DNS style arbitrary identifier string from config;
// passed to wayland security-context-v1 as application ID
@ -20,40 +20,37 @@ type Config struct {
// final args passed to container init
Args []string `json:"args"`
// system services to make available in the container
Enablements system.Enablement `json:"enablements"`
// session D-Bus proxy configuration;
// nil makes session bus proxy assume built-in defaults
SessionBus *dbus.Config `json:"session_bus,omitempty"`
// system D-Bus proxy configuration;
// nil disables system bus proxy
SystemBus *dbus.Config `json:"system_bus,omitempty"`
// direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1
// and the bare socket is mounted to the sandbox
DirectWayland bool `json:"direct_wayland,omitempty"`
// passwd username in container, defaults to passwd name of target uid or chronos
Username string `json:"username,omitempty"`
// absolute path to shell, empty for host shell
Shell string `json:"shell,omitempty"`
// absolute path to home directory in the init mount namespace
Data string `json:"data"`
// directory to enter and use as home in the container mount namespace, empty for Data
Dir string `json:"dir"`
// extra acl ops, dispatches before container init
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
// numerical application id, used for init user namespace credentials
Identity int `json:"identity"`
// list of supplementary groups inherited by container processes
Groups []string `json:"groups"`
// abstract container configuration baseline
Container *ContainerConfig `json:"container"`
Confinement ConfinementConfig `json:"confinement"`
}
// ConfinementConfig defines fortified child's confinement
type ConfinementConfig struct {
// numerical application id, determines uid in the init namespace
AppID int `json:"app_id"`
// list of supplementary groups to inherit
Groups []string `json:"groups"`
// passwd username in container, defaults to passwd name of target uid or chronos
Username string `json:"username,omitempty"`
// home directory in container, empty for outer
Inner string `json:"home_inner"`
// home directory in init namespace
Outer string `json:"home"`
// abstract sandbox configuration
Sandbox *SandboxConfig `json:"sandbox"`
// extra acl ops, runs after everything else
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
// reference to a system D-Bus proxy configuration,
// nil value disables system bus proxy
SystemBus *dbus.Config `json:"system_bus,omitempty"`
// reference to a session D-Bus proxy configuration,
// nil value makes session bus proxy assume built-in defaults
SessionBus *dbus.Config `json:"session_bus,omitempty"`
// system resources to expose to the container
Enablements system.Enablement `json:"enablements"`
}
// ExtraPermConfig describes an acl update op.
type ExtraPermConfig struct {
Ensure bool `json:"ensure,omitempty"`
Path string `json:"path"`
@ -81,3 +78,82 @@ func (e *ExtraPermConfig) String() string {
}
return string(buf)
}
// Template returns a fully populated instance of Config.
func Template() *Config {
return &Config{
ID: "org.chromium.Chromium",
Path: "/run/current-system/sw/bin/chromium",
Args: []string{
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland",
},
Confinement: ConfinementConfig{
AppID: 9,
Groups: []string{"video"},
Username: "chronos",
Outer: "/var/lib/persist/home/org.chromium.Chromium",
Inner: "/var/lib/fortify",
Sandbox: &SandboxConfig{
Hostname: "localhost",
Devel: true,
Userns: true,
Net: true,
Dev: true,
Seccomp: seccomp.FlagMultiarch,
Tty: true,
Multiarch: true,
MapRealUID: true,
DirectWayland: false,
// example API credentials pulled from Google Chrome
// DO NOT USE THESE IN A REAL BROWSER
Env: map[string]string{
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT",
},
Filesystem: []*FilesystemConfig{
{Src: "/nix/store"},
{Src: "/run/current-system"},
{Src: "/run/opengl-driver"},
{Src: "/var/db/nix-channels"},
{Src: "/var/lib/fortify/u0/org.chromium.Chromium",
Dst: "/data/data/org.chromium.Chromium", Write: true, Must: true},
{Src: "/dev/dri", Device: true},
},
Link: [][2]string{{"/run/user/65534", "/run/user/150"}},
Etc: "/etc",
AutoEtc: true,
Cover: []string{"/var/run/nscd"},
},
ExtraPerms: []*ExtraPermConfig{
{Path: "/var/lib/fortify/u0", Ensure: true, Execute: true},
{Path: "/var/lib/fortify/u0/org.chromium.Chromium", Read: true, Write: true, Execute: true},
},
SystemBus: &dbus.Config{
See: nil,
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
Own: nil,
Call: nil,
Broadcast: nil,
Log: false,
Filter: true,
},
SessionBus: &dbus.Config{
See: nil,
Talk: []string{"org.freedesktop.Notifications", "org.freedesktop.FileManager1", "org.freedesktop.ScreenSaver",
"org.freedesktop.secrets", "org.kde.kwalletd5", "org.kde.kwalletd6", "org.gnome.SessionManager"},
Own: []string{"org.chromium.Chromium.*", "org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"},
Call: map[string]string{"org.freedesktop.portal.*": "*"},
Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"},
Log: false,
Filter: true,
},
Enablements: system.EWayland | system.EDBus | system.EPulse,
},
}
}

View File

@ -1,59 +0,0 @@
package fst
import (
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
)
type (
// ContainerConfig describes the container configuration baseline to which the app implementation adds upon.
ContainerConfig struct {
// container hostname
Hostname string `json:"hostname,omitempty"`
// extra seccomp flags
Seccomp seccomp.FilterOpts `json:"seccomp"`
// allow ptrace and friends
Devel bool `json:"devel,omitempty"`
// allow userns creation in container
Userns bool `json:"userns,omitempty"`
// share host net namespace
Net bool `json:"net,omitempty"`
// allow dangerous terminal I/O
Tty bool `json:"tty,omitempty"`
// allow multiarch
Multiarch bool `json:"multiarch,omitempty"`
// initial process environment variables
Env map[string]string `json:"env"`
// map target user uid to privileged user uid in the user namespace
MapRealUID bool `json:"map_real_uid"`
// pass through all devices
Device bool `json:"device,omitempty"`
// container host filesystem bind mounts
Filesystem []*FilesystemConfig `json:"filesystem"`
// create symlinks inside container filesystem
Link [][2]string `json:"symlink"`
// read-only /etc directory
Etc string `json:"etc,omitempty"`
// automatically set up /etc symlinks
AutoEtc bool `json:"auto_etc"`
// cover these paths or create them if they do not already exist
Cover []string `json:"cover"`
}
// FilesystemConfig is an abstract representation of a bind mount.
FilesystemConfig struct {
// mount point in container, same as src if empty
Dst string `json:"dst,omitempty"`
// host filesystem path to make available to the container
Src string `json:"src"`
// do not mount filesystem read-only
Write bool `json:"write,omitempty"`
// do not disable device files
Device bool `json:"dev,omitempty"`
// fail if the bind mount cannot be established for any reason
Must bool `json:"require,omitempty"`
}
)

View File

@ -1,4 +1,4 @@
package app
package fst
import (
"crypto/rand"

View File

@ -1,22 +1,22 @@
package app_test
package fst_test
import (
"errors"
"testing"
. "git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/fst"
)
func TestParseAppID(t *testing.T) {
t.Run("bad length", func(t *testing.T) {
if err := ParseAppID(new(ID), "meow"); !errors.Is(err, ErrInvalidLength) {
t.Errorf("ParseAppID: error = %v, wantErr = %v", err, ErrInvalidLength)
if err := fst.ParseAppID(new(fst.ID), "meow"); !errors.Is(err, fst.ErrInvalidLength) {
t.Errorf("ParseAppID: error = %v, wantErr = %v", err, fst.ErrInvalidLength)
}
})
t.Run("bad byte", func(t *testing.T) {
wantErr := "invalid char '\\n' at byte 15"
if err := ParseAppID(new(ID), "02bc7f8936b2af6\n\ne2535cd71ef0bb7"); err == nil || err.Error() != wantErr {
if err := fst.ParseAppID(new(fst.ID), "02bc7f8936b2af6\n\ne2535cd71ef0bb7"); err == nil || err.Error() != wantErr {
t.Errorf("ParseAppID: error = %v, wantErr = %v", err, wantErr)
}
})
@ -30,30 +30,30 @@ func TestParseAppID(t *testing.T) {
func FuzzParseAppID(f *testing.F) {
for i := 0; i < 16; i++ {
id := new(ID)
if err := NewAppID(id); err != nil {
id := new(fst.ID)
if err := fst.NewAppID(id); err != nil {
panic(err.Error())
}
f.Add(id[0], id[1], id[2], id[3], id[4], id[5], id[6], id[7], id[8], id[9], id[10], id[11], id[12], id[13], id[14], id[15])
}
f.Fuzz(func(t *testing.T, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15 byte) {
testParseAppID(t, &ID{b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15})
testParseAppID(t, &fst.ID{b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15})
})
}
func testParseAppIDWithRandom(t *testing.T) {
id := new(ID)
if err := NewAppID(id); err != nil {
id := new(fst.ID)
if err := fst.NewAppID(id); err != nil {
t.Fatalf("cannot generate app ID: %v", err)
}
testParseAppID(t, id)
}
func testParseAppID(t *testing.T, id *ID) {
func testParseAppID(t *testing.T, id *fst.ID) {
s := id.String()
got := new(ID)
if err := ParseAppID(got, s); err != nil {
got := new(fst.ID)
if err := fst.ParseAppID(got, s); err != nil {
t.Fatalf("cannot parse app ID: %v", err)
}

View File

@ -1,4 +1,4 @@
package common
package fst
import (
"path/filepath"

View File

@ -1,4 +1,4 @@
package common
package fst
import (
"testing"

282
fst/sandbox.go Normal file
View File

@ -0,0 +1,282 @@
package fst
import (
"errors"
"fmt"
"io/fs"
"maps"
"path"
"slices"
"syscall"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
)
// SandboxConfig describes resources made available to the sandbox.
type (
SandboxConfig struct {
// container hostname
Hostname string `json:"hostname,omitempty"`
// extra seccomp flags
Seccomp seccomp.SyscallOpts `json:"seccomp"`
// allow ptrace and friends
Devel bool `json:"devel,omitempty"`
// allow userns creation in container
Userns bool `json:"userns,omitempty"`
// share host net namespace
Net bool `json:"net,omitempty"`
// expose main process tty
Tty bool `json:"tty,omitempty"`
// allow multiarch
Multiarch bool `json:"multiarch,omitempty"`
// initial process environment variables
Env map[string]string `json:"env"`
// map target user uid to privileged user uid in the user namespace
MapRealUID bool `json:"map_real_uid"`
// expose all devices
Dev bool `json:"dev,omitempty"`
// container host filesystem bind mounts
Filesystem []*FilesystemConfig `json:"filesystem"`
// create symlinks inside container filesystem
Link [][2]string `json:"symlink"`
// direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1
// and the bare socket is mounted to the sandbox
DirectWayland bool `json:"direct_wayland,omitempty"`
// read-only /etc directory
Etc string `json:"etc,omitempty"`
// automatically set up /etc symlinks
AutoEtc bool `json:"auto_etc"`
// cover these paths or create them if they do not already exist
Cover []string `json:"cover"`
}
// SandboxSys encapsulates system functions used during [sandbox.Container] initialisation.
SandboxSys interface {
Getuid() int
Getgid() int
Paths() Paths
ReadDir(name string) ([]fs.DirEntry, error)
EvalSymlinks(path string) (string, error)
Println(v ...any)
Printf(format string, v ...any)
}
// FilesystemConfig is a representation of [sandbox.BindMount].
FilesystemConfig struct {
// mount point in container, same as src if empty
Dst string `json:"dst,omitempty"`
// host filesystem path to make available to the container
Src string `json:"src"`
// do not mount filesystem read-only
Write bool `json:"write,omitempty"`
// do not disable device files
Device bool `json:"dev,omitempty"`
// fail if the bind mount cannot be established for any reason
Must bool `json:"require,omitempty"`
}
)
// ToContainer initialises [sandbox.Params] via [SandboxConfig].
// Note that remaining container setup must be queued by the [App] implementation.
func (s *SandboxConfig) ToContainer(sys SandboxSys, uid, gid *int) (*sandbox.Params, map[string]string, error) {
if s == nil {
return nil, nil, syscall.EBADE
}
container := &sandbox.Params{
Hostname: s.Hostname,
Ops: new(sandbox.Ops),
Seccomp: s.Seccomp,
}
/* this is only 4 KiB of memory on a 64-bit system,
permissive defaults on NixOS results in around 100 entries
so this capacity should eliminate copies for most setups */
*container.Ops = slices.Grow(*container.Ops, 1<<8)
if s.Devel {
container.Flags |= sandbox.FAllowDevel
}
if s.Userns {
container.Flags |= sandbox.FAllowUserns
}
if s.Net {
container.Flags |= sandbox.FAllowNet
}
if s.Tty {
container.Flags |= sandbox.FAllowTTY
}
if s.MapRealUID {
/* some programs fail to connect to dbus session running as a different uid
so this workaround is introduced to map priv-side caller uid in container */
container.Uid = sys.Getuid()
*uid = container.Uid
container.Gid = sys.Getgid()
*gid = container.Gid
} else {
*uid = sandbox.OverflowUid()
*gid = sandbox.OverflowGid()
}
container.
Proc("/proc").
Tmpfs(Tmp, 1<<12, 0755)
if !s.Dev {
container.Dev("/dev").Mqueue("/dev/mqueue")
} else {
container.Bind("/dev", "/dev", sandbox.BindDevice)
}
/* retrieve paths and hide them if they're made available in the sandbox;
this feature tries to improve user experience of permissive defaults, and
to warn about issues in custom configuration; it is NOT a security feature
and should not be treated as such, ALWAYS be careful with what you bind */
var hidePaths []string
sc := sys.Paths()
hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath)
_, systemBusAddr := dbus.Address()
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
return nil, nil, err
} else {
// there is usually only one, do not preallocate
for _, entry := range entries {
if entry.Method != "unix" {
continue
}
for _, pair := range entry.Values {
if pair[0] == "path" {
if path.IsAbs(pair[1]) {
// get parent dir of socket
dir := path.Dir(pair[1])
if dir == "." || dir == "/" {
sys.Printf("dbus socket %q is in an unusual location", pair[1])
}
hidePaths = append(hidePaths, dir)
} else {
sys.Printf("dbus socket %q is not absolute", pair[1])
}
}
}
}
}
hidePathMatch := make([]bool, len(hidePaths))
for i := range hidePaths {
if err := evalSymlinks(sys, &hidePaths[i]); err != nil {
return nil, nil, err
}
}
for _, c := range s.Filesystem {
if c == nil {
continue
}
if !path.IsAbs(c.Src) {
return nil, nil, fmt.Errorf("src path %q is not absolute", c.Src)
}
dest := c.Dst
if c.Dst == "" {
dest = c.Src
} else if !path.IsAbs(dest) {
return nil, nil, fmt.Errorf("dst path %q is not absolute", dest)
}
srcH := c.Src
if err := evalSymlinks(sys, &srcH); err != nil {
return nil, nil, err
}
for i := range hidePaths {
// skip matched entries
if hidePathMatch[i] {
continue
}
if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
return nil, nil, err
} else if ok {
hidePathMatch[i] = true
sys.Printf("hiding paths from %q", c.Src)
}
}
var flags int
if c.Write {
flags |= sandbox.BindWritable
}
if c.Device {
flags |= sandbox.BindDevice | sandbox.BindWritable
}
if !c.Must {
flags |= sandbox.BindOptional
}
container.Bind(c.Src, dest, flags)
}
// cover matched paths
for i, ok := range hidePathMatch {
if ok {
container.Tmpfs(hidePaths[i], 1<<13, 0755)
}
}
for _, l := range s.Link {
container.Link(l[0], l[1])
}
// perf: this might work better if implemented as a setup op in container init
if !s.AutoEtc {
if s.Etc != "" {
container.Bind(s.Etc, "/etc", 0)
}
} else {
etcPath := s.Etc
if etcPath == "" {
etcPath = "/etc"
}
container.Bind(etcPath, Tmp+"/etc", 0)
// link host /etc contents to prevent dropping passwd/group bind mounts
if d, err := sys.ReadDir(etcPath); err != nil {
return nil, nil, err
} else {
for _, ent := range d {
n := ent.Name()
switch n {
case "passwd":
case "group":
case "mtab":
container.Link("/proc/mounts", "/etc/"+n)
default:
container.Link(Tmp+"/etc/"+n, "/etc/"+n)
}
}
}
}
return container, maps.Clone(s.Env), nil
}
func evalSymlinks(sys SandboxSys, v *string) error {
if p, err := sys.EvalSymlinks(*v); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return err
}
sys.Printf("path %q does not yet exist", *v)
} else {
*v = p
}
return nil
}

View File

@ -1,91 +0,0 @@
package fst
import (
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
"git.gensokyo.uk/security/fortify/system"
)
// Template returns a fully populated instance of Config.
func Template() *Config {
return &Config{
ID: "org.chromium.Chromium",
Path: "/run/current-system/sw/bin/chromium",
Args: []string{
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland",
},
Enablements: system.EWayland | system.EDBus | system.EPulse,
SessionBus: &dbus.Config{
See: nil,
Talk: []string{"org.freedesktop.Notifications", "org.freedesktop.FileManager1", "org.freedesktop.ScreenSaver",
"org.freedesktop.secrets", "org.kde.kwalletd5", "org.kde.kwalletd6", "org.gnome.SessionManager"},
Own: []string{"org.chromium.Chromium.*", "org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"},
Call: map[string]string{"org.freedesktop.portal.*": "*"},
Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"},
Log: false,
Filter: true,
},
SystemBus: &dbus.Config{
See: nil,
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
Own: nil,
Call: nil,
Broadcast: nil,
Log: false,
Filter: true,
},
DirectWayland: false,
Username: "chronos",
Shell: "/run/current-system/sw/bin/zsh",
Data: "/var/lib/fortify/u0/org.chromium.Chromium",
Dir: "/data/data/org.chromium.Chromium",
ExtraPerms: []*ExtraPermConfig{
{Path: "/var/lib/fortify/u0", Ensure: true, Execute: true},
{Path: "/var/lib/fortify/u0/org.chromium.Chromium", Read: true, Write: true, Execute: true},
},
Identity: 9,
Groups: []string{"video", "dialout", "plugdev"},
Container: &ContainerConfig{
Hostname: "localhost",
Devel: true,
Userns: true,
Net: true,
Device: true,
Seccomp: seccomp.FilterMultiarch,
Tty: true,
Multiarch: true,
MapRealUID: true,
// example API credentials pulled from Google Chrome
// DO NOT USE THESE IN A REAL BROWSER
Env: map[string]string{
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT",
},
Filesystem: []*FilesystemConfig{
{Src: "/nix/store"},
{Src: "/run/current-system"},
{Src: "/run/opengl-driver"},
{Src: "/var/db/nix-channels"},
{Src: "/var/lib/fortify/u0/org.chromium.Chromium",
Dst: "/data/data/org.chromium.Chromium", Write: true, Must: true},
{Src: "/dev/dri", Device: true},
},
Link: [][2]string{{"/run/user/65534", "/run/user/150"}},
Etc: "/etc",
AutoEtc: true,
Cover: []string{"/var/run/nscd"},
},
}
}

View File

@ -1,140 +0,0 @@
package fst_test
import (
"encoding/json"
"testing"
"git.gensokyo.uk/security/fortify/fst"
)
func TestTemplate(t *testing.T) {
const want = `{
"id": "org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"enablements": 13,
"session_bus": {
"see": null,
"talk": [
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
],
"own": [
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"
],
"call": {
"org.freedesktop.portal.*": "*"
},
"broadcast": {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
},
"filter": true
},
"system_bus": {
"see": null,
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"own": null,
"call": null,
"broadcast": null,
"filter": true
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"data": "/var/lib/fortify/u0/org.chromium.Chromium",
"dir": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
"path": "/var/lib/fortify/u0",
"x": true
},
{
"path": "/var/lib/fortify/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
}
],
"identity": 9,
"groups": [
"video",
"dialout",
"plugdev"
],
"container": {
"hostname": "localhost",
"seccomp": 32,
"devel": true,
"userns": true,
"net": true,
"tty": true,
"multiarch": true,
"env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"map_real_uid": true,
"device": true,
"filesystem": [
{
"src": "/nix/store"
},
{
"src": "/run/current-system"
},
{
"src": "/run/opengl-driver"
},
{
"src": "/var/db/nix-channels"
},
{
"dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/fortify/u0/org.chromium.Chromium",
"write": true,
"require": true
},
{
"src": "/dev/dri",
"dev": true
}
],
"symlink": [
[
"/run/user/65534",
"/run/user/150"
]
],
"etc": "/etc",
"auto_etc": true,
"cover": [
"/var/run/nscd"
]
}
}`
if p, err := json.MarshalIndent(fst.Template(), "", "\t"); err != nil {
t.Fatalf("cannot marshal: %v", err)
} else if s := string(p); s != want {
t.Fatalf("Template:\n%s\nwant:\n%s",
s, want)
}
}

View File

@ -1,17 +1,38 @@
package helper
import (
"bytes"
"errors"
"io"
"syscall"
"strings"
)
type argsWt [][]byte
var (
ErrContainsNull = errors.New("argument contains null character")
)
type argsWt []string
// checks whether any element contains the null character
// must be called before args use and args must not be modified after call
func (a argsWt) check() error {
for _, arg := range a {
for _, b := range arg {
if b == '\x00' {
return ErrContainsNull
}
}
}
return nil
}
func (a argsWt) WriteTo(w io.Writer) (int64, error) {
// assuming already checked
nt := 0
// write null terminated arguments
for _, arg := range a {
n, err := w.Write(arg)
n, err := w.Write([]byte(arg + "\x00"))
nt += n
if err != nil {
@ -23,32 +44,18 @@ func (a argsWt) WriteTo(w io.Writer) (int64, error) {
}
func (a argsWt) String() string {
return string(
bytes.TrimSuffix(
bytes.ReplaceAll(
bytes.Join(a, nil),
[]byte{0}, []byte{' '},
),
[]byte{' '},
),
)
return strings.Join(a, " ")
}
// NewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
func NewCheckedArgs(args []string) (wt io.WriterTo, err error) {
a := make(argsWt, len(args))
for i, arg := range args {
a[i], err = syscall.ByteSliceFromString(arg)
if err != nil {
return
}
}
wt = a
return
// NewCheckedArgs returns a checked argument writer for args.
// Callers must not retain any references to args.
func NewCheckedArgs(args []string) (io.WriterTo, error) {
a := argsWt(args)
return a, a.check()
}
// MustNewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
// If s contains a NUL byte this function panics instead of returning an error.
// MustNewCheckedArgs returns a checked argument writer for args and panics if check fails.
// Callers must not retain any references to args.
func MustNewCheckedArgs(args []string) io.WriterTo {
a, err := NewCheckedArgs(args)
if err != nil {

View File

@ -4,33 +4,34 @@ import (
"errors"
"fmt"
"strings"
"syscall"
"testing"
"git.gensokyo.uk/security/fortify/helper"
)
func TestArgsString(t *testing.T) {
func Test_argsFd_String(t *testing.T) {
wantString := strings.Join(wantArgs, " ")
if got := argsWt.(fmt.Stringer).String(); got != wantString {
t.Errorf("String: %q, want %q",
t.Errorf("String(): got %v; want %v",
got, wantString)
}
}
func TestNewCheckedArgs(t *testing.T) {
args := []string{"\x00"}
if _, err := helper.NewCheckedArgs(args); !errors.Is(err, syscall.EINVAL) {
t.Errorf("NewCheckedArgs: error = %v, wantErr %v",
err, syscall.EINVAL)
if _, err := helper.NewCheckedArgs(args); !errors.Is(err, helper.ErrContainsNull) {
t.Errorf("NewCheckedArgs(%q) error = %v, wantErr %v",
args,
err, helper.ErrContainsNull)
}
t.Run("must panic", func(t *testing.T) {
badPayload := []string{"\x00"}
defer func() {
wantPanic := "invalid argument"
wantPanic := "argument contains null character"
if r := recover(); r != wantPanic {
t.Errorf("MustNewCheckedArgs: panic = %v, wantPanic %v",
t.Errorf("MustNewCheckedArgs(%q) panic = %v, wantPanic %v",
badPayload,
r, wantPanic)
}
}()

View File

@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"
"testing"
@ -56,8 +55,8 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput f
t.Run("start helper with status channel and wait", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
stdout := new(strings.Builder)
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, true)
stdout, stderr := new(strings.Builder), new(strings.Builder)
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, stderr }, true)
t.Run("wait not yet started helper", func(t *testing.T) {
defer func() {
@ -89,8 +88,8 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput f
t.Log("waiting on helper")
if err := h.Wait(); !errors.Is(err, context.Canceled) {
t.Errorf("Wait: error = %v",
err)
t.Errorf("Wait() err = %v stderr = %s",
err, stderr)
}
t.Run("wait already finalised helper", func(t *testing.T) {
@ -102,8 +101,8 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput f
}
})
if got := trimStdout(stdout); got != wantPayload {
t.Errorf("Start: stdout = %q, want %q",
if got := stderr.String(); got != wantPayload {
t.Errorf("Start: stderr = %v, want %v",
got, wantPayload)
}
})
@ -111,27 +110,23 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput f
t.Run("start helper and wait", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
stdout := new(strings.Builder)
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, false)
stdout, stderr := new(strings.Builder), new(strings.Builder)
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, stderr }, false)
if err := h.Start(); err != nil {
t.Errorf("Start: error = %v",
t.Errorf("Start() error = %v",
err)
return
}
if err := h.Wait(); err != nil {
t.Errorf("Wait: error = %v stdout = %q",
err, stdout)
t.Errorf("Wait() err = %v stdout = %s stderr = %s",
err, stdout, stderr)
}
if got := trimStdout(stdout); got != wantPayload {
t.Errorf("Start: stdout = %q, want %q",
if got := stderr.String(); got != wantPayload {
t.Errorf("Start() stderr = %v, want %v",
got, wantPayload)
}
})
}
func trimStdout(stdout fmt.Stringer) string {
return strings.TrimPrefix(stdout.String(), "=== RUN TestHelperInit\n")
}

View File

@ -63,7 +63,7 @@ func flagRestoreFiles(offset int, ap, sp string) (argsFile, statFile *os.File) {
func genericStub(argsFile, statFile *os.File) {
if argsFile != nil {
// this output is checked by parent
if _, err := io.Copy(os.Stdout, argsFile); err != nil {
if _, err := io.Copy(os.Stderr, argsFile); err != nil {
panic("cannot read args: " + err.Error())
}
}

View File

@ -1,59 +1,82 @@
// Package app defines the generic [App] interface.
package app
import (
"syscall"
"time"
"context"
"fmt"
"log"
"sync"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/sys"
)
type App interface {
// ID returns a copy of [ID] held by App.
ID() ID
func New(ctx context.Context, os sys.State) (fst.App, error) {
a := new(app)
a.sys = os
a.ctx = ctx
// Seal determines the outcome of config as a [SealedApp].
// The value of config might be overwritten and must not be used again.
Seal(config *fst.Config) (SealedApp, error)
id := new(fst.ID)
err := fst.NewAppID(id)
a.id = newID(id)
String() string
return a, err
}
type SealedApp interface {
// Run commits sealed system setup and starts the app process.
Run(rs *RunState) error
func MustNew(ctx context.Context, os sys.State) fst.App {
a, err := New(ctx, os)
if err != nil {
log.Fatalf("cannot create app: %v", err)
}
return a
}
// RunState stores the outcome of a call to [SealedApp.Run].
type RunState struct {
// Time is the exact point in time where the process was created.
// Location must be set to UTC.
//
// Time is nil if no process was ever created.
Time *time.Time
// RevertErr is stored by the deferred revert call.
RevertErr error
// WaitErr is the generic error value created by the standard library.
WaitErr error
type app struct {
id *stringPair[fst.ID]
sys sys.State
ctx context.Context
syscall.WaitStatus
*outcome
mu sync.RWMutex
}
// SetStart stores the current time in [RunState] once.
func (rs *RunState) SetStart() {
if rs.Time != nil {
panic("attempted to store time twice")
}
now := time.Now().UTC()
rs.Time = &now
func (a *app) ID() fst.ID { a.mu.RLock(); defer a.mu.RUnlock(); return a.id.unwrap() }
func (a *app) String() string {
if a == nil {
return "(invalid app)"
}
// Paths contains environment-dependent paths used by fortify.
type Paths struct {
// path to shared directory (usually `/tmp/fortify.%d`)
SharePath string `json:"share_path"`
// XDG_RUNTIME_DIR value (usually `/run/user/%d`)
RuntimePath string `json:"runtime_path"`
// application runtime directory (usually `/run/user/%d/fortify`)
RunDirPath string `json:"run_dir_path"`
a.mu.RLock()
defer a.mu.RUnlock()
if a.outcome != nil {
if a.outcome.user.uid == nil {
return fmt.Sprintf("(sealed app %s with invalid uid)", a.id)
}
return fmt.Sprintf("(sealed app %s as uid %s)", a.id, a.outcome.user.uid)
}
return fmt.Sprintf("(unsealed app %s)", a.id)
}
func (a *app) Seal(config *fst.Config) (fst.SealedApp, error) {
a.mu.Lock()
defer a.mu.Unlock()
if a.outcome != nil {
panic("app sealed twice")
}
if config == nil {
return nil, fmsg.WrapError(ErrConfig,
"attempted to seal app with nil config")
}
seal := new(outcome)
seal.id = a.id
err := seal.finalise(a.ctx, a.sys, config)
if err == nil {
a.outcome = seal
}
return seal, err
}

View File

@ -0,0 +1,219 @@
package app_test
import (
"git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system"
)
var testCasesNixos = []sealTestCase{
{
"nixos chromium direct wayland", new(stubNixOS),
&fst.Config{
ID: "org.chromium.Chromium",
Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
Confinement: fst.ConfinementConfig{
AppID: 1, Groups: []string{}, Username: "u0_a1",
Outer: "/var/lib/persist/module/fortify/0/1",
Sandbox: &fst.SandboxConfig{
Userns: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, AutoEtc: true,
Filesystem: []*fst.FilesystemConfig{
{Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true},
{Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true},
{Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"},
{Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true},
},
Cover: []string{"/var/run/nscd"},
},
SystemBus: &dbus.Config{
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
Filter: true,
},
SessionBus: &dbus.Config{
Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
"org.kde.kwalletd5", "org.kde.kwalletd6",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{}, Broadcast: map[string]string{},
Filter: true,
},
Enablements: system.EWayland | system.EDBus | system.EPulse,
},
},
fst.ID{
0x8e, 0x2c, 0x76, 0xb0,
0x66, 0xda, 0xbe, 0x57,
0x4c, 0xf0, 0x73, 0xbd,
0xb4, 0x6e, 0xb5, 0xc1,
},
system.New(1000001).
Ensure("/tmp/fortify.1971", 0711).
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, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1", 0711).
Ephemeral(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/1", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/1", acl.Read, acl.Write, acl.Execute).
UpdatePermType(system.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute).
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse").
CopyFile(nil, "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
MustProxyDBus("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", &dbus.Config{
Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
"org.kde.kwalletd5", "org.kde.kwalletd6",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{}, Broadcast: map[string]string{},
Filter: true,
}, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
}).
UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
&sandbox.Params{
Uid: 1971,
Gid: 100,
Flags: sandbox.FAllowNet | sandbox.FAllowUserns,
Dir: "/var/lib/persist/module/fortify/0/1",
Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
Env: []string{
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
"HOME=/var/lib/persist/module/fortify/0/1",
"PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie",
"PULSE_SERVER=unix:/run/user/1971/pulse/native",
"TERM=xterm-256color",
"USER=u0_a1",
"WAYLAND_DISPLAY=wayland-0",
"XDG_RUNTIME_DIR=/run/user/1971",
"XDG_SESSION_CLASS=user",
"XDG_SESSION_TYPE=tty",
},
Ops: new(sandbox.Ops).
Proc("/proc").
Tmpfs(fst.Tmp, 4096, 0755).
Dev("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", 0).
Bind("/usr/bin", "/usr/bin", 0).
Bind("/nix/store", "/nix/store", 0).
Bind("/run/current-system", "/run/current-system", 0).
Bind("/sys/block", "/sys/block", sandbox.BindOptional).
Bind("/sys/bus", "/sys/bus", sandbox.BindOptional).
Bind("/sys/class", "/sys/class", sandbox.BindOptional).
Bind("/sys/dev", "/sys/dev", sandbox.BindOptional).
Bind("/sys/devices", "/sys/devices", sandbox.BindOptional).
Bind("/run/opengl-driver", "/run/opengl-driver", 0).
Bind("/dev/dri", "/dev/dri", sandbox.BindDevice|sandbox.BindWritable|sandbox.BindOptional).
Bind("/etc", fst.Tmp+"/etc", 0).
Link(fst.Tmp+"/etc/alsa", "/etc/alsa").
Link(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
Link(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
Link(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
Link(fst.Tmp+"/etc/default", "/etc/default").
Link(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
Link(fst.Tmp+"/etc/fonts", "/etc/fonts").
Link(fst.Tmp+"/etc/fstab", "/etc/fstab").
Link(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
Link(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
Link(fst.Tmp+"/etc/hostid", "/etc/hostid").
Link(fst.Tmp+"/etc/hostname", "/etc/hostname").
Link(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Link(fst.Tmp+"/etc/hosts", "/etc/hosts").
Link(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
Link(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
Link(fst.Tmp+"/etc/issue", "/etc/issue").
Link(fst.Tmp+"/etc/kbd", "/etc/kbd").
Link(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
Link(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
Link(fst.Tmp+"/etc/localtime", "/etc/localtime").
Link(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
Link(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
Link(fst.Tmp+"/etc/lvm", "/etc/lvm").
Link(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
Link(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
Link(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
Link(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
Link("/proc/mounts", "/etc/mtab").
Link(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
Link(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
Link(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
Link(fst.Tmp+"/etc/nix", "/etc/nix").
Link(fst.Tmp+"/etc/nixos", "/etc/nixos").
Link(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
Link(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
Link(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
Link(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
Link(fst.Tmp+"/etc/os-release", "/etc/os-release").
Link(fst.Tmp+"/etc/pam", "/etc/pam").
Link(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
Link(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
Link(fst.Tmp+"/etc/pki", "/etc/pki").
Link(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
Link(fst.Tmp+"/etc/profile", "/etc/profile").
Link(fst.Tmp+"/etc/protocols", "/etc/protocols").
Link(fst.Tmp+"/etc/qemu", "/etc/qemu").
Link(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
Link(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
Link(fst.Tmp+"/etc/rpc", "/etc/rpc").
Link(fst.Tmp+"/etc/samba", "/etc/samba").
Link(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
Link(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
Link(fst.Tmp+"/etc/services", "/etc/services").
Link(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
Link(fst.Tmp+"/etc/shadow", "/etc/shadow").
Link(fst.Tmp+"/etc/shells", "/etc/shells").
Link(fst.Tmp+"/etc/ssh", "/etc/ssh").
Link(fst.Tmp+"/etc/ssl", "/etc/ssl").
Link(fst.Tmp+"/etc/static", "/etc/static").
Link(fst.Tmp+"/etc/subgid", "/etc/subgid").
Link(fst.Tmp+"/etc/subuid", "/etc/subuid").
Link(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
Link(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
Link(fst.Tmp+"/etc/systemd", "/etc/systemd").
Link(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
Link(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
Link(fst.Tmp+"/etc/udev", "/etc/udev").
Link(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
Link(fst.Tmp+"/etc/UPower", "/etc/UPower").
Link(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
Link(fst.Tmp+"/etc/X11", "/etc/X11").
Link(fst.Tmp+"/etc/zfs", "/etc/zfs").
Link(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
Link(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
Link(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Tmpfs("/run/user", 4096, 0755).
Tmpfs("/run/user/1971", 8388608, 0700).
Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", sandbox.BindWritable).
Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", sandbox.BindWritable).
Place("/etc/passwd", []byte("u0_a1:x:1971:100:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).
Place("/etc/group", []byte("fortify:x:100:\n")).
Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0", 0).
Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native", 0).
Place(fst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus", 0).
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket", 0).
Tmpfs("/var/run/nscd", 8192, 0755),
},
},
}

382
internal/app/app_pd_test.go Normal file
View File

@ -0,0 +1,382 @@
package app_test
import (
"os"
"git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system"
)
var testCasesPd = []sealTestCase{
{
"nixos permissive defaults no enablements", new(stubNixOS),
&fst.Config{
Confinement: fst.ConfinementConfig{
AppID: 0,
Username: "chronos",
Outer: "/home/chronos",
},
},
fst.ID{
0x4a, 0x45, 0x0b, 0x65,
0x96, 0xd7, 0xbc, 0x15,
0xbd, 0x01, 0x78, 0x0e,
0xb9, 0xa6, 0x07, 0xac,
},
system.New(1000000).
Ensure("/tmp/fortify.1971", 0711).
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, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac", 0711).
Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
&sandbox.Params{
Flags: sandbox.FAllowNet | sandbox.FAllowUserns | sandbox.FAllowTTY,
Dir: "/home/chronos",
Path: "/run/current-system/sw/bin/zsh",
Args: []string{"/run/current-system/sw/bin/zsh"},
Env: []string{
"HOME=/home/chronos",
"TERM=xterm-256color",
"USER=chronos",
"XDG_RUNTIME_DIR=/run/user/65534",
"XDG_SESSION_CLASS=user",
"XDG_SESSION_TYPE=tty",
},
Ops: new(sandbox.Ops).
Proc("/proc").
Tmpfs(fst.Tmp, 4096, 0755).
Dev("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", sandbox.BindWritable).
Bind("/boot", "/boot", sandbox.BindWritable).
Bind("/home", "/home", sandbox.BindWritable).
Bind("/lib", "/lib", sandbox.BindWritable).
Bind("/lib64", "/lib64", sandbox.BindWritable).
Bind("/nix", "/nix", sandbox.BindWritable).
Bind("/root", "/root", sandbox.BindWritable).
Bind("/run", "/run", sandbox.BindWritable).
Bind("/srv", "/srv", sandbox.BindWritable).
Bind("/sys", "/sys", sandbox.BindWritable).
Bind("/usr", "/usr", sandbox.BindWritable).
Bind("/var", "/var", sandbox.BindWritable).
Bind("/dev/kvm", "/dev/kvm", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
Tmpfs("/run/user/1971", 8192, 0755).
Tmpfs("/run/dbus", 8192, 0755).
Bind("/etc", fst.Tmp+"/etc", 0).
Link(fst.Tmp+"/etc/alsa", "/etc/alsa").
Link(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
Link(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
Link(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
Link(fst.Tmp+"/etc/default", "/etc/default").
Link(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
Link(fst.Tmp+"/etc/fonts", "/etc/fonts").
Link(fst.Tmp+"/etc/fstab", "/etc/fstab").
Link(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
Link(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
Link(fst.Tmp+"/etc/hostid", "/etc/hostid").
Link(fst.Tmp+"/etc/hostname", "/etc/hostname").
Link(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Link(fst.Tmp+"/etc/hosts", "/etc/hosts").
Link(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
Link(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
Link(fst.Tmp+"/etc/issue", "/etc/issue").
Link(fst.Tmp+"/etc/kbd", "/etc/kbd").
Link(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
Link(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
Link(fst.Tmp+"/etc/localtime", "/etc/localtime").
Link(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
Link(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
Link(fst.Tmp+"/etc/lvm", "/etc/lvm").
Link(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
Link(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
Link(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
Link(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
Link("/proc/mounts", "/etc/mtab").
Link(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
Link(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
Link(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
Link(fst.Tmp+"/etc/nix", "/etc/nix").
Link(fst.Tmp+"/etc/nixos", "/etc/nixos").
Link(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
Link(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
Link(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
Link(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
Link(fst.Tmp+"/etc/os-release", "/etc/os-release").
Link(fst.Tmp+"/etc/pam", "/etc/pam").
Link(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
Link(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
Link(fst.Tmp+"/etc/pki", "/etc/pki").
Link(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
Link(fst.Tmp+"/etc/profile", "/etc/profile").
Link(fst.Tmp+"/etc/protocols", "/etc/protocols").
Link(fst.Tmp+"/etc/qemu", "/etc/qemu").
Link(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
Link(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
Link(fst.Tmp+"/etc/rpc", "/etc/rpc").
Link(fst.Tmp+"/etc/samba", "/etc/samba").
Link(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
Link(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
Link(fst.Tmp+"/etc/services", "/etc/services").
Link(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
Link(fst.Tmp+"/etc/shadow", "/etc/shadow").
Link(fst.Tmp+"/etc/shells", "/etc/shells").
Link(fst.Tmp+"/etc/ssh", "/etc/ssh").
Link(fst.Tmp+"/etc/ssl", "/etc/ssl").
Link(fst.Tmp+"/etc/static", "/etc/static").
Link(fst.Tmp+"/etc/subgid", "/etc/subgid").
Link(fst.Tmp+"/etc/subuid", "/etc/subuid").
Link(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
Link(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
Link(fst.Tmp+"/etc/systemd", "/etc/systemd").
Link(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
Link(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
Link(fst.Tmp+"/etc/udev", "/etc/udev").
Link(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
Link(fst.Tmp+"/etc/UPower", "/etc/UPower").
Link(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
Link(fst.Tmp+"/etc/X11", "/etc/X11").
Link(fst.Tmp+"/etc/zfs", "/etc/zfs").
Link(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
Link(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
Link(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Tmpfs("/run/user", 4096, 0755).
Tmpfs("/run/user/65534", 8388608, 0700).
Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", sandbox.BindWritable).
Bind("/home/chronos", "/home/chronos", sandbox.BindWritable).
Place("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
Place("/etc/group", []byte("fortify:x:65534:\n")).
Tmpfs("/var/run/nscd", 8192, 0755),
},
},
{
"nixos permissive defaults chromium", new(stubNixOS),
&fst.Config{
ID: "org.chromium.Chromium",
Args: []string{"zsh", "-c", "exec chromium "},
Confinement: fst.ConfinementConfig{
AppID: 9,
Groups: []string{"video"},
Username: "chronos",
Outer: "/home/chronos",
SessionBus: &dbus.Config{
Talk: []string{
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{
"org.freedesktop.portal.*": "*",
},
Broadcast: map[string]string{
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
},
Filter: true,
},
SystemBus: &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
},
Enablements: system.EWayland | system.EDBus | system.EPulse,
},
},
fst.ID{
0xeb, 0xf0, 0x83, 0xd1,
0xb1, 0x75, 0x91, 0x17,
0x82, 0xd4, 0x13, 0x36,
0x9b, 0x64, 0xce, 0x7c,
},
system.New(1000009).
Ensure("/tmp/fortify.1971", 0711).
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, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0711).
Ephemeral(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/9", acl.Read, acl.Write, acl.Execute).
Ensure("/tmp/fortify.1971/wayland", 0711).
Wayland(new(*os.File), "/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse").
CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
MustProxyDBus("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", &dbus.Config{
Talk: []string{
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{
"org.freedesktop.portal.*": "*",
},
Broadcast: map[string]string{
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
},
Filter: true,
}, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
}).
UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
&sandbox.Params{
Flags: sandbox.FAllowNet | sandbox.FAllowUserns | sandbox.FAllowTTY,
Dir: "/home/chronos",
Path: "/run/current-system/sw/bin/zsh",
Args: []string{"zsh", "-c", "exec chromium "},
Env: []string{
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus",
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
"HOME=/home/chronos",
"PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie",
"PULSE_SERVER=unix:/run/user/65534/pulse/native",
"TERM=xterm-256color",
"USER=chronos",
"WAYLAND_DISPLAY=wayland-0",
"XDG_RUNTIME_DIR=/run/user/65534",
"XDG_SESSION_CLASS=user",
"XDG_SESSION_TYPE=tty",
},
Ops: new(sandbox.Ops).
Proc("/proc").
Tmpfs(fst.Tmp, 4096, 0755).
Dev("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", sandbox.BindWritable).
Bind("/boot", "/boot", sandbox.BindWritable).
Bind("/home", "/home", sandbox.BindWritable).
Bind("/lib", "/lib", sandbox.BindWritable).
Bind("/lib64", "/lib64", sandbox.BindWritable).
Bind("/nix", "/nix", sandbox.BindWritable).
Bind("/root", "/root", sandbox.BindWritable).
Bind("/run", "/run", sandbox.BindWritable).
Bind("/srv", "/srv", sandbox.BindWritable).
Bind("/sys", "/sys", sandbox.BindWritable).
Bind("/usr", "/usr", sandbox.BindWritable).
Bind("/var", "/var", sandbox.BindWritable).
Bind("/dev/dri", "/dev/dri", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
Bind("/dev/kvm", "/dev/kvm", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
Tmpfs("/run/user/1971", 8192, 0755).
Tmpfs("/run/dbus", 8192, 0755).
Bind("/etc", fst.Tmp+"/etc", 0).
Link(fst.Tmp+"/etc/alsa", "/etc/alsa").
Link(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
Link(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
Link(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
Link(fst.Tmp+"/etc/default", "/etc/default").
Link(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
Link(fst.Tmp+"/etc/fonts", "/etc/fonts").
Link(fst.Tmp+"/etc/fstab", "/etc/fstab").
Link(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
Link(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
Link(fst.Tmp+"/etc/hostid", "/etc/hostid").
Link(fst.Tmp+"/etc/hostname", "/etc/hostname").
Link(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Link(fst.Tmp+"/etc/hosts", "/etc/hosts").
Link(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
Link(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
Link(fst.Tmp+"/etc/issue", "/etc/issue").
Link(fst.Tmp+"/etc/kbd", "/etc/kbd").
Link(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
Link(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
Link(fst.Tmp+"/etc/localtime", "/etc/localtime").
Link(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
Link(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
Link(fst.Tmp+"/etc/lvm", "/etc/lvm").
Link(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
Link(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
Link(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
Link(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
Link("/proc/mounts", "/etc/mtab").
Link(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
Link(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
Link(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
Link(fst.Tmp+"/etc/nix", "/etc/nix").
Link(fst.Tmp+"/etc/nixos", "/etc/nixos").
Link(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
Link(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
Link(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
Link(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
Link(fst.Tmp+"/etc/os-release", "/etc/os-release").
Link(fst.Tmp+"/etc/pam", "/etc/pam").
Link(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
Link(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
Link(fst.Tmp+"/etc/pki", "/etc/pki").
Link(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
Link(fst.Tmp+"/etc/profile", "/etc/profile").
Link(fst.Tmp+"/etc/protocols", "/etc/protocols").
Link(fst.Tmp+"/etc/qemu", "/etc/qemu").
Link(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
Link(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
Link(fst.Tmp+"/etc/rpc", "/etc/rpc").
Link(fst.Tmp+"/etc/samba", "/etc/samba").
Link(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
Link(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
Link(fst.Tmp+"/etc/services", "/etc/services").
Link(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
Link(fst.Tmp+"/etc/shadow", "/etc/shadow").
Link(fst.Tmp+"/etc/shells", "/etc/shells").
Link(fst.Tmp+"/etc/ssh", "/etc/ssh").
Link(fst.Tmp+"/etc/ssl", "/etc/ssl").
Link(fst.Tmp+"/etc/static", "/etc/static").
Link(fst.Tmp+"/etc/subgid", "/etc/subgid").
Link(fst.Tmp+"/etc/subuid", "/etc/subuid").
Link(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
Link(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
Link(fst.Tmp+"/etc/systemd", "/etc/systemd").
Link(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
Link(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
Link(fst.Tmp+"/etc/udev", "/etc/udev").
Link(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
Link(fst.Tmp+"/etc/UPower", "/etc/UPower").
Link(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
Link(fst.Tmp+"/etc/X11", "/etc/X11").
Link(fst.Tmp+"/etc/zfs", "/etc/zfs").
Link(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
Link(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
Link(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Tmpfs("/run/user", 4096, 0755).
Tmpfs("/run/user/65534", 8388608, 0700).
Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", sandbox.BindWritable).
Bind("/home/chronos", "/home/chronos", sandbox.BindWritable).
Place("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
Place("/etc/group", []byte("fortify:x:65534:\n")).
Bind("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/65534/wayland-0", 0).
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native", 0).
Place(fst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus", 0).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket", 0).
Tmpfs("/var/run/nscd", 8192, 0755),
},
},
}

View File

@ -1,4 +1,4 @@
package setuid_test
package app_test
import (
"fmt"
@ -7,7 +7,7 @@ import (
"os/user"
"strconv"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/fst"
)
// fs methods are not implemented using a real FS
@ -125,8 +125,8 @@ func (s *stubNixOS) Open(name string) (fs.File, error) {
}
}
func (s *stubNixOS) Paths() app.Paths {
return app.Paths{
func (s *stubNixOS) Paths() fst.Paths {
return fst.Paths{
SharePath: "/tmp/fortify.1971",
RuntimePath: "/run/user/1971",
RunDirPath: "/run/user/1971/fortify",

View File

@ -1,4 +1,4 @@
package setuid_test
package app_test
import (
"encoding/json"
@ -9,7 +9,6 @@ import (
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/app/internal/setuid"
"git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system"
@ -19,7 +18,7 @@ type sealTestCase struct {
name string
os sys.State
config *fst.Config
id app.ID
id fst.ID
wantSys *system.I
wantContainer *sandbox.Params
}
@ -29,7 +28,7 @@ func TestApp(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
a := setuid.NewWithID(tc.id, tc.os)
a := app.NewWithID(tc.id, tc.os)
var (
gotSys *system.I
gotContainer *sandbox.Params
@ -39,7 +38,7 @@ func TestApp(t *testing.T) {
t.Errorf("Seal: error = %v", err)
return
} else {
gotSys, gotContainer = setuid.AppIParams(a, sa)
gotSys, gotContainer = app.AppIParams(a, sa)
}
}) {
return

View File

@ -1,16 +1,14 @@
package setuid
package app
import (
"errors"
"log"
. "git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
func PrintRunStateErr(rs *RunState, runErr error) (code int) {
code = rs.ExitStatus()
func PrintRunStateErr(rs *fst.RunState, runErr error) {
if runErr != nil {
if rs.Time == nil {
fmsg.PrintBaseError(runErr, "cannot start app:")
@ -51,8 +49,8 @@ func PrintRunStateErr(rs *RunState, runErr error) (code int) {
}
}
if code == 0 {
code = 126
if rs.ExitCode == 0 {
rs.ExitCode = 126
}
}
@ -99,14 +97,13 @@ func PrintRunStateErr(rs *RunState, runErr error) (code int) {
}
out:
if code == 0 {
code = 128
if rs.ExitCode == 0 {
rs.ExitCode = 128
}
}
if rs.WaitErr != nil {
fmsg.Verbosef("wait: %v", rs.WaitErr)
log.Println("inner wait failed:", rs.WaitErr)
}
return
}
// StateStoreError is returned for a failed state save
@ -124,7 +121,7 @@ type StateStoreError struct {
}
// save saves arbitrary errors in [StateStoreError] once.
func (e *StateStoreError) save(errs ...error) {
func (e *StateStoreError) save(errs []error) {
if len(errs) == 0 || e.Err != nil {
panic("invalid call to save")
}

View File

@ -1,20 +1,20 @@
package setuid
package app
import (
. "git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system"
)
func NewWithID(id ID, os sys.State) App {
func NewWithID(id fst.ID, os sys.State) fst.App {
a := new(app)
a.id = newID(&id)
a.sys = os
return a
}
func AppIParams(a App, sa SealedApp) (*system.I, *sandbox.Params) {
func AppIParams(a fst.App, sa fst.SealedApp) (*system.I, *sandbox.Params) {
v := a.(*app)
seal := sa.(*outcome)
if v.outcome != seal || v.id != seal.id {

View File

@ -1,189 +0,0 @@
package common
import (
"errors"
"fmt"
"io/fs"
"maps"
"path"
"syscall"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
)
// in practice there should be less than 30 entries added by the runtime;
// allocating slightly more as a margin for future expansion
const preallocateOpsCount = 1 << 5
// NewContainer initialises [sandbox.Params] via [fst.ContainerConfig].
// Note that remaining container setup must be queued by the caller.
func NewContainer(s *fst.ContainerConfig, os sys.State, uid, gid *int) (*sandbox.Params, map[string]string, error) {
if s == nil {
return nil, nil, syscall.EBADE
}
container := &sandbox.Params{
Hostname: s.Hostname,
Seccomp: s.Seccomp,
}
{
ops := make(sandbox.Ops, 0, preallocateOpsCount+len(s.Filesystem)+len(s.Link)+len(s.Cover))
container.Ops = &ops
}
if s.Multiarch {
container.Seccomp |= seccomp.FilterMultiarch
}
if s.Devel {
container.Flags |= sandbox.FAllowDevel
}
if s.Userns {
container.Flags |= sandbox.FAllowUserns
}
if s.Net {
container.Flags |= sandbox.FAllowNet
}
if s.Tty {
container.Flags |= sandbox.FAllowTTY
}
if s.MapRealUID {
/* some programs fail to connect to dbus session running as a different uid
so this workaround is introduced to map priv-side caller uid in container */
container.Uid = os.Getuid()
*uid = container.Uid
container.Gid = os.Getgid()
*gid = container.Gid
} else {
*uid = sandbox.OverflowUid()
*gid = sandbox.OverflowGid()
}
container.
Proc("/proc").
Tmpfs(fst.Tmp, 1<<12, 0755)
if !s.Device {
container.Dev("/dev").Mqueue("/dev/mqueue")
} else {
container.Bind("/dev", "/dev", sandbox.BindWritable|sandbox.BindDevice)
}
/* retrieve paths and hide them if they're made available in the sandbox;
this feature tries to improve user experience of permissive defaults, and
to warn about issues in custom configuration; it is NOT a security feature
and should not be treated as such, ALWAYS be careful with what you bind */
var hidePaths []string
sc := os.Paths()
hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath)
_, systemBusAddr := dbus.Address()
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
return nil, nil, err
} else {
// there is usually only one, do not preallocate
for _, entry := range entries {
if entry.Method != "unix" {
continue
}
for _, pair := range entry.Values {
if pair[0] == "path" {
if path.IsAbs(pair[1]) {
// get parent dir of socket
dir := path.Dir(pair[1])
if dir == "." || dir == "/" {
os.Printf("dbus socket %q is in an unusual location", pair[1])
}
hidePaths = append(hidePaths, dir)
} else {
os.Printf("dbus socket %q is not absolute", pair[1])
}
}
}
}
}
hidePathMatch := make([]bool, len(hidePaths))
for i := range hidePaths {
if err := evalSymlinks(os, &hidePaths[i]); err != nil {
return nil, nil, err
}
}
for _, c := range s.Filesystem {
if c == nil {
continue
}
if !path.IsAbs(c.Src) {
return nil, nil, fmt.Errorf("src path %q is not absolute", c.Src)
}
dest := c.Dst
if c.Dst == "" {
dest = c.Src
} else if !path.IsAbs(dest) {
return nil, nil, fmt.Errorf("dst path %q is not absolute", dest)
}
srcH := c.Src
if err := evalSymlinks(os, &srcH); err != nil {
return nil, nil, err
}
for i := range hidePaths {
// skip matched entries
if hidePathMatch[i] {
continue
}
if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
return nil, nil, err
} else if ok {
hidePathMatch[i] = true
os.Printf("hiding paths from %q", c.Src)
}
}
var flags int
if c.Write {
flags |= sandbox.BindWritable
}
if c.Device {
flags |= sandbox.BindDevice | sandbox.BindWritable
}
if !c.Must {
flags |= sandbox.BindOptional
}
container.Bind(c.Src, dest, flags)
}
// cover matched paths
for i, ok := range hidePathMatch {
if ok {
container.Tmpfs(hidePaths[i], 1<<13, 0755)
}
}
for _, l := range s.Link {
container.Link(l[0], l[1])
}
return container, maps.Clone(s.Env), nil
}
func evalSymlinks(os sys.State, v *string) error {
if p, err := os.EvalSymlinks(*v); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return err
}
os.Printf("path %q does not yet exist", *v)
} else {
*v = p
}
return nil
}

View File

@ -1,17 +0,0 @@
package instance
import (
"syscall"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/app/internal/setuid"
)
func PrintRunStateErr(whence int, rs *app.RunState, runErr error) (code int) {
switch whence {
case ISetuid:
return setuid.PrintRunStateErr(rs, runErr)
default:
panic(syscall.EINVAL)
}
}

View File

@ -1,33 +0,0 @@
// Package instance exposes cross-package implementation details and provides constructors for builtin implementations.
package instance
import (
"context"
"log"
"syscall"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/app/internal/setuid"
"git.gensokyo.uk/security/fortify/internal/sys"
)
const (
ISetuid = iota
)
func New(whence int, ctx context.Context, os sys.State) (app.App, error) {
switch whence {
case ISetuid:
return setuid.New(ctx, os)
default:
return nil, syscall.EINVAL
}
}
func MustNew(whence int, ctx context.Context, os sys.State) app.App {
a, err := New(whence, ctx, os)
if err != nil {
log.Fatalf("cannot create app: %v", err)
}
return a
}

View File

@ -1,6 +0,0 @@
package instance
import "git.gensokyo.uk/security/fortify/internal/app/internal/setuid"
// ShimMain is the main function of the shim process and runs as the unconstrained target user.
func ShimMain() { setuid.ShimMain() }

View File

@ -1,74 +0,0 @@
package setuid
import (
"context"
"fmt"
"sync"
"git.gensokyo.uk/security/fortify/fst"
. "git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/sys"
)
func New(ctx context.Context, os sys.State) (App, error) {
a := new(app)
a.sys = os
a.ctx = ctx
id := new(ID)
err := NewAppID(id)
a.id = newID(id)
return a, err
}
type app struct {
id *stringPair[ID]
sys sys.State
ctx context.Context
*outcome
mu sync.RWMutex
}
func (a *app) ID() ID { a.mu.RLock(); defer a.mu.RUnlock(); return a.id.unwrap() }
func (a *app) String() string {
if a == nil {
return "(invalid app)"
}
a.mu.RLock()
defer a.mu.RUnlock()
if a.outcome != nil {
if a.outcome.user.uid == nil {
return fmt.Sprintf("(sealed app %s with invalid uid)", a.id)
}
return fmt.Sprintf("(sealed app %s as uid %s)", a.id, a.outcome.user.uid)
}
return fmt.Sprintf("(unsealed app %s)", a.id)
}
func (a *app) Seal(config *fst.Config) (SealedApp, error) {
a.mu.Lock()
defer a.mu.Unlock()
if a.outcome != nil {
panic("app sealed twice")
}
if config == nil {
return nil, fmsg.WrapError(ErrConfig,
"attempted to seal app with nil config")
}
seal := new(outcome)
seal.id = a.id
err := seal.finalise(a.ctx, a.sys, config)
if err == nil {
a.outcome = seal
}
return seal, err
}

View File

@ -1,145 +0,0 @@
package setuid_test
import (
"git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system"
)
var testCasesNixos = []sealTestCase{
{
"nixos chromium direct wayland", new(stubNixOS),
&fst.Config{
ID: "org.chromium.Chromium",
Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
Enablements: system.EWayland | system.EDBus | system.EPulse,
Container: &fst.ContainerConfig{
Userns: true, Net: true, MapRealUID: true, Env: nil, AutoEtc: true,
Filesystem: []*fst.FilesystemConfig{
{Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true},
{Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true},
{Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"},
{Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true},
},
Cover: []string{"/var/run/nscd"},
},
SystemBus: &dbus.Config{
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
Filter: true,
},
SessionBus: &dbus.Config{
Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
"org.kde.kwalletd5", "org.kde.kwalletd6",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{}, Broadcast: map[string]string{},
Filter: true,
},
DirectWayland: true,
Username: "u0_a1",
Data: "/var/lib/persist/module/fortify/0/1",
Identity: 1, Groups: []string{},
},
app.ID{
0x8e, 0x2c, 0x76, 0xb0,
0x66, 0xda, 0xbe, 0x57,
0x4c, 0xf0, 0x73, 0xbd,
0xb4, 0x6e, 0xb5, 0xc1,
},
system.New(1000001).
Ensure("/tmp/fortify.1971", 0711).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/1", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/1", 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
UpdatePermType(system.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute).
Ephemeral(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute).
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse").
CopyFile(nil, "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
Ephemeral(system.Process, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1", 0711).
MustProxyDBus("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", &dbus.Config{
Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
"org.kde.kwalletd5", "org.kde.kwalletd6",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{}, Broadcast: map[string]string{},
Filter: true,
}, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
}).
UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
&sandbox.Params{
Uid: 1971,
Gid: 100,
Flags: sandbox.FAllowNet | sandbox.FAllowUserns,
Dir: "/var/lib/persist/module/fortify/0/1",
Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
Env: []string{
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
"HOME=/var/lib/persist/module/fortify/0/1",
"PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie",
"PULSE_SERVER=unix:/run/user/1971/pulse/native",
"SHELL=/run/current-system/sw/bin/zsh",
"TERM=xterm-256color",
"USER=u0_a1",
"WAYLAND_DISPLAY=wayland-0",
"XDG_RUNTIME_DIR=/run/user/1971",
"XDG_SESSION_CLASS=user",
"XDG_SESSION_TYPE=tty",
},
Ops: new(sandbox.Ops).
Proc("/proc").
Tmpfs(fst.Tmp, 4096, 0755).
Dev("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", 0).
Bind("/usr/bin", "/usr/bin", 0).
Bind("/nix/store", "/nix/store", 0).
Bind("/run/current-system", "/run/current-system", 0).
Bind("/sys/block", "/sys/block", sandbox.BindOptional).
Bind("/sys/bus", "/sys/bus", sandbox.BindOptional).
Bind("/sys/class", "/sys/class", sandbox.BindOptional).
Bind("/sys/dev", "/sys/dev", sandbox.BindOptional).
Bind("/sys/devices", "/sys/devices", sandbox.BindOptional).
Bind("/run/opengl-driver", "/run/opengl-driver", 0).
Bind("/dev/dri", "/dev/dri", sandbox.BindDevice|sandbox.BindWritable|sandbox.BindOptional).
Etc("/etc", "8e2c76b066dabe574cf073bdb46eb5c1").
Tmpfs("/run/user", 4096, 0755).
Tmpfs("/run/user/1971", 8388608, 0700).
Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", sandbox.BindWritable).
Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", sandbox.BindWritable).
Place("/etc/passwd", []byte("u0_a1:x:1971:100:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).
Place("/etc/group", []byte("fortify:x:100:\n")).
Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0", 0).
Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native", 0).
Place(fst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus", 0).
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket", 0).
Tmpfs("/var/run/nscd", 8192, 0755),
},
},
}

View File

@ -1,216 +0,0 @@
package setuid_test
import (
"os"
"git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system"
)
var testCasesPd = []sealTestCase{
{
"nixos permissive defaults no enablements", new(stubNixOS),
&fst.Config{Username: "chronos", Data: "/home/chronos"},
app.ID{
0x4a, 0x45, 0x0b, 0x65,
0x96, 0xd7, 0xbc, 0x15,
0xbd, 0x01, 0x78, 0x0e,
0xb9, 0xa6, 0x07, 0xac,
},
system.New(1000000).
Ensure("/tmp/fortify.1971", 0711).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
&sandbox.Params{
Flags: sandbox.FAllowNet | sandbox.FAllowUserns | sandbox.FAllowTTY,
Dir: "/home/chronos",
Path: "/run/current-system/sw/bin/zsh",
Args: []string{"/run/current-system/sw/bin/zsh"},
Env: []string{
"HOME=/home/chronos",
"SHELL=/run/current-system/sw/bin/zsh",
"TERM=xterm-256color",
"USER=chronos",
"XDG_RUNTIME_DIR=/run/user/65534",
"XDG_SESSION_CLASS=user",
"XDG_SESSION_TYPE=tty",
},
Ops: new(sandbox.Ops).
Proc("/proc").
Tmpfs(fst.Tmp, 4096, 0755).
Dev("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", sandbox.BindWritable).
Bind("/boot", "/boot", sandbox.BindWritable).
Bind("/home", "/home", sandbox.BindWritable).
Bind("/lib", "/lib", sandbox.BindWritable).
Bind("/lib64", "/lib64", sandbox.BindWritable).
Bind("/nix", "/nix", sandbox.BindWritable).
Bind("/root", "/root", sandbox.BindWritable).
Bind("/run", "/run", sandbox.BindWritable).
Bind("/srv", "/srv", sandbox.BindWritable).
Bind("/sys", "/sys", sandbox.BindWritable).
Bind("/usr", "/usr", sandbox.BindWritable).
Bind("/var", "/var", sandbox.BindWritable).
Bind("/dev/kvm", "/dev/kvm", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
Tmpfs("/run/user/1971", 8192, 0755).
Tmpfs("/run/dbus", 8192, 0755).
Etc("/etc", "4a450b6596d7bc15bd01780eb9a607ac").
Tmpfs("/run/user", 4096, 0755).
Tmpfs("/run/user/65534", 8388608, 0700).
Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", sandbox.BindWritable).
Bind("/home/chronos", "/home/chronos", sandbox.BindWritable).
Place("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
Place("/etc/group", []byte("fortify:x:65534:\n")).
Tmpfs("/var/run/nscd", 8192, 0755),
},
},
{
"nixos permissive defaults chromium", new(stubNixOS),
&fst.Config{
ID: "org.chromium.Chromium",
Args: []string{"zsh", "-c", "exec chromium "},
Identity: 9,
Groups: []string{"video"},
Username: "chronos",
Data: "/home/chronos",
SessionBus: &dbus.Config{
Talk: []string{
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{
"org.freedesktop.portal.*": "*",
},
Broadcast: map[string]string{
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
},
Filter: true,
},
SystemBus: &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
},
Enablements: system.EWayland | system.EDBus | system.EPulse,
},
app.ID{
0xeb, 0xf0, 0x83, 0xd1,
0xb1, 0x75, 0x91, 0x17,
0x82, 0xd4, 0x13, 0x36,
0x9b, 0x64, 0xce, 0x7c,
},
system.New(1000009).
Ensure("/tmp/fortify.1971", 0711).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/9", acl.Read, acl.Write, acl.Execute).
Ephemeral(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0711).
Wayland(new(*os.File), "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
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/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", acl.Execute).
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse").
CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
MustProxyDBus("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", &dbus.Config{
Talk: []string{
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{
"org.freedesktop.portal.*": "*",
},
Broadcast: map[string]string{
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
},
Filter: true,
}, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
}).
UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
&sandbox.Params{
Flags: sandbox.FAllowNet | sandbox.FAllowUserns | sandbox.FAllowTTY,
Dir: "/home/chronos",
Path: "/run/current-system/sw/bin/zsh",
Args: []string{"zsh", "-c", "exec chromium "},
Env: []string{
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus",
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
"HOME=/home/chronos",
"PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie",
"PULSE_SERVER=unix:/run/user/65534/pulse/native",
"SHELL=/run/current-system/sw/bin/zsh",
"TERM=xterm-256color",
"USER=chronos",
"WAYLAND_DISPLAY=wayland-0",
"XDG_RUNTIME_DIR=/run/user/65534",
"XDG_SESSION_CLASS=user",
"XDG_SESSION_TYPE=tty",
},
Ops: new(sandbox.Ops).
Proc("/proc").
Tmpfs(fst.Tmp, 4096, 0755).
Dev("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", sandbox.BindWritable).
Bind("/boot", "/boot", sandbox.BindWritable).
Bind("/home", "/home", sandbox.BindWritable).
Bind("/lib", "/lib", sandbox.BindWritable).
Bind("/lib64", "/lib64", sandbox.BindWritable).
Bind("/nix", "/nix", sandbox.BindWritable).
Bind("/root", "/root", sandbox.BindWritable).
Bind("/run", "/run", sandbox.BindWritable).
Bind("/srv", "/srv", sandbox.BindWritable).
Bind("/sys", "/sys", sandbox.BindWritable).
Bind("/usr", "/usr", sandbox.BindWritable).
Bind("/var", "/var", sandbox.BindWritable).
Bind("/dev/dri", "/dev/dri", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
Bind("/dev/kvm", "/dev/kvm", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
Tmpfs("/run/user/1971", 8192, 0755).
Tmpfs("/run/dbus", 8192, 0755).
Etc("/etc", "ebf083d1b175911782d413369b64ce7c").
Tmpfs("/run/user", 4096, 0755).
Tmpfs("/run/user/65534", 8388608, 0700).
Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", sandbox.BindWritable).
Bind("/home/chronos", "/home/chronos", sandbox.BindWritable).
Place("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
Place("/etc/group", []byte("fortify:x:65534:\n")).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/65534/wayland-0", 0).
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native", 0).
Place(fst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus", 0).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket", 0).
Tmpfs("/var/run/nscd", 8192, 0755),
},
},
}

View File

@ -1,195 +0,0 @@
package setuid
import (
"context"
"encoding/gob"
"errors"
"log"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"time"
"git.gensokyo.uk/security/fortify/internal"
. "git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system"
)
const shimWaitTimeout = 5 * time.Second
func (seal *outcome) Run(rs *RunState) error {
if !seal.f.CompareAndSwap(false, true) {
// run does much more than just starting a process; calling it twice, even if the first call fails, will result
// in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
// other Run a chance to return
return errors.New("outcome: attempted to run twice")
}
if rs == nil {
panic("invalid state")
}
// read comp value early to allow for early failure
fsuPath := internal.MustFsuPath()
if err := seal.sys.Commit(seal.ctx); err != nil {
return err
}
store := state.NewMulti(seal.runDirPath)
deferredStoreFunc := func(c state.Cursor) error { return nil } // noop until state in store
defer func() {
var revertErr error
storeErr := new(StateStoreError)
storeErr.Inner, storeErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
revertErr = func() error {
storeErr.InnerErr = deferredStoreFunc(c)
var rt system.Enablement
ec := system.Process
if states, err := c.Load(); err != nil {
// revert per-process state here to limit damage
storeErr.OpErr = err
return seal.sys.Revert((*system.Criteria)(&ec))
} else {
if l := len(states); l == 0 {
ec |= system.User
} else {
fmsg.Verbosef("found %d instances, cleaning up without user-scoped operations", l)
}
// accumulate enablements of remaining launchers
for i, s := range states {
if s.Config != nil {
rt |= s.Config.Enablements
} else {
log.Printf("state entry %d does not contain config", i)
}
}
}
ec |= rt ^ (system.EWayland | system.EX11 | system.EDBus | system.EPulse)
if fmsg.Load() {
if ec > 0 {
fmsg.Verbose("reverting operations scope", system.TypeString(ec))
}
}
return seal.sys.Revert((*system.Criteria)(&ec))
}()
})
storeErr.save(revertErr, store.Close())
rs.RevertErr = storeErr.equiv("error during cleanup:")
}()
ctx, cancel := context.WithCancel(seal.ctx)
defer cancel()
cmd := exec.CommandContext(ctx, fsuPath)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Dir = "/" // container init enters final working directory
// shim runs in the same session as monitor; see shim.go for behaviour
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) }
var e *gob.Encoder
if fd, encoder, err := sandbox.Setup(&cmd.ExtraFiles); err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot create shim setup pipe:")
} else {
e = encoder
cmd.Env = []string{
// passed through to shim by fsu
shimEnv + "=" + strconv.Itoa(fd),
// interpreted by fsu
"FORTIFY_APP_ID=" + seal.user.aid.String(),
}
}
if len(seal.user.supp) > 0 {
fmsg.Verbosef("attaching supplementary group ids %s", seal.user.supp)
// interpreted by fsu
cmd.Env = append(cmd.Env, "FORTIFY_GROUPS="+strings.Join(seal.user.supp, " "))
}
fmsg.Verbosef("setuid helper at %s", fsuPath)
fmsg.Suspend()
if err := cmd.Start(); err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot start setuid wrapper:")
}
rs.SetStart()
// this prevents blocking forever on an early failure
waitErr, setupErr := make(chan error, 1), make(chan error, 1)
go func() { waitErr <- cmd.Wait(); cancel() }()
go func() { setupErr <- e.Encode(&shimParams{os.Getpid(), seal.container, seal.user.data, fmsg.Load()}) }()
select {
case err := <-setupErr:
if err != nil {
fmsg.Resume()
return fmsg.WrapErrorSuffix(err,
"cannot transmit shim config:")
}
case <-ctx.Done():
fmsg.Resume()
return fmsg.WrapError(syscall.ECANCELED,
"shim setup canceled")
}
// returned after blocking on waitErr
var earlyStoreErr = new(StateStoreError)
{
// shim accepted setup payload, create process state
sd := state.State{
ID: seal.id.unwrap(),
PID: cmd.Process.Pid,
Time: *rs.Time,
}
earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
earlyStoreErr.InnerErr = c.Save(&sd, seal.ct)
})
}
// state in store at this point, destroy defunct state entry on return
deferredStoreFunc = func(c state.Cursor) error { return c.Destroy(seal.id.unwrap()) }
waitTimeout := make(chan struct{})
go func() { <-seal.ctx.Done(); time.Sleep(shimWaitTimeout); close(waitTimeout) }()
select {
case rs.WaitErr = <-waitErr:
rs.WaitStatus = cmd.ProcessState.Sys().(syscall.WaitStatus)
if fmsg.Load() {
switch {
case rs.Exited():
fmsg.Verbosef("process %d exited with code %d", cmd.Process.Pid, rs.ExitStatus())
case rs.CoreDump():
fmsg.Verbosef("process %d dumped core", cmd.Process.Pid)
case rs.Signaled():
fmsg.Verbosef("process %d got %s", cmd.Process.Pid, rs.Signal())
default:
fmsg.Verbosef("process %d exited with status %#x", cmd.Process.Pid, rs.WaitStatus)
}
}
case <-waitTimeout:
rs.WaitErr = syscall.ETIMEDOUT
fmsg.Resume()
log.Printf("process %d did not terminate", cmd.Process.Pid)
}
fmsg.Resume()
if seal.sync != nil {
if err := seal.sync.Close(); err != nil {
log.Printf("cannot close wayland security context: %v", err)
}
}
if seal.dbusMsg != nil {
seal.dbusMsg()
}
return earlyStoreErr.equiv("cannot save process state:")
}

180
internal/app/process.go Normal file
View File

@ -0,0 +1,180 @@
package app
import (
"context"
"errors"
"log"
"os/exec"
"time"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/system"
)
const shimSetupTimeout = 5 * time.Second
func (seal *outcome) Run(rs *fst.RunState) error {
if !seal.f.CompareAndSwap(false, true) {
// run does much more than just starting a process; calling it twice, even if the first call fails, will result
// in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
// other Run a chance to return
panic("attempted to run twice")
}
if rs == nil {
panic("invalid state")
}
// read comp values early to allow for early failure
fmsg.Verbosef("version %s", internal.Version())
fmsg.Verbosef("setuid helper at %s", internal.MustFsuPath())
/*
prepare/revert os state
*/
if err := seal.sys.Commit(seal.ctx); err != nil {
return err
}
store := state.NewMulti(seal.runDirPath)
deferredStoreFunc := func(c state.Cursor) error { return nil }
defer func() {
var revertErr error
storeErr := new(StateStoreError)
storeErr.Inner, storeErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
revertErr = func() error {
storeErr.InnerErr = deferredStoreFunc(c)
/*
revert app setup transaction
*/
var rt system.Enablement
ec := system.Process
if states, err := c.Load(); err != nil {
// revert per-process state here to limit damage
storeErr.OpErr = err
return seal.sys.Revert((*system.Criteria)(&ec))
} else {
if l := len(states); l == 0 {
fmsg.Verbose("no other launchers active, will clean up globals")
ec |= system.User
} else {
fmsg.Verbosef("found %d active launchers, cleaning up without globals", l)
}
// accumulate enablements of remaining launchers
for i, s := range states {
if s.Config != nil {
rt |= s.Config.Confinement.Enablements
} else {
log.Printf("state entry %d does not contain config", i)
}
}
}
ec |= rt ^ (system.EWayland | system.EX11 | system.EDBus | system.EPulse)
if fmsg.Load() {
if ec > 0 {
fmsg.Verbose("reverting operations type", system.TypeString(ec))
}
}
return seal.sys.Revert((*system.Criteria)(&ec))
}()
})
storeErr.save([]error{revertErr, store.Close()})
rs.RevertErr = storeErr.equiv("error returned during cleanup:")
}()
/*
shim process lifecycle
*/
waitErr := make(chan error, 1)
cmd := new(shimProcess)
if startTime, err := cmd.Start(
seal.user.aid.String(),
seal.user.supp,
); err != nil {
return err
} else {
// whether/when the fsu process was created
rs.Time = startTime
}
ctx, cancel := context.WithTimeout(seal.ctx, shimSetupTimeout)
defer cancel()
go func() {
waitErr <- cmd.Unwrap().Wait()
// cancel shim setup in case shim died before receiving payload
cancel()
}()
if err := cmd.Serve(ctx, &shimParams{
Container: seal.container,
Home: seal.user.data,
Verbose: fmsg.Load(),
}); err != nil {
return err
}
// shim accepted setup payload, create process state
sd := state.State{
ID: seal.id.unwrap(),
PID: cmd.Unwrap().Process.Pid,
Time: *rs.Time,
}
var earlyStoreErr = new(StateStoreError) // returned after blocking on waitErr
earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
earlyStoreErr.InnerErr = c.Save(&sd, seal.ct)
})
// destroy defunct state entry
deferredStoreFunc = func(c state.Cursor) error { return c.Destroy(seal.id.unwrap()) }
select {
case err := <-waitErr: // block until fsu/shim returns
if err != nil {
var exitError *exec.ExitError
if !errors.As(err, &exitError) {
// should be unreachable
rs.WaitErr = err
}
// store non-zero return code
rs.ExitCode = exitError.ExitCode()
} else {
rs.ExitCode = cmd.Unwrap().ProcessState.ExitCode()
}
if fmsg.Load() {
fmsg.Verbosef("process %d exited with exit code %d", cmd.Unwrap().Process.Pid, rs.ExitCode)
}
// this is reached when a fault makes an already running shim impossible to continue execution
// however a kill signal could not be delivered (should actually always happen like that since fsu)
// the effects of this is similar to the alternative exit path and ensures shim death
case err := <-cmd.Fallback():
rs.ExitCode = 255
log.Printf("cannot terminate shim on faulted setup: %v", err)
// alternative exit path relying on shim behaviour on monitor process exit
case <-seal.ctx.Done():
fmsg.Verbose("alternative exit path selected")
}
fmsg.Resume()
if seal.sync != nil {
if err := seal.sync.Close(); err != nil {
log.Printf("cannot close wayland security context: %v", err)
}
}
if seal.dbusMsg != nil {
seal.dbusMsg()
}
return earlyStoreErr.equiv("cannot save process state:")
}

View File

@ -1,4 +1,4 @@
package setuid
package app
import (
"bytes"
@ -8,6 +8,7 @@ import (
"fmt"
"io"
"io/fs"
"maps"
"os"
"path"
"regexp"
@ -20,8 +21,6 @@ import (
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal"
. "git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/app/instance/common"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox"
@ -66,7 +65,7 @@ var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z
// outcome stores copies of various parts of [fst.Config]
type outcome struct {
// copied from initialising [app]
id *stringPair[ID]
id *stringPair[fst.ID]
// copied from [sys.State] response
runDirPath string
@ -87,53 +86,6 @@ type outcome struct {
f atomic.Bool
}
// shareHost holds optional share directory state that must not be accessed directly
type shareHost struct {
// whether XDG_RUNTIME_DIR is used post fsu
useRuntimeDir bool
// process-specific directory in tmpdir, empty if unused
sharePath string
// process-specific directory in XDG_RUNTIME_DIR, empty if unused
runtimeSharePath string
seal *outcome
sc Paths
}
// ensureRuntimeDir must be called if direct access to paths within XDG_RUNTIME_DIR is required
func (share *shareHost) ensureRuntimeDir() {
if share.useRuntimeDir {
return
}
share.useRuntimeDir = true
share.seal.sys.Ensure(share.sc.RunDirPath, 0700)
share.seal.sys.UpdatePermType(system.User, share.sc.RunDirPath, acl.Execute)
share.seal.sys.Ensure(share.sc.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
share.seal.sys.UpdatePermType(system.User, share.sc.RuntimePath, acl.Execute)
}
// instance returns a process-specific share path within tmpdir
func (share *shareHost) instance() string {
if share.sharePath != "" {
return share.sharePath
}
share.sharePath = path.Join(share.sc.SharePath, share.seal.id.String())
share.seal.sys.Ephemeral(system.Process, share.sharePath, 0711)
return share.sharePath
}
// runtime returns a process-specific share path within XDG_RUNTIME_DIR
func (share *shareHost) runtime() string {
if share.runtimeSharePath != "" {
return share.runtimeSharePath
}
share.ensureRuntimeDir()
share.runtimeSharePath = path.Join(share.sc.RunDirPath, share.seal.id.String())
share.seal.sys.Ephemeral(system.Process, share.runtimeSharePath, 0700)
share.seal.sys.UpdatePerm(share.runtimeSharePath, acl.Execute)
return share.runtimeSharePath
}
// fsuUser stores post-fsu credentials and metadata
type fsuUser struct {
// application id
@ -158,6 +110,11 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
}
seal.ctx = ctx
shellPath := "/bin/sh"
if s, ok := sys.LookupEnv(shell); ok && path.IsAbs(s) {
shellPath = s
}
{
// encode initial configuration for state tracking
ct := new(bytes.Buffer)
@ -169,16 +126,20 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
}
// allowed aid range 0 to 9999, this is checked again in fsu
if config.Identity < 0 || config.Identity > 9999 {
if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 {
return fmsg.WrapError(ErrUser,
fmt.Sprintf("identity %d out of range", config.Identity))
fmt.Sprintf("aid %d out of range", config.Confinement.AppID))
}
/*
Resolve post-fsu user state
*/
seal.user = fsuUser{
aid: newInt(config.Identity),
data: config.Data,
home: config.Dir,
username: config.Username,
aid: newInt(config.Confinement.AppID),
data: config.Confinement.Outer,
home: config.Confinement.Inner,
username: config.Confinement.Username,
}
if seal.user.username == "" {
seal.user.username = "chronos"
@ -199,8 +160,8 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
} else {
seal.user.uid = newInt(u)
}
seal.user.supp = make([]string, len(config.Groups))
for i, name := range config.Groups {
seal.user.supp = make([]string, len(config.Confinement.Groups))
for i, name := range config.Confinement.Groups {
if g, err := sys.LookupGroup(name); err != nil {
return fmsg.WrapError(err,
fmt.Sprintf("unknown group %q", name))
@ -209,18 +170,13 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
}
}
// this also falls back to host path if encountering an invalid path
if !path.IsAbs(config.Shell) {
config.Shell = "/bin/sh"
if s, ok := sys.LookupEnv(shell); ok && path.IsAbs(s) {
config.Shell = s
}
}
// do not use the value of shell before this point
/*
Resolve initial container state
*/
// permissive defaults
if config.Container == nil {
fmsg.Verbose("container configuration not supplied, PROCEED WITH CAUTION")
if config.Confinement.Sandbox == nil {
fmsg.Verbose("sandbox configuration not supplied, PROCEED WITH CAUTION")
// fsu clears the environment so resolve paths early
if !path.IsAbs(config.Path) {
@ -231,11 +187,11 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
config.Path = p
}
} else {
config.Path = config.Shell
config.Path = shellPath
}
}
conf := &fst.ContainerConfig{
conf := &fst.SandboxConfig{
Userns: true,
Net: true,
Tty: true,
@ -268,20 +224,20 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
conf.Cover = append(conf.Cover, nscd)
}
// bind GPU stuff
if config.Enablements&(system.EX11|system.EWayland) != 0 {
if config.Confinement.Enablements&(system.EX11|system.EWayland) != 0 {
conf.Filesystem = append(conf.Filesystem, &fst.FilesystemConfig{Src: "/dev/dri", Device: true})
}
// opportunistically bind kvm
conf.Filesystem = append(conf.Filesystem, &fst.FilesystemConfig{Src: "/dev/kvm", Device: true})
config.Container = conf
config.Confinement.Sandbox = conf
}
var mapuid, mapgid *stringPair[int]
{
var uid, gid int
var err error
seal.container, seal.env, err = common.NewContainer(config.Container, sys, &uid, &gid)
seal.container, seal.env, err = config.Confinement.Sandbox.ToContainer(sys, &uid, &gid)
if err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot initialise container configuration:")
@ -299,23 +255,40 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
mapuid = newInt(uid)
mapgid = newInt(gid)
if seal.env == nil {
seal.env = make(map[string]string, 1<<6)
seal.env = make(map[string]string)
}
}
if !config.Container.AutoEtc {
if config.Container.Etc != "" {
seal.container.Bind(config.Container.Etc, "/etc", 0)
}
} else {
etcPath := config.Container.Etc
if etcPath == "" {
etcPath = "/etc"
}
seal.container.Etc(etcPath, seal.id.String())
}
/*
Initialise externals
*/
// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid
sc := sys.Paths()
seal.runDirPath = sc.RunDirPath
seal.sys = system.New(seal.user.uid.unwrap())
/*
Work directories
*/
// base fortify share path
seal.sys.Ensure(sc.SharePath, 0711)
// outer paths used by the main process
seal.sys.Ensure(sc.RunDirPath, 0700)
seal.sys.UpdatePermType(system.User, sc.RunDirPath, acl.Execute)
seal.sys.Ensure(sc.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
seal.sys.UpdatePermType(system.User, sc.RuntimePath, acl.Execute)
// outer process-specific share directory
sharePath := path.Join(sc.SharePath, seal.id.String())
seal.sys.Ephemeral(system.Process, sharePath, 0711)
// similar to share but within XDG_RUNTIME_DIR
sharePathLocal := path.Join(sc.RunDirPath, seal.id.String())
seal.sys.Ephemeral(system.Process, sharePathLocal, 0700)
seal.sys.UpdatePerm(sharePathLocal, acl.Execute)
// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as post-fsu user
innerRuntimeDir := path.Join("/run/user", mapuid.String())
seal.container.Tmpfs("/run/user", 1<<12, 0755)
seal.container.Tmpfs(innerRuntimeDir, 1<<23, 0700)
@ -323,23 +296,21 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
seal.env[xdgSessionClass] = "user"
seal.env[xdgSessionType] = "tty"
share := &shareHost{seal: seal, sc: sys.Paths()}
seal.runDirPath = share.sc.RunDirPath
seal.sys = system.New(seal.user.uid.unwrap())
// outer path for inner /tmp
{
seal.sys.Ensure(share.sc.SharePath, 0711)
tmpdir := path.Join(share.sc.SharePath, "tmpdir")
tmpdir := path.Join(sc.SharePath, "tmpdir")
seal.sys.Ensure(tmpdir, 0700)
seal.sys.UpdatePermType(system.User, tmpdir, acl.Execute)
tmpdirInst := path.Join(tmpdir, seal.user.aid.String())
seal.sys.Ensure(tmpdirInst, 01700)
seal.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute)
// mount inner /tmp from share so it shares persistence and storage behaviour of host /tmp
seal.container.Bind(tmpdirInst, "/tmp", sandbox.BindWritable)
}
{
/*
Passwd database
*/
homeDir := "/var/empty"
if seal.user.home != "" {
homeDir = seal.user.home
@ -352,27 +323,29 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
seal.container.Dir = homeDir
seal.env["HOME"] = homeDir
seal.env["USER"] = username
seal.env[shell] = config.Shell
seal.container.Place("/etc/passwd",
[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Fortify:"+homeDir+":"+config.Shell+"\n"))
[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Fortify:"+homeDir+":"+shellPath+"\n"))
seal.container.Place("/etc/group",
[]byte("fortify:x:"+mapgid.String()+":\n"))
}
// pass TERM for proper terminal I/O in initial process
/*
Display servers
*/
// pass $TERM for proper terminal I/O in shell
if t, ok := sys.LookupEnv(term); ok {
seal.env[term] = t
}
if config.Enablements&system.EWayland != 0 {
if config.Confinement.Enablements&system.EWayland != 0 {
// outer wayland socket (usually `/run/user/%d/wayland-%d`)
var socketPath string
if name, ok := sys.LookupEnv(wl.WaylandDisplay); !ok {
fmsg.Verbose(wl.WaylandDisplay + " is not set, assuming " + wl.FallbackName)
socketPath = path.Join(share.sc.RuntimePath, wl.FallbackName)
socketPath = path.Join(sc.RuntimePath, wl.FallbackName)
} else if !path.IsAbs(name) {
socketPath = path.Join(share.sc.RuntimePath, name)
socketPath = path.Join(sc.RuntimePath, name)
} else {
socketPath = name
}
@ -380,25 +353,25 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
innerPath := path.Join(innerRuntimeDir, wl.FallbackName)
seal.env[wl.WaylandDisplay] = wl.FallbackName
if !config.DirectWayland { // set up security-context-v1
if !config.Confinement.Sandbox.DirectWayland { // set up security-context-v1
socketDir := path.Join(sc.SharePath, "wayland")
outerPath := path.Join(socketDir, seal.id.String())
seal.sys.Ensure(socketDir, 0711)
appID := config.ID
if appID == "" {
// use instance ID in case app id is not set
appID = "uk.gensokyo.fortify." + seal.id.String()
}
// downstream socket paths
outerPath := path.Join(share.instance(), "wayland")
seal.sys.Wayland(&seal.sync, outerPath, socketPath, appID, seal.id.String())
seal.container.Bind(outerPath, innerPath, 0)
} else { // bind mount wayland socket (insecure)
fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION")
share.ensureRuntimeDir()
seal.container.Bind(socketPath, innerPath, 0)
seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
}
}
if config.Enablements&system.EX11 != 0 {
if config.Confinement.Enablements&system.EX11 != 0 {
if d, ok := sys.LookupEnv(display); !ok {
return fmsg.WrapError(ErrXDisplay,
"DISPLAY is not set")
@ -409,9 +382,13 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
}
}
if config.Enablements&system.EPulse != 0 {
/*
PulseAudio server and authentication
*/
if config.Confinement.Enablements&system.EPulse != 0 {
// PulseAudio runtime directory (usually `/run/user/%d/pulse`)
pulseRuntimeDir := path.Join(share.sc.RuntimePath, "pulse")
pulseRuntimeDir := path.Join(sc.RuntimePath, "pulse")
// PulseAudio socket (usually `/run/user/%d/pulse/native`)
pulseSocket := path.Join(pulseRuntimeDir, "native")
@ -439,7 +416,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
}
// hard link pulse socket into target-executable share
innerPulseRuntimeDir := path.Join(share.runtime(), "pulse")
innerPulseRuntimeDir := path.Join(sharePathLocal, "pulse")
innerPulseSocket := path.Join(innerRuntimeDir, "pulse", "native")
seal.sys.Link(pulseSocket, innerPulseRuntimeDir)
seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0)
@ -458,19 +435,22 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
}
}
if config.Enablements&system.EDBus != 0 {
/*
D-Bus proxy
*/
if config.Confinement.Enablements&system.EDBus != 0 {
// ensure dbus session bus defaults
if config.SessionBus == nil {
config.SessionBus = dbus.NewConfig(config.ID, true, true)
if config.Confinement.SessionBus == nil {
config.Confinement.SessionBus = dbus.NewConfig(config.ID, true, true)
}
// downstream socket paths
sharePath := share.instance()
sessionPath, systemPath := path.Join(sharePath, "bus"), path.Join(sharePath, "system_bus_socket")
// configure dbus proxy
if f, err := seal.sys.ProxyDBus(
config.SessionBus, config.SystemBus,
config.Confinement.SessionBus, config.Confinement.SystemBus,
sessionPath, systemPath,
); err != nil {
return err
@ -483,7 +463,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner
seal.container.Bind(sessionPath, sessionInner, 0)
seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
if config.SystemBus != nil {
if config.Confinement.SystemBus != nil {
systemInner := "/run/dbus/system_bus_socket"
seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner
seal.container.Bind(systemPath, systemInner, 0)
@ -491,12 +471,16 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
}
}
for _, dest := range config.Container.Cover {
/*
Miscellaneous
*/
for _, dest := range config.Confinement.Sandbox.Cover {
seal.container.Tmpfs(dest, 1<<13, 0755)
}
// append ExtraPerms last
for _, p := range config.ExtraPerms {
for _, p := range config.Confinement.ExtraPerms {
if p == nil {
continue
}
@ -520,19 +504,11 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
// flatten and sort env for deterministic behaviour
seal.container.Env = make([]string, 0, len(seal.env))
for k, v := range seal.env {
if strings.IndexByte(k, '=') != -1 {
return fmsg.WrapError(syscall.EINVAL,
fmt.Sprintf("invalid environment variable %s", k))
}
seal.container.Env = append(seal.container.Env, k+"="+v)
}
maps.All(seal.env)(func(k string, v string) bool { seal.container.Env = append(seal.container.Env, k+"="+v); return true })
slices.Sort(seal.container.Env)
if fmsg.Load() {
fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s, ops: %d",
seal.user.uid, seal.user.username, config.Groups, seal.container.Args, len(*seal.container.Ops))
}
fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s",
seal.user.uid, seal.user.username, config.Confinement.Groups, seal.container.Args)
return nil
}

View File

@ -1,78 +1,26 @@
package setuid
package app
import (
"context"
"encoding/gob"
"errors"
"log"
"os"
"os/exec"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
)
/*
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <signal.h>
static pid_t f_shim_param_ppid = -1;
// this cannot unblock fmsg since Go code is not async-signal-safe
static void f_shim_sigaction(int sig, siginfo_t *si, void *ucontext) {
if (sig != SIGCONT || si == NULL) {
// unreachable
fprintf(stderr, "sigaction: sa_sigaction got invalid siginfo\n");
return;
}
// monitor requests shim exit
if (si->si_pid == f_shim_param_ppid)
exit(254);
fprintf(stderr, "sigaction: got SIGCONT from process %d\n", si->si_pid);
// shim orphaned before monitor delivers a signal
if (getppid() != f_shim_param_ppid)
exit(3);
}
void f_shim_setup_cont_signal(pid_t ppid) {
struct sigaction new_action = {0}, old_action = {0};
if (sigaction(SIGCONT, NULL, &old_action) != 0)
return;
if (old_action.sa_handler != SIG_DFL) {
errno = ENOTRECOVERABLE;
return;
}
new_action.sa_sigaction = f_shim_sigaction;
if (sigemptyset(&new_action.sa_mask) != 0)
return;
new_action.sa_flags = SA_ONSTACK | SA_SIGINFO;
if (sigaction(SIGCONT, &new_action, NULL) != 0)
return;
errno = 0;
f_shim_param_ppid = ppid;
}
*/
import "C"
const shimEnv = "FORTIFY_SHIM"
type shimParams struct {
// monitor pid, checked against ppid in signal handler
Monitor int
// finalised container params
Container *sandbox.Params
// path to outer home directory
@ -106,16 +54,6 @@ func ShimMain() {
} else {
internal.InstallFmsg(params.Verbose)
closeSetup = f
// the Go runtime does not expose siginfo_t so SIGCONT is handled in C to check si_pid
if _, err = C.f_shim_setup_cont_signal(C.pid_t(params.Monitor)); err != nil {
log.Fatalf("cannot install SIGCONT handler: %v", err)
}
// pdeath_signal delivery is checked as if the dying process called kill(2), see kernel/exit.c
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGCONT), 0); errno != 0 {
log.Fatalf("cannot set parent-death signal: %v", errno)
}
}
if params.Container == nil || params.Container.Ops == nil {
@ -162,11 +100,6 @@ func ShimMain() {
if err := container.Serve(); err != nil {
fmsg.PrintBaseError(err, "cannot configure container:")
}
if err := seccomp.Load(seccomp.PresetCommon); err != nil {
log.Fatalf("cannot load syscall filter: %v", err)
}
if err := container.Wait(); err != nil {
var exitError *exec.ExitError
if !errors.As(err, &exitError) {
@ -179,3 +112,101 @@ func ShimMain() {
os.Exit(exitError.ExitCode())
}
}
type shimProcess struct {
// user switcher process
cmd *exec.Cmd
// fallback exit notifier with error returned killing the process
killFallback chan error
// monitor to shim encoder
encoder *gob.Encoder
}
func (s *shimProcess) Unwrap() *exec.Cmd { return s.cmd }
func (s *shimProcess) Fallback() chan error { return s.killFallback }
func (s *shimProcess) String() string {
if s.cmd == nil {
return "(unused shim manager)"
}
return s.cmd.String()
}
func (s *shimProcess) Start(
aid string,
supp []string,
) (*time.Time, error) {
// prepare user switcher invocation
fsuPath := internal.MustFsuPath()
s.cmd = exec.Command(fsuPath)
// pass shim setup pipe
if fd, e, err := sandbox.Setup(&s.cmd.ExtraFiles); err != nil {
return nil, fmsg.WrapErrorSuffix(err,
"cannot create shim setup pipe:")
} else {
s.encoder = e
s.cmd.Env = []string{
shimEnv + "=" + strconv.Itoa(fd),
"FORTIFY_APP_ID=" + aid,
}
}
// format fsu supplementary groups
if len(supp) > 0 {
fmsg.Verbosef("attaching supplementary group ids %s", supp)
s.cmd.Env = append(s.cmd.Env, "FORTIFY_GROUPS="+strings.Join(supp, " "))
}
s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
s.cmd.Dir = "/"
fmsg.Verbose("starting shim via fsu:", s.cmd)
// withhold messages to stderr
fmsg.Suspend()
if err := s.cmd.Start(); err != nil {
return nil, fmsg.WrapErrorSuffix(err,
"cannot start fsu:")
}
startTime := time.Now().UTC()
return &startTime, nil
}
func (s *shimProcess) Serve(ctx context.Context, params *shimParams) error {
// kill shim if something goes wrong and an error is returned
s.killFallback = make(chan error, 1)
killShim := func() {
if err := s.cmd.Process.Signal(os.Interrupt); err != nil {
s.killFallback <- err
}
}
defer func() { killShim() }()
encodeErr := make(chan error)
go func() { encodeErr <- s.encoder.Encode(params) }()
select {
// encode return indicates setup completion
case err := <-encodeErr:
if err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot transmit shim config:")
}
killShim = func() {}
return nil
// setup canceled before payload was accepted
case <-ctx.Done():
err := ctx.Err()
if errors.Is(err, context.Canceled) {
return fmsg.WrapError(syscall.ECANCELED,
"shim setup canceled")
}
if errors.Is(err, context.DeadlineExceeded) {
return fmsg.WrapError(syscall.ETIMEDOUT,
"deadline exceeded waiting for shim")
}
// unreachable
return err
}
}

View File

@ -1,13 +1,13 @@
package setuid
package app
import (
"strconv"
. "git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/fst"
)
func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} }
func newID(id *ID) *stringPair[ID] { return &stringPair[ID]{*id, id.String()} }
func newID(id *fst.ID) *stringPair[fst.ID] { return &stringPair[fst.ID]{*id, id.String()} }
// stringPair stores a value and its string representation.
type stringPair[T comparable] struct {

View File

@ -14,7 +14,6 @@ import (
"syscall"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
@ -34,10 +33,10 @@ func (s *multiStore) Do(aid int, f func(c Cursor)) (bool, error) {
// load or initialise new backend
b := new(multiBackend)
b.lock.Lock()
if v, ok := s.backends.LoadOrStore(aid, b); ok {
b = v.(*multiBackend)
} else {
b.lock.Lock()
b.path = path.Join(s.base, strconv.Itoa(aid))
// ensure directory
@ -130,7 +129,7 @@ type multiBackend struct {
lock sync.RWMutex
}
func (b *multiBackend) filename(id *app.ID) string {
func (b *multiBackend) filename(id *fst.ID) string {
return path.Join(b.path, id.String())
}
@ -190,8 +189,8 @@ func (b *multiBackend) load(decode bool) (Entries, error) {
return nil, fmt.Errorf("unexpected directory %q in store", e.Name())
}
id := new(app.ID)
if err := app.ParseAppID(id, e.Name()); err != nil {
id := new(fst.ID)
if err := fst.ParseAppID(id, e.Name()); err != nil {
return nil, err
}
@ -336,7 +335,7 @@ func (b *multiBackend) encodeState(w io.WriteSeeker, state *State, configWriter
return err
}
func (b *multiBackend) Destroy(id app.ID) error {
func (b *multiBackend) Destroy(id fst.ID) error {
b.lock.Lock()
defer b.lock.Unlock()

View File

@ -6,12 +6,11 @@ import (
"time"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/app"
)
var ErrNoConfig = errors.New("state does not contain config")
type Entries map[app.ID]*State
type Entries map[fst.ID]*State
type Store interface {
// Do calls f exactly once and ensures store exclusivity until f returns.
@ -30,7 +29,7 @@ type Store interface {
// Cursor provides access to the store
type Cursor interface {
Save(state *State, configWriter io.WriterTo) error
Destroy(id app.ID) error
Destroy(id fst.ID) error
Load() (Entries, error)
Len() (int, error)
}
@ -38,7 +37,7 @@ type Cursor interface {
// State is a fortify process's state
type State struct {
// fortify instance id
ID app.ID `json:"instance"`
ID fst.ID `json:"instance"`
// child process PID value
PID int `json:"pid"`
// sealed app configuration

View File

@ -11,7 +11,6 @@ import (
"time"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/state"
)
@ -134,7 +133,7 @@ func testStore(t *testing.T, s state.Store) {
}
func makeState(t *testing.T, s *state.State, ct io.Writer) {
if err := app.NewAppID(&s.ID); err != nil {
if err := fst.NewAppID(&s.ID); err != nil {
t.Fatalf("cannot create dummy state: %v", err)
}
if err := gob.NewEncoder(ct).Encode(fst.Template()); err != nil {

View File

@ -6,7 +6,7 @@ import (
"path"
"strconv"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
@ -41,14 +41,14 @@ type State interface {
Printf(format string, v ...any)
// Paths returns a populated [Paths] struct.
Paths() app.Paths
Paths() fst.Paths
// Uid invokes fsu and returns target uid.
// Any errors returned by Uid is already wrapped [fmsg.BaseError].
Uid(aid int) (int, error)
}
// CopyPaths is a generic implementation of [fst.Paths].
func CopyPaths(os State, v *app.Paths) {
// CopyPaths is a generic implementation of [System.Paths].
func CopyPaths(os State, v *fst.Paths) {
v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Getuid()))
fmsg.Verbosef("process share directory at %q", v.SharePath)

View File

@ -12,15 +12,15 @@ import (
"sync"
"syscall"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/sandbox"
)
// Std implements System using the standard library.
type Std struct {
paths app.Paths
paths fst.Paths
pathsOnce sync.Once
uidOnce sync.Once
@ -48,7 +48,7 @@ func (s *Std) Printf(format string, v ...any) { fmsg.Verbosef(form
const xdgRuntimeDir = "XDG_RUNTIME_DIR"
func (s *Std) Paths() app.Paths {
func (s *Std) Paths() fst.Paths {
s.pathsOnce.Do(func() { CopyPaths(s, &s.paths) })
return s.paths
}

45
main.go
View File

@ -20,7 +20,6 @@ import (
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/app/instance"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/internal/sys"
@ -74,7 +73,7 @@ func buildCommand(out io.Writer) command.Command {
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable")
c.Command("shim", command.UsageInternal, func([]string) error { instance.ShimMain(); return errSuccess })
c.Command("shim", command.UsageInternal, func([]string) error { app.ShimMain(); return errSuccess })
c.Command("app", "Launch app defined by the specified config file", func(args []string) error {
if len(args) < 1 {
@ -154,33 +153,33 @@ func buildCommand(out io.Writer) command.Command {
userName = passwd.Username
}
config.Identity = aid
config.Groups = groups
config.Data = homeDir
config.Username = userName
config.Confinement.AppID = aid
config.Confinement.Groups = groups
config.Confinement.Outer = homeDir
config.Confinement.Username = userName
if wayland {
config.Enablements |= system.EWayland
config.Confinement.Enablements |= system.EWayland
}
if x11 {
config.Enablements |= system.EX11
config.Confinement.Enablements |= system.EX11
}
if dBus {
config.Enablements |= system.EDBus
config.Confinement.Enablements |= system.EDBus
}
if pulse {
config.Enablements |= system.EPulse
config.Confinement.Enablements |= system.EPulse
}
// parse D-Bus config file from flags if applicable
if dBus {
if dbusConfigSession == "builtin" {
config.SessionBus = dbus.NewConfig(fid, true, mpris)
config.Confinement.SessionBus = dbus.NewConfig(fid, true, mpris)
} else {
if conf, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
log.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
} else {
config.SessionBus = conf
config.Confinement.SessionBus = conf
}
}
@ -189,14 +188,14 @@ func buildCommand(out io.Writer) command.Command {
if conf, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
log.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
} else {
config.SystemBus = conf
config.Confinement.SystemBus = conf
}
}
// override log from configuration
if dbusVerbose {
config.SessionBus.Log = true
config.SystemBus.Log = true
config.Confinement.SessionBus.Log = true
config.Confinement.SystemBus.Log = true
}
}
@ -240,11 +239,11 @@ func buildCommand(out io.Writer) command.Command {
case 1: // instance
name := args[0]
config, entry := tryShort(name)
config, instance := tryShort(name)
if config == nil {
config = tryPath(name)
}
printShowInstance(os.Stdout, time.Now().UTC(), entry, config, showFlagShort, flagJSON)
printShowInstance(os.Stdout, time.Now().UTC(), instance, config, showFlagShort, flagJSON)
default:
log.Fatal("show requires 1 argument")
@ -285,15 +284,15 @@ func runApp(config *fst.Config) {
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable
a := instance.MustNew(instance.ISetuid, ctx, std)
a := app.MustNew(ctx, std)
rs := new(app.RunState)
rs := new(fst.RunState)
if sa, err := a.Seal(config); err != nil {
fmsg.PrintBaseError(err, "cannot seal app:")
internal.Exit(1)
rs.ExitCode = 1
} else {
internal.Exit(instance.PrintRunStateErr(instance.ISetuid, rs, sa.Run(rs)))
// this updates ExitCode
app.PrintRunStateErr(rs, sa.Run(rs))
}
*(*int)(nil) = 0 // not reached
internal.Exit(rs.ExitCode)
}

View File

@ -88,39 +88,29 @@ in
conf = {
inherit (app) id;
path =
if app.path == null then
pkgs.writeScript "${app.name}-start" ''
path = pkgs.writeScript "${app.name}-start" ''
#!${pkgs.zsh}${pkgs.zsh.shellPath}
${script}
''
else
app.path;
args = if app.args == null then [ "${app.name}-start" ] else app.args;
'';
args = [ "${app.name}-start" ];
inherit enablements;
inherit (dbusConfig) session_bus system_bus;
direct_wayland = app.insecureWayland;
username = getsubname fid aid;
data = getsubhome fid aid;
identity = aid;
confinement = {
app_id = aid;
inherit (app) groups;
container = {
username = getsubname fid aid;
home = getsubhome fid aid;
sandbox = {
inherit (app)
devel
userns
net
device
dev
tty
multiarch
env
;
map_real_uid = app.mapRealUid;
direct_wayland = app.insecureWayland;
filesystem =
let
@ -183,6 +173,9 @@ in
);
};
inherit enablements;
inherit (dbusConfig) session_bus system_bus;
};
};
in
pkgs.writeShellScriptBin app.name ''
@ -204,11 +197,9 @@ in
${copy "${pkg}/share/icons"}
${copy "${pkg}/share/man"}
if test -d "$out/share/applications"; then
substituteInPlace $out/share/applications/* \
--replace-warn '${pkg}/bin/' "" \
--replace-warn '${pkg}/libexec/' ""
fi
''
)
++ acc

View File

@ -35,7 +35,7 @@ package
*Default:*
` <derivation fortify-static-x86_64-unknown-linux-musl-0.4.0> `
` <derivation fortify-static-x86_64-unknown-linux-musl-0.3.1> `
@ -73,25 +73,6 @@ list of package
## environment\.fortify\.apps\.\*\.args
Custom args\.
Setting this to null will default to script name\.
*Type:*
null or (list of string)
*Default:*
` null `
## environment\.fortify\.apps\.\*\.capability\.dbus
@ -222,11 +203,11 @@ null or anything
## environment\.fortify\.apps\.\*\.devel
## environment\.fortify\.apps\.\*\.dev
Whether to enable debugging-related kernel interfaces\.
Whether to enable access to all devices\.
@ -245,11 +226,11 @@ boolean
## environment\.fortify\.apps\.\*\.device
## environment\.fortify\.apps\.\*\.devel
Whether to enable access to all devices\.
Whether to enable debugging-related kernel interfaces\.
@ -505,25 +486,6 @@ boolean
## environment\.fortify\.apps\.\*\.path
Custom executable path\.
Setting this to null will default to the start script\.
*Type:*
null or string
*Default:*
` null `
## environment\.fortify\.apps\.\*\.script
@ -644,7 +606,7 @@ package
*Default:*
` <derivation fortify-fsu-0.4.0> `
` <derivation fortify-fsu-0.3.1> `

View File

@ -94,24 +94,6 @@ in
'';
};
path = mkOption {
type = nullOr str;
default = null;
description = ''
Custom executable path.
Setting this to null will default to the start script.
'';
};
args = mkOption {
type = nullOr (listOf str);
default = null;
description = ''
Custom args.
Setting this to null will default to script name.
'';
};
script = mkOption {
type = nullOr str;
default = null;
@ -177,7 +159,7 @@ in
nix = mkEnableOption "nix daemon access";
mapRealUid = mkEnableOption "mapping to priv-user uid";
device = mkEnableOption "access to all devices";
dev = mkEnableOption "access to all devices";
insecureWayland = mkEnableOption "direct access to the Wayland socket";
gpu = mkOption {

View File

@ -31,7 +31,7 @@
buildGoModule rec {
pname = "fortify";
version = "0.4.0";
version = "0.3.1";
src = builtins.path {
name = "${pname}-src";

View File

@ -67,7 +67,7 @@ func tryFd(name string) io.ReadCloser {
}
}
func tryShort(name string) (config *fst.Config, entry *state.State) {
func tryShort(name string) (config *fst.Config, instance *state.State) {
likePrefix := false
if len(name) <= 32 {
likePrefix = true
@ -96,8 +96,8 @@ func tryShort(name string) (config *fst.Config, entry *state.State) {
v := id.String()
if strings.HasPrefix(v, name) {
// match, use config from this state entry
entry = entries[id]
config = entry.Config
instance = entries[id]
config = instance.Config
break
}

View File

@ -56,7 +56,7 @@ func printShowInstance(
t := newPrinter(output)
defer t.MustFlush()
if config.Container == nil {
if config.Confinement.Sandbox == nil {
mustPrint(output, "Warning: this configuration uses permissive defaults!\n\n")
}
@ -69,21 +69,19 @@ func printShowInstance(
t.Printf("App\n")
if config.ID != "" {
t.Printf(" ID:\t%d (%s)\n", config.Identity, config.ID)
t.Printf(" ID:\t%d (%s)\n", config.Confinement.AppID, config.ID)
} else {
t.Printf(" ID:\t%d\n", config.Identity)
t.Printf(" ID:\t%d\n", config.Confinement.AppID)
}
t.Printf(" Enablements:\t%s\n", config.Enablements.String())
if len(config.Groups) > 0 {
t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", "))
t.Printf(" Enablements:\t%s\n", config.Confinement.Enablements.String())
if len(config.Confinement.Groups) > 0 {
t.Printf(" Groups:\t%q\n", config.Confinement.Groups)
}
if config.Data != "" {
t.Printf(" Data:\t%s\n", config.Data)
}
if config.Container != nil {
container := config.Container
if container.Hostname != "" {
t.Printf(" Hostname:\t%s\n", container.Hostname)
t.Printf(" Directory:\t%s\n", config.Confinement.Outer)
if config.Confinement.Sandbox != nil {
sandbox := config.Confinement.Sandbox
if sandbox.Hostname != "" {
t.Printf(" Hostname:\t%q\n", sandbox.Hostname)
}
flags := make([]string, 0, 7)
writeFlag := func(name string, value bool) {
@ -91,40 +89,38 @@ func printShowInstance(
flags = append(flags, name)
}
}
writeFlag("userns", container.Userns)
writeFlag("devel", container.Devel)
writeFlag("net", container.Net)
writeFlag("device", container.Device)
writeFlag("tty", container.Tty)
writeFlag("mapuid", container.MapRealUID)
writeFlag("directwl", config.DirectWayland)
writeFlag("autoetc", container.AutoEtc)
writeFlag("userns", sandbox.Userns)
writeFlag("net", sandbox.Net)
writeFlag("dev", sandbox.Dev)
writeFlag("tty", sandbox.Tty)
writeFlag("mapuid", sandbox.MapRealUID)
writeFlag("directwl", sandbox.DirectWayland)
writeFlag("autoetc", sandbox.AutoEtc)
if len(flags) == 0 {
flags = append(flags, "none")
}
t.Printf(" Flags:\t%s\n", strings.Join(flags, " "))
etc := container.Etc
etc := sandbox.Etc
if etc == "" {
etc = "/etc"
}
t.Printf(" Etc:\t%s\n", etc)
if len(container.Cover) > 0 {
t.Printf(" Cover:\t%s\n", strings.Join(container.Cover, " "))
if len(sandbox.Cover) > 0 {
t.Printf(" Cover:\t%s\n", strings.Join(sandbox.Cover, " "))
}
t.Printf(" Path:\t%s\n", config.Path)
}
if len(config.Args) > 0 {
t.Printf(" Arguments:\t%s\n", strings.Join(config.Args, " "))
// Env map[string]string `json:"env"`
// Link [][2]string `json:"symlink"`
}
t.Printf(" Command:\t%s\n", strings.Join(config.Args, " "))
t.Printf("\n")
if !short {
if config.Container != nil && len(config.Container.Filesystem) > 0 {
if config.Confinement.Sandbox != nil && len(config.Confinement.Sandbox.Filesystem) > 0 {
t.Printf("Filesystem\n")
for _, f := range config.Container.Filesystem {
for _, f := range config.Confinement.Sandbox.Filesystem {
if f == nil {
continue
}
@ -152,9 +148,9 @@ func printShowInstance(
}
t.Printf("\n")
}
if len(config.ExtraPerms) > 0 {
if len(config.Confinement.ExtraPerms) > 0 {
t.Printf("Extra ACL\n")
for _, p := range config.ExtraPerms {
for _, p := range config.Confinement.ExtraPerms {
if p == nil {
continue
}
@ -182,14 +178,14 @@ func printShowInstance(
t.Printf(" Broadcast:\t%q\n", c.Broadcast)
}
}
if config.SessionBus != nil {
if config.Confinement.SessionBus != nil {
t.Printf("Session bus\n")
printDBus(config.SessionBus)
printDBus(config.Confinement.SessionBus)
t.Printf("\n")
}
if config.SystemBus != nil {
if config.Confinement.SystemBus != nil {
t.Printf("System bus\n")
printDBus(config.SystemBus)
printDBus(config.Confinement.SystemBus)
t.Printf("\n")
}
}
@ -251,26 +247,22 @@ func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON boo
t := newPrinter(output)
defer t.MustFlush()
t.Println("\tInstance\tPID\tApplication\tUptime")
t.Println("\tInstance\tPID\tApp\tUptime\tEnablements\tCommand")
for _, e := range exp {
if len(e.s) != 1<<5 {
// unreachable
log.Printf("possible store corruption: invalid instance string %s", e.s)
continue
}
as := "(No configuration information)"
var (
es = "(No confinement information)"
cs = "(No command information)"
as = "(No configuration information)"
)
if e.Config != nil {
as = strconv.Itoa(e.Config.Identity)
id := e.Config.ID
if id == "" {
id = "uk.gensokyo.fortify." + e.s[:8]
es = e.Config.Confinement.Enablements.String()
cs = fmt.Sprintf("%q", e.Config.Args)
as = strconv.Itoa(e.Config.Confinement.AppID)
}
as += " (" + id + ")"
}
t.Printf("\t%s\t%d\t%s\t%s\n",
e.s[:8], e.PID, as, now.Sub(e.Time).Round(time.Second).String())
t.Printf("\t%s\t%d\t%s\t%s\t%s\t%s\n",
e.s[:8], e.PID, as, now.Sub(e.Time).Round(time.Second).String(), strings.TrimPrefix(es, ", "), cs)
}
t.Println()
}
type expandedStateEntry struct {

View File

@ -7,12 +7,11 @@ import (
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/state"
)
var (
testID = app.ID{
testID = fst.ID{
0x8e, 0x2c, 0x76, 0xb0,
0x66, 0xda, 0xbe, 0x57,
0x4c, 0xf0, 0x73, 0xbd,
@ -39,14 +38,13 @@ func Test_printShowInstance(t *testing.T) {
{"config", nil, fst.Template(), false, false, `App
ID: 9 (org.chromium.Chromium)
Enablements: wayland, dbus, pulseaudio
Groups: video, dialout, plugdev
Data: /var/lib/fortify/u0/org.chromium.Chromium
Hostname: localhost
Flags: userns devel net device tty mapuid autoetc
Groups: ["video"]
Directory: /var/lib/persist/home/org.chromium.Chromium
Hostname: "localhost"
Flags: userns net dev tty mapuid autoetc
Etc: /etc
Cover: /var/run/nscd
Path: /run/current-system/sw/bin/chromium
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Command: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Filesystem
+/nix/store
@ -77,33 +75,39 @@ System bus
App
ID: 0
Enablements: (no enablements)
Directory:
Command:
`},
{"config flag none", nil, &fst.Config{Container: new(fst.ContainerConfig)}, false, false, `App
{"config flag none", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: new(fst.SandboxConfig)}}, false, false, `App
ID: 0
Enablements: (no enablements)
Directory:
Flags: none
Etc: /etc
Path:
Command:
`},
{"config nil entries", nil, &fst.Config{Container: &fst.ContainerConfig{Filesystem: make([]*fst.FilesystemConfig, 1)}, ExtraPerms: make([]*fst.ExtraPermConfig, 1)}, false, false, `App
{"config nil entries", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: &fst.SandboxConfig{Filesystem: make([]*fst.FilesystemConfig, 1)}, ExtraPerms: make([]*fst.ExtraPermConfig, 1)}}, false, false, `App
ID: 0
Enablements: (no enablements)
Directory:
Flags: none
Etc: /etc
Path:
Command:
Filesystem
Extra ACL
`},
{"config pd dbus see", nil, &fst.Config{SessionBus: &dbus.Config{See: []string{"org.example.test"}}}, false, false, `Warning: this configuration uses permissive defaults!
{"config pd dbus see", nil, &fst.Config{Confinement: fst.ConfinementConfig{SessionBus: &dbus.Config{See: []string{"org.example.test"}}}}, false, false, `Warning: this configuration uses permissive defaults!
App
ID: 0
Enablements: (no enablements)
Directory:
Command:
Session bus
Filter: false
@ -118,14 +122,13 @@ Session bus
App
ID: 9 (org.chromium.Chromium)
Enablements: wayland, dbus, pulseaudio
Groups: video, dialout, plugdev
Data: /var/lib/fortify/u0/org.chromium.Chromium
Hostname: localhost
Flags: userns devel net device tty mapuid autoetc
Groups: ["video"]
Directory: /var/lib/persist/home/org.chromium.Chromium
Hostname: "localhost"
Flags: userns net dev tty mapuid autoetc
Etc: /etc
Cover: /var/run/nscd
Path: /run/current-system/sw/bin/chromium
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Command: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Filesystem
+/nix/store
@ -160,6 +163,8 @@ State
App
ID: 0
Enablements: (no enablements)
Directory:
Command:
`},
@ -195,67 +200,15 @@ App
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"enablements": 13,
"session_bus": {
"see": null,
"talk": [
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
],
"own": [
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"
],
"call": {
"org.freedesktop.portal.*": "*"
},
"broadcast": {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
},
"filter": true
},
"system_bus": {
"see": null,
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"own": null,
"call": null,
"broadcast": null,
"filter": true
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"data": "/var/lib/fortify/u0/org.chromium.Chromium",
"dir": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
"path": "/var/lib/fortify/u0",
"x": true
},
{
"path": "/var/lib/fortify/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
}
],
"identity": 9,
"confinement": {
"app_id": 9,
"groups": [
"video",
"dialout",
"plugdev"
"video"
],
"container": {
"username": "chronos",
"home_inner": "/var/lib/fortify",
"home": "/var/lib/persist/home/org.chromium.Chromium",
"sandbox": {
"hostname": "localhost",
"seccomp": 32,
"devel": true,
@ -269,7 +222,7 @@ App
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"map_real_uid": true,
"device": true,
"dev": true,
"filesystem": [
{
"src": "/nix/store"
@ -305,6 +258,57 @@ App
"cover": [
"/var/run/nscd"
]
},
"extra_perms": [
{
"ensure": true,
"path": "/var/lib/fortify/u0",
"x": true
},
{
"path": "/var/lib/fortify/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
}
],
"system_bus": {
"see": null,
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"own": null,
"call": null,
"broadcast": null,
"filter": true
},
"session_bus": {
"see": null,
"talk": [
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
],
"own": [
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"
],
"call": {
"org.freedesktop.portal.*": "*"
},
"broadcast": {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
},
"filter": true
},
"enablements": 13
}
},
"time": "1970-01-01T00:00:00.000000009Z"
@ -320,67 +324,15 @@ App
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"enablements": 13,
"session_bus": {
"see": null,
"talk": [
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
],
"own": [
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"
],
"call": {
"org.freedesktop.portal.*": "*"
},
"broadcast": {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
},
"filter": true
},
"system_bus": {
"see": null,
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"own": null,
"call": null,
"broadcast": null,
"filter": true
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"data": "/var/lib/fortify/u0/org.chromium.Chromium",
"dir": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
"path": "/var/lib/fortify/u0",
"x": true
},
{
"path": "/var/lib/fortify/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
}
],
"identity": 9,
"confinement": {
"app_id": 9,
"groups": [
"video",
"dialout",
"plugdev"
"video"
],
"container": {
"username": "chronos",
"home_inner": "/var/lib/fortify",
"home": "/var/lib/persist/home/org.chromium.Chromium",
"sandbox": {
"hostname": "localhost",
"seccomp": 32,
"devel": true,
@ -394,7 +346,7 @@ App
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"map_real_uid": true,
"device": true,
"dev": true,
"filesystem": [
{
"src": "/nix/store"
@ -430,6 +382,57 @@ App
"cover": [
"/var/run/nscd"
]
},
"extra_perms": [
{
"ensure": true,
"path": "/var/lib/fortify/u0",
"x": true
},
{
"path": "/var/lib/fortify/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
}
],
"system_bus": {
"see": null,
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"own": null,
"call": null,
"broadcast": null,
"filter": true
},
"session_bus": {
"see": null,
"talk": [
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
],
"own": [
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"
],
"call": {
"org.freedesktop.portal.*": "*"
},
"broadcast": {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
},
"filter": true
},
"enablements": 13
}
}
`},
@ -455,19 +458,23 @@ func Test_printPs(t *testing.T) {
short, json bool
want string
}{
{"no entries", make(state.Entries), false, false, " Instance PID Application Uptime\n"},
{"no entries short", make(state.Entries), true, false, ""},
{"nil instance", state.Entries{testID: nil}, false, false, " Instance PID Application Uptime\n"},
{"state corruption", state.Entries{app.ID{}: testState}, false, false, " Instance PID Application Uptime\n"},
{"no entries", make(state.Entries), false, false, ` Instance PID App Uptime Enablements Command
`},
{"no entries short", make(state.Entries), true, false, ``},
{"nil instance", state.Entries{testID: nil}, false, false, ` Instance PID App Uptime Enablements Command
`},
{"state corruption", state.Entries{fst.ID{}: testState}, false, false, ` Instance PID App Uptime Enablements Command
{"valid pd", state.Entries{testID: &state.State{ID: testID, PID: 1 << 8, Config: new(fst.Config), Time: testAppTime}}, false, false, ` Instance PID Application Uptime
8e2c76b0 256 0 (uk.gensokyo.fortify.8e2c76b0) 1h2m32s
`},
{"valid", state.Entries{testID: testState}, false, false, ` Instance PID Application Uptime
8e2c76b0 3735928559 9 (org.chromium.Chromium) 1h2m32s
{"valid", state.Entries{testID: testState}, false, false, ` Instance PID App Uptime Enablements Command
8e2c76b0 3735928559 9 1h2m32s wayland, dbus, pulseaudio ["chromium" "--ignore-gpu-blocklist" "--disable-smooth-scrolling" "--enable-features=UseOzonePlatform" "--ozone-platform=wayland"]
`},
{"valid short", state.Entries{testID: testState}, true, false, `8e2c76b0
`},
{"valid short", state.Entries{testID: testState}, true, false, "8e2c76b0\n"},
{"valid json", state.Entries{testID: testState}, false, true, `{
"8e2c76b066dabe574cf073bdb46eb5c1": {
"instance": [
@ -499,67 +506,15 @@ func Test_printPs(t *testing.T) {
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"enablements": 13,
"session_bus": {
"see": null,
"talk": [
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
],
"own": [
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"
],
"call": {
"org.freedesktop.portal.*": "*"
},
"broadcast": {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
},
"filter": true
},
"system_bus": {
"see": null,
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"own": null,
"call": null,
"broadcast": null,
"filter": true
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"data": "/var/lib/fortify/u0/org.chromium.Chromium",
"dir": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
"path": "/var/lib/fortify/u0",
"x": true
},
{
"path": "/var/lib/fortify/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
}
],
"identity": 9,
"confinement": {
"app_id": 9,
"groups": [
"video",
"dialout",
"plugdev"
"video"
],
"container": {
"username": "chronos",
"home_inner": "/var/lib/fortify",
"home": "/var/lib/persist/home/org.chromium.Chromium",
"sandbox": {
"hostname": "localhost",
"seccomp": 32,
"devel": true,
@ -573,7 +528,7 @@ func Test_printPs(t *testing.T) {
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"map_real_uid": true,
"device": true,
"dev": true,
"filesystem": [
{
"src": "/nix/store"
@ -609,6 +564,57 @@ func Test_printPs(t *testing.T) {
"cover": [
"/var/run/nscd"
]
},
"extra_perms": [
{
"ensure": true,
"path": "/var/lib/fortify/u0",
"x": true
},
{
"path": "/var/lib/fortify/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
}
],
"system_bus": {
"see": null,
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"own": null,
"call": null,
"broadcast": null,
"filter": true
},
"session_bus": {
"see": null,
"talk": [
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
],
"own": [
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"
],
"call": {
"org.freedesktop.portal.*": "*"
},
"broadcast": {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
},
"filter": true
},
"enablements": 13
}
},
"time": "1970-01-01T00:00:00.000000009Z"

View File

@ -27,18 +27,18 @@ const (
FAllowNet
)
func (flags HardeningFlags) seccomp(opts seccomp.FilterOpts) seccomp.FilterOpts {
func (flags HardeningFlags) seccomp(opts seccomp.SyscallOpts) seccomp.SyscallOpts {
if flags&FSyscallCompat == 0 {
opts |= seccomp.FilterExt
opts |= seccomp.FlagExt
}
if flags&FAllowDevel == 0 {
opts |= seccomp.FilterDenyDevel
opts |= seccomp.FlagDenyDevel
}
if flags&FAllowUserns == 0 {
opts |= seccomp.FilterDenyNS
opts |= seccomp.FlagDenyNS
}
if flags&FAllowTTY == 0 {
opts |= seccomp.FilterDenyTTY
opts |= seccomp.FlagDenyTTY
}
return opts
}
@ -95,15 +95,23 @@ type (
// Sequential container setup ops.
*Ops
// Extra seccomp options.
Seccomp seccomp.FilterOpts
Seccomp seccomp.SyscallOpts
// Permission bits of newly created parent directories.
// The zero value is interpreted as 0755.
ParentPerm os.FileMode
// Retain CAP_SYS_ADMIN.
Privileged bool
Flags HardeningFlags
}
Ops []Op
Op interface {
early(params *Params) error
apply(params *Params) error
prefix() string
Is(op Op) bool
fmt.Stringer
}
)
func (p *Container) Start() error {

View File

@ -164,7 +164,7 @@ func e(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoE
func TestContainerString(t *testing.T) {
container := sandbox.New(context.TODO(), "ldd", "/usr/bin/env")
container.Flags |= sandbox.FAllowDevel
container.Seccomp |= seccomp.FilterMultiarch
container.Seccomp |= seccomp.FlagMultiarch
want := `argv: ["ldd" "/usr/bin/env"], flags: 0x2, seccomp: 0x2e`
if got := container.String(); got != want {
t.Errorf("String: %s, want %s", got, want)

View File

@ -45,6 +45,10 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
log.Fatal("this process must run as pid 1")
}
/*
receive setup payload
*/
var (
params initParams
closeSetup func() error
@ -107,6 +111,10 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
// cache sysctl before pivot_root
LastCap()
/*
set up mount points from intermediate root
*/
if err := syscall.Mount("", "/", "",
syscall.MS_SILENT|syscall.MS_SLAVE|syscall.MS_REC,
""); err != nil {
@ -147,7 +155,6 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
if err := os.Mkdir(hostDir, 0755); err != nil {
log.Fatalf("%v", err)
}
// pivot_root uncovers basePath in hostDir
if err := syscall.PivotRoot(basePath, hostDir); err != nil {
log.Fatalf("cannot pivot into intermediate root: %v", err)
}
@ -166,7 +173,10 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
}
}
// setup requiring host root complete at this point
/*
pivot to sysroot
*/
if err := syscall.Mount(hostDir, hostDir, "",
syscall.MS_SILENT|syscall.MS_REC|syscall.MS_PRIVATE,
""); err != nil {
@ -206,33 +216,24 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
}
}
/*
caps/securebits and seccomp filter
*/
if _, _, errno := syscall.Syscall(PR_SET_NO_NEW_PRIVS, 1, 0, 0); errno != 0 {
log.Fatalf("prctl(PR_SET_NO_NEW_PRIVS): %v", errno)
}
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0); errno != 0 {
log.Fatalf("cannot clear the ambient capability set: %v", errno)
}
for i := uintptr(0); i <= LastCap(); i++ {
if params.Privileged && i == CAP_SYS_ADMIN {
continue
}
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_CAPBSET_DROP, i, 0); errno != 0 {
log.Fatalf("cannot drop capability from bonding set: %v", errno)
}
}
var keep [2]uint32
if params.Privileged {
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_SYS_ADMIN); errno != 0 {
log.Fatalf("cannot raise CAP_SYS_ADMIN: %v", errno)
}
}
if err := capset(
&capHeader{_LINUX_CAPABILITY_VERSION_3, 0},
&[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}},
&[2]capData{{0, 0, 0}, {0, 0, 0}},
); err != nil {
log.Fatalf("cannot capset: %v", err)
}
@ -241,13 +242,20 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
log.Fatalf("cannot load syscall filter: %v", err)
}
/*
pass through extra files
*/
extraFiles := make([]*os.File, params.Count)
for i := range extraFiles {
// setup fd is placed before all extra files
extraFiles[i] = os.NewFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
}
syscall.Umask(oldmask)
/*
prepare initial process
*/
cmd := exec.Command(params.Path)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Args = params.Args
@ -260,11 +268,22 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
}
msg.Suspend()
/*
close setup pipe
*/
if err := closeSetup(); err != nil {
log.Println("cannot close setup pipe:", err)
// not fatal
}
/*
perform init duties
*/
sig := make(chan os.Signal, 2)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
type winfo struct {
wpid int
wstatus syscall.WaitStatus
@ -301,10 +320,6 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
close(done)
}()
// handle signals to dump withheld messages
sig := make(chan os.Signal, 2)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
// closed after residualProcessTimeout has elapsed after initial process death
timeout := make(chan struct{})
@ -317,6 +332,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
} else {
msg.Verbosef("terminating on %s", s.String())
}
msg.BeforeExit()
os.Exit(0)
case w := <-info:
if w.wpid == cmd.Process.Pid {

View File

@ -8,16 +8,11 @@ import (
"git.gensokyo.uk/security/fortify/helper/proc"
)
const (
PresetStrict = FilterExt | FilterDenyNS | FilterDenyTTY | FilterDenyDevel
PresetCommon = PresetStrict | FilterMultiarch
)
// New returns an inactive Encoder instance.
func New(opts FilterOpts) *Encoder { return &Encoder{newExporter(opts)} }
func New(opts SyscallOpts) *Encoder { return &Encoder{newExporter(opts)} }
// Load loads a filter into the kernel.
func Load(opts FilterOpts) error { return buildFilter(-1, opts) }
func Load(opts SyscallOpts) error { return buildFilter(-1, opts) }
/*
An Encoder writes a BPF program to an output stream.
@ -47,11 +42,11 @@ func (e *Encoder) Close() error {
}
// NewFile returns an instance of exporter implementing [proc.File].
func NewFile(opts FilterOpts) proc.File { return &File{opts: opts} }
func NewFile(opts SyscallOpts) proc.File { return &File{opts: opts} }
// File implements [proc.File] and provides access to the read end of exporter pipe.
type File struct {
opts FilterOpts
opts SyscallOpts
proc.BaseFile
}

View File

@ -7,7 +7,7 @@ import (
)
type exporter struct {
opts FilterOpts
opts SyscallOpts
r, w *os.File
prepareOnce sync.Once
@ -53,6 +53,6 @@ func (e *exporter) closeWrite() error {
return e.closeErr
}
func newExporter(opts FilterOpts) *exporter {
func newExporter(opts SyscallOpts) *exporter {
return &exporter{opts: opts}
}

View File

@ -14,7 +14,7 @@ import (
func TestExport(t *testing.T) {
testCases := []struct {
name string
opts seccomp.FilterOpts
opts seccomp.SyscallOpts
want []byte
wantErr bool
}{
@ -28,7 +28,7 @@ func TestExport(t *testing.T) {
0xa7, 0x9b, 0x07, 0x0e, 0x04, 0xc0, 0xee, 0x9a,
0xcd, 0xf5, 0x8f, 0x55, 0xcf, 0xa8, 0x15, 0xa5,
}, false},
{"base", seccomp.FilterExt, []byte{
{"base", seccomp.FlagExt, []byte{
0xdc, 0x7f, 0x2e, 0x1c, 0x5e, 0x82, 0x9b, 0x79,
0xeb, 0xb7, 0xef, 0xc7, 0x59, 0x15, 0x0f, 0x54,
0xa8, 0x3a, 0x75, 0xc8, 0xdf, 0x6f, 0xee, 0x4d,
@ -38,10 +38,10 @@ func TestExport(t *testing.T) {
0x1d, 0xb0, 0x5d, 0x90, 0x99, 0x7c, 0x86, 0x59,
0xb9, 0x58, 0x91, 0x20, 0x6a, 0xc9, 0x95, 0x2d,
}, false},
{"everything", seccomp.FilterExt |
seccomp.FilterDenyNS | seccomp.FilterDenyTTY | seccomp.FilterDenyDevel |
seccomp.FilterMultiarch | seccomp.FilterLinux32 | seccomp.FilterCan |
seccomp.FilterBluetooth, []byte{
{"everything", seccomp.FlagExt |
seccomp.FlagDenyNS | seccomp.FlagDenyTTY | seccomp.FlagDenyDevel |
seccomp.FlagMultiarch | seccomp.FlagLinux32 | seccomp.FlagCan |
seccomp.FlagBluetooth, []byte{
0xe9, 0x9d, 0xd3, 0x45, 0xe1, 0x95, 0x41, 0x34,
0x73, 0xd3, 0xcb, 0xee, 0x07, 0xb4, 0xed, 0x57,
0xb9, 0x08, 0xbf, 0xa8, 0x9e, 0xa2, 0x07, 0x2f,
@ -51,7 +51,8 @@ func TestExport(t *testing.T) {
0x4c, 0x02, 0x4e, 0xd4, 0x88, 0x50, 0xbe, 0x69,
0xb6, 0x8a, 0x9a, 0x4c, 0x5f, 0x53, 0xa9, 0xdb,
}, false},
{"strict", seccomp.PresetStrict, []byte{
{"strict", seccomp.FlagExt |
seccomp.FlagDenyNS | seccomp.FlagDenyTTY | seccomp.FlagDenyDevel, []byte{
0xe8, 0x80, 0x29, 0x8d, 0xf2, 0xbd, 0x67, 0x51,
0xd0, 0x04, 0x0f, 0xc2, 0x1b, 0xc0, 0xed, 0x4c,
0x00, 0xf9, 0x5d, 0xc0, 0xd7, 0xba, 0x50, 0x6c,
@ -62,7 +63,7 @@ func TestExport(t *testing.T) {
0x14, 0x89, 0x60, 0xfb, 0xd3, 0x5c, 0xd7, 0x35,
}, false},
{"strict compat", 0 |
seccomp.FilterDenyNS | seccomp.FilterDenyTTY | seccomp.FilterDenyDevel, []byte{
seccomp.FlagDenyNS | seccomp.FlagDenyTTY | seccomp.FlagDenyDevel, []byte{
0x39, 0x87, 0x1b, 0x93, 0xff, 0xaf, 0xc8, 0xb9,
0x79, 0xfc, 0xed, 0xc0, 0xb0, 0xc3, 0x7b, 0x9e,
0x03, 0x92, 0x2f, 0x5b, 0x02, 0x74, 0x8d, 0xc5,
@ -72,16 +73,6 @@ func TestExport(t *testing.T) {
0x80, 0x8b, 0x1a, 0x6f, 0x84, 0xf3, 0x2b, 0xbd,
0xe1, 0xaa, 0x02, 0xae, 0x30, 0xee, 0xdc, 0xfa,
}, false},
{"fortify default", seccomp.FilterExt | seccomp.FilterDenyDevel, []byte{
0xc6, 0x98, 0xb0, 0x81, 0xff, 0x95, 0x7a, 0xfe,
0x17, 0xa6, 0xd9, 0x43, 0x74, 0x53, 0x7d, 0x37,
0xf2, 0xa6, 0x3f, 0x6f, 0x9d, 0xd7, 0x5d, 0xa7,
0x54, 0x65, 0x42, 0x40, 0x7a, 0x9e, 0x32, 0x47,
0x6e, 0xbd, 0xa3, 0x31, 0x2b, 0xa7, 0x78, 0x5d,
0x7f, 0x61, 0x85, 0x42, 0xbc, 0xfa, 0xf2, 0x7c,
0xa2, 0x7d, 0xcc, 0x2d, 0xdd, 0xba, 0x85, 0x20,
0x69, 0xd2, 0x8b, 0xcf, 0xe8, 0xca, 0xd3, 0x9a,
}, false},
}
buf := make([]byte, 8)
@ -137,10 +128,10 @@ func TestExport(t *testing.T) {
func BenchmarkExport(b *testing.B) {
buf := make([]byte, 8)
for i := 0; i < b.N; i++ {
e := seccomp.New(seccomp.FilterExt |
seccomp.FilterDenyNS | seccomp.FilterDenyTTY | seccomp.FilterDenyDevel |
seccomp.FilterMultiarch | seccomp.FilterLinux32 | seccomp.FilterCan |
seccomp.FilterBluetooth)
e := seccomp.New(seccomp.FlagExt |
seccomp.FlagDenyNS | seccomp.FlagDenyTTY | seccomp.FlagDenyDevel |
seccomp.FlagMultiarch | seccomp.FlagLinux32 | seccomp.FlagCan |
seccomp.FlagBluetooth)
if _, err := io.CopyBuffer(io.Discard, e, buf); err != nil {
b.Fatalf("cannot export: %v", err)
}

View File

@ -22,8 +22,8 @@ func GetOutput() func(v ...any) {
}
}
//export f_println
func f_println(v *C.char) {
//export F_println
func F_println(v *C.char) {
if fp := printlnP.Load(); fp != nil {
(*fp)(C.GoString(v))
}

View File

@ -28,7 +28,7 @@ struct f_syscall_act {
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
#define SECCOMP_RULESET_ADD(ruleset) do { \
if (opts & F_VERBOSE) f_println("adding seccomp ruleset \"" #ruleset "\""); \
if (opts & F_VERBOSE) F_println("adding seccomp ruleset \"" #ruleset "\""); \
for (int i = 0; i < LEN(ruleset); i++) { \
assert(ruleset[i].m_errno == EPERM || ruleset[i].m_errno == ENOSYS); \
\
@ -47,7 +47,7 @@ struct f_syscall_act {
} \
} while (0)
int32_t f_build_filter(int *ret_p, int fd, uint32_t arch, uint32_t multiarch, f_filter_opts opts) {
int32_t f_build_filter(int *ret_p, int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts) {
int32_t res = 0; // refer to resErr for meaning
int allow_multiarch = opts & F_MULTIARCH;
int allowed_personality = PER_LINUX;
@ -209,7 +209,7 @@ int32_t f_build_filter(int *ret_p, int fd, uint32_t arch, uint32_t multiarch, f_
struct
{
int family;
f_filter_opts flags_mask;
f_syscall_opts flags_mask;
} socket_family_allowlist[] = {
// NOTE: Keep in numerical order
{ AF_UNSPEC, 0 },

View File

@ -17,7 +17,7 @@ typedef enum {
F_LINUX32 = 1 << 6,
F_CAN = 1 << 7,
F_BLUETOOTH = 1 << 8,
} f_filter_opts;
} f_syscall_opts;
extern void f_println(char *v);
int32_t f_build_filter(int *ret_p, int fd, uint32_t arch, uint32_t multiarch, f_filter_opts opts);
extern void F_println(char *v);
int32_t f_build_filter(int *ret_p, int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts);

View File

@ -1,4 +1,3 @@
// Package seccomp provides filter presets and high level wrappers around libseccomp.
package seccomp
/*
@ -7,7 +6,6 @@ package seccomp
#include "seccomp-build.h"
*/
import "C"
import (
"errors"
"fmt"
@ -57,29 +55,29 @@ var resPrefix = [...]string{
7: "seccomp_load failed",
}
type FilterOpts = C.f_filter_opts
type SyscallOpts = C.f_syscall_opts
const (
filterVerbose FilterOpts = C.F_VERBOSE
// FilterExt are project-specific extensions.
FilterExt FilterOpts = C.F_EXT
// FilterDenyNS denies namespace setup syscalls.
FilterDenyNS FilterOpts = C.F_DENY_NS
// FilterDenyTTY denies faking input.
FilterDenyTTY FilterOpts = C.F_DENY_TTY
// FilterDenyDevel denies development-related syscalls.
FilterDenyDevel FilterOpts = C.F_DENY_DEVEL
// FilterMultiarch allows multiarch/emulation.
FilterMultiarch FilterOpts = C.F_MULTIARCH
// FilterLinux32 sets PER_LINUX32.
FilterLinux32 FilterOpts = C.F_LINUX32
// FilterCan allows AF_CAN.
FilterCan FilterOpts = C.F_CAN
// FilterBluetooth allows AF_BLUETOOTH.
FilterBluetooth FilterOpts = C.F_BLUETOOTH
flagVerbose SyscallOpts = C.F_VERBOSE
// FlagExt are project-specific extensions.
FlagExt SyscallOpts = C.F_EXT
// FlagDenyNS denies namespace setup syscalls.
FlagDenyNS SyscallOpts = C.F_DENY_NS
// FlagDenyTTY denies faking input.
FlagDenyTTY SyscallOpts = C.F_DENY_TTY
// FlagDenyDevel denies development-related syscalls.
FlagDenyDevel SyscallOpts = C.F_DENY_DEVEL
// FlagMultiarch allows multiarch/emulation.
FlagMultiarch SyscallOpts = C.F_MULTIARCH
// FlagLinux32 sets PER_LINUX32.
FlagLinux32 SyscallOpts = C.F_LINUX32
// FlagCan allows AF_CAN.
FlagCan SyscallOpts = C.F_CAN
// FlagBluetooth allows AF_BLUETOOTH.
FlagBluetooth SyscallOpts = C.F_BLUETOOTH
)
func buildFilter(fd int, opts FilterOpts) error {
func buildFilter(fd int, opts SyscallOpts) error {
var (
arch C.uint32_t = 0
multiarch C.uint32_t = 0
@ -100,7 +98,7 @@ func buildFilter(fd int, opts FilterOpts) error {
// this removes repeated transitions between C and Go execution
// when producing log output via F_println and CPrintln is nil
if fp := printlnP.Load(); fp != nil {
opts |= filterVerbose
opts |= flagVerbose
}
var ret C.int

View File

@ -13,22 +13,6 @@ import (
"unsafe"
)
type (
Ops []Op
Op interface {
// early is called in host root.
early(params *Params) error
// apply is called in intermediate root.
apply(params *Params) error
prefix() string
Is(op Op) bool
fmt.Stringer
}
)
func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
func init() { gob.Register(new(BindMount)) }
// BindMount bind mounts host path Source on container path Target.
@ -440,60 +424,3 @@ func (f *Ops) PlaceP(name string, dataP **[]byte) *Ops {
*f = append(*f, t)
return f
}
func init() { gob.Register(new(AutoEtc)) }
// AutoEtc expands host /etc into a toplevel symlink mirror with /etc semantics.
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
type AutoEtc struct{ Prefix string }
func (e *AutoEtc) early(*Params) error { return nil }
func (e *AutoEtc) apply(*Params) error {
const target = sysrootPath + "/etc/"
rel := e.hostRel() + "/"
if err := os.MkdirAll(target, 0755); err != nil {
return wrapErrSelf(err)
}
if d, err := os.ReadDir(toSysroot(e.hostPath())); err != nil {
return wrapErrSelf(err)
} else {
for _, ent := range d {
n := ent.Name()
switch n {
case ".host":
case "passwd":
case "group":
case "mtab":
if err = os.Symlink("/proc/mounts", target+n); err != nil {
return wrapErrSelf(err)
}
default:
if err = os.Symlink(rel+n, target+n); err != nil {
return wrapErrSelf(err)
}
}
}
}
return nil
}
func (e *AutoEtc) hostPath() string { return "/etc/" + e.hostRel() }
func (e *AutoEtc) hostRel() string { return ".host/" + e.Prefix }
func (e *AutoEtc) Is(op Op) bool {
ve, ok := op.(*AutoEtc)
return ok && ((e == nil && ve == nil) || (e != nil && ve != nil && *e == *ve))
}
func (*AutoEtc) prefix() string { return "setting up" }
func (e *AutoEtc) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }
func (f *Ops) Etc(host, prefix string) *Ops {
e := &AutoEtc{prefix}
f.Mkdir("/etc", 0755)
f.Bind(host, e.hostPath(), 0)
*f = append(*f, e)
return f
}

View File

@ -11,7 +11,6 @@ const (
PR_SET_NO_NEW_PRIVS = 0x26
CAP_SYS_ADMIN = 0x15
CAP_SETPCAP = 0x8
)
const (
@ -31,9 +30,10 @@ func SetDumpable(dumpable uintptr) error {
const (
_LINUX_CAPABILITY_VERSION_3 = 0x20080522
PR_CAP_AMBIENT = 0x2f
PR_CAP_AMBIENT_RAISE = 0x2
PR_CAP_AMBIENT_CLEAR_ALL = 0x4
PR_CAP_AMBIENT = 47
PR_CAP_AMBIENT_CLEAR_ALL = 4
CAP_SETPCAP = 8
)
type (
@ -49,12 +49,6 @@ type (
}
)
// See CAP_TO_INDEX in linux/capability.h:
func capToIndex(cap uintptr) uintptr { return cap >> 5 }
// See CAP_TO_MASK in linux/capability.h:
func capToMask(cap uintptr) uint32 { return 1 << uint(cap&31) }
func capset(hdrp *capHeader, datap *[2]capData) error {
if _, _, errno := syscall.Syscall(syscall.SYS_CAPSET,
uintptr(unsafe.Pointer(hdrp)),

View File

@ -4,6 +4,12 @@
config,
...
}:
let
testCases = import ./sandbox/case {
inherit (pkgs) lib callPackage foot;
inherit (config.environment.fortify.package) version;
};
in
{
users.users = {
alice = {
@ -102,6 +108,10 @@
home-manager = _: _: { home.stateVersion = "23.05"; };
apps = [
testCases.preset
testCases.tty
testCases.mapuid
{
name = "ne-foot";
verbose = true;

View File

@ -7,15 +7,10 @@ in the public sandbox/vfs package. Files in this package are excluded by the bui
package sandbox
import (
"crypto/sha512"
"encoding/hex"
"encoding/json"
"errors"
"io/fs"
"log"
"os"
"syscall"
"time"
)
var (
@ -28,7 +23,6 @@ func printf(format string, v ...any) { printfFunc(format, v...) }
func fatalf(format string, v ...any) { fatalfFunc(format, v...) }
type TestCase struct {
Env []string `json:"env"`
FS *FS `json:"fs"`
Mount []*MountinfoEntry `json:"mount"`
Seccomp bool `json:"seccomp"`
@ -40,46 +34,13 @@ type T struct {
MountsPath string
}
func (t *T) MustCheckFile(wantFilePath, markerPath string) {
func (t *T) MustCheckFile(wantFilePath string) {
var want *TestCase
mustDecode(wantFilePath, &want)
t.MustCheck(want)
if _, err := os.Create(markerPath); err != nil {
fatalf("cannot create success marker: %v", err)
}
}
func (t *T) MustCheck(want *TestCase) {
if want.Env != nil {
var (
fail bool
i int
got string
)
for i, got = range os.Environ() {
if i == len(want.Env) {
fatalf("got more than %d environment variables", len(want.Env))
}
if got != want.Env[i] {
fail = true
printf("[FAIL] %s", got)
} else {
printf("[ OK ] %s", got)
}
}
i++
if i != len(want.Env) {
fatalf("got %d environment variables, want %d", i, len(want.Env))
}
if fail {
fatalf("[FAIL] some environment variables did not match")
}
} else {
printf("[SKIP] skipping environ check")
}
if want.FS != nil && t.FS != nil {
if err := want.FS.Compare(".", t.FS); err != nil {
fatalf("%v", err)
@ -121,7 +82,7 @@ func (t *T) MustCheck(want *TestCase) {
}
if want.Seccomp {
if trySyscalls() != nil {
if TrySyscalls() != nil {
os.Exit(1)
}
} else {
@ -129,81 +90,6 @@ func (t *T) MustCheck(want *TestCase) {
}
}
func MustCheckFilter(pid int, want string) {
err := CheckFilter(pid, want)
if err == nil {
return
}
var perr *ptraceError
if !errors.As(err, &perr) {
fatalf("%s", err)
}
switch perr.op {
case "PTRACE_ATTACH":
fatalf("cannot attach to process %d: %v", pid, err)
case "PTRACE_SECCOMP_GET_FILTER":
if perr.errno == syscall.ENOENT {
fatalf("seccomp filter not installed for process %d", pid)
}
fatalf("cannot get filter: %v", err)
default:
fatalf("cannot check filter: %v", err)
}
*(*int)(nil) = 0 // not reached
}
func CheckFilter(pid int, want string) error {
if err := ptraceAttach(pid); err != nil {
return err
}
defer func() {
if err := ptraceDetach(pid); err != nil {
printf("cannot detach from process %d: %v", pid, err)
}
}()
h := sha512.New()
{
getFilter:
buf, err := getFilter[[8]byte](pid, 0)
/* this is not how ESRCH should be handled: the manpage advises the
use of waitpid, however that is not applicable for attaching to an
arbitrary process, and spawning target process here is not easily
possible under the current testing framework;
despite checking for /proc/pid/status indicating state t (tracing stop),
it does not appear to be directly related to the internal state used to
determine whether a process is ready to accept ptrace operations, it also
introduces a TOCTOU that is irrelevant in the testing vm; this behaviour
is kept anyway as it reduces the average iterations required here;
since this code is only ever compiled into the test program, whatever
implications this ugliness might have should not hurt anyone */
if errors.Is(err, syscall.ESRCH) {
time.Sleep(100 * time.Millisecond)
goto getFilter
}
if err != nil {
return err
}
for _, b := range buf {
h.Write(b[:])
}
}
if got := hex.EncodeToString(h.Sum(nil)); got != want {
printf("[FAIL] %s", got)
return syscall.ENOTRECOVERABLE
} else {
printf("[ OK ] %s", got)
return nil
}
}
func mustDecode(wantFilePath string, v any) {
if f, err := os.Open(wantFilePath); err != nil {
fatalf("cannot open %q: %v", wantFilePath, err)

30
test/sandbox/assert.nix Normal file
View File

@ -0,0 +1,30 @@
{
writeText,
buildGoModule,
pkg-config,
util-linux,
version,
}:
buildGoModule {
pname = "check-sandbox";
inherit version;
src = ../.;
vendorHash = null;
buildInputs = [ util-linux ];
nativeBuildInputs = [ pkg-config ];
preBuild = ''
go mod init git.gensokyo.uk/security/fortify/test >& /dev/null
cp ${writeText "main.go" ''
package main
import "os"
import "git.gensokyo.uk/security/fortify/test/sandbox"
func main() { (&sandbox.T{FS: os.DirFS("/")}).MustCheckFile(os.Args[1]) }
''} main.go
'';
}

View File

@ -1,4 +1,10 @@
lib: testProgram:
{
lib,
callPackage,
foot,
version,
}:
let
fs = mode: dir: data: {
mode = lib.fromHexString mode;
@ -23,6 +29,8 @@ let
;
};
checkSandbox = callPackage ../. { inherit version; };
callTestCase =
path:
let
@ -37,19 +45,14 @@ let
{
name = "check-sandbox-${tc.name}";
verbose = true;
inherit (tc) tty device mapRealUid;
share = testProgram;
inherit (tc) tty mapRealUid;
share = foot;
packages = [ ];
path = "${testProgram}/bin/fortify-test";
args = [
"test"
(toString (builtins.toFile "fortify-${tc.name}-want.json" (builtins.toJSON tc.want)))
];
command = builtins.toString (checkSandbox tc.name tc.want);
};
in
{
preset = callTestCase ./preset.nix;
tty = callTestCase ./tty.nix;
mapuid = callTestCase ./mapuid.nix;
device = callTestCase ./device.nix;
}

View File

@ -1,203 +0,0 @@
{
fs,
ent,
ignore,
}:
{
name = "device";
tty = false;
device = true;
mapRealUid = false;
want = {
env = [
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
"HOME=/var/lib/fortify/u0/a4"
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
"SHELL=/run/current-system/sw/bin/bash"
"TERM=linux"
"USER=u0_a4"
"WAYLAND_DISPLAY=wayland-0"
"XDG_RUNTIME_DIR=/run/user/65534"
"XDG_SESSION_CLASS=user"
"XDG_SESSION_TYPE=tty"
];
fs = fs "dead" {
".fortify" = fs "800001ed" { } null;
bin = fs "800001ed" { sh = fs "80001ff" null null; } null;
dev = fs "800001ed" null null;
etc = fs "800001ed" {
".clean" = fs "80001ff" null null;
".host" = fs "800001c0" null null;
".updated" = fs "80001ff" null null;
"NIXOS" = fs "80001ff" null null;
"X11" = fs "80001ff" null null;
"alsa" = fs "80001ff" null null;
"bashrc" = fs "80001ff" null null;
"binfmt.d" = fs "80001ff" null null;
"dbus-1" = fs "80001ff" null null;
"default" = fs "80001ff" null null;
"dhcpcd.exit-hook" = fs "80001ff" null null;
"fonts" = fs "80001ff" null null;
"fstab" = fs "80001ff" null null;
"fsurc" = fs "80001ff" null null;
"fuse.conf" = fs "80001ff" null null;
"group" = fs "180" null "fortify:x:65534:\n";
"host.conf" = fs "80001ff" null null;
"hostname" = fs "80001ff" null null;
"hosts" = fs "80001ff" null null;
"inputrc" = fs "80001ff" null null;
"issue" = fs "80001ff" null null;
"kbd" = fs "80001ff" null null;
"locale.conf" = fs "80001ff" null null;
"login.defs" = fs "80001ff" null null;
"lsb-release" = fs "80001ff" null null;
"lvm" = fs "80001ff" null null;
"machine-id" = fs "80001ff" null null;
"man_db.conf" = fs "80001ff" null null;
"modprobe.d" = fs "80001ff" null null;
"modules-load.d" = fs "80001ff" null null;
"mtab" = fs "80001ff" null null;
"nanorc" = fs "80001ff" null null;
"netgroup" = fs "80001ff" null null;
"nix" = fs "80001ff" null null;
"nixos" = fs "80001ff" null null;
"nscd.conf" = fs "80001ff" null null;
"nsswitch.conf" = fs "80001ff" null null;
"os-release" = fs "80001ff" null null;
"pam" = fs "80001ff" null null;
"pam.d" = fs "80001ff" null null;
"passwd" = fs "180" null "u0_a4:x:65534:65534:Fortify:/var/lib/fortify/u0/a4:/run/current-system/sw/bin/bash\n";
"pipewire" = fs "80001ff" null null;
"pki" = fs "80001ff" null null;
"polkit-1" = fs "80001ff" null null;
"profile" = fs "80001ff" null null;
"protocols" = fs "80001ff" null null;
"resolv.conf" = fs "80001ff" null null;
"resolvconf.conf" = fs "80001ff" null null;
"rpc" = fs "80001ff" null null;
"services" = fs "80001ff" null null;
"set-environment" = fs "80001ff" null null;
"shadow" = fs "80001ff" null null;
"shells" = fs "80001ff" null null;
"ssh" = fs "80001ff" null null;
"ssl" = fs "80001ff" null null;
"static" = fs "80001ff" null null;
"subgid" = fs "80001ff" null null;
"subuid" = fs "80001ff" null null;
"sudoers" = fs "80001ff" null null;
"sway" = fs "80001ff" null null;
"sysctl.d" = fs "80001ff" null null;
"systemd" = fs "80001ff" null null;
"terminfo" = fs "80001ff" null null;
"tmpfiles.d" = fs "80001ff" null null;
"udev" = fs "80001ff" null null;
"vconsole.conf" = fs "80001ff" null null;
"xdg" = fs "80001ff" null null;
"zoneinfo" = fs "80001ff" null null;
} null;
nix = fs "800001c0" { store = fs "801001fd" null null; } null;
proc = fs "8000016d" null null;
run = fs "800001ed" {
current-system = fs "80001ff" null null;
opengl-driver = fs "80001ff" null null;
user = fs "800001ed" {
"65534" = fs "800001c0" {
bus = fs "10001fd" null null;
pulse = fs "800001c0" { native = fs "10001b6" null null; } null;
wayland-0 = fs "1000038" null null;
} null;
} null;
} null;
sys = fs "800001c0" {
block = fs "800001ed" {
fd0 = fs "80001ff" null null;
loop0 = fs "80001ff" null null;
loop1 = fs "80001ff" null null;
loop2 = fs "80001ff" null null;
loop3 = fs "80001ff" null null;
loop4 = fs "80001ff" null null;
loop5 = fs "80001ff" null null;
loop6 = fs "80001ff" null null;
loop7 = fs "80001ff" null null;
sr0 = fs "80001ff" null null;
vda = fs "80001ff" null null;
} null;
bus = fs "800001ed" null null;
class = fs "800001ed" null null;
dev = fs "800001ed" {
block = fs "800001ed" null null;
char = fs "800001ed" null null;
} null;
devices = fs "800001ed" null null;
} null;
tmp = fs "800001f8" { } null;
usr = fs "800001c0" { bin = fs "800001ed" { env = fs "80001ff" null null; } null; } null;
var = fs "800001c0" {
lib = fs "800001c0" {
fortify = fs "800001c0" {
u0 = fs "800001c0" {
a4 = fs "800001c0" {
".cache" = fs "800001ed" { ".keep" = fs "80001ff" null ""; } null;
".config" = fs "800001ed" { "environment.d" = fs "800001ed" { "10-home-manager.conf" = fs "80001ff" null null; } null; } null;
".local" = fs "800001ed" {
state = fs "800001ed" {
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
nix = fs "800001ed" {
profiles = fs "800001ed" {
home-manager = fs "80001ff" null null;
home-manager-1-link = fs "80001ff" null null;
profile = fs "80001ff" null null;
profile-1-link = fs "80001ff" null null;
} null;
} null;
} null;
} null;
".nix-defexpr" = fs "800001ed" {
channels = fs "80001ff" null null;
channels_root = fs "80001ff" null null;
} null;
".nix-profile" = fs "80001ff" null null;
} null;
} null;
} null;
} null;
run = fs "800001ed" { nscd = fs "800001ed" { } null; } null;
} null;
} null;
mount = [
(ent "/sysroot" "/" "rw,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000004,gid=1000004")
(ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw")
(ent "/" "/.fortify" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000004,gid=1000004")
(ent "/" "/dev" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666")
(ent "/" "/dev/shm" "rw,nosuid,nodev" "tmpfs" "tmpfs" ignore)
(ent "/" ignore ignore ignore ignore ignore) # order not deterministic
(ent "/" ignore ignore ignore ignore ignore)
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
(ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/class" "/sys/class" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000004,gid=1000004")
(ent "/" "/run/user/65534" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8192k,mode=700,uid=1000004,gid=1000004")
(ent "/tmp/fortify.1000/tmpdir/4" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/lib/fortify/u0/a4" "/var/lib/fortify/u0/a4" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000004,gid=1000004")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000004,gid=1000004")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/var/run/nscd" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8k,mode=755,uid=1000004,gid=1000004")
];
seccomp = true;
};
}

View File

@ -6,25 +6,13 @@
{
name = "mapuid";
tty = false;
device = false;
mapRealUid = true;
want = {
env = [
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus"
"HOME=/var/lib/fortify/u0/a3"
"PULSE_SERVER=unix:/run/user/1000/pulse/native"
"SHELL=/run/current-system/sw/bin/bash"
"TERM=linux"
"USER=u0_a3"
"WAYLAND_DISPLAY=wayland-0"
"XDG_RUNTIME_DIR=/run/user/1000"
"XDG_SESSION_CLASS=user"
"XDG_SESSION_TYPE=tty"
];
fs = fs "dead" {
".fortify" = fs "800001ed" { } null;
".fortify" = fs "800001ed" {
etc = fs "800001ed" null null;
} null;
bin = fs "800001ed" { sh = fs "80001ff" null null; } null;
dev = fs "800001ed" {
core = fs "80001ff" null null;
@ -53,7 +41,6 @@
} null;
etc = fs "800001ed" {
".clean" = fs "80001ff" null null;
".host" = fs "800001c0" null null;
".updated" = fs "80001ff" null null;
"NIXOS" = fs "80001ff" null null;
"X11" = fs "80001ff" null null;
@ -97,6 +84,7 @@
"pki" = fs "80001ff" null null;
"polkit-1" = fs "80001ff" null null;
"profile" = fs "80001ff" null null;
"profiles" = fs "80001ff" null null;
"protocols" = fs "80001ff" null null;
"resolv.conf" = fs "80001ff" null null;
"resolvconf.conf" = fs "80001ff" null null;
@ -213,7 +201,7 @@
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/etc" "/.fortify/etc" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000003,gid=1000003")
(ent "/" "/run/user/1000" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8192k,mode=700,uid=1000003,gid=1000003")
(ent "/tmp/fortify.1000/tmpdir/3" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")

View File

@ -6,25 +6,13 @@
{
name = "preset";
tty = false;
device = false;
mapRealUid = false;
want = {
env = [
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
"HOME=/var/lib/fortify/u0/a1"
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
"SHELL=/run/current-system/sw/bin/bash"
"TERM=linux"
"USER=u0_a1"
"WAYLAND_DISPLAY=wayland-0"
"XDG_RUNTIME_DIR=/run/user/65534"
"XDG_SESSION_CLASS=user"
"XDG_SESSION_TYPE=tty"
];
fs = fs "dead" {
".fortify" = fs "800001ed" { } null;
".fortify" = fs "800001ed" {
etc = fs "800001ed" null null;
} null;
bin = fs "800001ed" { sh = fs "80001ff" null null; } null;
dev = fs "800001ed" {
core = fs "80001ff" null null;
@ -53,7 +41,6 @@
} null;
etc = fs "800001ed" {
".clean" = fs "80001ff" null null;
".host" = fs "800001c0" null null;
".updated" = fs "80001ff" null null;
"NIXOS" = fs "80001ff" null null;
"X11" = fs "80001ff" null null;
@ -97,6 +84,7 @@
"pki" = fs "80001ff" null null;
"polkit-1" = fs "80001ff" null null;
"profile" = fs "80001ff" null null;
"profiles" = fs "80001ff" null null;
"protocols" = fs "80001ff" null null;
"resolv.conf" = fs "80001ff" null null;
"resolvconf.conf" = fs "80001ff" null null;
@ -213,7 +201,7 @@
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/etc" "/.fortify/etc" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000001,gid=1000001")
(ent "/" "/run/user/65534" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8192k,mode=700,uid=1000001,gid=1000001")
(ent "/tmp/fortify.1000/tmpdir/1" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")

View File

@ -6,25 +6,13 @@
{
name = "tty";
tty = true;
device = false;
mapRealUid = false;
want = {
env = [
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
"HOME=/var/lib/fortify/u0/a2"
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
"SHELL=/run/current-system/sw/bin/bash"
"TERM=linux"
"USER=u0_a2"
"WAYLAND_DISPLAY=wayland-0"
"XDG_RUNTIME_DIR=/run/user/65534"
"XDG_SESSION_CLASS=user"
"XDG_SESSION_TYPE=tty"
];
fs = fs "dead" {
".fortify" = fs "800001ed" { } null;
".fortify" = fs "800001ed" {
etc = fs "800001ed" null null;
} null;
bin = fs "800001ed" { sh = fs "80001ff" null null; } null;
dev = fs "800001ed" {
console = fs "4200190" null null;
@ -54,7 +42,6 @@
} null;
etc = fs "800001ed" {
".clean" = fs "80001ff" null null;
".host" = fs "800001c0" null null;
".updated" = fs "80001ff" null null;
"NIXOS" = fs "80001ff" null null;
"X11" = fs "80001ff" null null;
@ -98,6 +85,7 @@
"pki" = fs "80001ff" null null;
"polkit-1" = fs "80001ff" null null;
"profile" = fs "80001ff" null null;
"profiles" = fs "80001ff" null null;
"protocols" = fs "80001ff" null null;
"resolv.conf" = fs "80001ff" null null;
"resolvconf.conf" = fs "80001ff" null null;
@ -215,7 +203,7 @@
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/etc" "/.fortify/etc" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000002,gid=1000002")
(ent "/" "/run/user/65534" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8192k,mode=700,uid=1000002,gid=1000002")
(ent "/tmp/fortify.1000/tmpdir/2" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")

View File

@ -1,77 +0,0 @@
{
lib,
pkgs,
config,
...
}:
let
testProgram = pkgs.callPackage ./tool/package.nix { inherit (config.environment.fortify.package) version; };
testCases = import ./case lib testProgram;
in
{
users.users = {
alice = {
isNormalUser = true;
description = "Alice Foobar";
password = "foobar";
uid = 1000;
};
};
home-manager.users.alice.home.stateVersion = "24.11";
# Automatically login on tty1 as a normal user:
services.getty.autologinUser = "alice";
environment = {
systemPackages = with pkgs; [
# For checking seccomp outcome:
testProgram
];
variables = {
SWAYSOCK = "/tmp/sway-ipc.sock";
WLR_RENDERER = "pixman";
};
};
# Automatically configure and start Sway when logging in on tty1:
programs.bash.loginShellInit = ''
if [ "$(tty)" = "/dev/tty1" ]; then
set -e
mkdir -p ~/.config/sway
(sed s/Mod4/Mod1/ /etc/sway/config &&
echo 'output * bg ${pkgs.nixos-artwork.wallpapers.simple-light-gray.gnomeFilePath} fill' &&
echo 'output Virtual-1 res 1680x1050') > ~/.config/sway/config
sway --validate
systemd-cat --identifier=session sway && touch /tmp/sway-exit-ok
fi
'';
programs.sway.enable = true;
virtualisation.qemu.options = [
# Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch:
"-vga none -device virtio-gpu-pci"
# Increase performance:
"-smp 8"
];
environment.fortify = {
enable = true;
stateDir = "/var/lib/fortify";
users.alice = 0;
home-manager = _: _: { home.stateVersion = "23.05"; };
apps = with testCases; [
preset
tty
mapuid
device
];
};
}

View File

@ -1,39 +1,14 @@
{
lib,
nixosTest,
writeShellScript,
writeText,
callPackage,
self,
withRace ? false,
version,
}:
nixosTest {
name = "fortify-sandbox" + (if withRace then "-race" else "");
nodes.machine =
{ options, pkgs, ... }:
{
# Run with Go race detector:
environment.fortify = lib.mkIf withRace rec {
# race detector does not support static linking
package = (pkgs.callPackage ../../package.nix { }).overrideAttrs (previousAttrs: {
GOFLAGS = previousAttrs.GOFLAGS ++ [ "-race" ];
});
fsuPackage = options.environment.fortify.fsuPackage.default.override { fortify = package; };
};
imports = [
./configuration.nix
self.nixosModules.fortify
self.inputs.home-manager.nixosModules.home-manager
];
};
# adapted from nixos sway integration tests
# testScriptWithTypes:49: error: Cannot call function of unknown type
# (machine.succeed if succeed else machine.execute)(
# ^
# Found 1 error in 1 file (checked 1 source file)
skipTypeCheck = true;
testScript = builtins.readFile ./test.py;
}
name: want:
writeShellScript "fortify-${name}-check-sandbox-script" ''
set -e
${callPackage ./assert.nix { inherit version; }}/bin/test \
${writeText "fortify-${name}-want.json" (builtins.toJSON want)}
touch /tmp/sandbox-ok
''

View File

@ -1,119 +0,0 @@
package sandbox
import (
"bufio"
"fmt"
"io"
"os"
"strings"
"syscall"
"time"
"unsafe"
)
const (
NULL = 0
PTRACE_ATTACH = 16
PTRACE_DETACH = 17
PTRACE_SECCOMP_GET_FILTER = 0x420c
)
type ptraceError struct {
op string
errno syscall.Errno
}
func (p *ptraceError) Error() string { return fmt.Sprintf("%s: %v", p.op, p.errno) }
func (p *ptraceError) Unwrap() error {
if p.errno == 0 {
return nil
}
return p.errno
}
func ptrace(op uintptr, pid, addr int, data unsafe.Pointer) (r uintptr, errno syscall.Errno) {
r, _, errno = syscall.Syscall6(syscall.SYS_PTRACE, op, uintptr(pid), uintptr(addr), uintptr(data), NULL, NULL)
return
}
func ptraceAttach(pid int) error {
const (
statePrefix = "State:"
stateSuffix = "t (tracing stop)"
)
var r io.ReadSeekCloser
if f, err := os.Open(fmt.Sprintf("/proc/%d/status", pid)); err != nil {
return err
} else {
r = f
}
if _, errno := ptrace(PTRACE_ATTACH, pid, 0, nil); errno != 0 {
return &ptraceError{"PTRACE_ATTACH", errno}
}
// ugly! but there does not appear to be another way
for {
time.Sleep(10 * time.Millisecond)
if _, err := r.Seek(0, io.SeekStart); err != nil {
return err
}
s := bufio.NewScanner(r)
var found bool
for s.Scan() {
found = strings.HasPrefix(s.Text(), statePrefix)
if found {
break
}
}
if err := s.Err(); err != nil {
return err
}
if !found {
return syscall.EBADE
}
if strings.HasSuffix(s.Text(), stateSuffix) {
break
}
}
return nil
}
func ptraceDetach(pid int) error {
if _, errno := ptrace(PTRACE_DETACH, pid, 0, nil); errno != 0 {
return &ptraceError{"PTRACE_DETACH", errno}
}
return nil
}
type sockFilter struct { /* Filter block */
code uint16 /* Actual filter code */
jt uint8 /* Jump true */
jf uint8 /* Jump false */
k uint32 /* Generic multiuse field */
}
func getFilter[T comparable](pid, index int) ([]T, error) {
if s := unsafe.Sizeof(*new(T)); s != 8 {
panic(fmt.Sprintf("invalid filter block size %d", s))
}
var buf []T
if n, errno := ptrace(PTRACE_SECCOMP_GET_FILTER, pid, index, nil); errno != 0 {
return nil, &ptraceError{"PTRACE_SECCOMP_GET_FILTER", errno}
} else {
buf = make([]T, n)
}
if _, errno := ptrace(PTRACE_SECCOMP_GET_FILTER, pid, index, unsafe.Pointer(&buf[0])); errno != 0 {
return nil, &ptraceError{"PTRACE_SECCOMP_GET_FILTER", errno}
}
return buf, nil
}

View File

@ -10,7 +10,9 @@ import (
*/
import "C"
func trySyscalls() error {
const NULL = 0
func TrySyscalls() error {
testCases := []struct {
name string
errno syscall.Errno

View File

@ -1,72 +0,0 @@
import json
import shlex
q = shlex.quote
def swaymsg(command: str = "", succeed=True, type="command"):
assert command != "" or type != "command", "Must specify command or type"
shell = q(f"swaymsg -t {q(type)} -- {q(command)}")
with machine.nested(
f"sending swaymsg {shell!r}" + " (allowed to fail)" * (not succeed)
):
ret = (machine.succeed if succeed else machine.execute)(
f"su - alice -c {shell}"
)
# execute also returns a status code, but disregard.
if not succeed:
_, ret = ret
if not succeed and not ret:
return None
parsed = json.loads(ret)
return parsed
start_all()
machine.wait_for_unit("multi-user.target")
# To check fortify's version:
print(machine.succeed("sudo -u alice -i fortify version"))
# Wait for Sway to complete startup:
machine.wait_for_file("/run/user/1000/wayland-1")
machine.wait_for_file("/tmp/sway-ipc.sock")
# Check seccomp outcome:
swaymsg("exec fortify run cat")
pid = int(machine.wait_until_succeeds("pgrep -U 1000000 -x cat", timeout=5))
print(machine.succeed(f"fortify-test filter {pid} c698b081ff957afe17a6d94374537d37f2a63f6f9dd75da7546542407a9e32476ebda3312ba7785d7f618542bcfaf27ca27dcc2dddba852069d28bcfe8cad39a &>/dev/stdout", timeout=5))
machine.succeed(f"kill -TERM {pid}")
# Verify capabilities/securebits in user namespace:
print(machine.succeed("sudo -u alice -i fortify run capsh --print"))
print(machine.succeed("sudo -u alice -i fortify run capsh --has-no-new-privs"))
print(machine.fail("sudo -u alice -i fortify run capsh --has-a=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i fortify run capsh --has-b=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i fortify run capsh --has-i=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i fortify run capsh --has-p=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i fortify run umount -R /dev"))
# Check sandbox outcome:
check_offset = 0
def check_sandbox(name):
global check_offset
check_offset += 1
swaymsg(f"exec script /dev/null -E always -qec check-sandbox-{name}")
machine.wait_for_file(f"/tmp/fortify.1000/tmpdir/{check_offset}/sandbox-ok", timeout=15)
check_sandbox("preset")
check_sandbox("tty")
check_sandbox("mapuid")
check_sandbox("device")
# Exit Sway and verify process exit status 0:
swaymsg("exit", succeed=False)
machine.wait_for_file("/tmp/sway-exit-ok")
# Print fortify runDir contents:
print(machine.succeed("find /run/user/1000/fortify"))

View File

@ -1,39 +0,0 @@
package main
import (
"log"
"os"
"strconv"
"strings"
"git.gensokyo.uk/security/fortify/test/sandbox"
)
func main() {
log.SetFlags(0)
log.SetPrefix("test: ")
if len(os.Args) < 2 {
log.Fatal("invalid argument")
}
switch os.Args[1] {
case "filter":
if len(os.Args) != 4 {
log.Fatal("invalid argument")
}
if pid, err := strconv.Atoi(strings.TrimSpace(os.Args[2])); err != nil {
log.Fatalf("%s", err)
} else if pid < 1 {
log.Fatalf("%d out of range", pid)
} else {
sandbox.MustCheckFilter(pid, os.Args[3])
return
}
default:
(&sandbox.T{FS: os.DirFS("/")}).MustCheckFile(os.Args[1], "/tmp/sandbox-ok")
return
}
}

View File

@ -1,30 +0,0 @@
{
lib,
buildGoModule,
pkg-config,
util-linux,
version,
}:
buildGoModule rec {
pname = "check-sandbox";
inherit version;
src = builtins.path {
name = "${pname}-src";
path = lib.cleanSource ../.;
filter = path: type: (type == "directory") || (type == "regular" && lib.hasSuffix ".go" path);
};
vendorHash = null;
buildInputs = [ util-linux ];
nativeBuildInputs = [ pkg-config ];
preBuild = ''
go mod init git.gensokyo.uk/security/fortify/test/sandbox >& /dev/null
'';
postInstall = ''
mv $out/bin/tool $out/bin/fortify-test
'';
}

View File

@ -69,8 +69,8 @@ def check_state(name, enablements):
if len(config['args']) != 1 or config['args'][0] != command:
raise Exception(f"unexpected args {config['args']}")
if config['enablements'] != enablements:
raise Exception(f"unexpected enablements {instance['config']['enablements']}")
if config['confinement']['enablements'] != enablements:
raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
def fortify(command):
@ -99,15 +99,34 @@ print(denyOutputVerbose)
# Fail direct fsu call:
print(machine.fail("sudo -u alice -i fsu"))
# Verify capabilities/securebits in user namespace:
print(machine.succeed("sudo -u alice -i fortify run capsh --print"))
print(machine.succeed("sudo -u alice -i fortify run capsh --has-no-new-privs"))
print(machine.fail("sudo -u alice -i fortify run capsh --has-a=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i fortify run capsh --has-b=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i fortify run capsh --has-i=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i fortify run capsh --has-p=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i fortify run umount -R /dev"))
# Verify PrintBaseError behaviour:
if denyOutput != "fsu: uid 1001 is not in the fsurc file\n":
raise Exception(f"unexpected deny output:\n{denyOutput}")
if denyOutputVerbose != "fsu: uid 1001 is not in the fsurc file\nfortify: *cannot obtain uid from fsu: permission denied\n":
raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}")
# Check sandbox outcome:
check_offset = 0
def check_sandbox(name):
global check_offset
check_offset += 1
swaymsg(f"exec script /dev/null -E always -qec check-sandbox-{name}")
machine.wait_for_file(f"/tmp/fortify.1000/tmpdir/{check_offset}/sandbox-ok", timeout=15)
check_sandbox("preset")
check_sandbox("tty")
check_sandbox("mapuid")
def aid(offset):
return 1+check_offset+offset
@ -169,16 +188,6 @@ machine.send_chars("exit\n")
machine.wait_for_file("/tmp/p0-exit-ok", timeout=15)
machine.fail("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000000")
# Check interrupt shim behaviour:
swaymsg("exec sh -c 'ne-foot; echo -n $? > /tmp/monitor-exit-code'")
wait_for_window(f"u0_a{aid(0)}@machine")
machine.succeed("pkill -INT -f 'fortify -v app '")
machine.wait_until_fails("pgrep foot", timeout=5)
machine.wait_for_file("/tmp/monitor-exit-code")
interrupt_exit_code = int(machine.succeed("cat /tmp/monitor-exit-code"))
if interrupt_exit_code != 254:
raise Exception(f"unexpected exit code {interrupt_exit_code}")
# Start app (foot) with Wayland enablement:
swaymsg("exec ne-foot")
wait_for_window(f"u0_a{aid(0)}@machine")
@ -186,11 +195,25 @@ machine.send_chars("clear; wayland-info && touch /tmp/client-ok\n")
machine.wait_for_file(tmpdir_path(0, "client-ok"), timeout=15)
collect_state_ui("foot_wayland")
check_state("ne-foot", 1)
# Verify lack of acl on XDG_RUNTIME_DIR:
machine.fail(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {aid(0) + 1000000}")
# Verify acl on XDG_RUNTIME_DIR:
print(machine.succeed(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {aid(0) + 1000000}"))
machine.send_chars("exit\n")
machine.wait_until_fails("pgrep foot", timeout=5)
machine.fail(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {aid(0) + 1000000}", timeout=5)
# Verify acl cleanup on XDG_RUNTIME_DIR:
machine.wait_until_fails(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {aid(0) + 1000000}", timeout=5)
# Start app (foot) with Wayland enablement from a terminal:
swaymsg("exec foot $SHELL -c '(ne-foot) & sleep 1 && fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && cat'")
wait_for_window(f"u0_a{aid(0)}@machine")
machine.send_chars("clear; wayland-info && touch /tmp/term-ok\n")
machine.wait_for_file(tmpdir_path(0, "term-ok"), timeout=15)
machine.wait_for_file("/tmp/ps-show-ok", timeout=5)
collect_state_ui("foot_wayland_term")
check_state("ne-foot", 1)
machine.send_chars("exit\n")
wait_for_window("foot")
machine.send_key("ctrl-c")
machine.wait_until_fails("pgrep foot", timeout=5)
# Test PulseAudio (fortify does not support PipeWire yet):
swaymsg("exec pa-foot")
@ -229,22 +252,6 @@ machine.wait_until_fails(f"getfacl --absolute-names --omit-header --numeric /run
# Test syscall filter:
print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure"))
# Start app (foot) with Wayland enablement from a terminal:
swaymsg("exec foot $SHELL -c '(ne-foot) & disown && exec $SHELL'")
wait_for_window(f"u0_a{aid(0)}@machine")
machine.send_chars("clear; wayland-info && touch /tmp/term-ok\n")
machine.wait_for_file(tmpdir_path(0, "term-ok"), timeout=15)
machine.send_key("alt-h")
machine.send_chars("clear; fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && exec cat\n")
machine.wait_for_file("/tmp/ps-show-ok", timeout=5)
collect_state_ui("foot_wayland_term")
check_state("ne-foot", 1)
machine.send_key("alt-l")
machine.send_chars("exit\n")
wait_for_window("alice@machine")
machine.send_key("ctrl-c")
machine.wait_until_fails("pgrep foot", timeout=5)
# Exit Sway and verify process exit status 0:
swaymsg("exit", succeed=False)
machine.wait_for_file("/tmp/sway-exit-ok")