Compare commits
No commits in common. "5c4058d5ac7e7944973ca8216c258fc50c194e22" and "24618ab9a1524e8b8986a9bf67667288e642fcf1" have entirely different histories.
5c4058d5ac
...
24618ab9a1
@ -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)
|
@ -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.
|
||||||
|
@ -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 {
|
||||||
|
@ -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']}")
|
||||||
|
@ -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
12
flake.lock
generated
@ -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
100
flake.nix
@ -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
|
||||||
'';
|
'';
|
||||||
|
@ -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].
|
||||||
|
@ -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},
|
||||||
|
227
fst/sandbox.go
227
fst/sandbox.go
@ -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
2
go.mod
@ -1,3 +1,3 @@
|
|||||||
module git.gensokyo.uk/security/fortify
|
module git.gensokyo.uk/security/fortify
|
||||||
|
|
||||||
go 1.23
|
go 1.22
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -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 {
|
||||||
|
18
internal/app/init0/early.go
Normal file
18
internal/app/init0/early.go
Normal 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
165
internal/app/init0/main.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
internal/app/init0/payload.go
Normal file
13
internal/app/init0/payload.go
Normal 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
|
||||||
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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, ¶ms, 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())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
23
internal/app/shim/payload.go
Normal file
23
internal/app/shim/payload.go
Normal 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
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
18
main.go
@ -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)
|
||||||
}
|
}
|
||||||
|
20
nixos.nix
20
nixos.nix
@ -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;
|
||||||
|
31
options.nix
31
options.nix
@ -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;
|
||||||
|
19
package.nix
19
package.nix
@ -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;
|
|
||||||
}
|
}
|
||||||
|
3
parse.go
3
parse.go
@ -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) {
|
||||||
|
12
print.go
12
print.go
@ -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",
|
||||||
|
@ -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
6
sandbox/const.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package sandbox
|
||||||
|
|
||||||
|
const (
|
||||||
|
PR_SET_NO_NEW_PRIVS = 0x26
|
||||||
|
CAP_SYS_ADMIN = 0x15
|
||||||
|
)
|
@ -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)},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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(¶ms.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(¶ms.InitParams); err != nil {
|
||||||
if err := op.apply(¶ms.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() {
|
||||||
|
156
sandbox/mount.go
156
sandbox/mount.go
@ -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)
|
|
||||||
}
|
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
|
@ -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.
|
||||||
|
@ -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])
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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`
|
|
||||||
)
|
|
@ -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
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
||||||
|
@ -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'
|
||||||
'')
|
'')
|
||||||
];
|
];
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
'';
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -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;
|
|
||||||
};
|
|
||||||
}
|
|
@ -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;
|
|
||||||
};
|
|
||||||
}
|
|
@ -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
|
||||||
''
|
''
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
'';
|
||||||
}
|
}
|
@ -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}))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -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
79
test/sandbox/mount.nix
Normal 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
|
||||||
|
'';
|
||||||
|
}
|
@ -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
27
test/sandbox/seccomp.nix
Normal 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
|
||||||
|
'';
|
||||||
|
}
|
70
test/test.py
70
test/test.py
@ -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"))
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
4
wl/wl.go
4
wl/wl.go
@ -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)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user