Compare commits
29 Commits
71135f339a
...
d8e9d71f87
Author | SHA1 | Date | |
---|---|---|---|
d8e9d71f87 | |||
558974b996 | |||
4de4049713 | |||
2d4cabe786 | |||
80f9b62d25 | |||
673b648bd3 | |||
45ad788c6d | |||
56539d8db5 | |||
840ceb615a | |||
741d011543 | |||
d050b3de25 | |||
5de28800ad | |||
8e50293ab7 | |||
12c6d66bfd | |||
d7d2bd33ed | |||
c21a4cff14 | |||
4fa38d6063 | |||
6d4ac3d9fd | |||
a5d2f040fb | |||
c62689e17f | |||
39dc8e7bd8 | |||
5a732d153e | |||
b4549c72be | |||
1818dc3a4c | |||
65094b63cd | |||
f0a082ec84 | |||
751aa350ee | |||
e6cd2bb2a8 | |||
0fb72e5d99 |
@ -22,6 +22,23 @@ jobs:
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
fpkg:
|
||||
name: Fpkg
|
||||
runs-on: nix
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run NixOS test
|
||||
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.fpkg
|
||||
|
||||
- name: Upload test output
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: "fpkg-vm-output"
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
race:
|
||||
name: Data race detector
|
||||
runs-on: nix
|
||||
@ -43,6 +60,7 @@ jobs:
|
||||
name: Flake checks
|
||||
needs:
|
||||
- fortify
|
||||
- fpkg
|
||||
- race
|
||||
runs-on: nix
|
||||
steps:
|
||||
|
@ -7,8 +7,9 @@
|
||||
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
closureInfo,
|
||||
writeScript,
|
||||
writeScriptBin,
|
||||
runtimeShell,
|
||||
writeText,
|
||||
symlinkJoin,
|
||||
@ -16,12 +17,15 @@
|
||||
runCommand,
|
||||
fetchFromGitHub,
|
||||
|
||||
zstd,
|
||||
nix,
|
||||
sqlite,
|
||||
|
||||
name ? throw "name is required",
|
||||
version ? throw "version is required",
|
||||
pname ? "${name}-${version}",
|
||||
modules ? [ ],
|
||||
nixosModules ? [ ],
|
||||
script ? ''
|
||||
exec "$SHELL" "$@"
|
||||
'',
|
||||
@ -73,6 +77,8 @@ let
|
||||
etc.nixpkgs.source = nixpkgs.outPath;
|
||||
systemPackages = [ pkgs.nix ];
|
||||
};
|
||||
|
||||
imports = nixosModules;
|
||||
};
|
||||
nixos = nixpkgs.lib.nixosSystem {
|
||||
inherit system;
|
||||
@ -165,11 +171,7 @@ let
|
||||
broadcast = { };
|
||||
});
|
||||
|
||||
enablements =
|
||||
(if allow_wayland then 1 else 0)
|
||||
+ (if allow_x11 then 2 else 0)
|
||||
+ (if allow_dbus then 4 else 0)
|
||||
+ (if allow_pulse then 8 else 0);
|
||||
enablements = (if allow_wayland then 1 else 0) + (if allow_x11 then 2 else 0) + (if allow_dbus then 4 else 0) + (if allow_pulse then 8 else 0);
|
||||
|
||||
mesa = if gpu then mesaWrappers else null;
|
||||
nix_gl = if gpu then nixGL else null;
|
||||
@ -178,26 +180,73 @@ let
|
||||
};
|
||||
in
|
||||
|
||||
writeScriptBin "build-fpkg-${pname}" ''
|
||||
#!${runtimeShell} -el
|
||||
OUT="$(mktemp -d)"
|
||||
TAR="$(mktemp -u)"
|
||||
set -x
|
||||
stdenv.mkDerivation {
|
||||
name = "${pname}.pkg";
|
||||
inherit version;
|
||||
__structuredAttrs = true;
|
||||
|
||||
nix copy --no-check-sigs --to "$OUT" "${nix}" "${nixos.config.system.build.toplevel}"
|
||||
nix store --store "$OUT" optimise
|
||||
chmod -R +r "$OUT/nix/var"
|
||||
nix copy --no-check-sigs --to "file://$OUT/res?compression=zstd&compression-level=19¶llel-compression=true" \
|
||||
"${homeManagerConfiguration.activationPackage}" \
|
||||
"${launcher}" ${if gpu then "${mesaWrappers} ${nixGL}" else ""}
|
||||
mkdir -p "$OUT/etc"
|
||||
tar -C "$OUT/etc" -xf "${etc}/etc.tar"
|
||||
cp "${writeText "bundle.json" info}" "$OUT/bundle.json"
|
||||
nativeBuildInputs = [
|
||||
zstd
|
||||
nix
|
||||
sqlite
|
||||
];
|
||||
|
||||
# creating an intermediate file improves zstd performance
|
||||
tar -C "$OUT" -cf "$TAR" .
|
||||
chmod +w -R "$OUT" && rm -rf "$OUT"
|
||||
buildCommand = ''
|
||||
NIX_ROOT="$(mktemp -d)"
|
||||
export USER="nobody"
|
||||
|
||||
zstd -T0 -19 -fo "${pname}.pkg" "$TAR"
|
||||
rm "$TAR"
|
||||
''
|
||||
# create bootstrap store
|
||||
bootstrapClosureInfo="${
|
||||
closureInfo {
|
||||
rootPaths = [
|
||||
nix
|
||||
nixos.config.system.build.toplevel
|
||||
];
|
||||
}
|
||||
}"
|
||||
echo "copying bootstrap store paths..."
|
||||
mkdir -p "$NIX_ROOT/nix/store"
|
||||
xargs -n 1 -a "$bootstrapClosureInfo/store-paths" cp -at "$NIX_ROOT/nix/store/"
|
||||
NIX_REMOTE="local?root=$NIX_ROOT" nix-store --load-db < "$bootstrapClosureInfo/registration"
|
||||
NIX_REMOTE="local?root=$NIX_ROOT" nix-store --optimise
|
||||
sqlite3 "$NIX_ROOT/nix/var/nix/db/db.sqlite" "UPDATE ValidPaths SET registrationTime = ''${SOURCE_DATE_EPOCH}"
|
||||
chmod -R +r "$NIX_ROOT/nix/var"
|
||||
|
||||
# create binary cache
|
||||
closureInfo="${
|
||||
closureInfo {
|
||||
rootPaths =
|
||||
[
|
||||
homeManagerConfiguration.activationPackage
|
||||
launcher
|
||||
]
|
||||
++ optionals gpu [
|
||||
mesaWrappers
|
||||
nixGL
|
||||
];
|
||||
}
|
||||
}"
|
||||
echo "copying application paths..."
|
||||
TMP_STORE="$(mktemp -d)"
|
||||
mkdir -p "$TMP_STORE/nix/store"
|
||||
xargs -n 1 -a "$closureInfo/store-paths" cp -at "$TMP_STORE/nix/store/"
|
||||
NIX_REMOTE="local?root=$TMP_STORE" nix-store --load-db < "$closureInfo/registration"
|
||||
sqlite3 "$TMP_STORE/nix/var/nix/db/db.sqlite" "UPDATE ValidPaths SET registrationTime = ''${SOURCE_DATE_EPOCH}"
|
||||
NIX_REMOTE="local?root=$TMP_STORE" nix --offline --extra-experimental-features nix-command \
|
||||
--verbose --log-format raw-with-logs \
|
||||
copy --all --no-check-sigs --to \
|
||||
"file://$NIX_ROOT/res?compression=zstd&compression-level=19¶llel-compression=true"
|
||||
|
||||
# package /etc
|
||||
mkdir -p "$NIX_ROOT/etc"
|
||||
tar -C "$NIX_ROOT/etc" -xf "${etc}/etc.tar"
|
||||
|
||||
# write metadata
|
||||
cp "${writeText "bundle.json" info}" "$NIX_ROOT/bundle.json"
|
||||
|
||||
# create an intermediate file to improve zstd performance
|
||||
INTER="$(mktemp)"
|
||||
tar -C "$NIX_ROOT" -cf "$INTER" .
|
||||
zstd -T0 -19 -fo "$out" "$INTER"
|
||||
'';
|
||||
}
|
||||
|
@ -1,191 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
func actionInstall(args []string) {
|
||||
set := flag.NewFlagSet("install", flag.ExitOnError)
|
||||
var (
|
||||
dropShellInstall bool
|
||||
dropShellActivate bool
|
||||
)
|
||||
set.BoolVar(&dropShellInstall, "si", false, "Drop to a shell on installation")
|
||||
set.BoolVar(&dropShellActivate, "sa", false, "Drop to a shell on activation")
|
||||
|
||||
// Ignore errors; set is set for ExitOnError.
|
||||
_ = set.Parse(args)
|
||||
|
||||
args = set.Args()
|
||||
|
||||
if len(args) != 1 {
|
||||
log.Fatal("invalid argument")
|
||||
}
|
||||
pkgPath := args[0]
|
||||
if !path.IsAbs(pkgPath) {
|
||||
if dir, err := os.Getwd(); err != nil {
|
||||
log.Fatalf("cannot get current directory: %v", err)
|
||||
} else {
|
||||
pkgPath = path.Join(dir, pkgPath)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Look up paths to programs started by fpkg.
|
||||
This is done here to ease error handling as cleanup is not yet required.
|
||||
*/
|
||||
|
||||
var (
|
||||
_ = lookPath("zstd")
|
||||
tar = lookPath("tar")
|
||||
chmod = lookPath("chmod")
|
||||
rm = lookPath("rm")
|
||||
)
|
||||
|
||||
/*
|
||||
Extract package and set up for cleanup.
|
||||
*/
|
||||
|
||||
var workDir string
|
||||
if p, err := os.MkdirTemp("", "fpkg.*"); err != nil {
|
||||
log.Fatalf("cannot create temporary directory: %v", err)
|
||||
} else {
|
||||
workDir = p
|
||||
}
|
||||
cleanup := func() {
|
||||
// should be faster than a native implementation
|
||||
mustRun(chmod, "-R", "+w", workDir)
|
||||
mustRun(rm, "-rf", workDir)
|
||||
}
|
||||
beforeRunFail.Store(&cleanup)
|
||||
|
||||
mustRun(tar, "-C", workDir, "-xf", pkgPath)
|
||||
|
||||
/*
|
||||
Parse bundle and app metadata, do pre-install checks.
|
||||
*/
|
||||
|
||||
bundle := loadBundleInfo(path.Join(workDir, "bundle.json"), cleanup)
|
||||
pathSet := pathSetByApp(bundle.ID)
|
||||
|
||||
app := bundle
|
||||
if s, err := os.Stat(pathSet.metaPath); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
cleanup()
|
||||
log.Fatalf("cannot access %q: %v", pathSet.metaPath, err)
|
||||
}
|
||||
// did not modify app, clean installation condition met later
|
||||
} else if s.IsDir() {
|
||||
cleanup()
|
||||
log.Fatalf("metadata path %q is not a file", pathSet.metaPath)
|
||||
} else {
|
||||
app = loadBundleInfo(pathSet.metaPath, cleanup)
|
||||
if app.ID != bundle.ID {
|
||||
cleanup()
|
||||
log.Fatalf("app %q claims to have identifier %q", bundle.ID, app.ID)
|
||||
}
|
||||
// sec: should verify credentials
|
||||
}
|
||||
|
||||
if app != bundle {
|
||||
// do not try to re-install
|
||||
if app.NixGL == bundle.NixGL &&
|
||||
app.CurrentSystem == bundle.CurrentSystem &&
|
||||
app.Launcher == bundle.Launcher &&
|
||||
app.ActivationPackage == bundle.ActivationPackage {
|
||||
cleanup()
|
||||
log.Printf("package %q is identical to local application %q", pkgPath, app.ID)
|
||||
internal.Exit(0)
|
||||
}
|
||||
|
||||
// AppID determines uid
|
||||
if app.AppID != bundle.AppID {
|
||||
cleanup()
|
||||
log.Fatalf("package %q app id %d differs from installed %d", pkgPath, bundle.AppID, app.AppID)
|
||||
}
|
||||
|
||||
// sec: should compare version string
|
||||
fmsg.Verbosef("installing application %q version %q over local %q", bundle.ID, bundle.Version, app.Version)
|
||||
} else {
|
||||
fmsg.Verbosef("application %q clean installation", bundle.ID)
|
||||
// sec: should install credentials
|
||||
}
|
||||
|
||||
/*
|
||||
Setup steps for files owned by the target user.
|
||||
*/
|
||||
|
||||
withCacheDir("install", []string{
|
||||
// export inner bundle path in the environment
|
||||
"export BUNDLE=" + fst.Tmp + "/bundle",
|
||||
// replace inner /etc
|
||||
"mkdir -p etc",
|
||||
"chmod -R +w etc",
|
||||
"rm -rf etc",
|
||||
"cp -dRf $BUNDLE/etc etc",
|
||||
// replace inner /nix
|
||||
"mkdir -p nix",
|
||||
"chmod -R +w nix",
|
||||
"rm -rf nix",
|
||||
"cp -dRf /nix nix",
|
||||
// copy from binary cache
|
||||
"nix copy --offline --no-check-sigs --all --from file://$BUNDLE/res --to $PWD",
|
||||
// deduplicate nix store
|
||||
"nix store --offline --store $PWD optimise",
|
||||
// make cache directory world-readable for autoetc
|
||||
"chmod 0755 .",
|
||||
}, workDir, bundle, pathSet, dropShellInstall, cleanup)
|
||||
|
||||
if bundle.GPU {
|
||||
withCacheDir("mesa-wrappers", []string{
|
||||
// link nixGL mesa wrappers
|
||||
"mkdir -p nix/.nixGL",
|
||||
"ln -s " + bundle.Mesa + "/bin/nixGLIntel nix/.nixGL/nixGL",
|
||||
"ln -s " + bundle.Mesa + "/bin/nixVulkanIntel nix/.nixGL/nixVulkan",
|
||||
}, workDir, bundle, pathSet, false, cleanup)
|
||||
}
|
||||
|
||||
/*
|
||||
Activate home-manager generation.
|
||||
*/
|
||||
|
||||
withNixDaemon("activate", []string{
|
||||
// clean up broken links
|
||||
"mkdir -p .local/state/{nix,home-manager}",
|
||||
"chmod -R +w .local/state/{nix,home-manager}",
|
||||
"rm -rf .local/state/{nix,home-manager}",
|
||||
// run activation script
|
||||
bundle.ActivationPackage + "/activate",
|
||||
}, false, func(config *fst.Config) *fst.Config { return config }, bundle, pathSet, dropShellActivate, cleanup)
|
||||
|
||||
/*
|
||||
Installation complete. Write metadata to block re-installs or downgrades.
|
||||
*/
|
||||
|
||||
// serialise metadata to ensure consistency
|
||||
if f, err := os.OpenFile(pathSet.metaPath+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
|
||||
cleanup()
|
||||
log.Fatalf("cannot create metadata file: %v", err)
|
||||
} else if err = json.NewEncoder(f).Encode(bundle); err != nil {
|
||||
cleanup()
|
||||
log.Fatalf("cannot write metadata: %v", err)
|
||||
} else if err = f.Close(); err != nil {
|
||||
log.Printf("cannot close metadata file: %v", err)
|
||||
// not fatal
|
||||
}
|
||||
|
||||
if err := os.Rename(pathSet.metaPath+"~", pathSet.metaPath); err != nil {
|
||||
cleanup()
|
||||
log.Fatalf("cannot rename metadata file: %v", err)
|
||||
}
|
||||
|
||||
cleanup()
|
||||
}
|
386
cmd/fpkg/main.go
386
cmd/fpkg/main.go
@ -1,50 +1,402 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"syscall"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/command"
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||
"git.gensokyo.uk/security/fortify/helper/seccomp"
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
init0 "git.gensokyo.uk/security/fortify/internal/app/init"
|
||||
"git.gensokyo.uk/security/fortify/internal/app/shim"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"git.gensokyo.uk/security/fortify/internal/sys"
|
||||
)
|
||||
|
||||
const shellPath = "/run/current-system/sw/bin/bash"
|
||||
|
||||
var (
|
||||
errSuccess = errors.New("success")
|
||||
|
||||
std sys.State = new(sys.Std)
|
||||
)
|
||||
|
||||
func init() {
|
||||
fmsg.Prepare("fpkg")
|
||||
if err := os.Setenv("SHELL", shellPath); err != nil {
|
||||
log.Fatalf("cannot set $SHELL: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// early init argv0 check, skips root check and duplicate PR_SET_DUMPABLE
|
||||
init0.TryArgv0()
|
||||
|
||||
if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
|
||||
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
|
||||
// not fatal: this program runs as the privileged user
|
||||
}
|
||||
|
||||
if os.Geteuid() == 0 {
|
||||
log.Fatal("this program must not run as root")
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(),
|
||||
syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop() // unreachable
|
||||
|
||||
var (
|
||||
flagVerbose bool
|
||||
flagDropShell bool
|
||||
)
|
||||
c := command.New(os.Stderr, log.Printf, "fpkg", func([]string) error {
|
||||
fmsg.Store(flagVerbose)
|
||||
if flagVerbose {
|
||||
seccomp.CPrintln = log.Println
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
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")
|
||||
|
||||
// internal commands
|
||||
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 (
|
||||
flagDropShellActivate bool
|
||||
)
|
||||
c.NewCommand("install", "Install an application from its package", func(args []string) error {
|
||||
if len(args) != 1 {
|
||||
log.Println("invalid argument")
|
||||
return syscall.EINVAL
|
||||
}
|
||||
pkgPath := args[0]
|
||||
if !path.IsAbs(pkgPath) {
|
||||
if dir, err := os.Getwd(); err != nil {
|
||||
log.Printf("cannot get current directory: %v", err)
|
||||
return err
|
||||
} else {
|
||||
pkgPath = path.Join(dir, pkgPath)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Look up paths to programs started by fpkg.
|
||||
This is done here to ease error handling as cleanup is not yet required.
|
||||
*/
|
||||
|
||||
var (
|
||||
_ = lookPath("zstd")
|
||||
tar = lookPath("tar")
|
||||
chmod = lookPath("chmod")
|
||||
rm = lookPath("rm")
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
|
||||
/*
|
||||
Extract package and set up for cleanup.
|
||||
*/
|
||||
|
||||
var workDir string
|
||||
if p, err := os.MkdirTemp("", "fpkg.*"); err != nil {
|
||||
log.Printf("cannot create temporary directory: %v", err)
|
||||
return err
|
||||
} else {
|
||||
workDir = p
|
||||
}
|
||||
cleanup := func() {
|
||||
// should be faster than a native implementation
|
||||
mustRun(chmod, "-R", "+w", workDir)
|
||||
mustRun(rm, "-rf", workDir)
|
||||
}
|
||||
beforeRunFail.Store(&cleanup)
|
||||
|
||||
mustRun(tar, "-C", workDir, "-xf", pkgPath)
|
||||
|
||||
/*
|
||||
Parse bundle and app metadata, do pre-install checks.
|
||||
*/
|
||||
|
||||
bundle := loadBundleInfo(path.Join(workDir, "bundle.json"), cleanup)
|
||||
pathSet := pathSetByApp(bundle.ID)
|
||||
|
||||
app := bundle
|
||||
if s, err := os.Stat(pathSet.metaPath); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
cleanup()
|
||||
log.Printf("cannot access %q: %v", pathSet.metaPath, err)
|
||||
return err
|
||||
}
|
||||
// did not modify app, clean installation condition met later
|
||||
} else if s.IsDir() {
|
||||
cleanup()
|
||||
log.Printf("metadata path %q is not a file", pathSet.metaPath)
|
||||
return syscall.EBADMSG
|
||||
} else {
|
||||
app = loadBundleInfo(pathSet.metaPath, cleanup)
|
||||
if app.ID != bundle.ID {
|
||||
cleanup()
|
||||
log.Printf("app %q claims to have identifier %q",
|
||||
bundle.ID, app.ID)
|
||||
return syscall.EBADE
|
||||
}
|
||||
// sec: should verify credentials
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmsg.Prepare("fpkg")
|
||||
if app != bundle {
|
||||
// do not try to re-install
|
||||
if app.NixGL == bundle.NixGL &&
|
||||
app.CurrentSystem == bundle.CurrentSystem &&
|
||||
app.Launcher == bundle.Launcher &&
|
||||
app.ActivationPackage == bundle.ActivationPackage {
|
||||
cleanup()
|
||||
log.Printf("package %q is identical to local application %q",
|
||||
pkgPath, app.ID)
|
||||
return errSuccess
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
fmsg.Store(flagVerbose)
|
||||
// AppID determines uid
|
||||
if app.AppID != bundle.AppID {
|
||||
cleanup()
|
||||
log.Printf("package %q app id %d differs from installed %d",
|
||||
pkgPath, bundle.AppID, app.AppID)
|
||||
return syscall.EBADE
|
||||
}
|
||||
|
||||
args := flag.Args()
|
||||
// sec: should compare version string
|
||||
fmsg.Verbosef("installing application %q version %q over local %q",
|
||||
bundle.ID, bundle.Version, app.Version)
|
||||
} else {
|
||||
fmsg.Verbosef("application %q clean installation", bundle.ID)
|
||||
// sec: should install credentials
|
||||
}
|
||||
|
||||
/*
|
||||
Setup steps for files owned by the target user.
|
||||
*/
|
||||
|
||||
withCacheDir(ctx, "install", []string{
|
||||
// export inner bundle path in the environment
|
||||
"export BUNDLE=" + fst.Tmp + "/bundle",
|
||||
// replace inner /etc
|
||||
"mkdir -p etc",
|
||||
"chmod -R +w etc",
|
||||
"rm -rf etc",
|
||||
"cp -dRf $BUNDLE/etc etc",
|
||||
// replace inner /nix
|
||||
"mkdir -p nix",
|
||||
"chmod -R +w nix",
|
||||
"rm -rf nix",
|
||||
"cp -dRf /nix nix",
|
||||
// copy from binary cache
|
||||
"nix copy --offline --no-check-sigs --all --from file://$BUNDLE/res --to $PWD",
|
||||
// deduplicate nix store
|
||||
"nix store --offline --store $PWD optimise",
|
||||
// make cache directory world-readable for autoetc
|
||||
"chmod 0755 .",
|
||||
}, workDir, bundle, pathSet, flagDropShell, cleanup)
|
||||
|
||||
if bundle.GPU {
|
||||
withCacheDir(ctx, "mesa-wrappers", []string{
|
||||
// link nixGL mesa wrappers
|
||||
"mkdir -p nix/.nixGL",
|
||||
"ln -s " + bundle.Mesa + "/bin/nixGLIntel nix/.nixGL/nixGL",
|
||||
"ln -s " + bundle.Mesa + "/bin/nixVulkanIntel nix/.nixGL/nixVulkan",
|
||||
}, workDir, bundle, pathSet, false, cleanup)
|
||||
}
|
||||
|
||||
/*
|
||||
Activate home-manager generation.
|
||||
*/
|
||||
|
||||
withNixDaemon(ctx, "activate", []string{
|
||||
// clean up broken links
|
||||
"mkdir -p .local/state/{nix,home-manager}",
|
||||
"chmod -R +w .local/state/{nix,home-manager}",
|
||||
"rm -rf .local/state/{nix,home-manager}",
|
||||
// run activation script
|
||||
bundle.ActivationPackage + "/activate",
|
||||
}, false, func(config *fst.Config) *fst.Config { return config },
|
||||
bundle, pathSet, flagDropShellActivate, cleanup)
|
||||
|
||||
/*
|
||||
Installation complete. Write metadata to block re-installs or downgrades.
|
||||
*/
|
||||
|
||||
// serialise metadata to ensure consistency
|
||||
if f, err := os.OpenFile(pathSet.metaPath+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
|
||||
cleanup()
|
||||
log.Printf("cannot create metadata file: %v", err)
|
||||
return err
|
||||
} else if err = json.NewEncoder(f).Encode(bundle); err != nil {
|
||||
cleanup()
|
||||
log.Printf("cannot write metadata: %v", err)
|
||||
return err
|
||||
} else if err = f.Close(); err != nil {
|
||||
log.Printf("cannot close metadata file: %v", err)
|
||||
// not fatal
|
||||
}
|
||||
|
||||
if err := os.Rename(pathSet.metaPath+"~", pathSet.metaPath); err != nil {
|
||||
cleanup()
|
||||
log.Printf("cannot rename metadata file: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
cleanup()
|
||||
return errSuccess
|
||||
}).
|
||||
Flag(&flagDropShellActivate, "s", command.BoolFlag(false), "Drop to a shell on activation")
|
||||
}
|
||||
|
||||
{
|
||||
var (
|
||||
flagDropShellNixGL bool
|
||||
flagAutoDrivers bool
|
||||
)
|
||||
c.NewCommand("start", "Start an application", func(args []string) error {
|
||||
if len(args) < 1 {
|
||||
log.Fatal("invalid argument")
|
||||
log.Println("invalid argument")
|
||||
return syscall.EINVAL
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "install":
|
||||
actionInstall(args[1:])
|
||||
case "start":
|
||||
actionStart(args[1:])
|
||||
/*
|
||||
Parse app metadata.
|
||||
*/
|
||||
|
||||
default:
|
||||
log.Fatal("invalid argument")
|
||||
id := args[0]
|
||||
pathSet := pathSetByApp(id)
|
||||
app := loadBundleInfo(pathSet.metaPath, func() {})
|
||||
if app.ID != id {
|
||||
log.Printf("app %q claims to have identifier %q", id, app.ID)
|
||||
return syscall.EBADE
|
||||
}
|
||||
|
||||
internal.Exit(0)
|
||||
/*
|
||||
Prepare nixGL.
|
||||
*/
|
||||
|
||||
if app.GPU && flagAutoDrivers {
|
||||
withNixDaemon(ctx, "nix-gl", []string{
|
||||
"mkdir -p /nix/.nixGL/auto",
|
||||
"rm -rf /nix/.nixGL/auto",
|
||||
"export NIXPKGS_ALLOW_UNFREE=1",
|
||||
"nix build --impure " +
|
||||
"--out-link /nix/.nixGL/auto/opengl " +
|
||||
"--override-input nixpkgs path:/etc/nixpkgs " +
|
||||
"path:" + app.NixGL,
|
||||
"nix build --impure " +
|
||||
"--out-link /nix/.nixGL/auto/vulkan " +
|
||||
"--override-input nixpkgs path:/etc/nixpkgs " +
|
||||
"path:" + app.NixGL + "#nixVulkanNvidia",
|
||||
}, true, func(config *fst.Config) *fst.Config {
|
||||
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
|
||||
{Src: "/etc/resolv.conf"},
|
||||
{Src: "/sys/block"},
|
||||
{Src: "/sys/bus"},
|
||||
{Src: "/sys/class"},
|
||||
{Src: "/sys/dev"},
|
||||
{Src: "/sys/devices"},
|
||||
}...)
|
||||
appendGPUFilesystem(config)
|
||||
return config
|
||||
}, app, pathSet, flagDropShellNixGL, func() {})
|
||||
}
|
||||
|
||||
/*
|
||||
Create app configuration.
|
||||
*/
|
||||
|
||||
argv := make([]string, 1, len(args))
|
||||
if !flagDropShell {
|
||||
argv[0] = app.Launcher
|
||||
} else {
|
||||
argv[0] = shellPath
|
||||
}
|
||||
argv = append(argv, args[1:]...)
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
if app.GPU {
|
||||
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem,
|
||||
&fst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(fst.Tmp, "nixGL")})
|
||||
appendGPUFilesystem(config)
|
||||
}
|
||||
|
||||
/*
|
||||
Spawn app.
|
||||
*/
|
||||
|
||||
mustRunApp(ctx, config, func() {})
|
||||
return errSuccess
|
||||
}).
|
||||
Flag(&flagDropShellNixGL, "s", command.BoolFlag(false), "Drop to a shell on nixGL build").
|
||||
Flag(&flagAutoDrivers, "auto-drivers", command.BoolFlag(false), "Attempt automatic opengl driver detection")
|
||||
}
|
||||
|
||||
c.MustParse(os.Args[1:], func(err error) {
|
||||
fmsg.Verbosef("command returned %v", err)
|
||||
if errors.Is(err, errSuccess) {
|
||||
fmsg.BeforeExit()
|
||||
os.Exit(0)
|
||||
}
|
||||
})
|
||||
log.Fatal("unreachable")
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
@ -69,3 +70,32 @@ func pathSetByApp(id string) *appPathSet {
|
||||
pathSet.nixPath = path.Join(pathSet.cacheDir, "nix")
|
||||
return pathSet
|
||||
}
|
||||
|
||||
func appendGPUFilesystem(config *fst.Config) {
|
||||
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
|
||||
// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
|
||||
{Src: "/dev/dri", Device: true},
|
||||
// mali
|
||||
{Src: "/dev/mali", Device: true},
|
||||
{Src: "/dev/mali0", Device: true},
|
||||
{Src: "/dev/umplock", Device: true},
|
||||
// nvidia
|
||||
{Src: "/dev/nvidiactl", Device: true},
|
||||
{Src: "/dev/nvidia-modeset", Device: true},
|
||||
// nvidia OpenCL/CUDA
|
||||
{Src: "/dev/nvidia-uvm", Device: true},
|
||||
{Src: "/dev/nvidia-uvm-tools", Device: true},
|
||||
|
||||
// flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff
|
||||
{Src: "/dev/nvidia0", Device: true}, {Src: "/dev/nvidia1", Device: true},
|
||||
{Src: "/dev/nvidia2", Device: true}, {Src: "/dev/nvidia3", Device: true},
|
||||
{Src: "/dev/nvidia4", Device: true}, {Src: "/dev/nvidia5", Device: true},
|
||||
{Src: "/dev/nvidia6", Device: true}, {Src: "/dev/nvidia7", Device: true},
|
||||
{Src: "/dev/nvidia8", Device: true}, {Src: "/dev/nvidia9", Device: true},
|
||||
{Src: "/dev/nvidia10", Device: true}, {Src: "/dev/nvidia11", Device: true},
|
||||
{Src: "/dev/nvidia12", Device: true}, {Src: "/dev/nvidia13", Device: true},
|
||||
{Src: "/dev/nvidia14", Device: true}, {Src: "/dev/nvidia15", Device: true},
|
||||
{Src: "/dev/nvidia16", Device: true}, {Src: "/dev/nvidia17", Device: true},
|
||||
{Src: "/dev/nvidia18", Device: true}, {Src: "/dev/nvidia19", Device: true},
|
||||
}...)
|
||||
}
|
||||
|
@ -1,65 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
"git.gensokyo.uk/security/fortify/internal/app"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
|
||||
func mustRunApp(ctx context.Context, config *fst.Config, beforeFail func()) {
|
||||
rs := new(fst.RunState)
|
||||
a := app.MustNew(std)
|
||||
|
||||
var (
|
||||
Fmain = compPoison
|
||||
)
|
||||
|
||||
func fortifyApp(config *fst.Config, beforeFail func()) {
|
||||
var (
|
||||
cmd *exec.Cmd
|
||||
st io.WriteCloser
|
||||
)
|
||||
if p, ok := internal.Path(Fmain); !ok {
|
||||
beforeFail()
|
||||
log.Fatal("invalid fortify path, this copy of fpkg is not compiled correctly")
|
||||
} else if r, w, err := os.Pipe(); err != nil {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot pipe: %v", err)
|
||||
if sa, err := a.Seal(config); err != nil {
|
||||
fmsg.PrintBaseError(err, "cannot seal app:")
|
||||
rs.ExitCode = 1
|
||||
} else {
|
||||
if fmsg.Load() {
|
||||
cmd = exec.Command(p, "-v", "app", "3")
|
||||
} else {
|
||||
cmd = exec.Command(p, "app", "3")
|
||||
}
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
cmd.ExtraFiles = []*os.File{r}
|
||||
st = w
|
||||
// this updates ExitCode
|
||||
app.PrintRunStateErr(rs, sa.Run(ctx, rs))
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := json.NewEncoder(st).Encode(config); err != nil {
|
||||
if rs.ExitCode != 0 {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot send configuration: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot start fortify: %v", err)
|
||||
}
|
||||
if err := cmd.Wait(); err != nil {
|
||||
var exitError *exec.ExitError
|
||||
if errors.As(err, &exitError) {
|
||||
beforeFail()
|
||||
internal.Exit(exitError.ExitCode())
|
||||
} else {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot wait: %v", err)
|
||||
}
|
||||
os.Exit(rs.ExitCode)
|
||||
}
|
||||
}
|
||||
|
@ -1,178 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"path"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
)
|
||||
|
||||
func actionStart(args []string) {
|
||||
set := flag.NewFlagSet("start", flag.ExitOnError)
|
||||
var (
|
||||
dropShell bool
|
||||
dropShellNixGL bool
|
||||
autoDrivers bool
|
||||
)
|
||||
set.BoolVar(&dropShell, "s", false, "Drop to a shell")
|
||||
set.BoolVar(&dropShellNixGL, "sg", false, "Drop to a shell on nixGL build")
|
||||
set.BoolVar(&autoDrivers, "autodrivers", false, "Attempt automatic opengl driver detection")
|
||||
|
||||
// Ignore errors; set is set for ExitOnError.
|
||||
_ = set.Parse(args)
|
||||
|
||||
args = set.Args()
|
||||
|
||||
if len(args) < 1 {
|
||||
log.Fatal("invalid argument")
|
||||
}
|
||||
|
||||
/*
|
||||
Parse app metadata.
|
||||
*/
|
||||
|
||||
id := args[0]
|
||||
pathSet := pathSetByApp(id)
|
||||
app := loadBundleInfo(pathSet.metaPath, func() {})
|
||||
if app.ID != id {
|
||||
log.Fatalf("app %q claims to have identifier %q", id, app.ID)
|
||||
}
|
||||
|
||||
/*
|
||||
Prepare nixGL.
|
||||
*/
|
||||
|
||||
if app.GPU && autoDrivers {
|
||||
withNixDaemon("nix-gl", []string{
|
||||
"mkdir -p /nix/.nixGL/auto",
|
||||
"rm -rf /nix/.nixGL/auto",
|
||||
"export NIXPKGS_ALLOW_UNFREE=1",
|
||||
"nix build --impure " +
|
||||
"--out-link /nix/.nixGL/auto/opengl " +
|
||||
"--override-input nixpkgs path:/etc/nixpkgs " +
|
||||
"path:" + app.NixGL,
|
||||
"nix build --impure " +
|
||||
"--out-link /nix/.nixGL/auto/vulkan " +
|
||||
"--override-input nixpkgs path:/etc/nixpkgs " +
|
||||
"path:" + app.NixGL + "#nixVulkanNvidia",
|
||||
}, true, func(config *fst.Config) *fst.Config {
|
||||
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
|
||||
{Src: "/etc/resolv.conf"},
|
||||
{Src: "/sys/block"},
|
||||
{Src: "/sys/bus"},
|
||||
{Src: "/sys/class"},
|
||||
{Src: "/sys/dev"},
|
||||
{Src: "/sys/devices"},
|
||||
}...)
|
||||
appendGPUFilesystem(config)
|
||||
return config
|
||||
}, app, pathSet, dropShellNixGL, func() {})
|
||||
}
|
||||
|
||||
/*
|
||||
Create app configuration.
|
||||
*/
|
||||
|
||||
command := make([]string, 1, len(args))
|
||||
if !dropShell {
|
||||
command[0] = app.Launcher
|
||||
} else {
|
||||
command[0] = shellPath
|
||||
}
|
||||
command = append(command, args[1:]...)
|
||||
|
||||
config := &fst.Config{
|
||||
ID: app.ID,
|
||||
Command: command,
|
||||
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 || dropShell,
|
||||
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.
|
||||
*/
|
||||
|
||||
if app.GPU {
|
||||
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem,
|
||||
&fst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(fst.Tmp, "nixGL")})
|
||||
appendGPUFilesystem(config)
|
||||
}
|
||||
|
||||
/*
|
||||
Spawn app.
|
||||
*/
|
||||
|
||||
fortifyApp(config, func() {})
|
||||
internal.Exit(0)
|
||||
}
|
||||
|
||||
func appendGPUFilesystem(config *fst.Config) {
|
||||
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
|
||||
// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
|
||||
{Src: "/dev/dri", Device: true},
|
||||
// mali
|
||||
{Src: "/dev/mali", Device: true},
|
||||
{Src: "/dev/mali0", Device: true},
|
||||
{Src: "/dev/umplock", Device: true},
|
||||
// nvidia
|
||||
{Src: "/dev/nvidiactl", Device: true},
|
||||
{Src: "/dev/nvidia-modeset", Device: true},
|
||||
// nvidia OpenCL/CUDA
|
||||
{Src: "/dev/nvidia-uvm", Device: true},
|
||||
{Src: "/dev/nvidia-uvm-tools", Device: true},
|
||||
|
||||
// flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff
|
||||
{Src: "/dev/nvidia0", Device: true}, {Src: "/dev/nvidia1", Device: true},
|
||||
{Src: "/dev/nvidia2", Device: true}, {Src: "/dev/nvidia3", Device: true},
|
||||
{Src: "/dev/nvidia4", Device: true}, {Src: "/dev/nvidia5", Device: true},
|
||||
{Src: "/dev/nvidia6", Device: true}, {Src: "/dev/nvidia7", Device: true},
|
||||
{Src: "/dev/nvidia8", Device: true}, {Src: "/dev/nvidia9", Device: true},
|
||||
{Src: "/dev/nvidia10", Device: true}, {Src: "/dev/nvidia11", Device: true},
|
||||
{Src: "/dev/nvidia12", Device: true}, {Src: "/dev/nvidia13", Device: true},
|
||||
{Src: "/dev/nvidia14", Device: true}, {Src: "/dev/nvidia15", Device: true},
|
||||
{Src: "/dev/nvidia16", Device: true}, {Src: "/dev/nvidia17", Device: true},
|
||||
{Src: "/dev/nvidia18", Device: true}, {Src: "/dev/nvidia19", Device: true},
|
||||
}...)
|
||||
}
|
60
cmd/fpkg/test/configuration.nix
Normal file
60
cmd/fpkg/test/configuration.nix
Normal file
@ -0,0 +1,60 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
users.users = {
|
||||
alice = {
|
||||
isNormalUser = true;
|
||||
description = "Alice Foobar";
|
||||
password = "foobar";
|
||||
uid = 1000;
|
||||
};
|
||||
};
|
||||
|
||||
home-manager.users.alice.home.stateVersion = "24.11";
|
||||
|
||||
# Automatically login on tty1 as a normal user:
|
||||
services.getty.autologinUser = "alice";
|
||||
|
||||
environment = {
|
||||
variables = {
|
||||
SWAYSOCK = "/tmp/sway-ipc.sock";
|
||||
WLR_RENDERER = "pixman";
|
||||
};
|
||||
};
|
||||
|
||||
# Automatically configure and start Sway when logging in on tty1:
|
||||
programs.bash.loginShellInit = ''
|
||||
if [ "$(tty)" = "/dev/tty1" ]; then
|
||||
set -e
|
||||
|
||||
mkdir -p ~/.config/sway
|
||||
(sed s/Mod4/Mod1/ /etc/sway/config &&
|
||||
echo 'output * bg ${pkgs.nixos-artwork.wallpapers.simple-light-gray.gnomeFilePath} fill' &&
|
||||
echo 'output Virtual-1 res 1680x1050') > ~/.config/sway/config
|
||||
|
||||
sway --validate
|
||||
systemd-cat --identifier=sway sway && touch /tmp/sway-exit-ok
|
||||
fi
|
||||
'';
|
||||
|
||||
programs.sway.enable = true;
|
||||
|
||||
virtualisation = {
|
||||
diskSize = 6 * 1024;
|
||||
|
||||
qemu.options = [
|
||||
# Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch:
|
||||
"-vga none -device virtio-gpu-pci"
|
||||
|
||||
# Increase zstd performance:
|
||||
"-smp 8"
|
||||
];
|
||||
};
|
||||
|
||||
environment.fortify = {
|
||||
enable = true;
|
||||
stateDir = "/var/lib/fortify";
|
||||
users.alice = 0;
|
||||
|
||||
home-manager = _: _: { home.stateVersion = "23.05"; };
|
||||
};
|
||||
}
|
34
cmd/fpkg/test/default.nix
Normal file
34
cmd/fpkg/test/default.nix
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
nixosTest,
|
||||
callPackage,
|
||||
|
||||
system,
|
||||
self,
|
||||
}:
|
||||
let
|
||||
buildPackage = self.buildPackage.${system};
|
||||
in
|
||||
nixosTest {
|
||||
name = "fpkg";
|
||||
nodes.machine = {
|
||||
environment.etc = {
|
||||
"foot.pkg".source = callPackage ./foot.nix { inherit buildPackage; };
|
||||
};
|
||||
|
||||
imports = [
|
||||
./configuration.nix
|
||||
|
||||
self.nixosModules.fortify
|
||||
self.inputs.home-manager.nixosModules.home-manager
|
||||
];
|
||||
};
|
||||
|
||||
# adapted from nixos sway integration tests
|
||||
|
||||
# testScriptWithTypes:49: error: Cannot call function of unknown type
|
||||
# (machine.succeed if succeed else machine.execute)(
|
||||
# ^
|
||||
# Found 1 error in 1 file (checked 1 source file)
|
||||
skipTypeCheck = true;
|
||||
testScript = builtins.readFile ./test.py;
|
||||
}
|
48
cmd/fpkg/test/foot.nix
Normal file
48
cmd/fpkg/test/foot.nix
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
lib,
|
||||
buildPackage,
|
||||
foot,
|
||||
wayland-utils,
|
||||
inconsolata,
|
||||
}:
|
||||
|
||||
buildPackage {
|
||||
name = "foot";
|
||||
inherit (foot) version;
|
||||
|
||||
app_id = 2;
|
||||
id = "org.codeberg.dnkl.foot";
|
||||
|
||||
modules = [
|
||||
{
|
||||
home.packages = [
|
||||
foot
|
||||
|
||||
# For wayland-info:
|
||||
wayland-utils
|
||||
];
|
||||
}
|
||||
];
|
||||
|
||||
nixosModules = [
|
||||
{
|
||||
# To help with OCR:
|
||||
environment.etc."xdg/foot/foot.ini".text = lib.generators.toINI { } {
|
||||
main = {
|
||||
font = "inconsolata:size=14";
|
||||
};
|
||||
colors = rec {
|
||||
foreground = "000000";
|
||||
background = "ffffff";
|
||||
regular2 = foreground;
|
||||
};
|
||||
};
|
||||
|
||||
fonts.packages = [ inconsolata ];
|
||||
}
|
||||
];
|
||||
|
||||
script = ''
|
||||
exec foot "$@"
|
||||
'';
|
||||
}
|
108
cmd/fpkg/test/test.py
Normal file
108
cmd/fpkg/test/test.py
Normal file
@ -0,0 +1,108 @@
|
||||
import json
|
||||
import shlex
|
||||
|
||||
q = shlex.quote
|
||||
NODE_GROUPS = ["nodes", "floating_nodes"]
|
||||
|
||||
|
||||
def swaymsg(command: str = "", succeed=True, type="command"):
|
||||
assert command != "" or type != "command", "Must specify command or type"
|
||||
shell = q(f"swaymsg -t {q(type)} -- {q(command)}")
|
||||
with machine.nested(
|
||||
f"sending swaymsg {shell!r}" + " (allowed to fail)" * (not succeed)
|
||||
):
|
||||
ret = (machine.succeed if succeed else machine.execute)(
|
||||
f"su - alice -c {shell}"
|
||||
)
|
||||
|
||||
# execute also returns a status code, but disregard.
|
||||
if not succeed:
|
||||
_, ret = ret
|
||||
|
||||
if not succeed and not ret:
|
||||
return None
|
||||
|
||||
parsed = json.loads(ret)
|
||||
return parsed
|
||||
|
||||
|
||||
def walk(tree):
|
||||
yield tree
|
||||
for group in NODE_GROUPS:
|
||||
for node in tree.get(group, []):
|
||||
yield from walk(node)
|
||||
|
||||
|
||||
def wait_for_window(pattern):
|
||||
def func(last_chance):
|
||||
nodes = (node["name"] for node in walk(swaymsg(type="get_tree")))
|
||||
|
||||
if last_chance:
|
||||
nodes = list(nodes)
|
||||
machine.log(f"Last call! Current list of windows: {nodes}")
|
||||
|
||||
return any(pattern in name for name in nodes)
|
||||
|
||||
retry(func)
|
||||
|
||||
|
||||
def collect_state_ui(name):
|
||||
swaymsg(f"exec fortify ps > '/tmp/{name}.ps'")
|
||||
machine.copy_from_vm(f"/tmp/{name}.ps", "")
|
||||
swaymsg(f"exec fortify --json ps > '/tmp/{name}.json'")
|
||||
machine.copy_from_vm(f"/tmp/{name}.json", "")
|
||||
machine.screenshot(name)
|
||||
|
||||
|
||||
def check_state(name, enablements):
|
||||
instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 fortify --json ps"))
|
||||
if len(instances) != 1:
|
||||
raise Exception(f"unexpected state length {len(instances)}")
|
||||
instance = next(iter(instances.values()))
|
||||
|
||||
config = instance['config']
|
||||
|
||||
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 command {instance['config']['command']}")
|
||||
|
||||
if config['confinement']['enablements'] != enablements:
|
||||
raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
|
||||
|
||||
|
||||
start_all()
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
# To check fortify's version:
|
||||
print(machine.succeed("sudo -u alice -i fortify version"))
|
||||
|
||||
# Wait for Sway to complete startup:
|
||||
machine.wait_for_file("/run/user/1000/wayland-1")
|
||||
machine.wait_for_file("/tmp/sway-ipc.sock")
|
||||
|
||||
# Prepare fpkg directory:
|
||||
machine.succeed("install -dm 0700 -o alice -g users /var/lib/fortify/1000")
|
||||
|
||||
# Install fpkg app:
|
||||
swaymsg("exec fpkg -v install /etc/foot.pkg && touch /tmp/fpkg-install-done")
|
||||
machine.wait_for_file("/tmp/fpkg-install-done")
|
||||
|
||||
# Start app (foot) with Wayland enablement:
|
||||
swaymsg("exec fpkg -v start org.codeberg.dnkl.foot")
|
||||
wait_for_window("fortify@machine-foot")
|
||||
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
|
||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-client")
|
||||
collect_state_ui("app_wayland")
|
||||
check_state("foot", 13)
|
||||
# Verify acl on XDG_RUNTIME_DIR:
|
||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002"))
|
||||
machine.send_chars("exit\n")
|
||||
machine.wait_until_fails("pgrep foot")
|
||||
# Verify acl cleanup on XDG_RUNTIME_DIR:
|
||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002")
|
||||
|
||||
# Exit Sway and verify process exit status 0:
|
||||
swaymsg("exit", succeed=False)
|
||||
machine.wait_for_file("/tmp/sway-exit-ok")
|
||||
|
||||
# Print fortify runDir contents:
|
||||
print(machine.succeed("find /run/user/1000/fortify"))
|
@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
@ -10,10 +11,11 @@ import (
|
||||
)
|
||||
|
||||
func withNixDaemon(
|
||||
ctx context.Context,
|
||||
action string, command []string, net bool, updateConfig func(config *fst.Config) *fst.Config,
|
||||
app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
|
||||
) {
|
||||
fortifyAppDropShell(updateConfig(&fst.Config{
|
||||
mustRunAppDropShell(ctx, updateConfig(&fst.Config{
|
||||
ID: app.ID,
|
||||
Command: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
|
||||
// start nix-daemon
|
||||
@ -56,8 +58,11 @@ func withNixDaemon(
|
||||
}), dropShell, beforeFail)
|
||||
}
|
||||
|
||||
func withCacheDir(action string, command []string, workDir string, app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
|
||||
fortifyAppDropShell(&fst.Config{
|
||||
func withCacheDir(
|
||||
ctx context.Context,
|
||||
action string, command []string, workDir string,
|
||||
app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
|
||||
mustRunAppDropShell(ctx, &fst.Config{
|
||||
ID: app.ID,
|
||||
Command: []string{shellPath, "-lc", strings.Join(command, " && ")},
|
||||
Confinement: fst.ConfinementConfig{
|
||||
@ -90,12 +95,12 @@ func withCacheDir(action string, command []string, workDir string, app *bundleIn
|
||||
}, dropShell, beforeFail)
|
||||
}
|
||||
|
||||
func fortifyAppDropShell(config *fst.Config, dropShell bool, beforeFail func()) {
|
||||
func mustRunAppDropShell(ctx context.Context, config *fst.Config, dropShell bool, beforeFail func()) {
|
||||
if dropShell {
|
||||
config.Command = []string{shellPath, "-l"}
|
||||
fortifyApp(config, beforeFail)
|
||||
mustRunApp(ctx, config, beforeFail)
|
||||
beforeFail()
|
||||
internal.Exit(0)
|
||||
}
|
||||
fortifyApp(config, beforeFail)
|
||||
mustRunApp(ctx, config, beforeFail)
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
|
||||
fsuConfFile = "/etc/fsurc"
|
||||
envShim = "FORTIFY_SHIM"
|
||||
envAID = "FORTIFY_APP_ID"
|
||||
@ -22,10 +21,6 @@ const (
|
||||
PR_SET_NO_NEW_PRIVS = 0x26
|
||||
)
|
||||
|
||||
var (
|
||||
Fmain = compPoison
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("fsu: ")
|
||||
@ -40,20 +35,16 @@ func main() {
|
||||
log.Fatal("this program must not be started by root")
|
||||
}
|
||||
|
||||
var fmain string
|
||||
if p, ok := checkPath(Fmain); !ok {
|
||||
log.Fatal("invalid fortify path, this copy of fsu is not compiled correctly")
|
||||
} else {
|
||||
fmain = p
|
||||
}
|
||||
|
||||
var toolPath string
|
||||
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
|
||||
if p, err := os.Readlink(pexe); err != nil {
|
||||
log.Fatalf("cannot read parent executable path: %v", err)
|
||||
} else if strings.HasSuffix(p, " (deleted)") {
|
||||
log.Fatal("fortify executable has been deleted")
|
||||
} else if p != fmain {
|
||||
} else if p != mustCheckPath(fmain) && p != mustCheckPath(fpkg) {
|
||||
log.Fatal("this program must be started by fortify")
|
||||
} else {
|
||||
toolPath = p
|
||||
}
|
||||
|
||||
// uid = 1000000 +
|
||||
@ -147,13 +138,9 @@ func main() {
|
||||
if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
|
||||
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
|
||||
}
|
||||
if err := syscall.Exec(fmain, []string{"fortify", "shim"}, []string{envShim + "=" + shimSetupFd}); err != nil {
|
||||
if err := syscall.Exec(toolPath, []string{"fortify", "shim"}, []string{envShim + "=" + shimSetupFd}); err != nil {
|
||||
log.Fatalf("cannot start shim: %v", err)
|
||||
}
|
||||
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func checkPath(p string) (string, bool) {
|
||||
return p, p != compPoison && p != "" && path.IsAbs(p)
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
lib,
|
||||
buildGoModule,
|
||||
fortify ? abort "fortify package required",
|
||||
}:
|
||||
@ -15,5 +16,15 @@ buildGoModule {
|
||||
go mod init fsu >& /dev/null
|
||||
'';
|
||||
|
||||
ldflags = [ "-X main.Fmain=${fortify}/libexec/fortify" ];
|
||||
ldflags =
|
||||
lib.attrsets.foldlAttrs
|
||||
(
|
||||
ldflags: name: value:
|
||||
ldflags ++ [ "-X main.${name}=${value}" ]
|
||||
)
|
||||
[ "-s -w" ]
|
||||
{
|
||||
fmain = "${fortify}/libexec/fortify";
|
||||
fpkg = "${fortify}/libexec/fpkg";
|
||||
};
|
||||
}
|
||||
|
21
cmd/fsu/path.go
Normal file
21
cmd/fsu/path.go
Normal file
@ -0,0 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"path"
|
||||
)
|
||||
|
||||
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
|
||||
|
||||
var (
|
||||
fmain = compPoison
|
||||
fpkg = compPoison
|
||||
)
|
||||
|
||||
func mustCheckPath(p string) string {
|
||||
if p != compPoison && p != "" && path.IsAbs(p) {
|
||||
return p
|
||||
}
|
||||
log.Fatal("this program is compiled incorrectly")
|
||||
return compPoison
|
||||
}
|
@ -110,7 +110,7 @@ func (p *Proxy) Start(ctx context.Context, output io.Writer, sandbox bool) error
|
||||
bc.Bind(k, k)
|
||||
}
|
||||
|
||||
h = helper.MustNewBwrap(bc, toolPath, p.seal, argF, nil, nil)
|
||||
h = helper.MustNewBwrap(bc, toolPath, true, p.seal, argF, nil, nil)
|
||||
p.bwrap = bc
|
||||
}
|
||||
|
||||
|
7
dist/release.sh
vendored
7
dist/release.sh
vendored
@ -10,9 +10,10 @@ cp -rv "comp" "${out}"
|
||||
|
||||
go generate ./...
|
||||
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w -buildid= -extldflags '-static'
|
||||
-X git.gensokyo.uk/security/fortify/internal.Version=${VERSION}
|
||||
-X git.gensokyo.uk/security/fortify/internal.Fsu=/usr/bin/fsu
|
||||
-X main.Fmain=/usr/bin/fortify" ./...
|
||||
-X git.gensokyo.uk/security/fortify/internal.version=${VERSION}
|
||||
-X git.gensokyo.uk/security/fortify/internal.fsu=/usr/bin/fsu
|
||||
-X main.fmain=/usr/bin/fortify
|
||||
-X main.fpkg=/usr/bin/fpkg" ./...
|
||||
|
||||
rm -f "./${out}.tar.gz" && tar -C dist -czf "${out}.tar.gz" "${pname}"
|
||||
rm -rf "./${out}"
|
||||
|
46
error.go
46
error.go
@ -1,46 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/internal/app"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
func logWaitError(err error) {
|
||||
var e *fmsg.BaseError
|
||||
if !fmsg.AsBaseError(err, &e) {
|
||||
log.Println("wait failed:", err)
|
||||
} else {
|
||||
// Wait only returns either *app.ProcessError or *app.StateStoreError wrapped in a *app.BaseError
|
||||
var se *app.StateStoreError
|
||||
if !errors.As(err, &se) {
|
||||
// does not need special handling
|
||||
log.Print(e.Message())
|
||||
} else {
|
||||
// inner error are either unwrapped store errors
|
||||
// or joined errors returned by *appSealTx revert
|
||||
// wrapped in *app.BaseError
|
||||
var ej app.RevertCompoundError
|
||||
if !errors.As(se.InnerErr, &ej) {
|
||||
// does not require special handling
|
||||
log.Print(e.Message())
|
||||
} else {
|
||||
errs := ej.Unwrap()
|
||||
|
||||
// every error here is wrapped in *app.BaseError
|
||||
for _, ei := range errs {
|
||||
var eb *fmsg.BaseError
|
||||
if !errors.As(ei, &eb) {
|
||||
// unreachable
|
||||
log.Println("invalid error type returned by revert:", ei)
|
||||
} else {
|
||||
// print inner *app.BaseError message
|
||||
log.Print(eb.Message())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
flake.nix
25
flake.nix
@ -58,6 +58,7 @@
|
||||
in
|
||||
{
|
||||
fortify = callPackage ./test { inherit system self; };
|
||||
fpkg = callPackage ./cmd/fpkg/test { inherit system self; };
|
||||
race = callPackage ./test {
|
||||
inherit system self;
|
||||
withRace = true;
|
||||
@ -67,7 +68,7 @@
|
||||
cd ${./.}
|
||||
|
||||
echo "running nixfmt..."
|
||||
nixfmt --check .
|
||||
nixfmt --width=256 --check .
|
||||
|
||||
touch $out
|
||||
'';
|
||||
@ -97,24 +98,32 @@
|
||||
packages = forAllSystems (
|
||||
system:
|
||||
let
|
||||
inherit (self.packages.${system}) fortify;
|
||||
inherit (self.packages.${system}) fortify fsu;
|
||||
pkgs = nixpkgsFor.${system};
|
||||
in
|
||||
{
|
||||
default = self.packages.${system}.fortify;
|
||||
default = fortify;
|
||||
fortify = pkgs.pkgsStatic.callPackage ./package.nix {
|
||||
inherit (pkgs) bubblewrap xdg-dbus-proxy glibc;
|
||||
inherit (pkgs)
|
||||
bubblewrap
|
||||
xdg-dbus-proxy
|
||||
glibc
|
||||
zstd
|
||||
gnutar
|
||||
coreutils
|
||||
;
|
||||
};
|
||||
fsu = pkgs.callPackage ./cmd/fsu/package.nix { inherit (self.packages.${system}) fortify; };
|
||||
|
||||
dist =
|
||||
pkgs.runCommand "${fortify.name}-dist" { inherit (self.devShells.${system}.default) buildInputs; }
|
||||
''
|
||||
dist = pkgs.runCommand "${fortify.name}-dist" { inherit (self.devShells.${system}.default) buildInputs; } ''
|
||||
# go requires XDG_CACHE_HOME for the build cache
|
||||
export XDG_CACHE_HOME="$(mktemp -d)"
|
||||
|
||||
# get a different workdir as go does not like /build
|
||||
cd $(mktemp -d) && cp -r ${fortify.src}/. . && chmod -R +w .
|
||||
cd $(mktemp -d) \
|
||||
&& cp -r ${fortify.src}/. . \
|
||||
&& chmod +w cmd && cp -r ${fsu.src}/. cmd/fsu/ \
|
||||
&& chmod -R +w .
|
||||
|
||||
export FORTIFY_VERSION="v${fortify.version}"
|
||||
./dist/release.sh && mkdir $out && cp -v "dist/fortify-$FORTIFY_VERSION.tar.gz"* $out
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
||||
@ -23,6 +24,9 @@ type bubblewrap struct {
|
||||
// name of the command to run in bwrap
|
||||
name string
|
||||
|
||||
// whether to set process group id
|
||||
setpgid bool
|
||||
|
||||
lock sync.RWMutex
|
||||
*helperCmd
|
||||
}
|
||||
@ -38,6 +42,10 @@ func (b *bubblewrap) Start(ctx context.Context, stat bool) error {
|
||||
}
|
||||
|
||||
args := b.finalise(ctx, stat)
|
||||
if b.setpgid {
|
||||
b.Cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
}
|
||||
|
||||
b.Cmd.Args = slices.Grow(b.Cmd.Args, 4+len(args))
|
||||
b.Cmd.Args = append(b.Cmd.Args, "--args", strconv.Itoa(int(b.argsFd)), "--", b.name)
|
||||
b.Cmd.Args = append(b.Cmd.Args, args...)
|
||||
@ -48,12 +56,12 @@ func (b *bubblewrap) Start(ctx context.Context, stat bool) error {
|
||||
// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
|
||||
// Function argF returns an array of arguments passed directly to the child process.
|
||||
func MustNewBwrap(
|
||||
conf *bwrap.Config, name string,
|
||||
conf *bwrap.Config, name string, setpgid bool,
|
||||
wt io.WriterTo, argF func(argsFD, statFD int) []string,
|
||||
extraFiles []*os.File,
|
||||
syncFd *os.File,
|
||||
) Helper {
|
||||
b, err := NewBwrap(conf, name, wt, argF, extraFiles, syncFd)
|
||||
b, err := NewBwrap(conf, name, setpgid, wt, argF, extraFiles, syncFd)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
} else {
|
||||
@ -65,7 +73,7 @@ func MustNewBwrap(
|
||||
// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
|
||||
// Function argF returns an array of arguments passed directly to the child process.
|
||||
func NewBwrap(
|
||||
conf *bwrap.Config, name string,
|
||||
conf *bwrap.Config, name string, setpgid bool,
|
||||
wt io.WriterTo, argF func(argsFd, statFd int) []string,
|
||||
extraFiles []*os.File,
|
||||
syncFd *os.File,
|
||||
@ -73,6 +81,7 @@ func NewBwrap(
|
||||
b := new(bubblewrap)
|
||||
|
||||
b.name = name
|
||||
b.setpgid = setpgid
|
||||
b.helperCmd = newHelperCmd(b, BubblewrapName, wt, argF, extraFiles)
|
||||
|
||||
if v, err := NewCheckedArgs(conf.Args(syncFd, b.extraFiles, &b.files)); err != nil {
|
||||
|
@ -31,7 +31,7 @@ func TestBwrap(t *testing.T) {
|
||||
})
|
||||
|
||||
h := helper.MustNewBwrap(
|
||||
sc, "fortify",
|
||||
sc, "fortify", false,
|
||||
argsWt, argF,
|
||||
nil, nil,
|
||||
)
|
||||
@ -44,7 +44,7 @@ func TestBwrap(t *testing.T) {
|
||||
|
||||
t.Run("valid new helper nil check", func(t *testing.T) {
|
||||
if got := helper.MustNewBwrap(
|
||||
sc, "fortify",
|
||||
sc, "fortify", false,
|
||||
argsWt, argF,
|
||||
nil, nil,
|
||||
); got == nil {
|
||||
@ -64,7 +64,7 @@ func TestBwrap(t *testing.T) {
|
||||
}()
|
||||
|
||||
helper.MustNewBwrap(
|
||||
&bwrap.Config{Hostname: "\x00"}, "fortify",
|
||||
&bwrap.Config{Hostname: "\x00"}, "fortify", false,
|
||||
nil, argF,
|
||||
nil, nil,
|
||||
)
|
||||
@ -74,7 +74,7 @@ func TestBwrap(t *testing.T) {
|
||||
helper.InternalReplaceExecCommand(t)
|
||||
|
||||
h := helper.MustNewBwrap(
|
||||
sc, "crash-test-dummy",
|
||||
sc, "crash-test-dummy", false,
|
||||
nil, argFChecked,
|
||||
nil, nil,
|
||||
)
|
||||
@ -98,6 +98,11 @@ func TestBwrap(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("implementation compliance", func(t *testing.T) {
|
||||
testHelper(t, func() helper.Helper { return helper.MustNewBwrap(sc, "crash-test-dummy", argsWt, argF, nil, nil) })
|
||||
testHelper(t, func() helper.Helper {
|
||||
return helper.MustNewBwrap(
|
||||
sc, "crash-test-dummy", false,
|
||||
argsWt, argF, nil, nil,
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
179
internal/app/errors.go
Normal file
179
internal/app/errors.go
Normal file
@ -0,0 +1,179 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
func PrintRunStateErr(rs *fst.RunState, runErr error) {
|
||||
if runErr != nil {
|
||||
if rs.Time == nil {
|
||||
fmsg.PrintBaseError(runErr, "cannot start app:")
|
||||
} else {
|
||||
var e *fmsg.BaseError
|
||||
if !fmsg.AsBaseError(runErr, &e) {
|
||||
log.Println("wait failed:", runErr)
|
||||
} else {
|
||||
// Wait only returns either *app.ProcessError or *app.StateStoreError wrapped in a *app.BaseError
|
||||
var se *StateStoreError
|
||||
if !errors.As(runErr, &se) {
|
||||
// does not need special handling
|
||||
log.Print(e.Message())
|
||||
} else {
|
||||
// inner error are either unwrapped store errors
|
||||
// or joined errors returned by *appSealTx revert
|
||||
// wrapped in *app.BaseError
|
||||
var ej RevertCompoundError
|
||||
if !errors.As(se.InnerErr, &ej) {
|
||||
// does not require special handling
|
||||
log.Print(e.Message())
|
||||
} else {
|
||||
errs := ej.Unwrap()
|
||||
|
||||
// every error here is wrapped in *app.BaseError
|
||||
for _, ei := range errs {
|
||||
var eb *fmsg.BaseError
|
||||
if !errors.As(ei, &eb) {
|
||||
// unreachable
|
||||
log.Println("invalid error type returned by revert:", ei)
|
||||
} else {
|
||||
// print inner *app.BaseError message
|
||||
log.Print(eb.Message())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if rs.ExitCode == 0 {
|
||||
rs.ExitCode = 126
|
||||
}
|
||||
}
|
||||
|
||||
if rs.RevertErr != nil {
|
||||
var stateStoreError *StateStoreError
|
||||
if !errors.As(rs.RevertErr, &stateStoreError) || stateStoreError == nil {
|
||||
fmsg.PrintBaseError(rs.RevertErr, "generic fault during cleanup:")
|
||||
goto out
|
||||
}
|
||||
|
||||
if stateStoreError.Err != nil {
|
||||
if len(stateStoreError.Err) == 2 {
|
||||
if stateStoreError.Err[0] != nil {
|
||||
if joinedErrs, ok := stateStoreError.Err[0].(interface{ Unwrap() []error }); !ok {
|
||||
fmsg.PrintBaseError(stateStoreError.Err[0], "generic fault during revert:")
|
||||
} else {
|
||||
for _, err := range joinedErrs.Unwrap() {
|
||||
if err != nil {
|
||||
fmsg.PrintBaseError(err, "fault during revert:")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if stateStoreError.Err[1] != nil {
|
||||
log.Printf("cannot close store: %v", stateStoreError.Err[1])
|
||||
}
|
||||
} else {
|
||||
log.Printf("fault during cleanup: %v",
|
||||
errors.Join(stateStoreError.Err...))
|
||||
}
|
||||
}
|
||||
|
||||
if stateStoreError.OpErr != nil {
|
||||
log.Printf("blind revert due to store fault: %v",
|
||||
stateStoreError.OpErr)
|
||||
}
|
||||
|
||||
if stateStoreError.DoErr != nil {
|
||||
fmsg.PrintBaseError(stateStoreError.DoErr, "state store operation unsuccessful:")
|
||||
}
|
||||
|
||||
if stateStoreError.Inner && stateStoreError.InnerErr != nil {
|
||||
fmsg.PrintBaseError(stateStoreError.InnerErr, "cannot destroy state entry:")
|
||||
}
|
||||
|
||||
out:
|
||||
if rs.ExitCode == 0 {
|
||||
rs.ExitCode = 128
|
||||
}
|
||||
}
|
||||
if rs.WaitErr != nil {
|
||||
log.Println("inner wait failed:", rs.WaitErr)
|
||||
}
|
||||
}
|
||||
|
||||
// StateStoreError is returned for a failed state save
|
||||
type StateStoreError struct {
|
||||
// whether inner function was called
|
||||
Inner bool
|
||||
// returned by the Save/Destroy method of [state.Cursor]
|
||||
InnerErr error
|
||||
// returned by the Do method of [state.Store]
|
||||
DoErr error
|
||||
// stores an arbitrary store operation error
|
||||
OpErr error
|
||||
// stores arbitrary errors
|
||||
Err []error
|
||||
}
|
||||
|
||||
// save saves arbitrary errors in [StateStoreError] once.
|
||||
func (e *StateStoreError) save(errs []error) {
|
||||
if len(errs) == 0 || e.Err != nil {
|
||||
panic("invalid call to save")
|
||||
}
|
||||
e.Err = errs
|
||||
}
|
||||
|
||||
func (e *StateStoreError) equiv(a ...any) error {
|
||||
if e.Inner && e.InnerErr == nil && e.DoErr == nil && e.OpErr == nil && errors.Join(e.Err...) == nil {
|
||||
return nil
|
||||
} else {
|
||||
return fmsg.WrapErrorSuffix(e, a...)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *StateStoreError) Error() string {
|
||||
if e.Inner && e.InnerErr != nil {
|
||||
return e.InnerErr.Error()
|
||||
}
|
||||
if e.DoErr != nil {
|
||||
return e.DoErr.Error()
|
||||
}
|
||||
if e.OpErr != nil {
|
||||
return e.OpErr.Error()
|
||||
}
|
||||
if err := errors.Join(e.Err...); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
// equiv nullifies e for values where this is reached
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func (e *StateStoreError) Unwrap() (errs []error) {
|
||||
errs = make([]error, 0, 3)
|
||||
if e.InnerErr != nil {
|
||||
errs = append(errs, e.InnerErr)
|
||||
}
|
||||
if e.DoErr != nil {
|
||||
errs = append(errs, e.DoErr)
|
||||
}
|
||||
if e.OpErr != nil {
|
||||
errs = append(errs, e.OpErr)
|
||||
}
|
||||
if err := errors.Join(e.Err...); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// A RevertCompoundError encapsulates errors returned by
|
||||
// the Revert method of [system.I].
|
||||
type RevertCompoundError interface {
|
||||
Error() string
|
||||
Unwrap() []error
|
||||
}
|
@ -12,6 +12,7 @@ import (
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/helper"
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
"git.gensokyo.uk/security/fortify/internal/app/shim"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"git.gensokyo.uk/security/fortify/internal/state"
|
||||
@ -32,6 +33,10 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
|
||||
panic("invalid state")
|
||||
}
|
||||
|
||||
// read comp values early to allow for early failure
|
||||
fmsg.Verbosef("version %s", internal.Version())
|
||||
fmsg.Verbosef("setuid helper at %s", internal.MustFsuPath())
|
||||
|
||||
/*
|
||||
resolve exec paths
|
||||
*/
|
||||
@ -79,7 +84,8 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
|
||||
ec.Set(system.Process)
|
||||
if states, err := c.Load(); err != nil {
|
||||
// revert per-process state here to limit damage
|
||||
return errors.Join(err, seal.sys.Revert(ec))
|
||||
storeErr.OpErr = err
|
||||
return seal.sys.Revert(ec)
|
||||
} else {
|
||||
if l := len(states); l == 0 {
|
||||
fmsg.Verbose("no other launchers active, will clean up globals")
|
||||
@ -115,14 +121,10 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
|
||||
}
|
||||
}
|
||||
|
||||
err := seal.sys.Revert(ec)
|
||||
if err != nil {
|
||||
err = err.(RevertCompoundError)
|
||||
}
|
||||
return err
|
||||
return seal.sys.Revert(ec)
|
||||
}()
|
||||
})
|
||||
storeErr.Err = errors.Join(revertErr, store.Close())
|
||||
storeErr.save([]error{revertErr, store.Close()})
|
||||
rs.RevertErr = storeErr.equiv("error returned during cleanup:")
|
||||
}()
|
||||
|
||||
@ -170,7 +172,9 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
|
||||
Time: *rs.Time,
|
||||
}
|
||||
var earlyStoreErr = new(StateStoreError) // returned after blocking on waitErr
|
||||
earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) { earlyStoreErr.InnerErr = c.Save(&sd, seal.ct) })
|
||||
earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
|
||||
earlyStoreErr.InnerErr = c.Save(&sd, seal.ct)
|
||||
})
|
||||
// destroy defunct state entry
|
||||
deferredStoreFunc = func(c state.Cursor) error { return c.Destroy(seal.id.unwrap()) }
|
||||
|
||||
@ -212,69 +216,3 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
|
||||
|
||||
return earlyStoreErr.equiv("cannot save process state:")
|
||||
}
|
||||
|
||||
// StateStoreError is returned for a failed state save
|
||||
type StateStoreError struct {
|
||||
// whether inner function was called
|
||||
Inner bool
|
||||
// returned by the Do method of [state.Store]
|
||||
DoErr error
|
||||
// returned by the Save/Destroy method of [state.Cursor]
|
||||
InnerErr error
|
||||
// stores an arbitrary error
|
||||
Err error
|
||||
}
|
||||
|
||||
// save saves exactly one arbitrary error in [StateStoreError].
|
||||
func (e *StateStoreError) save(err error) {
|
||||
if err == nil || e.Err != nil {
|
||||
panic("invalid call to save")
|
||||
}
|
||||
e.Err = err
|
||||
}
|
||||
|
||||
func (e *StateStoreError) equiv(a ...any) error {
|
||||
if e.Inner && e.DoErr == nil && e.InnerErr == nil && e.Err == nil {
|
||||
return nil
|
||||
} else {
|
||||
return fmsg.WrapErrorSuffix(e, a...)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *StateStoreError) Error() string {
|
||||
if e.Inner && e.InnerErr != nil {
|
||||
return e.InnerErr.Error()
|
||||
}
|
||||
|
||||
if e.DoErr != nil {
|
||||
return e.DoErr.Error()
|
||||
}
|
||||
|
||||
if e.Err != nil {
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
// equiv nullifies e for values where this is reached
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func (e *StateStoreError) Unwrap() (errs []error) {
|
||||
errs = make([]error, 0, 3)
|
||||
if e.DoErr != nil {
|
||||
errs = append(errs, e.DoErr)
|
||||
}
|
||||
if e.InnerErr != nil {
|
||||
errs = append(errs, e.InnerErr)
|
||||
}
|
||||
if e.Err != nil {
|
||||
errs = append(errs, e.Err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// A RevertCompoundError encapsulates errors returned by
|
||||
// the Revert method of [system.I].
|
||||
type RevertCompoundError interface {
|
||||
Error() string
|
||||
Unwrap() []error
|
||||
}
|
||||
|
@ -125,7 +125,7 @@ func Main() {
|
||||
seccomp.CPrintln = log.Println
|
||||
}
|
||||
if b, err := helper.NewBwrap(
|
||||
conf, path.Join(fst.Tmp, "sbin/init"),
|
||||
conf, path.Join(fst.Tmp, "sbin/init"), false,
|
||||
nil, func(int, int) []string { return make([]string, 0) },
|
||||
extraFiles,
|
||||
syncFd,
|
||||
|
@ -52,14 +52,8 @@ func (s *Shim) Start(
|
||||
syncFd *os.File,
|
||||
) (*time.Time, error) {
|
||||
// prepare user switcher invocation
|
||||
var fsu string
|
||||
if p, ok := internal.Path(internal.Fsu); !ok {
|
||||
return nil, fmsg.WrapError(errors.New("bad fsu path"),
|
||||
"invalid fsu path, this copy of fortify is not compiled correctly")
|
||||
} else {
|
||||
fsu = p
|
||||
}
|
||||
s.cmd = exec.Command(fsu)
|
||||
fsuPath := internal.MustFsuPath()
|
||||
s.cmd = exec.Command(fsuPath)
|
||||
|
||||
// pass shim setup pipe
|
||||
if fd, e, err := proc.Setup(&s.cmd.ExtraFiles); err != nil {
|
||||
|
@ -3,10 +3,15 @@ package internal
|
||||
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
|
||||
|
||||
var (
|
||||
Version = compPoison
|
||||
version = compPoison
|
||||
)
|
||||
|
||||
// Check validates string value set at compile time.
|
||||
func Check(s string) (string, bool) {
|
||||
return s, s != compPoison && s != ""
|
||||
// check validates string value set at compile time.
|
||||
func check(s string) (string, bool) { return s, s != compPoison && s != "" }
|
||||
|
||||
func Version() string {
|
||||
if v, ok := check(version); ok {
|
||||
return v
|
||||
}
|
||||
return "impure"
|
||||
}
|
||||
|
@ -1,11 +1,23 @@
|
||||
package internal
|
||||
|
||||
import "path"
|
||||
import (
|
||||
"log"
|
||||
"path"
|
||||
|
||||
var (
|
||||
Fsu = compPoison
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
func Path(p string) (string, bool) {
|
||||
return p, p != compPoison && p != "" && path.IsAbs(p)
|
||||
var (
|
||||
fsu = compPoison
|
||||
)
|
||||
|
||||
func MustFsuPath() string {
|
||||
if name, ok := checkPath(fsu); ok {
|
||||
return name
|
||||
}
|
||||
fmsg.BeforeExit()
|
||||
log.Fatal("invalid fsu path, this program is compiled incorrectly")
|
||||
return compPoison
|
||||
}
|
||||
|
||||
func checkPath(p string) (string, bool) { return p, p != compPoison && p != "" && path.IsAbs(p) }
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
@ -79,14 +78,10 @@ func (s *Std) Uid(aid int) (int, error) {
|
||||
defer func() { s.uidCopy[aid] = u }()
|
||||
|
||||
u.uid = -1
|
||||
if fsu, ok := internal.Check(internal.Fsu); !ok {
|
||||
fmsg.BeforeExit()
|
||||
log.Fatal("invalid fsu path, this copy of fortify is not compiled correctly")
|
||||
// unreachable
|
||||
return 0, syscall.EBADE
|
||||
} else {
|
||||
cmd := exec.Command(fsu)
|
||||
cmd.Path = fsu
|
||||
fsuPath := internal.MustFsuPath()
|
||||
|
||||
cmd := exec.Command(fsuPath)
|
||||
cmd.Path = fsuPath
|
||||
cmd.Stderr = os.Stderr // pass through fatal messages
|
||||
cmd.Env = []string{"FORTIFY_APP_ID=" + strconv.Itoa(aid)}
|
||||
cmd.Dir = "/"
|
||||
@ -103,8 +98,7 @@ func (s *Std) Uid(aid int) (int, error) {
|
||||
} else if errors.As(u.err, &exitError) && exitError != nil && exitError.ExitCode() == 1 {
|
||||
u.err = fmsg.WrapError(syscall.EACCES, "") // fsu prints to stderr in this case
|
||||
} else if os.IsNotExist(u.err) {
|
||||
u.err = fmsg.WrapError(os.ErrNotExist, fmt.Sprintf("the setuid helper is missing: %s", fsu))
|
||||
u.err = fmsg.WrapError(os.ErrNotExist, fmt.Sprintf("the setuid helper is missing: %s", fsuPath))
|
||||
}
|
||||
return u.uid, u.err
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ func Exec(ctx context.Context, p string) ([]*Entry, error) {
|
||||
Syscall: &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
|
||||
NewSession: true,
|
||||
DieWithParent: true,
|
||||
}).Bind("/", "/").DevTmpfs("/dev"), toolPath,
|
||||
}).Bind("/", "/").DevTmpfs("/dev"), toolPath, false,
|
||||
nil, func(_, _ int) []string { return []string{p} },
|
||||
nil, nil,
|
||||
); err != nil {
|
||||
|
50
main.go
50
main.go
@ -68,10 +68,20 @@ func buildCommand(out io.Writer) command.Command {
|
||||
flagVerbose bool
|
||||
flagJSON bool
|
||||
)
|
||||
c := command.New(out, log.Printf, "fortify", func([]string) error { fmsg.Store(flagVerbose); return nil }).
|
||||
c := command.New(out, log.Printf, "fortify", func([]string) error {
|
||||
fmsg.Store(flagVerbose)
|
||||
if flagVerbose {
|
||||
seccomp.CPrintln = log.Println
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
|
||||
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("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 {
|
||||
if len(args) < 1 {
|
||||
log.Fatal("app requires at least 1 argument")
|
||||
@ -249,11 +259,7 @@ func buildCommand(out io.Writer) command.Command {
|
||||
}).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id")
|
||||
|
||||
c.Command("version", "Show fortify version", func(args []string) error {
|
||||
if v, ok := internal.Check(internal.Version); ok {
|
||||
fmt.Println(v)
|
||||
} else {
|
||||
fmt.Println("impure")
|
||||
}
|
||||
fmt.Println(internal.Version())
|
||||
return errSuccess
|
||||
})
|
||||
|
||||
@ -272,45 +278,21 @@ func buildCommand(out io.Writer) command.Command {
|
||||
return errSuccess
|
||||
})
|
||||
|
||||
// internal commands
|
||||
c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess })
|
||||
c.Command("init", command.UsageInternal, func([]string) error { init0.Main(); return errSuccess })
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func runApp(a fst.App, config *fst.Config) {
|
||||
rs := new(fst.RunState)
|
||||
ctx, stop := signal.NotifyContext(context.Background(),
|
||||
syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop() // unreachable
|
||||
|
||||
if fmsg.Load() {
|
||||
seccomp.CPrintln = log.Println
|
||||
}
|
||||
|
||||
rs := new(fst.RunState)
|
||||
if sa, err := a.Seal(config); err != nil {
|
||||
fmsg.PrintBaseError(err, "cannot seal app:")
|
||||
internal.Exit(1)
|
||||
} else if err = sa.Run(ctx, rs); err != nil {
|
||||
if rs.Time == nil {
|
||||
fmsg.PrintBaseError(err, "cannot start app:")
|
||||
rs.ExitCode = 1
|
||||
} else {
|
||||
logWaitError(err)
|
||||
}
|
||||
|
||||
if rs.ExitCode == 0 {
|
||||
rs.ExitCode = 126
|
||||
}
|
||||
}
|
||||
if rs.RevertErr != nil {
|
||||
fmsg.PrintBaseError(rs.RevertErr, "generic error returned during cleanup:")
|
||||
if rs.ExitCode == 0 {
|
||||
rs.ExitCode = 128
|
||||
}
|
||||
}
|
||||
if rs.WaitErr != nil {
|
||||
log.Println("inner wait failed:", rs.WaitErr)
|
||||
// this updates ExitCode
|
||||
app.PrintRunStateErr(rs, sa.Run(ctx, rs))
|
||||
}
|
||||
internal.Exit(rs.ExitCode)
|
||||
}
|
||||
|
17
nixos.nix
17
nixos.nix
@ -77,21 +77,12 @@ in
|
||||
};
|
||||
in
|
||||
{
|
||||
session_bus =
|
||||
if app.dbus.session != null then
|
||||
(app.dbus.session (extendDBusDefault app.id))
|
||||
else
|
||||
(extendDBusDefault app.id default);
|
||||
session_bus = if app.dbus.session != null then (app.dbus.session (extendDBusDefault app.id)) else (extendDBusDefault app.id default);
|
||||
system_bus = app.dbus.system;
|
||||
};
|
||||
command = if app.command == null then app.name else app.command;
|
||||
script = if app.script == null then ("exec " + command + " $@") else app.script;
|
||||
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 = {
|
||||
inherit (app) id;
|
||||
command = [
|
||||
@ -165,9 +156,7 @@ in
|
||||
};
|
||||
in
|
||||
pkgs.writeShellScriptBin app.name ''
|
||||
exec fortify${
|
||||
if app.verbose then " -v" else ""
|
||||
} app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@
|
||||
exec fortify${if app.verbose then " -v" else ""} app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@
|
||||
''
|
||||
) cfg.apps;
|
||||
in
|
||||
|
@ -3,7 +3,14 @@
|
||||
let
|
||||
inherit (lib) types mkOption mkEnableOption;
|
||||
fortify = pkgs.pkgsStatic.callPackage ./package.nix {
|
||||
inherit (pkgs) bubblewrap xdg-dbus-proxy glibc;
|
||||
inherit (pkgs)
|
||||
bubblewrap
|
||||
xdg-dbus-proxy
|
||||
glibc
|
||||
zstd
|
||||
gnutar
|
||||
coreutils
|
||||
;
|
||||
};
|
||||
in
|
||||
|
||||
|
42
package.nix
42
package.nix
@ -14,6 +14,11 @@
|
||||
wayland-scanner,
|
||||
xorg,
|
||||
|
||||
# for fpkg
|
||||
zstd,
|
||||
gnutar,
|
||||
coreutils,
|
||||
|
||||
glibc, # for ldd
|
||||
withStatic ? stdenv.hostPlatform.isStatic,
|
||||
}:
|
||||
@ -25,10 +30,7 @@ buildGoModule rec {
|
||||
src = builtins.path {
|
||||
name = "${pname}-src";
|
||||
path = lib.cleanSource ./.;
|
||||
filter =
|
||||
path: type:
|
||||
!(type == "regular" && lib.hasSuffix ".nix" 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;
|
||||
|
||||
@ -39,17 +41,15 @@ buildGoModule rec {
|
||||
ldflags ++ [ "-X git.gensokyo.uk/security/fortify/internal.${name}=${value}" ]
|
||||
)
|
||||
(
|
||||
[
|
||||
"-s -w"
|
||||
]
|
||||
[ "-s -w" ]
|
||||
++ lib.optionals withStatic [
|
||||
"-linkmode external"
|
||||
"-extldflags \"-static\""
|
||||
]
|
||||
)
|
||||
{
|
||||
Version = "v${version}";
|
||||
Fsu = "/run/wrappers/bin/fsu";
|
||||
version = "v${version}";
|
||||
fsu = "/run/wrappers/bin/fsu";
|
||||
};
|
||||
|
||||
# nix build environment does not allow acls
|
||||
@ -79,19 +79,33 @@ buildGoModule rec {
|
||||
HOME="$(mktemp -d)" PATH="${pkg-config}/bin:$PATH" go generate ./...
|
||||
'';
|
||||
|
||||
postInstall = ''
|
||||
postInstall =
|
||||
let
|
||||
appPackages = [
|
||||
glibc
|
||||
bubblewrap
|
||||
xdg-dbus-proxy
|
||||
];
|
||||
in
|
||||
''
|
||||
install -D --target-directory=$out/share/zsh/site-functions comp/*
|
||||
|
||||
mkdir "$out/libexec"
|
||||
mv "$out"/bin/* "$out/libexec/"
|
||||
|
||||
makeBinaryWrapper "$out/libexec/fortify" "$out/bin/fortify" \
|
||||
--inherit-argv0 --prefix PATH : ${lib.makeBinPath appPackages}
|
||||
|
||||
makeBinaryWrapper "$out/libexec/fpkg" "$out/bin/fpkg" \
|
||||
--inherit-argv0 --prefix PATH : ${
|
||||
lib.makeBinPath [
|
||||
glibc
|
||||
bubblewrap
|
||||
xdg-dbus-proxy
|
||||
lib.makeBinPath (
|
||||
appPackages
|
||||
++ [
|
||||
zstd
|
||||
gnutar
|
||||
coreutils
|
||||
]
|
||||
)
|
||||
}
|
||||
'';
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/acl"
|
||||
@ -41,7 +43,13 @@ func (a *ACL) apply(sys *I) error {
|
||||
func (a *ACL) revert(sys *I, ec *Criteria) error {
|
||||
if ec.hasType(a) {
|
||||
sys.println("stripping ACL", a)
|
||||
return sys.wrapErrSuffix(acl.Update(a.path, sys.uid),
|
||||
err := acl.Update(a.path, sys.uid)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// the ACL is effectively stripped if the file no longer exists
|
||||
sys.printf("target of ACL %s no longer exists", a)
|
||||
err = nil
|
||||
}
|
||||
return sys.wrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot strip ACL entry from %q:", a.path))
|
||||
} else {
|
||||
sys.println("skipping ACL", a)
|
||||
|
@ -2,6 +2,7 @@ package system
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"strings"
|
||||
@ -96,7 +97,12 @@ func (d *DBus) revert(sys *I, _ *Criteria) error {
|
||||
sys.println("terminating message bus proxy")
|
||||
d.proxy.Close()
|
||||
defer sys.println("message bus proxy exit")
|
||||
return sys.wrapErrSuffix(d.proxy.Wait(), "message bus proxy error:")
|
||||
err := d.proxy.Wait()
|
||||
if errors.Is(err, context.Canceled) {
|
||||
sys.println("message bus proxy canceled upstream")
|
||||
err = nil
|
||||
}
|
||||
return sys.wrapErrSuffix(err, "message bus proxy error:")
|
||||
}
|
||||
|
||||
func (d *DBus) Is(o Op) bool {
|
||||
|
@ -102,6 +102,21 @@
|
||||
home-manager = _: _: { home.stateVersion = "23.05"; };
|
||||
|
||||
apps = [
|
||||
{
|
||||
name = "check-sandbox";
|
||||
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";
|
||||
verbose = true;
|
||||
|
61
test/sandbox/assert.go
Normal file
61
test/sandbox/assert.go
Normal file
@ -0,0 +1,61 @@
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
assert = log.New(os.Stderr, "sandbox: ", 0)
|
||||
fatalfFunc = assert.Fatalf
|
||||
)
|
||||
|
||||
func fatalf(format string, v ...any) { fatalfFunc(format, v...) }
|
||||
|
||||
func MustAssertMounts(name, hostMountsFile, wantFile string) {
|
||||
hostMounts := make([]*Mntent, 0, 128)
|
||||
if err := IterMounts(hostMountsFile, func(e *Mntent) {
|
||||
hostMounts = append(hostMounts, e)
|
||||
}); err != nil {
|
||||
fatalf("cannot parse host mounts: %v", err)
|
||||
}
|
||||
|
||||
var want []Mntent
|
||||
if f, err := os.Open(wantFile); err != nil {
|
||||
fatalf("cannot open %q: %v", wantFile, err)
|
||||
} else if err = json.NewDecoder(f).Decode(&want); err != nil {
|
||||
fatalf("cannot decode %q: %v", wantFile, err)
|
||||
} else if err = f.Close(); err != nil {
|
||||
fatalf("cannot close %q: %v", wantFile, err)
|
||||
}
|
||||
|
||||
for i := range want {
|
||||
if want[i].Opts == "host_passthrough" {
|
||||
for _, ent := range hostMounts {
|
||||
if want[i].FSName == ent.FSName {
|
||||
want[i].Opts = ent.Opts
|
||||
goto out
|
||||
}
|
||||
}
|
||||
fatalf("host passthrough missing %q", want[i].FSName)
|
||||
out:
|
||||
}
|
||||
}
|
||||
|
||||
i := 0
|
||||
if err := IterMounts(name, func(e *Mntent) {
|
||||
if i == len(want) {
|
||||
fatalf("got more than %d entries", i)
|
||||
}
|
||||
if *e != want[i] {
|
||||
fatalf("entry %d\n got: %s\nwant: %s", i,
|
||||
e, &want[i])
|
||||
}
|
||||
|
||||
assert.Printf("%s", e)
|
||||
i++
|
||||
}); err != nil {
|
||||
fatalf("cannot iterate mounts: %v", err)
|
||||
}
|
||||
}
|
3
test/sandbox/assert_test.go
Normal file
3
test/sandbox/assert_test.go
Normal file
@ -0,0 +1,3 @@
|
||||
package sandbox
|
||||
|
||||
func ReplaceFatal(f func(format string, v ...any)) { fatalfFunc = f }
|
12
test/sandbox/default.nix
Normal file
12
test/sandbox/default.nix
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
writeShellScript,
|
||||
callPackage,
|
||||
|
||||
version,
|
||||
}:
|
||||
writeShellScript "check-sandbox" ''
|
||||
set -e
|
||||
${callPackage ./mount.nix { inherit version; }}/bin/test
|
||||
|
||||
touch /tmp/sandbox-ok
|
||||
''
|
134
test/sandbox/mount.go
Normal file
134
test/sandbox/mount.go
Normal file
@ -0,0 +1,134 @@
|
||||
package sandbox
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <mntent.h>
|
||||
|
||||
const char *F_PROC_MOUNTS = "";
|
||||
const char *F_SET_TYPE = "r";
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type Mntent struct {
|
||||
/* name of mounted filesystem */
|
||||
FSName string `json:"fsname"`
|
||||
/* 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"`
|
||||
}
|
||||
|
||||
func (e *Mntent) String() string {
|
||||
return fmt.Sprintf("%s %s %s %s %d %d",
|
||||
e.FSName, e.Dir, e.Type, e.Opts, e.Freq, e.Passno)
|
||||
}
|
||||
|
||||
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
|
||||
f *C.FILE
|
||||
mu sync.RWMutex
|
||||
|
||||
ent *C.struct_mntent
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *mounts) open() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.f != nil {
|
||||
panic("open called twice")
|
||||
}
|
||||
|
||||
if m.p == "" {
|
||||
m.p = "/proc/mounts"
|
||||
}
|
||||
|
||||
name := C.CString(m.p)
|
||||
f, err := C.setmntent(name, C.F_SET_TYPE)
|
||||
C.free(unsafe.Pointer(name))
|
||||
|
||||
if f == nil {
|
||||
return err
|
||||
}
|
||||
m.f = f
|
||||
runtime.SetFinalizer(m, (*mounts).close)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *mounts) close() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.f == nil {
|
||||
panic("close called before open")
|
||||
}
|
||||
|
||||
C.endmntent(m.f)
|
||||
runtime.SetFinalizer(m, nil)
|
||||
}
|
||||
|
||||
func (m *mounts) scan() bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.f == nil {
|
||||
panic("invalid file")
|
||||
}
|
||||
|
||||
m.ent, m.err = C.getmntent(m.f)
|
||||
return m.ent != nil
|
||||
}
|
||||
|
||||
func (m *mounts) Err() error {
|
||||
m.mu.RLock()
|
||||
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" "ro,nosuid,nodev,relatime,size=98784k,nr_inodes=24696,mode=700,uid=1000,gid=100" 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
|
||||
'';
|
||||
}
|
136
test/sandbox/mount_test.go
Normal file
136
test/sandbox/mount_test.go
Normal file
@ -0,0 +1,136 @@
|
||||
package sandbox_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/test/sandbox"
|
||||
)
|
||||
|
||||
func TestMounts(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
sample string
|
||||
want []sandbox.Mntent
|
||||
}{
|
||||
{"fpkg", `tmpfs / tmpfs rw,nosuid,nodev,relatime,uid=1000002,gid=1000002 0 0
|
||||
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
|
||||
tmpfs /.fortify tmpfs rw,nosuid,nodev,relatime,size=4k,mode=755,uid=1000002,gid=1000002 0 0
|
||||
tmpfs /dev tmpfs rw,nosuid,nodev,relatime,mode=755,uid=1000002,gid=1000002 0 0
|
||||
devtmpfs /dev/null devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
|
||||
devtmpfs /dev/zero devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
|
||||
devtmpfs /dev/full devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
|
||||
devtmpfs /dev/random devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
|
||||
devtmpfs /dev/urandom devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
|
||||
devtmpfs /dev/tty devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
|
||||
devpts /dev/pts devpts rw,nosuid,noexec,relatime,mode=620,ptmxmode=666 0 0
|
||||
mqueue /dev/mqueue mqueue rw,relatime 0 0
|
||||
/dev/disk/by-label/nixos /nix/store ext4 ro,nosuid,nodev,relatime 0 0
|
||||
/dev/disk/by-label/nixos /.fortify/app ext4 ro,nosuid,nodev,relatime 0 0
|
||||
/dev/disk/by-label/nixos /etc/resolv.conf ext4 ro,nosuid,nodev,relatime 0 0
|
||||
sysfs /sys/block sysfs ro,nosuid,nodev,noexec,relatime 0 0
|
||||
sysfs /sys/bus sysfs ro,nosuid,nodev,noexec,relatime 0 0
|
||||
sysfs /sys/class sysfs ro,nosuid,nodev,noexec,relatime 0 0
|
||||
sysfs /sys/dev sysfs ro,nosuid,nodev,noexec,relatime 0 0
|
||||
sysfs /sys/devices sysfs ro,nosuid,nodev,noexec,relatime 0 0
|
||||
/dev/disk/by-label/nixos /.fortify/nixGL ext4 ro,nosuid,nodev,relatime 0 0
|
||||
devtmpfs /dev/dri devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
|
||||
/dev/disk/by-label/nixos /.fortify/etc ext4 ro,nosuid,nodev,relatime 0 0
|
||||
tmpfs /run/user tmpfs rw,nosuid,nodev,relatime,size=1024k,mode=755,uid=1000002,gid=1000002 0 0
|
||||
tmpfs /run/user/65534 tmpfs rw,nosuid,nodev,relatime,size=8192k,mode=755,uid=1000002,gid=1000002 0 0
|
||||
/dev/disk/by-label/nixos /tmp ext4 rw,nosuid,nodev,relatime 0 0
|
||||
/dev/disk/by-label/nixos /data/data/org.codeberg.dnkl.foot ext4 rw,nosuid,nodev,relatime 0 0
|
||||
tmpfs /etc/passwd tmpfs ro,nosuid,nodev,relatime,uid=1000002,gid=1000002 0 0
|
||||
tmpfs /etc/group tmpfs ro,nosuid,nodev,relatime,uid=1000002,gid=1000002 0 0
|
||||
/dev/disk/by-label/nixos /run/user/65534/wayland-0 ext4 ro,nosuid,nodev,relatime 0 0
|
||||
tmpfs /run/user/65534/pulse/native tmpfs ro,nosuid,nodev,relatime,size=98784k,nr_inodes=24696,mode=700,uid=1000,gid=100 0 0
|
||||
/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
|
||||
`, []sandbox.Mntent{
|
||||
{"tmpfs", "/", "tmpfs", "rw,nosuid,nodev,relatime,uid=1000002,gid=1000002", 0, 0},
|
||||
{"proc", "/proc", "proc", "rw,nosuid,nodev,noexec,relatime", 0, 0},
|
||||
{"tmpfs", "/.fortify", "tmpfs", "rw,nosuid,nodev,relatime,size=4k,mode=755,uid=1000002,gid=1000002", 0, 0},
|
||||
{"tmpfs", "/dev", "tmpfs", "rw,nosuid,nodev,relatime,mode=755,uid=1000002,gid=1000002", 0, 0},
|
||||
{"devtmpfs", "/dev/null", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
|
||||
{"devtmpfs", "/dev/zero", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
|
||||
{"devtmpfs", "/dev/full", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
|
||||
{"devtmpfs", "/dev/random", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
|
||||
{"devtmpfs", "/dev/urandom", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
|
||||
{"devtmpfs", "/dev/tty", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
|
||||
{"devpts", "/dev/pts", "devpts", "rw,nosuid,noexec,relatime,mode=620,ptmxmode=666", 0, 0},
|
||||
{"mqueue", "/dev/mqueue", "mqueue", "rw,relatime", 0, 0},
|
||||
{"/dev/disk/by-label/nixos", "/nix/store", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
|
||||
{"/dev/disk/by-label/nixos", "/.fortify/app", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
|
||||
{"/dev/disk/by-label/nixos", "/etc/resolv.conf", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
|
||||
{"sysfs", "/sys/block", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
|
||||
{"sysfs", "/sys/bus", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
|
||||
{"sysfs", "/sys/class", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
|
||||
{"sysfs", "/sys/dev", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
|
||||
{"sysfs", "/sys/devices", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
|
||||
{"/dev/disk/by-label/nixos", "/.fortify/nixGL", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
|
||||
{"devtmpfs", "/dev/dri", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
|
||||
{"/dev/disk/by-label/nixos", "/.fortify/etc", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
|
||||
{"tmpfs", "/run/user", "tmpfs", "rw,nosuid,nodev,relatime,size=1024k,mode=755,uid=1000002,gid=1000002", 0, 0},
|
||||
{"tmpfs", "/run/user/65534", "tmpfs", "rw,nosuid,nodev,relatime,size=8192k,mode=755,uid=1000002,gid=1000002", 0, 0},
|
||||
{"/dev/disk/by-label/nixos", "/tmp", "ext4", "rw,nosuid,nodev,relatime", 0, 0},
|
||||
{"/dev/disk/by-label/nixos", "/data/data/org.codeberg.dnkl.foot", "ext4", "rw,nosuid,nodev,relatime", 0, 0},
|
||||
{"tmpfs", "/etc/passwd", "tmpfs", "ro,nosuid,nodev,relatime,uid=1000002,gid=1000002", 0, 0},
|
||||
{"tmpfs", "/etc/group", "tmpfs", "ro,nosuid,nodev,relatime,uid=1000002,gid=1000002", 0, 0},
|
||||
{"/dev/disk/by-label/nixos", "/run/user/65534/wayland-0", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
|
||||
{"tmpfs", "/run/user/65534/pulse/native", "tmpfs", "ro,nosuid,nodev,relatime,size=98784k,nr_inodes=24696,mode=700,uid=1000,gid=100", 0, 0},
|
||||
{"/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},
|
||||
}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
name := path.Join(t.TempDir(), "sample")
|
||||
if err := os.WriteFile(name, []byte(tc.sample), 0400); err != nil {
|
||||
t.Fatalf("cannot write sample: %v", err)
|
||||
}
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
i := 0
|
||||
if err := sandbox.IterMounts(name, func(e *sandbox.Mntent) {
|
||||
if i == len(tc.want) {
|
||||
t.Errorf("IterMounts: got more than %d entries", i)
|
||||
t.FailNow()
|
||||
}
|
||||
if *e != tc.want[i] {
|
||||
t.Errorf("IterMounts: entry %d\n got: %s\nwant: %s", i,
|
||||
e, &tc.want[i])
|
||||
t.FailNow()
|
||||
}
|
||||
i++
|
||||
}); err != nil {
|
||||
t.Fatalf("IterMounts: error = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run(tc.name+" assert", func(t *testing.T) {
|
||||
sandbox.ReplaceFatal(t.Fatalf)
|
||||
|
||||
wantFile := path.Join(t.TempDir(), "want.json")
|
||||
if f, err := os.OpenFile(wantFile, os.O_CREATE|os.O_WRONLY, 0400); err != nil {
|
||||
t.Fatalf("cannot create %q: %v", wantFile, err)
|
||||
} else if err = json.NewEncoder(f).Encode(tc.want); err != nil {
|
||||
t.Fatalf("cannot encode to %q: %v", wantFile, err)
|
||||
} else if err = f.Close(); err != nil {
|
||||
t.Fatalf("cannot close %q: %v", wantFile, err)
|
||||
}
|
||||
|
||||
sandbox.MustAssertMounts(name, name, wantFile)
|
||||
|
||||
if err := os.Remove(wantFile); err != nil {
|
||||
t.Fatalf("cannot remove %q: %v", wantFile, err)
|
||||
}
|
||||
})
|
||||
|
||||
if err := os.Remove(name); err != nil {
|
||||
t.Fatalf("cannot remove %q: %v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
59
test/test.py
59
test/test.py
@ -95,12 +95,19 @@ print(denyOutput)
|
||||
denyOutputVerbose = machine.fail("sudo -u untrusted -i fortify -v run &>/dev/stdout")
|
||||
print(denyOutputVerbose)
|
||||
|
||||
# Fail direct fsu call:
|
||||
print(machine.fail("sudo -u alice -i fsu"))
|
||||
|
||||
# Verify PrintBaseError behaviour:
|
||||
if denyOutput != "fsu: uid 1001 is not in the fsurc file\n":
|
||||
raise Exception(f"unexpected deny output:\n{denyOutput}")
|
||||
if denyOutputVerbose != "fsu: uid 1001 is not in the fsurc file\nfortify: *cannot obtain uid from fsu: permission denied\n":
|
||||
raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}")
|
||||
|
||||
# Check sandbox state:
|
||||
swaymsg("exec check-sandbox")
|
||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/sandbox-ok")
|
||||
|
||||
# Start fortify permissive defaults outside Wayland session:
|
||||
print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare"))
|
||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-bare")
|
||||
@ -110,36 +117,55 @@ output = machine.succeed("sudo -u alice -i fortify run -a 0 true &>/dev/stdout")
|
||||
if output != "":
|
||||
raise Exception(f"unexpected output\n{output}")
|
||||
|
||||
# Verify silent output permissive defaults signal:
|
||||
def silent_output_interrupt(flags):
|
||||
swaymsg("exec foot")
|
||||
wait_for_window("alice@machine")
|
||||
# aid 0 does not have home-manager
|
||||
machine.send_chars(f"exec fortify run {flags}-a 0 sh -c 'export PATH=/run/current-system/sw/bin:$PATH && touch /tmp/pd-silent-ready && sleep infinity' &>/tmp/pd-silent\n")
|
||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/pd-silent-ready")
|
||||
machine.succeed("rm /tmp/fortify.1000/tmpdir/0/pd-silent-ready")
|
||||
machine.send_key("ctrl-c")
|
||||
machine.wait_until_fails("pgrep foot")
|
||||
machine.wait_until_fails(f"pgrep -u alice -f 'fortify run {flags}-a 0 '")
|
||||
output = machine.succeed("cat /tmp/pd-silent && rm /tmp/pd-silent")
|
||||
if output != "":
|
||||
raise Exception(f"unexpected output\n{output}")
|
||||
|
||||
|
||||
silent_output_interrupt("")
|
||||
silent_output_interrupt("--dbus ") # this one is especially painful as it maintains a helper
|
||||
silent_output_interrupt("--wayland -X --dbus --pulse ")
|
||||
|
||||
# Verify graceful failure on bad Wayland display name:
|
||||
print(machine.fail("sudo -u alice -i fortify -v run --wayland true"))
|
||||
|
||||
# Start fortify permissive defaults within Wayland session:
|
||||
fortify(
|
||||
'-v run --wayland --dbus notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-done')
|
||||
fortify('-v run --wayland --dbus notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-done')
|
||||
machine.wait_for_file("/tmp/dbus-done")
|
||||
collect_state_ui("dbus_notify_exited")
|
||||
machine.succeed("pkill -9 mako")
|
||||
|
||||
# Start app (foot) with Wayland enablement:
|
||||
swaymsg("exec ne-foot")
|
||||
wait_for_window("u0_a1@machine")
|
||||
wait_for_window("u0_a2@machine")
|
||||
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
|
||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/success-client")
|
||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-client")
|
||||
collect_state_ui("foot_wayland")
|
||||
check_state("ne-foot", 1)
|
||||
# Verify acl on XDG_RUNTIME_DIR:
|
||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000001"))
|
||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002"))
|
||||
machine.send_chars("exit\n")
|
||||
machine.wait_until_fails("pgrep foot")
|
||||
# Verify acl cleanup on XDG_RUNTIME_DIR:
|
||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000001")
|
||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002")
|
||||
|
||||
# Start app (foot) with Wayland enablement from a terminal:
|
||||
swaymsg(
|
||||
"exec foot $SHELL -c '(ne-foot) & sleep 1 && fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && cat'")
|
||||
wait_for_window("u0_a1@machine")
|
||||
wait_for_window("u0_a2@machine")
|
||||
machine.send_chars("clear; wayland-info && touch /tmp/success-client-term\n")
|
||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/success-client-term")
|
||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-client-term")
|
||||
machine.wait_for_file("/tmp/ps-show-ok")
|
||||
collect_state_ui("foot_wayland_term")
|
||||
check_state("ne-foot", 1)
|
||||
@ -150,9 +176,9 @@ machine.wait_until_fails("pgrep foot")
|
||||
|
||||
# Test PulseAudio (fortify does not support PipeWire yet):
|
||||
swaymsg("exec pa-foot")
|
||||
wait_for_window("u0_a2@machine")
|
||||
wait_for_window("u0_a3@machine")
|
||||
machine.send_chars("clear; pactl info && touch /tmp/success-pulse\n")
|
||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-pulse")
|
||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/3/success-pulse")
|
||||
collect_state_ui("pulse_wayland")
|
||||
check_state("pa-foot", 9)
|
||||
machine.send_chars("exit\n")
|
||||
@ -160,9 +186,9 @@ machine.wait_until_fails("pgrep foot")
|
||||
|
||||
# Test XWayland (foot does not support X):
|
||||
swaymsg("exec x11-alacritty")
|
||||
wait_for_window("u0_a3@machine")
|
||||
wait_for_window("u0_a4@machine")
|
||||
machine.send_chars("clear; glinfo && touch /tmp/success-client-x11\n")
|
||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/3/success-client-x11")
|
||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/4/success-client-x11")
|
||||
collect_state_ui("alacritty_x11")
|
||||
check_state("x11-alacritty", 2)
|
||||
machine.send_chars("exit\n")
|
||||
@ -170,24 +196,23 @@ machine.wait_until_fails("pgrep alacritty")
|
||||
|
||||
# Start app (foot) with direct Wayland access:
|
||||
swaymsg("exec da-foot")
|
||||
wait_for_window("u0_a4@machine")
|
||||
wait_for_window("u0_a5@machine")
|
||||
machine.send_chars("clear; wayland-info && touch /tmp/success-direct\n")
|
||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/4/success-direct")
|
||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/5/success-direct")
|
||||
collect_state_ui("foot_direct")
|
||||
check_state("da-foot", 1)
|
||||
# Verify acl on XDG_RUNTIME_DIR:
|
||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000004"))
|
||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000005"))
|
||||
machine.send_chars("exit\n")
|
||||
machine.wait_until_fails("pgrep foot")
|
||||
# Verify acl cleanup on XDG_RUNTIME_DIR:
|
||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000004")
|
||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000005")
|
||||
|
||||
# Test syscall filter:
|
||||
print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure"))
|
||||
|
||||
# Exit Sway and verify process exit status 0:
|
||||
swaymsg("exit", succeed=False)
|
||||
machine.wait_until_fails("pgrep -x sway")
|
||||
machine.wait_for_file("/tmp/sway-exit-ok")
|
||||
|
||||
# Print fortify runDir contents:
|
||||
|
Loading…
Reference in New Issue
Block a user