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/*
|
path: result/*
|
||||||
retention-days: 1
|
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:
|
race:
|
||||||
name: Data race detector
|
name: Data race detector
|
||||||
runs-on: nix
|
runs-on: nix
|
||||||
@ -43,6 +60,7 @@ jobs:
|
|||||||
name: Flake checks
|
name: Flake checks
|
||||||
needs:
|
needs:
|
||||||
- fortify
|
- fortify
|
||||||
|
- fpkg
|
||||||
- race
|
- race
|
||||||
runs-on: nix
|
runs-on: nix
|
||||||
steps:
|
steps:
|
||||||
|
@ -7,8 +7,9 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
lib,
|
lib,
|
||||||
|
stdenv,
|
||||||
|
closureInfo,
|
||||||
writeScript,
|
writeScript,
|
||||||
writeScriptBin,
|
|
||||||
runtimeShell,
|
runtimeShell,
|
||||||
writeText,
|
writeText,
|
||||||
symlinkJoin,
|
symlinkJoin,
|
||||||
@ -16,12 +17,15 @@
|
|||||||
runCommand,
|
runCommand,
|
||||||
fetchFromGitHub,
|
fetchFromGitHub,
|
||||||
|
|
||||||
|
zstd,
|
||||||
nix,
|
nix,
|
||||||
|
sqlite,
|
||||||
|
|
||||||
name ? throw "name is required",
|
name ? throw "name is required",
|
||||||
version ? throw "version is required",
|
version ? throw "version is required",
|
||||||
pname ? "${name}-${version}",
|
pname ? "${name}-${version}",
|
||||||
modules ? [ ],
|
modules ? [ ],
|
||||||
|
nixosModules ? [ ],
|
||||||
script ? ''
|
script ? ''
|
||||||
exec "$SHELL" "$@"
|
exec "$SHELL" "$@"
|
||||||
'',
|
'',
|
||||||
@ -73,6 +77,8 @@ let
|
|||||||
etc.nixpkgs.source = nixpkgs.outPath;
|
etc.nixpkgs.source = nixpkgs.outPath;
|
||||||
systemPackages = [ pkgs.nix ];
|
systemPackages = [ pkgs.nix ];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
imports = nixosModules;
|
||||||
};
|
};
|
||||||
nixos = nixpkgs.lib.nixosSystem {
|
nixos = nixpkgs.lib.nixosSystem {
|
||||||
inherit system;
|
inherit system;
|
||||||
@ -165,11 +171,7 @@ let
|
|||||||
broadcast = { };
|
broadcast = { };
|
||||||
});
|
});
|
||||||
|
|
||||||
enablements =
|
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);
|
||||||
(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;
|
mesa = if gpu then mesaWrappers else null;
|
||||||
nix_gl = if gpu then nixGL else null;
|
nix_gl = if gpu then nixGL else null;
|
||||||
@ -178,26 +180,73 @@ let
|
|||||||
};
|
};
|
||||||
in
|
in
|
||||||
|
|
||||||
writeScriptBin "build-fpkg-${pname}" ''
|
stdenv.mkDerivation {
|
||||||
#!${runtimeShell} -el
|
name = "${pname}.pkg";
|
||||||
OUT="$(mktemp -d)"
|
inherit version;
|
||||||
TAR="$(mktemp -u)"
|
__structuredAttrs = true;
|
||||||
set -x
|
|
||||||
|
|
||||||
nix copy --no-check-sigs --to "$OUT" "${nix}" "${nixos.config.system.build.toplevel}"
|
nativeBuildInputs = [
|
||||||
nix store --store "$OUT" optimise
|
zstd
|
||||||
chmod -R +r "$OUT/nix/var"
|
nix
|
||||||
nix copy --no-check-sigs --to "file://$OUT/res?compression=zstd&compression-level=19¶llel-compression=true" \
|
sqlite
|
||||||
"${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"
|
|
||||||
|
|
||||||
# creating an intermediate file improves zstd performance
|
buildCommand = ''
|
||||||
tar -C "$OUT" -cf "$TAR" .
|
NIX_ROOT="$(mktemp -d)"
|
||||||
chmod +w -R "$OUT" && rm -rf "$OUT"
|
export USER="nobody"
|
||||||
|
|
||||||
zstd -T0 -19 -fo "${pname}.pkg" "$TAR"
|
# create bootstrap store
|
||||||
rm "$TAR"
|
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()
|
|
||||||
}
|
|
402
cmd/fpkg/main.go
402
cmd/fpkg/main.go
@ -1,50 +1,402 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"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"
|
"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/fmsg"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/sys"
|
||||||
)
|
)
|
||||||
|
|
||||||
const shellPath = "/run/current-system/sw/bin/bash"
|
const shellPath = "/run/current-system/sw/bin/bash"
|
||||||
|
|
||||||
|
var (
|
||||||
|
errSuccess = errors.New("success")
|
||||||
|
|
||||||
|
std sys.State = new(sys.Std)
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
fmsg.Prepare("fpkg")
|
||||||
if err := os.Setenv("SHELL", shellPath); err != nil {
|
if err := os.Setenv("SHELL", shellPath); err != nil {
|
||||||
log.Fatalf("cannot set $SHELL: %v", err)
|
log.Fatalf("cannot set $SHELL: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
flagVerbose bool
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fmsg.Prepare("fpkg")
|
// early init argv0 check, skips root check and duplicate PR_SET_DUMPABLE
|
||||||
|
init0.TryArgv0()
|
||||||
|
|
||||||
flag.Parse()
|
if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
|
||||||
fmsg.Store(flagVerbose)
|
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
|
||||||
|
// not fatal: this program runs as the privileged user
|
||||||
args := flag.Args()
|
|
||||||
if len(args) < 1 {
|
|
||||||
log.Fatal("invalid argument")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch args[0] {
|
if os.Geteuid() == 0 {
|
||||||
case "install":
|
log.Fatal("this program must not run as root")
|
||||||
actionInstall(args[1:])
|
|
||||||
case "start":
|
|
||||||
actionStart(args[1:])
|
|
||||||
|
|
||||||
default:
|
|
||||||
log.Fatal("invalid argument")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal.Exit(0)
|
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")
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.Println("invalid argument")
|
||||||
|
return syscall.EINVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Parse app metadata.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
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"
|
"strconv"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -69,3 +70,32 @@ func pathSetByApp(id string) *appPathSet {
|
|||||||
pathSet.nixPath = path.Join(pathSet.cacheDir, "nix")
|
pathSet.nixPath = path.Join(pathSet.cacheDir, "nix")
|
||||||
return pathSet
|
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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"context"
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
"git.gensokyo.uk/security/fortify/internal"
|
"git.gensokyo.uk/security/fortify/internal/app"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
)
|
)
|
||||||
|
|
||||||
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
|
func mustRunApp(ctx context.Context, config *fst.Config, beforeFail func()) {
|
||||||
|
rs := new(fst.RunState)
|
||||||
|
a := app.MustNew(std)
|
||||||
|
|
||||||
var (
|
if sa, err := a.Seal(config); err != nil {
|
||||||
Fmain = compPoison
|
fmsg.PrintBaseError(err, "cannot seal app:")
|
||||||
)
|
rs.ExitCode = 1
|
||||||
|
|
||||||
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)
|
|
||||||
} else {
|
} else {
|
||||||
if fmsg.Load() {
|
// this updates ExitCode
|
||||||
cmd = exec.Command(p, "-v", "app", "3")
|
app.PrintRunStateErr(rs, sa.Run(ctx, rs))
|
||||||
} 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
if rs.ExitCode != 0 {
|
||||||
if err := json.NewEncoder(st).Encode(config); err != nil {
|
|
||||||
beforeFail()
|
|
||||||
log.Fatalf("cannot send configuration: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
beforeFail()
|
beforeFail()
|
||||||
log.Fatalf("cannot start fortify: %v", err)
|
os.Exit(rs.ExitCode)
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -10,10 +11,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func withNixDaemon(
|
func withNixDaemon(
|
||||||
|
ctx context.Context,
|
||||||
action string, command []string, net bool, updateConfig func(config *fst.Config) *fst.Config,
|
action string, command []string, net bool, updateConfig func(config *fst.Config) *fst.Config,
|
||||||
app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
|
app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
|
||||||
) {
|
) {
|
||||||
fortifyAppDropShell(updateConfig(&fst.Config{
|
mustRunAppDropShell(ctx, updateConfig(&fst.Config{
|
||||||
ID: app.ID,
|
ID: app.ID,
|
||||||
Command: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
|
Command: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
|
||||||
// start nix-daemon
|
// start nix-daemon
|
||||||
@ -56,8 +58,11 @@ func withNixDaemon(
|
|||||||
}), dropShell, beforeFail)
|
}), dropShell, beforeFail)
|
||||||
}
|
}
|
||||||
|
|
||||||
func withCacheDir(action string, command []string, workDir string, app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
|
func withCacheDir(
|
||||||
fortifyAppDropShell(&fst.Config{
|
ctx context.Context,
|
||||||
|
action string, command []string, workDir string,
|
||||||
|
app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
|
||||||
|
mustRunAppDropShell(ctx, &fst.Config{
|
||||||
ID: app.ID,
|
ID: app.ID,
|
||||||
Command: []string{shellPath, "-lc", strings.Join(command, " && ")},
|
Command: []string{shellPath, "-lc", strings.Join(command, " && ")},
|
||||||
Confinement: fst.ConfinementConfig{
|
Confinement: fst.ConfinementConfig{
|
||||||
@ -90,12 +95,12 @@ func withCacheDir(action string, command []string, workDir string, app *bundleIn
|
|||||||
}, dropShell, beforeFail)
|
}, 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 {
|
if dropShell {
|
||||||
config.Command = []string{shellPath, "-l"}
|
config.Command = []string{shellPath, "-l"}
|
||||||
fortifyApp(config, beforeFail)
|
mustRunApp(ctx, config, beforeFail)
|
||||||
beforeFail()
|
beforeFail()
|
||||||
internal.Exit(0)
|
internal.Exit(0)
|
||||||
}
|
}
|
||||||
fortifyApp(config, beforeFail)
|
mustRunApp(ctx, config, beforeFail)
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
|
|
||||||
fsuConfFile = "/etc/fsurc"
|
fsuConfFile = "/etc/fsurc"
|
||||||
envShim = "FORTIFY_SHIM"
|
envShim = "FORTIFY_SHIM"
|
||||||
envAID = "FORTIFY_APP_ID"
|
envAID = "FORTIFY_APP_ID"
|
||||||
@ -22,10 +21,6 @@ const (
|
|||||||
PR_SET_NO_NEW_PRIVS = 0x26
|
PR_SET_NO_NEW_PRIVS = 0x26
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
Fmain = compPoison
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log.SetFlags(0)
|
log.SetFlags(0)
|
||||||
log.SetPrefix("fsu: ")
|
log.SetPrefix("fsu: ")
|
||||||
@ -40,20 +35,16 @@ func main() {
|
|||||||
log.Fatal("this program must not be started by root")
|
log.Fatal("this program must not be started by root")
|
||||||
}
|
}
|
||||||
|
|
||||||
var fmain string
|
var toolPath string
|
||||||
if p, ok := checkPath(Fmain); !ok {
|
|
||||||
log.Fatal("invalid fortify path, this copy of fsu is not compiled correctly")
|
|
||||||
} else {
|
|
||||||
fmain = p
|
|
||||||
}
|
|
||||||
|
|
||||||
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
|
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
|
||||||
if p, err := os.Readlink(pexe); err != nil {
|
if p, err := os.Readlink(pexe); err != nil {
|
||||||
log.Fatalf("cannot read parent executable path: %v", err)
|
log.Fatalf("cannot read parent executable path: %v", err)
|
||||||
} else if strings.HasSuffix(p, " (deleted)") {
|
} else if strings.HasSuffix(p, " (deleted)") {
|
||||||
log.Fatal("fortify executable has been 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")
|
log.Fatal("this program must be started by fortify")
|
||||||
|
} else {
|
||||||
|
toolPath = p
|
||||||
}
|
}
|
||||||
|
|
||||||
// uid = 1000000 +
|
// uid = 1000000 +
|
||||||
@ -147,13 +138,9 @@ func main() {
|
|||||||
if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
|
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())
|
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)
|
log.Fatalf("cannot start shim: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
panic("unreachable")
|
panic("unreachable")
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkPath(p string) (string, bool) {
|
|
||||||
return p, p != compPoison && p != "" && path.IsAbs(p)
|
|
||||||
}
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
lib,
|
||||||
buildGoModule,
|
buildGoModule,
|
||||||
fortify ? abort "fortify package required",
|
fortify ? abort "fortify package required",
|
||||||
}:
|
}:
|
||||||
@ -15,5 +16,15 @@ buildGoModule {
|
|||||||
go mod init fsu >& /dev/null
|
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)
|
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
|
p.bwrap = bc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
7
dist/release.sh
vendored
7
dist/release.sh
vendored
@ -10,9 +10,10 @@ cp -rv "comp" "${out}"
|
|||||||
|
|
||||||
go generate ./...
|
go generate ./...
|
||||||
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w -buildid= -extldflags '-static'
|
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.version=${VERSION}
|
||||||
-X git.gensokyo.uk/security/fortify/internal.Fsu=/usr/bin/fsu
|
-X git.gensokyo.uk/security/fortify/internal.fsu=/usr/bin/fsu
|
||||||
-X main.Fmain=/usr/bin/fortify" ./...
|
-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 -f "./${out}.tar.gz" && tar -C dist -czf "${out}.tar.gz" "${pname}"
|
||||||
rm -rf "./${out}"
|
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
37
flake.nix
37
flake.nix
@ -58,6 +58,7 @@
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
fortify = callPackage ./test { inherit system self; };
|
fortify = callPackage ./test { inherit system self; };
|
||||||
|
fpkg = callPackage ./cmd/fpkg/test { inherit system self; };
|
||||||
race = callPackage ./test {
|
race = callPackage ./test {
|
||||||
inherit system self;
|
inherit system self;
|
||||||
withRace = true;
|
withRace = true;
|
||||||
@ -67,7 +68,7 @@
|
|||||||
cd ${./.}
|
cd ${./.}
|
||||||
|
|
||||||
echo "running nixfmt..."
|
echo "running nixfmt..."
|
||||||
nixfmt --check .
|
nixfmt --width=256 --check .
|
||||||
|
|
||||||
touch $out
|
touch $out
|
||||||
'';
|
'';
|
||||||
@ -97,28 +98,36 @@
|
|||||||
packages = forAllSystems (
|
packages = forAllSystems (
|
||||||
system:
|
system:
|
||||||
let
|
let
|
||||||
inherit (self.packages.${system}) fortify;
|
inherit (self.packages.${system}) fortify fsu;
|
||||||
pkgs = nixpkgsFor.${system};
|
pkgs = nixpkgsFor.${system};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
default = self.packages.${system}.fortify;
|
default = fortify;
|
||||||
fortify = pkgs.pkgsStatic.callPackage ./package.nix {
|
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; };
|
fsu = pkgs.callPackage ./cmd/fsu/package.nix { inherit (self.packages.${system}) fortify; };
|
||||||
|
|
||||||
dist =
|
dist = pkgs.runCommand "${fortify.name}-dist" { inherit (self.devShells.${system}.default) buildInputs; } ''
|
||||||
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)"
|
||||||
# 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
|
# 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}"
|
export FORTIFY_VERSION="v${fortify.version}"
|
||||||
./dist/release.sh && mkdir $out && cp -v "dist/fortify-$FORTIFY_VERSION.tar.gz"* $out
|
./dist/release.sh && mkdir $out && cp -v "dist/fortify-$FORTIFY_VERSION.tar.gz"* $out
|
||||||
'';
|
'';
|
||||||
|
|
||||||
fhs = pkgs.buildFHSEnv {
|
fhs = pkgs.buildFHSEnv {
|
||||||
pname = "fortify-fhs";
|
pname = "fortify-fhs";
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
"git.gensokyo.uk/security/fortify/helper/proc"
|
||||||
@ -23,6 +24,9 @@ type bubblewrap struct {
|
|||||||
// name of the command to run in bwrap
|
// name of the command to run in bwrap
|
||||||
name string
|
name string
|
||||||
|
|
||||||
|
// whether to set process group id
|
||||||
|
setpgid bool
|
||||||
|
|
||||||
lock sync.RWMutex
|
lock sync.RWMutex
|
||||||
*helperCmd
|
*helperCmd
|
||||||
}
|
}
|
||||||
@ -38,6 +42,10 @@ func (b *bubblewrap) Start(ctx context.Context, stat bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
args := b.finalise(ctx, stat)
|
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 = 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", strconv.Itoa(int(b.argsFd)), "--", b.name)
|
||||||
b.Cmd.Args = append(b.Cmd.Args, args...)
|
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.
|
// 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.
|
// Function argF returns an array of arguments passed directly to the child process.
|
||||||
func MustNewBwrap(
|
func MustNewBwrap(
|
||||||
conf *bwrap.Config, name string,
|
conf *bwrap.Config, name string, setpgid bool,
|
||||||
wt io.WriterTo, argF func(argsFD, statFD int) []string,
|
wt io.WriterTo, argF func(argsFD, statFD int) []string,
|
||||||
extraFiles []*os.File,
|
extraFiles []*os.File,
|
||||||
syncFd *os.File,
|
syncFd *os.File,
|
||||||
) Helper {
|
) Helper {
|
||||||
b, err := NewBwrap(conf, name, wt, argF, extraFiles, syncFd)
|
b, err := NewBwrap(conf, name, setpgid, wt, argF, extraFiles, syncFd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err.Error())
|
panic(err.Error())
|
||||||
} else {
|
} else {
|
||||||
@ -65,7 +73,7 @@ func MustNewBwrap(
|
|||||||
// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
|
// 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.
|
// Function argF returns an array of arguments passed directly to the child process.
|
||||||
func NewBwrap(
|
func NewBwrap(
|
||||||
conf *bwrap.Config, name string,
|
conf *bwrap.Config, name string, setpgid bool,
|
||||||
wt io.WriterTo, argF func(argsFd, statFd int) []string,
|
wt io.WriterTo, argF func(argsFd, statFd int) []string,
|
||||||
extraFiles []*os.File,
|
extraFiles []*os.File,
|
||||||
syncFd *os.File,
|
syncFd *os.File,
|
||||||
@ -73,6 +81,7 @@ func NewBwrap(
|
|||||||
b := new(bubblewrap)
|
b := new(bubblewrap)
|
||||||
|
|
||||||
b.name = name
|
b.name = name
|
||||||
|
b.setpgid = setpgid
|
||||||
b.helperCmd = newHelperCmd(b, BubblewrapName, wt, argF, extraFiles)
|
b.helperCmd = newHelperCmd(b, BubblewrapName, wt, argF, extraFiles)
|
||||||
|
|
||||||
if v, err := NewCheckedArgs(conf.Args(syncFd, b.extraFiles, &b.files)); err != nil {
|
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(
|
h := helper.MustNewBwrap(
|
||||||
sc, "fortify",
|
sc, "fortify", false,
|
||||||
argsWt, argF,
|
argsWt, argF,
|
||||||
nil, nil,
|
nil, nil,
|
||||||
)
|
)
|
||||||
@ -44,7 +44,7 @@ func TestBwrap(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("valid new helper nil check", func(t *testing.T) {
|
t.Run("valid new helper nil check", func(t *testing.T) {
|
||||||
if got := helper.MustNewBwrap(
|
if got := helper.MustNewBwrap(
|
||||||
sc, "fortify",
|
sc, "fortify", false,
|
||||||
argsWt, argF,
|
argsWt, argF,
|
||||||
nil, nil,
|
nil, nil,
|
||||||
); got == nil {
|
); got == nil {
|
||||||
@ -64,7 +64,7 @@ func TestBwrap(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
helper.MustNewBwrap(
|
helper.MustNewBwrap(
|
||||||
&bwrap.Config{Hostname: "\x00"}, "fortify",
|
&bwrap.Config{Hostname: "\x00"}, "fortify", false,
|
||||||
nil, argF,
|
nil, argF,
|
||||||
nil, nil,
|
nil, nil,
|
||||||
)
|
)
|
||||||
@ -74,7 +74,7 @@ func TestBwrap(t *testing.T) {
|
|||||||
helper.InternalReplaceExecCommand(t)
|
helper.InternalReplaceExecCommand(t)
|
||||||
|
|
||||||
h := helper.MustNewBwrap(
|
h := helper.MustNewBwrap(
|
||||||
sc, "crash-test-dummy",
|
sc, "crash-test-dummy", false,
|
||||||
nil, argFChecked,
|
nil, argFChecked,
|
||||||
nil, nil,
|
nil, nil,
|
||||||
)
|
)
|
||||||
@ -98,6 +98,11 @@ func TestBwrap(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("implementation compliance", func(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/fst"
|
||||||
"git.gensokyo.uk/security/fortify/helper"
|
"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/app/shim"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
"git.gensokyo.uk/security/fortify/internal/state"
|
"git.gensokyo.uk/security/fortify/internal/state"
|
||||||
@ -32,6 +33,10 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
|
|||||||
panic("invalid state")
|
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
|
resolve exec paths
|
||||||
*/
|
*/
|
||||||
@ -79,7 +84,8 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
|
|||||||
ec.Set(system.Process)
|
ec.Set(system.Process)
|
||||||
if states, err := c.Load(); err != nil {
|
if states, err := c.Load(); err != nil {
|
||||||
// revert per-process state here to limit damage
|
// 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 {
|
} else {
|
||||||
if l := len(states); l == 0 {
|
if l := len(states); l == 0 {
|
||||||
fmsg.Verbose("no other launchers active, will clean up globals")
|
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)
|
return seal.sys.Revert(ec)
|
||||||
if err != nil {
|
|
||||||
err = err.(RevertCompoundError)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}()
|
}()
|
||||||
})
|
})
|
||||||
storeErr.Err = errors.Join(revertErr, store.Close())
|
storeErr.save([]error{revertErr, store.Close()})
|
||||||
rs.RevertErr = storeErr.equiv("error returned during cleanup:")
|
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,
|
Time: *rs.Time,
|
||||||
}
|
}
|
||||||
var earlyStoreErr = new(StateStoreError) // returned after blocking on waitErr
|
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
|
// destroy defunct state entry
|
||||||
deferredStoreFunc = func(c state.Cursor) error { return c.Destroy(seal.id.unwrap()) }
|
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:")
|
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
|
seccomp.CPrintln = log.Println
|
||||||
}
|
}
|
||||||
if b, err := helper.NewBwrap(
|
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) },
|
nil, func(int, int) []string { return make([]string, 0) },
|
||||||
extraFiles,
|
extraFiles,
|
||||||
syncFd,
|
syncFd,
|
||||||
|
@ -52,14 +52,8 @@ func (s *Shim) Start(
|
|||||||
syncFd *os.File,
|
syncFd *os.File,
|
||||||
) (*time.Time, error) {
|
) (*time.Time, error) {
|
||||||
// prepare user switcher invocation
|
// prepare user switcher invocation
|
||||||
var fsu string
|
fsuPath := internal.MustFsuPath()
|
||||||
if p, ok := internal.Path(internal.Fsu); !ok {
|
s.cmd = exec.Command(fsuPath)
|
||||||
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)
|
|
||||||
|
|
||||||
// pass shim setup pipe
|
// pass shim setup pipe
|
||||||
if fd, e, err := proc.Setup(&s.cmd.ExtraFiles); err != nil {
|
if fd, e, err := proc.Setup(&s.cmd.ExtraFiles); err != nil {
|
||||||
|
@ -3,10 +3,15 @@ package internal
|
|||||||
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
|
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Version = compPoison
|
version = compPoison
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check validates string value set at compile time.
|
// check validates string value set at compile time.
|
||||||
func Check(s string) (string, bool) {
|
func check(s string) (string, bool) { return s, s != compPoison && s != "" }
|
||||||
return s, s != compPoison && s != ""
|
|
||||||
|
func Version() string {
|
||||||
|
if v, ok := check(version); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return "impure"
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,23 @@
|
|||||||
package internal
|
package internal
|
||||||
|
|
||||||
import "path"
|
import (
|
||||||
|
"log"
|
||||||
|
"path"
|
||||||
|
|
||||||
var (
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
Fsu = compPoison
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Path(p string) (string, bool) {
|
var (
|
||||||
return p, p != compPoison && p != "" && path.IsAbs(p)
|
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"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/user"
|
"os/user"
|
||||||
@ -79,32 +78,27 @@ func (s *Std) Uid(aid int) (int, error) {
|
|||||||
defer func() { s.uidCopy[aid] = u }()
|
defer func() { s.uidCopy[aid] = u }()
|
||||||
|
|
||||||
u.uid = -1
|
u.uid = -1
|
||||||
if fsu, ok := internal.Check(internal.Fsu); !ok {
|
fsuPath := internal.MustFsuPath()
|
||||||
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
|
|
||||||
cmd.Stderr = os.Stderr // pass through fatal messages
|
|
||||||
cmd.Env = []string{"FORTIFY_APP_ID=" + strconv.Itoa(aid)}
|
|
||||||
cmd.Dir = "/"
|
|
||||||
var (
|
|
||||||
p []byte
|
|
||||||
exitError *exec.ExitError
|
|
||||||
)
|
|
||||||
|
|
||||||
if p, u.err = cmd.Output(); u.err == nil {
|
cmd := exec.Command(fsuPath)
|
||||||
u.uid, u.err = strconv.Atoi(string(p))
|
cmd.Path = fsuPath
|
||||||
if u.err != nil {
|
cmd.Stderr = os.Stderr // pass through fatal messages
|
||||||
u.err = fmsg.WrapErrorSuffix(u.err, "cannot parse uid from fsu:")
|
cmd.Env = []string{"FORTIFY_APP_ID=" + strconv.Itoa(aid)}
|
||||||
}
|
cmd.Dir = "/"
|
||||||
} else if errors.As(u.err, &exitError) && exitError != nil && exitError.ExitCode() == 1 {
|
var (
|
||||||
u.err = fmsg.WrapError(syscall.EACCES, "") // fsu prints to stderr in this case
|
p []byte
|
||||||
} else if os.IsNotExist(u.err) {
|
exitError *exec.ExitError
|
||||||
u.err = fmsg.WrapError(os.ErrNotExist, fmt.Sprintf("the setuid helper is missing: %s", fsu))
|
)
|
||||||
|
|
||||||
|
if p, u.err = cmd.Output(); u.err == nil {
|
||||||
|
u.uid, u.err = strconv.Atoi(string(p))
|
||||||
|
if u.err != nil {
|
||||||
|
u.err = fmsg.WrapErrorSuffix(u.err, "cannot parse uid from fsu:")
|
||||||
}
|
}
|
||||||
return u.uid, u.err
|
} 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", 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},
|
Syscall: &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
|
||||||
NewSession: true,
|
NewSession: true,
|
||||||
DieWithParent: true,
|
DieWithParent: true,
|
||||||
}).Bind("/", "/").DevTmpfs("/dev"), toolPath,
|
}).Bind("/", "/").DevTmpfs("/dev"), toolPath, false,
|
||||||
nil, func(_, _ int) []string { return []string{p} },
|
nil, func(_, _ int) []string { return []string{p} },
|
||||||
nil, nil,
|
nil, nil,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
52
main.go
52
main.go
@ -68,10 +68,20 @@ func buildCommand(out io.Writer) command.Command {
|
|||||||
flagVerbose bool
|
flagVerbose bool
|
||||||
flagJSON 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(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
|
||||||
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable")
|
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable")
|
||||||
|
|
||||||
|
// internal commands
|
||||||
|
c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess })
|
||||||
|
c.Command("init", command.UsageInternal, func([]string) error { init0.Main(); return errSuccess })
|
||||||
|
|
||||||
c.Command("app", "Launch app defined by the specified config file", func(args []string) error {
|
c.Command("app", "Launch app defined by the specified config file", func(args []string) error {
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
log.Fatal("app requires at least 1 argument")
|
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")
|
}).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id")
|
||||||
|
|
||||||
c.Command("version", "Show fortify version", func(args []string) error {
|
c.Command("version", "Show fortify version", func(args []string) error {
|
||||||
if v, ok := internal.Check(internal.Version); ok {
|
fmt.Println(internal.Version())
|
||||||
fmt.Println(v)
|
|
||||||
} else {
|
|
||||||
fmt.Println("impure")
|
|
||||||
}
|
|
||||||
return errSuccess
|
return errSuccess
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -272,45 +278,21 @@ func buildCommand(out io.Writer) command.Command {
|
|||||||
return errSuccess
|
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
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func runApp(a fst.App, config *fst.Config) {
|
func runApp(a fst.App, config *fst.Config) {
|
||||||
rs := new(fst.RunState)
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(),
|
ctx, stop := signal.NotifyContext(context.Background(),
|
||||||
syscall.SIGINT, syscall.SIGTERM)
|
syscall.SIGINT, syscall.SIGTERM)
|
||||||
defer stop() // unreachable
|
defer stop() // unreachable
|
||||||
|
|
||||||
if fmsg.Load() {
|
rs := new(fst.RunState)
|
||||||
seccomp.CPrintln = log.Println
|
|
||||||
}
|
|
||||||
|
|
||||||
if sa, err := a.Seal(config); err != nil {
|
if sa, err := a.Seal(config); err != nil {
|
||||||
fmsg.PrintBaseError(err, "cannot seal app:")
|
fmsg.PrintBaseError(err, "cannot seal app:")
|
||||||
internal.Exit(1)
|
rs.ExitCode = 1
|
||||||
} else if err = sa.Run(ctx, rs); err != nil {
|
} else {
|
||||||
if rs.Time == nil {
|
// this updates ExitCode
|
||||||
fmsg.PrintBaseError(err, "cannot start app:")
|
app.PrintRunStateErr(rs, sa.Run(ctx, rs))
|
||||||
} 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)
|
|
||||||
}
|
}
|
||||||
internal.Exit(rs.ExitCode)
|
internal.Exit(rs.ExitCode)
|
||||||
}
|
}
|
||||||
|
17
nixos.nix
17
nixos.nix
@ -77,21 +77,12 @@ in
|
|||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
session_bus =
|
session_bus = if app.dbus.session != null then (app.dbus.session (extendDBusDefault app.id)) else (extendDBusDefault app.id default);
|
||||||
if app.dbus.session != null then
|
|
||||||
(app.dbus.session (extendDBusDefault app.id))
|
|
||||||
else
|
|
||||||
(extendDBusDefault app.id default);
|
|
||||||
system_bus = app.dbus.system;
|
system_bus = app.dbus.system;
|
||||||
};
|
};
|
||||||
command = if app.command == null then app.name else app.command;
|
command = if app.command == null then app.name else app.command;
|
||||||
script = if app.script == null then ("exec " + command + " $@") else app.script;
|
script = if app.script == null then ("exec " + command + " $@") else app.script;
|
||||||
enablements =
|
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);
|
||||||
with app.capability;
|
|
||||||
(if wayland then 1 else 0)
|
|
||||||
+ (if x11 then 2 else 0)
|
|
||||||
+ (if dbus then 4 else 0)
|
|
||||||
+ (if pulse then 8 else 0);
|
|
||||||
conf = {
|
conf = {
|
||||||
inherit (app) id;
|
inherit (app) id;
|
||||||
command = [
|
command = [
|
||||||
@ -165,9 +156,7 @@ in
|
|||||||
};
|
};
|
||||||
in
|
in
|
||||||
pkgs.writeShellScriptBin app.name ''
|
pkgs.writeShellScriptBin app.name ''
|
||||||
exec fortify${
|
exec fortify${if app.verbose then " -v" else ""} app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@
|
||||||
if app.verbose then " -v" else ""
|
|
||||||
} app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@
|
|
||||||
''
|
''
|
||||||
) cfg.apps;
|
) cfg.apps;
|
||||||
in
|
in
|
||||||
|
@ -3,7 +3,14 @@
|
|||||||
let
|
let
|
||||||
inherit (lib) types mkOption mkEnableOption;
|
inherit (lib) types mkOption mkEnableOption;
|
||||||
fortify = pkgs.pkgsStatic.callPackage ./package.nix {
|
fortify = pkgs.pkgsStatic.callPackage ./package.nix {
|
||||||
inherit (pkgs) bubblewrap xdg-dbus-proxy glibc;
|
inherit (pkgs)
|
||||||
|
bubblewrap
|
||||||
|
xdg-dbus-proxy
|
||||||
|
glibc
|
||||||
|
zstd
|
||||||
|
gnutar
|
||||||
|
coreutils
|
||||||
|
;
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
|
|
||||||
|
58
package.nix
58
package.nix
@ -14,6 +14,11 @@
|
|||||||
wayland-scanner,
|
wayland-scanner,
|
||||||
xorg,
|
xorg,
|
||||||
|
|
||||||
|
# for fpkg
|
||||||
|
zstd,
|
||||||
|
gnutar,
|
||||||
|
coreutils,
|
||||||
|
|
||||||
glibc, # for ldd
|
glibc, # for ldd
|
||||||
withStatic ? stdenv.hostPlatform.isStatic,
|
withStatic ? stdenv.hostPlatform.isStatic,
|
||||||
}:
|
}:
|
||||||
@ -25,10 +30,7 @@ buildGoModule rec {
|
|||||||
src = builtins.path {
|
src = builtins.path {
|
||||||
name = "${pname}-src";
|
name = "${pname}-src";
|
||||||
path = lib.cleanSource ./.;
|
path = lib.cleanSource ./.;
|
||||||
filter =
|
filter = path: type: !(type == "regular" && (lib.hasSuffix ".nix" path || lib.hasSuffix ".py" path)) && !(type == "directory" && lib.hasSuffix "/cmd/fsu" path);
|
||||||
path: type:
|
|
||||||
!(type == "regular" && lib.hasSuffix ".nix" path)
|
|
||||||
&& !(type == "directory" && lib.hasSuffix "/cmd/fsu" path);
|
|
||||||
};
|
};
|
||||||
vendorHash = null;
|
vendorHash = null;
|
||||||
|
|
||||||
@ -39,17 +41,15 @@ buildGoModule rec {
|
|||||||
ldflags ++ [ "-X git.gensokyo.uk/security/fortify/internal.${name}=${value}" ]
|
ldflags ++ [ "-X git.gensokyo.uk/security/fortify/internal.${name}=${value}" ]
|
||||||
)
|
)
|
||||||
(
|
(
|
||||||
[
|
[ "-s -w" ]
|
||||||
"-s -w"
|
|
||||||
]
|
|
||||||
++ lib.optionals withStatic [
|
++ lib.optionals withStatic [
|
||||||
"-linkmode external"
|
"-linkmode external"
|
||||||
"-extldflags \"-static\""
|
"-extldflags \"-static\""
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Version = "v${version}";
|
version = "v${version}";
|
||||||
Fsu = "/run/wrappers/bin/fsu";
|
fsu = "/run/wrappers/bin/fsu";
|
||||||
};
|
};
|
||||||
|
|
||||||
# nix build environment does not allow acls
|
# nix build environment does not allow acls
|
||||||
@ -79,19 +79,33 @@ buildGoModule rec {
|
|||||||
HOME="$(mktemp -d)" PATH="${pkg-config}/bin:$PATH" go generate ./...
|
HOME="$(mktemp -d)" PATH="${pkg-config}/bin:$PATH" go generate ./...
|
||||||
'';
|
'';
|
||||||
|
|
||||||
postInstall = ''
|
postInstall =
|
||||||
install -D --target-directory=$out/share/zsh/site-functions comp/*
|
let
|
||||||
|
appPackages = [
|
||||||
|
glibc
|
||||||
|
bubblewrap
|
||||||
|
xdg-dbus-proxy
|
||||||
|
];
|
||||||
|
in
|
||||||
|
''
|
||||||
|
install -D --target-directory=$out/share/zsh/site-functions comp/*
|
||||||
|
|
||||||
mkdir "$out/libexec"
|
mkdir "$out/libexec"
|
||||||
mv "$out"/bin/* "$out/libexec/"
|
mv "$out"/bin/* "$out/libexec/"
|
||||||
|
|
||||||
makeBinaryWrapper "$out/libexec/fortify" "$out/bin/fortify" \
|
makeBinaryWrapper "$out/libexec/fortify" "$out/bin/fortify" \
|
||||||
--inherit-argv0 --prefix PATH : ${
|
--inherit-argv0 --prefix PATH : ${lib.makeBinPath appPackages}
|
||||||
lib.makeBinPath [
|
|
||||||
glibc
|
makeBinaryWrapper "$out/libexec/fpkg" "$out/bin/fpkg" \
|
||||||
bubblewrap
|
--inherit-argv0 --prefix PATH : ${
|
||||||
xdg-dbus-proxy
|
lib.makeBinPath (
|
||||||
]
|
appPackages
|
||||||
}
|
++ [
|
||||||
'';
|
zstd
|
||||||
|
gnutar
|
||||||
|
coreutils
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
'';
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package system
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/acl"
|
"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 {
|
func (a *ACL) revert(sys *I, ec *Criteria) error {
|
||||||
if ec.hasType(a) {
|
if ec.hasType(a) {
|
||||||
sys.println("stripping ACL", 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))
|
fmt.Sprintf("cannot strip ACL entry from %q:", a.path))
|
||||||
} else {
|
} else {
|
||||||
sys.println("skipping ACL", a)
|
sys.println("skipping ACL", a)
|
||||||
|
@ -2,6 +2,7 @@ package system
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
@ -96,7 +97,12 @@ func (d *DBus) revert(sys *I, _ *Criteria) error {
|
|||||||
sys.println("terminating message bus proxy")
|
sys.println("terminating message bus proxy")
|
||||||
d.proxy.Close()
|
d.proxy.Close()
|
||||||
defer sys.println("message bus proxy exit")
|
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 {
|
func (d *DBus) Is(o Op) bool {
|
||||||
|
@ -102,6 +102,21 @@
|
|||||||
home-manager = _: _: { home.stateVersion = "23.05"; };
|
home-manager = _: _: { home.stateVersion = "23.05"; };
|
||||||
|
|
||||||
apps = [
|
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";
|
name = "ne-foot";
|
||||||
verbose = true;
|
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")
|
denyOutputVerbose = machine.fail("sudo -u untrusted -i fortify -v run &>/dev/stdout")
|
||||||
print(denyOutputVerbose)
|
print(denyOutputVerbose)
|
||||||
|
|
||||||
|
# Fail direct fsu call:
|
||||||
|
print(machine.fail("sudo -u alice -i fsu"))
|
||||||
|
|
||||||
# Verify PrintBaseError behaviour:
|
# Verify PrintBaseError behaviour:
|
||||||
if denyOutput != "fsu: uid 1001 is not in the fsurc file\n":
|
if denyOutput != "fsu: uid 1001 is not in the fsurc file\n":
|
||||||
raise Exception(f"unexpected deny output:\n{denyOutput}")
|
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":
|
if denyOutputVerbose != "fsu: uid 1001 is not in the fsurc file\nfortify: *cannot obtain uid from fsu: permission denied\n":
|
||||||
raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}")
|
raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}")
|
||||||
|
|
||||||
|
# Check sandbox state:
|
||||||
|
swaymsg("exec check-sandbox")
|
||||||
|
machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/sandbox-ok")
|
||||||
|
|
||||||
# Start fortify permissive defaults outside Wayland session:
|
# Start fortify permissive defaults outside Wayland session:
|
||||||
print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare"))
|
print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare"))
|
||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/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 != "":
|
if output != "":
|
||||||
raise Exception(f"unexpected output\n{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:
|
# Verify graceful failure on bad Wayland display name:
|
||||||
print(machine.fail("sudo -u alice -i fortify -v run --wayland true"))
|
print(machine.fail("sudo -u alice -i fortify -v run --wayland true"))
|
||||||
|
|
||||||
# Start fortify permissive defaults within Wayland session:
|
# Start fortify permissive defaults within Wayland session:
|
||||||
fortify(
|
fortify('-v run --wayland --dbus notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-done')
|
||||||
'-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")
|
machine.wait_for_file("/tmp/dbus-done")
|
||||||
collect_state_ui("dbus_notify_exited")
|
collect_state_ui("dbus_notify_exited")
|
||||||
machine.succeed("pkill -9 mako")
|
machine.succeed("pkill -9 mako")
|
||||||
|
|
||||||
# Start app (foot) with Wayland enablement:
|
# Start app (foot) with Wayland enablement:
|
||||||
swaymsg("exec ne-foot")
|
swaymsg("exec ne-foot")
|
||||||
wait_for_window("u0_a1@machine")
|
wait_for_window("u0_a2@machine")
|
||||||
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
|
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")
|
collect_state_ui("foot_wayland")
|
||||||
check_state("ne-foot", 1)
|
check_state("ne-foot", 1)
|
||||||
# Verify acl on XDG_RUNTIME_DIR:
|
# Verify acl on XDG_RUNTIME_DIR:
|
||||||
print(machine.succeed("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.send_chars("exit\n")
|
||||||
machine.wait_until_fails("pgrep foot")
|
machine.wait_until_fails("pgrep foot")
|
||||||
# Verify acl cleanup on XDG_RUNTIME_DIR:
|
# 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:
|
# Start app (foot) with Wayland enablement from a terminal:
|
||||||
swaymsg(
|
swaymsg(
|
||||||
"exec foot $SHELL -c '(ne-foot) & sleep 1 && fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && cat'")
|
"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.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")
|
machine.wait_for_file("/tmp/ps-show-ok")
|
||||||
collect_state_ui("foot_wayland_term")
|
collect_state_ui("foot_wayland_term")
|
||||||
check_state("ne-foot", 1)
|
check_state("ne-foot", 1)
|
||||||
@ -150,9 +176,9 @@ machine.wait_until_fails("pgrep foot")
|
|||||||
|
|
||||||
# Test PulseAudio (fortify does not support PipeWire yet):
|
# Test PulseAudio (fortify does not support PipeWire yet):
|
||||||
swaymsg("exec pa-foot")
|
swaymsg("exec pa-foot")
|
||||||
wait_for_window("u0_a2@machine")
|
wait_for_window("u0_a3@machine")
|
||||||
machine.send_chars("clear; pactl info && touch /tmp/success-pulse\n")
|
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")
|
collect_state_ui("pulse_wayland")
|
||||||
check_state("pa-foot", 9)
|
check_state("pa-foot", 9)
|
||||||
machine.send_chars("exit\n")
|
machine.send_chars("exit\n")
|
||||||
@ -160,9 +186,9 @@ machine.wait_until_fails("pgrep foot")
|
|||||||
|
|
||||||
# Test XWayland (foot does not support X):
|
# Test XWayland (foot does not support X):
|
||||||
swaymsg("exec x11-alacritty")
|
swaymsg("exec x11-alacritty")
|
||||||
wait_for_window("u0_a3@machine")
|
wait_for_window("u0_a4@machine")
|
||||||
machine.send_chars("clear; glinfo && touch /tmp/success-client-x11\n")
|
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")
|
collect_state_ui("alacritty_x11")
|
||||||
check_state("x11-alacritty", 2)
|
check_state("x11-alacritty", 2)
|
||||||
machine.send_chars("exit\n")
|
machine.send_chars("exit\n")
|
||||||
@ -170,24 +196,23 @@ machine.wait_until_fails("pgrep alacritty")
|
|||||||
|
|
||||||
# Start app (foot) with direct Wayland access:
|
# Start app (foot) with direct Wayland access:
|
||||||
swaymsg("exec da-foot")
|
swaymsg("exec da-foot")
|
||||||
wait_for_window("u0_a4@machine")
|
wait_for_window("u0_a5@machine")
|
||||||
machine.send_chars("clear; wayland-info && touch /tmp/success-direct\n")
|
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")
|
collect_state_ui("foot_direct")
|
||||||
check_state("da-foot", 1)
|
check_state("da-foot", 1)
|
||||||
# Verify acl on XDG_RUNTIME_DIR:
|
# Verify acl on XDG_RUNTIME_DIR:
|
||||||
print(machine.succeed("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.send_chars("exit\n")
|
||||||
machine.wait_until_fails("pgrep foot")
|
machine.wait_until_fails("pgrep foot")
|
||||||
# Verify acl cleanup on XDG_RUNTIME_DIR:
|
# 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:
|
# Test syscall filter:
|
||||||
print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure"))
|
print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure"))
|
||||||
|
|
||||||
# Exit Sway and verify process exit status 0:
|
# Exit Sway and verify process exit status 0:
|
||||||
swaymsg("exit", succeed=False)
|
swaymsg("exit", succeed=False)
|
||||||
machine.wait_until_fails("pgrep -x sway")
|
|
||||||
machine.wait_for_file("/tmp/sway-exit-ok")
|
machine.wait_for_file("/tmp/sway-exit-ok")
|
||||||
|
|
||||||
# Print fortify runDir contents:
|
# Print fortify runDir contents:
|
||||||
|
Loading…
Reference in New Issue
Block a user