aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--cmd/fpkg/bundle.go79
-rw-r--r--cmd/fpkg/install.go241
-rw-r--r--cmd/fpkg/main.go48
-rw-r--r--cmd/fpkg/paths.go71
-rw-r--r--cmd/fpkg/proc.go64
-rw-r--r--cmd/fpkg/start.go92
-rwxr-xr-xdist/install.sh1
7 files changed, 596 insertions, 0 deletions
diff --git a/cmd/fpkg/bundle.go b/cmd/fpkg/bundle.go
new file mode 100644
index 00000000..eb3150bb
--- /dev/null
+++ b/cmd/fpkg/bundle.go
@@ -0,0 +1,79 @@
+package main
+
+import (
+ "encoding/json"
+ "os"
+
+ "git.gensokyo.uk/security/fortify/dbus"
+ "git.gensokyo.uk/security/fortify/internal/fmsg"
+ "git.gensokyo.uk/security/fortify/internal/system"
+)
+
+type bundleInfo struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+
+ // passed through to [fst.Config]
+ ID string `json:"id"`
+ // passed through to [fst.Config]
+ AppID int `json:"app_id"`
+ // passed through to [fst.Config]
+ Groups []string `json:"groups,omitempty"`
+ // passed through to [fst.Config]
+ UserNS bool `json:"userns,omitempty"`
+ // passed through to [fst.Config]
+ Net bool `json:"net,omitempty"`
+ // passed through to [fst.Config]
+ Dev bool `json:"dev,omitempty"`
+ // passed through to [fst.Config]
+ NoNewSession bool `json:"no_new_session,omitempty"`
+ // passed through to [fst.Config]
+ MapRealUID bool `json:"map_real_uid,omitempty"`
+ // passed through to [fst.Config]
+ DirectWayland bool `json:"direct_wayland,omitempty"`
+ // passed through to [fst.Config]
+ SystemBus *dbus.Config `json:"system_bus,omitempty"`
+ // passed through to [fst.Config]
+ SessionBus *dbus.Config `json:"session_bus,omitempty"`
+ // passed through to [fst.Config]
+ Enablements system.Enablements `json:"enablements"`
+
+ // allow gpu access within sandbox
+ GPU bool `json:"gpu"`
+ // inner nix store path to activate-and-exec script
+ Launcher string `json:"launcher"`
+ // store path to /run/current-system
+ CurrentSystem string `json:"current_system"`
+ // home-manager activation package
+ ActivationPackage string `json:"activation_package"`
+}
+
+func loadBundleInfo(name string, beforeFail func()) *bundleInfo {
+ bundle := new(bundleInfo)
+ if f, err := os.Open(name); err != nil {
+ beforeFail()
+ fmsg.Fatalf("cannot open bundle: %v", err)
+ } else if err = json.NewDecoder(f).Decode(&bundle); err != nil {
+ beforeFail()
+ fmsg.Fatalf("cannot parse bundle metadata: %v", err)
+ } else if err = f.Close(); err != nil {
+ fmsg.Printf("cannot close bundle metadata: %v", err)
+ // not fatal
+ }
+
+ if bundle.ID == "" {
+ beforeFail()
+ fmsg.Fatal("application identifier must not be empty")
+ }
+
+ return bundle
+}
+
+func formatHostname(name string) string {
+ if h, err := os.Hostname(); err != nil {
+ fmsg.Printf("cannot get hostname: %v", err)
+ return "fortify-" + name
+ } else {
+ return h + "-" + name
+ }
+}
diff --git a/cmd/fpkg/install.go b/cmd/fpkg/install.go
new file mode 100644
index 00000000..2ba2c223
--- /dev/null
+++ b/cmd/fpkg/install.go
@@ -0,0 +1,241 @@
+package main
+
+import (
+ "encoding/json"
+ "flag"
+ "os"
+ "path"
+
+ "git.gensokyo.uk/security/fortify/fst"
+ "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 {
+ fmsg.Fatal("invalid argument")
+ }
+ pkgPath := args[0]
+ if !path.IsAbs(pkgPath) {
+ if dir, err := os.Getwd(); err != nil {
+ fmsg.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 {
+ fmsg.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()
+ fmsg.Fatalf("cannot access %q: %v", pathSet.metaPath, err)
+ panic("unreachable")
+ }
+ // did not modify app, clean installation condition met later
+ } else if s.IsDir() {
+ cleanup()
+ fmsg.Fatalf("metadata path %q is not a file", pathSet.metaPath)
+ panic("unreachable")
+ } else {
+ app = loadBundleInfo(pathSet.metaPath, cleanup)
+ if app.ID != bundle.ID {
+ cleanup()
+ fmsg.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.Launcher == bundle.Launcher {
+ cleanup()
+ fmsg.Printf("package %q is identical to local application %q", pkgPath, app.ID)
+ fmsg.Exit(0)
+ }
+
+ // AppID determines uid
+ if app.AppID != bundle.AppID {
+ cleanup()
+ fmsg.Fatalf("package %q app id %d differs from installed %d", pkgPath, bundle.AppID, app.AppID)
+ panic("unreachable")
+ }
+
+ // sec: should compare version string
+ fmsg.VPrintf("installing application %q version %q over local %q", bundle.ID, bundle.Version, app.Version)
+ } else {
+ fmsg.VPrintf("application %q clean installation", bundle.ID)
+ // sec: should install credentials
+ }
+
+ /*
+ Setup steps for files owned by the target user.
+ */
+
+ installConfig := &fst.Config{
+ ID: bundle.ID,
+ Command: []string{shell, "-lc", "export BUNDLE=" + fst.Tmp + "/bundle && " + // export inner bundle path in the environment
+ "mkdir -p etc && chmod -R +w etc && rm -rf etc && cp -dRf $BUNDLE/etc etc && " + // replace inner /etc
+ "mkdir -p nix && chmod -R +w nix && rm -rf nix && cp -dRf /nix nix && " + // replace inner /nix
+ "nix copy --offline --no-check-sigs --all --from file://$BUNDLE/res --to $PWD && " + // copy from binary cache
+ "chmod 0755 .", // make cache directory world-readable for autoetc
+ },
+ Confinement: fst.ConfinementConfig{
+ AppID: bundle.AppID,
+ Username: "nixos",
+ Inner: path.Join("/data/data", bundle.ID, "cache"),
+ Outer: pathSet.cacheDir, // this also ensures cacheDir via fshim
+ Sandbox: &fst.SandboxConfig{
+ Hostname: formatHostname(bundle.Name) + "-install",
+ NoNewSession: dropShellInstall, // nix copy should not need job control
+ Filesystem: []*fst.FilesystemConfig{
+ {Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
+ {Src: workDir, Dst: path.Join(fst.Tmp, "bundle"), Must: true},
+ },
+ Link: [][2]string{
+ {bundle.CurrentSystem, "/run/current-system"},
+ {"/run/current-system/sw/bin", "/bin"},
+ {"/run/current-system/sw/bin", "/usr/bin"},
+ },
+ Etc: path.Join(workDir, "etc"),
+ AutoEtc: true,
+ },
+ ExtraPerms: []*fst.ExtraPermConfig{
+ {Path: dataHome, Execute: true},
+ {Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
+ {Path: workDir, Execute: true},
+ },
+ },
+ }
+
+ if dropShellInstall {
+ installConfig.Command = []string{shell, "-l"}
+ fortifyApp(installConfig, cleanup)
+ cleanup()
+ fmsg.Exit(0)
+ }
+ fortifyApp(installConfig, cleanup)
+
+ /*
+ Activate home-manager generation.
+ */
+
+ activateConfig := &fst.Config{
+ ID: bundle.ID,
+ Command: []string{shell, "-lc", "nix-daemon --store / & " + // start nix-daemon
+ "(while [ ! -S /nix/var/nix/daemon-socket/socket ]; do sleep 0.01; done) && " + // wait for socket to appear
+ bundle.ActivationPackage + "/activate && " + // run activation script
+ "pkill nix-daemon", // terminate nix-daemon
+ },
+ Confinement: fst.ConfinementConfig{
+ AppID: bundle.AppID,
+ Groups: bundle.Groups,
+ Username: "fortify",
+ Inner: path.Join("/data/data", bundle.ID),
+ Outer: pathSet.homeDir,
+ Sandbox: &fst.SandboxConfig{
+ Hostname: formatHostname(bundle.Name) + "-activate",
+ UserNS: true, // nix sandbox requires userns
+ NoNewSession: dropShellActivate, // home-manager activation should not need job control
+ Filesystem: []*fst.FilesystemConfig{
+ {Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
+ },
+ Link: [][2]string{
+ {bundle.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},
+ {Path: workDir, Execute: true},
+ },
+ },
+ }
+
+ if dropShellActivate {
+ activateConfig.Command = []string{shell, "-l"}
+ fortifyApp(activateConfig, cleanup)
+ cleanup()
+ fmsg.Exit(0)
+ }
+ fortifyApp(activateConfig, 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()
+ fmsg.Fatalf("cannot create metadata file: %v", err)
+ panic("unreachable")
+ } else if err = json.NewEncoder(f).Encode(bundle); err != nil {
+ cleanup()
+ fmsg.Fatalf("cannot write metadata: %v", err)
+ panic("unreachable")
+ } else if err = f.Close(); err != nil {
+ fmsg.Printf("cannot close metadata file: %v", err)
+ // not fatal
+ }
+
+ if err := os.Rename(pathSet.metaPath+"~", pathSet.metaPath); err != nil {
+ cleanup()
+ fmsg.Fatalf("cannot rename metadata file: %v", err)
+ panic("unreachable")
+ }
+
+ cleanup()
+}
diff --git a/cmd/fpkg/main.go b/cmd/fpkg/main.go
new file mode 100644
index 00000000..f348cb5e
--- /dev/null
+++ b/cmd/fpkg/main.go
@@ -0,0 +1,48 @@
+package main
+
+import (
+ "flag"
+ "os"
+
+ "git.gensokyo.uk/security/fortify/internal/fmsg"
+)
+
+const shell = "/run/current-system/sw/bin/bash"
+
+func init() {
+ if err := os.Setenv("SHELL", shell); err != nil {
+ fmsg.Fatalf("cannot set $SHELL: %v", err)
+ }
+}
+
+var (
+ flagVerbose bool
+)
+
+func init() {
+ flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
+}
+
+func main() {
+ fmsg.SetPrefix("fpkg")
+
+ flag.Parse()
+ fmsg.SetVerbose(flagVerbose)
+
+ args := flag.Args()
+ if len(args) < 1 {
+ fmsg.Fatal("invalid argument")
+ }
+
+ switch args[0] {
+ case "install":
+ actionInstall(args[1:])
+ case "start":
+ actionStart(args[1:])
+
+ default:
+ fmsg.Fatal("invalid argument")
+ }
+
+ fmsg.Exit(0)
+}
diff --git a/cmd/fpkg/paths.go b/cmd/fpkg/paths.go
new file mode 100644
index 00000000..cf36c096
--- /dev/null
+++ b/cmd/fpkg/paths.go
@@ -0,0 +1,71 @@
+package main
+
+import (
+ "os"
+ "os/exec"
+ "path"
+ "strconv"
+ "sync/atomic"
+
+ "git.gensokyo.uk/security/fortify/internal/fmsg"
+)
+
+var (
+ dataHome string
+)
+
+func init() {
+ // dataHome
+ if p, ok := os.LookupEnv("FORTIFY_DATA_HOME"); ok {
+ dataHome = p
+ } else {
+ dataHome = "/var/lib/fortify/" + strconv.Itoa(os.Getuid())
+ }
+}
+
+func lookPath(file string) string {
+ if p, err := exec.LookPath(file); err != nil {
+ fmsg.Fatalf("%s: command not found", file)
+ panic("unreachable")
+ } else {
+ return p
+ }
+}
+
+var beforeRunFail = new(atomic.Pointer[func()])
+
+func mustRun(name string, arg ...string) {
+ fmsg.VPrintf("spawning process: %q %q", name, arg)
+ cmd := exec.Command(name, arg...)
+ cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
+ if err := cmd.Run(); err != nil {
+ if f := beforeRunFail.Swap(nil); f != nil {
+ (*f)()
+ }
+ fmsg.Fatalf("%s: %v", name, err)
+ panic("unreachable")
+ }
+}
+
+type appPathSet struct {
+ // ${dataHome}/${id}
+ baseDir string
+ // ${baseDir}/app
+ metaPath string
+ // ${baseDir}/files
+ homeDir string
+ // ${baseDir}/cache
+ cacheDir string
+ // ${baseDir}/cache/nix
+ nixPath string
+}
+
+func pathSetByApp(id string) *appPathSet {
+ pathSet := new(appPathSet)
+ pathSet.baseDir = path.Join(dataHome, id)
+ pathSet.metaPath = path.Join(pathSet.baseDir, "app")
+ pathSet.homeDir = path.Join(pathSet.baseDir, "files")
+ pathSet.cacheDir = path.Join(pathSet.baseDir, "cache")
+ pathSet.nixPath = path.Join(pathSet.cacheDir, "nix")
+ return pathSet
+}
diff --git a/cmd/fpkg/proc.go b/cmd/fpkg/proc.go
new file mode 100644
index 00000000..a995c9f1
--- /dev/null
+++ b/cmd/fpkg/proc.go
@@ -0,0 +1,64 @@
+package main
+
+import (
+ "encoding/json"
+ "errors"
+ "io"
+ "os"
+ "os/exec"
+
+ "git.gensokyo.uk/security/fortify/fst"
+ "git.gensokyo.uk/security/fortify/internal"
+ "git.gensokyo.uk/security/fortify/internal/fmsg"
+)
+
+func fortifyApp(config *fst.Config, beforeFail func()) {
+ var (
+ cmd *exec.Cmd
+ st io.WriteCloser
+ )
+ if p, ok := internal.Check(internal.Fortify); !ok {
+ beforeFail()
+ fmsg.Fatal("invalid fortify path, this copy of fpkg is not compiled correctly")
+ panic("unreachable")
+ } else if r, w, err := os.Pipe(); err != nil {
+ beforeFail()
+ fmsg.Fatalf("cannot pipe: %v", err)
+ panic("unreachable")
+ } else {
+ if fmsg.Verbose() {
+ cmd = exec.Command(p, "-v", "app", "3")
+ } else {
+ cmd = exec.Command(p, "app", "3")
+ }
+ cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
+ cmd.ExtraFiles = []*os.File{r}
+ st = w
+ }
+
+ go func() {
+ if err := json.NewEncoder(st).Encode(config); err != nil {
+ beforeFail()
+ fmsg.Fatalf("cannot send configuration: %v", err)
+ panic("unreachable")
+ }
+ }()
+
+ if err := cmd.Start(); err != nil {
+ beforeFail()
+ fmsg.Fatalf("cannot start fortify: %v", err)
+ panic("unreachable")
+ }
+ if err := cmd.Wait(); err != nil {
+ var exitError *exec.ExitError
+ if errors.As(err, &exitError) {
+ beforeFail()
+ fmsg.Exit(exitError.ExitCode())
+ panic("unreachable")
+ } else {
+ beforeFail()
+ fmsg.Fatalf("cannot wait: %v", err)
+ panic("unreachable")
+ }
+ }
+}
diff --git a/cmd/fpkg/start.go b/cmd/fpkg/start.go
new file mode 100644
index 00000000..498a3f35
--- /dev/null
+++ b/cmd/fpkg/start.go
@@ -0,0 +1,92 @@
+package main
+
+import (
+ "flag"
+ "path"
+
+ "git.gensokyo.uk/security/fortify/fst"
+ "git.gensokyo.uk/security/fortify/internal/fmsg"
+)
+
+func actionStart(args []string) {
+ set := flag.NewFlagSet("start", flag.ExitOnError)
+ var dropShell bool
+ set.BoolVar(&dropShell, "s", false, "Drop to a shell on activation")
+
+ // Ignore errors; set is set for ExitOnError.
+ _ = set.Parse(args)
+
+ args = set.Args()
+
+ if len(args) < 1 {
+ fmsg.Fatal("invalid argument")
+ }
+ id := args[0]
+ pathSet := pathSetByApp(id)
+ app := loadBundleInfo(pathSet.metaPath, func() {})
+
+ if app.ID != id {
+ fmsg.Fatalf("app %q claims to have identifier %q", id, app.ID)
+ }
+
+ command := make([]string, 1, len(args))
+ if !dropShell {
+ command[0] = app.Launcher
+ } else {
+ command[0] = shell
+ }
+ 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,
+ 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,
+ },
+ }
+
+ if app.GPU {
+ config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem,
+ &fst.FilesystemConfig{Src: "/dev/dri", Device: true})
+ }
+
+ fortifyApp(config, func() {})
+ fmsg.Exit(0)
+}
diff --git a/dist/install.sh b/dist/install.sh
index 96949699..dcb5dc0d 100755
--- a/dist/install.sh
+++ b/dist/install.sh
@@ -4,6 +4,7 @@ cd "$(dirname -- "$0")" || exit 1
install -vDm0755 "bin/fortify" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fortify"
install -vDm0755 "bin/fshim" "${FORTIFY_INSTALL_PREFIX}/usr/libexec/fortify/fshim"
install -vDm0755 "bin/finit" "${FORTIFY_INSTALL_PREFIX}/usr/libexec/fortify/finit"
+install -vDm0755 "bin/fpkg" "${FORTIFY_INSTALL_PREFIX}/usr/libexec/fortify/fpkg"
install -vDm0755 "bin/fuserdb" "${FORTIFY_INSTALL_PREFIX}/usr/libexec/fortify/fuserdb"
install -vDm6511 "bin/fsu" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fsu"