Compare commits

..

No commits in common. "5c4058d5ac7e7944973ca8216c258fc50c194e22" and "24618ab9a1524e8b8986a9bf67667288e642fcf1" have entirely different histories.

69 changed files with 1924 additions and 3437 deletions

View File

@ -4,15 +4,12 @@ import (
"encoding/json" "encoding/json"
"log" "log"
"os" "os"
"path"
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
"git.gensokyo.uk/security/fortify/system" "git.gensokyo.uk/security/fortify/system"
) )
type appInfo struct { type bundleInfo struct {
Name string `json:"name"` Name string `json:"name"`
Version string `json:"version"` Version string `json:"version"`
@ -23,15 +20,13 @@ type appInfo struct {
// passed through to [fst.Config] // passed through to [fst.Config]
Groups []string `json:"groups,omitempty"` Groups []string `json:"groups,omitempty"`
// passed through to [fst.Config] // passed through to [fst.Config]
Devel bool `json:"devel,omitempty"` UserNS bool `json:"userns,omitempty"`
// passed through to [fst.Config]
Userns bool `json:"userns,omitempty"`
// passed through to [fst.Config] // passed through to [fst.Config]
Net bool `json:"net,omitempty"` Net bool `json:"net,omitempty"`
// passed through to [fst.Config] // passed through to [fst.Config]
Dev bool `json:"dev,omitempty"` Dev bool `json:"dev,omitempty"`
// passed through to [fst.Config] // passed through to [fst.Config]
Tty bool `json:"tty,omitempty"` NoNewSession bool `json:"no_new_session,omitempty"`
// passed through to [fst.Config] // passed through to [fst.Config]
MapRealUID bool `json:"map_real_uid,omitempty"` MapRealUID bool `json:"map_real_uid,omitempty"`
// passed through to [fst.Config] // passed through to [fst.Config]
@ -43,9 +38,11 @@ type appInfo struct {
// passed through to [fst.Config] // passed through to [fst.Config]
Enablements system.Enablements `json:"enablements"` Enablements system.Enablements `json:"enablements"`
// passed through to [fst.Config] // passed through inverted to [bwrap.SyscallPolicy]
Devel bool `json:"devel,omitempty"`
// passed through to [bwrap.SyscallPolicy]
Multiarch bool `json:"multiarch,omitempty"` Multiarch bool `json:"multiarch,omitempty"`
// passed through to [fst.Config] // passed through to [bwrap.SyscallPolicy]
Bluetooth bool `json:"bluetooth,omitempty"` Bluetooth bool `json:"bluetooth,omitempty"`
// allow gpu access within sandbox // allow gpu access within sandbox
@ -62,64 +59,8 @@ type appInfo struct {
ActivationPackage string `json:"activation_package"` ActivationPackage string `json:"activation_package"`
} }
func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool) *fst.Config { func loadBundleInfo(name string, beforeFail func()) *bundleInfo {
config := &fst.Config{ bundle := new(bundleInfo)
ID: app.ID,
Path: argv[0],
Args: argv,
Confinement: fst.ConfinementConfig{
AppID: app.AppID,
Groups: app.Groups,
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,
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},
{Src: "/etc/resolv.conf"},
{Src: "/sys/block"},
{Src: "/sys/bus"},
{Src: "/sys/class"},
{Src: "/sys/dev"},
{Src: "/sys/devices"},
},
Link: [][2]string{
{app.CurrentSystem, "/run/current-system"},
{"/run/current-system/sw/bin", "/bin"},
{"/run/current-system/sw/bin", "/usr/bin"},
},
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},
},
SystemBus: app.SystemBus,
SessionBus: app.SessionBus,
Enablements: app.Enablements,
},
}
if app.Multiarch {
config.Confinement.Sandbox.Seccomp |= seccomp.FlagMultiarch
}
if app.Bluetooth {
config.Confinement.Sandbox.Seccomp |= seccomp.FlagBluetooth
}
return config
}
func loadAppInfo(name string, beforeFail func()) *appInfo {
bundle := new(appInfo)
if f, err := os.Open(name); err != nil { if f, err := os.Open(name); err != nil {
beforeFail() beforeFail()
log.Fatalf("cannot open bundle: %v", err) log.Fatalf("cannot open bundle: %v", err)

View File

@ -12,7 +12,9 @@ import (
"git.gensokyo.uk/security/fortify/command" "git.gensokyo.uk/security/fortify/command"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/app/init0"
"git.gensokyo.uk/security/fortify/internal/app/shim" "git.gensokyo.uk/security/fortify/internal/app/shim"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/sys" "git.gensokyo.uk/security/fortify/internal/sys"
@ -37,6 +39,7 @@ func init() {
func main() { func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE // early init path, skips root check and duplicate PR_SET_DUMPABLE
sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg) sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
init0.TryArgv0()
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil { if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err) log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
@ -62,7 +65,9 @@ func main() {
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console"). 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") Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next fortify action")
// internal commands
c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess }) c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess })
c.Command("init", command.UsageInternal, func([]string) error { init0.Main(); return errSuccess })
{ {
var ( var (
@ -119,7 +124,7 @@ func main() {
Parse bundle and app metadata, do pre-install checks. Parse bundle and app metadata, do pre-install checks.
*/ */
bundle := loadAppInfo(path.Join(workDir, "bundle.json"), cleanup) bundle := loadBundleInfo(path.Join(workDir, "bundle.json"), cleanup)
pathSet := pathSetByApp(bundle.ID) pathSet := pathSetByApp(bundle.ID)
app := bundle app := bundle
@ -135,7 +140,7 @@ func main() {
log.Printf("metadata path %q is not a file", pathSet.metaPath) log.Printf("metadata path %q is not a file", pathSet.metaPath)
return syscall.EBADMSG return syscall.EBADMSG
} else { } else {
app = loadAppInfo(pathSet.metaPath, cleanup) app = loadBundleInfo(pathSet.metaPath, cleanup)
if app.ID != bundle.ID { if app.ID != bundle.ID {
cleanup() cleanup()
log.Printf("app %q claims to have identifier %q", log.Printf("app %q claims to have identifier %q",
@ -268,7 +273,7 @@ func main() {
id := args[0] id := args[0]
pathSet := pathSetByApp(id) pathSet := pathSetByApp(id)
app := loadAppInfo(pathSet.metaPath, func() {}) app := loadBundleInfo(pathSet.metaPath, func() {})
if app.ID != id { if app.ID != id {
log.Printf("app %q claims to have identifier %q", id, app.ID) log.Printf("app %q claims to have identifier %q", id, app.ID)
return syscall.EBADE return syscall.EBADE
@ -317,7 +322,51 @@ func main() {
} }
argv = append(argv, args[1:]...) argv = append(argv, args[1:]...)
config := app.toFst(pathSet, argv, flagDropShell) config := &fst.Config{
ID: app.ID,
Command: argv,
Confinement: fst.ConfinementConfig{
AppID: app.AppID,
Groups: app.Groups,
Username: "fortify",
Inner: path.Join("/data/data", app.ID),
Outer: pathSet.homeDir,
Sandbox: &fst.SandboxConfig{
Hostname: formatHostname(app.Name),
UserNS: app.UserNS,
Net: app.Net,
Dev: app.Dev,
Syscall: &bwrap.SyscallPolicy{DenyDevel: !app.Devel, Multiarch: app.Multiarch, Bluetooth: app.Bluetooth},
NoNewSession: app.NoNewSession || 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},
{Src: "/etc/resolv.conf"},
{Src: "/sys/block"},
{Src: "/sys/bus"},
{Src: "/sys/class"},
{Src: "/sys/dev"},
{Src: "/sys/devices"},
},
Link: [][2]string{
{app.CurrentSystem, "/run/current-system"},
{"/run/current-system/sw/bin", "/bin"},
{"/run/current-system/sw/bin", "/usr/bin"},
},
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},
},
SystemBus: app.SystemBus,
SessionBus: app.SessionBus,
Enablements: app.Enablements,
},
}
/* /*
Expose GPU devices. Expose GPU devices.

View File

@ -11,14 +11,14 @@ import (
func mustRunApp(ctx context.Context, config *fst.Config, beforeFail func()) { func mustRunApp(ctx context.Context, config *fst.Config, beforeFail func()) {
rs := new(fst.RunState) rs := new(fst.RunState)
a := app.MustNew(ctx, std) a := app.MustNew(std)
if sa, err := a.Seal(config); err != nil { if sa, err := a.Seal(config); err != nil {
fmsg.PrintBaseError(err, "cannot seal app:") fmsg.PrintBaseError(err, "cannot seal app:")
rs.ExitCode = 1 rs.ExitCode = 1
} else { } else {
// this updates ExitCode // this updates ExitCode
app.PrintRunStateErr(rs, sa.Run(rs)) app.PrintRunStateErr(rs, sa.Run(ctx, rs))
} }
if rs.ExitCode != 0 { if rs.ExitCode != 0 {

View File

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

View File

@ -6,19 +6,18 @@ import (
"strings" "strings"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
) )
func withNixDaemon( func withNixDaemon(
ctx context.Context, ctx context.Context,
action string, command []string, net bool, updateConfig func(config *fst.Config) *fst.Config, action string, command []string, net bool, updateConfig func(config *fst.Config) *fst.Config,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(), app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
) { ) {
mustRunAppDropShell(ctx, updateConfig(&fst.Config{ mustRunAppDropShell(ctx, updateConfig(&fst.Config{
ID: app.ID, ID: app.ID,
Path: shellPath, Command: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
Args: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
// start nix-daemon // start nix-daemon
"nix-daemon --store / & " + "nix-daemon --store / & " +
// wait for socket to appear // wait for socket to appear
@ -36,10 +35,10 @@ func withNixDaemon(
Outer: pathSet.homeDir, Outer: pathSet.homeDir,
Sandbox: &fst.SandboxConfig{ Sandbox: &fst.SandboxConfig{
Hostname: formatHostname(app.Name) + "-" + action, Hostname: formatHostname(app.Name) + "-" + action,
Userns: true, // nix sandbox requires userns UserNS: true, // nix sandbox requires userns
Net: net, Net: net,
Seccomp: seccomp.FlagMultiarch, Syscall: &bwrap.SyscallPolicy{Multiarch: true},
Tty: dropShell, NoNewSession: dropShell,
Filesystem: []*fst.FilesystemConfig{ Filesystem: []*fst.FilesystemConfig{
{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true}, {Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
}, },
@ -62,11 +61,10 @@ func withNixDaemon(
func withCacheDir( func withCacheDir(
ctx context.Context, ctx context.Context,
action string, command []string, workDir string, action string, command []string, workDir string,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) { app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
mustRunAppDropShell(ctx, &fst.Config{ mustRunAppDropShell(ctx, &fst.Config{
ID: app.ID, ID: app.ID,
Path: shellPath, Command: []string{shellPath, "-lc", strings.Join(command, " && ")},
Args: []string{shellPath, "-lc", strings.Join(command, " && ")},
Confinement: fst.ConfinementConfig{ Confinement: fst.ConfinementConfig{
AppID: app.AppID, AppID: app.AppID,
Username: "nixos", Username: "nixos",
@ -74,8 +72,8 @@ func withCacheDir(
Outer: pathSet.cacheDir, // this also ensures cacheDir via shim Outer: pathSet.cacheDir, // this also ensures cacheDir via shim
Sandbox: &fst.SandboxConfig{ Sandbox: &fst.SandboxConfig{
Hostname: formatHostname(app.Name) + "-" + action, Hostname: formatHostname(app.Name) + "-" + action,
Seccomp: seccomp.FlagMultiarch, Syscall: &bwrap.SyscallPolicy{Multiarch: true},
Tty: dropShell, NoNewSession: dropShell,
Filesystem: []*fst.FilesystemConfig{ Filesystem: []*fst.FilesystemConfig{
{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true}, {Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
{Src: workDir, Dst: path.Join(fst.Tmp, "bundle"), Must: true}, {Src: workDir, Dst: path.Join(fst.Tmp, "bundle"), Must: true},
@ -99,7 +97,7 @@ func withCacheDir(
func mustRunAppDropShell(ctx context.Context, config *fst.Config, dropShell bool, beforeFail func()) { func mustRunAppDropShell(ctx context.Context, config *fst.Config, dropShell bool, beforeFail func()) {
if dropShell { if dropShell {
config.Args = []string{shellPath, "-l"} config.Command = []string{shellPath, "-l"}
mustRunApp(ctx, config, beforeFail) mustRunApp(ctx, config, beforeFail)
beforeFail() beforeFail()
internal.Exit(0) internal.Exit(0)

12
flake.lock generated
View File

@ -7,11 +7,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1742234739, "lastModified": 1739757849,
"narHash": "sha256-zFL6zsf/5OztR1NSNQF33dvS1fL/BzVUjabZq4qrtY4=", "narHash": "sha256-Gs076ot1YuAAsYVcyidLKUMIc4ooOaRGO0PqTY7sBzA=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "f6af7280a3390e65c2ad8fd059cdc303426cbd59", "rev": "9d3d080aec2a35e05a15cedd281c2384767c2cfe",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -23,11 +23,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1742512142, "lastModified": 1741445498,
"narHash": "sha256-8XfURTDxOm6+33swQJu/hx6xw1Tznl8vJJN5HwVqckg=", "narHash": "sha256-F5Em0iv/CxkN5mZ9hRn3vPknpoWdcdCyR0e4WklHwiE=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "7105ae3957700a9646cc4b766f5815b23ed0c682", "rev": "52e3095f6d812b91b22fb7ad0bfc1ab416453634",
"type": "github" "type": "github"
}, },
"original": { "original": {

100
flake.nix
View File

@ -27,7 +27,7 @@
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; }); nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
in in
{ {
nixosModules.fortify = import ./nixos.nix self.packages; nixosModules.fortify = import ./nixos.nix;
buildPackage = forAllSystems ( buildPackage = forAllSystems (
system: system:
@ -105,21 +105,9 @@
default = fortify; default = fortify;
fortify = pkgs.pkgsStatic.callPackage ./package.nix { fortify = pkgs.pkgsStatic.callPackage ./package.nix {
inherit (pkgs) inherit (pkgs)
# passthru.buildInputs
go
gcc
# nativeBuildInputs
pkg-config
wayland-scanner
makeBinaryWrapper
# appPackages
glibc
bubblewrap bubblewrap
xdg-dbus-proxy xdg-dbus-proxy
glibc
# fpkg
zstd zstd
gnutar gnutar
coreutils coreutils
@ -127,7 +115,7 @@
}; };
fsu = pkgs.callPackage ./cmd/fsu/package.nix { inherit (self.packages.${system}) fortify; }; fsu = pkgs.callPackage ./cmd/fsu/package.nix { inherit (self.packages.${system}) fortify; };
dist = pkgs.runCommand "${fortify.name}-dist" { buildInputs = fortify.targetPkgs ++ [ pkgs.pkgsStatic.musl ]; } '' dist = pkgs.runCommand "${fortify.name}-dist" { inherit (self.devShells.${system}.default) buildInputs; } ''
# go requires XDG_CACHE_HOME for the build cache # go requires XDG_CACHE_HOME for the build cache
export XDG_CACHE_HOME="$(mktemp -d)" export XDG_CACHE_HOME="$(mktemp -d)"
@ -140,21 +128,93 @@
export FORTIFY_VERSION="v${fortify.version}" export FORTIFY_VERSION="v${fortify.version}"
./dist/release.sh && mkdir $out && cp -v "dist/fortify-$FORTIFY_VERSION.tar.gz"* $out ./dist/release.sh && mkdir $out && cp -v "dist/fortify-$FORTIFY_VERSION.tar.gz"* $out
''; '';
fhs = pkgs.buildFHSEnv {
pname = "fortify-fhs";
inherit (fortify) version;
targetPkgs =
pkgs:
with pkgs;
[
go
gcc
pkg-config
wayland-scanner
]
++ (
with pkgs.pkgsStatic;
[
musl
libffi
libseccomp
acl
wayland
wayland-protocols
]
++ (with xorg; [
libxcb
libXau
libXdmcp
xorgproto
])
);
extraOutputsToInstall = [ "dev" ];
profile = ''
export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
'';
};
} }
); );
devShells = forAllSystems ( devShells = forAllSystems (
system: system:
let let
inherit (self.packages.${system}) fortify; inherit (self.packages.${system}) fortify fhs;
pkgs = nixpkgsFor.${system}; pkgs = nixpkgsFor.${system};
in in
{ {
default = pkgs.mkShell { buildInputs = fortify.targetPkgs; }; default = pkgs.mkShell {
withPackage = pkgs.mkShell { buildInputs = [ fortify ] ++ fortify.targetPkgs; }; buildInputs =
with pkgs;
[
go
gcc
]
# buildInputs
++ (
with pkgsStatic;
[
musl
libffi
libseccomp
acl
wayland
wayland-protocols
]
++ (with xorg; [
libxcb
libXau
libXdmcp
])
)
# nativeBuildInputs
++ [
pkg-config
wayland-scanner
makeBinaryWrapper
];
};
fhs = fhs.env;
withPackage = nixpkgsFor.${system}.mkShell {
buildInputs = [ self.packages.${system}.fortify ] ++ self.devShells.${system}.default.buildInputs;
};
generateDoc = generateDoc =
let let
pkgs = nixpkgsFor.${system};
inherit (pkgs) lib; inherit (pkgs) lib;
doc = doc =
@ -163,7 +223,7 @@
specialArgs = { specialArgs = {
inherit pkgs; inherit pkgs;
}; };
modules = [ (import ./options.nix self.packages) ]; modules = [ ./options.nix ];
}; };
cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval; cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval;
in in
@ -173,7 +233,7 @@
sed -i '/*Declared by:*/,+1 d' $out sed -i '/*Declared by:*/,+1 d' $out
''; '';
in in
pkgs.mkShell { nixpkgsFor.${system}.mkShell {
shellHook = '' shellHook = ''
exec cat ${docText} > options.md exec cat ${docText} > options.md
''; '';

View File

@ -2,6 +2,7 @@
package fst package fst
import ( import (
"context"
"time" "time"
) )
@ -18,7 +19,7 @@ type App interface {
type SealedApp interface { type SealedApp interface {
// Run commits sealed system setup and starts the app process. // Run commits sealed system setup and starts the app process.
Run(rs *RunState) error Run(ctx context.Context, rs *RunState) error
} }
// RunState stores the outcome of a call to [SealedApp.Run]. // RunState stores the outcome of a call to [SealedApp.Run].

View File

@ -2,7 +2,7 @@ package fst
import ( import (
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/sandbox/seccomp" "git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/system" "git.gensokyo.uk/security/fortify/system"
) )
@ -14,11 +14,8 @@ type Config struct {
// passed to wayland security-context-v1 as application ID // passed to wayland security-context-v1 as application ID
// and used as part of defaults in dbus session proxy // and used as part of defaults in dbus session proxy
ID string `json:"id"` ID string `json:"id"`
// final argv, passed to init
// absolute path to executable file Command []string `json:"command"`
Path string `json:"path,omitempty"`
// final args passed to container init
Args []string `json:"args"`
Confinement ConfinementConfig `json:"confinement"` Confinement ConfinementConfig `json:"confinement"`
} }
@ -29,13 +26,13 @@ type ConfinementConfig struct {
AppID int `json:"app_id"` AppID int `json:"app_id"`
// list of supplementary groups to inherit // list of supplementary groups to inherit
Groups []string `json:"groups"` Groups []string `json:"groups"`
// passwd username in container, defaults to passwd name of target uid or chronos // passwd username in the sandbox, defaults to passwd name of target uid or chronos
Username string `json:"username,omitempty"` Username string `json:"username,omitempty"`
// home directory in container, empty for outer // home directory in sandbox, empty for outer
Inner string `json:"home_inner"` Inner string `json:"home_inner"`
// home directory in init namespace // home directory in init namespace
Outer string `json:"home"` Outer string `json:"home"`
// abstract sandbox configuration // bwrap sandbox confinement configuration
Sandbox *SandboxConfig `json:"sandbox"` Sandbox *SandboxConfig `json:"sandbox"`
// extra acl ops, runs after everything else // extra acl ops, runs after everything else
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"` ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
@ -47,7 +44,7 @@ type ConfinementConfig struct {
// nil value makes session bus proxy assume built-in defaults // nil value makes session bus proxy assume built-in defaults
SessionBus *dbus.Config `json:"session_bus,omitempty"` SessionBus *dbus.Config `json:"session_bus,omitempty"`
// system resources to expose to the container // system resources to expose to the sandbox
Enablements system.Enablements `json:"enablements"` Enablements system.Enablements `json:"enablements"`
} }
@ -79,12 +76,24 @@ func (e *ExtraPermConfig) String() string {
return string(buf) return string(buf)
} }
type FilesystemConfig struct {
// mount point in sandbox, same as src if empty
Dst string `json:"dst,omitempty"`
// host filesystem path to make available to sandbox
Src string `json:"src"`
// write access
Write bool `json:"write,omitempty"`
// device access
Device bool `json:"dev,omitempty"`
// fail if mount fails
Must bool `json:"require,omitempty"`
}
// Template returns a fully populated instance of Config. // Template returns a fully populated instance of Config.
func Template() *Config { func Template() *Config {
return &Config{ return &Config{
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Path: "/run/current-system/sw/bin/chromium", Command: []string{
Args: []string{
"chromium", "chromium",
"--ignore-gpu-blocklist", "--ignore-gpu-blocklist",
"--disable-smooth-scrolling", "--disable-smooth-scrolling",
@ -99,13 +108,11 @@ func Template() *Config {
Inner: "/var/lib/fortify", Inner: "/var/lib/fortify",
Sandbox: &SandboxConfig{ Sandbox: &SandboxConfig{
Hostname: "localhost", Hostname: "localhost",
Devel: true, UserNS: true,
Userns: true,
Net: true, Net: true,
Dev: true, Dev: true,
Seccomp: seccomp.FlagMultiarch, Syscall: &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
Tty: true, NoNewSession: true,
Multiarch: true,
MapRealUID: true, MapRealUID: true,
DirectWayland: false, DirectWayland: false,
// example API credentials pulled from Google Chrome // example API credentials pulled from Google Chrome
@ -127,7 +134,7 @@ func Template() *Config {
Link: [][2]string{{"/run/user/65534", "/run/user/150"}}, Link: [][2]string{{"/run/user/65534", "/run/user/150"}},
Etc: "/etc", Etc: "/etc",
AutoEtc: true, AutoEtc: true,
Cover: []string{"/var/run/nscd"}, Override: []string{"/var/run/nscd"},
}, },
ExtraPerms: []*ExtraPermConfig{ ExtraPerms: []*ExtraPermConfig{
{Path: "/var/lib/fortify/u0", Ensure: true, Execute: true}, {Path: "/var/lib/fortify/u0", Ensure: true, Execute: true},

View File

@ -4,63 +4,50 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"maps"
"path" "path"
"slices"
"syscall"
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/sandbox" "git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
) )
// SandboxConfig describes resources made available to the sandbox. // SandboxConfig describes resources made available to the sandbox.
type ( type SandboxConfig struct {
SandboxConfig struct { // unix hostname within sandbox
// container hostname
Hostname string `json:"hostname,omitempty"` Hostname string `json:"hostname,omitempty"`
// allow userns within sandbox
// extra seccomp flags UserNS bool `json:"userns,omitempty"`
Seccomp seccomp.SyscallOpts `json:"seccomp"` // share net namespace
// 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"` Net bool `json:"net,omitempty"`
// expose main process tty // share all devices
Tty bool `json:"tty,omitempty"` Dev bool `json:"dev,omitempty"`
// allow multiarch // seccomp syscall filter policy
Multiarch bool `json:"multiarch,omitempty"` Syscall *bwrap.SyscallPolicy `json:"syscall"`
// do not run in new session
// initial process environment variables NoNewSession bool `json:"no_new_session,omitempty"`
Env map[string]string `json:"env"`
// map target user uid to privileged user uid in the user namespace // map target user uid to privileged user uid in the user namespace
MapRealUID bool `json:"map_real_uid"` 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 // 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 // and the bare socket is mounted to the sandbox
DirectWayland bool `json:"direct_wayland,omitempty"` DirectWayland bool `json:"direct_wayland,omitempty"`
// final environment variables
Env map[string]string `json:"env"`
// sandbox host filesystem access
Filesystem []*FilesystemConfig `json:"filesystem"`
// symlinks created inside the sandbox
Link [][2]string `json:"symlink"`
// read-only /etc directory // read-only /etc directory
Etc string `json:"etc,omitempty"` Etc string `json:"etc,omitempty"`
// automatically set up /etc symlinks // automatically set up /etc symlinks
AutoEtc bool `json:"auto_etc"` AutoEtc bool `json:"auto_etc"`
// cover these paths or create them if they do not already exist // mount tmpfs over these paths,
Cover []string `json:"cover"` // runs right before [ConfinementConfig.ExtraPerms]
Override []string `json:"override"`
} }
// SandboxSys encapsulates system functions used during [sandbox.Container] initialisation. // SandboxSys encapsulates system functions used during the creation of [bwrap.Config].
SandboxSys interface { type SandboxSys interface {
Getuid() int Geteuid() int
Getgid() int
Paths() Paths Paths() Paths
ReadDir(name string) ([]fs.DirEntry, error) ReadDir(name string) ([]fs.DirEntry, error)
EvalSymlinks(path string) (string, error) EvalSymlinks(path string) (string, error)
@ -69,84 +56,73 @@ type (
Printf(format string, v ...any) Printf(format string, v ...any)
} }
// FilesystemConfig is a representation of [sandbox.BindMount]. // Bwrap returns the address of the corresponding bwrap.Config to s.
FilesystemConfig struct { // Note that remaining tmpfs entries must be queued by the caller prior to launch.
// mount point in container, same as src if empty func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
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 { if s == nil {
return nil, nil, syscall.EBADE return nil, errors.New("nil sandbox config")
} }
container := &sandbox.Params{ if s.Syscall == nil {
Hostname: s.Hostname, sys.Println("syscall filter not configured, PROCEED WITH CAUTION")
Ops: new(sandbox.Ops),
Seccomp: s.Seccomp,
} }
if !s.MapRealUID {
// mapped uid defaults to 65534 to work around file ownership checks due to a bwrap limitation
*uid = 65534
} else {
// some programs fail to connect to dbus session running as a different uid, so a separate workaround
// is introduced to map priv-side caller uid in namespace
*uid = sys.Geteuid()
}
conf := (&bwrap.Config{
Net: s.Net,
UserNS: s.UserNS,
UID: uid,
GID: uid,
Hostname: s.Hostname,
Clearenv: true,
SetEnv: s.Env,
/* this is only 4 KiB of memory on a 64-bit system, /* this is only 4 KiB of memory on a 64-bit system,
permissive defaults on NixOS results in around 100 entries permissive defaults on NixOS results in around 100 entries
so this capacity should eliminate copies for most setups */ so this capacity should eliminate copies for most setups */
*container.Ops = slices.Grow(*container.Ops, 1<<8) Filesystem: make([]bwrap.FSBuilder, 0, 256),
if s.Devel { Syscall: s.Syscall,
container.Flags |= sandbox.FAllowDevel NewSession: !s.NoNewSession,
} DieWithParent: true,
if s.Userns { AsInit: true,
container.Flags |= sandbox.FAllowUserns
}
if s.Net {
container.Flags |= sandbox.FAllowNet
}
if s.Tty {
container.Flags |= sandbox.FAllowTTY
}
if s.MapRealUID { // initialise unconditionally as Once cannot be justified
/* some programs fail to connect to dbus session running as a different uid // for saving such a miniscule amount of memory
so this workaround is introduced to map priv-side caller uid in container */ Chmod: make(bwrap.ChmodConfig),
container.Uid = sys.Getuid() }).
*uid = container.Uid Procfs("/proc").
container.Gid = sys.Getgid() Tmpfs(Tmp, 4*1024)
*gid = container.Gid
} else {
*uid = sandbox.OverflowUid()
*gid = sandbox.OverflowGid()
}
container.
Proc("/proc").
Tmpfs(Tmp, 1<<12, 0755)
if !s.Dev { if !s.Dev {
container.Dev("/dev").Mqueue("/dev/mqueue") conf.DevTmpfs("/dev").Mqueue("/dev/mqueue")
} else { } else {
container.Bind("/dev", "/dev", sandbox.BindDevice) conf.Bind("/dev", "/dev", false, true, true)
} }
/* retrieve paths and hide them if they're made available in the sandbox; if !s.AutoEtc {
this feature tries to improve user experience of permissive defaults, and if s.Etc == "" {
to warn about issues in custom configuration; it is NOT a security feature conf.Dir("/etc")
and should not be treated as such, ALWAYS be careful with what you bind */ } else {
conf.Bind(s.Etc, "/etc")
}
}
// retrieve paths and hide them if they're made available in the sandbox
var hidePaths []string var hidePaths []string
sc := sys.Paths() sc := sys.Paths()
hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath) hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath)
_, systemBusAddr := dbus.Address() _, systemBusAddr := dbus.Address()
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil { if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
return nil, nil, err return nil, err
} else { } else {
// there is usually only one, do not preallocate // there is usually only one, do not preallocate
for _, entry := range entries { for _, entry := range entries {
@ -172,7 +148,7 @@ func (s *SandboxConfig) ToContainer(sys SandboxSys, uid, gid *int) (*sandbox.Par
hidePathMatch := make([]bool, len(hidePaths)) hidePathMatch := make([]bool, len(hidePaths))
for i := range hidePaths { for i := range hidePaths {
if err := evalSymlinks(sys, &hidePaths[i]); err != nil { if err := evalSymlinks(sys, &hidePaths[i]); err != nil {
return nil, nil, err return nil, err
} }
} }
@ -182,19 +158,19 @@ func (s *SandboxConfig) ToContainer(sys SandboxSys, uid, gid *int) (*sandbox.Par
} }
if !path.IsAbs(c.Src) { if !path.IsAbs(c.Src) {
return nil, nil, fmt.Errorf("src path %q is not absolute", c.Src) return nil, fmt.Errorf("src path %q is not absolute", c.Src)
} }
dest := c.Dst dest := c.Dst
if c.Dst == "" { if c.Dst == "" {
dest = c.Src dest = c.Src
} else if !path.IsAbs(dest) { } else if !path.IsAbs(dest) {
return nil, nil, fmt.Errorf("dst path %q is not absolute", dest) return nil, fmt.Errorf("dst path %q is not absolute", dest)
} }
srcH := c.Src srcH := c.Src
if err := evalSymlinks(sys, &srcH); err != nil { if err := evalSymlinks(sys, &srcH); err != nil {
return nil, nil, err return nil, err
} }
for i := range hidePaths { for i := range hidePaths {
@ -204,71 +180,54 @@ func (s *SandboxConfig) ToContainer(sys SandboxSys, uid, gid *int) (*sandbox.Par
} }
if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil { if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
return nil, nil, err return nil, err
} else if ok { } else if ok {
hidePathMatch[i] = true hidePathMatch[i] = true
sys.Printf("hiding paths from %q", c.Src) sys.Printf("hiding paths from %q", c.Src)
} }
} }
var flags int conf.Bind(c.Src, dest, !c.Must, c.Write, c.Device)
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 // hide marked paths before setting up shares
for i, ok := range hidePathMatch { for i, ok := range hidePathMatch {
if ok { if ok {
container.Tmpfs(hidePaths[i], 1<<13, 0755) conf.Tmpfs(hidePaths[i], 8192)
} }
} }
for _, l := range s.Link { for _, l := range s.Link {
container.Link(l[0], l[1]) conf.Symlink(l[0], l[1])
} }
// perf: this might work better if implemented as a setup op in container init if s.AutoEtc {
if !s.AutoEtc { etc := s.Etc
if s.Etc != "" { if etc == "" {
container.Bind(s.Etc, "/etc", 0) etc = "/etc"
} }
} else { conf.Bind(etc, Tmp+"/etc")
etcPath := s.Etc
if etcPath == "" {
etcPath = "/etc"
}
container.
Bind(etcPath, Tmp+"/etc", 0).
Mkdir("/etc", 0700)
// link host /etc contents to prevent dropping passwd/group bind mounts // link host /etc contents to prevent passwd/group from being overwritten
if d, err := sys.ReadDir(etcPath); err != nil { if d, err := sys.ReadDir(etc); err != nil {
return nil, nil, err return nil, err
} else { } else {
for _, ent := range d { for _, ent := range d {
n := ent.Name() name := ent.Name()
switch n { switch name {
case "passwd": case "passwd":
case "group": case "group":
case "mtab": case "mtab":
container.Link("/proc/mounts", "/etc/"+n) conf.Symlink("/proc/mounts", "/etc/"+name)
default: default:
container.Link(Tmp+"/etc/"+n, "/etc/"+n) conf.Symlink(Tmp+"/etc/"+name, "/etc/"+name)
} }
} }
} }
} }
return container, maps.Clone(s.Env), nil return conf, nil
} }
func evalSymlinks(sys SandboxSys, v *string) error { func evalSymlinks(sys SandboxSys, v *string) error {

2
go.mod
View File

@ -1,3 +1,3 @@
module git.gensokyo.uk/security/fortify module git.gensokyo.uk/security/fortify
go 1.23 go 1.22

View File

@ -5,7 +5,6 @@ import (
"errors" "errors"
"io" "io"
"os" "os"
"os/exec"
"slices" "slices"
"sync" "sync"
@ -62,7 +61,7 @@ func (h *helperContainer) Start() error {
h.Env = append(h.Env, FortifyStatus+"=1") h.Env = append(h.Env, FortifyStatus+"=1")
// stat is populated on fulfill // stat is populated on fulfill
h.Cancel = func(*exec.Cmd) error { return h.stat.Close() } h.Cancel = func() error { return h.stat.Close() }
} else { } else {
h.Env = append(h.Env, FortifyStatus+"=0") h.Env = append(h.Env, FortifyStatus+"=0")
} }

View File

@ -1,7 +1,6 @@
package app package app
import ( import (
"context"
"fmt" "fmt"
"log" "log"
"sync" "sync"
@ -11,10 +10,9 @@ import (
"git.gensokyo.uk/security/fortify/internal/sys" "git.gensokyo.uk/security/fortify/internal/sys"
) )
func New(ctx context.Context, os sys.State) (fst.App, error) { func New(os sys.State) (fst.App, error) {
a := new(app) a := new(app)
a.sys = os a.sys = os
a.ctx = ctx
id := new(fst.ID) id := new(fst.ID)
err := fst.NewAppID(id) err := fst.NewAppID(id)
@ -23,8 +21,8 @@ func New(ctx context.Context, os sys.State) (fst.App, error) {
return a, err return a, err
} }
func MustNew(ctx context.Context, os sys.State) fst.App { func MustNew(os sys.State) fst.App {
a, err := New(ctx, os) a, err := New(os)
if err != nil { if err != nil {
log.Fatalf("cannot create app: %v", err) log.Fatalf("cannot create app: %v", err)
} }
@ -34,7 +32,6 @@ func MustNew(ctx context.Context, os sys.State) fst.App {
type app struct { type app struct {
id *stringPair[fst.ID] id *stringPair[fst.ID]
sys sys.State sys sys.State
ctx context.Context
*outcome *outcome
mu sync.RWMutex mu sync.RWMutex
@ -74,7 +71,7 @@ func (a *app) Seal(config *fst.Config) (fst.SealedApp, error) {
seal := new(outcome) seal := new(outcome)
seal.id = a.id seal.id = a.id
err := seal.finalise(a.ctx, a.sys, config) err := seal.finalise(a.sys, config)
if err == nil { if err == nil {
a.outcome = seal a.outcome = seal
} }

View File

@ -4,7 +4,7 @@ import (
"git.gensokyo.uk/security/fortify/acl" "git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/sandbox" "git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/system" "git.gensokyo.uk/security/fortify/system"
) )
@ -13,19 +13,19 @@ var testCasesNixos = []sealTestCase{
"nixos chromium direct wayland", new(stubNixOS), "nixos chromium direct wayland", new(stubNixOS),
&fst.Config{ &fst.Config{
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start", Command: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
Confinement: fst.ConfinementConfig{ Confinement: fst.ConfinementConfig{
AppID: 1, Groups: []string{}, Username: "u0_a1", AppID: 1, Groups: []string{}, Username: "u0_a1",
Outer: "/var/lib/persist/module/fortify/0/1", Outer: "/var/lib/persist/module/fortify/0/1",
Sandbox: &fst.SandboxConfig{ Sandbox: &fst.SandboxConfig{
Userns: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, AutoEtc: true, UserNS: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, AutoEtc: true,
Filesystem: []*fst.FilesystemConfig{ Filesystem: []*fst.FilesystemConfig{
{Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true}, {Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true},
{Src: "/nix/store", Must: true}, {Src: "/run/current-system", 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: "/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}, {Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true},
}, },
Cover: []string{"/var/run/nscd"}, Override: []string{"/var/run/nscd"},
}, },
SystemBus: &dbus.Config{ SystemBus: &dbus.Config{
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"}, Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
@ -88,133 +88,136 @@ var testCasesNixos = []sealTestCase{
}). }).
UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write). UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write), UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
&sandbox.Params{ (&bwrap.Config{
Uid: 1971, Net: true,
Gid: 100, UserNS: true,
Flags: sandbox.FAllowNet | sandbox.FAllowUserns, Chdir: "/var/lib/persist/module/fortify/0/1",
Dir: "/var/lib/persist/module/fortify/0/1", Clearenv: true,
Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start", SetEnv: map[string]string{
Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"}, "DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1971/bus",
Env: []string{ "DBUS_SYSTEM_BUS_ADDRESS": "unix:path=/run/dbus/system_bus_socket",
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus", "HOME": "/var/lib/persist/module/fortify/0/1",
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket", "PULSE_COOKIE": fst.Tmp + "/pulse-cookie",
"HOME=/var/lib/persist/module/fortify/0/1", "PULSE_SERVER": "unix:/run/user/1971/pulse/native",
"PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie", "SHELL": "/run/current-system/sw/bin/zsh",
"PULSE_SERVER=unix:/run/user/1971/pulse/native", "TERM": "xterm-256color",
"TERM=xterm-256color", "USER": "u0_a1",
"USER=u0_a1", "WAYLAND_DISPLAY": "wayland-0",
"WAYLAND_DISPLAY=wayland-0", "XDG_RUNTIME_DIR": "/run/user/1971",
"XDG_RUNTIME_DIR=/run/user/1971", "XDG_SESSION_CLASS": "user",
"XDG_SESSION_CLASS=user", "XDG_SESSION_TYPE": "tty",
"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).
Mkdir("/etc", 0700).
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, 0755).
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),
}, },
Chmod: make(bwrap.ChmodConfig),
NewSession: true,
DieWithParent: true,
AsInit: true,
}).SetUID(1971).SetGID(1971).
Procfs("/proc").
Tmpfs(fst.Tmp, 4096).
DevTmpfs("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin").
Bind("/usr/bin", "/usr/bin").
Bind("/nix/store", "/nix/store").
Bind("/run/current-system", "/run/current-system").
Bind("/sys/block", "/sys/block", true).
Bind("/sys/bus", "/sys/bus", true).
Bind("/sys/class", "/sys/class", true).
Bind("/sys/dev", "/sys/dev", true).
Bind("/sys/devices", "/sys/devices", true).
Bind("/run/opengl-driver", "/run/opengl-driver").
Bind("/dev/dri", "/dev/dri", true, true, true).
Bind("/etc", fst.Tmp+"/etc").
Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
Symlink(fst.Tmp+"/etc/default", "/etc/default").
Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab").
Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
Symlink(fst.Tmp+"/etc/services", "/etc/services").
Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
Symlink(fst.Tmp+"/etc/static", "/etc/static").
Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/1971", 8388608).
Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", false, true).
Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", false, true).
CopyBind("/etc/passwd", []byte("u0_a1:x:1971:1971:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).
CopyBind("/etc/group", []byte("fortify:x:1971:\n")).
Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0").
Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native").
CopyBind(fst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus").
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket").
Tmpfs("/var/run/nscd", 8192).
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
Symlink("fortify", "/.fortify/sbin/init0"),
}, },
} }

View File

@ -6,7 +6,7 @@ import (
"git.gensokyo.uk/security/fortify/acl" "git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/sandbox" "git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/system" "git.gensokyo.uk/security/fortify/system"
) )
@ -14,6 +14,7 @@ var testCasesPd = []sealTestCase{
{ {
"nixos permissive defaults no enablements", new(stubNixOS), "nixos permissive defaults no enablements", new(stubNixOS),
&fst.Config{ &fst.Config{
Command: make([]string, 0),
Confinement: fst.ConfinementConfig{ Confinement: fst.ConfinementConfig{
AppID: 0, AppID: 0,
Username: "chronos", Username: "chronos",
@ -34,132 +35,136 @@ var testCasesPd = []sealTestCase{
Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute). 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", 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), Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
&sandbox.Params{ (&bwrap.Config{
Flags: sandbox.FAllowNet | sandbox.FAllowUserns | sandbox.FAllowTTY, Net: true,
Dir: "/home/chronos", UserNS: true,
Path: "/run/current-system/sw/bin/zsh", Clearenv: true,
Args: []string{"/run/current-system/sw/bin/zsh"}, Syscall: new(bwrap.SyscallPolicy),
Env: []string{ Chdir: "/home/chronos",
"HOME=/home/chronos", SetEnv: map[string]string{
"TERM=xterm-256color", "HOME": "/home/chronos",
"USER=chronos", "SHELL": "/run/current-system/sw/bin/zsh",
"XDG_RUNTIME_DIR=/run/user/65534", "TERM": "xterm-256color",
"XDG_SESSION_CLASS=user", "USER": "chronos",
"XDG_SESSION_TYPE=tty", "XDG_RUNTIME_DIR": "/run/user/65534",
}, "XDG_SESSION_CLASS": "user",
Ops: new(sandbox.Ops). "XDG_SESSION_TYPE": "tty"},
Proc("/proc"). Chmod: make(bwrap.ChmodConfig),
Tmpfs(fst.Tmp, 4096, 0755). DieWithParent: true,
Dev("/dev").Mqueue("/dev/mqueue"). AsInit: true,
Bind("/bin", "/bin", sandbox.BindWritable). }).SetUID(65534).SetGID(65534).
Bind("/boot", "/boot", sandbox.BindWritable). Procfs("/proc").
Bind("/home", "/home", sandbox.BindWritable). Tmpfs(fst.Tmp, 4096).
Bind("/lib", "/lib", sandbox.BindWritable). DevTmpfs("/dev").Mqueue("/dev/mqueue").
Bind("/lib64", "/lib64", sandbox.BindWritable). Bind("/bin", "/bin", false, true).
Bind("/nix", "/nix", sandbox.BindWritable). Bind("/boot", "/boot", false, true).
Bind("/root", "/root", sandbox.BindWritable). Bind("/home", "/home", false, true).
Bind("/run", "/run", sandbox.BindWritable). Bind("/lib", "/lib", false, true).
Bind("/srv", "/srv", sandbox.BindWritable). Bind("/lib64", "/lib64", false, true).
Bind("/sys", "/sys", sandbox.BindWritable). Bind("/nix", "/nix", false, true).
Bind("/usr", "/usr", sandbox.BindWritable). Bind("/root", "/root", false, true).
Bind("/var", "/var", sandbox.BindWritable). Bind("/run", "/run", false, true).
Bind("/dev/kvm", "/dev/kvm", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional). Bind("/srv", "/srv", false, true).
Tmpfs("/run/user/1971", 8192, 0755). Bind("/sys", "/sys", false, true).
Tmpfs("/run/dbus", 8192, 0755). Bind("/usr", "/usr", false, true).
Bind("/etc", fst.Tmp+"/etc", 0). Bind("/var", "/var", false, true).
Mkdir("/etc", 0700). Bind("/dev/kvm", "/dev/kvm", true, true, true).
Link(fst.Tmp+"/etc/alsa", "/etc/alsa"). Tmpfs("/run/user/1971", 8192).
Link(fst.Tmp+"/etc/bashrc", "/etc/bashrc"). Tmpfs("/run/dbus", 8192).
Link(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d"). Bind("/etc", fst.Tmp+"/etc").
Link(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1"). Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
Link(fst.Tmp+"/etc/default", "/etc/default"). Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
Link(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes"). Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
Link(fst.Tmp+"/etc/fonts", "/etc/fonts"). Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
Link(fst.Tmp+"/etc/fstab", "/etc/fstab"). Symlink(fst.Tmp+"/etc/default", "/etc/default").
Link(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf"). Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
Link(fst.Tmp+"/etc/host.conf", "/etc/host.conf"). Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
Link(fst.Tmp+"/etc/hostid", "/etc/hostid"). Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
Link(fst.Tmp+"/etc/hostname", "/etc/hostname"). Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
Link(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM"). Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
Link(fst.Tmp+"/etc/hosts", "/etc/hosts"). Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
Link(fst.Tmp+"/etc/inputrc", "/etc/inputrc"). Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
Link(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d"). Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Link(fst.Tmp+"/etc/issue", "/etc/issue"). Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
Link(fst.Tmp+"/etc/kbd", "/etc/kbd"). Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
Link(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev"). Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
Link(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf"). Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
Link(fst.Tmp+"/etc/localtime", "/etc/localtime"). Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
Link(fst.Tmp+"/etc/login.defs", "/etc/login.defs"). Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
Link(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release"). Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
Link(fst.Tmp+"/etc/lvm", "/etc/lvm"). Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
Link(fst.Tmp+"/etc/machine-id", "/etc/machine-id"). Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
Link(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf"). Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
Link(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d"). Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
Link(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d"). Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
Link("/proc/mounts", "/etc/mtab"). Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
Link(fst.Tmp+"/etc/nanorc", "/etc/nanorc"). Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
Link(fst.Tmp+"/etc/netgroup", "/etc/netgroup"). Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
Link(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager"). Symlink("/proc/mounts", "/etc/mtab").
Link(fst.Tmp+"/etc/nix", "/etc/nix"). Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
Link(fst.Tmp+"/etc/nixos", "/etc/nixos"). Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
Link(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS"). Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
Link(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf"). Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
Link(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf"). Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
Link(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd"). Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
Link(fst.Tmp+"/etc/os-release", "/etc/os-release"). Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
Link(fst.Tmp+"/etc/pam", "/etc/pam"). Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
Link(fst.Tmp+"/etc/pam.d", "/etc/pam.d"). Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
Link(fst.Tmp+"/etc/pipewire", "/etc/pipewire"). Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
Link(fst.Tmp+"/etc/pki", "/etc/pki"). Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
Link(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1"). Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
Link(fst.Tmp+"/etc/profile", "/etc/profile"). Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
Link(fst.Tmp+"/etc/protocols", "/etc/protocols"). Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
Link(fst.Tmp+"/etc/qemu", "/etc/qemu"). Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
Link(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf"). Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
Link(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf"). Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
Link(fst.Tmp+"/etc/rpc", "/etc/rpc"). Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
Link(fst.Tmp+"/etc/samba", "/etc/samba"). Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
Link(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf"). Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
Link(fst.Tmp+"/etc/secureboot", "/etc/secureboot"). Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
Link(fst.Tmp+"/etc/services", "/etc/services"). Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
Link(fst.Tmp+"/etc/set-environment", "/etc/set-environment"). Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
Link(fst.Tmp+"/etc/shadow", "/etc/shadow"). Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
Link(fst.Tmp+"/etc/shells", "/etc/shells"). Symlink(fst.Tmp+"/etc/services", "/etc/services").
Link(fst.Tmp+"/etc/ssh", "/etc/ssh"). Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
Link(fst.Tmp+"/etc/ssl", "/etc/ssl"). Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
Link(fst.Tmp+"/etc/static", "/etc/static"). Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
Link(fst.Tmp+"/etc/subgid", "/etc/subgid"). Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
Link(fst.Tmp+"/etc/subuid", "/etc/subuid"). Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
Link(fst.Tmp+"/etc/sudoers", "/etc/sudoers"). Symlink(fst.Tmp+"/etc/static", "/etc/static").
Link(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d"). Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
Link(fst.Tmp+"/etc/systemd", "/etc/systemd"). Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
Link(fst.Tmp+"/etc/terminfo", "/etc/terminfo"). Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
Link(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d"). Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
Link(fst.Tmp+"/etc/udev", "/etc/udev"). Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
Link(fst.Tmp+"/etc/udisks2", "/etc/udisks2"). Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
Link(fst.Tmp+"/etc/UPower", "/etc/UPower"). Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
Link(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf"). Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
Link(fst.Tmp+"/etc/X11", "/etc/X11"). Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
Link(fst.Tmp+"/etc/zfs", "/etc/zfs"). Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
Link(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc"). Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
Link(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo"). Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
Link(fst.Tmp+"/etc/zprofile", "/etc/zprofile"). Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv"). Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc"). Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
Tmpfs("/run/user", 4096, 0755). Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Tmpfs("/run/user/65534", 8388608, 0755). Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", sandbox.BindWritable). Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Bind("/home/chronos", "/home/chronos", sandbox.BindWritable). Tmpfs("/run/user", 1048576).
Place("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")). Tmpfs("/run/user/65534", 8388608).
Place("/etc/group", []byte("fortify:x:65534:\n")). Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", false, true).
Tmpfs("/var/run/nscd", 8192, 0755), Bind("/home/chronos", "/home/chronos", false, true).
}, CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
CopyBind("/etc/group", []byte("fortify:x:65534:\n")).
Tmpfs("/var/run/nscd", 8192).
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
Symlink("fortify", "/.fortify/sbin/init0"),
}, },
{ {
"nixos permissive defaults chromium", new(stubNixOS), "nixos permissive defaults chromium", new(stubNixOS),
&fst.Config{ &fst.Config{
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Args: []string{"zsh", "-c", "exec chromium "}, Command: []string{"/run/current-system/sw/bin/zsh", "-c", "exec chromium "},
Confinement: fst.ConfinementConfig{ Confinement: fst.ConfinementConfig{
AppID: 9, AppID: 9,
Groups: []string{"video"}, Groups: []string{"video"},
@ -249,136 +254,141 @@ var testCasesPd = []sealTestCase{
}). }).
UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write). UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write), UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
&sandbox.Params{ (&bwrap.Config{
Flags: sandbox.FAllowNet | sandbox.FAllowUserns | sandbox.FAllowTTY, Net: true,
Dir: "/home/chronos", UserNS: true,
Path: "/run/current-system/sw/bin/zsh", Chdir: "/home/chronos",
Args: []string{"zsh", "-c", "exec chromium "}, Clearenv: true,
Env: []string{ Syscall: new(bwrap.SyscallPolicy),
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus", SetEnv: map[string]string{
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket", "DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/65534/bus",
"HOME=/home/chronos", "DBUS_SYSTEM_BUS_ADDRESS": "unix:path=/run/dbus/system_bus_socket",
"PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie", "HOME": "/home/chronos",
"PULSE_SERVER=unix:/run/user/65534/pulse/native", "PULSE_COOKIE": fst.Tmp + "/pulse-cookie",
"TERM=xterm-256color", "PULSE_SERVER": "unix:/run/user/65534/pulse/native",
"USER=chronos", "SHELL": "/run/current-system/sw/bin/zsh",
"WAYLAND_DISPLAY=wayland-0", "TERM": "xterm-256color",
"XDG_RUNTIME_DIR=/run/user/65534", "USER": "chronos",
"XDG_SESSION_CLASS=user", "WAYLAND_DISPLAY": "wayland-0",
"XDG_SESSION_TYPE=tty", "XDG_RUNTIME_DIR": "/run/user/65534",
}, "XDG_SESSION_CLASS": "user",
Ops: new(sandbox.Ops). "XDG_SESSION_TYPE": "tty",
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).
Mkdir("/etc", 0700).
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, 0755).
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),
}, },
Chmod: make(bwrap.ChmodConfig),
DieWithParent: true,
AsInit: true,
}).SetUID(65534).SetGID(65534).
Procfs("/proc").
Tmpfs(fst.Tmp, 4096).
DevTmpfs("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", false, true).
Bind("/boot", "/boot", false, true).
Bind("/home", "/home", false, true).
Bind("/lib", "/lib", false, true).
Bind("/lib64", "/lib64", false, true).
Bind("/nix", "/nix", false, true).
Bind("/root", "/root", false, true).
Bind("/run", "/run", false, true).
Bind("/srv", "/srv", false, true).
Bind("/sys", "/sys", false, true).
Bind("/usr", "/usr", false, true).
Bind("/var", "/var", false, true).
Bind("/dev/dri", "/dev/dri", true, true, true).
Bind("/dev/kvm", "/dev/kvm", true, true, true).
Tmpfs("/run/user/1971", 8192).
Tmpfs("/run/dbus", 8192).
Bind("/etc", fst.Tmp+"/etc").
Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
Symlink(fst.Tmp+"/etc/default", "/etc/default").
Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab").
Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
Symlink(fst.Tmp+"/etc/services", "/etc/services").
Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
Symlink(fst.Tmp+"/etc/static", "/etc/static").
Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/65534", 8388608).
Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", false, true).
Bind("/home/chronos", "/home/chronos", false, true).
CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
CopyBind("/etc/group", []byte("fortify:x:65534:\n")).
Bind("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/65534/wayland-0").
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native").
CopyBind(fst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket").
Tmpfs("/var/run/nscd", 8192).
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
Symlink("fortify", "/.fortify/sbin/init0"),
}, },
} }

View File

@ -17,8 +17,7 @@ type stubNixOS struct {
usernameErr map[string]error usernameErr map[string]error
} }
func (s *stubNixOS) Getuid() int { return 1971 } func (s *stubNixOS) Geteuid() int { return 1971 }
func (s *stubNixOS) Getgid() int { return 100 }
func (s *stubNixOS) TempDir() string { return "/tmp" } func (s *stubNixOS) TempDir() string { return "/tmp" }
func (s *stubNixOS) MustExecutable() string { return "/run/wrappers/bin/fortify" } func (s *stubNixOS) MustExecutable() string { return "/run/wrappers/bin/fortify" }
func (s *stubNixOS) Exit(code int) { panic("called exit on stub with code " + strconv.Itoa(code)) } func (s *stubNixOS) Exit(code int) { panic("called exit on stub with code " + strconv.Itoa(code)) }
@ -55,8 +54,10 @@ func (s *stubNixOS) LookPath(file string) (string, error) {
} }
switch file { switch file {
case "zsh": case "sudo":
return "/run/current-system/sw/bin/zsh", nil return "/run/wrappers/bin/sudo", nil
case "machinectl":
return "/home/ophestra/.nix-profile/bin/machinectl", nil
default: default:
panic(fmt.Sprintf("attempted to look up unexpected executable %q", file)) panic(fmt.Sprintf("attempted to look up unexpected executable %q", file))
} }

View File

@ -8,9 +8,9 @@ import (
"time" "time"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/app" "git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/sys" "git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system" "git.gensokyo.uk/security/fortify/system"
) )
@ -20,7 +20,7 @@ type sealTestCase struct {
config *fst.Config config *fst.Config
id fst.ID id fst.ID
wantSys *system.I wantSys *system.I
wantContainer *sandbox.Params wantBwrap *bwrap.Config
} }
func TestApp(t *testing.T) { func TestApp(t *testing.T) {
@ -31,14 +31,14 @@ func TestApp(t *testing.T) {
a := app.NewWithID(tc.id, tc.os) a := app.NewWithID(tc.id, tc.os)
var ( var (
gotSys *system.I gotSys *system.I
gotContainer *sandbox.Params gotBwrap *bwrap.Config
) )
if !t.Run("seal", func(t *testing.T) { if !t.Run("seal", func(t *testing.T) {
if sa, err := a.Seal(tc.config); err != nil { if sa, err := a.Seal(tc.config); err != nil {
t.Errorf("Seal: error = %v", err) t.Errorf("Seal: error = %v", err)
return return
} else { } else {
gotSys, gotContainer = app.AppIParams(a, sa) gotSys, gotBwrap = app.AppSystemBwrap(a, sa)
} }
}) { }) {
return return
@ -51,10 +51,10 @@ func TestApp(t *testing.T) {
} }
}) })
t.Run("compare params", func(t *testing.T) { t.Run("compare bwrap", func(t *testing.T) {
if !reflect.DeepEqual(gotContainer, tc.wantContainer) { if !reflect.DeepEqual(gotBwrap, tc.wantBwrap) {
t.Errorf("seal: params =\n%s\n, want\n%s", t.Errorf("seal: bwrap =\n%s\n, want\n%s",
mustMarshal(gotContainer), mustMarshal(tc.wantContainer)) mustMarshal(gotBwrap), mustMarshal(tc.wantBwrap))
} }
}) })
}) })

View File

@ -2,8 +2,8 @@ package app
import ( import (
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/sys" "git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system" "git.gensokyo.uk/security/fortify/system"
) )
@ -14,7 +14,7 @@ func NewWithID(id fst.ID, os sys.State) fst.App {
return a return a
} }
func AppIParams(a fst.App, sa fst.SealedApp) (*system.I, *sandbox.Params) { func AppSystemBwrap(a fst.App, sa fst.SealedApp) (*system.I, *bwrap.Config) {
v := a.(*app) v := a.(*app)
seal := sa.(*outcome) seal := sa.(*outcome)
if v.outcome != seal || v.id != seal.id { if v.outcome != seal || v.id != seal.id {

View File

@ -0,0 +1,18 @@
package init0
import (
"os"
"path"
"git.gensokyo.uk/security/fortify/internal"
)
// used by the parent process
// TryArgv0 calls [Main] if the last element of argv0 is "init0".
func TryArgv0() {
if len(os.Args) > 0 && path.Base(os.Args[0]) == "init0" {
Main()
internal.Exit(0)
}
}

165
internal/app/init0/main.go Normal file
View File

@ -0,0 +1,165 @@
package init0
import (
"errors"
"log"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/sandbox"
)
const (
// time to wait for linger processes after death of initial process
residualProcessTimeout = 5 * time.Second
)
// everything beyond this point runs within pid namespace
// proceed with caution!
func Main() {
// sharing stdout with shim
// USE WITH CAUTION
fmsg.Prepare("init0")
// setting this prevents ptrace
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
}
if os.Getpid() != 1 {
log.Fatal("this process must run as pid 1")
}
// receive setup payload
var (
payload Payload
closeSetup func() error
)
if f, err := sandbox.Receive(Env, &payload, nil); err != nil {
if errors.Is(err, sandbox.ErrInvalid) {
log.Fatal("invalid config descriptor")
}
if errors.Is(err, sandbox.ErrNotSet) {
log.Fatal("FORTIFY_INIT not set")
}
log.Fatalf("cannot decode init setup payload: %v", err)
} else {
fmsg.Store(payload.Verbose)
closeSetup = f
// child does not need to see this
if err = os.Unsetenv(Env); err != nil {
log.Printf("cannot unset %s: %v", Env, err)
// not fatal
} else {
fmsg.Verbose("received configuration")
}
}
// die with parent
if err := sandbox.SetPdeathsig(syscall.SIGKILL); err != nil {
log.Fatalf("prctl(PR_SET_PDEATHSIG, SIGKILL): %v", err)
}
cmd := exec.Command(payload.Argv0)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Args = payload.Argv
cmd.Env = os.Environ()
if err := cmd.Start(); err != nil {
log.Fatalf("cannot start %q: %v", payload.Argv0, err)
}
fmsg.Suspend()
// close setup pipe as setup is now complete
if err := closeSetup(); err != nil {
log.Println("cannot close setup pipe:", err)
// not fatal
}
sig := make(chan os.Signal, 2)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
type winfo struct {
wpid int
wstatus syscall.WaitStatus
}
info := make(chan winfo, 1)
done := make(chan struct{})
go func() {
var (
err error
wpid = -2
wstatus syscall.WaitStatus
)
// keep going until no child process is left
for wpid != -1 {
if err != nil {
break
}
if wpid != -2 {
info <- winfo{wpid, wstatus}
}
err = syscall.EINTR
for errors.Is(err, syscall.EINTR) {
wpid, err = syscall.Wait4(-1, &wstatus, 0, nil)
}
}
if !errors.Is(err, syscall.ECHILD) {
log.Println("unexpected wait4 response:", err)
}
close(done)
}()
// closed after residualProcessTimeout has elapsed after initial process death
timeout := make(chan struct{})
r := 2
for {
select {
case s := <-sig:
if fmsg.Resume() {
fmsg.Verbosef("terminating on %s after process start", s.String())
} else {
fmsg.Verbosef("terminating on %s", s.String())
}
internal.Exit(0)
case w := <-info:
if w.wpid == cmd.Process.Pid {
// initial process exited, output is most likely available again
fmsg.Resume()
switch {
case w.wstatus.Exited():
r = w.wstatus.ExitStatus()
case w.wstatus.Signaled():
r = 128 + int(w.wstatus.Signal())
default:
r = 255
}
go func() {
time.Sleep(residualProcessTimeout)
close(timeout)
}()
}
case <-done:
internal.Exit(r)
case <-timeout:
log.Println("timeout exceeded waiting for lingering processes")
internal.Exit(r)
}
}
}

View File

@ -0,0 +1,13 @@
package init0
const Env = "FORTIFY_INIT"
type Payload struct {
// target full exec path
Argv0 string
// child full argv
Argv []string
// verbosity pass through
Verbose bool
}

View File

@ -3,12 +3,15 @@ package app
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"log" "log"
"os/exec" "os/exec"
"path/filepath"
"strings" "strings"
"time" "time"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/app/shim" "git.gensokyo.uk/security/fortify/internal/app/shim"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
@ -18,7 +21,7 @@ import (
const shimSetupTimeout = 5 * time.Second const shimSetupTimeout = 5 * time.Second
func (seal *outcome) Run(rs *fst.RunState) error { func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
if !seal.f.CompareAndSwap(false, true) { 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 // 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 // in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
@ -34,11 +37,33 @@ func (seal *outcome) Run(rs *fst.RunState) error {
fmsg.Verbosef("version %s", internal.Version()) fmsg.Verbosef("version %s", internal.Version())
fmsg.Verbosef("setuid helper at %s", internal.MustFsuPath()) fmsg.Verbosef("setuid helper at %s", internal.MustFsuPath())
/*
resolve exec paths
*/
shimExec := [2]string{helper.BubblewrapName}
if len(seal.command) > 0 {
shimExec[1] = seal.command[0]
}
for i, n := range shimExec {
if len(n) == 0 {
continue
}
if filepath.Base(n) == n {
if s, err := exec.LookPath(n); err == nil {
shimExec[i] = s
} else {
return fmsg.WrapError(err,
fmt.Sprintf("executable file %q not found in $PATH", n))
}
}
}
/* /*
prepare/revert os state prepare/revert os state
*/ */
if err := seal.sys.Commit(seal.ctx); err != nil { if err := seal.sys.Commit(ctx); err != nil {
return err return err
} }
store := state.NewMulti(seal.runDirPath) store := state.NewMulti(seal.runDirPath)
@ -112,6 +137,7 @@ func (seal *outcome) Run(rs *fst.RunState) error {
if startTime, err := cmd.Start( if startTime, err := cmd.Start(
seal.user.aid.String(), seal.user.aid.String(),
seal.user.supp, seal.user.supp,
seal.bwrapSync,
); err != nil { ); err != nil {
return err return err
} else { } else {
@ -119,7 +145,7 @@ func (seal *outcome) Run(rs *fst.RunState) error {
rs.Time = startTime rs.Time = startTime
} }
ctx, cancel := context.WithTimeout(seal.ctx, shimSetupTimeout) c, cancel := context.WithTimeout(ctx, shimSetupTimeout)
defer cancel() defer cancel()
go func() { go func() {
@ -128,8 +154,10 @@ func (seal *outcome) Run(rs *fst.RunState) error {
cancel() cancel()
}() }()
if err := cmd.Serve(ctx, &shim.Params{ if err := cmd.Serve(c, &shim.Payload{
Container: seal.container, Argv: seal.command,
Exec: shimExec,
Bwrap: seal.container,
Home: seal.user.data, Home: seal.user.data,
Verbose: fmsg.Load(), Verbose: fmsg.Load(),
@ -171,22 +199,18 @@ func (seal *outcome) Run(rs *fst.RunState) error {
// this is reached when a fault makes an already running shim impossible to continue execution // 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) // 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 // the effects of this is similar to the alternative exit path and ensures shim death
case err := <-cmd.Fallback(): case err := <-cmd.WaitFallback():
rs.ExitCode = 255 rs.ExitCode = 255
log.Printf("cannot terminate shim on faulted setup: %v", err) log.Printf("cannot terminate shim on faulted setup: %v", err)
// alternative exit path relying on shim behaviour on monitor process exit // alternative exit path relying on shim behaviour on monitor process exit
case <-seal.ctx.Done(): case <-ctx.Done():
fmsg.Verbose("alternative exit path selected") fmsg.Verbose("alternative exit path selected")
} }
fmsg.Resume() 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 { if seal.dbusMsg != nil {
// dump dbus message buffer
seal.dbusMsg() seal.dbusMsg()
} }

View File

@ -2,28 +2,24 @@ package app
import ( import (
"bytes" "bytes"
"context"
"encoding/gob" "encoding/gob"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"maps"
"os" "os"
"path" "path"
"regexp" "regexp"
"slices"
"strings" "strings"
"sync/atomic" "sync/atomic"
"syscall"
"git.gensokyo.uk/security/fortify/acl" "git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/sys" "git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system" "git.gensokyo.uk/security/fortify/system"
"git.gensokyo.uk/security/fortify/wl" "git.gensokyo.uk/security/fortify/wl"
) )
@ -69,19 +65,19 @@ type outcome struct {
// copied from [sys.State] response // copied from [sys.State] response
runDirPath string runDirPath string
// passed through from [fst.Config]
command []string
// initial [fst.Config] gob stream for state data; // initial [fst.Config] gob stream for state data;
// this is prepared ahead of time as config is clobbered during seal creation // this is prepared ahead of time as config is mutated during seal creation
ct io.WriterTo ct io.WriterTo
// dump dbus proxy message buffer // dump dbus proxy message buffer
dbusMsg func() dbusMsg func()
user fsuUser user fsuUser
sys *system.I sys *system.I
ctx context.Context container *bwrap.Config
bwrapSync *os.File
container *sandbox.Params
env map[string]string
sync *os.File
f atomic.Bool f atomic.Bool
} }
@ -104,17 +100,7 @@ type fsuUser struct {
username string username string
} }
func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Config) error { func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
if seal.ctx != nil {
panic("finalise called twice")
}
seal.ctx = ctx
shellPath := "/bin/sh"
if s, ok := sys.LookupEnv(shell); ok && path.IsAbs(s) {
shellPath = s
}
{ {
// encode initial configuration for state tracking // encode initial configuration for state tracking
ct := new(bytes.Buffer) ct := new(bytes.Buffer)
@ -125,6 +111,9 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
seal.ct = ct seal.ct = ct
} }
// pass through command slice; this value is never touched in the main process
seal.command = config.Command
// allowed aid range 0 to 9999, this is checked again in fsu // allowed aid range 0 to 9999, this is checked again in fsu
if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 { if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 {
return fmsg.WrapError(ErrUser, return fmsg.WrapError(ErrUser,
@ -178,23 +167,11 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
if config.Confinement.Sandbox == nil { if config.Confinement.Sandbox == nil {
fmsg.Verbose("sandbox configuration not supplied, PROCEED WITH CAUTION") fmsg.Verbose("sandbox configuration not supplied, PROCEED WITH CAUTION")
// fsu clears the environment so resolve paths early
if !path.IsAbs(config.Path) {
if len(config.Args) > 0 {
if p, err := sys.LookPath(config.Args[0]); err != nil {
return fmsg.WrapError(err, err.Error())
} else {
config.Path = p
}
} else {
config.Path = shellPath
}
}
conf := &fst.SandboxConfig{ conf := &fst.SandboxConfig{
Userns: true, UserNS: true,
Net: true, Net: true,
Tty: true, Syscall: new(bwrap.SyscallPolicy),
NoNewSession: true,
AutoEtc: true, AutoEtc: true,
} }
// bind entries in / // bind entries in /
@ -221,7 +198,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
// hide nscd from sandbox if present // hide nscd from sandbox if present
nscd := "/var/run/nscd" nscd := "/var/run/nscd"
if _, err := sys.Stat(nscd); !errors.Is(err, fs.ErrNotExist) { if _, err := sys.Stat(nscd); !errors.Is(err, fs.ErrNotExist) {
conf.Cover = append(conf.Cover, nscd) conf.Override = append(conf.Override, nscd)
} }
// bind GPU stuff // bind GPU stuff
if config.Confinement.Enablements.Has(system.EX11) || config.Confinement.Enablements.Has(system.EWayland) { if config.Confinement.Enablements.Has(system.EX11) || config.Confinement.Enablements.Has(system.EWayland) {
@ -233,29 +210,17 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
config.Confinement.Sandbox = conf config.Confinement.Sandbox = conf
} }
var mapuid, mapgid *stringPair[int] var mapuid *stringPair[int]
{ {
var uid, gid int var uid int
var err error var err error
seal.container, seal.env, err = config.Confinement.Sandbox.ToContainer(sys, &uid, &gid) seal.container, err = config.Confinement.Sandbox.Bwrap(sys, &uid)
if err != nil { if err != nil {
return fmsg.WrapErrorSuffix(err, return err
"cannot initialise container configuration:")
} }
if !path.IsAbs(config.Path) {
return fmsg.WrapError(syscall.EINVAL,
"invalid program path")
}
if len(config.Args) == 0 {
config.Args = []string{config.Path}
}
seal.container.Path = config.Path
seal.container.Args = config.Args
mapuid = newInt(uid) mapuid = newInt(uid)
mapgid = newInt(gid) if seal.container.SetEnv == nil {
if seal.env == nil { seal.container.SetEnv = make(map[string]string)
seal.env = make(map[string]string)
} }
} }
@ -290,27 +255,35 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as post-fsu user // inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as post-fsu user
innerRuntimeDir := path.Join("/run/user", mapuid.String()) innerRuntimeDir := path.Join("/run/user", mapuid.String())
seal.container.Tmpfs("/run/user", 1<<12, 0755) seal.container.Tmpfs("/run/user", 1*1024*1024)
seal.container.Tmpfs(innerRuntimeDir, 1<<23, 0755) seal.container.Tmpfs(innerRuntimeDir, 8*1024*1024)
seal.env[xdgRuntimeDir] = innerRuntimeDir seal.container.SetEnv[xdgRuntimeDir] = innerRuntimeDir
seal.env[xdgSessionClass] = "user" seal.container.SetEnv[xdgSessionClass] = "user"
seal.env[xdgSessionType] = "tty" seal.container.SetEnv[xdgSessionType] = "tty"
// outer path for inner /tmp // outer path for inner /tmp
{ {
tmpdir := path.Join(sc.SharePath, "tmpdir") tmpdir := path.Join(sc.SharePath, "tmpdir")
seal.sys.Ensure(tmpdir, 0700) seal.sys.Ensure(tmpdir, 0700)
seal.sys.UpdatePermType(system.User, tmpdir, acl.Execute) seal.sys.UpdatePermType(system.User, tmpdir, acl.Execute)
tmpdirInst := path.Join(tmpdir, seal.user.aid.String()) tmpdirProc := path.Join(tmpdir, seal.user.aid.String())
seal.sys.Ensure(tmpdirInst, 01700) seal.sys.Ensure(tmpdirProc, 01700)
seal.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.User, tmpdirProc, acl.Read, acl.Write, acl.Execute)
seal.container.Bind(tmpdirInst, "/tmp", sandbox.BindWritable) seal.container.Bind(tmpdirProc, "/tmp", false, true)
} }
/* /*
Passwd database Passwd database
*/ */
// look up shell
sh := "/bin/sh"
if s, ok := sys.LookupEnv(shell); ok {
seal.container.SetEnv[shell] = s
sh = s
}
// bind home directory
homeDir := "/var/empty" homeDir := "/var/empty"
if seal.user.home != "" { if seal.user.home != "" {
homeDir = seal.user.home homeDir = seal.user.home
@ -319,25 +292,27 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
if seal.user.username != "" { if seal.user.username != "" {
username = seal.user.username username = seal.user.username
} }
seal.container.Bind(seal.user.data, homeDir, sandbox.BindWritable) seal.container.Bind(seal.user.data, homeDir, false, true)
seal.container.Dir = homeDir seal.container.Chdir = homeDir
seal.env["HOME"] = homeDir seal.container.SetEnv["HOME"] = homeDir
seal.env["USER"] = username seal.container.SetEnv["USER"] = username
seal.container.Place("/etc/passwd", // generate /etc/passwd and /etc/group
[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Fortify:"+homeDir+":"+shellPath+"\n")) seal.container.CopyBind("/etc/passwd",
seal.container.Place("/etc/group", []byte(username+":x:"+mapuid.String()+":"+mapuid.String()+":Fortify:"+homeDir+":"+sh+"\n"))
[]byte("fortify:x:"+mapgid.String()+":\n")) seal.container.CopyBind("/etc/group",
[]byte("fortify:x:"+mapuid.String()+":\n"))
/* /*
Display servers Display servers
*/ */
// pass $TERM for proper terminal I/O in shell // pass $TERM to launcher
if t, ok := sys.LookupEnv(term); ok { if t, ok := sys.LookupEnv(term); ok {
seal.env[term] = t seal.container.SetEnv[term] = t
} }
// set up wayland
if config.Confinement.Enablements.Has(system.EWayland) { if config.Confinement.Enablements.Has(system.EWayland) {
// outer wayland socket (usually `/run/user/%d/wayland-%d`) // outer wayland socket (usually `/run/user/%d/wayland-%d`)
var socketPath string var socketPath string
@ -351,7 +326,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
} }
innerPath := path.Join(innerRuntimeDir, wl.FallbackName) innerPath := path.Join(innerRuntimeDir, wl.FallbackName)
seal.env[wl.WaylandDisplay] = wl.FallbackName seal.container.SetEnv[wl.WaylandDisplay] = wl.FallbackName
if !config.Confinement.Sandbox.DirectWayland { // set up security-context-v1 if !config.Confinement.Sandbox.DirectWayland { // set up security-context-v1
socketDir := path.Join(sc.SharePath, "wayland") socketDir := path.Join(sc.SharePath, "wayland")
@ -362,23 +337,25 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
// use instance ID in case app id is not set // use instance ID in case app id is not set
appID = "uk.gensokyo.fortify." + seal.id.String() appID = "uk.gensokyo.fortify." + seal.id.String()
} }
seal.sys.Wayland(&seal.sync, outerPath, socketPath, appID, seal.id.String()) seal.sys.Wayland(&seal.bwrapSync, outerPath, socketPath, appID, seal.id.String())
seal.container.Bind(outerPath, innerPath, 0) seal.container.Bind(outerPath, innerPath)
} else { // bind mount wayland socket (insecure) } else { // bind mount wayland socket (insecure)
fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION") fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION")
seal.container.Bind(socketPath, innerPath, 0) seal.container.Bind(socketPath, innerPath)
seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
} }
} }
// set up X11
if config.Confinement.Enablements.Has(system.EX11) { if config.Confinement.Enablements.Has(system.EX11) {
// discover X11 and grant user permission via the `ChangeHosts` command
if d, ok := sys.LookupEnv(display); !ok { if d, ok := sys.LookupEnv(display); !ok {
return fmsg.WrapError(ErrXDisplay, return fmsg.WrapError(ErrXDisplay,
"DISPLAY is not set") "DISPLAY is not set")
} else { } else {
seal.sys.ChangeHosts("#" + seal.user.uid.String()) seal.sys.ChangeHosts("#" + seal.user.uid.String())
seal.env[display] = d seal.container.SetEnv[display] = d
seal.container.Bind("/tmp/.X11-unix", "/tmp/.X11-unix", 0) seal.container.Bind("/tmp/.X11-unix", "/tmp/.X11-unix")
} }
} }
@ -419,8 +396,8 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
innerPulseRuntimeDir := path.Join(sharePathLocal, "pulse") innerPulseRuntimeDir := path.Join(sharePathLocal, "pulse")
innerPulseSocket := path.Join(innerRuntimeDir, "pulse", "native") innerPulseSocket := path.Join(innerRuntimeDir, "pulse", "native")
seal.sys.Link(pulseSocket, innerPulseRuntimeDir) seal.sys.Link(pulseSocket, innerPulseRuntimeDir)
seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0) seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket)
seal.env[pulseServer] = "unix:" + innerPulseSocket seal.container.SetEnv[pulseServer] = "unix:" + innerPulseSocket
// publish current user's pulse cookie for target user // publish current user's pulse cookie for target user
if src, err := discoverPulseCookie(sys); err != nil { if src, err := discoverPulseCookie(sys); err != nil {
@ -428,9 +405,9 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
fmsg.Verbose(strings.TrimSpace(err.(*fmsg.BaseError).Message())) fmsg.Verbose(strings.TrimSpace(err.(*fmsg.BaseError).Message()))
} else { } else {
innerDst := fst.Tmp + "/pulse-cookie" innerDst := fst.Tmp + "/pulse-cookie"
seal.env[pulseCookie] = innerDst seal.container.SetEnv[pulseCookie] = innerDst
var payload *[]byte payload := new([]byte)
seal.container.PlaceP(innerDst, &payload) seal.container.CopyBindRef(innerDst, &payload)
seal.sys.CopyFile(payload, src, 256, 256) seal.sys.CopyFile(payload, src, 256, 256)
} }
} }
@ -460,13 +437,13 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
// share proxy sockets // share proxy sockets
sessionInner := path.Join(innerRuntimeDir, "bus") sessionInner := path.Join(innerRuntimeDir, "bus")
seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner seal.container.SetEnv[dbusSessionBusAddress] = "unix:path=" + sessionInner
seal.container.Bind(sessionPath, sessionInner, 0) seal.container.Bind(sessionPath, sessionInner)
seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write) seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
if config.Confinement.SystemBus != nil { if config.Confinement.SystemBus != nil {
systemInner := "/run/dbus/system_bus_socket" systemInner := "/run/dbus/system_bus_socket"
seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner seal.container.SetEnv[dbusSystemBusAddress] = "unix:path=" + systemInner
seal.container.Bind(systemPath, systemInner, 0) seal.container.Bind(systemPath, systemInner)
seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write) seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
} }
} }
@ -475,8 +452,9 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
Miscellaneous Miscellaneous
*/ */
for _, dest := range config.Confinement.Sandbox.Cover { // queue overriding tmpfs at the end of seal.container.Filesystem
seal.container.Tmpfs(dest, 1<<13, 0755) for _, dest := range config.Confinement.Sandbox.Override {
seal.container.Tmpfs(dest, 8*1024)
} }
// append ExtraPerms last // append ExtraPerms last
@ -502,13 +480,12 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
seal.sys.UpdatePermType(system.User, p.Path, perms...) seal.sys.UpdatePermType(system.User, p.Path, perms...)
} }
// flatten and sort env for deterministic behaviour // mount fortify in sandbox for init
seal.container.Env = make([]string, 0, len(seal.env)) seal.container.Bind(sys.MustExecutable(), path.Join(fst.Tmp, "sbin/fortify"))
maps.All(seal.env)(func(k string, v string) bool { seal.container.Env = append(seal.container.Env, k+"="+v); return true }) seal.container.Symlink("fortify", path.Join(fst.Tmp, "sbin/init0"))
slices.Sort(seal.container.Env)
fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s", fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, command: %s",
seal.user.uid, seal.user.username, config.Confinement.Groups, seal.container.Args) seal.user.uid, seal.user.username, config.Confinement.Groups, config.Command)
return nil return nil
} }

View File

@ -7,26 +7,18 @@ import (
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"path"
"strconv"
"syscall" "syscall"
"time"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/app/init0"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/sandbox" "git.gensokyo.uk/security/fortify/sandbox"
) )
const Env = "FORTIFY_SHIM"
type Params struct {
// finalised container params
Container *sandbox.Params
// path to outer home directory
Home string
// verbosity pass through
Verbose bool
}
// everything beyond this point runs as unconstrained target user // everything beyond this point runs as unconstrained target user
// proceed with caution! // proceed with caution!
@ -35,15 +27,17 @@ func Main() {
// USE WITH CAUTION // USE WITH CAUTION
fmsg.Prepare("shim") fmsg.Prepare("shim")
// setting this prevents ptrace
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil { if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err) log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
} }
// receive setup payload
var ( var (
params Params payload Payload
closeSetup func() error closeSetup func() error
) )
if f, err := sandbox.Receive(Env, &params, nil); err != nil { if f, err := sandbox.Receive(Env, &payload, nil); err != nil {
if errors.Is(err, sandbox.ErrInvalid) { if errors.Is(err, sandbox.ErrInvalid) {
log.Fatal("invalid config descriptor") log.Fatal("invalid config descriptor")
} }
@ -51,26 +45,32 @@ func Main() {
log.Fatal("FORTIFY_SHIM not set") log.Fatal("FORTIFY_SHIM not set")
} }
log.Fatalf("cannot receive shim setup params: %v", err) log.Fatalf("cannot decode shim setup payload: %v", err)
} else { } else {
internal.InstallFmsg(params.Verbose) internal.InstallFmsg(payload.Verbose)
closeSetup = f closeSetup = f
} }
if params.Container == nil || params.Container.Ops == nil { if payload.Bwrap == nil {
log.Fatal("invalid container params") log.Fatal("bwrap config not supplied")
}
// restore bwrap sync fd
var syncFd *os.File
if payload.Sync != nil {
syncFd = os.NewFile(*payload.Sync, "sync")
} }
// close setup socket // close setup socket
if err := closeSetup(); err != nil { if err := closeSetup(); err != nil {
log.Printf("cannot close setup pipe: %v", err) log.Println("cannot close setup pipe:", err)
// not fatal // not fatal
} }
// ensure home directory as target user // ensure home directory as target user
if s, err := os.Stat(params.Home); err != nil { if s, err := os.Stat(payload.Home); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
if err = os.Mkdir(params.Home, 0700); err != nil { if err = os.Mkdir(payload.Home, 0700); err != nil {
log.Fatalf("cannot create home directory: %v", err) log.Fatalf("cannot create home directory: %v", err)
} }
} else { } else {
@ -79,37 +79,72 @@ func Main() {
// home directory is created, proceed // home directory is created, proceed
} else if !s.IsDir() { } else if !s.IsDir() {
log.Fatalf("path %q is not a directory", params.Home) log.Fatalf("data path %q is not a directory", payload.Home)
} }
var name string var ic init0.Payload
if len(params.Container.Args) > 0 {
name = params.Container.Args[0] // resolve argv0
ic.Argv = payload.Argv
if len(ic.Argv) > 0 {
// looked up from $PATH by parent
ic.Argv0 = payload.Exec[1]
} else {
// no argv, look up shell instead
var ok bool
if payload.Bwrap.SetEnv == nil {
log.Fatal("no command was specified and environment is unset")
} }
if ic.Argv0, ok = payload.Bwrap.SetEnv["SHELL"]; !ok {
log.Fatal("no command was specified and $SHELL was unset")
}
ic.Argv = []string{ic.Argv0}
}
conf := payload.Bwrap
var extraFiles []*os.File
// serve setup payload
if fd, encoder, err := sandbox.Setup(&extraFiles); err != nil {
log.Fatalf("cannot pipe: %v", err)
} else {
conf.SetEnv[init0.Env] = strconv.Itoa(fd)
go func() {
fmsg.Verbose("transmitting config to init")
if err = encoder.Encode(&ic); err != nil {
log.Fatalf("cannot transmit init config: %v", err)
}
}()
}
helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop() // unreachable defer stop() // unreachable
container := sandbox.New(ctx, name) if b, err := helper.NewBwrap(
container.Params = *params.Container ctx, path.Join(fst.Tmp, "sbin/init0"),
container.Stdin, container.Stdout, container.Stderr = os.Stdin, os.Stdout, os.Stderr nil, false,
container.Cancel = func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) } func(int, int) []string { return make([]string, 0) },
container.WaitDelay = 2 * time.Second func(cmd *exec.Cmd) { cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr },
extraFiles,
if err := container.Start(); err != nil { conf, syncFd,
fmsg.PrintBaseError(err, "cannot start container:") ); err != nil {
os.Exit(1) log.Fatalf("malformed sandbox config: %v", err)
} } else {
if err := container.Serve(); err != nil { // run and pass through exit code
fmsg.PrintBaseError(err, "cannot configure container:") if err = b.Start(); err != nil {
} log.Fatalf("cannot start target process: %v", err)
if err := container.Wait(); err != nil { } else if err = b.Wait(); err != nil {
var exitError *exec.ExitError var exitError *exec.ExitError
if !errors.As(err, &exitError) { if !errors.As(err, &exitError) {
if errors.Is(err, context.Canceled) {
os.Exit(2)
}
log.Printf("wait: %v", err) log.Printf("wait: %v", err)
os.Exit(127) internal.Exit(127)
panic("unreachable")
}
internal.Exit(exitError.ExitCode())
panic("unreachable")
} }
os.Exit(exitError.ExitCode())
} }
} }

View File

@ -8,9 +8,9 @@ import (
"os/exec" "os/exec"
"strconv" "strconv"
"strings" "strings"
"syscall"
"time" "time"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/sandbox" "git.gensokyo.uk/security/fortify/sandbox"
@ -25,11 +25,10 @@ type Shim struct {
killFallback chan error killFallback chan error
// monitor to shim encoder // monitor to shim encoder
encoder *gob.Encoder encoder *gob.Encoder
// bwrap --sync-fd value
sync *uintptr
} }
func (s *Shim) Unwrap() *exec.Cmd { return s.cmd }
func (s *Shim) Fallback() chan error { return s.killFallback }
func (s *Shim) String() string { func (s *Shim) String() string {
if s.cmd == nil { if s.cmd == nil {
return "(unused shim manager)" return "(unused shim manager)"
@ -37,9 +36,21 @@ func (s *Shim) String() string {
return s.cmd.String() return s.cmd.String()
} }
func (s *Shim) Unwrap() *exec.Cmd {
return s.cmd
}
func (s *Shim) WaitFallback() chan error {
return s.killFallback
}
func (s *Shim) Start( func (s *Shim) Start(
// string representation of application id
aid string, aid string,
// string representation of supplementary group ids
supp []string, supp []string,
// bwrap --sync-fd
syncFd *os.File,
) (*time.Time, error) { ) (*time.Time, error) {
// prepare user switcher invocation // prepare user switcher invocation
fsuPath := internal.MustFsuPath() fsuPath := internal.MustFsuPath()
@ -65,6 +76,12 @@ func (s *Shim) Start(
s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
s.cmd.Dir = "/" s.cmd.Dir = "/"
// pass sync fd if set
if syncFd != nil {
fd := proc.ExtraFile(s.cmd, syncFd)
s.sync = &fd
}
fmsg.Verbose("starting shim via fsu:", s.cmd) fmsg.Verbose("starting shim via fsu:", s.cmd)
// withhold messages to stderr // withhold messages to stderr
fmsg.Suspend() fmsg.Suspend()
@ -73,11 +90,10 @@ func (s *Shim) Start(
"cannot start fsu:") "cannot start fsu:")
} }
startTime := time.Now().UTC() startTime := time.Now().UTC()
return &startTime, nil return &startTime, nil
} }
func (s *Shim) Serve(ctx context.Context, params *Params) error { func (s *Shim) Serve(ctx context.Context, payload *Payload) error {
// kill shim if something goes wrong and an error is returned // kill shim if something goes wrong and an error is returned
s.killFallback = make(chan error, 1) s.killFallback = make(chan error, 1)
killShim := func() { killShim := func() {
@ -87,8 +103,9 @@ func (s *Shim) Serve(ctx context.Context, params *Params) error {
} }
defer func() { killShim() }() defer func() { killShim() }()
payload.Sync = s.sync
encodeErr := make(chan error) encodeErr := make(chan error)
go func() { encodeErr <- s.encoder.Encode(params) }() go func() { encodeErr <- s.encoder.Encode(payload) }()
select { select {
// encode return indicates setup completion // encode return indicates setup completion
@ -104,11 +121,11 @@ func (s *Shim) Serve(ctx context.Context, params *Params) error {
case <-ctx.Done(): case <-ctx.Done():
err := ctx.Err() err := ctx.Err()
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
return fmsg.WrapError(syscall.ECANCELED, return fmsg.WrapError(errors.New("shim setup canceled"),
"shim setup canceled") "shim setup canceled")
} }
if errors.Is(err, context.DeadlineExceeded) { if errors.Is(err, context.DeadlineExceeded) {
return fmsg.WrapError(syscall.ETIMEDOUT, return fmsg.WrapError(errors.New("deadline exceeded waiting for shim"),
"deadline exceeded waiting for shim") "deadline exceeded waiting for shim")
} }
// unreachable // unreachable

View File

@ -0,0 +1,23 @@
package shim
import (
"git.gensokyo.uk/security/fortify/helper/bwrap"
)
const Env = "FORTIFY_SHIM"
type Payload struct {
// child full argv
Argv []string
// bwrap, target full exec path
Exec [2]string
// bwrap config
Bwrap *bwrap.Config
// path to outer home directory
Home string
// sync fd
Sync *uintptr
// verbosity pass through
Verbose bool
}

View File

@ -96,7 +96,7 @@ func testStore(t *testing.T, s state.Store) {
} else { } else {
slices.Sort(aids) slices.Sort(aids)
want := []int{0, 1} want := []int{0, 1}
if !slices.Equal(aids, want) { if slices.Compare(aids, want) != 0 {
t.Fatalf("List() = %#v, want %#v", aids, want) t.Fatalf("List() = %#v, want %#v", aids, want)
} }
} }

View File

@ -12,10 +12,8 @@ import (
// State provides safe interaction with operating system state. // State provides safe interaction with operating system state.
type State interface { type State interface {
// Getuid provides [os.Getuid]. // Geteuid provides [os.Geteuid].
Getuid() int Geteuid() int
// Getgid provides [os.Getgid].
Getgid() int
// LookupEnv provides [os.LookupEnv]. // LookupEnv provides [os.LookupEnv].
LookupEnv(key string) (string, bool) LookupEnv(key string) (string, bool)
// TempDir provides [os.TempDir]. // TempDir provides [os.TempDir].
@ -49,7 +47,7 @@ type State interface {
// CopyPaths is a generic implementation of [System.Paths]. // CopyPaths is a generic implementation of [System.Paths].
func CopyPaths(os State, v *fst.Paths) { func CopyPaths(os State, v *fst.Paths) {
v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Getuid())) v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Geteuid()))
fmsg.Verbosef("process share directory at %q", v.SharePath) fmsg.Verbosef("process share directory at %q", v.SharePath)

View File

@ -31,8 +31,7 @@ type Std struct {
uidMu sync.RWMutex uidMu sync.RWMutex
} }
func (s *Std) Getuid() int { return os.Getuid() } func (s *Std) Geteuid() int { return os.Geteuid() }
func (s *Std) Getgid() int { return os.Getgid() }
func (s *Std) LookupEnv(key string) (string, bool) { return os.LookupEnv(key) } func (s *Std) LookupEnv(key string) (string, bool) { return os.LookupEnv(key) }
func (s *Std) TempDir() string { return os.TempDir() } func (s *Std) TempDir() string { return os.TempDir() }
func (s *Std) LookPath(file string) (string, error) { return exec.LookPath(file) } func (s *Std) LookPath(file string) (string, error) { return exec.LookPath(file) }

18
main.go
View File

@ -20,6 +20,7 @@ import (
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/app" "git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/app/init0"
"git.gensokyo.uk/security/fortify/internal/app/shim" "git.gensokyo.uk/security/fortify/internal/app/shim"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/state" "git.gensokyo.uk/security/fortify/internal/state"
@ -42,6 +43,7 @@ var std sys.State = new(sys.Std)
func main() { func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE // early init path, skips root check and duplicate PR_SET_DUMPABLE
sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg) sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
init0.TryArgv0()
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil { if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err) log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
@ -74,7 +76,9 @@ func buildCommand(out io.Writer) command.Command {
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console"). Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable") Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable")
// internal commands
c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess }) c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess })
c.Command("init", command.UsageInternal, func([]string) error { init0.Main(); return errSuccess })
c.Command("app", "Launch app defined by the specified config file", func(args []string) error { c.Command("app", "Launch app defined by the specified config file", func(args []string) error {
if len(args) < 1 { if len(args) < 1 {
@ -83,9 +87,10 @@ func buildCommand(out io.Writer) command.Command {
// config extraArgs... // config extraArgs...
config := tryPath(args[0]) config := tryPath(args[0])
config.Args = append(config.Args, args[1:]...) config.Command = append(config.Command, args[1:]...)
runApp(config) // invoke app
runApp(app.MustNew(std), config)
panic("unreachable") panic("unreachable")
}) })
@ -108,7 +113,7 @@ func buildCommand(out io.Writer) command.Command {
// initialise config from flags // initialise config from flags
config := &fst.Config{ config := &fst.Config{
ID: fid, ID: fid,
Args: args, Command: args,
} }
if aid < 0 || aid > 9999 { if aid < 0 || aid > 9999 {
@ -194,7 +199,7 @@ func buildCommand(out io.Writer) command.Command {
} }
// invoke app // invoke app
runApp(config) runApp(app.MustNew(std), config)
panic("unreachable") panic("unreachable")
}). }).
Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"), Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
@ -274,11 +279,10 @@ func buildCommand(out io.Writer) command.Command {
return c return c
} }
func runApp(config *fst.Config) { func runApp(a fst.App, config *fst.Config) {
ctx, stop := signal.NotifyContext(context.Background(), ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM) syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable defer stop() // unreachable
a := app.MustNew(ctx, std)
rs := new(fst.RunState) rs := new(fst.RunState)
if sa, err := a.Seal(config); err != nil { if sa, err := a.Seal(config); err != nil {
@ -286,7 +290,7 @@ func runApp(config *fst.Config) {
rs.ExitCode = 1 rs.ExitCode = 1
} else { } else {
// this updates ExitCode // this updates ExitCode
app.PrintRunStateErr(rs, sa.Run(rs)) app.PrintRunStateErr(rs, sa.Run(ctx, rs))
} }
internal.Exit(rs.ExitCode) internal.Exit(rs.ExitCode)
} }

View File

@ -1,4 +1,3 @@
packages:
{ {
lib, lib,
pkgs, pkgs,
@ -27,7 +26,7 @@ let
in in
{ {
imports = [ (import ./options.nix packages) ]; imports = [ ./options.nix ];
config = mkIf cfg.enable { config = mkIf cfg.enable {
security.wrappers.fsu = { security.wrappers.fsu = {
@ -86,11 +85,12 @@ in
enablements = with app.capability; (if wayland then 1 else 0) + (if x11 then 2 else 0) + (if dbus then 4 else 0) + (if pulse then 8 else 0); enablements = with app.capability; (if wayland then 1 else 0) + (if x11 then 2 else 0) + (if dbus then 4 else 0) + (if pulse then 8 else 0);
conf = { conf = {
inherit (app) id; inherit (app) id;
path = pkgs.writeScript "${app.name}-start" '' command = [
(pkgs.writeScript "${app.name}-start" ''
#!${pkgs.zsh}${pkgs.zsh.shellPath} #!${pkgs.zsh}${pkgs.zsh.shellPath}
${script} ${script}
''; '')
args = [ "${app.name}-start" ]; ];
confinement = { confinement = {
app_id = aid; app_id = aid;
inherit (app) groups; inherit (app) groups;
@ -98,15 +98,17 @@ in
home = getsubhome fid aid; home = getsubhome fid aid;
sandbox = { sandbox = {
inherit (app) inherit (app)
devel
userns userns
net net
dev dev
tty
multiarch
env env
; ;
syscall = {
inherit (app) compat multiarch bluetooth;
deny_devel = !app.devel;
};
map_real_uid = app.mapRealUid; map_real_uid = app.mapRealUid;
no_new_session = app.tty;
direct_wayland = app.insecureWayland; direct_wayland = app.insecureWayland;
filesystem = filesystem =
let let
@ -146,7 +148,7 @@ in
] ]
++ app.extraPaths; ++ app.extraPaths;
auto_etc = true; auto_etc = true;
cover = [ "/var/run/nscd" ]; override = [ "/var/run/nscd" ];
}; };
inherit enablements; inherit enablements;
inherit (dbusConfig) session_bus system_bus; inherit (dbusConfig) session_bus system_bus;

View File

@ -1,8 +1,17 @@
packages:
{ lib, pkgs, ... }: { lib, pkgs, ... }:
let let
inherit (lib) types mkOption mkEnableOption; inherit (lib) types mkOption mkEnableOption;
fortify = pkgs.pkgsStatic.callPackage ./package.nix {
inherit (pkgs)
bubblewrap
xdg-dbus-proxy
glibc
zstd
gnutar
coreutils
;
};
in in
{ {
@ -12,13 +21,13 @@ in
package = mkOption { package = mkOption {
type = types.package; type = types.package;
default = packages.${pkgs.system}.fortify; default = fortify;
description = "The fortify package to use."; description = "The fortify package to use.";
}; };
fsuPackage = mkOption { fsuPackage = mkOption {
type = types.package; type = types.package;
default = packages.${pkgs.system}.fsu; default = pkgs.callPackage ./cmd/fsu/package.nix { inherit fortify; };
description = "The fsu package to use."; description = "The fsu package to use.";
}; };
@ -148,19 +157,21 @@ in
''; '';
}; };
devel = mkEnableOption "debugging-related kernel interfaces"; nix = mkEnableOption "nix daemon";
userns = mkEnableOption "user namespace creation"; userns = mkEnableOption "user namespace";
mapRealUid = mkEnableOption "mapping to priv-user uid";
dev = mkEnableOption "access to all devices";
tty = mkEnableOption "access to the controlling terminal"; tty = mkEnableOption "access to the controlling terminal";
multiarch = mkEnableOption "multiarch kernel-level support"; insecureWayland = mkEnableOption "direct access to the Wayland socket";
net = mkEnableOption "network access" // { net = mkEnableOption "network access" // {
default = true; default = true;
}; };
nix = mkEnableOption "nix daemon access"; compat = mkEnableOption "disable syscall filter extensions";
mapRealUid = mkEnableOption "mapping to priv-user uid"; devel = mkEnableOption "development kernel APIs";
dev = mkEnableOption "access to all devices"; multiarch = mkEnableOption "multiarch kernel support";
insecureWayland = mkEnableOption "direct access to the Wayland socket"; bluetooth = mkEnableOption "AF_BLUETOOTH socket operations";
gpu = mkOption { gpu = mkOption {
type = nullOr bool; type = nullOr bool;

View File

@ -19,13 +19,6 @@
gnutar, gnutar,
coreutils, coreutils,
# for passthru.buildInputs
go,
gcc,
# for check
util-linux,
glibc, # for ldd glibc, # for ldd
withStatic ? stdenv.hostPlatform.isStatic, withStatic ? stdenv.hostPlatform.isStatic,
}: }:
@ -37,7 +30,7 @@ buildGoModule rec {
src = builtins.path { src = builtins.path {
name = "${pname}-src"; name = "${pname}-src";
path = lib.cleanSource ./.; path = lib.cleanSource ./.;
filter = path: type: !(type == "regular" && (lib.hasSuffix ".nix" path || lib.hasSuffix ".py" path)) && !(type == "directory" && lib.hasSuffix "/test" path) && !(type == "directory" && lib.hasSuffix "/cmd/fsu" path); filter = path: type: !(type == "regular" && (lib.hasSuffix ".nix" path || lib.hasSuffix ".py" path)) && !(type == "directory" && lib.hasSuffix "/cmd/fsu" path);
}; };
vendorHash = null; vendorHash = null;
@ -115,14 +108,4 @@ buildGoModule rec {
) )
} }
''; '';
passthru.targetPkgs =
[
go
gcc
xorg.xorgproto
util-linux
]
++ buildInputs
++ nativeBuildInputs;
} }

View File

@ -50,12 +50,9 @@ func tryPath(name string) (config *fst.Config) {
func tryFd(name string) io.ReadCloser { func tryFd(name string) io.ReadCloser {
if v, err := strconv.Atoi(name); err != nil { if v, err := strconv.Atoi(name); err != nil {
if !errors.Is(err, strconv.ErrSyntax) {
fmsg.Verbosef("name cannot be interpreted as int64: %v", err) fmsg.Verbosef("name cannot be interpreted as int64: %v", err)
}
return nil return nil
} else { } else {
fmsg.Verbosef("trying config stream from %d", v)
fd := uintptr(v) fd := uintptr(v)
if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 { if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
if errors.Is(errno, syscall.EBADF) { if errors.Is(errno, syscall.EBADF) {

View File

@ -89,10 +89,10 @@ func printShowInstance(
flags = append(flags, name) flags = append(flags, name)
} }
} }
writeFlag("userns", sandbox.Userns) writeFlag("userns", sandbox.UserNS)
writeFlag("net", sandbox.Net) writeFlag("net", sandbox.Net)
writeFlag("dev", sandbox.Dev) writeFlag("dev", sandbox.Dev)
writeFlag("tty", sandbox.Tty) writeFlag("tty", sandbox.NoNewSession)
writeFlag("mapuid", sandbox.MapRealUID) writeFlag("mapuid", sandbox.MapRealUID)
writeFlag("directwl", sandbox.DirectWayland) writeFlag("directwl", sandbox.DirectWayland)
writeFlag("autoetc", sandbox.AutoEtc) writeFlag("autoetc", sandbox.AutoEtc)
@ -107,14 +107,14 @@ func printShowInstance(
} }
t.Printf(" Etc:\t%s\n", etc) t.Printf(" Etc:\t%s\n", etc)
if len(sandbox.Cover) > 0 { if len(sandbox.Override) > 0 {
t.Printf(" Cover:\t%s\n", strings.Join(sandbox.Cover, " ")) t.Printf(" Overrides:\t%s\n", strings.Join(sandbox.Override, " "))
} }
// Env map[string]string `json:"env"` // Env map[string]string `json:"env"`
// Link [][2]string `json:"symlink"` // Link [][2]string `json:"symlink"`
} }
t.Printf(" Command:\t%s\n", strings.Join(config.Args, " ")) t.Printf(" Command:\t%s\n", strings.Join(config.Command, " "))
t.Printf("\n") t.Printf("\n")
if !short { if !short {
@ -256,7 +256,7 @@ func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON boo
) )
if e.Config != nil { if e.Config != nil {
es = e.Config.Confinement.Enablements.String() es = e.Config.Confinement.Enablements.String()
cs = fmt.Sprintf("%q", e.Config.Args) cs = fmt.Sprintf("%q", e.Config.Command)
as = strconv.Itoa(e.Config.Confinement.AppID) as = strconv.Itoa(e.Config.Confinement.AppID)
} }
t.Printf("\t%s\t%d\t%s\t%s\t%s\t%s\n", t.Printf("\t%s\t%d\t%s\t%s\t%s\t%s\n",

View File

@ -43,7 +43,7 @@ func Test_printShowInstance(t *testing.T) {
Hostname: "localhost" Hostname: "localhost"
Flags: userns net dev tty mapuid autoetc Flags: userns net dev tty mapuid autoetc
Etc: /etc Etc: /etc
Cover: /var/run/nscd Overrides: /var/run/nscd
Command: 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 Filesystem
@ -127,7 +127,7 @@ App
Hostname: "localhost" Hostname: "localhost"
Flags: userns net dev tty mapuid autoetc Flags: userns net dev tty mapuid autoetc
Etc: /etc Etc: /etc
Cover: /var/run/nscd Overrides: /var/run/nscd
Command: 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 Filesystem
@ -192,8 +192,7 @@ App
"pid": 3735928559, "pid": 3735928559,
"config": { "config": {
"id": "org.chromium.Chromium", "id": "org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium", "command": [
"args": [
"chromium", "chromium",
"--ignore-gpu-blocklist", "--ignore-gpu-blocklist",
"--disable-smooth-scrolling", "--disable-smooth-scrolling",
@ -210,19 +209,24 @@ App
"home": "/var/lib/persist/home/org.chromium.Chromium", "home": "/var/lib/persist/home/org.chromium.Chromium",
"sandbox": { "sandbox": {
"hostname": "localhost", "hostname": "localhost",
"seccomp": 32,
"devel": true,
"userns": true, "userns": true,
"net": true, "net": true,
"tty": true, "dev": true,
"syscall": {
"compat": false,
"deny_devel": true,
"multiarch": true, "multiarch": true,
"linux32": false,
"can": false,
"bluetooth": false
},
"no_new_session": true,
"map_real_uid": true,
"env": { "env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY", "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com", "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT" "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
}, },
"map_real_uid": true,
"dev": true,
"filesystem": [ "filesystem": [
{ {
"src": "/nix/store" "src": "/nix/store"
@ -255,7 +259,7 @@ App
], ],
"etc": "/etc", "etc": "/etc",
"auto_etc": true, "auto_etc": true,
"cover": [ "override": [
"/var/run/nscd" "/var/run/nscd"
] ]
}, },
@ -316,8 +320,7 @@ App
`}, `},
{"json config", nil, fst.Template(), false, true, `{ {"json config", nil, fst.Template(), false, true, `{
"id": "org.chromium.Chromium", "id": "org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium", "command": [
"args": [
"chromium", "chromium",
"--ignore-gpu-blocklist", "--ignore-gpu-blocklist",
"--disable-smooth-scrolling", "--disable-smooth-scrolling",
@ -334,19 +337,24 @@ App
"home": "/var/lib/persist/home/org.chromium.Chromium", "home": "/var/lib/persist/home/org.chromium.Chromium",
"sandbox": { "sandbox": {
"hostname": "localhost", "hostname": "localhost",
"seccomp": 32,
"devel": true,
"userns": true, "userns": true,
"net": true, "net": true,
"tty": true, "dev": true,
"syscall": {
"compat": false,
"deny_devel": true,
"multiarch": true, "multiarch": true,
"linux32": false,
"can": false,
"bluetooth": false
},
"no_new_session": true,
"map_real_uid": true,
"env": { "env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY", "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com", "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT" "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
}, },
"map_real_uid": true,
"dev": true,
"filesystem": [ "filesystem": [
{ {
"src": "/nix/store" "src": "/nix/store"
@ -379,7 +387,7 @@ App
], ],
"etc": "/etc", "etc": "/etc",
"auto_etc": true, "auto_etc": true,
"cover": [ "override": [
"/var/run/nscd" "/var/run/nscd"
] ]
}, },
@ -498,8 +506,7 @@ func Test_printPs(t *testing.T) {
"pid": 3735928559, "pid": 3735928559,
"config": { "config": {
"id": "org.chromium.Chromium", "id": "org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium", "command": [
"args": [
"chromium", "chromium",
"--ignore-gpu-blocklist", "--ignore-gpu-blocklist",
"--disable-smooth-scrolling", "--disable-smooth-scrolling",
@ -516,19 +523,24 @@ func Test_printPs(t *testing.T) {
"home": "/var/lib/persist/home/org.chromium.Chromium", "home": "/var/lib/persist/home/org.chromium.Chromium",
"sandbox": { "sandbox": {
"hostname": "localhost", "hostname": "localhost",
"seccomp": 32,
"devel": true,
"userns": true, "userns": true,
"net": true, "net": true,
"tty": true, "dev": true,
"syscall": {
"compat": false,
"deny_devel": true,
"multiarch": true, "multiarch": true,
"linux32": false,
"can": false,
"bluetooth": false
},
"no_new_session": true,
"map_real_uid": true,
"env": { "env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY", "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com", "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT" "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
}, },
"map_real_uid": true,
"dev": true,
"filesystem": [ "filesystem": [
{ {
"src": "/nix/store" "src": "/nix/store"
@ -561,7 +573,7 @@ func Test_printPs(t *testing.T) {
], ],
"etc": "/etc", "etc": "/etc",
"auto_etc": true, "auto_etc": true,
"cover": [ "override": [
"/var/run/nscd" "/var/run/nscd"
] ]
}, },

6
sandbox/const.go Normal file
View File

@ -0,0 +1,6 @@
package sandbox
const (
PR_SET_NO_NEW_PRIVS = 0x26
CAP_SYS_ADMIN = 0x15
)

View File

@ -54,6 +54,7 @@ type (
// with behaviour identical to its [exec.Cmd] counterpart. // with behaviour identical to its [exec.Cmd] counterpart.
ExtraFiles []*os.File ExtraFiles []*os.File
InitParams
// Custom [exec.Cmd] initialisation function. // Custom [exec.Cmd] initialisation function.
CommandContext func(ctx context.Context) (cmd *exec.Cmd) CommandContext func(ctx context.Context) (cmd *exec.Cmd)
@ -66,16 +67,14 @@ type (
Stdout io.Writer Stdout io.Writer
Stderr io.Writer Stderr io.Writer
Cancel func(cmd *exec.Cmd) error Cancel func() error
WaitDelay time.Duration WaitDelay time.Duration
cmd *exec.Cmd cmd *exec.Cmd
ctx context.Context ctx context.Context
Params
} }
// Params holds container configuration and is safe to serialise. InitParams struct {
Params struct {
// Working directory in the container. // Working directory in the container.
Dir string Dir string
// Initial process environment. // Initial process environment.
@ -101,9 +100,7 @@ type (
Ops []Op Ops []Op
Op interface { Op interface {
early(params *Params) error apply(params *InitParams) error
apply(params *Params) error
prefix() string
Is(op Op) bool Is(op Op) bool
fmt.Stringer fmt.Stringer
@ -144,12 +141,7 @@ func (p *Container) Start() error {
} }
p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
p.cmd.WaitDelay = p.WaitDelay p.cmd.Cancel, p.cmd.WaitDelay = p.Cancel, p.WaitDelay
if p.Cancel != nil {
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
} else {
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(syscall.SIGTERM) }
}
p.cmd.Dir = "/" p.cmd.Dir = "/"
p.cmd.SysProcAttr = &syscall.SysProcAttr{ p.cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: p.Flags&FAllowTTY == 0, Setsid: p.Flags&FAllowTTY == 0,
@ -191,11 +183,7 @@ func (p *Container) Serve() error {
panic("invalid serve") panic("invalid serve")
} }
setup := p.setup
p.setup = nil
if p.Path != "" && !path.IsAbs(p.Path) { if p.Path != "" && !path.IsAbs(p.Path) {
p.cancel()
return msg.WrapErr(syscall.EINVAL, return msg.WrapErr(syscall.EINVAL,
fmt.Sprintf("invalid executable path %q", p.Path)) fmt.Sprintf("invalid executable path %q", p.Path))
} }
@ -204,7 +192,6 @@ func (p *Container) Serve() error {
if p.name == "" { if p.name == "" {
p.Path = os.Getenv("SHELL") p.Path = os.Getenv("SHELL")
if !path.IsAbs(p.Path) { if !path.IsAbs(p.Path) {
p.cancel()
return msg.WrapErr(syscall.EBADE, return msg.WrapErr(syscall.EBADE,
"no command specified and $SHELL is invalid") "no command specified and $SHELL is invalid")
} }
@ -212,26 +199,23 @@ func (p *Container) Serve() error {
} else if path.IsAbs(p.name) { } else if path.IsAbs(p.name) {
p.Path = p.name p.Path = p.name
} else if v, err := exec.LookPath(p.name); err != nil { } else if v, err := exec.LookPath(p.name); err != nil {
p.cancel()
return msg.WrapErr(err, err.Error()) return msg.WrapErr(err, err.Error())
} else { } else {
p.Path = v p.Path = v
} }
} }
err := setup.Encode( setup := p.setup
p.setup = nil
return setup.Encode(
&initParams{ &initParams{
p.Params, p.InitParams,
syscall.Getuid(), syscall.Getuid(),
syscall.Getgid(), syscall.Getgid(),
len(p.ExtraFiles), len(p.ExtraFiles),
msg.IsVerbose(), msg.IsVerbose(),
}, },
) )
if err != nil {
p.cancel()
}
return err
} }
func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() } func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() }
@ -243,6 +227,6 @@ func (p *Container) String() string {
func New(ctx context.Context, name string, args ...string) *Container { func New(ctx context.Context, name string, args ...string) *Container {
return &Container{name: name, ctx: ctx, return &Container{name: name, ctx: ctx,
Params: Params{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)}, InitParams: InitParams{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)},
} }
} }

View File

@ -3,11 +3,10 @@ package sandbox_test
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/gob" "encoding/json"
"log" "log"
"os" "os"
"os/exec" "os/exec"
"strings"
"syscall" "syscall"
"testing" "testing"
"time" "time"
@ -18,12 +17,7 @@ import (
"git.gensokyo.uk/security/fortify/ldd" "git.gensokyo.uk/security/fortify/ldd"
"git.gensokyo.uk/security/fortify/sandbox" "git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/sandbox/seccomp" "git.gensokyo.uk/security/fortify/sandbox/seccomp"
"git.gensokyo.uk/security/fortify/sandbox/vfs" check "git.gensokyo.uk/security/fortify/test/sandbox"
)
const (
ignore = "\x00"
ignoreV = -1
) )
func TestContainer(t *testing.T) { func TestContainer(t *testing.T) {
@ -39,7 +33,7 @@ func TestContainer(t *testing.T) {
name string name string
flags sandbox.HardeningFlags flags sandbox.HardeningFlags
ops *sandbox.Ops ops *sandbox.Ops
mnt []*vfs.MountInfoEntry mnt []*check.Mntent
host string host string
}{ }{
{"minimal", 0, new(sandbox.Ops), nil, "test-minimal"}, {"minimal", 0, new(sandbox.Ops), nil, "test-minimal"},
@ -48,23 +42,21 @@ func TestContainer(t *testing.T) {
{"tmpfs", 0, {"tmpfs", 0,
new(sandbox.Ops). new(sandbox.Ops).
Tmpfs(fst.Tmp, 0, 0755), Tmpfs(fst.Tmp, 0, 0755),
[]*vfs.MountInfoEntry{ []*check.Mntent{
e("/", fst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), {FSName: "tmpfs", Dir: fst.Tmp, Type: "tmpfs", Opts: "\x00"},
}, "test-tmpfs"}, }, "test-tmpfs"},
{"dev", sandbox.FAllowTTY, // go test output is not a tty {"dev", sandbox.FAllowTTY, // go test output is not a tty
new(sandbox.Ops). new(sandbox.Ops).
Dev("/dev"). Dev("/dev"),
Mqueue("/dev/mqueue"), []*check.Mntent{
[]*vfs.MountInfoEntry{ {FSName: "devtmpfs", Dir: "/dev", Type: "tmpfs", Opts: "\x00"},
e("/", "/dev", "rw,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore), {FSName: "devtmpfs", Dir: "/dev/null", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
e("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), {FSName: "devtmpfs", Dir: "/dev/zero", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
e("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), {FSName: "devtmpfs", Dir: "/dev/full", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
e("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), {FSName: "devtmpfs", Dir: "/dev/random", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
e("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), {FSName: "devtmpfs", Dir: "/dev/urandom", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
e("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), {FSName: "devtmpfs", Dir: "/dev/tty", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
e("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), {FSName: "devpts", Dir: "/dev/pts", Type: "devpts", Opts: "rw,nosuid,noexec,relatime,mode=620,ptmxmode=666", Freq: 0, Passno: 0},
e("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
e("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
}, ""}, }, ""},
} }
@ -73,7 +65,7 @@ func TestContainer(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
container := sandbox.New(ctx, "/usr/bin/sandbox.test", "-test.v", container := sandbox.New(ctx, os.Args[0], "-test.v",
"-test.run=TestHelperCheckContainer", "--", "check", tc.host) "-test.run=TestHelperCheckContainer", "--", "check", tc.host)
container.Uid = 1000 container.Uid = 1000
container.Gid = 100 container.Gid = 100
@ -92,10 +84,7 @@ func TestContainer(t *testing.T) {
container. container.
Tmpfs("/tmp", 0, 0755). Tmpfs("/tmp", 0, 0755).
Bind(os.Args[0], os.Args[0], 0). Bind(os.Args[0], os.Args[0], 0)
Mkdir("/usr/bin", 0755).
Link(os.Args[0], "/usr/bin/sandbox.test").
Place("/etc/hostname", []byte(container.Args[5]))
// in case test has cgo enabled // in case test has cgo enabled
var libPaths []string var libPaths []string
if entries, err := ldd.ExecFilter(ctx, if entries, err := ldd.ExecFilter(ctx,
@ -110,26 +99,25 @@ func TestContainer(t *testing.T) {
for _, name := range libPaths { for _, name := range libPaths {
container.Bind(name, name, 0) container.Bind(name, name, 0)
} }
// needs /proc to check mountinfo
container.Proc("/proc")
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths)) mnt := make([]*check.Mntent, 0, 3+len(libPaths))
mnt = append(mnt, e("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore)) mnt = append(mnt, &check.Mntent{FSName: "rootfs", Dir: "/", Type: "tmpfs", Opts: "host_passthrough"})
mnt = append(mnt, tc.mnt...) mnt = append(mnt, tc.mnt...)
mnt = append(mnt, mnt = append(mnt,
e("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), &check.Mntent{FSName: "tmpfs", Dir: "/tmp", Type: "tmpfs", Opts: "host_passthrough"},
e(ignore, os.Args[0], "ro,nosuid,nodev,relatime", ignore, ignore, ignore), &check.Mntent{FSName: "\x00", Dir: os.Args[0], Type: "\x00", Opts: "\x00"})
e(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
)
for _, name := range libPaths { for _, name := range libPaths {
mnt = append(mnt, e(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore)) mnt = append(mnt, &check.Mntent{FSName: "\x00", Dir: name, Type: "\x00", Opts: "\x00", Freq: -1, Passno: -1})
} }
mnt = append(mnt, e("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw")) mnt = append(mnt, &check.Mntent{FSName: "proc", Dir: "/proc", Type: "proc", Opts: "rw,nosuid,nodev,noexec,relatime"})
want := new(bytes.Buffer) mntentWant := new(bytes.Buffer)
if err := gob.NewEncoder(want).Encode(mnt); err != nil { if err := json.NewEncoder(mntentWant).Encode(mnt); err != nil {
t.Fatalf("cannot serialise expected mount points: %v", err) t.Fatalf("cannot serialise mntent: %v", err)
} }
container.Stdin = want container.Stdin = mntentWant
// needs /proc to check mntent
container.Proc("/proc")
if err := container.Start(); err != nil { if err := container.Start(); err != nil {
fmsg.PrintBaseError(err, "start:") fmsg.PrintBaseError(err, "start:")
@ -146,21 +134,6 @@ func TestContainer(t *testing.T) {
} }
} }
func e(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry {
return &vfs.MountInfoEntry{
ID: ignoreV,
Parent: ignoreV,
Devno: vfs.DevT{ignoreV, ignoreV},
Root: root,
Target: target,
VfsOptstr: vfsOptstr,
OptFields: []string{ignore},
FsType: fsType,
Source: source,
FsOptstr: fsOptstr,
}
}
func TestContainerString(t *testing.T) { func TestContainerString(t *testing.T) {
container := sandbox.New(context.TODO(), "ldd", "/usr/bin/env") container := sandbox.New(context.TODO(), "ldd", "/usr/bin/env")
container.Flags |= sandbox.FAllowDevel container.Flags |= sandbox.FAllowDevel
@ -198,55 +171,9 @@ func TestHelperCheckContainer(t *testing.T) {
} else if name != os.Args[5] { } else if name != os.Args[5] {
t.Errorf("Hostname: %q, want %q", name, os.Args[5]) t.Errorf("Hostname: %q, want %q", name, os.Args[5])
} }
if p, err := os.ReadFile("/etc/hostname"); err != nil {
t.Fatalf("%v", err)
} else if string(p) != os.Args[5] {
t.Errorf("/etc/hostname: %q, want %q", string(p), os.Args[5])
}
})
t.Run("mount", func(t *testing.T) {
var mnt []*vfs.MountInfoEntry
if err := gob.NewDecoder(os.Stdin).Decode(&mnt); err != nil {
t.Fatalf("cannot receive expected mount points: %v", err)
}
var d *vfs.MountInfoDecoder
if f, err := os.Open("/proc/self/mountinfo"); err != nil {
t.Fatalf("cannot open mountinfo: %v", err)
} else {
d = vfs.NewMountInfoDecoder(f)
}
i := 0
for cur := range d.Entries() {
if i == len(mnt) {
t.Errorf("got more than %d entries", len(mnt))
break
}
// ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime")
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime")
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime")
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime")
if !cur.EqualWithIgnore(mnt[i], "\x00") {
t.Errorf("[FAIL] %s", cur)
} else {
t.Logf("[ OK ] %s", cur)
}
i++
}
if err := d.Err(); err != nil {
t.Errorf("cannot parse mountinfo: %v", err)
}
if i != len(mnt) {
t.Errorf("got %d entries, want %d", i, len(mnt))
}
}) })
t.Run("seccomp", func(t *testing.T) { check.MustAssertSeccomp() })
t.Run("mntent", func(t *testing.T) { check.MustAssertMounts("", "/proc/mounts", "/proc/self/fd/0") })
} }
func commandContext(ctx context.Context) *exec.Cmd { func commandContext(ctx context.Context) *exec.Cmd {

View File

@ -28,7 +28,7 @@ const (
) )
type initParams struct { type initParams struct {
Params InitParams
HostUid, HostGid int HostUid, HostGid int
// extra files count // extra files count
@ -98,7 +98,6 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err) log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
} }
oldmask := syscall.Umask(0)
if params.Hostname != "" { if params.Hostname != "" {
if err := syscall.Sethostname([]byte(params.Hostname)); err != nil { if err := syscall.Sethostname([]byte(params.Hostname)); err != nil {
log.Fatalf("cannot set hostname: %v", err) log.Fatalf("cannot set hostname: %v", err)
@ -115,19 +114,6 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
log.Fatalf("cannot make / rslave: %v", err) log.Fatalf("cannot make / rslave: %v", err)
} }
for i, op := range *params.Ops {
if op == nil {
log.Fatalf("invalid op %d", i)
}
if err := op.early(&params.Params); err != nil {
msg.PrintBaseErr(err,
fmt.Sprintf("cannot prepare op %d:", i))
msg.BeforeExit()
os.Exit(1)
}
}
if err := syscall.Mount("rootfs", basePath, "tmpfs", if err := syscall.Mount("rootfs", basePath, "tmpfs",
syscall.MS_NODEV|syscall.MS_NOSUID, syscall.MS_NODEV|syscall.MS_NOSUID,
""); err != nil { ""); err != nil {
@ -157,9 +143,8 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
} }
for i, op := range *params.Ops { for i, op := range *params.Ops {
// ops already checked during early setup msg.Verbosef("mounting %s", op)
msg.Verbosef("%s %s", op.prefix(), op) if err := op.apply(&params.InitParams); err != nil {
if err := op.apply(&params.Params); err != nil {
msg.PrintBaseErr(err, msg.PrintBaseErr(err,
fmt.Sprintf("cannot apply op %d:", i)) fmt.Sprintf("cannot apply op %d:", i))
msg.BeforeExit() msg.BeforeExit()
@ -231,7 +216,6 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
for i := range extraFiles { for i := range extraFiles {
extraFiles[i] = os.NewFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i)) extraFiles[i] = os.NewFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
} }
syscall.Umask(oldmask)
/* /*
prepare initial process prepare initial process
@ -239,6 +223,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
cmd := exec.Command(params.Path) cmd := exec.Command(params.Path)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
cmd.Args = params.Args cmd.Args = params.Args
cmd.Env = params.Env cmd.Env = params.Env
cmd.ExtraFiles = extraFiles cmd.ExtraFiles = extraFiles
@ -323,13 +308,10 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
switch { switch {
case w.wstatus.Exited(): case w.wstatus.Exited():
r = w.wstatus.ExitStatus() r = w.wstatus.ExitStatus()
msg.Verbosef("initial process exited with code %d", w.wstatus.ExitStatus())
case w.wstatus.Signaled(): case w.wstatus.Signaled():
r = 128 + int(w.wstatus.Signal()) r = 128 + int(w.wstatus.Signal())
msg.Verbosef("initial process exited with signal %s", w.wstatus.Signal())
default: default:
r = 255 r = 255
msg.Verbosef("initial process exited with status %#x", w.wstatus)
} }
go func() { go func() {

View File

@ -4,105 +4,86 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "strings"
"syscall" "syscall"
"git.gensokyo.uk/security/fortify/sandbox/vfs"
) )
func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error { const (
if eq { BindOptional = 1 << iota
msg.Verbosef("resolved %q flags %#x", target, flags) BindSource
BindRecursive
BindWritable
BindDevices
)
func bindMount(src, dest string, flags int) error {
target := toSysroot(dest)
var source string
if flags&BindSource == 0 {
// this is what bwrap does, so the behaviour is kept for now,
// however recursively resolving links might improve user experience
if rp, err := realpathHost(src); err != nil {
if os.IsNotExist(err) {
if flags&BindOptional != 0 {
return nil
} else { } else {
msg.Verbosef("resolved %q on %q flags %#x", source, target, flags)
}
if err := syscall.Mount(source, target, "",
syscall.MS_SILENT|syscall.MS_BIND|flags&syscall.MS_REC, ""); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot mount %q on %q:", source, target))
}
var targetFinal string
if v, err := filepath.EvalSymlinks(target); err != nil {
return msg.WrapErr(err, err.Error())
} else {
targetFinal = v
if targetFinal != target {
msg.Verbosef("target resolves to %q", targetFinal)
}
}
// final target path according to the kernel through proc
var targetKFinal string
{
var destFd int
if err := IgnoringEINTR(func() (err error) {
destFd, err = syscall.Open(targetFinal, O_PATH|syscall.O_CLOEXEC, 0)
return
}); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot open %q:", targetFinal))
}
if v, err := os.Readlink(p.fd(destFd)); err != nil {
return msg.WrapErr(err, err.Error())
} else if err = syscall.Close(destFd); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot close %q:", targetFinal))
} else {
targetKFinal = v
}
}
mf := syscall.MS_NOSUID | flags&syscall.MS_NODEV | flags&syscall.MS_RDONLY
return hostProc.mountinfo(func(d *vfs.MountInfoDecoder) error {
n, err := d.Unfold(targetKFinal)
if err != nil {
if errors.Is(err, syscall.ESTALE) {
return msg.WrapErr(err, return msg.WrapErr(err,
fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal)) fmt.Sprintf("path %q does not exist", src))
}
}
return msg.WrapErr(err, err.Error())
} else {
source = toHost(rp)
}
} else if flags&BindOptional != 0 {
return msg.WrapErr(syscall.EINVAL,
"flag source excludes optional")
} else {
source = toHost(src)
}
if fi, err := os.Stat(source); err != nil {
return msg.WrapErr(err, err.Error())
} else if fi.IsDir() {
if err = os.MkdirAll(target, 0755); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot create directory %q:", dest))
}
} else if err = ensureFile(target, 0444); err != nil {
if errors.Is(err, syscall.EISDIR) {
return msg.WrapErr(err,
fmt.Sprintf("path %q is a directory", dest))
} }
return wrapErrSuffix(err, return wrapErrSuffix(err,
"cannot unfold mount hierarchy:") fmt.Sprintf("cannot create %q:", dest))
} }
if err = remountWithFlags(n, mf); err != nil { var mf uintptr = syscall.MS_SILENT | syscall.MS_BIND
return err if flags&BindRecursive != 0 {
mf |= syscall.MS_REC
} }
if flags&syscall.MS_REC == 0 { if flags&BindWritable == 0 {
return nil mf |= syscall.MS_RDONLY
} }
if flags&BindDevices == 0 {
for cur := range n.Collective() { mf |= syscall.MS_NODEV
err = remountWithFlags(cur, mf) }
if err != nil && !errors.Is(err, syscall.EACCES) { if msg.IsVerbose() {
return err if strings.TrimPrefix(source, hostPath) == strings.TrimPrefix(target, sysrootPath) {
msg.Verbosef("resolved %q flags %#x", target, mf)
} else {
msg.Verbosef("resolved %q on %q flags %#x", source, target, mf)
} }
} }
return wrapErrSuffix(syscall.Mount(source, target, "", mf, ""),
return nil fmt.Sprintf("cannot bind %q on %q:", src, dest))
})
}
func remountWithFlags(n *vfs.MountInfoNode, mf uintptr) error {
kf, unmatched := n.Flags()
if len(unmatched) != 0 {
msg.Verbosef("unmatched vfs options: %q", unmatched)
}
if kf&mf != mf {
return wrapErrSuffix(syscall.Mount("none", n.Clean, "",
syscall.MS_SILENT|syscall.MS_BIND|syscall.MS_REMOUNT|kf|mf,
""),
fmt.Sprintf("cannot remount %q:", n.Clean))
}
return nil
} }
func mountTmpfs(fsname, name string, size int, perm os.FileMode) error { func mountTmpfs(fsname, name string, size int, perm os.FileMode) error {
target := toSysroot(name) target := toSysroot(name)
if err := os.MkdirAll(target, parentPerm(perm)); err != nil { if err := os.MkdirAll(target, perm); err != nil {
return msg.WrapErr(err, err.Error()) return err
} }
opt := fmt.Sprintf("mode=%#o", perm) opt := fmt.Sprintf("mode=%#o", perm)
if size > 0 { if size > 0 {
@ -112,14 +93,3 @@ func mountTmpfs(fsname, name string, size int, perm os.FileMode) error {
syscall.MS_NOSUID|syscall.MS_NODEV, opt), syscall.MS_NOSUID|syscall.MS_NODEV, opt),
fmt.Sprintf("cannot mount tmpfs on %q:", name)) fmt.Sprintf("cannot mount tmpfs on %q:", name))
} }
func parentPerm(perm os.FileMode) os.FileMode {
pperm := 0755
if perm&0070 == 0 {
pperm &= ^0050
}
if perm&0007 == 0 {
pperm &= ^0005
}
return os.FileMode(pperm)
}

View File

@ -2,15 +2,11 @@ package sandbox
import ( import (
"errors" "errors"
"fmt"
"io/fs" "io/fs"
"os" "os"
"path" "path"
"strconv"
"strings" "strings"
"syscall" "syscall"
"git.gensokyo.uk/security/fortify/sandbox/vfs"
) )
const ( const (
@ -30,65 +26,50 @@ func toHost(name string) string {
return path.Join(hostPath, name) return path.Join(hostPath, name)
} }
func createFile(name string, perm, pperm os.FileMode, content []byte) error { func realpathHost(name string) (string, error) {
if err := os.MkdirAll(path.Dir(name), pperm); err != nil { source := toHost(name)
return msg.WrapErr(err, err.Error()) rp, err := os.Readlink(source)
if err != nil {
if errors.Is(err, syscall.EINVAL) {
// not a symlink
return name, nil
}
return "", err
}
if !path.IsAbs(rp) {
return name, nil
}
msg.Verbosef("path %q resolves to %q", name, rp)
return rp, nil
}
func createFile(name string, perm os.FileMode, content []byte) error {
if err := os.MkdirAll(path.Dir(name), 0755); err != nil {
return err
} }
f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm) f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm)
if err != nil { if err != nil {
return msg.WrapErr(err, err.Error()) return err
} }
if content != nil { if content != nil {
_, err = f.Write(content) _, err = f.Write(content)
if err != nil {
err = msg.WrapErr(err, err.Error())
}
} }
return errors.Join(f.Close(), err) return errors.Join(f.Close(), err)
} }
func ensureFile(name string, perm, pperm os.FileMode) error { func ensureFile(name string, perm os.FileMode) error {
fi, err := os.Stat(name) fi, err := os.Stat(name)
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
return err return err
} }
return createFile(name, perm, pperm, nil) return createFile(name, perm, nil)
} }
if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 { if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 {
err = msg.WrapErr(syscall.EISDIR, err = syscall.EISDIR
fmt.Sprintf("path %q is a directory", name))
} }
return err return err
} }
var hostProc = newProcPats(hostPath)
func newProcPats(prefix string) *procPaths {
return &procPaths{prefix + "/proc", prefix + "/proc/self"}
}
type procPaths struct {
prefix string
self string
}
func (p *procPaths) stdout() string { return p.self + "/fd/1" }
func (p *procPaths) fd(fd int) string { return p.self + "/fd/" + strconv.Itoa(fd) }
func (p *procPaths) mountinfo(f func(d *vfs.MountInfoDecoder) error) error {
if r, err := os.Open(p.self + "/mountinfo"); err != nil {
return msg.WrapErr(err, err.Error())
} else {
d := vfs.NewMountInfoDecoder(r)
err0 := f(d)
if err = r.Close(); err != nil {
return wrapErrSuffix(err,
"cannot close mountinfo:")
} else if err = d.Err(); err != nil {
return wrapErrSuffix(err,
"cannot parse mountinfo:")
}
return err0
}
}

View File

@ -93,7 +93,7 @@ func TestExport(t *testing.T) {
t.Errorf("Close: error = %v", err) t.Errorf("Close: error = %v", err)
return return
} }
if got := digest.Sum(nil); !slices.Equal(got, tc.want) { if got := digest.Sum(nil); slices.Compare(got, tc.want) != 0 {
t.Fatalf("Export() hash = %x, want %x", t.Fatalf("Export() hash = %x, want %x",
got, tc.want) got, tc.want)
return return
@ -111,14 +111,11 @@ func TestExport(t *testing.T) {
t.Run("close partial read", func(t *testing.T) { t.Run("close partial read", func(t *testing.T) {
e := seccomp.New(0) e := seccomp.New(0)
if _, err := e.Read(nil); err != nil { if _, err := e.Read(make([]byte, 0)); err != nil {
t.Errorf("Read: error = %v", err) t.Errorf("Read: error = %v", err)
return return
} }
// the underlying implementation uses buffered io, so the outcome of this is nondeterministic; if err := e.Close(); err == nil || !errors.Is(err, syscall.ECANCELED) || !errors.Is(err, syscall.EBADF) {
// that is not harmful however, so both outcomes are checked for here
if err := e.Close(); err != nil &&
(!errors.Is(err, syscall.ECANCELED) || !errors.Is(err, syscall.EBADF)) {
t.Errorf("Close: error = %v", err) t.Errorf("Close: error = %v", err)
return return
} }

View File

@ -6,8 +6,6 @@ import (
"math" "math"
"os" "os"
"path" "path"
"path/filepath"
"slices"
"syscall" "syscall"
"unsafe" "unsafe"
) )
@ -16,77 +14,20 @@ func init() { gob.Register(new(BindMount)) }
// BindMount bind mounts host path Source on container path Target. // BindMount bind mounts host path Source on container path Target.
type BindMount struct { type BindMount struct {
Source, SourceFinal, Target string Source, Target string
Flags int Flags int
} }
const ( func (b *BindMount) apply(*InitParams) error {
BindOptional = 1 << iota if !path.IsAbs(b.Source) || !path.IsAbs(b.Target) {
BindWritable
BindDevice
)
func (b *BindMount) early(*Params) error {
if !path.IsAbs(b.Source) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", b.Source))
}
if v, err := filepath.EvalSymlinks(b.Source); err != nil {
if os.IsNotExist(err) && b.Flags&BindOptional != 0 {
b.SourceFinal = "\x00"
return nil
}
return msg.WrapErr(err, err.Error())
} else {
b.SourceFinal = v
return nil
}
}
func (b *BindMount) apply(*Params) error {
if b.SourceFinal == "\x00" {
if b.Flags&BindOptional == 0 {
// unreachable
return syscall.EBADE
}
return nil
}
if !path.IsAbs(b.SourceFinal) || !path.IsAbs(b.Target) {
return msg.WrapErr(syscall.EBADE, return msg.WrapErr(syscall.EBADE,
"path is not absolute") "path is not absolute")
} }
return bindMount(b.Source, b.Target, b.Flags)
source := toHost(b.SourceFinal)
target := toSysroot(b.Target)
// this perm value emulates bwrap behaviour as it clears bits from 0755 based on
// op->perms which is never set for any bind setup op so always results in 0700
if fi, err := os.Stat(source); err != nil {
return msg.WrapErr(err, err.Error())
} else if fi.IsDir() {
if err = os.MkdirAll(target, 0700); err != nil {
return msg.WrapErr(err, err.Error())
}
} else if err = ensureFile(target, 0444, 0700); err != nil {
return err
}
var flags uintptr = syscall.MS_REC
if b.Flags&BindWritable == 0 {
flags |= syscall.MS_RDONLY
}
if b.Flags&BindDevice == 0 {
flags |= syscall.MS_NODEV
}
return hostProc.bindMount(source, target, flags, b.SourceFinal == b.Target)
} }
func (b *BindMount) Is(op Op) bool { vb, ok := op.(*BindMount); return ok && *b == *vb } func (b *BindMount) Is(op Op) bool { vb, ok := op.(*BindMount); return ok && *b == *vb }
func (*BindMount) prefix() string { return "mounting" }
func (b *BindMount) String() string { func (b *BindMount) String() string {
if b.Source == b.Target { if b.Source == b.Target {
return fmt.Sprintf("%q flags %#x", b.Source, b.Flags) return fmt.Sprintf("%q flags %#x", b.Source, b.Flags)
@ -94,70 +35,54 @@ func (b *BindMount) String() string {
return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags&BindWritable) return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags&BindWritable)
} }
func (f *Ops) Bind(source, target string, flags int) *Ops { func (f *Ops) Bind(source, target string, flags int) *Ops {
*f = append(*f, &BindMount{source, "", target, flags}) *f = append(*f, &BindMount{source, target, flags | BindRecursive})
return f return f
} }
func init() { gob.Register(new(MountProc)) } func init() { gob.Register(new(MountProc)) }
// MountProc mounts a private instance of proc. // MountProc mounts a private proc instance on container Path.
type MountProc string type MountProc struct {
Path string
func (p MountProc) early(*Params) error { return nil }
func (p MountProc) apply(*Params) error {
v := string(p)
if !path.IsAbs(v) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", v))
} }
target := toSysroot(v) func (p *MountProc) apply(*InitParams) error {
if !path.IsAbs(p.Path) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", p.Path))
}
target := toSysroot(p.Path)
if err := os.MkdirAll(target, 0755); err != nil { if err := os.MkdirAll(target, 0755); err != nil {
return msg.WrapErr(err, err.Error()) return msg.WrapErr(err, err.Error())
} }
return wrapErrSuffix(syscall.Mount("proc", target, "proc", return wrapErrSuffix(syscall.Mount("proc", target, "proc",
syscall.MS_NOSUID|syscall.MS_NOEXEC|syscall.MS_NODEV, ""), syscall.MS_NOSUID|syscall.MS_NOEXEC|syscall.MS_NODEV, ""),
fmt.Sprintf("cannot mount proc on %q:", v)) fmt.Sprintf("cannot mount proc on %q:", p.Path))
}
func (p MountProc) Is(op Op) bool { vp, ok := op.(MountProc); return ok && p == vp }
func (MountProc) prefix() string { return "mounting" }
func (p MountProc) String() string { return fmt.Sprintf("proc on %q", string(p)) }
func (f *Ops) Proc(dest string) *Ops {
*f = append(*f, MountProc(dest))
return f
} }
func init() { gob.Register(new(MountDev)) } func init() { gob.Register(new(MountDev)) }
// MountDev mounts part of host dev. // MountDev mounts dev on container Path.
type MountDev string type MountDev struct {
Path string
func (d MountDev) early(*Params) error { return nil }
func (d MountDev) apply(params *Params) error {
v := string(d)
if !path.IsAbs(v) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", v))
} }
target := toSysroot(v)
if err := mountTmpfs("devtmpfs", v, 0, 0755); err != nil { func (d *MountDev) apply(params *InitParams) error {
if !path.IsAbs(d.Path) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", d.Path))
}
target := toSysroot(d.Path)
if err := mountTmpfs("devtmpfs", d.Path, 0, 0755); err != nil {
return err return err
} }
for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} { for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
targetPath := toSysroot(path.Join(v, name)) if err := bindMount(
if err := ensureFile(targetPath, 0444, 0755); err != nil { "/dev/"+name, path.Join(d.Path, name),
return err BindSource|BindDevices,
}
if err := hostProc.bindMount(
toHost("/dev/"+name),
targetPath,
0,
true,
); err != nil { ); err != nil {
return err return err
} }
@ -200,17 +125,9 @@ func (d MountDev) apply(params *Params) error {
syscall.SYS_IOCTL, 1, syscall.TIOCGWINSZ, syscall.SYS_IOCTL, 1, syscall.TIOCGWINSZ,
uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&buf[0])),
); errno == 0 { ); errno == 0 {
consolePath := toSysroot(path.Join(v, "console")) if err := bindMount(
if err := ensureFile(consolePath, 0444, 0755); err != nil { "/proc/self/fd/1", path.Join(d.Path, "console"),
return err BindDevices,
}
if name, err := os.Readlink(hostProc.stdout()); err != nil {
return msg.WrapErr(err, err.Error())
} else if err = hostProc.bindMount(
toHost(name),
consolePath,
0,
false,
); err != nil { ); err != nil {
return err return err
} }
@ -220,42 +137,17 @@ func (d MountDev) apply(params *Params) error {
return nil return nil
} }
func (d MountDev) Is(op Op) bool { vd, ok := op.(MountDev); return ok && d == vd } func (d *MountDev) Is(op Op) bool { vd, ok := op.(*MountDev); return ok && *d == *vd }
func (MountDev) prefix() string { return "mounting" } func (d *MountDev) String() string { return fmt.Sprintf("dev on %q", d.Path) }
func (d MountDev) String() string { return fmt.Sprintf("dev on %q", string(d)) }
func (f *Ops) Dev(dest string) *Ops { func (f *Ops) Dev(dest string) *Ops {
*f = append(*f, MountDev(dest)) *f = append(*f, &MountDev{dest})
return f return f
} }
func init() { gob.Register(new(MountMqueue)) } func (p *MountProc) Is(op Op) bool { vp, ok := op.(*MountProc); return ok && *p == *vp }
func (p *MountProc) String() string { return fmt.Sprintf("proc on %q", p.Path) }
// MountMqueue mounts a private mqueue instance on container Path. func (f *Ops) Proc(dest string) *Ops {
type MountMqueue string *f = append(*f, &MountProc{dest})
func (m MountMqueue) early(*Params) error { return nil }
func (m MountMqueue) apply(*Params) error {
v := string(m)
if !path.IsAbs(v) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", v))
}
target := toSysroot(v)
if err := os.MkdirAll(target, 0755); err != nil {
return msg.WrapErr(err, err.Error())
}
return wrapErrSuffix(syscall.Mount("mqueue", target, "mqueue",
syscall.MS_NOSUID|syscall.MS_NOEXEC|syscall.MS_NODEV, ""),
fmt.Sprintf("cannot mount mqueue on %q:", v))
}
func (m MountMqueue) Is(op Op) bool { vm, ok := op.(MountMqueue); return ok && m == vm }
func (MountMqueue) prefix() string { return "mounting" }
func (m MountMqueue) String() string { return fmt.Sprintf("mqueue on %q", string(m)) }
func (f *Ops) Mqueue(dest string) *Ops {
*f = append(*f, MountMqueue(dest))
return f return f
} }
@ -268,8 +160,7 @@ type MountTmpfs struct {
Perm os.FileMode Perm os.FileMode
} }
func (t *MountTmpfs) early(*Params) error { return nil } func (t *MountTmpfs) apply(*InitParams) error {
func (t *MountTmpfs) apply(*Params) error {
if !path.IsAbs(t.Path) { if !path.IsAbs(t.Path) {
return msg.WrapErr(syscall.EBADE, return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", t.Path)) fmt.Sprintf("path %q is not absolute", t.Path))
@ -282,133 +173,8 @@ func (t *MountTmpfs) apply(*Params) error {
} }
func (t *MountTmpfs) Is(op Op) bool { vt, ok := op.(*MountTmpfs); return ok && *t == *vt } func (t *MountTmpfs) Is(op Op) bool { vt, ok := op.(*MountTmpfs); return ok && *t == *vt }
func (*MountTmpfs) prefix() string { return "mounting" }
func (t *MountTmpfs) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) } func (t *MountTmpfs) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops { func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops {
*f = append(*f, &MountTmpfs{dest, size, perm}) *f = append(*f, &MountTmpfs{dest, size, perm})
return f return f
} }
func init() { gob.Register(new(Symlink)) }
// Symlink creates a symlink in the container filesystem.
type Symlink [2]string
func (l *Symlink) early(*Params) error { return nil }
func (l *Symlink) apply(*Params) error {
// symlink target is an arbitrary path value, so only validate link name here
if !path.IsAbs(l[1]) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", l[1]))
}
target := toSysroot(l[1])
if err := ensureFile(target, 0444, 0755); err != nil {
return err
}
if err := os.Remove(target); err != nil {
return msg.WrapErr(err, err.Error())
}
if err := os.Symlink(l[0], target); err != nil {
return msg.WrapErr(err, err.Error())
}
return nil
}
func (l *Symlink) Is(op Op) bool { vl, ok := op.(*Symlink); return ok && *l == *vl }
func (*Symlink) prefix() string { return "creating" }
func (l *Symlink) String() string { return fmt.Sprintf("symlink on %q target %q", l[1], l[0]) }
func (f *Ops) Link(target, linkName string) *Ops {
*f = append(*f, &Symlink{target, linkName})
return f
}
func init() { gob.Register(new(Mkdir)) }
// Mkdir creates a directory in the container filesystem.
type Mkdir struct {
Path string
Perm os.FileMode
}
func (m *Mkdir) early(*Params) error { return nil }
func (m *Mkdir) apply(*Params) error {
if !path.IsAbs(m.Path) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", m.Path))
}
if err := os.MkdirAll(toSysroot(m.Path), m.Perm); err != nil {
return msg.WrapErr(err, err.Error())
}
return nil
}
func (m *Mkdir) Is(op Op) bool { vm, ok := op.(*Mkdir); return ok && m == vm }
func (*Mkdir) prefix() string { return "creating" }
func (m *Mkdir) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }
func (f *Ops) Mkdir(dest string, perm os.FileMode) *Ops {
*f = append(*f, &Mkdir{dest, perm})
return f
}
func init() { gob.Register(new(Tmpfile)) }
// Tmpfile places a file in container Path containing Data.
type Tmpfile struct {
Path string
Data []byte
}
func (t *Tmpfile) early(*Params) error { return nil }
func (t *Tmpfile) apply(*Params) error {
if !path.IsAbs(t.Path) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", t.Path))
}
var tmpPath string
if f, err := os.CreateTemp("/", "tmp.*"); err != nil {
return msg.WrapErr(err, err.Error())
} else if _, err = f.Write(t.Data); err != nil {
return wrapErrSuffix(err,
"cannot write to intermediate file:")
} else if err = f.Close(); err != nil {
return wrapErrSuffix(err,
"cannot close intermediate file:")
} else {
tmpPath = f.Name()
}
target := toSysroot(t.Path)
if err := ensureFile(target, 0444, 0755); err != nil {
return err
} else if err = hostProc.bindMount(
tmpPath,
target,
syscall.MS_RDONLY|syscall.MS_NODEV,
false,
); err != nil {
return err
} else if err = os.Remove(tmpPath); err != nil {
return msg.WrapErr(err, err.Error())
}
return nil
}
func (t *Tmpfile) Is(op Op) bool {
vt, ok := op.(*Tmpfile)
return ok && t.Path == vt.Path && slices.Equal(t.Data, vt.Data)
}
func (*Tmpfile) prefix() string { return "placing" }
func (t *Tmpfile) String() string {
return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data))
}
func (f *Ops) Place(name string, data []byte) *Ops { *f = append(*f, &Tmpfile{name, data}); return f }
func (f *Ops) PlaceP(name string, dataP **[]byte) *Ops {
t := &Tmpfile{Path: name}
*dataP = &t.Data
*f = append(*f, t)
return f
}

View File

@ -2,12 +2,6 @@ package sandbox
import "syscall" import "syscall"
const (
O_PATH = 0x200000
PR_SET_NO_NEW_PRIVS = 0x26
CAP_SYS_ADMIN = 0x15
)
const ( const (
SUID_DUMP_DISABLE = iota SUID_DUMP_DISABLE = iota
SUID_DUMP_USER SUID_DUMP_USER
@ -22,6 +16,14 @@ func SetDumpable(dumpable uintptr) error {
return nil return nil
} }
func SetPdeathsig(sig syscall.Signal) error {
if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(sig), 0); errno != 0 {
return errno
}
return nil
}
// IgnoringEINTR makes a function call and repeats it if it returns an // IgnoringEINTR makes a function call and repeats it if it returns an
// EINTR error. This appears to be required even though we install all // EINTR error. This appears to be required even though we install all
// signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846. // signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846.

View File

@ -1,30 +0,0 @@
package vfs
import "strings"
func Unmangle(s string) string {
if !strings.ContainsRune(s, '\\') {
return s
}
v := make([]byte, len(s))
var (
j int
c byte
)
for i := 0; i < len(s); i++ {
c = s[i]
if c == '\\' && len(s) > i+3 &&
(s[i+1] == '0' || s[i+1] == '1') &&
(s[i+2] >= '0' && s[i+2] <= '7') &&
(s[i+3] >= '0' && s[i+3] <= '7') {
c = ((s[i+1] - '0') << 6) |
((s[i+2] - '0') << 3) |
(s[i+3] - '0')
i += 3
}
v[j] = c
j++
}
return string(v[:j])
}

View File

@ -1,27 +0,0 @@
package vfs_test
import (
"testing"
"git.gensokyo.uk/security/fortify/sandbox/vfs"
)
func TestUnmangle(t *testing.T) {
testCases := []struct {
want string
sample string
}{
{`\, `, `\134\054\040`},
{`(10) source -- maybe empty string`, `(10)\040source\040--\040maybe empty string`},
}
for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) {
got := vfs.Unmangle(tc.sample)
if got != tc.want {
t.Errorf("Unmangle: %q, want %q",
got, tc.want)
}
})
}
}

View File

@ -1,260 +0,0 @@
// Package vfs provides bindings and iterators over proc_pid_mountinfo(5).
package vfs
import (
"bufio"
"errors"
"fmt"
"io"
"iter"
"slices"
"strconv"
"strings"
"syscall"
)
const (
MS_NOSYMFOLLOW = 0x100
)
var (
ErrMountInfoFields = errors.New("unexpected field count")
ErrMountInfoEmpty = errors.New("unexpected empty field")
ErrMountInfoDevno = errors.New("bad maj:min field")
ErrMountInfoSep = errors.New("bad optional fields separator")
)
type (
// A MountInfoDecoder reads and decodes proc_pid_mountinfo(5) entries from an input stream.
MountInfoDecoder struct {
s *bufio.Scanner
m *MountInfo
current *MountInfo
parseErr error
complete bool
}
// MountInfo represents the contents of a proc_pid_mountinfo(5) document.
MountInfo struct {
Next *MountInfo
MountInfoEntry
}
// MountInfoEntry represents a proc_pid_mountinfo(5) entry.
MountInfoEntry struct {
// mount ID: a unique ID for the mount (may be reused after umount(2)).
ID int `json:"id"`
// parent ID: the ID of the parent mount (or of self for the root of this mount namespace's mount tree).
Parent int `json:"parent"`
// major:minor: the value of st_dev for files on this filesystem (see stat(2)).
Devno DevT `json:"devno"`
// root: the pathname of the directory in the filesystem which forms the root of this mount.
Root string `json:"root"`
// mount point: the pathname of the mount point relative to the process's root directory.
Target string `json:"target"`
// mount options: per-mount options (see mount(2)).
VfsOptstr string `json:"vfs_optstr"`
// optional fields: zero or more fields of the form "tag[:value]"; see below.
// separator: the end of the optional fields is marked by a single hyphen.
OptFields []string `json:"opt_fields"`
// filesystem type: the filesystem type in the form "type[.subtype]".
FsType string `json:"fstype"`
// mount source: filesystem-specific information or "none".
Source string `json:"source"`
// super options: per-superblock options (see mount(2)).
FsOptstr string `json:"fs_optstr"`
}
DevT [2]int
)
// Flags interprets VfsOptstr and returns the resulting flags and unmatched options.
func (e *MountInfoEntry) Flags() (flags uintptr, unmatched []string) {
for _, s := range strings.Split(e.VfsOptstr, ",") {
switch s {
case "rw":
case "ro":
flags |= syscall.MS_RDONLY
case "nosuid":
flags |= syscall.MS_NOSUID
case "nodev":
flags |= syscall.MS_NODEV
case "noexec":
flags |= syscall.MS_NOEXEC
case "nosymfollow":
flags |= MS_NOSYMFOLLOW
case "noatime":
flags |= syscall.MS_NOATIME
case "nodiratime":
flags |= syscall.MS_NODIRATIME
case "relatime":
flags |= syscall.MS_RELATIME
default:
unmatched = append(unmatched, s)
}
}
return
}
// NewMountInfoDecoder returns a new decoder that reads from r.
//
// The decoder introduces its own buffering and may read data from r beyond the mountinfo entries requested.
func NewMountInfoDecoder(r io.Reader) *MountInfoDecoder {
return &MountInfoDecoder{s: bufio.NewScanner(r)}
}
func (d *MountInfoDecoder) Decode(v **MountInfo) (err error) {
for d.scan() {
}
err = d.Err()
if err == nil {
*v = d.m
}
return
}
// Entries returns an iterator over mountinfo entries.
func (d *MountInfoDecoder) Entries() iter.Seq[*MountInfoEntry] {
return func(yield func(*MountInfoEntry) bool) {
for cur := d.m; cur != nil; cur = cur.Next {
if !yield(&cur.MountInfoEntry) {
return
}
}
for d.scan() {
if !yield(&d.current.MountInfoEntry) {
return
}
}
}
}
func (d *MountInfoDecoder) Err() error {
if err := d.s.Err(); err != nil {
return err
}
return d.parseErr
}
func (d *MountInfoDecoder) scan() bool {
if d.complete {
return false
}
if !d.s.Scan() {
d.complete = true
return false
}
m := new(MountInfo)
if err := parseMountInfoLine(d.s.Text(), &m.MountInfoEntry); err != nil {
d.parseErr = err
d.complete = true
return false
}
if d.current == nil {
d.m = m
d.current = d.m
} else {
d.current.Next = m
d.current = d.current.Next
}
return true
}
func parseMountInfoLine(s string, ent *MountInfoEntry) error {
// prevent proceeding with misaligned fields due to optional fields
f := strings.Split(s, " ")
if len(f) < 10 {
return ErrMountInfoFields
}
// 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
// (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11)
// (1) id
if id, err := strconv.Atoi(f[0]); err != nil { // 0
return err
} else {
ent.ID = id
}
// (2) parent
if parent, err := strconv.Atoi(f[1]); err != nil { // 1
return err
} else {
ent.Parent = parent
}
// (3) maj:min
if n, err := fmt.Sscanf(f[2], "%d:%d", &ent.Devno[0], &ent.Devno[1]); err != nil {
return err
} else if n != 2 {
// unreachable
return ErrMountInfoDevno
}
// (4) mountroot
ent.Root = Unmangle(f[3])
if ent.Root == "" {
return ErrMountInfoEmpty
}
// (5) target
ent.Target = Unmangle(f[4])
if ent.Target == "" {
return ErrMountInfoEmpty
}
// (6) vfs options (fs-independent)
ent.VfsOptstr = Unmangle(f[5])
if ent.VfsOptstr == "" {
return ErrMountInfoEmpty
}
// (7) optional fields, terminated by " - "
i := len(f) - 4
ent.OptFields = f[6:i]
// (8) optional fields end marker
if f[i] != "-" {
return ErrMountInfoSep
}
i++
// (9) FS type
ent.FsType = Unmangle(f[i])
if ent.FsType == "" {
return ErrMountInfoEmpty
}
i++
// (10) source -- maybe empty string
ent.Source = Unmangle(f[i])
i++
// (11) fs options (fs specific)
ent.FsOptstr = Unmangle(f[i])
return nil
}
func (e *MountInfoEntry) EqualWithIgnore(want *MountInfoEntry, ignore string) bool {
return (e.ID == want.ID || want.ID == -1) &&
(e.Parent == want.Parent || want.Parent == -1) &&
(e.Devno == want.Devno || (want.Devno[0] == -1 && want.Devno[1] == -1)) &&
(e.Root == want.Root || want.Root == ignore) &&
(e.Target == want.Target || want.Target == ignore) &&
(e.VfsOptstr == want.VfsOptstr || want.VfsOptstr == ignore) &&
(slices.Equal(e.OptFields, want.OptFields) || (len(want.OptFields) == 1 && want.OptFields[0] == ignore)) &&
(e.FsType == want.FsType || want.FsType == ignore) &&
(e.Source == want.Source || want.Source == ignore) &&
(e.FsOptstr == want.FsOptstr || want.FsOptstr == ignore)
}
func (e *MountInfoEntry) String() string {
return fmt.Sprintf("%d %d %d:%d %s %s %s %s %s %s %s",
e.ID, e.Parent, e.Devno[0], e.Devno[1], e.Root, e.Target, e.VfsOptstr,
strings.Join(append(e.OptFields, "-"), " "), e.FsType, e.Source, e.FsOptstr)
}

View File

@ -1,404 +0,0 @@
package vfs_test
import (
"encoding/json"
"errors"
"iter"
"path"
"reflect"
"slices"
"strconv"
"strings"
"syscall"
"testing"
"git.gensokyo.uk/security/fortify/sandbox/vfs"
)
func TestMountInfo(t *testing.T) {
testCases := []mountInfoTest{
{"count", sampleMountinfoBase + `
21 20 0:53/ /mnt/test rw,relatime - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoFields, "", nil, nil, nil},
{"sep", sampleMountinfoBase + `
21 20 0:53 / /mnt/test rw,relatime shared:212 _ tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoSep, "", nil, nil, nil},
{"id", sampleMountinfoBase + `
id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
strconv.ErrSyntax, "", nil, nil, nil},
{"parent", sampleMountinfoBase + `
21 parent 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
strconv.ErrSyntax, "", nil, nil, nil},
{"devno", sampleMountinfoBase + `
21 20 053 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
nil, "unexpected EOF", nil, nil, nil},
{"maj", sampleMountinfoBase + `
21 20 maj:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
nil, "expected integer", nil, nil, nil},
{"min", sampleMountinfoBase + `
21 20 0:min / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
nil, "expected integer", nil, nil, nil},
{"mountroot", sampleMountinfoBase + `
21 20 0:53 /mnt/test rw,relatime - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoEmpty, "", nil, nil, nil},
{"target", sampleMountinfoBase + `
21 20 0:53 / rw,relatime - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoEmpty, "", nil, nil, nil},
{"vfs options", sampleMountinfoBase + `
21 20 0:53 / /mnt/test - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoEmpty, "", nil, nil, nil},
{"FS type", sampleMountinfoBase + `
21 20 0:53 / /mnt/test rw,relatime - rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoEmpty, "", nil, nil, nil},
{"base", sampleMountinfoBase, nil, "", []*wantMountInfo{
m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil),
m(16, 20, 0, 15, "/", "/sys", "rw,relatime", o(), "sysfs", "/sys", "rw", syscall.MS_RELATIME, nil),
m(17, 20, 0, 5, "/", "/dev", "rw,relatime", o(), "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755", syscall.MS_RELATIME, nil),
m(18, 17, 0, 10, "/", "/dev/pts", "rw,relatime", o(), "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000", syscall.MS_RELATIME, nil),
m(19, 17, 0, 16, "/", "/dev/shm", "rw,relatime", o(), "tmpfs", "tmpfs", "rw", syscall.MS_RELATIME, nil),
m(20, 1, 8, 4, "/", "/", "ro,noatime,nodiratime,meow", o(), "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered", syscall.MS_RDONLY|syscall.MS_NOATIME|syscall.MS_NODIRATIME, []string{"meow"}),
},
mn(20, 1, 8, 4, "/", "/", "ro,noatime,nodiratime,meow", o(), "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered", false,
mn(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", false, nil,
mn(16, 20, 0, 15, "/", "/sys", "rw,relatime", o(), "sysfs", "/sys", "rw", false, nil,
mn(17, 20, 0, 5, "/", "/dev", "rw,relatime", o(), "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755", false,
mn(18, 17, 0, 10, "/", "/dev/pts", "rw,relatime", o(), "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000", false, nil,
mn(19, 17, 0, 16, "/", "/dev/shm", "rw,relatime", o(), "tmpfs", "tmpfs", "rw", false, nil, nil)),
nil))), nil), func(n *vfs.MountInfoNode) []*vfs.MountInfoNode {
return []*vfs.MountInfoNode{
n,
n.FirstChild,
n.FirstChild.NextSibling,
n.FirstChild.NextSibling.NextSibling,
n.FirstChild.NextSibling.NextSibling.FirstChild,
n.FirstChild.NextSibling.NextSibling.FirstChild.NextSibling,
}
}},
{"sample", sampleMountinfo, nil, "", []*wantMountInfo{
m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil),
m(16, 20, 0, 15, "/", "/sys", "rw,relatime", o(), "sysfs", "/sys", "rw", syscall.MS_RELATIME, nil),
m(17, 20, 0, 5, "/", "/dev", "rw,relatime", o(), "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755", syscall.MS_RELATIME, nil),
m(18, 17, 0, 10, "/", "/dev/pts", "rw,relatime", o(), "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000", syscall.MS_RELATIME, nil),
m(19, 17, 0, 16, "/", "/dev/shm", "rw,relatime", o(), "tmpfs", "tmpfs", "rw", syscall.MS_RELATIME, nil),
m(20, 1, 8, 4, "/", "/", "rw,noatime", o(), "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered", syscall.MS_NOATIME, nil),
m(21, 16, 0, 17, "/", "/sys/fs/cgroup", "rw,nosuid,nodev,noexec,relatime", o(), "tmpfs", "tmpfs", "rw,mode=755", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
m(22, 21, 0, 18, "/", "/sys/fs/cgroup/systemd", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
m(23, 21, 0, 19, "/", "/sys/fs/cgroup/cpuset", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,cpuset", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
m(24, 21, 0, 20, "/", "/sys/fs/cgroup/ns", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,ns", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
m(25, 21, 0, 21, "/", "/sys/fs/cgroup/cpu", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,cpu", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
m(26, 21, 0, 22, "/", "/sys/fs/cgroup/cpuacct", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,cpuacct", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
m(27, 21, 0, 23, "/", "/sys/fs/cgroup/memory", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,memory", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
m(28, 21, 0, 24, "/", "/sys/fs/cgroup/devices", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,devices", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
m(29, 21, 0, 25, "/", "/sys/fs/cgroup/freezer", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,freezer", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
m(30, 21, 0, 26, "/", "/sys/fs/cgroup/net_cls", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,net_cls", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
m(31, 21, 0, 27, "/", "/sys/fs/cgroup/blkio", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,blkio", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
m(32, 16, 0, 28, "/", "/sys/kernel/security", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil),
m(33, 17, 0, 29, "/", "/dev/hugepages", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil),
m(34, 16, 0, 30, "/", "/sys/kernel/debug", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil),
m(35, 15, 0, 31, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=25,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil),
m(36, 17, 0, 32, "/", "/dev/mqueue", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil),
m(37, 15, 0, 14, "/", "/proc/bus/usb", "rw,relatime", o(), "usbfs", "/proc/bus/usb", "rw", syscall.MS_RELATIME, nil),
m(38, 33, 0, 33, "/", "/dev/hugepages", "rw,relatime", o(), "hugetlbfs", "hugetlbfs", "rw", syscall.MS_RELATIME, nil),
m(39, 36, 0, 12, "/", "/dev/mqueue", "rw,relatime", o(), "mqueue", "mqueue", "rw", syscall.MS_RELATIME, nil),
m(40, 20, 8, 6, "/", "/boot", "rw,noatime", o(), "ext3", "/dev/sda6", "rw,errors=continue,barrier=0,data=ordered", syscall.MS_NOATIME, nil),
m(41, 20, 253, 0, "/", "/home/kzak", "rw,noatime", o(), "ext4", "/dev/mapper/kzak-home", "rw,barrier=1,data=ordered", syscall.MS_NOATIME, nil),
m(42, 35, 0, 34, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", o(), "binfmt_misc", "none", "rw", syscall.MS_RELATIME, nil),
m(43, 16, 0, 35, "/", "/sys/fs/fuse/connections", "rw,relatime", o(), "fusectl", "fusectl", "rw", syscall.MS_RELATIME, nil),
m(44, 41, 0, 36, "/", "/home/kzak/.gvfs", "rw,nosuid,nodev,relatime", o(), "fuse.gvfs-fuse-daemon", "gvfs-fuse-daemon", "rw,user_id=500,group_id=500", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_RELATIME, nil),
m(45, 20, 0, 37, "/", "/var/lib/nfs/rpc_pipefs", "rw,relatime", o(), "rpc_pipefs", "sunrpc", "rw", syscall.MS_RELATIME, nil),
m(47, 20, 0, 38, "/", "/mnt/sounds", "rw,relatime", o(), "cifs", "//foo.home/bar/", "rw,unc=\\\\foo.home\\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344", syscall.MS_RELATIME, nil),
m(49, 20, 0, 56, "/", "/mnt/test/foobar", "rw,relatime,nosymfollow", o("shared:323"), "tmpfs", "tmpfs", "rw", syscall.MS_RELATIME|vfs.MS_NOSYMFOLLOW, nil),
}, nil, nil},
{"sample nosrc", sampleMountinfoNoSrc, nil, "", []*wantMountInfo{
m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil),
m(16, 20, 0, 15, "/", "/sys", "rw,relatime", o(), "sysfs", "/sys", "rw", syscall.MS_RELATIME, nil),
m(17, 20, 0, 5, "/", "/dev", "rw,relatime", o(), "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755", syscall.MS_RELATIME, nil),
m(18, 17, 0, 10, "/", "/dev/pts", "rw,relatime", o(), "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000", syscall.MS_RELATIME, nil),
m(19, 17, 0, 16, "/", "/dev/shm", "rw,relatime", o(), "tmpfs", "tmpfs", "rw", syscall.MS_RELATIME, nil),
m(20, 1, 8, 4, "/", "/", "rw,noatime", o(), "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered", syscall.MS_NOATIME, nil),
m(21, 20, 0, 53, "/", "/mnt/test", "rw,relatime", o("shared:212"), "tmpfs", "", "rw", syscall.MS_RELATIME, nil),
}, nil, nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("decode", func(t *testing.T) {
var got *vfs.MountInfo
d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
err := d.Decode(&got)
tc.check(t, d, "Decode",
func(yield func(*vfs.MountInfoEntry) bool) {
for cur := got; cur != nil; cur = cur.Next {
if !yield(&cur.MountInfoEntry) {
return
}
}
}, func() error { return err })
t.Run("reuse", func(t *testing.T) {
tc.check(t, d, "Entries",
d.Entries(), d.Err)
})
})
t.Run("iter", func(t *testing.T) {
d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
tc.check(t, d, "Entries",
d.Entries(), d.Err)
t.Run("reuse", func(t *testing.T) {
tc.check(t, d, "Entries",
d.Entries(), d.Err)
})
})
t.Run("yield", func(t *testing.T) {
d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
v := false
d.Entries()(func(entry *vfs.MountInfoEntry) bool { v = !v; return v })
d.Entries()(func(entry *vfs.MountInfoEntry) bool { return false })
tc.check(t, d, "Entries",
d.Entries(), d.Err)
t.Run("reuse", func(t *testing.T) {
tc.check(t, d, "Entries",
d.Entries(), d.Err)
})
})
})
}
}
type mountInfoTest struct {
name string
sample string
wantErr error
wantError string
want []*wantMountInfo
wantNode *vfs.MountInfoNode
wantCollectF func(n *vfs.MountInfoNode) []*vfs.MountInfoNode
}
func (tc *mountInfoTest) check(t *testing.T, d *vfs.MountInfoDecoder, funcName string,
got iter.Seq[*vfs.MountInfoEntry], gotErr func() error) {
i := 0
for cur := range got {
if i == len(tc.want) {
if funcName != "Decode" && (tc.wantErr != nil || tc.wantError != "") {
continue
}
t.Errorf("%s: got more than %d entries", funcName, len(tc.want))
break
}
if !reflect.DeepEqual(cur, &tc.want[i].MountInfoEntry) {
t.Errorf("%s: entry %d\ngot: %#v\nwant: %#v",
funcName, i, cur, tc.want[i])
}
flags, unmatched := cur.Flags()
if flags != tc.want[i].flags {
t.Errorf("Flags(%q): %#x, want %#x",
cur.VfsOptstr, flags, tc.want[i].flags)
}
if !slices.Equal(unmatched, tc.want[i].unmatched) {
t.Errorf("Flags(%q): unmatched = %#q, want %#q",
cur.VfsOptstr, unmatched, tc.want[i].unmatched)
}
i++
}
if i != len(tc.want) {
t.Errorf("%s: got %d entries, want %d", funcName, i, len(tc.want))
}
if tc.wantErr == nil && tc.wantError == "" && tc.wantCollectF != nil {
t.Run("unfold", func(t *testing.T) {
n, err := d.Unfold("/")
if err != nil {
t.Errorf("Unfold: error = %v", err)
} else {
t.Run("stop", func(t *testing.T) {
v := false
n.Collective()(func(node *vfs.MountInfoNode) bool { v = !v; return v })
})
if !reflect.DeepEqual(n, tc.wantNode) {
t.Errorf("Unfold: %s, want %s",
mustMarshal(n), mustMarshal(tc.wantNode))
}
t.Run("collective", func(t *testing.T) {
wantCollect := tc.wantCollectF(n)
if gotCollect := slices.Collect(n.Collective()); !reflect.DeepEqual(gotCollect, wantCollect) {
t.Errorf("Collective: \ngot %#v\nwant %#v",
gotCollect, wantCollect)
}
})
}
})
} else if tc.wantNode != nil || tc.wantCollectF != nil {
panic("invalid test case")
} else if _, err := d.Unfold("/"); !errors.Is(err, tc.wantErr) {
if tc.wantError == "" {
t.Errorf("Unfold: error = %v, wantErr %v",
err, tc.wantErr)
} else if err != nil && err.Error() != tc.wantError {
t.Errorf("Unfold: error = %q, wantError %q",
err, tc.wantError)
}
}
if err := gotErr(); !errors.Is(err, tc.wantErr) {
if tc.wantError == "" {
t.Errorf("%s: error = %v, wantErr %v",
funcName, err, tc.wantErr)
} else if err != nil && err.Error() != tc.wantError {
t.Errorf("%s: error = %q, wantError %q",
funcName, err, tc.wantError)
}
}
}
func mustMarshal(v any) string {
p, err := json.Marshal(v)
if err != nil {
panic(err.Error())
}
return string(p)
}
type wantMountInfo struct {
vfs.MountInfoEntry
flags uintptr
unmatched []string
}
func m(
id, parent, maj, min int, root, target, vfsOptstr string, optFields []string, fsType, source, fsOptstr string,
flags uintptr, unmatched []string,
) *wantMountInfo {
return &wantMountInfo{
vfs.MountInfoEntry{
ID: id,
Parent: parent,
Devno: vfs.DevT{maj, min},
Root: root,
Target: target,
VfsOptstr: vfsOptstr,
OptFields: optFields,
FsType: fsType,
Source: source,
FsOptstr: fsOptstr,
}, flags, unmatched,
}
}
func mn(
id, parent, maj, min int, root, target, vfsOptstr string, optFields []string, fsType, source, fsOptstr string,
covered bool, firstChild, nextSibling *vfs.MountInfoNode,
) *vfs.MountInfoNode {
return &vfs.MountInfoNode{
MountInfoEntry: &vfs.MountInfoEntry{
ID: id,
Parent: parent,
Devno: vfs.DevT{maj, min},
Root: root,
Target: target,
VfsOptstr: vfsOptstr,
OptFields: optFields,
FsType: fsType,
Source: source,
FsOptstr: fsOptstr,
},
FirstChild: firstChild,
NextSibling: nextSibling,
Clean: path.Clean(target),
Covered: covered,
}
}
func o(field ...string) []string {
if field == nil {
return []string{}
}
return field
}
const (
sampleMountinfoBase = `15 20 0:3 / /proc rw,relatime - proc /proc rw
16 20 0:15 / /sys rw,relatime - sysfs /sys rw
17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755
18 17 0:10 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000
19 17 0:16 / /dev/shm rw,relatime - tmpfs tmpfs rw
20 1 8:4 / / ro,noatime,nodiratime,meow - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered`
sampleMountinfo = `15 20 0:3 / /proc rw,relatime - proc /proc rw
16 20 0:15 / /sys rw,relatime - sysfs /sys rw
17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755
18 17 0:10 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000
19 17 0:16 / /dev/shm rw,relatime - tmpfs tmpfs rw
20 1 8:4 / / rw,noatime - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755
22 21 0:18 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd
23 21 0:19 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuset
24 21 0:20 / /sys/fs/cgroup/ns rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,ns
25 21 0:21 / /sys/fs/cgroup/cpu rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpu
26 21 0:22 / /sys/fs/cgroup/cpuacct rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuacct
27 21 0:23 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory
28 21 0:24 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,devices
29 21 0:25 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,freezer
30 21 0:26 / /sys/fs/cgroup/net_cls rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,net_cls
31 21 0:27 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,blkio
32 16 0:28 / /sys/kernel/security rw,relatime - autofs systemd-1 rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
33 17 0:29 / /dev/hugepages rw,relatime - autofs systemd-1 rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
34 16 0:30 / /sys/kernel/debug rw,relatime - autofs systemd-1 rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
35 15 0:31 / /proc/sys/fs/binfmt_misc rw,relatime - autofs systemd-1 rw,fd=25,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
36 17 0:32 / /dev/mqueue rw,relatime - autofs systemd-1 rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
37 15 0:14 / /proc/bus/usb rw,relatime - usbfs /proc/bus/usb rw
38 33 0:33 / /dev/hugepages rw,relatime - hugetlbfs hugetlbfs rw
39 36 0:12 / /dev/mqueue rw,relatime - mqueue mqueue rw
40 20 8:6 / /boot rw,noatime - ext3 /dev/sda6 rw,errors=continue,barrier=0,data=ordered
41 20 253:0 / /home/kzak rw,noatime - ext4 /dev/mapper/kzak-home rw,barrier=1,data=ordered
42 35 0:34 / /proc/sys/fs/binfmt_misc rw,relatime - binfmt_misc none rw
43 16 0:35 / /sys/fs/fuse/connections rw,relatime - fusectl fusectl rw
44 41 0:36 / /home/kzak/.gvfs rw,nosuid,nodev,relatime - fuse.gvfs-fuse-daemon gvfs-fuse-daemon rw,user_id=500,group_id=500
45 20 0:37 / /var/lib/nfs/rpc_pipefs rw,relatime - rpc_pipefs sunrpc rw
47 20 0:38 / /mnt/sounds rw,relatime - cifs //foo.home/bar/ rw,unc=\\foo.home\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344
49 20 0:56 / /mnt/test/foobar rw,relatime,nosymfollow shared:323 - tmpfs tmpfs rw`
sampleMountinfoNoSrc = `15 20 0:3 / /proc rw,relatime - proc /proc rw
16 20 0:15 / /sys rw,relatime - sysfs /sys rw
17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755
18 17 0:10 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000
19 17 0:16 / /dev/shm rw,relatime - tmpfs tmpfs rw
20 1 8:4 / / rw,noatime - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered
21 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw`
)

View File

@ -1,107 +0,0 @@
package vfs
import (
"iter"
"path"
"strings"
"syscall"
)
// MountInfoNode positions a [MountInfoEntry] in its mount hierarchy.
type MountInfoNode struct {
*MountInfoEntry
FirstChild *MountInfoNode `json:"first_child"`
NextSibling *MountInfoNode `json:"next_sibling"`
Clean string `json:"clean"`
Covered bool `json:"covered"`
}
// Collective returns an iterator over visible mountinfo nodes.
func (n *MountInfoNode) Collective() iter.Seq[*MountInfoNode] {
return func(yield func(*MountInfoNode) bool) { n.visit(yield) }
}
func (n *MountInfoNode) visit(yield func(*MountInfoNode) bool) bool {
if !n.Covered && !yield(n) {
return false
}
for cur := n.FirstChild; cur != nil; cur = cur.NextSibling {
if !cur.visit(yield) {
return false
}
}
return true
}
// Unfold unfolds the mount hierarchy and resolves covered paths.
func (d *MountInfoDecoder) Unfold(target string) (*MountInfoNode, error) {
targetClean := path.Clean(target)
var mountinfoSize int
for range d.Entries() {
mountinfoSize++
}
if err := d.Err(); err != nil {
return nil, err
}
mountinfo := make([]*MountInfoNode, mountinfoSize)
// mount ID to index lookup
idIndex := make(map[int]int, mountinfoSize)
// final entry to match target
targetIndex := -1
{
i := 0
for ent := range d.Entries() {
mountinfo[i] = &MountInfoNode{Clean: path.Clean(ent.Target), MountInfoEntry: ent}
idIndex[ent.ID] = i
if mountinfo[i].Clean == targetClean {
targetIndex = i
}
i++
}
}
if targetIndex == -1 {
return nil, syscall.ESTALE
}
for _, cur := range mountinfo {
var parent *MountInfoNode
if p, ok := idIndex[cur.Parent]; !ok {
continue
} else {
parent = mountinfo[p]
}
if !strings.HasPrefix(cur.Clean, targetClean) {
continue
}
if parent.Clean == cur.Clean {
parent.Covered = true
}
covered := false
nsp := &parent.FirstChild
for s := parent.FirstChild; s != nil; s = s.NextSibling {
if strings.HasPrefix(cur.Clean, s.Clean) {
covered = true
break
}
if strings.HasPrefix(s.Clean, cur.Clean) {
*nsp = s.NextSibling
} else {
nsp = &s.NextSibling
}
}
if covered {
continue
}
*nsp = cur
}
return mountinfo[targetIndex], nil
}

View File

@ -1,93 +0,0 @@
package vfs_test
import (
"errors"
"reflect"
"slices"
"strings"
"syscall"
"testing"
"git.gensokyo.uk/security/fortify/sandbox/vfs"
)
func TestUnfold(t *testing.T) {
testCases := []struct {
name string
sample string
target string
wantErr error
want *vfs.MountInfoNode
wantCollectF func(n *vfs.MountInfoNode) []*vfs.MountInfoNode
wantCollectN []string
}{
{
"no match",
sampleMountinfoBase,
"/mnt",
syscall.ESTALE, nil, nil, nil,
},
{
"cover",
`33 1 0:33 / / rw,relatime shared:1 - tmpfs impure rw,size=16777216k,mode=755
37 33 0:32 / /proc rw,nosuid,nodev,noexec,relatime shared:41 - proc proc rw
551 33 0:121 / /mnt rw,relatime shared:666 - tmpfs tmpfs rw
595 551 0:123 / /mnt rw,relatime shared:990 - tmpfs tmpfs rw
611 595 0:142 / /mnt/etc rw,relatime shared:1112 - tmpfs tmpfs rw
625 644 0:142 /passwd /mnt/etc/passwd rw,relatime shared:1112 - tmpfs tmpfs rw
641 625 0:33 /etc/passwd /mnt/etc/passwd rw,relatime shared:1 - tmpfs impure rw,size=16777216k,mode=755
644 611 0:33 /etc/passwd /mnt/etc/passwd rw,relatime shared:1 - tmpfs impure rw,size=16777216k,mode=755
`, "/mnt", nil,
mn(595, 551, 0, 123, "/", "/mnt", "rw,relatime", o("shared:990"), "tmpfs", "tmpfs", "rw", false,
mn(611, 595, 0, 142, "/", "/mnt/etc", "rw,relatime", o("shared:1112"), "tmpfs", "tmpfs", "rw", false,
mn(644, 611, 0, 33, "/etc/passwd", "/mnt/etc/passwd", "rw,relatime", o("shared:1"), "tmpfs", "impure", "rw,size=16777216k,mode=755", true,
mn(625, 644, 0, 142, "/passwd", "/mnt/etc/passwd", "rw,relatime", o("shared:1112"), "tmpfs", "tmpfs", "rw", true,
mn(641, 625, 0, 33, "/etc/passwd", "/mnt/etc/passwd", "rw,relatime", o("shared:1"), "tmpfs", "impure", "rw,size=16777216k,mode=755", false,
nil, nil), nil), nil), nil), nil), func(n *vfs.MountInfoNode) []*vfs.MountInfoNode {
return []*vfs.MountInfoNode{n, n.FirstChild, n.FirstChild.FirstChild.FirstChild.FirstChild}
}, []string{"/mnt", "/mnt/etc", "/mnt/etc/passwd"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
got, err := d.Unfold(tc.target)
if !errors.Is(err, tc.wantErr) {
t.Errorf("Unfold: error = %v, wantErr %v",
err, tc.wantErr)
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("Unfold:\ngot %s\nwant %s",
mustMarshal(got), mustMarshal(tc.want))
}
if err == nil && tc.wantCollectF != nil {
t.Run("collective", func(t *testing.T) {
wantCollect := tc.wantCollectF(got)
gotCollect := slices.Collect(got.Collective())
if !reflect.DeepEqual(gotCollect, wantCollect) {
t.Errorf("Collective: \ngot %#v\nwant %#v",
gotCollect, wantCollect)
}
t.Run("target", func(t *testing.T) {
gotCollectN := slices.Collect[string](func(yield func(v string) bool) {
for _, cur := range gotCollect {
if !yield(cur.Clean) {
return
}
}
})
if !reflect.DeepEqual(gotCollectN, tc.wantCollectN) {
t.Errorf("Collective: got %q, want %q",
gotCollectN, tc.wantCollectN)
}
})
})
}
})
}
}

View File

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

View File

@ -1,7 +1,6 @@
{ {
lib, lib,
nixosTest, nixosTest,
buildFHSEnv,
writeShellScriptBin, writeShellScriptBin,
system, system,
@ -13,21 +12,6 @@ nixosTest {
name = "fortify" + (if withRace then "-race" else ""); name = "fortify" + (if withRace then "-race" else "");
nodes.machine = nodes.machine =
{ options, pkgs, ... }: { options, pkgs, ... }:
let
fhs =
let
fortify = options.environment.fortify.package.default;
in
buildFHSEnv {
pname = "fortify-fhs";
inherit (fortify) version;
targetPkgs = _: fortify.targetPkgs;
extraOutputsToInstall = [ "dev" ];
profile = ''
export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
'';
};
in
{ {
environment.systemPackages = [ environment.systemPackages = [
# For go tests: # For go tests:
@ -37,7 +21,7 @@ nixosTest {
cp -r "${self.packages.${system}.fortify.src}" "$WORK" cp -r "${self.packages.${system}.fortify.src}" "$WORK"
chmod -R +w "$WORK" chmod -R +w "$WORK"
cd "$WORK" cd "$WORK"
${fhs}/bin/fortify-fhs -c \ ${self.packages.${system}.fhs}/bin/fortify-fhs -c \
'go generate ./... && go test ${if withRace then "-race" else "-count 16"} ./... && touch /tmp/go-test-ok' 'go generate ./... && go test ${if withRace then "-race" else "-count 16"} ./... && touch /tmp/go-test-ok'
'') '')
]; ];

View File

@ -1,9 +1,3 @@
/*
Package sandbox provides utilities for checking sandbox outcome.
This package must never be used outside integration tests, there is a much better native implementation of mountinfo
in the public sandbox/vfs package. Files in this package are excluded by the build system to prevent accidental misuse.
*/
package sandbox package sandbox
import ( import (
@ -22,89 +16,76 @@ var (
func printf(format string, v ...any) { printfFunc(format, v...) } func printf(format string, v ...any) { printfFunc(format, v...) }
func fatalf(format string, v ...any) { fatalfFunc(format, v...) } func fatalf(format string, v ...any) { fatalfFunc(format, v...) }
type TestCase struct { func mustDecode(wantFile string, v any) {
FS *FS `json:"fs"` if f, err := os.Open(wantFile); err != nil {
Mount []*MountinfoEntry `json:"mount"` fatalf("cannot open %q: %v", wantFile, err)
Seccomp bool `json:"seccomp"` } else if err = json.NewDecoder(f).Decode(v); err != nil {
fatalf("cannot decode %q: %v", wantFile, err)
} else if err = f.Close(); err != nil {
fatalf("cannot close %q: %v", wantFile, err)
}
} }
type T struct { func MustAssertMounts(name, hostMountsFile, wantFile string) {
FS fs.FS hostMounts := make([]*Mntent, 0, 128)
if err := IterMounts(hostMountsFile, func(e *Mntent) {
MountsPath string hostMounts = append(hostMounts, e)
}); err != nil {
fatalf("cannot parse host mounts: %v", err)
} }
func (t *T) MustCheckFile(wantFilePath string) { var want []Mntent
var want *TestCase mustDecode(wantFile, &want)
mustDecode(wantFilePath, &want)
t.MustCheck(want) for i := range want {
if want[i].Opts == "host_passthrough" {
for _, ent := range hostMounts {
if want[i].FSName == ent.FSName {
// special case for tmpfs bind mounts
if want[i].FSName == "tmpfs" && want[i].Dir != ent.Dir {
continue
} }
func (t *T) MustCheck(want *TestCase) { want[i].Opts = ent.Opts
if want.FS != nil && t.FS != nil { goto out
if err := want.FS.Compare(".", t.FS); err != nil { }
fatalf("%v", err) }
fatalf("host passthrough missing %q", want[i].FSName)
out:
} }
} else {
printf("[SKIP] skipping fs check")
} }
if want.Mount != nil {
var fail bool
m := mustParseMountinfo(t.MountsPath)
i := 0 i := 0
for ent := range m.Entries() { if err := IterMounts(name, func(e *Mntent) {
if i == len(want.Mount) { if i == len(want) {
fatalf("got more than %d entries", i) fatalf("got more than %d entries", i)
} }
if !ent.EqualWithIgnore(want.Mount[i], "//ignore") { if !e.Is(&want[i]) {
fail = true fatalf("entry %d\n got: %s\nwant: %s", i,
printf("[FAIL] %s", ent) e, &want[i])
} else {
printf("[ OK ] %s", ent)
} }
printf("%s", e)
i++ i++
}); err != nil {
fatalf("cannot iterate mounts: %v", err)
} }
if err := m.Err(); err != nil { }
func MustAssertFS(e fs.FS, wantFile string) {
var want *FS
mustDecode(wantFile, &want)
if want == nil {
fatalf("invalid payload")
}
if err := want.Compare(".", e); err != nil {
fatalf("%v", err) fatalf("%v", err)
} }
if i != len(want.Mount) {
fatalf("got %d entries, want %d", i, len(want.Mount))
} }
if fail { func MustAssertSeccomp() {
fatalf("[FAIL] some mount points did not match")
}
} else {
printf("[SKIP] skipping mounts check")
}
if want.Seccomp {
if TrySyscalls() != nil { if TrySyscalls() != nil {
os.Exit(1) os.Exit(1)
} }
} else {
printf("[SKIP] skipping seccomp check")
}
}
func mustDecode(wantFilePath string, v any) {
if f, err := os.Open(wantFilePath); err != nil {
fatalf("cannot open %q: %v", wantFilePath, err)
} else if err = json.NewDecoder(f).Decode(v); err != nil {
fatalf("cannot decode %q: %v", wantFilePath, err)
} else if err = f.Close(); err != nil {
fatalf("cannot close %q: %v", wantFilePath, err)
}
}
func mustParseMountinfo(name string) *Mountinfo {
m := NewMountinfo(name)
if err := m.Parse(); err != nil {
fatalf("%v", err)
panic("unreachable")
}
return m
} }

View File

@ -1,30 +0,0 @@
{
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,58 +0,0 @@
{
lib,
callPackage,
foot,
version,
}:
let
fs = mode: dir: data: {
mode = lib.fromHexString mode;
inherit
dir
data
;
};
ignore = "//ignore";
ent = root: target: vfs_optstr: fstype: source: fs_optstr: {
id = -1;
parent = -1;
inherit
root
target
vfs_optstr
fstype
source
fs_optstr
;
};
checkSandbox = callPackage ../. { inherit version; };
callTestCase =
path:
let
tc = import path {
inherit
fs
ent
ignore
;
};
in
{
name = "check-sandbox-${tc.name}";
verbose = true;
inherit (tc) tty mapRealUid;
share = foot;
packages = [ ];
command = builtins.toString (checkSandbox tc.name tc.want);
};
in
{
preset = callTestCase ./preset.nix;
tty = callTestCase ./tty.nix;
mapuid = callTestCase ./mapuid.nix;
}

View File

@ -1,221 +0,0 @@
{
fs,
ent,
ignore,
}:
{
name = "mapuid";
tty = false;
mapRealUid = true;
want = {
fs = fs "dead" {
".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;
dri = fs "800001ed" {
by-path = fs "800001ed" {
"pci-0000:00:09.0-card" = fs "80001ff" null null;
"pci-0000:00:09.0-render" = fs "80001ff" null null;
} null;
card0 = fs "42001b0" null null;
renderD128 = fs "42001b6" null null;
} null;
fd = fs "80001ff" null null;
full = fs "42001b6" null null;
mqueue = fs "801001ff" { } null;
null = fs "42001b6" null "";
ptmx = fs "80001ff" null null;
pts = fs "800001ed" { ptmx = fs "42001b6" null null; } null;
random = fs "42001b6" null null;
shm = fs "800001ed" { } null;
stderr = fs "80001ff" null null;
stdin = fs "80001ff" null null;
stdout = fs "80001ff" null null;
tty = fs "42001b6" null null;
urandom = fs "42001b6" null null;
zero = fs "42001b6" null null;
} null;
etc = fs "800001c0" {
".clean" = fs "80001ff" 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:100:\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_a3:x:1000:100:Fortify:/var/lib/fortify/u0/a3:/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;
"profiles" = 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 "800001c0" {
current-system = fs "8000016d" null null;
opengl-driver = fs "8000016d" null null;
user = fs "800001ed" {
"1000" = fs "800001ed" {
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" {
a3 = 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=1000003,gid=1000003")
(ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw")
(ent "/" "/.fortify" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000003,gid=1000003")
(ent "/" "/dev" "rw,nosuid,nodev,relatime" "tmpfs" "devtmpfs" "rw,mode=755,uid=1000003,gid=1000003")
(ent "/null" "/dev/null" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/zero" "/dev/zero" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/full" "/dev/full" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/random" "/dev/random" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/urandom" "/dev/urandom" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/tty" "/dev/tty" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,mode=620,ptmxmode=666")
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
(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 ignore "/run/current-system" "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 ignore "/run/opengl-driver" "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 "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(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=755,uid=1000003,gid=1000003")
(ent "/tmp/fortify.1000/tmpdir/3" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/lib/fortify/u0/a3" "/var/lib/fortify/u0/a3" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000003,gid=1000003")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000003,gid=1000003")
(ent ignore "/run/user/1000/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/1000/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/1000/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=1000003,gid=1000003")
];
seccomp = true;
};
}

View File

@ -1,223 +0,0 @@
{
fs,
ent,
ignore,
}:
{
name = "tty";
tty = true;
mapRealUid = false;
want = {
fs = fs "dead" {
".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;
core = fs "80001ff" null null;
dri = fs "800001ed" {
by-path = fs "800001ed" {
"pci-0000:00:09.0-card" = fs "80001ff" null null;
"pci-0000:00:09.0-render" = fs "80001ff" null null;
} null;
card0 = fs "42001b0" null null;
renderD128 = fs "42001b6" null null;
} null;
fd = fs "80001ff" null null;
full = fs "42001b6" null null;
mqueue = fs "801001ff" { } null;
null = fs "42001b6" null "";
ptmx = fs "80001ff" null null;
pts = fs "800001ed" { ptmx = fs "42001b6" null null; } null;
random = fs "42001b6" null null;
shm = fs "800001ed" { } null;
stderr = fs "80001ff" null null;
stdin = fs "80001ff" null null;
stdout = fs "80001ff" null null;
tty = fs "42001b6" null null;
urandom = fs "42001b6" null null;
zero = fs "42001b6" null null;
} null;
etc = fs "800001c0" {
".clean" = fs "80001ff" 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_a2:x:65534:65534:Fortify:/var/lib/fortify/u0/a2:/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;
"profiles" = 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 "800001c0" {
current-system = fs "8000016d" null null;
opengl-driver = fs "8000016d" null null;
user = fs "800001ed" {
"65534" = fs "800001ed" {
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" {
a2 = 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=1000002,gid=1000002")
(ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw")
(ent "/" "/.fortify" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000002,gid=1000002")
(ent "/" "/dev" "rw,nosuid,nodev,relatime" "tmpfs" "devtmpfs" "rw,mode=755,uid=1000002,gid=1000002")
(ent "/null" "/dev/null" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/zero" "/dev/zero" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/full" "/dev/full" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/random" "/dev/random" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/urandom" "/dev/urandom" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/tty" "/dev/tty" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,mode=620,ptmxmode=666")
(ent ignore "/dev/console" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666")
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
(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 ignore "/run/current-system" "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 ignore "/run/opengl-driver" "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 "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(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=755,uid=1000002,gid=1000002")
(ent "/tmp/fortify.1000/tmpdir/2" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/lib/fortify/u0/a2" "/var/lib/fortify/u0/a2" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000002,gid=1000002")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000002,gid=1000002")
(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=1000002,gid=1000002")
];
seccomp = true;
};
}

View File

@ -1,14 +1,14 @@
{ {
writeShellScript, writeShellScript,
writeText,
callPackage, callPackage,
version, version,
}: }:
name: want: writeShellScript "check-sandbox" ''
writeShellScript "fortify-${name}-check-sandbox-script" ''
set -e set -e
${callPackage ./assert.nix { inherit version; }}/bin/test \ ${callPackage ./mount.nix { inherit version; }}/bin/test
${writeText "fortify-${name}-want.json" (builtins.toJSON want)} ${callPackage ./fs.nix { inherit version; }}/bin/test
${callPackage ./seccomp.nix { inherit version; }}/bin/test
touch /tmp/sandbox-ok touch /tmp/sandbox-ok
'' ''

View File

@ -30,7 +30,7 @@ func printDir(prefix string, dir []fs.DirEntry) {
} }
names[i] = fmt.Sprintf("%q", name) names[i] = fmt.Sprintf("%q", name)
} }
printf("[FAIL] d %s: %s", prefix, strings.Join(names, " ")) printf("[FAIL] d %q: %s", prefix, strings.Join(names, " "))
} }
func (s *FS) Compare(prefix string, e fs.FS) error { func (s *FS) Compare(prefix string, e fs.FS) error {
@ -71,7 +71,7 @@ func (s *FS) Compare(prefix string, e fs.FS) error {
if fi, err := got.Info(); err != nil { if fi, err := got.Info(); err != nil {
return err return err
} else if fi.Mode() != want.Mode { } else if fi.Mode() != want.Mode {
printf("[FAIL] m %s: %x, want %x", printf("[FAIL] m %q: %x, want %x",
name, uint32(fi.Mode()), uint32(want.Mode)) name, uint32(fi.Mode()), uint32(want.Mode))
return ErrFSBadMode return ErrFSBadMode
} }
@ -84,8 +84,6 @@ func (s *FS) Compare(prefix string, e fs.FS) error {
return err return err
} else if string(v) != *want.Data { } else if string(v) != *want.Data {
printf("[FAIL] f %s", name) printf("[FAIL] f %s", name)
printf("got: %s", v)
printf("want: %s", *want.Data)
return ErrFSBadData return ErrFSBadData
} }
printf("[ OK ] f %s", name) printf("[ OK ] f %s", name)

View File

@ -1,17 +1,29 @@
{ {
fs, lib,
ent, writeText,
ignore, buildGoModule,
}:
{
name = "preset";
tty = false;
mapRealUid = false;
want = { version,
fs = fs "dead" { }:
let
wantFS =
let
fs = mode: dir: data: {
mode = lib.fromHexString mode;
inherit
dir
data
;
};
in
fs "dead" {
".fortify" = fs "800001ed" { ".fortify" = fs "800001ed" {
etc = fs "800001ed" null null; etc = fs "800001ed" null null;
sbin = fs "800001c0" {
fortify = fs "16d" null null;
init0 = fs "80001ff" null null;
} null;
host-mounts = fs "124" null null;
} null; } null;
bin = fs "800001ed" { sh = fs "80001ff" null null; } null; bin = fs "800001ed" { sh = fs "80001ff" null null; } null;
dev = fs "800001ed" { dev = fs "800001ed" {
@ -179,43 +191,24 @@
} null; } null;
} null; } null;
mount = [ mainFile = writeText "main.go" ''
(ent "/sysroot" "/" "rw,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000001,gid=1000001") package main
(ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw")
(ent "/" "/.fortify" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000001,gid=1000001")
(ent "/" "/dev" "rw,nosuid,nodev,relatime" "tmpfs" "devtmpfs" "rw,mode=755,uid=1000001,gid=1000001")
(ent "/null" "/dev/null" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/zero" "/dev/zero" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/full" "/dev/full" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/random" "/dev/random" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/urandom" "/dev/urandom" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/tty" "/dev/tty" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,mode=620,ptmxmode=666")
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
(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 ignore "/run/current-system" "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 ignore "/run/opengl-driver" "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 "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(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=755,uid=1000001,gid=1000001")
(ent "/tmp/fortify.1000/tmpdir/1" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/lib/fortify/u0/a1" "/var/lib/fortify/u0/a1" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000001,gid=1000001")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000001,gid=1000001")
(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=1000001,gid=1000001")
];
seccomp = true; import "os"
}; import "git.gensokyo.uk/security/fortify/test/sandbox"
func main() { sandbox.MustAssertFS(os.DirFS("/"), "${writeText "want-fs.json" (builtins.toJSON wantFS)}") }
'';
in
buildGoModule {
pname = "check-fs";
inherit version;
src = ../.;
vendorHash = null;
preBuild = ''
go mod init git.gensokyo.uk/security/fortify/test >& /dev/null
cp ${mainFile} main.go
'';
} }

View File

@ -31,16 +31,16 @@ func TestCompare(t *testing.T) {
"[ OK ] s .fortify\x00[ OK ] d .\x00", nil}, "[ OK ] s .fortify\x00[ OK ] d .\x00", nil},
{"bad length", fstest.MapFS{".fortify": {Mode: 0x800001ed}}, {"bad length", fstest.MapFS{".fortify": {Mode: 0x800001ed}},
&sandbox.FS{Dir: make(map[string]*sandbox.FS)}, &sandbox.FS{Dir: make(map[string]*sandbox.FS)},
"[FAIL] d .: \".fortify/\"\x00", sandbox.ErrFSBadLength}, "[FAIL] d \".\": \".fortify/\"\x00", sandbox.ErrFSBadLength},
{"top level bad mode", fstest.MapFS{".fortify": {Mode: 0x800001ed}}, {"top level bad mode", fstest.MapFS{".fortify": {Mode: 0x800001ed}},
&sandbox.FS{Dir: map[string]*sandbox.FS{".fortify": {Mode: 0xdeadbeef}}}, &sandbox.FS{Dir: map[string]*sandbox.FS{".fortify": {Mode: 0xdeadbeef}}},
"[FAIL] m .fortify: 800001ed, want deadbeef\x00", sandbox.ErrFSBadMode}, "[FAIL] m \".fortify\": 800001ed, want deadbeef\x00", sandbox.ErrFSBadMode},
{"invalid entry condition", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}}, {"invalid entry condition", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}},
&sandbox.FS{Dir: map[string]*sandbox.FS{"test": {Dir: make(map[string]*sandbox.FS)}}}, &sandbox.FS{Dir: map[string]*sandbox.FS{"test": {Dir: make(map[string]*sandbox.FS)}}},
"[FAIL] d .: \"test\"\x00", sandbox.ErrFSInvalidEnt}, "[FAIL] d \".\": \"test\"\x00", sandbox.ErrFSInvalidEnt},
{"nonexistent", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}}, {"nonexistent", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}},
&sandbox.FS{Dir: map[string]*sandbox.FS{".test": {}}}, &sandbox.FS{Dir: map[string]*sandbox.FS{".test": {}}},
"[FAIL] d .: \"test\"\x00", fs.ErrNotExist}, "[FAIL] d \".\": \"test\"\x00", fs.ErrNotExist},
{"file", fstest.MapFS{"etc": {Mode: 0x800001c0}, {"file", fstest.MapFS{"etc": {Mode: 0x800001c0},
"etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644}, "etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644},
"etc/group": {Data: []byte(fsGroupSample), Mode: 0644}, "etc/group": {Data: []byte(fsGroupSample), Mode: 0644},
@ -54,7 +54,7 @@ func TestCompare(t *testing.T) {
}, &sandbox.FS{Dir: map[string]*sandbox.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*sandbox.FS{ }, &sandbox.FS{Dir: map[string]*sandbox.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*sandbox.FS{
"passwd": {Mode: 0x1a4, Data: &fsGroupSample}, "passwd": {Mode: 0x1a4, Data: &fsGroupSample},
"group": {Mode: 0x1a4, Data: &fsGroupSample}, "group": {Mode: 0x1a4, Data: &fsGroupSample},
}}}}, "[ OK ] f etc/group\x00[FAIL] f etc/passwd\x00got: u0_a20:x:65534:65534:Fortify:/var/lib/persist/module/fortify/u0/a20:/run/current-system/sw/bin/zsh\x00want: fortify:x:65534:\x00", sandbox.ErrFSBadData}, }}}}, "[ OK ] f etc/group\x00[FAIL] f etc/passwd\x00", sandbox.ErrFSBadData},
} }
for _, tc := range testCases { for _, tc := range testCases {
@ -75,4 +75,10 @@ func TestCompare(t *testing.T) {
} }
}) })
} }
t.Run("assert", func(t *testing.T) {
oldFatal := sandbox.SwapFatal(t.Fatalf)
t.Cleanup(func() { sandbox.SwapFatal(oldFatal) })
sandbox.MustAssertFS(make(fstest.MapFS), sandbox.MustWantFile(t, &sandbox.FS{Mode: 0xDEADBEEF}))
})
} }

View File

@ -1,157 +1,146 @@
package sandbox package sandbox
/* /*
#cgo linux pkg-config: --static mount
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
#include <libmount.h> #include <mntent.h>
const char *F_MOUNTINFO_PATH = "/proc/self/mountinfo"; const char *F_PROC_MOUNTS = "";
const char *F_SET_TYPE = "r";
*/ */
import "C" import "C"
import ( import (
"errors"
"fmt" "fmt"
"iter"
"runtime" "runtime"
"sync" "sync"
"unsafe" "unsafe"
) )
var ( type Mntent struct {
ErrMountinfoParse = errors.New("invalid mountinfo records") /* name of mounted filesystem */
ErrMountinfoIter = errors.New("cannot allocate iterator") FSName string `json:"fsname"`
ErrMountinfoFault = errors.New("cannot iterate on filesystems") /* filesystem path prefix */
) Dir string `json:"dir"`
/* mount type (see mntent.h) */
Type string `json:"type"`
/* mount options (see mntent.h) */
Opts string `json:"opts"`
/* dump frequency in days */
Freq int `json:"freq"`
/* pass number on parallel fsck */
Passno int `json:"passno"`
}
type ( func (e *Mntent) String() string {
Mountinfo struct { return fmt.Sprintf("%s %s %s %s %d %d",
mu sync.RWMutex e.FSName, e.Dir, e.Type, e.Opts, e.Freq, e.Passno)
}
func (e *Mntent) Is(want *Mntent) bool {
if want == nil {
return e == nil
}
return (e.FSName == want.FSName || want.FSName == "\x00") &&
(e.Dir == want.Dir || want.Dir == "\x00") &&
(e.Type == want.Type || want.Type == "\x00") &&
(e.Opts == want.Opts || want.Opts == "\x00") &&
(e.Freq == want.Freq || want.Freq == -1) &&
(e.Passno == want.Passno || want.Passno == -1)
}
func IterMounts(name string, f func(e *Mntent)) error {
m := new(mounts)
m.p = name
if err := m.open(); err != nil {
return err
}
for m.scan() {
e := new(Mntent)
m.copy(e)
f(e)
}
m.close()
return m.Err()
}
type mounts struct {
p string p string
f *C.FILE
mu sync.RWMutex
ent *C.struct_mntent
err error err error
tb *C.struct_libmnt_table
itr *C.struct_libmnt_iter
fs *C.struct_libmnt_fs
} }
// MountinfoEntry represents deterministic mountinfo parts of a libmnt_fs entry. func (m *mounts) open() error {
MountinfoEntry struct {
// mount ID: a unique ID for the mount (may be reused after umount(2)).
ID int `json:"id"`
// parent ID: the ID of the parent mount (or of self for the root of this mount namespace's mount tree).
Parent int `json:"parent"`
// root: the pathname of the directory in the filesystem which forms the root of this mount.
Root string `json:"root"`
// mount point: the pathname of the mount point relative to the process's root directory.
Target string `json:"target"`
// mount options: per-mount options (see mount(2)).
VfsOptstr string `json:"vfs_optstr"`
// filesystem type: the filesystem type in the form "type[.subtype]".
FsType string `json:"fstype"`
// mount source: filesystem-specific information or "none".
Source string `json:"source"`
// super options: per-superblock options (see mount(2)).
FsOptstr string `json:"fs_optstr"`
}
)
func (m *Mountinfo) copy(v *MountinfoEntry) {
if m.fs == nil {
panic("invalid entry")
}
v.ID = int(C.mnt_fs_get_id(m.fs))
v.Parent = int(C.mnt_fs_get_parent_id(m.fs))
v.Root = C.GoString(C.mnt_fs_get_root(m.fs))
v.Target = C.GoString(C.mnt_fs_get_target(m.fs))
v.VfsOptstr = C.GoString(C.mnt_fs_get_vfs_options(m.fs))
v.FsType = C.GoString(C.mnt_fs_get_fstype(m.fs))
v.Source = C.GoString(C.mnt_fs_get_source(m.fs))
v.FsOptstr = C.GoString(C.mnt_fs_get_fs_options(m.fs))
}
func NewMountinfo(p string) *Mountinfo { m := new(Mountinfo); m.p = p; return m }
func (m *Mountinfo) Err() error { m.mu.RLock(); defer m.mu.RUnlock(); return m.err }
func (m *Mountinfo) Parse() error {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
if m.tb != nil { if m.f != nil {
panic("open called twice") panic("open called twice")
} }
if m.p == "" { if m.p == "" {
m.tb = C.mnt_new_table_from_file(C.F_MOUNTINFO_PATH) m.p = "/proc/mounts"
} else { }
name := C.CString(m.p) name := C.CString(m.p)
m.tb = C.mnt_new_table_from_file(name) f, err := C.setmntent(name, C.F_SET_TYPE)
C.free(unsafe.Pointer(name)) C.free(unsafe.Pointer(name))
if f == nil {
return err
} }
if m.tb == nil { m.f = f
return ErrMountinfoParse runtime.SetFinalizer(m, (*mounts).close)
} return err
m.itr = C.mnt_new_iter(C.MNT_ITER_FORWARD)
if m.itr == nil {
C.mnt_unref_table(m.tb)
return ErrMountinfoIter
} }
runtime.SetFinalizer(m, (*Mountinfo).Unref) func (m *mounts) close() {
return nil
}
func (m *Mountinfo) Unref() {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
if m.tb == nil { if m.f == nil {
panic("unref called before parse") panic("close called before open")
} }
C.mnt_unref_table(m.tb) C.endmntent(m.f)
C.mnt_free_iter(m.itr)
runtime.SetFinalizer(m, nil) runtime.SetFinalizer(m, nil)
} }
func (m *Mountinfo) Entries() iter.Seq[*MountinfoEntry] { func (m *mounts) scan() bool {
return func(yield func(*MountinfoEntry) bool) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
C.mnt_reset_iter(m.itr, -1) if m.f == nil {
panic("invalid file")
var rc C.int
ent := new(MountinfoEntry)
for rc = C.mnt_table_next_fs(m.tb, m.itr, &m.fs); rc == 0; rc = C.mnt_table_next_fs(m.tb, m.itr, &m.fs) {
m.copy(ent)
if !yield(ent) {
return
}
}
if rc < 0 {
m.err = ErrMountinfoFault
return
}
}
} }
func (e *MountinfoEntry) EqualWithIgnore(want *MountinfoEntry, ignore string) bool { m.ent, m.err = C.getmntent(m.f)
return (e.ID == want.ID || want.ID == -1) && return m.ent != nil
(e.Parent == want.Parent || want.Parent == -1) &&
(e.Root == want.Root || want.Root == ignore) &&
(e.Target == want.Target || want.Target == ignore) &&
(e.VfsOptstr == want.VfsOptstr || want.VfsOptstr == ignore) &&
(e.FsType == want.FsType || want.FsType == ignore) &&
(e.Source == want.Source || want.Source == ignore) &&
(e.FsOptstr == want.FsOptstr || want.FsOptstr == ignore)
} }
func (e *MountinfoEntry) String() string { func (m *mounts) Err() error {
return fmt.Sprintf("%d %d %s %s %s %s %s %s", m.mu.RLock()
e.ID, e.Parent, e.Root, e.Target, e.VfsOptstr, e.FsType, e.Source, e.FsOptstr) defer m.mu.RUnlock()
return m.err
}
func (m *mounts) copy(v *Mntent) {
m.mu.RLock()
defer m.mu.RUnlock()
if m.ent == nil {
panic("invalid entry")
}
v.FSName = C.GoString(m.ent.mnt_fsname)
v.Dir = C.GoString(m.ent.mnt_dir)
v.Type = C.GoString(m.ent.mnt_type)
v.Opts = C.GoString(m.ent.mnt_opts)
v.Freq = int(m.ent.mnt_freq)
v.Passno = int(m.ent.mnt_passno)
} }

79
test/sandbox/mount.nix Normal file
View File

@ -0,0 +1,79 @@
{
writeText,
buildGoModule,
version,
}:
let
wantMounts =
let
ent = fsname: dir: type: opts: freq: passno: {
inherit
fsname
dir
type
opts
freq
passno
;
};
in
[
(ent "tmpfs" "/" "tmpfs" "rw,nosuid,nodev,relatime,uid=1000001,gid=1000001" 0 0)
(ent "proc" "/proc" "proc" "rw,nosuid,nodev,noexec,relatime" 0 0)
(ent "tmpfs" "/.fortify" "tmpfs" "rw,nosuid,nodev,relatime,size=4k,mode=755,uid=1000001,gid=1000001" 0 0)
(ent "tmpfs" "/dev" "tmpfs" "rw,nosuid,nodev,relatime,mode=755,uid=1000001,gid=1000001" 0 0)
(ent "devtmpfs" "/dev/null" "devtmpfs" "host_passthrough" 0 0)
(ent "devtmpfs" "/dev/zero" "devtmpfs" "host_passthrough" 0 0)
(ent "devtmpfs" "/dev/full" "devtmpfs" "host_passthrough" 0 0)
(ent "devtmpfs" "/dev/random" "devtmpfs" "host_passthrough" 0 0)
(ent "devtmpfs" "/dev/urandom" "devtmpfs" "host_passthrough" 0 0)
(ent "devtmpfs" "/dev/tty" "devtmpfs" "host_passthrough" 0 0)
(ent "devpts" "/dev/pts" "devpts" "rw,nosuid,noexec,relatime,mode=620,ptmxmode=666" 0 0)
(ent "mqueue" "/dev/mqueue" "mqueue" "rw,relatime" 0 0)
(ent "/dev/disk/by-label/nixos" "/bin" "ext4" "ro,nosuid,nodev,relatime" 0 0)
(ent "/dev/disk/by-label/nixos" "/usr/bin" "ext4" "ro,nosuid,nodev,relatime" 0 0)
(ent "overlay" "/nix/store" "overlay" "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on" 0 0)
(ent "overlay" "/run/current-system" "overlay" "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on" 0 0)
(ent "sysfs" "/sys/block" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
(ent "sysfs" "/sys/bus" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
(ent "sysfs" "/sys/class" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
(ent "sysfs" "/sys/dev" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
(ent "sysfs" "/sys/devices" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
(ent "overlay" "/run/opengl-driver" "overlay" "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on" 0 0)
(ent "devtmpfs" "/dev/dri" "devtmpfs" "host_passthrough" 0 0)
(ent "proc" "/.fortify/host-mounts" "proc" "ro,nosuid,nodev,noexec,relatime" 0 0)
(ent "/dev/disk/by-label/nixos" "/.fortify/etc" "ext4" "ro,nosuid,nodev,relatime" 0 0)
(ent "tmpfs" "/run/user" "tmpfs" "rw,nosuid,nodev,relatime,size=1024k,mode=755,uid=1000001,gid=1000001" 0 0)
(ent "tmpfs" "/run/user/65534" "tmpfs" "rw,nosuid,nodev,relatime,size=8192k,mode=755,uid=1000001,gid=1000001" 0 0)
(ent "/dev/disk/by-label/nixos" "/tmp" "ext4" "rw,nosuid,nodev,relatime" 0 0)
(ent "/dev/disk/by-label/nixos" "/var/lib/fortify/u0/a1" "ext4" "rw,nosuid,nodev,relatime" 0 0)
(ent "tmpfs" "/etc/passwd" "tmpfs" "ro,nosuid,nodev,relatime,uid=1000001,gid=1000001" 0 0)
(ent "tmpfs" "/etc/group" "tmpfs" "ro,nosuid,nodev,relatime,uid=1000001,gid=1000001" 0 0)
(ent "/dev/disk/by-label/nixos" "/run/user/65534/wayland-0" "ext4" "ro,nosuid,nodev,relatime" 0 0)
(ent "tmpfs" "/run/user/65534/pulse/native" "tmpfs" "host_passthrough" 0 0)
(ent "/dev/disk/by-label/nixos" "/run/user/65534/bus" "ext4" "ro,nosuid,nodev,relatime" 0 0)
(ent "tmpfs" "/var/run/nscd" "tmpfs" "rw,nosuid,nodev,relatime,size=8k,mode=755,uid=1000001,gid=1000001" 0 0)
(ent "overlay" "/.fortify/sbin/fortify" "overlay" "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on" 0 0)
];
mainFile = writeText "main.go" ''
package main
import "git.gensokyo.uk/security/fortify/test/sandbox"
func main() { sandbox.MustAssertMounts("", "/.fortify/host-mounts", "${writeText "want-mounts.json" (builtins.toJSON wantMounts)}") }
'';
in
buildGoModule {
pname = "check-mounts";
inherit version;
src = ../.;
vendorHash = null;
preBuild = ''
go mod init git.gensokyo.uk/security/fortify/test >& /dev/null
cp ${mainFile} main.go
'';
}

View File

@ -8,79 +8,80 @@ import (
"git.gensokyo.uk/security/fortify/test/sandbox" "git.gensokyo.uk/security/fortify/test/sandbox"
) )
func TestMountinfo(t *testing.T) { func TestMounts(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
sample string sample string
want []*sandbox.MountinfoEntry want []sandbox.Mntent
}{ }{
{"util-linux", `15 20 0:3 / /proc rw,relatime - proc /proc rw {"fpkg", `tmpfs / tmpfs rw,nosuid,nodev,relatime,uid=1000002,gid=1000002 0 0
16 20 0:15 / /sys rw,relatime - sysfs /sys rw proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755 tmpfs /.fortify tmpfs rw,nosuid,nodev,relatime,size=4k,mode=755,uid=1000002,gid=1000002 0 0
18 17 0:10 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000 tmpfs /dev tmpfs rw,nosuid,nodev,relatime,mode=755,uid=1000002,gid=1000002 0 0
19 17 0:16 / /dev/shm rw,relatime - tmpfs tmpfs rw devtmpfs /dev/null devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
20 1 8:4 / / rw,noatime - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered devtmpfs /dev/zero devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755 devtmpfs /dev/full devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
22 21 0:18 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd devtmpfs /dev/random devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
23 21 0:19 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuset devtmpfs /dev/urandom devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
24 21 0:20 / /sys/fs/cgroup/ns rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,ns devtmpfs /dev/tty devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
25 21 0:21 / /sys/fs/cgroup/cpu rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpu devpts /dev/pts devpts rw,nosuid,noexec,relatime,mode=620,ptmxmode=666 0 0
26 21 0:22 / /sys/fs/cgroup/cpuacct rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuacct mqueue /dev/mqueue mqueue rw,relatime 0 0
27 21 0:23 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory /dev/disk/by-label/nixos /nix/store ext4 ro,nosuid,nodev,relatime 0 0
28 21 0:24 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,devices /dev/disk/by-label/nixos /.fortify/app ext4 ro,nosuid,nodev,relatime 0 0
29 21 0:25 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,freezer /dev/disk/by-label/nixos /etc/resolv.conf ext4 ro,nosuid,nodev,relatime 0 0
30 21 0:26 / /sys/fs/cgroup/net_cls rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,net_cls sysfs /sys/block sysfs ro,nosuid,nodev,noexec,relatime 0 0
31 21 0:27 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,blkio sysfs /sys/bus sysfs ro,nosuid,nodev,noexec,relatime 0 0
32 16 0:28 / /sys/kernel/security rw,relatime - autofs systemd-1 rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct sysfs /sys/class sysfs ro,nosuid,nodev,noexec,relatime 0 0
33 17 0:29 / /dev/hugepages rw,relatime - autofs systemd-1 rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct sysfs /sys/dev sysfs ro,nosuid,nodev,noexec,relatime 0 0
34 16 0:30 / /sys/kernel/debug rw,relatime - autofs systemd-1 rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct sysfs /sys/devices sysfs ro,nosuid,nodev,noexec,relatime 0 0
35 15 0:31 / /proc/sys/fs/binfmt_misc rw,relatime - autofs systemd-1 rw,fd=25,pgrp=1,timeout=300,minproto=5,maxproto=5,direct /dev/disk/by-label/nixos /.fortify/nixGL ext4 ro,nosuid,nodev,relatime 0 0
36 17 0:32 / /dev/mqueue rw,relatime - autofs systemd-1 rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct devtmpfs /dev/dri devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
37 15 0:14 / /proc/bus/usb rw,relatime - usbfs /proc/bus/usb rw /dev/disk/by-label/nixos /.fortify/etc ext4 ro,nosuid,nodev,relatime 0 0
38 33 0:33 / /dev/hugepages rw,relatime - hugetlbfs hugetlbfs rw tmpfs /run/user tmpfs rw,nosuid,nodev,relatime,size=1024k,mode=755,uid=1000002,gid=1000002 0 0
39 36 0:12 / /dev/mqueue rw,relatime - mqueue mqueue rw tmpfs /run/user/65534 tmpfs rw,nosuid,nodev,relatime,size=8192k,mode=755,uid=1000002,gid=1000002 0 0
40 20 8:6 / /boot rw,noatime - ext3 /dev/sda6 rw,errors=continue,barrier=0,data=ordered /dev/disk/by-label/nixos /tmp ext4 rw,nosuid,nodev,relatime 0 0
41 20 253:0 / /home/kzak rw,noatime - ext4 /dev/mapper/kzak-home rw,barrier=1,data=ordered /dev/disk/by-label/nixos /data/data/org.codeberg.dnkl.foot ext4 rw,nosuid,nodev,relatime 0 0
42 35 0:34 / /proc/sys/fs/binfmt_misc rw,relatime - binfmt_misc none rw tmpfs /etc/passwd tmpfs ro,nosuid,nodev,relatime,uid=1000002,gid=1000002 0 0
43 16 0:35 / /sys/fs/fuse/connections rw,relatime - fusectl fusectl rw tmpfs /etc/group tmpfs ro,nosuid,nodev,relatime,uid=1000002,gid=1000002 0 0
44 41 0:36 / /home/kzak/.gvfs rw,nosuid,nodev,relatime - fuse.gvfs-fuse-daemon gvfs-fuse-daemon rw,user_id=500,group_id=500 /dev/disk/by-label/nixos /run/user/65534/wayland-0 ext4 ro,nosuid,nodev,relatime 0 0
45 20 0:37 / /var/lib/nfs/rpc_pipefs rw,relatime - rpc_pipefs sunrpc rw tmpfs /run/user/65534/pulse/native tmpfs ro,nosuid,nodev,relatime,size=98784k,nr_inodes=24696,mode=700,uid=1000,gid=100 0 0
47 20 0:38 / /mnt/sounds rw,relatime - cifs //foo.home/bar/ rw,unc=\\foo.home\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344 /dev/disk/by-label/nixos /run/user/65534/bus ext4 ro,nosuid,nodev,relatime 0 0
49 20 0:56 / /mnt/test/foobar rw,relatime,nosymfollow shared:323 - tmpfs tmpfs rw`, []*sandbox.MountinfoEntry{ overlay /.fortify/sbin/fortify overlay ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on 0 0
e(15, 20, "/", "/proc", "rw,relatime", "proc", "/proc", "rw"), `, []sandbox.Mntent{
e(16, 20, "/", "/sys", "rw,relatime", "sysfs", "/sys", "rw"), {"tmpfs", "/", "tmpfs", "rw,nosuid,nodev,relatime,uid=1000002,gid=1000002", 0, 0},
e(17, 20, "/", "/dev", "rw,relatime", "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755"), {"proc", "/proc", "proc", "rw,nosuid,nodev,noexec,relatime", 0, 0},
e(18, 17, "/", "/dev/pts", "rw,relatime", "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000"), {"tmpfs", "/.fortify", "tmpfs", "rw,nosuid,nodev,relatime,size=4k,mode=755,uid=1000002,gid=1000002", 0, 0},
e(19, 17, "/", "/dev/shm", "rw,relatime", "tmpfs", "tmpfs", "rw"), {"tmpfs", "/dev", "tmpfs", "rw,nosuid,nodev,relatime,mode=755,uid=1000002,gid=1000002", 0, 0},
e(20, 1, "/", "/", "rw,noatime", "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered"), {"devtmpfs", "/dev/null", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
e(21, 16, "/", "/sys/fs/cgroup", "rw,nosuid,nodev,noexec,relatime", "tmpfs", "tmpfs", "rw,mode=755"), {"devtmpfs", "/dev/zero", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
e(22, 21, "/", "/sys/fs/cgroup/systemd", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd"), {"devtmpfs", "/dev/full", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
e(23, 21, "/", "/sys/fs/cgroup/cpuset", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpuset"), {"devtmpfs", "/dev/random", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
e(24, 21, "/", "/sys/fs/cgroup/ns", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,ns"), {"devtmpfs", "/dev/urandom", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
e(25, 21, "/", "/sys/fs/cgroup/cpu", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpu"), {"devtmpfs", "/dev/tty", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
e(26, 21, "/", "/sys/fs/cgroup/cpuacct", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpuacct"), {"devpts", "/dev/pts", "devpts", "rw,nosuid,noexec,relatime,mode=620,ptmxmode=666", 0, 0},
e(27, 21, "/", "/sys/fs/cgroup/memory", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,memory"), {"mqueue", "/dev/mqueue", "mqueue", "rw,relatime", 0, 0},
e(28, 21, "/", "/sys/fs/cgroup/devices", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,devices"), {"/dev/disk/by-label/nixos", "/nix/store", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
e(29, 21, "/", "/sys/fs/cgroup/freezer", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,freezer"), {"/dev/disk/by-label/nixos", "/.fortify/app", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
e(30, 21, "/", "/sys/fs/cgroup/net_cls", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,net_cls"), {"/dev/disk/by-label/nixos", "/etc/resolv.conf", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
e(31, 21, "/", "/sys/fs/cgroup/blkio", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,blkio"), {"sysfs", "/sys/block", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
e(32, 16, "/", "/sys/kernel/security", "rw,relatime", "autofs", "systemd-1", "rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"), {"sysfs", "/sys/bus", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
e(33, 17, "/", "/dev/hugepages", "rw,relatime", "autofs", "systemd-1", "rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"), {"sysfs", "/sys/class", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
e(34, 16, "/", "/sys/kernel/debug", "rw,relatime", "autofs", "systemd-1", "rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"), {"sysfs", "/sys/dev", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
e(35, 15, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", "autofs", "systemd-1", "rw,fd=25,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"), {"sysfs", "/sys/devices", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
e(36, 17, "/", "/dev/mqueue", "rw,relatime", "autofs", "systemd-1", "rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"), {"/dev/disk/by-label/nixos", "/.fortify/nixGL", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
e(37, 15, "/", "/proc/bus/usb", "rw,relatime", "usbfs", "/proc/bus/usb", "rw"), {"devtmpfs", "/dev/dri", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
e(38, 33, "/", "/dev/hugepages", "rw,relatime", "hugetlbfs", "hugetlbfs", "rw"), {"/dev/disk/by-label/nixos", "/.fortify/etc", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
e(39, 36, "/", "/dev/mqueue", "rw,relatime", "mqueue", "mqueue", "rw"), {"tmpfs", "/run/user", "tmpfs", "rw,nosuid,nodev,relatime,size=1024k,mode=755,uid=1000002,gid=1000002", 0, 0},
e(40, 20, "/", "/boot", "rw,noatime", "ext3", "/dev/sda6", "rw,errors=continue,barrier=0,data=ordered"), {"tmpfs", "/run/user/65534", "tmpfs", "rw,nosuid,nodev,relatime,size=8192k,mode=755,uid=1000002,gid=1000002", 0, 0},
e(41, 20, "/", "/home/kzak", "rw,noatime", "ext4", "/dev/mapper/kzak-home", "rw,barrier=1,data=ordered"), {"/dev/disk/by-label/nixos", "/tmp", "ext4", "rw,nosuid,nodev,relatime", 0, 0},
e(42, 35, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", "binfmt_misc", "none", "rw"), {"/dev/disk/by-label/nixos", "/data/data/org.codeberg.dnkl.foot", "ext4", "rw,nosuid,nodev,relatime", 0, 0},
e(43, 16, "/", "/sys/fs/fuse/connections", "rw,relatime", "fusectl", "fusectl", "rw"), {"tmpfs", "/etc/passwd", "tmpfs", "ro,nosuid,nodev,relatime,uid=1000002,gid=1000002", 0, 0},
e(44, 41, "/", "/home/kzak/.gvfs", "rw,nosuid,nodev,relatime", "fuse.gvfs-fuse-daemon", "gvfs-fuse-daemon", "rw,user_id=500,group_id=500"), {"tmpfs", "/etc/group", "tmpfs", "ro,nosuid,nodev,relatime,uid=1000002,gid=1000002", 0, 0},
e(45, 20, "/", "/var/lib/nfs/rpc_pipefs", "rw,relatime", "rpc_pipefs", "sunrpc", "rw"), {"/dev/disk/by-label/nixos", "/run/user/65534/wayland-0", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
e(47, 20, "/", "/mnt/sounds", "rw,relatime", "cifs", "//foo.home/bar/", "rw,unc=\\\\foo.home\\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344"), {"tmpfs", "/run/user/65534/pulse/native", "tmpfs", "ro,nosuid,nodev,relatime,size=98784k,nr_inodes=24696,mode=700,uid=1000,gid=100", 0, 0},
e(49, 20, "/", "/mnt/test/foobar", "rw,relatime,nosymfollow", "tmpfs", "tmpfs", "rw"), {"/dev/disk/by-label/nixos", "/run/user/65534/bus", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
{"overlay", "/.fortify/sbin/fortify", "overlay", "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on", 0, 0},
}}, }},
} }
@ -91,33 +92,27 @@ func TestMountinfo(t *testing.T) {
} }
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
m := sandbox.NewMountinfo(name)
if err := m.Parse(); err != nil {
t.Fatalf("Parse: error = %v", err)
}
i := 0 i := 0
for ent := range m.Entries() { if err := sandbox.IterMounts(name, func(e *sandbox.Mntent) {
if i == len(tc.want) { if i == len(tc.want) {
t.Errorf("Entries: got more than %d entries", i) t.Errorf("IterMounts: got more than %d entries", i)
t.FailNow() t.FailNow()
} }
if !ent.EqualWithIgnore(tc.want[i], "\x00") { if *e != tc.want[i] {
t.Errorf("Entries: entry %d\n got: %#v\nwant: %#v", i, t.Errorf("IterMounts: entry %d\n got: %s\nwant: %s", i,
ent, &tc.want[i]) e, &tc.want[i])
t.FailNow() t.FailNow()
} else {
t.Logf("%s", ent)
} }
i++ i++
}); err != nil {
t.Fatalf("IterMounts: error = %v", err)
} }
})
if err := m.Err(); err != nil { t.Run(tc.name+" assert", func(t *testing.T) {
t.Fatalf("Mountinfo: error = %v", err) oldFatal := sandbox.SwapFatal(t.Fatalf)
} t.Cleanup(func() { sandbox.SwapFatal(oldFatal) })
sandbox.MustAssertMounts(name, name, sandbox.MustWantFile(t, tc.want))
m.Unref()
}) })
if err := os.Remove(name); err != nil { if err := os.Remove(name); err != nil {
@ -125,18 +120,3 @@ func TestMountinfo(t *testing.T) {
} }
} }
} }
func e(
id, parent int, root, target, vfsOptstr string, fsType, source, fsOptstr string,
) *sandbox.MountinfoEntry {
return &sandbox.MountinfoEntry{
ID: id,
Parent: parent,
Root: root,
Target: target,
VfsOptstr: vfsOptstr,
FsType: fsType,
Source: source,
FsOptstr: fsOptstr,
}
}

27
test/sandbox/seccomp.nix Normal file
View File

@ -0,0 +1,27 @@
{
writeText,
buildGoModule,
version,
}:
let
mainFile = writeText "main.go" ''
package main
import "git.gensokyo.uk/security/fortify/test/sandbox"
func main() { sandbox.MustAssertSeccomp() }
'';
in
buildGoModule {
pname = "check-seccomp";
inherit version;
src = ../.;
vendorHash = null;
preBuild = ''
go mod init git.gensokyo.uk/security/fortify/test >& /dev/null
cp ${mainFile} main.go
'';
}

View File

@ -62,12 +62,9 @@ def check_state(name, enablements):
config = instance['config'] config = instance['config']
command = f"{name}-start" if len(config['command']) != 1 or not (config['command'][0].startswith("/nix/store/")) or not (
if not (config['path'].startswith("/nix/store/")) or not (config['path'].endswith(command)): config['command'][0].endswith(f"{name}-start")):
raise Exception(f"unexpected path {config['path']}") raise Exception(f"unexpected command {instance['config']['command']}")
if len(config['args']) != 1 or config['args'][0] != command:
raise Exception(f"unexpected args {config['args']}")
if config['confinement']['enablements'] != enablements: if config['confinement']['enablements'] != enablements:
raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}") raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
@ -105,26 +102,9 @@ if denyOutput != "fsu: uid 1001 is not in the fsurc file\n":
if denyOutputVerbose != "fsu: uid 1001 is not in the fsurc file\nfortify: *cannot obtain uid from fsu: permission denied\n": 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}") raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}")
# Check sandbox outcome: # Check sandbox state:
check_offset = 0 swaymsg("exec check-sandbox")
def check_sandbox(name): machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/sandbox-ok", timeout=15)
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
def tmpdir_path(offset, name):
return f"/tmp/fortify.1000/tmpdir/{aid(offset)}/{name}"
# Start fortify permissive defaults outside Wayland session: # Start fortify permissive defaults outside Wayland session:
print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare")) print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare"))
@ -166,23 +146,23 @@ machine.succeed("pkill -9 mako")
# Start app (foot) with Wayland enablement: # Start app (foot) with Wayland enablement:
swaymsg("exec ne-foot") swaymsg("exec ne-foot")
wait_for_window(f"u0_a{aid(0)}@machine") wait_for_window("u0_a2@machine")
machine.send_chars("clear; wayland-info && touch /tmp/client-ok\n") machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
machine.wait_for_file(tmpdir_path(0, "client-ok"), timeout=10) machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-client", timeout=10)
collect_state_ui("foot_wayland") collect_state_ui("foot_wayland")
check_state("ne-foot", 1) check_state("ne-foot", 1)
# Verify acl on XDG_RUNTIME_DIR: # Verify acl on XDG_RUNTIME_DIR:
print(machine.succeed(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {aid(0) + 1000000}")) print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002"))
machine.send_chars("exit\n") machine.send_chars("exit\n")
machine.wait_until_fails("pgrep foot", timeout=5) machine.wait_until_fails("pgrep foot", timeout=5)
# Verify acl cleanup on XDG_RUNTIME_DIR: # 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) machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002", timeout=5)
# Start app (foot) with Wayland enablement from a terminal: # 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'") 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") wait_for_window("u0_a2@machine")
machine.send_chars("clear; wayland-info && touch /tmp/term-ok\n") machine.send_chars("clear; wayland-info && touch /tmp/success-client-term\n")
machine.wait_for_file(tmpdir_path(0, "term-ok"), timeout=10) machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-client-term", timeout=10)
machine.wait_for_file("/tmp/ps-show-ok", timeout=5) machine.wait_for_file("/tmp/ps-show-ok", timeout=5)
collect_state_ui("foot_wayland_term") collect_state_ui("foot_wayland_term")
check_state("ne-foot", 1) check_state("ne-foot", 1)
@ -193,9 +173,9 @@ machine.wait_until_fails("pgrep foot", timeout=5)
# Test PulseAudio (fortify does not support PipeWire yet): # Test PulseAudio (fortify does not support PipeWire yet):
swaymsg("exec pa-foot") swaymsg("exec pa-foot")
wait_for_window(f"u0_a{aid(1)}@machine") wait_for_window("u0_a3@machine")
machine.send_chars("clear; pactl info && touch /tmp/pulse-ok\n") machine.send_chars("clear; pactl info && touch /tmp/success-pulse\n")
machine.wait_for_file(tmpdir_path(1, "pulse-ok"), timeout=15) machine.wait_for_file("/tmp/fortify.1000/tmpdir/3/success-pulse", timeout=10)
collect_state_ui("pulse_wayland") collect_state_ui("pulse_wayland")
check_state("pa-foot", 9) check_state("pa-foot", 9)
machine.send_chars("exit\n") machine.send_chars("exit\n")
@ -203,9 +183,9 @@ machine.wait_until_fails("pgrep foot", timeout=5)
# Test XWayland (foot does not support X): # Test XWayland (foot does not support X):
swaymsg("exec x11-alacritty") swaymsg("exec x11-alacritty")
wait_for_window(f"u0_a{aid(2)}@machine") wait_for_window("u0_a4@machine")
machine.send_chars("clear; glinfo && touch /tmp/x11-ok\n") machine.send_chars("clear; glinfo && touch /tmp/success-client-x11\n")
machine.wait_for_file(tmpdir_path(2, "x11-ok"), timeout=10) machine.wait_for_file("/tmp/fortify.1000/tmpdir/4/success-client-x11", timeout=10)
collect_state_ui("alacritty_x11") collect_state_ui("alacritty_x11")
check_state("x11-alacritty", 2) check_state("x11-alacritty", 2)
machine.send_chars("exit\n") machine.send_chars("exit\n")
@ -213,17 +193,17 @@ machine.wait_until_fails("pgrep alacritty", timeout=5)
# Start app (foot) with direct Wayland access: # Start app (foot) with direct Wayland access:
swaymsg("exec da-foot") swaymsg("exec da-foot")
wait_for_window(f"u0_a{aid(3)}@machine") wait_for_window("u0_a5@machine")
machine.send_chars("clear; wayland-info && touch /tmp/direct-ok\n") machine.send_chars("clear; wayland-info && touch /tmp/success-direct\n")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/5/success-direct", timeout=10)
collect_state_ui("foot_direct") collect_state_ui("foot_direct")
machine.wait_for_file(tmpdir_path(3, "direct-ok"), timeout=10)
check_state("da-foot", 1) check_state("da-foot", 1)
# Verify acl on XDG_RUNTIME_DIR: # Verify acl on XDG_RUNTIME_DIR:
print(machine.succeed(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {aid(3) + 1000000}")) print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000005"))
machine.send_chars("exit\n") machine.send_chars("exit\n")
machine.wait_until_fails("pgrep foot", timeout=5) machine.wait_until_fails("pgrep foot", timeout=5)
# Verify acl cleanup on XDG_RUNTIME_DIR: # Verify acl cleanup on XDG_RUNTIME_DIR:
machine.wait_until_fails(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {aid(3) + 1000000}", timeout=5) machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000005", timeout=5)
# Test syscall filter: # Test syscall filter:
print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure")) print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure"))

View File

@ -94,7 +94,7 @@ func bindRawConn(done chan struct{}, rc syscall.RawConn, p, appID, instanceID st
// keep socket alive until done is requested // keep socket alive until done is requested
<-done <-done
runtime.KeepAlive(syncPipe[1]) runtime.KeepAlive(syncPipe[1].Fd())
}); err != nil { }); err != nil {
setupDone <- err setupDone <- err
} }
@ -107,7 +107,7 @@ func bindRawConn(done chan struct{}, rc syscall.RawConn, p, appID, instanceID st
return syncPipe[1], <-setupDone return syncPipe[1], <-setupDone
} }
func bind(fd uintptr, p, appID, instanceID string, syncFd uintptr) error { func bind(fd uintptr, p, appID, instanceID string, syncFD uintptr) error {
// ensure p is available // ensure p is available
if f, err := os.Create(p); err != nil { if f, err := os.Create(p); err != nil {
return err return err
@ -117,5 +117,5 @@ func bind(fd uintptr, p, appID, instanceID string, syncFd uintptr) error {
return err return err
} }
return bindWaylandFd(p, fd, appID, instanceID, syncFd) return bindWaylandFd(p, fd, appID, instanceID, syncFD)
} }

View File

@ -25,11 +25,11 @@ var resErr = [...]error{
2: errors.New("wp_security_context_v1 not available"), 2: errors.New("wp_security_context_v1 not available"),
} }
func bindWaylandFd(socketPath string, fd uintptr, appID, instanceID string, syncFd uintptr) error { func bindWaylandFd(socketPath string, fd uintptr, appID, instanceID string, syncFD uintptr) error {
if hasNull(appID) || hasNull(instanceID) { if hasNull(appID) || hasNull(instanceID) {
return ErrContainsNull return ErrContainsNull
} }
res := C.f_bind_wayland_fd(C.CString(socketPath), C.int(fd), C.CString(appID), C.CString(instanceID), C.int(syncFd)) res := C.f_bind_wayland_fd(C.CString(socketPath), C.int(fd), C.CString(appID), C.CString(instanceID), C.int(syncFD))
return resErr[int32(res)] return resErr[int32(res)]
} }