Compare commits

...

60 Commits

Author SHA1 Message Date
Ophestra Umiker 563c39c2d9
release: 0.0.10
test / test (push) Successful in 19s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-28 20:38:10 +09:00
Ophestra Umiker aa1f96eeeb
fsu: check parent executable path
test / test (push) Successful in 19s Details
Only allow main program to launch fsu. This change and further checks in the main program reduces attack surface.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-28 18:52:23 +09:00
Ophestra Umiker 431dc095e5
app/start: skip cleanup if shim is nil
test / test (push) Successful in 19s Details
Shim is created before any system operation happens.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-28 14:21:15 +09:00
Ophestra Umiker 60e91b9b0f
shim: expose checkPid in constructor
test / test (push) Successful in 1m44s Details
This will be supported soon when launching via fsu.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-28 00:02:55 +09:00
Ophestra Umiker d9cb2a9f2b
fsu: implement simple setuid user switcher
Contains path to fortify, set at compile time, authenticates based on a simple uid range assignment file which also acts as the allow list.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-28 00:02:34 +09:00
Ophestra Umiker 09feda3783
fortify: exit if seal returns error
test / test (push) Successful in 20s Details
Wait should not be called on an unsealed App.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-27 23:18:16 +09:00
Ophestra Umiker 51e84ba8a5
system/dbus: compare sealed value by string
test / test (push) Successful in 19s Details
Stringer method of dbus.Proxy returns a string representation of its args stream when sealed.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-27 12:09:34 +09:00
Ophestra Umiker 7df9d8d01d
system: move sd_booted implementation to os abstraction
This implements lazy loading of the systemd marker (they are not accessed in init and shim) and ensures consistent behaviour when running with a stub.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-27 12:09:34 +09:00
Ophestra Umiker 6d8bcb63f2
release: 0.0.9
release / release (push) Successful in 27s Details
test / test (push) Successful in 22s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-27 01:25:24 +09:00
Ophestra Umiker c7b77d6e5e
fmsg: initialise dequeue prior to withhold/resume
test / test (push) Successful in 23s Details
This fixes the hang on resume when no messages were ever printed.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-27 01:24:30 +09:00
Ophestra Umiker 2f34627d37
release: 0.0.8
release / release (push) Successful in 31s Details
test / test (push) Successful in 20s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-27 00:49:50 +09:00
Ophestra Umiker 1d6ea81205
shim: user switcher process management struct
test / test (push) Successful in 19s Details
This change moves all user switcher and shim management to the shim package and withholds output while shim is alive. This also eliminated all exit scenarios where revert is skipped.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-27 00:46:15 +09:00
Ophestra Umiker ae1a102882
fmsg: support temporarily withholding output
test / test (push) Successful in 31s Details
Trying to print to a shared stdout is a terrible idea. This change makes it possible to withhold output for the lifetime of the sandbox.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-26 23:09:32 +09:00
Ophestra Umiker 093e99d062
app: separate nixos test cases from tests
test / test (push) Successful in 20s Details
Test cases are very long, separating them improves editor performance.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 17:44:29 +09:00
Ophestra Umiker ad7e389eee
app: test app permissive defaults sealing behaviour
test / test (push) Successful in 20s Details
This test seals App against a deterministic os stub and checks the resulting sys and bwrap values against known correct ones. The effects of sys and bwrap on the OS and sandbox is deterministic and tested in their own respective packages.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 17:12:13 +09:00
Ophestra Umiker 5b249e4a66
system: print number of ops completed at point of failure
test / test (push) Successful in 21s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 17:08:21 +09:00
Ophestra Umiker 2a348c7f91
system: include more info in ACL Stringer
test / test (push) Successful in 24s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 16:23:22 +09:00
Ophestra Umiker eb767e7642
app/start: cleaner command not found message
test / test (push) Successful in 27s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 16:12:18 +09:00
Ophestra Umiker 3bfe8dbf5d
internal: ReadDir wrapper return fs.DirEntry
test / test (push) Successful in 25s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 14:56:29 +09:00
Ophestra Umiker 8fa791a2f8
app/seal: symlink /etc entries in permissive default
test / test (push) Successful in 20s Details
Fortify overrides /etc/passwd and /etc/group in the sandbox. Bind mounting /etc results in them being replaced when the passwd database is updated on host.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 13:31:57 +09:00
Ophestra Umiker b932ac8260
app/config: support creating symlinks within sandbox
test / test (push) Successful in 21s Details
This is already supported by the underlying bwrap helper. This change exposes access to it in Config.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 13:29:01 +09:00
Ophestra Umiker 050ffceb27
helper/bwrap: register generic PermConfig types with gob
test / test (push) Successful in 21s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 13:26:01 +09:00
Ophestra Umiker 31350d74e5
shim: kill shim if setup becomes impossible
test / test (push) Successful in 23s Details
This prevents a hang when setup faults but the shim keeps waiting on the socket. Setup is automatically aborted when the shim is killed.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 13:19:37 +09:00
Ophestra Umiker 3b82cc55de
internal: use fallback paths when XDG_RUNTIME_DIR is not absolute
test / test (push) Successful in 24s Details
There are scenarios where XDG_RUNTIME_DIR is set to an empty string or garbage. This check tries to ensure reasonable behaviour in most of those cases.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 12:14:57 +09:00
Ophestra Umiker 6bc5be7e5a
internal: wrap calls to os standard library functions
test / test (push) Successful in 19s Details
This change helps tests stub out and simulate OS behaviour during the sealing process. This also removes dependency on XDG_RUNTIME_DIR as the internal.System implementation provided to App provides a compat directory inside the tmpdir-based share when XDG_RUNTIME_DIR is unavailable.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-23 21:46:21 +09:00
Ophestra Umiker e35c5fe3ed
system: sys comparison method
test / test (push) Successful in 24s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-23 14:15:13 +09:00
Ophestra Umiker 20195ece47
system: return sys in queueing methods
test / test (push) Successful in 54s Details
This enables building an instance in a single statement.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-23 12:34:16 +09:00
Ophestra Umiker cafed5f234
shim: abort setup on failed start and process exit
test / test (push) Successful in 25s Details
Shim setup listens on a socket in the process share, if shim setup hasn't happened on exit revert will fail. This change makes sure shim setup is aborted on a doomed launch.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-21 21:23:56 +09:00
Ophestra Umiker 42e0b168e3
fmsg: produce all output through fmsg
test / test (push) Successful in 17s Details
The behaviour of print functions from package fmt is not thread safe. Functions provided by fmsg wrap around Logger methods. This makes prefix much cleaner and makes it easy to deal with future changes to logging.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-21 20:47:02 +09:00
Ophestra Umiker 380d1f4585
app: move wayland mediation to shim package
test / test (push) Successful in 29s Details
Values used in the Wayland mediation implementation is stored in various struct fields strewn across multiple app structs and checks are messy and confusing. This commit unifies them into a single struct and access it using much better looking methods.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-21 18:46:06 +09:00
Ophestra Umiker 133f23e0de
release: 0.0.7
release / release (push) Successful in 21s Details
test / test (push) Successful in 11s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-20 19:50:59 +09:00
Ophestra Umiker 65af1684e3
migrate to git.ophivana.moe/security/fortify
test / test (push) Successful in 14s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-20 19:50:13 +09:00
Ophestra Umiker cdda33555c
update README document
We have a highly configurable sandbox now, just not really the Android sandbox.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-20 00:24:50 +09:00
Ophestra Umiker ad0034b09a
app: move app ID to app struct
App ID is inherent to App, and it makes no sense to generate it as part of the app sealing process.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-20 00:22:18 +09:00
Ophestra Umiker 1da845d78b
workflows: call apt-get without sudo
Workflow scripts run as root in act-runner containers, so calling sudo is redundant and pointless.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-18 22:56:49 +09:00
Ophestra Umiker 55bb348d5f
state: store launch method instead of launcher path
Launcher path is constant for each launch method on the same system.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-18 22:25:09 +09:00
Ophestra Umiker ecce832d93
release: 0.0.6
release / release (push) Successful in 1m46s Details
test / test (push) Successful in 1m39s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-18 01:26:42 +09:00
Ophestra Umiker 65bd7d18db
app/share: fix order to ensure SharePath before any of its subdirectories
shareTmpdirChild happened to request an ephemeral dir within SharePath and was called before shareRuntime which ensures that path. This commit moves SharePath initialisation to shareSystem and moves shareTmpdirChild into ShareSystem. Further cleanup and tests are desperately needed for the app package but for now this fix will have to do.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-18 01:21:58 +09:00
Ophestra Umiker 4ebb98649e
release: 0.0.5
release / release (push) Successful in 1m26s Details
test / test (push) Successful in 3m6s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-17 20:48:41 +09:00
Ophestra Umiker 919e5b5cd5
init: start timeout only if reaped PID is the initial process
Fix a very obvious bug introduced in 5401882ed0.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-17 20:46:48 +09:00
Ophestra Umiker 40161c5938
nix: remove fortify package from default devShell
This change makes it possible to start a devShell when tests aren't passing.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-17 20:35:10 +09:00
Ophestra Umiker 679e719f9e
system: tests for all Op implementations except DBus
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-17 20:28:55 +09:00
Ophestra Umiker 064db9f020
system/mkdir: type label in String method
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-17 16:37:23 +09:00
Ophestra Umiker 73a698c7cb
ldd: run ldd with read-only filesystem and unshared net
This is only called on trusted programs, however extra hardening is never a bad idea.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-17 15:37:27 +09:00
Ophestra Umiker 57c1b3eda6
system: handle invalid enablement in String method
Invalid enablement is only caused by bad API usage, however panicking on the spot leaves behind messy state that has to be manually cleaned up.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-17 14:31:13 +09:00
Ophestra Umiker 5401882ed0
init: post initial process death exit timeout
Wait for 5 seconds before printing a message and exiting after picking up the initial process's wait status. This also kills any lingering processes.This behaviour is helpful for applications launched without a terminal attached.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-17 02:38:24 +09:00
Ophestra Umiker dd78728fb3
workflows: test workflow to run tests every commit
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-17 00:18:20 +09:00
Ophestra Umiker 354c23dd28
workflows: add lines between steps
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-17 00:17:40 +09:00
Ophestra Umiker c21168a741
system: move enablements from state package
This removes the unnecessary import of the state package.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-16 14:38:57 +09:00
Ophestra Umiker 084cd84f36
app: port app to use the system package
This commit does away with almost all baggage left over from the Ego port. Error wrapping also got simplified. All API changes happens to be internal which means no changes to main except renaming of the BaseError type.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-16 01:38:59 +09:00
Ophestra Umiker 430f1a5b4e
system: isolate app/system into generic implementation
This improves maintainability and extensibility of system operations, makes writing tests for them possible, and operations now apply and revert in order, instead of being bunched up into their own categories.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-16 01:31:23 +09:00
Ophestra Umiker 0fd63e85e7
fmsg/errors: isolate app/error into a separate package
These functions are not in any way specific to the app package.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-16 01:29:44 +09:00
Ophestra Umiker 33cf0bed54
dbus: various accessors for dbus.Proxy internal fields
These values are useful during sandbox setup and exporting them makes more sense than storing them twice.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-16 01:27:49 +09:00
Ophestra Umiker 689f5bed57
release: 0.0.4
release / release (push) Successful in 1m32s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-15 02:56:49 +09:00
Ophestra Umiker 184a5f29fa
helper/bwrap: add fortify permissive default test case
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-15 02:56:13 +09:00
Ophestra Umiker 3015266e5a
helper/bwrap: sort SetEnv arguments
This guarantees consistency of resulting args.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-15 02:55:48 +09:00
Ophestra Umiker aa5dd2313c
app: filter /tmp from permissive default
Tmpdir is bind mounted over further along in execution so there is no point sharing it here.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-15 02:54:50 +09:00
Ophestra Umiker 2faf510146
helper/bwrap: ordered filesystem args
The argument builder was written based on the incorrect assumption that bwrap arguments are unordered. The argument builder is replaced in this commit to correct that mistake.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-15 02:15:55 +09:00
Ophestra Umiker a0db19b9ad
helper/bwrap: format mode in octal
Bubblewrap expects an octal representation of mode.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-14 13:47:50 +09:00
Ophestra Umiker aaed5080f4
fortify: move PR_SET_DUMPABLE to the beginning of main
This call does need flag values.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-14 02:48:37 +09:00
91 changed files with 4170 additions and 1795 deletions

View File

@ -13,22 +13,26 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup go - name: Setup go
uses: https://github.com/actions/setup-go@v5 uses: https://github.com/actions/setup-go@v5
with: with:
go-version: '>=1.20.1' go-version: '>=1.23.0'
- name: Get dependencies - name: Get dependencies
run: >- run: >-
sudo apt-get update && apt-get update &&
sudo apt-get install -y apt-get install -y
gcc gcc
pkg-config pkg-config
libacl1-dev libacl1-dev
if: ${{ runner.os == 'Linux' }} if: ${{ runner.os == 'Linux' }}
- name: Build for Linux - name: Build for Linux
run: >- run: >-
sh -c "go build -v -ldflags '-s -w -X main.Version=${{ github.ref_name }}' -o bin/fortify && sh -c "go build -v -ldflags '-s -w -X main.Version=${{ github.ref_name }}' -o bin/fortify &&
sha256sum --tag -b bin/fortify > bin/fortify.sha256" sha256sum --tag -b bin/fortify > bin/fortify.sha256"
- name: Release - name: Release
id: use-go-action id: use-go-action
uses: https://gitea.com/actions/release-action@main uses: https://gitea.com/actions/release-action@main

37
.gitea/workflows/test.yml Normal file
View File

@ -0,0 +1,37 @@
name: test
on:
- push
- pull_request
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup go
uses: https://github.com/actions/setup-go@v5
with:
go-version: '>=1.23.0'
- name: Get dependencies
run: >-
apt-get update &&
apt-get install -y
gcc
pkg-config
libacl1-dev
if: ${{ runner.os == 'Linux' }}
- name: Run tests
run: >-
go test ./...
- name: Build for Linux
run: >-
sh -c "go build -v -ldflags '-s -w -X main.Version=${{ github.ref_name }}' -o bin/fortify &&
sha256sum --tag -b bin/fortify > bin/fortify.sha256"

View File

@ -1,9 +1,9 @@
Fortify Fortify
======= =======
[![Go Reference](https://pkg.go.dev/badge/git.ophivana.moe/cat/fortify.svg)](https://pkg.go.dev/git.ophivana.moe/cat/fortify) [![Go Reference](https://pkg.go.dev/badge/git.ophivana.moe/security/fortify.svg)](https://pkg.go.dev/git.ophivana.moe/security/fortify)
Lets you run graphical applications as another user ~~in an Android-like sandbox environment~~ (WIP) with a nice NixOS Lets you run graphical applications as another user in a confined environment with a nice NixOS
module to configure target users and provide launchers and desktop files for your privileged user. module to configure target users and provide launchers and desktop files for your privileged user.
Why would you want this? Why would you want this?
@ -12,7 +12,7 @@ Why would you want this?
- It protects applications from each other. - It protects applications from each other.
- It provides UID isolation on top of ~~the standard application sandbox~~ (WIP). - It provides UID isolation on top of the standard application sandbox.
There are a few different things to set up for this to work: There are a few different things to set up for this to work:
@ -26,7 +26,7 @@ There are a few different things to set up for this to work:
If you have a flakes-enabled nix environment, you can try out the tool by running: If you have a flakes-enabled nix environment, you can try out the tool by running:
```shell ```shell
nix run git+https://git.ophivana.moe/cat/fortify -- -h nix run git+https://git.ophivana.moe/security/fortify -- -h
``` ```
## Module usage ## Module usage
@ -41,7 +41,7 @@ To use the module, import it into your configuration with
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
fortify = { fortify = {
url = "git+https://git.ophivana.moe/cat/fortify"; url = "git+https://git.ophivana.moe/security/fortify";
# Optional but recommended to limit the size of your system closure. # Optional but recommended to limit the size of your system closure.
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";

View File

@ -25,7 +25,25 @@ const (
Other = C.ACL_OTHER Other = C.ACL_OTHER
) )
type Perm C.acl_perm_t type (
Perm C.acl_perm_t
Perms []Perm
)
func (ps Perms) String() string {
var s = []byte("---")
for _, p := range ps {
switch p {
case Read:
s[0] = 'r'
case Write:
s[1] = 'w'
case Execute:
s[2] = 'x'
}
}
return string(s)
}
func UpdatePerm(path string, uid int, perms ...Perm) error { func UpdatePerm(path string, uid int, perms ...Perm) error {
// read acl from file // read acl from file

140
cmd/fsu/main.go Normal file
View File

@ -0,0 +1,140 @@
package main
import (
"bufio"
"log"
"os"
"path"
"strconv"
"strings"
"syscall"
)
const (
fsuConfFile = "/etc/fsurc"
envShim = "FORTIFY_SHIM"
envAID = "FORTIFY_APP_ID"
fpPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
)
// FortifyPath is the path to fortify, set at compile time.
var FortifyPath = fpPoison
func main() {
log.SetFlags(0)
log.SetPrefix("fsu: ")
log.SetOutput(os.Stderr)
if os.Geteuid() != 0 {
log.Fatal("this program must be owned by uid 0 and have the setuid bit set")
}
puid := os.Getuid()
if puid == 0 {
log.Fatal("this program must not be started by root")
}
// validate compiled in fortify path
if FortifyPath == fpPoison || !path.IsAbs(FortifyPath) {
log.Fatal("invalid fortify path, this copy of fsu is not compiled correctly")
}
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
if p, err := os.Readlink(pexe); err != nil {
log.Fatalf("cannot read parent executable path: %v", err)
} else if strings.HasSuffix(p, " (deleted)") {
log.Fatal("fortify executable has been deleted")
} else if p != FortifyPath {
log.Fatal("this program must be started by fortify")
}
// uid = 1000000 +
// fid * 10000 +
// aid
uid := 1000000
// authenticate before accepting user input
if fid, ok := parseConfig(fsuConfFile, puid); !ok {
log.Fatalf("uid %d is not in the fsurc file", puid)
} else {
uid += fid * 10000
}
// pass through setup path to shim
var shimSetupPath string
if s, ok := os.LookupEnv(envShim); !ok {
log.Fatal("FORTIFY_SHIM not set")
} else if !path.IsAbs(s) {
log.Fatal("FORTIFY_SHIM is not absolute")
} else {
shimSetupPath = s
}
// allowed aid range 0 to 9999
if as, ok := os.LookupEnv(envAID); !ok {
log.Fatal("FORTIFY_APP_ID not set")
} else if aid, err := strconv.Atoi(as); err != nil || aid < 0 || aid > 9999 {
log.Fatal("invalid aid")
} else {
uid += aid
}
if err := syscall.Setresgid(uid, uid, uid); err != nil {
log.Fatalf("cannot set gid: %v", err)
}
if err := syscall.Setresuid(uid, uid, uid); err != nil {
log.Fatalf("cannot set uid: %v", err)
}
if err := syscall.Exec(FortifyPath, []string{"fortify", "shim"}, []string{envShim + "=" + shimSetupPath}); err != nil {
log.Fatalf("cannot start shim: %v", err)
}
panic("unreachable")
}
func parseConfig(p string, puid int) (fid int, ok bool) {
// refuse to run if fsurc is not protected correctly
if s, err := os.Stat(p); err != nil {
log.Fatal(err)
} else if s.Mode().Perm() != 0400 {
log.Fatal("bad fsurc perm")
} else if st := s.Sys().(*syscall.Stat_t); st.Uid != 0 || st.Gid != 0 {
log.Fatal("fsurc must be owned by uid 0")
}
if r, err := os.Open(p); err != nil {
log.Fatal(err)
return -1, false
} else {
s := bufio.NewScanner(r)
var line int
for s.Scan() {
line++
// <puid> <fid>
lf := strings.SplitN(s.Text(), " ", 2)
if len(lf) != 2 {
log.Fatalf("invalid entry on line %d", line)
}
var puid0 int
if puid0, err = strconv.Atoi(lf[0]); err != nil || puid0 < 1 {
log.Fatalf("invalid parent uid on line %d", line)
}
ok = puid0 == puid
if ok {
// allowed fid range 0 to 99
if fid, err = strconv.Atoi(lf[1]); err != nil || fid < 0 || fid > 99 {
log.Fatalf("invalid fortify uid on line %d", line)
}
return
}
}
if err = s.Err(); err != nil {
log.Fatalf("cannot read fsurc: %v", err)
}
return -1, false
}
}

View File

@ -4,12 +4,11 @@ import (
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
"os"
"git.ophivana.moe/cat/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/cat/fortify/internal" "git.ophivana.moe/security/fortify/internal/app"
"git.ophivana.moe/cat/fortify/internal/app" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/security/fortify/internal/system"
) )
var ( var (
@ -24,7 +23,7 @@ var (
dbusVerbose bool dbusVerbose bool
userName string userName string
enablements [state.EnableLength]bool enablements [system.ELen]bool
launchMethodText string launchMethodText string
) )
@ -42,15 +41,15 @@ func init() {
flag.BoolVar(&dbusVerbose, "dbus-log", false, "Force logging in the D-Bus proxy") flag.BoolVar(&dbusVerbose, "dbus-log", false, "Force logging in the D-Bus proxy")
flag.StringVar(&userName, "u", "chronos", "Passwd name of user to run as") flag.StringVar(&userName, "u", "chronos", "Passwd name of user to run as")
flag.BoolVar(&enablements[state.EnableWayland], "wayland", false, "Share Wayland socket") flag.BoolVar(&enablements[system.EWayland], "wayland", false, "Share Wayland socket")
flag.BoolVar(&enablements[state.EnableX], "X", false, "Share X11 socket and allow connection") flag.BoolVar(&enablements[system.EX11], "X", false, "Share X11 socket and allow connection")
flag.BoolVar(&enablements[state.EnableDBus], "dbus", false, "Proxy D-Bus connection") flag.BoolVar(&enablements[system.EDBus], "dbus", false, "Proxy D-Bus connection")
flag.BoolVar(&enablements[state.EnablePulse], "pulse", false, "Share PulseAudio socket and cookie") flag.BoolVar(&enablements[system.EPulse], "pulse", false, "Share PulseAudio socket and cookie")
} }
func init() { func init() {
methodHelpString := "Method of launching the child process, can be one of \"sudo\"" methodHelpString := "Method of launching the child process, can be one of \"sudo\""
if internal.SdBootedV { if os.SdBooted() {
methodHelpString += ", \"systemd\"" methodHelpString += ", \"systemd\""
} }
@ -60,12 +59,12 @@ func init() {
func tryTemplate() { func tryTemplate() {
if printTemplate { if printTemplate {
if s, err := json.MarshalIndent(app.Template(), "", " "); err != nil { if s, err := json.MarshalIndent(app.Template(), "", " "); err != nil {
fatalf("cannot generate template: %v", err) fmsg.Fatalf("cannot generate template: %v", err)
panic("unreachable") panic("unreachable")
} else { } else {
fmt.Println(string(s)) fmt.Println(string(s))
} }
os.Exit(0) fmsg.Exit(0)
} }
} }
@ -77,10 +76,10 @@ func loadConfig() *app.Config {
// config from file // config from file
c := new(app.Config) c := new(app.Config)
if f, err := os.Open(confPath); err != nil { if f, err := os.Open(confPath); err != nil {
fatalf("cannot access config file '%s': %s\n", confPath, err) fmsg.Fatalf("cannot access config file %q: %s", confPath, err)
panic("unreachable") panic("unreachable")
} else if err = json.NewDecoder(f).Decode(&c); err != nil { } else if err = json.NewDecoder(f).Decode(&c); err != nil {
fatalf("cannot parse config file '%s': %s\n", confPath, err) fmsg.Fatalf("cannot parse config file %q: %s", confPath, err)
panic("unreachable") panic("unreachable")
} else { } else {
return c return c
@ -98,19 +97,19 @@ func configFromFlags() (config *app.Config) {
} }
// enablements from flags // enablements from flags
for i := state.Enablement(0); i < state.EnableLength; i++ { for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
if enablements[i] { if enablements[i] {
config.Confinement.Enablements.Set(i) config.Confinement.Enablements.Set(i)
} }
} }
// parse D-Bus config file from flags if applicable // parse D-Bus config file from flags if applicable
if enablements[state.EnableDBus] { if enablements[system.EDBus] {
if dbusConfigSession == "builtin" { if dbusConfigSession == "builtin" {
config.Confinement.SessionBus = dbus.NewConfig(dbusID, true, mpris) config.Confinement.SessionBus = dbus.NewConfig(dbusID, true, mpris)
} else { } else {
if c, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil { if c, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
fatalf("cannot load session bus proxy config from %q: %s\n", dbusConfigSession, err) fmsg.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
} else { } else {
config.Confinement.SessionBus = c config.Confinement.SessionBus = c
} }
@ -119,7 +118,7 @@ func configFromFlags() (config *app.Config) {
// system bus proxy is optional // system bus proxy is optional
if dbusConfigSystem != "nil" { if dbusConfigSystem != "nil" {
if c, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil { if c, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
fatalf("cannot load system bus proxy config from %q: %s\n", dbusConfigSystem, err) fmsg.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
} else { } else {
config.Confinement.SystemBus = c config.Confinement.SystemBus = c
} }

View File

@ -9,7 +9,7 @@ import (
"strings" "strings"
"testing" "testing"
"git.ophivana.moe/cat/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
) )
func TestConfig_Args(t *testing.T) { func TestConfig_Args(t *testing.T) {

View File

@ -5,8 +5,8 @@ import (
"strings" "strings"
"testing" "testing"
"git.ophivana.moe/cat/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/cat/fortify/helper" "git.ophivana.moe/security/fortify/helper"
) )
func TestNew(t *testing.T) { func TestNew(t *testing.T) {

View File

@ -6,8 +6,8 @@ import (
"io" "io"
"sync" "sync"
"git.ophivana.moe/cat/fortify/helper" "git.ophivana.moe/security/fortify/helper"
"git.ophivana.moe/cat/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/helper/bwrap"
) )
// ProxyName is the file name or path to the proxy program. // ProxyName is the file name or path to the proxy program.
@ -28,6 +28,21 @@ type Proxy struct {
lock sync.RWMutex lock sync.RWMutex
} }
func (p *Proxy) Session() [2]string {
return p.session
}
func (p *Proxy) System() [2]string {
return p.system
}
func (p *Proxy) Sealed() bool {
p.lock.RLock()
defer p.lock.RUnlock()
return p.seal != nil
}
var ( var (
ErrConfig = errors.New("no configuration to seal") ErrConfig = errors.New("no configuration to seal")
) )

View File

@ -9,9 +9,9 @@ import (
"strconv" "strconv"
"strings" "strings"
"git.ophivana.moe/cat/fortify/helper" "git.ophivana.moe/security/fortify/helper"
"git.ophivana.moe/cat/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/cat/fortify/ldd" "git.ophivana.moe/security/fortify/ldd"
) )
// Start launches the D-Bus proxy and sets up the Wait method. // Start launches the D-Bus proxy and sets up the Wait method.
@ -79,11 +79,9 @@ func (p *Proxy) Start(ready chan error, output io.Writer, sandbox bool) error {
} }
} }
} }
bindTargetDedup := make([][2]string, 0, len(bindTarget))
for k := range bindTarget { for k := range bindTarget {
bindTargetDedup = append(bindTargetDedup, [2]string{k, k}) bc.Bind(k, k, false, true)
} }
bc.Bind = append(bc.Bind, bindTargetDedup...)
roBindTarget := make(map[string]struct{}, 2+1+len(proxyDeps)) roBindTarget := make(map[string]struct{}, 2+1+len(proxyDeps))
@ -103,11 +101,9 @@ func (p *Proxy) Start(ready chan error, output io.Writer, sandbox bool) error {
} }
} }
roBindTargetDedup := make([][2]string, 0, len(roBindTarget))
for k := range roBindTarget { for k := range roBindTarget {
roBindTargetDedup = append(roBindTargetDedup, [2]string{k, k}) bc.Bind(k, k)
} }
bc.ROBind = append(bc.ROBind, roBindTargetDedup...)
h = helper.MustNewBwrap(bc, p.seal, toolPath, argF) h = helper.MustNewBwrap(bc, p.seal, toolPath, argF)
cmd = h.Unwrap() cmd = h.Unwrap()

View File

@ -3,7 +3,7 @@ package dbus_test
import ( import (
"sync" "sync"
"git.ophivana.moe/cat/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
) )
var samples = []dbusTestCase{ var samples = []dbusTestCase{

View File

@ -3,7 +3,7 @@ package dbus_test
import ( import (
"testing" "testing"
"git.ophivana.moe/cat/fortify/helper" "git.ophivana.moe/security/fortify/helper"
) )
func TestHelperChildStub(t *testing.T) { func TestHelperChildStub(t *testing.T) {

View File

@ -2,22 +2,21 @@ package main
import ( import (
"errors" "errors"
"fmt"
"os"
"git.ophivana.moe/cat/fortify/internal/app" "git.ophivana.moe/security/fortify/internal/app"
"git.ophivana.moe/security/fortify/internal/fmsg"
) )
func logWaitError(err error) { func logWaitError(err error) {
var e *app.BaseError var e *fmsg.BaseError
if !app.AsBaseError(err, &e) { if !fmsg.AsBaseError(err, &e) {
fmt.Println("fortify: wait failed:", err) fmsg.Println("wait failed:", err)
} else { } else {
// Wait only returns either *app.ProcessError or *app.StateStoreError wrapped in a *app.BaseError // Wait only returns either *app.ProcessError or *app.StateStoreError wrapped in a *app.BaseError
var se *app.StateStoreError var se *app.StateStoreError
if !errors.As(err, &se) { if !errors.As(err, &se) {
// does not need special handling // does not need special handling
fmt.Print("fortify: " + e.Message()) fmsg.Print(e.Message())
} else { } else {
// inner error are either unwrapped store errors // inner error are either unwrapped store errors
// or joined errors returned by *appSealTx revert // or joined errors returned by *appSealTx revert
@ -25,19 +24,19 @@ func logWaitError(err error) {
var ej app.RevertCompoundError var ej app.RevertCompoundError
if !errors.As(se.InnerErr, &ej) { if !errors.As(se.InnerErr, &ej) {
// does not require special handling // does not require special handling
fmt.Print("fortify: " + e.Message()) fmsg.Print(e.Message())
} else { } else {
errs := ej.Unwrap() errs := ej.Unwrap()
// every error here is wrapped in *app.BaseError // every error here is wrapped in *app.BaseError
for _, ei := range errs { for _, ei := range errs {
var eb *app.BaseError var eb *fmsg.BaseError
if !errors.As(ei, &eb) { if !errors.As(ei, &eb) {
// unreachable // unreachable
fmt.Println("fortify: invalid error type returned by revert:", ei) fmsg.Println("invalid error type returned by revert:", ei)
} else { } else {
// print inner *app.BaseError message // print inner *app.BaseError message
fmt.Print("fortify: " + eb.Message()) fmsg.Print(eb.Message())
} }
} }
} }
@ -46,16 +45,11 @@ func logWaitError(err error) {
} }
func logBaseError(err error, message string) { func logBaseError(err error, message string) {
var e *app.BaseError var e *fmsg.BaseError
if app.AsBaseError(err, &e) { if fmsg.AsBaseError(err, &e) {
fmt.Print("fortify: " + e.Message()) fmsg.Print(e.Message())
} else { } else {
fmt.Println(message, err) fmsg.Println(message, err)
} }
} }
func fatalf(format string, a ...any) {
fmt.Printf("fortify: "+format, a...)
os.Exit(1)
}

View File

@ -34,6 +34,10 @@
devShells = forAllSystems (system: { devShells = forAllSystems (system: {
default = nixpkgsFor.${system}.mkShell { default = nixpkgsFor.${system}.mkShell {
buildInputs = with nixpkgsFor.${system}; self.packages.${system}.fortify.buildInputs;
};
withPackage = nixpkgsFor.${system}.mkShell {
buildInputs = buildInputs =
with nixpkgsFor.${system}; with nixpkgsFor.${system};
self.packages.${system}.fortify.buildInputs ++ [ self.packages.${system}.fortify ]; self.packages.${system}.fortify.buildInputs ++ [ self.packages.${system}.fortify ];

2
go.mod
View File

@ -1,3 +1,3 @@
module git.ophivana.moe/cat/fortify module git.ophivana.moe/security/fortify
go 1.22 go 1.22

View File

@ -6,7 +6,7 @@ import (
"strings" "strings"
"testing" "testing"
"git.ophivana.moe/cat/fortify/helper" "git.ophivana.moe/security/fortify/helper"
) )
func Test_argsFD_String(t *testing.T) { func Test_argsFD_String(t *testing.T) {

View File

@ -7,7 +7,7 @@ import (
"strconv" "strconv"
"sync" "sync"
"git.ophivana.moe/cat/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/helper/bwrap"
) )
// BubblewrapName is the file name or path to bubblewrap. // BubblewrapName is the file name or path to bubblewrap.

77
helper/bwrap/arg.go Normal file
View File

@ -0,0 +1,77 @@
package bwrap
import "encoding/gob"
type Builder interface {
Len() int
Append(args *[]string)
}
type FSBuilder interface {
Path() string
Builder
}
func init() {
gob.Register(new(pairF))
gob.Register(new(stringF))
}
type pairF [3]string
func (p *pairF) Path() string {
return p[2]
}
func (p *pairF) Len() int {
return len(p) // compiler replaces this with 3
}
func (p *pairF) Append(args *[]string) {
*args = append(*args, p[0], p[1], p[2])
}
type stringF [2]string
func (s stringF) Path() string {
return s[1]
}
func (s stringF) Len() int {
return len(s) // compiler replaces this with 2
}
func (s stringF) Append(args *[]string) {
*args = append(*args, s[0], s[1])
}
// Args returns a slice of bwrap args corresponding to c.
func (c *Config) Args() (args []string) {
builders := []Builder{
c.boolArgs(),
c.intArgs(),
c.stringArgs(),
c.pairArgs(),
}
// copy FSBuilder slice to builder slice
fb := make([]Builder, len(c.Filesystem)+1)
for i, f := range c.Filesystem {
fb[i] = f
}
fb[len(fb)-1] = c.Chmod
builders = append(builders, fb...)
// accumulate arg count
argc := 0
for _, b := range builders {
argc += b.Len()
}
args = make([]string, 0, argc)
for _, b := range builders {
b.Append(&args)
}
return
}

View File

@ -0,0 +1,13 @@
package bwrap
const (
Tmpfs = iota
Dir
Symlink
)
var awkwardArgs = [...]string{
Tmpfs: "--tmpfs",
Dir: "--dir",
Symlink: "--symlink",
}

View File

@ -0,0 +1,81 @@
package bwrap
const (
UnshareAll = iota
UnshareUser
UnshareIPC
UnsharePID
UnshareNet
UnshareUTS
UnshareCGroup
ShareNet
UserNS
Clearenv
NewSession
DieWithParent
AsInit
)
var boolArgs = [...][]string{
UnshareAll: {"--unshare-all", "--unshare-user"},
UnshareUser: {"--unshare-user"},
UnshareIPC: {"--unshare-ipc"},
UnsharePID: {"--unshare-pid"},
UnshareNet: {"--unshare-net"},
UnshareUTS: {"--unshare-uts"},
UnshareCGroup: {"--unshare-cgroup"},
ShareNet: {"--share-net"},
UserNS: {"--disable-userns", "--assert-userns-disabled"},
Clearenv: {"--clearenv"},
NewSession: {"--new-session"},
DieWithParent: {"--die-with-parent"},
AsInit: {"--as-pid-1"},
}
func (c *Config) boolArgs() Builder {
b := boolArg{
UserNS: !c.UserNS,
Clearenv: c.Clearenv,
NewSession: c.NewSession,
DieWithParent: c.DieWithParent,
AsInit: c.AsInit,
}
if c.Unshare == nil {
b[UnshareAll] = true
b[ShareNet] = c.Net
} else {
b[UnshareUser] = c.Unshare.User
b[UnshareIPC] = c.Unshare.IPC
b[UnsharePID] = c.Unshare.PID
b[UnshareNet] = c.Unshare.Net
b[UnshareUTS] = c.Unshare.UTS
b[UnshareCGroup] = c.Unshare.CGroup
}
return &b
}
type boolArg [len(boolArgs)]bool
func (b *boolArg) Len() (l int) {
for i, v := range b {
if v {
l += len(boolArgs[i])
}
}
return
}
func (b *boolArg) Append(args *[]string) {
for i, v := range b {
if v {
*args = append(*args, boolArgs[i]...)
}
}
}

View File

@ -0,0 +1,47 @@
package bwrap
import "strconv"
const (
UID = iota
GID
Perms
Size
)
var intArgs = [...]string{
UID: "--uid",
GID: "--gid",
Perms: "--perms",
Size: "--size",
}
func (c *Config) intArgs() Builder {
// Arg types:
// Perms
// are handled by the sequential builder
return &intArg{
UID: c.UID,
GID: c.GID,
}
}
type intArg [len(intArgs)]*int
func (n *intArg) Len() (l int) {
for _, v := range n {
if v != nil {
l += 2
}
}
return
}
func (n *intArg) Append(args *[]string) {
for i, v := range n {
if v != nil {
*args = append(*args, intArgs[i], strconv.Itoa(*v))
}
}
}

View File

@ -0,0 +1,73 @@
package bwrap
import (
"slices"
)
const (
SetEnv = iota
Bind
BindTry
DevBind
DevBindTry
ROBind
ROBindTry
Chmod
)
var pairArgs = [...]string{
SetEnv: "--setenv",
Bind: "--bind",
BindTry: "--bind-try",
DevBind: "--dev-bind",
DevBindTry: "--dev-bind-try",
ROBind: "--ro-bind",
ROBindTry: "--ro-bind-try",
Chmod: "--chmod",
}
func (c *Config) pairArgs() Builder {
var n pairArg
n[SetEnv] = make([][2]string, len(c.SetEnv))
keys := make([]string, 0, len(c.SetEnv))
for k := range c.SetEnv {
keys = append(keys, k)
}
slices.Sort(keys)
for i, k := range keys {
n[SetEnv][i] = [2]string{k, c.SetEnv[k]}
}
// Arg types:
// Bind
// BindTry
// DevBind
// DevBindTry
// ROBind
// ROBindTry
// Chmod
// are handled by the sequential builder
return &n
}
type pairArg [len(pairArgs)][][2]string
func (p *pairArg) Len() (l int) {
for _, v := range p {
l += len(v) * 3
}
return
}
func (p *pairArg) Append(args *[]string) {
for i, arg := range p {
for _, v := range arg {
*args = append(*args, pairArgs[i], v[0], v[1])
}
}
}

View File

@ -0,0 +1,65 @@
package bwrap
const (
Hostname = iota
Chdir
UnsetEnv
LockFile
RemountRO
Procfs
DevTmpfs
Mqueue
)
var stringArgs = [...]string{
Hostname: "--hostname",
Chdir: "--chdir",
UnsetEnv: "--unsetenv",
LockFile: "--lock-file",
RemountRO: "--remount-ro",
Procfs: "--proc",
DevTmpfs: "--dev",
Mqueue: "--mqueue",
}
func (c *Config) stringArgs() Builder {
n := stringArg{
UnsetEnv: c.UnsetEnv,
LockFile: c.LockFile,
}
if c.Hostname != "" {
n[Hostname] = []string{c.Hostname}
}
if c.Chdir != "" {
n[Chdir] = []string{c.Chdir}
}
// Arg types:
// RemountRO
// Procfs
// DevTmpfs
// Mqueue
// are handled by the sequential builder
return &n
}
type stringArg [len(stringArgs)][]string
func (s *stringArg) Len() (l int) {
for _, arg := range s {
l += len(arg) * 2
}
return
}
func (s *stringArg) Append(args *[]string) {
for i, arg := range s {
for _, v := range arg {
*args = append(*args, stringArgs[i], v)
}
}
}

View File

@ -1,64 +0,0 @@
package bwrap
const (
UnshareAll = iota
UnshareUser
UnshareIPC
UnsharePID
UnshareNet
UnshareUTS
UnshareCGroup
ShareNet
UserNS
Clearenv
NewSession
DieWithParent
AsInit
boolC
)
var boolArgs = func() (b [boolC][]string) {
b[UnshareAll] = []string{"--unshare-all", "--unshare-user"}
b[UnshareUser] = []string{"--unshare-user"}
b[UnshareIPC] = []string{"--unshare-ipc"}
b[UnsharePID] = []string{"--unshare-pid"}
b[UnshareNet] = []string{"--unshare-net"}
b[UnshareUTS] = []string{"--unshare-uts"}
b[UnshareCGroup] = []string{"--unshare-cgroup"}
b[ShareNet] = []string{"--share-net"}
b[UserNS] = []string{"--disable-userns", "--assert-userns-disabled"}
b[Clearenv] = []string{"--clearenv"}
b[NewSession] = []string{"--new-session"}
b[DieWithParent] = []string{"--die-with-parent"}
b[AsInit] = []string{"--as-pid-1"}
return
}()
func (c *Config) boolArgs() (b [boolC]bool) {
if c.Unshare == nil {
b[UnshareAll] = true
b[ShareNet] = c.Net
} else {
b[UnshareUser] = c.Unshare.User
b[UnshareIPC] = c.Unshare.IPC
b[UnsharePID] = c.Unshare.PID
b[UnshareNet] = c.Unshare.Net
b[UnshareUTS] = c.Unshare.UTS
b[UnshareCGroup] = c.Unshare.CGroup
}
b[UserNS] = !c.UserNS
b[Clearenv] = c.Clearenv
b[NewSession] = c.NewSession
b[DieWithParent] = c.DieWithParent
b[AsInit] = c.AsInit
return
}

View File

@ -1,77 +1,14 @@
package bwrap package bwrap
import ( import (
"encoding/gob"
"os" "os"
"strconv" "strconv"
) )
func (c *Config) Args() (args []string) { func init() {
b := c.boolArgs() gob.Register(new(PermConfig[SymlinkConfig]))
n := c.intArgs() gob.Register(new(PermConfig[*TmpfsConfig]))
g := c.interfaceArgs()
s := c.stringArgs()
p := c.pairArgs()
argc := 0
for i, arg := range b {
if arg {
argc += len(boolArgs[i])
}
}
for _, arg := range n {
if arg != nil {
argc += 2
}
}
for _, arg := range g {
argc += len(arg) * 3
}
for _, arg := range s {
argc += len(arg) * 2
}
for _, arg := range p {
argc += len(arg) * 3
}
args = make([]string, 0, argc)
for i, arg := range b {
if arg {
args = append(args, boolArgs[i]...)
}
}
for i, arg := range n {
if arg != nil {
args = append(args, intArgs[i], strconv.Itoa(*arg))
}
}
for i, arg := range g {
for _, v := range arg {
if v.Later() {
continue
}
args = append(args, v.Value(interfaceArgs[i])...)
}
}
for i, arg := range s {
for _, v := range arg {
args = append(args, stringArgs[i], v)
}
}
for i, arg := range p {
for _, v := range arg {
args = append(args, pairArgs[i], v[0], v[1])
}
}
for i, arg := range g {
for _, v := range arg {
if !v.Later() {
continue
}
args = append(args, v.Value(interfaceArgs[i])...)
}
}
return
} }
type Config struct { type Config struct {
@ -114,53 +51,12 @@ type Config struct {
// (--lock-file DEST) // (--lock-file DEST)
LockFile []string `json:"lock_file,omitempty"` LockFile []string `json:"lock_file,omitempty"`
// bind mount host path on sandbox // ordered filesystem args
// (--bind SRC DEST) Filesystem []FSBuilder
Bind [][2]string `json:"bind,omitempty"`
// equal to Bind but ignores non-existent host path
// (--bind-try SRC DEST)
BindTry [][2]string `json:"bind_try,omitempty"`
// bind mount host path on sandbox, allowing device access
// (--dev-bind SRC DEST)
DevBind [][2]string `json:"dev_bind,omitempty"`
// equal to DevBind but ignores non-existent host path
// (--dev-bind-try SRC DEST)
DevBindTry [][2]string `json:"dev_bind_try,omitempty"`
// bind mount host path readonly on sandbox
// (--ro-bind SRC DEST)
ROBind [][2]string `json:"ro_bind,omitempty"`
// equal to ROBind but ignores non-existent host path
// (--ro-bind-try SRC DEST)
ROBindTry [][2]string `json:"ro_bind_try,omitempty"`
// remount path as readonly; does not recursively remount
// (--remount-ro DEST)
RemountRO []string `json:"remount_ro,omitempty"`
// mount new procfs in sandbox
// (--proc DEST)
Procfs []string `json:"proc,omitempty"`
// mount new dev in sandbox
// (--dev DEST)
DevTmpfs []string `json:"dev,omitempty"`
// mount new tmpfs in sandbox
// (--tmpfs DEST)
Tmpfs []PermConfig[TmpfsConfig] `json:"tmpfs,omitempty"`
// mount new mqueue in sandbox
// (--mqueue DEST)
Mqueue []string `json:"mqueue,omitempty"`
// create dir in sandbox
// (--dir DEST)
Dir []PermConfig[string] `json:"dir,omitempty"`
// create symlink within sandbox
// (--symlink SRC DEST)
Symlink []PermConfig[[2]string] `json:"symlink,omitempty"`
// change permissions (must already exist) // change permissions (must already exist)
// (--chmod OCTAL PATH) // (--chmod OCTAL PATH)
Chmod map[string]os.FileMode `json:"chmod,omitempty"` Chmod ChmodConfig `json:"chmod,omitempty"`
// create a new terminal session // create a new terminal session
// (--new-session) // (--new-session)
@ -217,6 +113,34 @@ type UnshareConfig struct {
CGroup bool `json:"cgroup"` CGroup bool `json:"cgroup"`
} }
type PermConfig[T FSBuilder] struct {
// set permissions of next argument
// (--perms OCTAL)
Mode *os.FileMode `json:"mode,omitempty"`
// path to get the new permission
// (--bind-data, --file, etc.)
Inner T `json:"path"`
}
func (p *PermConfig[T]) Path() string {
return p.Inner.Path()
}
func (p *PermConfig[T]) Len() int {
if p.Mode != nil {
return p.Inner.Len() + 2
} else {
return p.Inner.Len()
}
}
func (p *PermConfig[T]) Append(args *[]string) {
if p.Mode != nil {
*args = append(*args, intArgs[Perms], strconv.FormatInt(int64(*p.Mode), 8))
}
p.Inner.Append(args)
}
type TmpfsConfig struct { type TmpfsConfig struct {
// set size of tmpfs // set size of tmpfs
// (--size BYTES) // (--size BYTES)
@ -226,62 +150,47 @@ type TmpfsConfig struct {
Dir string `json:"dir"` Dir string `json:"dir"`
} }
type argOf interface { func (t *TmpfsConfig) Path() string {
Value(arg string) (args []string) return t.Dir
Later() bool
} }
func copyToArgOfSlice[T [2]string | string | TmpfsConfig](src []PermConfig[T]) (dst []argOf) { func (t *TmpfsConfig) Len() int {
dst = make([]argOf, len(src)) if t.Size > 0 {
for i, arg := range src { return 4
dst[i] = arg
}
return
}
type PermConfig[T [2]string | string | TmpfsConfig] struct {
// append this at the end of the argument stream
Last bool
// set permissions of next argument
// (--perms OCTAL)
Mode *os.FileMode `json:"mode,omitempty"`
// path to get the new permission
// (--bind-data, --file, etc.)
Path T
}
func (p PermConfig[T]) Later() bool {
return p.Last
}
func (p PermConfig[T]) Value(arg string) (args []string) {
// max possible size
if p.Mode != nil {
args = make([]string, 0, 6)
args = append(args, "--perms", strconv.Itoa(int(*p.Mode)))
} else { } else {
args = make([]string, 0, 4) return 2
}
} }
switch v := any(p.Path).(type) { func (t *TmpfsConfig) Append(args *[]string) {
case string: if t.Size > 0 {
args = append(args, arg, v) *args = append(*args, intArgs[Size], strconv.Itoa(t.Size))
return }
case [2]string: *args = append(*args, awkwardArgs[Tmpfs], t.Dir)
args = append(args, arg, v[0], v[1])
return
case TmpfsConfig:
if arg != "--tmpfs" {
panic("unreachable")
} }
if v.Size > 0 { type SymlinkConfig [2]string
args = append(args, "--size", strconv.Itoa(v.Size))
func (s SymlinkConfig) Path() string {
return s[1]
} }
args = append(args, arg, v.Dir)
return func (s SymlinkConfig) Len() int {
default: return 3
panic("unreachable") }
func (s SymlinkConfig) Append(args *[]string) {
*args = append(*args, awkwardArgs[Symlink], s[0], s[1])
}
type ChmodConfig map[string]os.FileMode
func (c ChmodConfig) Len() int {
return len(c)
}
func (c ChmodConfig) Append(args *[]string) {
for path, mode := range c {
*args = append(*args, pairArgs[Chmod], strconv.FormatInt(int64(mode), 8), path)
} }
} }

View File

@ -1,22 +0,0 @@
package bwrap
const (
UID = iota
GID
intC
)
var intArgs = func() (n [intC]string) {
n[UID] = "--uid"
n[GID] = "--gid"
return
}()
func (c *Config) intArgs() (n [intC]*int) {
n[UID] = c.UID
n[GID] = c.GID
return
}

View File

@ -1,25 +0,0 @@
package bwrap
const (
Tmpfs = iota
Dir
Symlink
interfaceC
)
var interfaceArgs = func() (g [interfaceC]string) {
g[Tmpfs] = "--tmpfs"
g[Dir] = "--dir"
g[Symlink] = "--symlink"
return
}()
func (c *Config) interfaceArgs() (g [interfaceC][]argOf) {
g[Tmpfs] = copyToArgOfSlice(c.Tmpfs)
g[Dir] = copyToArgOfSlice(c.Dir)
g[Symlink] = copyToArgOfSlice(c.Symlink)
return
}

View File

@ -1,54 +0,0 @@
package bwrap
import "strconv"
const (
SetEnv = iota
Bind
BindTry
DevBind
DevBindTry
ROBind
ROBindTry
Chmod
pairC
)
var pairArgs = func() (n [pairC]string) {
n[SetEnv] = "--setenv"
n[Bind] = "--bind"
n[BindTry] = "--bind-try"
n[DevBind] = "--dev-bind"
n[DevBindTry] = "--dev-bind-try"
n[ROBind] = "--ro-bind"
n[ROBindTry] = "--ro-bind-try"
n[Chmod] = "--chmod"
return
}()
func (c *Config) pairArgs() (n [pairC][][2]string) {
n[SetEnv] = make([][2]string, 0, len(c.SetEnv))
for k, v := range c.SetEnv {
n[SetEnv] = append(n[SetEnv], [2]string{k, v})
}
n[Bind] = c.Bind
n[BindTry] = c.BindTry
n[DevBind] = c.DevBind
n[DevBindTry] = c.DevBindTry
n[ROBind] = c.ROBind
n[ROBindTry] = c.ROBindTry
n[Chmod] = make([][2]string, 0, len(c.Chmod))
for path, octal := range c.Chmod {
n[Chmod] = append(n[Chmod], [2]string{strconv.Itoa(int(octal)), path})
}
return
}

138
helper/bwrap/config.set.go Normal file
View File

@ -0,0 +1,138 @@
package bwrap
import "os"
/*
Bind binds mount src on host to dest in sandbox.
Bind(src, dest) bind mount host path readonly on sandbox
(--ro-bind SRC DEST).
Bind(src, dest, true) equal to ROBind but ignores non-existent host path
(--ro-bind-try SRC DEST).
Bind(src, dest, false, true) bind mount host path on sandbox.
(--bind SRC DEST).
Bind(src, dest, true, true) equal to Bind but ignores non-existent host path
(--bind-try SRC DEST).
Bind(src, dest, false, true, true) bind mount host path on sandbox, allowing device access
(--dev-bind SRC DEST).
Bind(src, dest, true, true, true) equal to DevBind but ignores non-existent host path
(--dev-bind-try SRC DEST).
*/
func (c *Config) Bind(src, dest string, opts ...bool) *Config {
var (
try bool
write bool
dev bool
)
if len(opts) > 0 {
try = opts[0]
}
if len(opts) > 1 {
write = opts[1]
}
if len(opts) > 2 {
dev = opts[2]
}
if dev {
if try {
c.Filesystem = append(c.Filesystem, &pairF{pairArgs[DevBindTry], src, dest})
} else {
c.Filesystem = append(c.Filesystem, &pairF{pairArgs[DevBind], src, dest})
}
return c
} else if write {
if try {
c.Filesystem = append(c.Filesystem, &pairF{pairArgs[BindTry], src, dest})
} else {
c.Filesystem = append(c.Filesystem, &pairF{pairArgs[Bind], src, dest})
}
return c
} else {
if try {
c.Filesystem = append(c.Filesystem, &pairF{pairArgs[ROBindTry], src, dest})
} else {
c.Filesystem = append(c.Filesystem, &pairF{pairArgs[ROBind], src, dest})
}
return c
}
}
// RemountRO remount path as readonly; does not recursively remount
// (--remount-ro DEST)
func (c *Config) RemountRO(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{stringArgs[RemountRO], dest})
return c
}
// Procfs mount new procfs in sandbox
// (--proc DEST)
func (c *Config) Procfs(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{stringArgs[Procfs], dest})
return c
}
// DevTmpfs mount new dev in sandbox
// (--dev DEST)
func (c *Config) DevTmpfs(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{stringArgs[DevTmpfs], dest})
return c
}
// Tmpfs mount new tmpfs in sandbox
// (--tmpfs DEST)
func (c *Config) Tmpfs(dest string, size int, perm ...os.FileMode) *Config {
tmpfs := &PermConfig[*TmpfsConfig]{Inner: &TmpfsConfig{Dir: dest}}
if size >= 0 {
tmpfs.Inner.Size = size
}
if len(perm) == 1 {
tmpfs.Mode = &perm[0]
}
c.Filesystem = append(c.Filesystem, tmpfs)
return c
}
// Mqueue mount new mqueue in sandbox
// (--mqueue DEST)
func (c *Config) Mqueue(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{stringArgs[Mqueue], dest})
return c
}
// Dir create dir in sandbox
// (--dir DEST)
func (c *Config) Dir(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{stringArgs[Dir], dest})
return c
}
// Symlink create symlink within sandbox
// (--symlink SRC DEST)
func (c *Config) Symlink(src, dest string, perm ...os.FileMode) *Config {
symlink := &PermConfig[SymlinkConfig]{Inner: SymlinkConfig{src, dest}}
if len(perm) == 1 {
symlink.Mode = &perm[0]
}
c.Filesystem = append(c.Filesystem, symlink)
return c
}
// SetUID sets custom uid in the sandbox, requires new user namespace (--uid UID).
func (c *Config) SetUID(uid int) *Config {
if uid >= 0 {
c.UID = &uid
}
return c
}
// SetGID sets custom gid in the sandbox, requires new user namespace (--gid GID).
func (c *Config) SetGID(gid int) *Config {
if gid >= 0 {
c.GID = &gid
}
return c
}

View File

@ -1,44 +0,0 @@
package bwrap
const (
Hostname = iota
Chdir
UnsetEnv
LockFile
RemountRO
Procfs
DevTmpfs
Mqueue
stringC
)
var stringArgs = func() (n [stringC]string) {
n[Hostname] = "--hostname"
n[Chdir] = "--chdir"
n[UnsetEnv] = "--unsetenv"
n[LockFile] = "--lock-file"
n[RemountRO] = "--remount-ro"
n[Procfs] = "--proc"
n[DevTmpfs] = "--dev"
n[Mqueue] = "--mqueue"
return
}()
func (c *Config) stringArgs() (n [stringC][]string) {
if c.Hostname != "" {
n[Hostname] = []string{c.Hostname}
}
if c.Chdir != "" {
n[Chdir] = []string{c.Chdir}
}
n[UnsetEnv] = c.UnsetEnv
n[LockFile] = c.LockFile
n[RemountRO] = c.RemountRO
n[Procfs] = c.Procfs
n[DevTmpfs] = c.DevTmpfs
n[Mqueue] = c.Mqueue
return
}

View File

@ -13,47 +13,38 @@ func TestConfig_Args(t *testing.T) {
}{ }{
{ {
name: "xdg-dbus-proxy constraint sample", name: "xdg-dbus-proxy constraint sample",
conf: &Config{ conf: (&Config{
Unshare: nil, Unshare: nil,
UserNS: false, UserNS: false,
Clearenv: true, Clearenv: true,
Symlink: []PermConfig[[2]string]{
{Path: [2]string{"usr/bin", "/bin"}},
{Path: [2]string{"var/home", "/home"}},
{Path: [2]string{"usr/lib", "/lib"}},
{Path: [2]string{"usr/lib64", "/lib64"}},
{Path: [2]string{"run/media", "/media"}},
{Path: [2]string{"var/mnt", "/mnt"}},
{Path: [2]string{"var/opt", "/opt"}},
{Path: [2]string{"sysroot/ostree", "/ostree"}},
{Path: [2]string{"var/roothome", "/root"}},
{Path: [2]string{"usr/sbin", "/sbin"}},
{Path: [2]string{"var/srv", "/srv"}},
},
Bind: [][2]string{
{"/run", "/run"},
{"/tmp", "/tmp"},
{"/var", "/var"},
{"/run/user/1971/.dbus-proxy/", "/run/user/1971/.dbus-proxy/"},
},
ROBind: [][2]string{
{"/boot", "/boot"},
{"/dev", "/dev"},
{"/proc", "/proc"},
{"/sys", "/sys"},
{"/sysroot", "/sysroot"},
{"/usr", "/usr"},
{"/etc", "/etc"},
},
DieWithParent: true, DieWithParent: true,
}, }).
Symlink("usr/bin", "/bin").
Symlink("var/home", "/home").
Symlink("usr/lib", "/lib").
Symlink("usr/lib64", "/lib64").
Symlink("run/media", "/media").
Symlink("var/mnt", "/mnt").
Symlink("var/opt", "/opt").
Symlink("sysroot/ostree", "/ostree").
Symlink("var/roothome", "/root").
Symlink("usr/sbin", "/sbin").
Symlink("var/srv", "/srv").
Bind("/run", "/run", false, true).
Bind("/tmp", "/tmp", false, true).
Bind("/var", "/var", false, true).
Bind("/run/user/1971/.dbus-proxy/", "/run/user/1971/.dbus-proxy/", false, true).
Bind("/boot", "/boot").
Bind("/dev", "/dev").
Bind("/proc", "/proc").
Bind("/sys", "/sys").
Bind("/sysroot", "/sysroot").
Bind("/usr", "/usr").
Bind("/etc", "/etc"),
want: []string{ want: []string{
"--unshare-all", "--unshare-all", "--unshare-user",
"--unshare-user", "--disable-userns", "--assert-userns-disabled",
"--disable-userns", "--clearenv", "--die-with-parent",
"--assert-userns-disabled",
"--clearenv",
"--die-with-parent",
"--symlink", "usr/bin", "/bin", "--symlink", "usr/bin", "/bin",
"--symlink", "var/home", "/home", "--symlink", "var/home", "/home",
"--symlink", "usr/lib", "/lib", "--symlink", "usr/lib", "/lib",
@ -78,6 +69,148 @@ func TestConfig_Args(t *testing.T) {
"--ro-bind", "/etc", "/etc", "--ro-bind", "/etc", "/etc",
}, },
}, },
{
name: "fortify permissive default nixos",
conf: (&Config{
Unshare: nil,
Net: true,
UserNS: true,
Clearenv: true,
SetEnv: map[string]string{
"HOME": "/home/chronos",
"TERM": "xterm-256color",
"FORTIFY_INIT": "3",
"XDG_RUNTIME_DIR": "/run/user/150",
"XDG_SESSION_CLASS": "user",
"XDG_SESSION_TYPE": "tty",
"SHELL": "/run/current-system/sw/bin/zsh",
"USER": "chronos",
},
DieWithParent: true,
AsInit: true,
}).SetUID(65534).SetGID(65534).
Procfs("/proc").DevTmpfs("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", false, true).
Bind("/boot", "/boot", false, true).
Bind("/etc", "/etc", false, true).
Bind("/home", "/home", false, true).
Bind("/lib", "/lib", false, true).
Bind("/lib64", "/lib64", false, true).
Bind("/nix", "/nix", false, true).
Bind("/root", "/root", false, true).
Bind("/srv", "/srv", false, true).
Bind("/sys", "/sys", false, true).
Bind("/usr", "/usr", false, true).
Bind("/var", "/var", false, true).
Bind("/run/NetworkManager", "/run/NetworkManager", false, true).
Bind("/run/agetty.reload", "/run/agetty.reload", false, true).
Bind("/run/binfmt", "/run/binfmt", false, true).
Bind("/run/booted-system", "/run/booted-system", false, true).
Bind("/run/credentials", "/run/credentials", false, true).
Bind("/run/cryptsetup", "/run/cryptsetup", false, true).
Bind("/run/current-system", "/run/current-system", false, true).
Bind("/run/host", "/run/host", false, true).
Bind("/run/keys", "/run/keys", false, true).
Bind("/run/libvirt", "/run/libvirt", false, true).
Bind("/run/libvirtd.pid", "/run/libvirtd.pid", false, true).
Bind("/run/lock", "/run/lock", false, true).
Bind("/run/log", "/run/log", false, true).
Bind("/run/lvm", "/run/lvm", false, true).
Bind("/run/mount", "/run/mount", false, true).
Bind("/run/nginx", "/run/nginx", false, true).
Bind("/run/nscd", "/run/nscd", false, true).
Bind("/run/opengl-driver", "/run/opengl-driver", false, true).
Bind("/run/pppd", "/run/pppd", false, true).
Bind("/run/resolvconf", "/run/resolvconf", false, true).
Bind("/run/sddm", "/run/sddm", false, true).
Bind("/run/syncoid", "/run/syncoid", false, true).
Bind("/run/systemd", "/run/systemd", false, true).
Bind("/run/tmpfiles.d", "/run/tmpfiles.d", false, true).
Bind("/run/udev", "/run/udev", false, true).
Bind("/run/udisks2", "/run/udisks2", false, true).
Bind("/run/utmp", "/run/utmp", false, true).
Bind("/run/virtlogd.pid", "/run/virtlogd.pid", false, true).
Bind("/run/wrappers", "/run/wrappers", false, true).
Bind("/run/zed.pid", "/run/zed.pid", false, true).
Bind("/run/zed.state", "/run/zed.state", false, true).
Bind("/tmp/fortify.1971/tmpdir/150", "/tmp", false, true).
Tmpfs("/tmp/fortify.1971", 1048576).
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/150", 8388608).
Bind("/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/passwd", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/passwd").
Bind("/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/group", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/group").
Bind("/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/passwd", "/etc/passwd").
Bind("/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/group", "/etc/group").
Tmpfs("/var/run/nscd", 8192),
want: []string{
"--unshare-all", "--unshare-user", "--share-net",
"--clearenv", "--die-with-parent", "--as-pid-1",
"--uid", "65534",
"--gid", "65534",
"--setenv", "FORTIFY_INIT", "3",
"--setenv", "HOME", "/home/chronos",
"--setenv", "SHELL", "/run/current-system/sw/bin/zsh",
"--setenv", "TERM", "xterm-256color",
"--setenv", "USER", "chronos",
"--setenv", "XDG_RUNTIME_DIR", "/run/user/150",
"--setenv", "XDG_SESSION_CLASS", "user",
"--setenv", "XDG_SESSION_TYPE", "tty",
"--proc", "/proc", "--dev", "/dev",
"--mqueue", "/dev/mqueue",
"--bind", "/bin", "/bin",
"--bind", "/boot", "/boot",
"--bind", "/etc", "/etc",
"--bind", "/home", "/home",
"--bind", "/lib", "/lib",
"--bind", "/lib64", "/lib64",
"--bind", "/nix", "/nix",
"--bind", "/root", "/root",
"--bind", "/srv", "/srv",
"--bind", "/sys", "/sys",
"--bind", "/usr", "/usr",
"--bind", "/var", "/var",
"--bind", "/run/NetworkManager", "/run/NetworkManager",
"--bind", "/run/agetty.reload", "/run/agetty.reload",
"--bind", "/run/binfmt", "/run/binfmt",
"--bind", "/run/booted-system", "/run/booted-system",
"--bind", "/run/credentials", "/run/credentials",
"--bind", "/run/cryptsetup", "/run/cryptsetup",
"--bind", "/run/current-system", "/run/current-system",
"--bind", "/run/host", "/run/host",
"--bind", "/run/keys", "/run/keys",
"--bind", "/run/libvirt", "/run/libvirt",
"--bind", "/run/libvirtd.pid", "/run/libvirtd.pid",
"--bind", "/run/lock", "/run/lock",
"--bind", "/run/log", "/run/log",
"--bind", "/run/lvm", "/run/lvm",
"--bind", "/run/mount", "/run/mount",
"--bind", "/run/nginx", "/run/nginx",
"--bind", "/run/nscd", "/run/nscd",
"--bind", "/run/opengl-driver", "/run/opengl-driver",
"--bind", "/run/pppd", "/run/pppd",
"--bind", "/run/resolvconf", "/run/resolvconf",
"--bind", "/run/sddm", "/run/sddm",
"--bind", "/run/syncoid", "/run/syncoid",
"--bind", "/run/systemd", "/run/systemd",
"--bind", "/run/tmpfiles.d", "/run/tmpfiles.d",
"--bind", "/run/udev", "/run/udev",
"--bind", "/run/udisks2", "/run/udisks2",
"--bind", "/run/utmp", "/run/utmp",
"--bind", "/run/virtlogd.pid", "/run/virtlogd.pid",
"--bind", "/run/wrappers", "/run/wrappers",
"--bind", "/run/zed.pid", "/run/zed.pid",
"--bind", "/run/zed.state", "/run/zed.state",
"--bind", "/tmp/fortify.1971/tmpdir/150", "/tmp",
"--size", "1048576", "--tmpfs", "/tmp/fortify.1971",
"--size", "1048576", "--tmpfs", "/run/user",
"--size", "8388608", "--tmpfs", "/run/user/150",
"--ro-bind", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/passwd", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/passwd",
"--ro-bind", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/group", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/group",
"--ro-bind", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/passwd", "/etc/passwd",
"--ro-bind", "/tmp/fortify.1971/67a97cc824a64ef789f16b20ca6ce311/group", "/etc/group",
"--size", "8192", "--tmpfs", "/var/run/nscd",
},
},
} }
for _, tc := range testCases { for _, tc := range testCases {

View File

@ -7,8 +7,8 @@ import (
"strings" "strings"
"testing" "testing"
"git.ophivana.moe/cat/fortify/helper" "git.ophivana.moe/security/fortify/helper"
"git.ophivana.moe/cat/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/helper/bwrap"
) )
func TestBwrap(t *testing.T) { func TestBwrap(t *testing.T) {

View File

@ -5,7 +5,7 @@ import (
"os" "os"
"testing" "testing"
"git.ophivana.moe/cat/fortify/helper" "git.ophivana.moe/security/fortify/helper"
) )
func TestDirect(t *testing.T) { func TestDirect(t *testing.T) {

View File

@ -6,7 +6,7 @@ import (
"testing" "testing"
"time" "time"
"git.ophivana.moe/cat/fortify/helper" "git.ophivana.moe/security/fortify/helper"
) )
var ( var (

View File

@ -10,7 +10,8 @@ import (
"syscall" "syscall"
"testing" "testing"
"git.ophivana.moe/cat/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal/fmsg"
) )
// InternalChildStub is an internal function but exported because it is cross-package; // InternalChildStub is an internal function but exported because it is cross-package;
@ -33,7 +34,7 @@ func InternalChildStub() {
genericStub(argsFD, statFD) genericStub(argsFD, statFD)
} }
os.Exit(0) fmsg.Exit(0)
} }
// InternalReplaceExecCommand is an internal function but exported because it is cross-package; // InternalReplaceExecCommand is an internal function but exported because it is cross-package;

View File

@ -3,7 +3,7 @@ package helper_test
import ( import (
"testing" "testing"
"git.ophivana.moe/cat/fortify/helper" "git.ophivana.moe/security/fortify/helper"
) )
func TestHelperChildStub(t *testing.T) { func TestHelperChildStub(t *testing.T) {

View File

@ -1,32 +1,45 @@
package app package app
import ( import (
"net"
"os/exec"
"sync" "sync"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/shim"
) )
type App interface { type App interface {
Seal(config *Config) error // ID returns a copy of App's unique ID.
ID() ID
// Start sets up the system and starts the App.
Start() error Start() error
// Wait waits for App's process to exit and reverts system setup.
Wait() (int, error) Wait() (int, error)
// WaitErr returns error returned by the underlying wait syscall.
WaitErr() error WaitErr() error
Seal(config *Config) error
String() string String() string
} }
type app struct { type app struct {
// application unique identifier
id *ID
// operating system interface
os internal.System
// shim process manager
shim *shim.Shim
// child process related information // child process related information
seal *appSeal seal *appSeal
// underlying fortified child process
cmd *exec.Cmd
// wayland connection if wayland mediation is enabled
wayland *net.UnixConn
// error returned waiting for process // error returned waiting for process
wait error waitErr error
lock sync.RWMutex lock sync.RWMutex
} }
func (a *app) ID() ID {
return *a.id
}
func (a *app) String() string { func (a *app) String() string {
if a == nil { if a == nil {
return "(invalid fortified app)" return "(invalid fortified app)"
@ -35,21 +48,24 @@ func (a *app) String() string {
a.lock.RLock() a.lock.RLock()
defer a.lock.RUnlock() defer a.lock.RUnlock()
if a.cmd != nil { if a.shim != nil {
return a.cmd.String() return a.shim.String()
} }
if a.seal != nil { if a.seal != nil {
return "(sealed fortified app as uid " + a.seal.sys.Uid + ")" return "(sealed fortified app as uid " + a.seal.sys.user.Uid + ")"
} }
return "(unsealed fortified app)" return "(unsealed fortified app)"
} }
func (a *app) WaitErr() error { func (a *app) WaitErr() error {
return a.wait return a.waitErr
} }
func New() App { func New(os internal.System) (App, error) {
return new(app) a := new(app)
a.id = new(ID)
a.os = os
return a, newAppID(a.id)
} }

View File

@ -0,0 +1,592 @@
package app_test
import (
"fmt"
"io/fs"
"os/user"
"strconv"
"git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/app"
"git.ophivana.moe/security/fortify/internal/system"
)
var testCasesNixos = []sealTestCase{
{
"nixos permissive defaults no enablements", new(stubNixOS),
&app.Config{
User: "chronos",
Command: make([]string, 0),
Method: "sudo",
},
app.ID{
0x4a, 0x45, 0x0b, 0x65,
0x96, 0xd7, 0xbc, 0x15,
0xbd, 0x01, 0x78, 0x0e,
0xb9, 0xa6, 0x07, 0xac,
},
system.New(150).
Ensure("/tmp/fortify.1971", 0701).
Ephemeral(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac", 0701).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/150", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/150", acl.Read, acl.Write, acl.Execute).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute).
WriteType(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/passwd", "chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n").
WriteType(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/group", "fortify:x:65534:\n"),
(&bwrap.Config{
Net: true,
UserNS: true,
Clearenv: true,
SetEnv: map[string]string{
"HOME": "/home/chronos",
"SHELL": "/run/current-system/sw/bin/zsh",
"TERM": "xterm-256color",
"USER": "chronos",
"XDG_RUNTIME_DIR": "/run/user/150",
"XDG_SESSION_CLASS": "user",
"XDG_SESSION_TYPE": "tty"},
Chmod: make(bwrap.ChmodConfig),
DieWithParent: true,
AsInit: true,
}).SetUID(65534).SetGID(65534).
Procfs("/proc").DevTmpfs("/dev").Mqueue("/dev/mqueue").
Tmpfs("/dev/fortify", 4096).
Bind("/bin", "/bin", false, true).
Bind("/boot", "/boot", false, true).
Bind("/etc", "/dev/fortify/etc").
Bind("/home", "/home", false, true).
Bind("/lib", "/lib", false, true).
Bind("/lib64", "/lib64", false, true).
Bind("/nix", "/nix", false, true).
Bind("/root", "/root", false, true).
Bind("/srv", "/srv", false, true).
Bind("/sys", "/sys", false, true).
Bind("/usr", "/usr", false, true).
Bind("/var", "/var", false, true).
Bind("/run/agetty.reload", "/run/agetty.reload", false, true).
Bind("/run/binfmt", "/run/binfmt", false, true).
Bind("/run/booted-system", "/run/booted-system", false, true).
Bind("/run/credentials", "/run/credentials", false, true).
Bind("/run/cryptsetup", "/run/cryptsetup", false, true).
Bind("/run/current-system", "/run/current-system", false, true).
Bind("/run/host", "/run/host", false, true).
Bind("/run/keys", "/run/keys", false, true).
Bind("/run/libvirt", "/run/libvirt", false, true).
Bind("/run/libvirtd.pid", "/run/libvirtd.pid", false, true).
Bind("/run/lock", "/run/lock", false, true).
Bind("/run/log", "/run/log", false, true).
Bind("/run/lvm", "/run/lvm", false, true).
Bind("/run/mount", "/run/mount", false, true).
Bind("/run/NetworkManager", "/run/NetworkManager", false, true).
Bind("/run/nginx", "/run/nginx", false, true).
Bind("/run/nixos", "/run/nixos", false, true).
Bind("/run/nscd", "/run/nscd", false, true).
Bind("/run/opengl-driver", "/run/opengl-driver", false, true).
Bind("/run/pppd", "/run/pppd", false, true).
Bind("/run/resolvconf", "/run/resolvconf", false, true).
Bind("/run/sddm", "/run/sddm", false, true).
Bind("/run/store", "/run/store", false, true).
Bind("/run/syncoid", "/run/syncoid", false, true).
Bind("/run/system", "/run/system", false, true).
Bind("/run/systemd", "/run/systemd", false, true).
Bind("/run/tmpfiles.d", "/run/tmpfiles.d", false, true).
Bind("/run/udev", "/run/udev", false, true).
Bind("/run/udisks2", "/run/udisks2", false, true).
Bind("/run/utmp", "/run/utmp", false, true).
Bind("/run/virtlogd.pid", "/run/virtlogd.pid", false, true).
Bind("/run/wrappers", "/run/wrappers", false, true).
Bind("/run/zed.pid", "/run/zed.pid", false, true).
Bind("/run/zed.state", "/run/zed.state", false, true).
Symlink("/dev/fortify/etc/alsa", "/etc/alsa").
Symlink("/dev/fortify/etc/bashrc", "/etc/bashrc").
Symlink("/dev/fortify/etc/binfmt.d", "/etc/binfmt.d").
Symlink("/dev/fortify/etc/dbus-1", "/etc/dbus-1").
Symlink("/dev/fortify/etc/default", "/etc/default").
Symlink("/dev/fortify/etc/ethertypes", "/etc/ethertypes").
Symlink("/dev/fortify/etc/fonts", "/etc/fonts").
Symlink("/dev/fortify/etc/fstab", "/etc/fstab").
Symlink("/dev/fortify/etc/fuse.conf", "/etc/fuse.conf").
Symlink("/dev/fortify/etc/host.conf", "/etc/host.conf").
Symlink("/dev/fortify/etc/hostid", "/etc/hostid").
Symlink("/dev/fortify/etc/hostname", "/etc/hostname").
Symlink("/dev/fortify/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink("/dev/fortify/etc/hosts", "/etc/hosts").
Symlink("/dev/fortify/etc/inputrc", "/etc/inputrc").
Symlink("/dev/fortify/etc/ipsec.d", "/etc/ipsec.d").
Symlink("/dev/fortify/etc/issue", "/etc/issue").
Symlink("/dev/fortify/etc/kbd", "/etc/kbd").
Symlink("/dev/fortify/etc/libblockdev", "/etc/libblockdev").
Symlink("/dev/fortify/etc/locale.conf", "/etc/locale.conf").
Symlink("/dev/fortify/etc/localtime", "/etc/localtime").
Symlink("/dev/fortify/etc/login.defs", "/etc/login.defs").
Symlink("/dev/fortify/etc/lsb-release", "/etc/lsb-release").
Symlink("/dev/fortify/etc/lvm", "/etc/lvm").
Symlink("/dev/fortify/etc/machine-id", "/etc/machine-id").
Symlink("/dev/fortify/etc/man_db.conf", "/etc/man_db.conf").
Symlink("/dev/fortify/etc/modprobe.d", "/etc/modprobe.d").
Symlink("/dev/fortify/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab").
Symlink("/dev/fortify/etc/nanorc", "/etc/nanorc").
Symlink("/dev/fortify/etc/netgroup", "/etc/netgroup").
Symlink("/dev/fortify/etc/NetworkManager", "/etc/NetworkManager").
Symlink("/dev/fortify/etc/nix", "/etc/nix").
Symlink("/dev/fortify/etc/nixos", "/etc/nixos").
Symlink("/dev/fortify/etc/NIXOS", "/etc/NIXOS").
Symlink("/dev/fortify/etc/nscd.conf", "/etc/nscd.conf").
Symlink("/dev/fortify/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink("/dev/fortify/etc/opensnitchd", "/etc/opensnitchd").
Symlink("/dev/fortify/etc/os-release", "/etc/os-release").
Symlink("/dev/fortify/etc/pam", "/etc/pam").
Symlink("/dev/fortify/etc/pam.d", "/etc/pam.d").
Symlink("/dev/fortify/etc/pipewire", "/etc/pipewire").
Symlink("/dev/fortify/etc/pki", "/etc/pki").
Symlink("/dev/fortify/etc/polkit-1", "/etc/polkit-1").
Symlink("/dev/fortify/etc/profile", "/etc/profile").
Symlink("/dev/fortify/etc/protocols", "/etc/protocols").
Symlink("/dev/fortify/etc/qemu", "/etc/qemu").
Symlink("/dev/fortify/etc/resolv.conf", "/etc/resolv.conf").
Symlink("/dev/fortify/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink("/dev/fortify/etc/rpc", "/etc/rpc").
Symlink("/dev/fortify/etc/samba", "/etc/samba").
Symlink("/dev/fortify/etc/sddm.conf", "/etc/sddm.conf").
Symlink("/dev/fortify/etc/secureboot", "/etc/secureboot").
Symlink("/dev/fortify/etc/services", "/etc/services").
Symlink("/dev/fortify/etc/set-environment", "/etc/set-environment").
Symlink("/dev/fortify/etc/shadow", "/etc/shadow").
Symlink("/dev/fortify/etc/shells", "/etc/shells").
Symlink("/dev/fortify/etc/ssh", "/etc/ssh").
Symlink("/dev/fortify/etc/ssl", "/etc/ssl").
Symlink("/dev/fortify/etc/static", "/etc/static").
Symlink("/dev/fortify/etc/subgid", "/etc/subgid").
Symlink("/dev/fortify/etc/subuid", "/etc/subuid").
Symlink("/dev/fortify/etc/sudoers", "/etc/sudoers").
Symlink("/dev/fortify/etc/sysctl.d", "/etc/sysctl.d").
Symlink("/dev/fortify/etc/systemd", "/etc/systemd").
Symlink("/dev/fortify/etc/terminfo", "/etc/terminfo").
Symlink("/dev/fortify/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink("/dev/fortify/etc/udev", "/etc/udev").
Symlink("/dev/fortify/etc/udisks2", "/etc/udisks2").
Symlink("/dev/fortify/etc/UPower", "/etc/UPower").
Symlink("/dev/fortify/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink("/dev/fortify/etc/X11", "/etc/X11").
Symlink("/dev/fortify/etc/zfs", "/etc/zfs").
Symlink("/dev/fortify/etc/zinputrc", "/etc/zinputrc").
Symlink("/dev/fortify/etc/zoneinfo", "/etc/zoneinfo").
Symlink("/dev/fortify/etc/zprofile", "/etc/zprofile").
Symlink("/dev/fortify/etc/zshenv", "/etc/zshenv").
Symlink("/dev/fortify/etc/zshrc", "/etc/zshrc").
Bind("/tmp/fortify.1971/tmpdir/150", "/tmp", false, true).
Tmpfs("/tmp/fortify.1971", 1048576).
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/150", 8388608).
Bind("/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/passwd", "/etc/passwd").
Bind("/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/group", "/etc/group").
Tmpfs("/var/run/nscd", 8192),
},
{
"nixos permissive defaults chromium", new(stubNixOS),
&app.Config{
ID: "org.chromium.Chromium",
User: "chronos",
Command: []string{"/run/current-system/sw/bin/zsh", "-c", "exec chromium "},
Confinement: app.ConfinementConfig{
SessionBus: &dbus.Config{
Talk: []string{
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{
"org.freedesktop.portal.*": "*",
},
Broadcast: map[string]string{
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
},
Filter: true,
},
SystemBus: &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
},
Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(),
},
Method: "systemd",
},
app.ID{
0xeb, 0xf0, 0x83, 0xd1,
0xb1, 0x75, 0x91, 0x17,
0x82, 0xd4, 0x13, 0x36,
0x9b, 0x64, 0xce, 0x7c,
},
system.New(150).
Ensure("/tmp/fortify.1971", 0701).
Ephemeral(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0701).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/150", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/150", acl.Read, acl.Write, acl.Execute).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", acl.Execute).
WriteType(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/passwd", "chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n").
WriteType(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/group", "fortify:x:65534:\n").
Link("/run/user/1971/wayland-0", "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/wayland").
UpdatePermType(system.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute).
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse").
CopyFile("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie").
MustProxyDBus("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", &dbus.Config{
Talk: []string{
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{
"org.freedesktop.portal.*": "*",
},
Broadcast: map[string]string{
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
},
Filter: true,
}, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
}).
UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
(&bwrap.Config{
Net: true,
UserNS: true,
Clearenv: true,
SetEnv: map[string]string{
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/150/bus",
"DBUS_SYSTEM_BUS_ADDRESS": "unix:path=/run/dbus/system_bus_socket",
"HOME": "/home/chronos",
"PULSE_COOKIE": "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie",
"PULSE_SERVER": "unix:/run/user/150/pulse/native",
"SHELL": "/run/current-system/sw/bin/zsh",
"TERM": "xterm-256color",
"USER": "chronos",
"WAYLAND_DISPLAY": "/run/user/150/wayland-0",
"XDG_RUNTIME_DIR": "/run/user/150",
"XDG_SESSION_CLASS": "user",
"XDG_SESSION_TYPE": "tty",
},
Chmod: make(bwrap.ChmodConfig),
DieWithParent: true,
AsInit: true,
}).SetUID(65534).SetGID(65534).
Procfs("/proc").DevTmpfs("/dev").Mqueue("/dev/mqueue").
Tmpfs("/dev/fortify", 4096).
Bind("/bin", "/bin", false, true).
Bind("/boot", "/boot", false, true).
Bind("/etc", "/dev/fortify/etc").
Bind("/home", "/home", false, true).
Bind("/lib", "/lib", false, true).
Bind("/lib64", "/lib64", false, true).
Bind("/nix", "/nix", false, true).
Bind("/root", "/root", false, true).
Bind("/srv", "/srv", false, true).
Bind("/sys", "/sys", false, true).
Bind("/usr", "/usr", false, true).
Bind("/var", "/var", false, true).
Bind("/run/agetty.reload", "/run/agetty.reload", false, true).
Bind("/run/binfmt", "/run/binfmt", false, true).
Bind("/run/booted-system", "/run/booted-system", false, true).
Bind("/run/credentials", "/run/credentials", false, true).
Bind("/run/cryptsetup", "/run/cryptsetup", false, true).
Bind("/run/current-system", "/run/current-system", false, true).
Bind("/run/host", "/run/host", false, true).
Bind("/run/keys", "/run/keys", false, true).
Bind("/run/libvirt", "/run/libvirt", false, true).
Bind("/run/libvirtd.pid", "/run/libvirtd.pid", false, true).
Bind("/run/lock", "/run/lock", false, true).
Bind("/run/log", "/run/log", false, true).
Bind("/run/lvm", "/run/lvm", false, true).
Bind("/run/mount", "/run/mount", false, true).
Bind("/run/NetworkManager", "/run/NetworkManager", false, true).
Bind("/run/nginx", "/run/nginx", false, true).
Bind("/run/nixos", "/run/nixos", false, true).
Bind("/run/nscd", "/run/nscd", false, true).
Bind("/run/opengl-driver", "/run/opengl-driver", false, true).
Bind("/run/pppd", "/run/pppd", false, true).
Bind("/run/resolvconf", "/run/resolvconf", false, true).
Bind("/run/sddm", "/run/sddm", false, true).
Bind("/run/store", "/run/store", false, true).
Bind("/run/syncoid", "/run/syncoid", false, true).
Bind("/run/system", "/run/system", false, true).
Bind("/run/systemd", "/run/systemd", false, true).
Bind("/run/tmpfiles.d", "/run/tmpfiles.d", false, true).
Bind("/run/udev", "/run/udev", false, true).
Bind("/run/udisks2", "/run/udisks2", false, true).
Bind("/run/utmp", "/run/utmp", false, true).
Bind("/run/virtlogd.pid", "/run/virtlogd.pid", false, true).
Bind("/run/wrappers", "/run/wrappers", false, true).
Bind("/run/zed.pid", "/run/zed.pid", false, true).
Bind("/run/zed.state", "/run/zed.state", false, true).
Bind("/dev/dri", "/dev/dri", true, true, true).
Symlink("/dev/fortify/etc/alsa", "/etc/alsa").
Symlink("/dev/fortify/etc/bashrc", "/etc/bashrc").
Symlink("/dev/fortify/etc/binfmt.d", "/etc/binfmt.d").
Symlink("/dev/fortify/etc/dbus-1", "/etc/dbus-1").
Symlink("/dev/fortify/etc/default", "/etc/default").
Symlink("/dev/fortify/etc/ethertypes", "/etc/ethertypes").
Symlink("/dev/fortify/etc/fonts", "/etc/fonts").
Symlink("/dev/fortify/etc/fstab", "/etc/fstab").
Symlink("/dev/fortify/etc/fuse.conf", "/etc/fuse.conf").
Symlink("/dev/fortify/etc/host.conf", "/etc/host.conf").
Symlink("/dev/fortify/etc/hostid", "/etc/hostid").
Symlink("/dev/fortify/etc/hostname", "/etc/hostname").
Symlink("/dev/fortify/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink("/dev/fortify/etc/hosts", "/etc/hosts").
Symlink("/dev/fortify/etc/inputrc", "/etc/inputrc").
Symlink("/dev/fortify/etc/ipsec.d", "/etc/ipsec.d").
Symlink("/dev/fortify/etc/issue", "/etc/issue").
Symlink("/dev/fortify/etc/kbd", "/etc/kbd").
Symlink("/dev/fortify/etc/libblockdev", "/etc/libblockdev").
Symlink("/dev/fortify/etc/locale.conf", "/etc/locale.conf").
Symlink("/dev/fortify/etc/localtime", "/etc/localtime").
Symlink("/dev/fortify/etc/login.defs", "/etc/login.defs").
Symlink("/dev/fortify/etc/lsb-release", "/etc/lsb-release").
Symlink("/dev/fortify/etc/lvm", "/etc/lvm").
Symlink("/dev/fortify/etc/machine-id", "/etc/machine-id").
Symlink("/dev/fortify/etc/man_db.conf", "/etc/man_db.conf").
Symlink("/dev/fortify/etc/modprobe.d", "/etc/modprobe.d").
Symlink("/dev/fortify/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab").
Symlink("/dev/fortify/etc/nanorc", "/etc/nanorc").
Symlink("/dev/fortify/etc/netgroup", "/etc/netgroup").
Symlink("/dev/fortify/etc/NetworkManager", "/etc/NetworkManager").
Symlink("/dev/fortify/etc/nix", "/etc/nix").
Symlink("/dev/fortify/etc/nixos", "/etc/nixos").
Symlink("/dev/fortify/etc/NIXOS", "/etc/NIXOS").
Symlink("/dev/fortify/etc/nscd.conf", "/etc/nscd.conf").
Symlink("/dev/fortify/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink("/dev/fortify/etc/opensnitchd", "/etc/opensnitchd").
Symlink("/dev/fortify/etc/os-release", "/etc/os-release").
Symlink("/dev/fortify/etc/pam", "/etc/pam").
Symlink("/dev/fortify/etc/pam.d", "/etc/pam.d").
Symlink("/dev/fortify/etc/pipewire", "/etc/pipewire").
Symlink("/dev/fortify/etc/pki", "/etc/pki").
Symlink("/dev/fortify/etc/polkit-1", "/etc/polkit-1").
Symlink("/dev/fortify/etc/profile", "/etc/profile").
Symlink("/dev/fortify/etc/protocols", "/etc/protocols").
Symlink("/dev/fortify/etc/qemu", "/etc/qemu").
Symlink("/dev/fortify/etc/resolv.conf", "/etc/resolv.conf").
Symlink("/dev/fortify/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink("/dev/fortify/etc/rpc", "/etc/rpc").
Symlink("/dev/fortify/etc/samba", "/etc/samba").
Symlink("/dev/fortify/etc/sddm.conf", "/etc/sddm.conf").
Symlink("/dev/fortify/etc/secureboot", "/etc/secureboot").
Symlink("/dev/fortify/etc/services", "/etc/services").
Symlink("/dev/fortify/etc/set-environment", "/etc/set-environment").
Symlink("/dev/fortify/etc/shadow", "/etc/shadow").
Symlink("/dev/fortify/etc/shells", "/etc/shells").
Symlink("/dev/fortify/etc/ssh", "/etc/ssh").
Symlink("/dev/fortify/etc/ssl", "/etc/ssl").
Symlink("/dev/fortify/etc/static", "/etc/static").
Symlink("/dev/fortify/etc/subgid", "/etc/subgid").
Symlink("/dev/fortify/etc/subuid", "/etc/subuid").
Symlink("/dev/fortify/etc/sudoers", "/etc/sudoers").
Symlink("/dev/fortify/etc/sysctl.d", "/etc/sysctl.d").
Symlink("/dev/fortify/etc/systemd", "/etc/systemd").
Symlink("/dev/fortify/etc/terminfo", "/etc/terminfo").
Symlink("/dev/fortify/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink("/dev/fortify/etc/udev", "/etc/udev").
Symlink("/dev/fortify/etc/udisks2", "/etc/udisks2").
Symlink("/dev/fortify/etc/UPower", "/etc/UPower").
Symlink("/dev/fortify/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink("/dev/fortify/etc/X11", "/etc/X11").
Symlink("/dev/fortify/etc/zfs", "/etc/zfs").
Symlink("/dev/fortify/etc/zinputrc", "/etc/zinputrc").
Symlink("/dev/fortify/etc/zoneinfo", "/etc/zoneinfo").
Symlink("/dev/fortify/etc/zprofile", "/etc/zprofile").
Symlink("/dev/fortify/etc/zshenv", "/etc/zshenv").
Symlink("/dev/fortify/etc/zshrc", "/etc/zshrc").
Bind("/tmp/fortify.1971/tmpdir/150", "/tmp", false, true).
Tmpfs("/tmp/fortify.1971", 1048576).
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/150", 8388608).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/passwd", "/etc/passwd").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/group", "/etc/group").
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/150/wayland-0").
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/150/pulse/native").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie", "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/150/bus").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket").
Tmpfs("/var/run/nscd", 8192),
},
}
// fs methods are not implemented using a real FS
// to help better understand filesystem access behaviour
type stubNixOS struct {
lookPathErr map[string]error
usernameErr map[string]error
}
func (s *stubNixOS) Geteuid() int {
return 1971
}
func (s *stubNixOS) LookupEnv(key string) (string, bool) {
switch key {
case "SHELL":
return "/run/current-system/sw/bin/zsh", true
case "TERM":
return "xterm-256color", true
case "WAYLAND_DISPLAY":
return "wayland-0", true
case "PULSE_COOKIE":
return "", false
case "HOME":
return "/home/ophestra", true
case "XDG_CONFIG_HOME":
return "/home/ophestra/xdg/config", true
default:
panic(fmt.Sprintf("attempted to access unexpected environment variable %q", key))
}
}
func (s *stubNixOS) TempDir() string {
return "/tmp"
}
func (s *stubNixOS) LookPath(file string) (string, error) {
if s.lookPathErr != nil {
if err, ok := s.lookPathErr[file]; ok {
return "", err
}
}
switch file {
case "sudo":
return "/run/wrappers/bin/sudo", nil
case "machinectl":
return "/home/ophestra/.nix-profile/bin/machinectl", nil
default:
panic(fmt.Sprintf("attempted to look up unexpected executable %q", file))
}
}
func (s *stubNixOS) Executable() (string, error) {
return "/home/ophestra/.nix-profile/bin/fortify", nil
}
func (s *stubNixOS) Lookup(username string) (*user.User, error) {
if s.usernameErr != nil {
if err, ok := s.usernameErr[username]; ok {
return nil, err
}
}
switch username {
case "chronos":
return &user.User{
Uid: "150",
Gid: "101",
Username: "chronos",
HomeDir: "/home/chronos",
}, nil
default:
return nil, user.UnknownUserError(username)
}
}
func (s *stubNixOS) ReadDir(name string) ([]fs.DirEntry, error) {
switch name {
case "/":
return stubDirEntries("bin", "boot", "dev", "etc", "home", "lib",
"lib64", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var")
case "/run":
return stubDirEntries("agetty.reload", "binfmt", "booted-system",
"credentials", "cryptsetup", "current-system", "dbus", "host", "keys",
"libvirt", "libvirtd.pid", "lock", "log", "lvm", "mount", "NetworkManager",
"nginx", "nixos", "nscd", "opengl-driver", "pppd", "resolvconf", "sddm",
"store", "syncoid", "system", "systemd", "tmpfiles.d", "udev", "udisks2",
"user", "utmp", "virtlogd.pid", "wrappers", "zed.pid", "zed.state")
case "/etc":
return stubDirEntries("alsa", "bashrc", "binfmt.d", "dbus-1", "default",
"ethertypes", "fonts", "fstab", "fuse.conf", "group", "host.conf", "hostid",
"hostname", "hostname.CHECKSUM", "hosts", "inputrc", "ipsec.d", "issue", "kbd",
"libblockdev", "locale.conf", "localtime", "login.defs", "lsb-release", "lvm",
"machine-id", "man_db.conf", "modprobe.d", "modules-load.d", "mtab", "nanorc",
"netgroup", "NetworkManager", "nix", "nixos", "NIXOS", "nscd.conf", "nsswitch.conf",
"opensnitchd", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1",
"profile", "protocols", "qemu", "resolv.conf", "resolvconf.conf", "rpc", "samba",
"sddm.conf", "secureboot", "services", "set-environment", "shadow", "shells", "ssh",
"ssl", "static", "subgid", "subuid", "sudoers", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "udisks2", "UPower", "vconsole.conf", "X11", "zfs", "zinputrc",
"zoneinfo", "zprofile", "zshenv", "zshrc")
default:
panic(fmt.Sprintf("attempted to read unexpected directory %q", name))
}
}
func (s *stubNixOS) Stat(name string) (fs.FileInfo, error) {
switch name {
case "/var/run/nscd":
return nil, nil
case "/run/user/1971/pulse":
return nil, nil
case "/run/user/1971/pulse/native":
return stubFileInfoMode(0666), nil
case "/home/ophestra/.pulse-cookie":
return stubFileInfoIsDir(true), nil
case "/home/ophestra/xdg/config/pulse/cookie":
return stubFileInfoIsDir(false), nil
default:
panic(fmt.Sprintf("attempted to stat unexpected path %q", name))
}
}
func (s *stubNixOS) Open(name string) (fs.File, error) {
switch name {
default:
panic(fmt.Sprintf("attempted to open unexpected file %q", name))
}
}
func (s *stubNixOS) Exit(code int) {
panic("called exit on stub with code " + strconv.Itoa(code))
}
func (s *stubNixOS) Paths() internal.Paths {
return internal.Paths{
SharePath: "/tmp/fortify.1971",
RuntimePath: "/run/user/1971",
RunDirPath: "/run/user/1971/fortify",
}
}
func (s *stubNixOS) SdBooted() bool {
return true
}

134
internal/app/app_test.go Normal file
View File

@ -0,0 +1,134 @@
package app_test
import (
"io/fs"
"reflect"
"testing"
"time"
"git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/app"
"git.ophivana.moe/security/fortify/internal/system"
)
type sealTestCase struct {
name string
os internal.System
config *app.Config
id app.ID
wantSys *system.I
wantBwrap *bwrap.Config
}
func TestApp(t *testing.T) {
testCases := append(testCasesNixos)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
a := app.NewWithID(tc.id, tc.os)
if !t.Run("seal", func(t *testing.T) {
if err := a.Seal(tc.config); err != nil {
t.Errorf("Seal: error = %v", err)
}
}) {
return
}
gotSys, gotBwrap := app.AppSystemBwrap(a)
t.Run("compare sys", func(t *testing.T) {
if !gotSys.Equal(tc.wantSys) {
t.Errorf("Seal: sys = %#v, want %#v",
gotSys, tc.wantSys)
}
})
t.Run("compare bwrap", func(t *testing.T) {
if !reflect.DeepEqual(gotBwrap, tc.wantBwrap) {
t.Errorf("seal: bwrap = %#v, want %#v",
gotBwrap, tc.wantBwrap)
}
})
})
}
}
func stubDirEntries(names ...string) (e []fs.DirEntry, err error) {
e = make([]fs.DirEntry, len(names))
for i, name := range names {
e[i] = stubDirEntryPath(name)
}
return
}
type stubDirEntryPath string
func (p stubDirEntryPath) Name() string {
return string(p)
}
func (p stubDirEntryPath) IsDir() bool {
panic("attempted to call IsDir")
}
func (p stubDirEntryPath) Type() fs.FileMode {
panic("attempted to call Type")
}
func (p stubDirEntryPath) Info() (fs.FileInfo, error) {
panic("attempted to call Info")
}
type stubFileInfoMode fs.FileMode
func (s stubFileInfoMode) Name() string {
panic("attempted to call Name")
}
func (s stubFileInfoMode) Size() int64 {
panic("attempted to call Size")
}
func (s stubFileInfoMode) Mode() fs.FileMode {
return fs.FileMode(s)
}
func (s stubFileInfoMode) ModTime() time.Time {
panic("attempted to call ModTime")
}
func (s stubFileInfoMode) IsDir() bool {
panic("attempted to call IsDir")
}
func (s stubFileInfoMode) Sys() any {
panic("attempted to call Sys")
}
type stubFileInfoIsDir bool
func (s stubFileInfoIsDir) Name() string {
panic("attempted to call Name")
}
func (s stubFileInfoIsDir) Size() int64 {
panic("attempted to call Size")
}
func (s stubFileInfoIsDir) Mode() fs.FileMode {
panic("attempted to call Mode")
}
func (s stubFileInfoIsDir) ModTime() time.Time {
panic("attempted to call ModTime")
}
func (s stubFileInfoIsDir) IsDir() bool {
return bool(s)
}
func (s stubFileInfoIsDir) Sys() any {
panic("attempted to call Sys")
}

View File

@ -1,9 +1,11 @@
package app package app
import ( import (
"git.ophivana.moe/cat/fortify/dbus" "os"
"git.ophivana.moe/cat/fortify/helper/bwrap"
"git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal/system"
) )
// Config is used to seal an *App // Config is used to seal an *App
@ -34,7 +36,7 @@ type ConfinementConfig struct {
SessionBus *dbus.Config `json:"session_bus,omitempty"` SessionBus *dbus.Config `json:"session_bus,omitempty"`
// child capability enablements // child capability enablements
Enablements state.Enablements `json:"enablements"` Enablements system.Enablements `json:"enablements"`
} }
// SandboxConfig describes resources made available to the sandbox. // SandboxConfig describes resources made available to the sandbox.
@ -54,8 +56,10 @@ type SandboxConfig struct {
Env map[string]string `json:"env"` Env map[string]string `json:"env"`
// sandbox host filesystem access // sandbox host filesystem access
Filesystem []*FilesystemConfig `json:"filesystem"` Filesystem []*FilesystemConfig `json:"filesystem"`
// tmpfs mount points to mount last // symlinks created inside the sandbox
Tmpfs []bwrap.TmpfsConfig `json:"tmpfs"` Link [][2]string `json:"symlink"`
// paths to override by mounting tmpfs over them
Override []string `json:"override"`
} }
type FilesystemConfig struct { type FilesystemConfig struct {
@ -71,61 +75,44 @@ type FilesystemConfig struct {
Must bool `json:"require,omitempty"` Must bool `json:"require,omitempty"`
} }
// Bwrap returns the address of the corresponding bwrap.Config to s.
// Note that remaining tmpfs entries must be queued by the caller prior to launch.
func (s *SandboxConfig) Bwrap() *bwrap.Config { func (s *SandboxConfig) Bwrap() *bwrap.Config {
if s == nil { if s == nil {
return nil return nil
} }
nobody := 65534 conf := (&bwrap.Config{
conf := &bwrap.Config{
Net: s.Net, Net: s.Net,
UserNS: s.UserNS, UserNS: s.UserNS,
UID: &nobody,
GID: &nobody,
Hostname: s.Hostname, Hostname: s.Hostname,
Clearenv: true, Clearenv: true,
SetEnv: s.Env, SetEnv: s.Env,
Procfs: []string{"/proc"},
DevTmpfs: []string{"/dev"},
Mqueue: []string{"/dev/mqueue"},
NewSession: !s.NoNewSession, NewSession: !s.NoNewSession,
DieWithParent: true, DieWithParent: true,
AsInit: true, AsInit: true,
}
// initialise map
Chmod: make(map[string]os.FileMode),
}).
SetUID(65534).SetGID(65534).
Procfs("/proc").DevTmpfs("/dev").Mqueue("/dev/mqueue").
Tmpfs("/dev/fortify", 4*1024)
for _, c := range s.Filesystem { for _, c := range s.Filesystem {
if c == nil { if c == nil {
continue continue
} }
p := [2]string{c.Src, c.Dst} src := c.Src
dest := c.Dst
if c.Dst == "" { if c.Dst == "" {
p[1] = c.Src dest = c.Src
}
conf.Bind(src, dest, !c.Must, c.Write, c.Device)
} }
switch { for _, l := range s.Link {
case c.Device: conf.Symlink(l[0], l[1])
if c.Must {
conf.DevBind = append(conf.DevBind, p)
} else {
conf.DevBindTry = append(conf.DevBindTry, p)
}
case c.Write:
if c.Must {
conf.Bind = append(conf.Bind, p)
} else {
conf.BindTry = append(conf.BindTry, p)
}
default:
if c.Must {
conf.ROBind = append(conf.ROBind, p)
} else {
conf.ROBindTry = append(conf.ROBindTry, p)
}
}
}
for _, tmpfs := range s.Tmpfs {
conf.Tmpfs = append(conf.Tmpfs, bwrap.PermConfig[bwrap.TmpfsConfig]{Path: tmpfs, Last: true})
} }
return conf return conf
@ -164,9 +151,8 @@ func Template() *Config {
{Src: "/data/user/0", Dst: "/data/data", Write: true, Must: true}, {Src: "/data/user/0", Dst: "/data/data", Write: true, Must: true},
{Src: "/var/tmp", Write: true}, {Src: "/var/tmp", Write: true},
}, },
Tmpfs: []bwrap.TmpfsConfig{ Link: [][2]string{{"/dev/fortify/etc", "/etc"}},
{Size: 8 * 1024, Dir: "/var/run/nscd"}, Override: []string{"/var/run/nscd"},
},
}, },
SystemBus: &dbus.Config{ SystemBus: &dbus.Config{
See: nil, See: nil,
@ -188,7 +174,7 @@ func Template() *Config {
Log: false, Log: false,
Filter: true, Filter: true,
}, },
Enablements: state.EnableWayland.Mask() | state.EnableDBus.Mask() | state.EnablePulse.Mask(), Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(),
}, },
} }
} }

View File

@ -1,33 +0,0 @@
package app
import (
"io"
"os"
)
func copyFile(dst, src string) error {
srcD, err := os.Open(src)
if err != nil {
return err
}
defer func() {
if srcD.Close() != nil {
// unreachable
panic("src file closed prematurely")
}
}()
dstD, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer func() {
if dstD.Close() != nil {
// unreachable
panic("dst file closed prematurely")
}
}()
_, err = io.Copy(dstD, srcD)
return err
}

View File

@ -0,0 +1,19 @@
package app
import (
"git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/system"
)
func NewWithID(id ID, os internal.System) App {
a := new(app)
a.id = &id
a.os = os
return a
}
func AppSystemBwrap(a App) (*system.I, *bwrap.Config) {
v := a.(*app)
return v.seal.sys.I, v.seal.sys.bwrap
}

View File

@ -5,14 +5,13 @@ import (
"encoding/hex" "encoding/hex"
) )
type appID [16]byte type ID [16]byte
func (a *appID) String() string { func (a *ID) String() string {
return hex.EncodeToString(a[:]) return hex.EncodeToString(a[:])
} }
func newAppID() (*appID, error) { func newAppID(id *ID) error {
a := &appID{} _, err := rand.Read(id[:])
_, err := rand.Read(a[:]) return err
return a, err
} }

View File

@ -1,21 +1,19 @@
package app package app
import ( import (
"os/exec"
"strings" "strings"
"git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/cat/fortify/internal/verbose"
) )
func (a *app) commandBuilderMachineCtl(shimEnv string) (args []string) { func (a *app) commandBuilderMachineCtl(shimEnv string) (args []string) {
args = make([]string, 0, 9+len(a.seal.sys.bwrap.SetEnv)) args = make([]string, 0, 9+len(a.seal.sys.bwrap.SetEnv))
// shell --uid=$USER // shell --uid=$USER
args = append(args, "shell", "--uid="+a.seal.sys.Username) args = append(args, "shell", "--uid="+a.seal.sys.user.Username)
// --quiet // --quiet
if !verbose.Get() { if !fmsg.Verbose() {
args = append(args, "--quiet") args = append(args, "--quiet")
} }
@ -32,7 +30,7 @@ func (a *app) commandBuilderMachineCtl(shimEnv string) (args []string) {
args = append(args, "--", ".host") args = append(args, "--", ".host")
// /bin/sh -c // /bin/sh -c
if sh, err := exec.LookPath("sh"); err != nil { if sh, err := a.os.LookPath("sh"); err != nil {
// hardcode /bin/sh path since it exists more often than not // hardcode /bin/sh path since it exists more often than not
args = append(args, "/bin/sh", "-c") args = append(args, "/bin/sh", "-c")
} else { } else {
@ -49,14 +47,6 @@ func (a *app) commandBuilderMachineCtl(shimEnv string) (args []string) {
} }
innerCommand.WriteString("; ") innerCommand.WriteString("; ")
// override message bus address if enabled
if a.seal.et.Has(state.EnableDBus) {
innerCommand.WriteString(dbusSessionBusAddress + "=" + "'" + "unix:path=" + a.seal.sys.dbusAddr[0][1] + "' ")
if a.seal.sys.dbusSystem {
innerCommand.WriteString(dbusSystemBusAddress + "=" + "'" + "unix:path=" + a.seal.sys.dbusAddr[1][1] + "' ")
}
}
// launch fortify as shim // launch fortify as shim
innerCommand.WriteString("exec " + a.seal.sys.executable + " shim") innerCommand.WriteString("exec " + a.seal.sys.executable + " shim")

View File

@ -1,9 +1,7 @@
package app package app
import ( import (
"os" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/cat/fortify/internal/verbose"
) )
const ( const (
@ -14,11 +12,11 @@ func (a *app) commandBuilderSudo(shimEnv string) (args []string) {
args = make([]string, 0, 8) args = make([]string, 0, 8)
// -Hiu $USER // -Hiu $USER
args = append(args, "-Hiu", a.seal.sys.Username) args = append(args, "-Hiu", a.seal.sys.user.Username)
// -A? // -A?
if _, ok := os.LookupEnv(sudoAskPass); ok { if _, ok := a.os.LookupEnv(sudoAskPass); ok {
verbose.Printf("%s set, adding askpass flag\n", sudoAskPass) fmsg.VPrintln(sudoAskPass, "set, adding askpass flag")
args = append(args, "-A") args = append(args, "-A")
} }

View File

@ -2,17 +2,17 @@ package app
import ( import (
"errors" "errors"
"os" "io/fs"
"os/exec"
"os/user" "os/user"
"path" "path"
"strconv" "strconv"
"git.ophivana.moe/cat/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/cat/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/cat/fortify/internal" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/security/fortify/internal/shim"
"git.ophivana.moe/cat/fortify/internal/verbose" "git.ophivana.moe/security/fortify/internal/state"
"git.ophivana.moe/security/fortify/internal/system"
) )
const ( const (
@ -20,6 +20,11 @@ const (
LaunchMethodMachineCtl LaunchMethodMachineCtl
) )
var method = [...]string{
LaunchMethodSudo: "sudo",
LaunchMethodMachineCtl: "systemd",
}
var ( var (
ErrConfig = errors.New("no configuration to seal") ErrConfig = errors.New("no configuration to seal")
ErrUser = errors.New("unknown user") ErrUser = errors.New("unknown user")
@ -30,11 +35,41 @@ var (
ErrMachineCtl = errors.New("machinectl not available") ErrMachineCtl = errors.New("machinectl not available")
) )
type ( // appSeal seals the application with child-related information
SealConfigError BaseError type appSeal struct {
LauncherLookupError BaseError // app unique ID string representation
SecurityError BaseError id string
) // wayland mediation, disabled if nil
wl *shim.Wayland
// freedesktop application ID
fid string
// argv to start process with in the final confined environment
command []string
// persistent process state store
store state.Store
// uint8 representation of launch method sealed from config
launchOption uint8
// process-specific share directory path
share string
// process-specific share directory path local to XDG_RUNTIME_DIR
shareLocal string
// path to launcher program
toolPath string
// pass-through enablement tracking from config
et system.Enablements
// prevents sharing from happening twice
shared bool
// seal system-level component
sys *appSealSys
internal.Paths
// protected by upstream mutex
}
// Seal seals the app launch context // Seal seals the app launch context
func (a *app) Seal(config *Config) error { func (a *app) Seal(config *Config) error {
@ -46,77 +81,75 @@ func (a *app) Seal(config *Config) error {
} }
if config == nil { if config == nil {
return (*SealConfigError)(wrapError(ErrConfig, "attempted to seal app with nil config")) return fmsg.WrapError(ErrConfig,
"attempted to seal app with nil config")
} }
// create seal // create seal
seal := new(appSeal) seal := new(appSeal)
// generate application ID
if id, err := newAppID(); err != nil {
return (*SecurityError)(wrapError(err, "cannot generate application ID:", err))
} else {
seal.id = id
}
// fetch system constants // fetch system constants
seal.SystemConstants = internal.GetSC() seal.Paths = a.os.Paths()
// pass through config values // pass through config values
seal.id = a.id.String()
seal.fid = config.ID seal.fid = config.ID
seal.command = config.Command seal.command = config.Command
// parses launch method text and looks up tool path // parses launch method text and looks up tool path
switch config.Method { switch config.Method {
case "sudo": case method[LaunchMethodSudo]:
seal.launchOption = LaunchMethodSudo seal.launchOption = LaunchMethodSudo
if sudoPath, err := exec.LookPath("sudo"); err != nil { if sudoPath, err := a.os.LookPath("sudo"); err != nil {
return (*LauncherLookupError)(wrapError(ErrSudo, "sudo not found")) return fmsg.WrapError(ErrSudo,
"sudo not found")
} else { } else {
seal.toolPath = sudoPath seal.toolPath = sudoPath
} }
case "systemd": case method[LaunchMethodMachineCtl]:
seal.launchOption = LaunchMethodMachineCtl seal.launchOption = LaunchMethodMachineCtl
if !internal.SdBootedV { if !a.os.SdBooted() {
return (*LauncherLookupError)(wrapError(ErrSystemd, return fmsg.WrapError(ErrSystemd,
"system has not been booted with systemd as init system")) "system has not been booted with systemd as init system")
} }
if machineCtlPath, err := exec.LookPath("machinectl"); err != nil { if machineCtlPath, err := a.os.LookPath("machinectl"); err != nil {
return (*LauncherLookupError)(wrapError(ErrMachineCtl, "machinectl not found")) return fmsg.WrapError(ErrMachineCtl,
"machinectl not found")
} else { } else {
seal.toolPath = machineCtlPath seal.toolPath = machineCtlPath
} }
default: default:
return (*SealConfigError)(wrapError(ErrLaunch, "invalid launch method")) return fmsg.WrapError(ErrLaunch,
"invalid launch method")
} }
// create seal system component // create seal system component
seal.sys = new(appSealTx) seal.sys = new(appSealSys)
// look up fortify executable path // look up fortify executable path
if p, err := os.Executable(); err != nil { if p, err := a.os.Executable(); err != nil {
return (*LauncherLookupError)(wrapError(err, "cannot look up fortify executable path:", err)) return fmsg.WrapErrorSuffix(err, "cannot look up fortify executable path:")
} else { } else {
seal.sys.executable = p seal.sys.executable = p
} }
// look up user from system // look up user from system
if u, err := user.Lookup(config.User); err != nil { if u, err := a.os.Lookup(config.User); err != nil {
if errors.As(err, new(user.UnknownUserError)) { if errors.As(err, new(user.UnknownUserError)) {
return (*SealConfigError)(wrapError(ErrUser, "unknown user", config.User)) return fmsg.WrapError(ErrUser, "unknown user", config.User)
} else { } else {
// unreachable // unreachable
panic(err) panic(err)
} }
} else { } else {
seal.sys.User = u seal.sys.user = u
seal.sys.runtime = path.Join("/run/user", u.Uid) seal.sys.runtime = path.Join("/run/user", u.Uid)
} }
// map sandbox config to bwrap // map sandbox config to bwrap
if config.Confinement.Sandbox == nil { if config.Confinement.Sandbox == nil {
verbose.Println("sandbox configuration not supplied, PROCEED WITH CAUTION") fmsg.VPrintln("sandbox configuration not supplied, PROCEED WITH CAUTION")
// permissive defaults // permissive defaults
conf := &SandboxConfig{ conf := &SandboxConfig{
@ -125,26 +158,29 @@ func (a *app) Seal(config *Config) error {
NoNewSession: true, NoNewSession: true,
} }
// bind entries in / // bind entries in /
if d, err := os.ReadDir("/"); err != nil { if d, err := a.os.ReadDir("/"); err != nil {
return err return err
} else { } else {
b := make([]*FilesystemConfig, 0, len(d)) b := make([]*FilesystemConfig, 0, len(d))
for _, ent := range d { for _, ent := range d {
name := ent.Name() p := "/" + ent.Name()
switch name { switch p {
case "proc": case "/proc":
case "dev": case "/dev":
case "run": case "/run":
case "mnt": case "/tmp":
case "/mnt":
case "/etc":
b = append(b, &FilesystemConfig{Src: p, Dst: "/dev/fortify/etc", Write: false, Must: true})
default: default:
p := "/" + name
b = append(b, &FilesystemConfig{Src: p, Write: true, Must: true}) b = append(b, &FilesystemConfig{Src: p, Write: true, Must: true})
} }
} }
conf.Filesystem = append(conf.Filesystem, b...) conf.Filesystem = append(conf.Filesystem, b...)
} }
// bind entries in /run // bind entries in /run
if d, err := os.ReadDir("/run"); err != nil { if d, err := a.os.ReadDir("/run"); err != nil {
return err return err
} else { } else {
b := make([]*FilesystemConfig, 0, len(d)) b := make([]*FilesystemConfig, 0, len(d))
@ -162,50 +198,77 @@ func (a *app) Seal(config *Config) error {
} }
// hide nscd from sandbox if present // hide nscd from sandbox if present
nscd := "/var/run/nscd" nscd := "/var/run/nscd"
if _, err := os.Stat(nscd); !errors.Is(err, os.ErrNotExist) { if _, err := a.os.Stat(nscd); !errors.Is(err, fs.ErrNotExist) {
conf.Tmpfs = append(conf.Tmpfs, bwrap.TmpfsConfig{Size: 8 * 1024, Dir: nscd}) conf.Override = append(conf.Override, nscd)
} }
// bind GPU stuff // bind GPU stuff
if config.Confinement.Enablements.Has(state.EnableX) || config.Confinement.Enablements.Has(state.EnableWayland) { if config.Confinement.Enablements.Has(system.EX11) || config.Confinement.Enablements.Has(system.EWayland) {
conf.Filesystem = append(conf.Filesystem, &FilesystemConfig{Src: "/dev/dri", Device: true}) conf.Filesystem = append(conf.Filesystem, &FilesystemConfig{Src: "/dev/dri", Device: true})
} }
// link host /etc to prevent passwd/group from being overwritten
if d, err := a.os.ReadDir("/etc"); err != nil {
return err
} else {
b := make([][2]string, 0, len(d))
for _, ent := range d {
name := ent.Name()
switch name {
case "passwd":
case "group":
case "mtab":
b = append(b, [2]string{
"/proc/mounts",
"/etc/" + name,
})
default:
b = append(b, [2]string{
"/dev/fortify/etc/" + name,
"/etc/" + name,
})
}
}
conf.Link = append(conf.Link, b...)
}
config.Confinement.Sandbox = conf config.Confinement.Sandbox = conf
} }
seal.sys.bwrap = config.Confinement.Sandbox.Bwrap() seal.sys.bwrap = config.Confinement.Sandbox.Bwrap()
seal.sys.override = config.Confinement.Sandbox.Override
if seal.sys.bwrap.SetEnv == nil { if seal.sys.bwrap.SetEnv == nil {
seal.sys.bwrap.SetEnv = make(map[string]string) seal.sys.bwrap.SetEnv = make(map[string]string)
} }
// create wayland client wait channel if mediated wayland is enabled // create wayland struct and client wait channel if mediated wayland is enabled
// this channel being set enables mediated wayland setup later on // this field being set enables mediated wayland setup later on
if config.Confinement.Sandbox.Wayland { if config.Confinement.Sandbox.Wayland {
seal.wlDone = make(chan struct{}) seal.wl = shim.NewWayland()
} }
// open process state store // open process state store
// the simple store only starts holding an open file after first action // the simple store only starts holding an open file after first action
// store activity begins after Start is called and must end before Wait // store activity begins after Start is called and must end before Wait
seal.store = state.NewSimple(seal.SystemConstants.RunDirPath, seal.sys.Uid) seal.store = state.NewSimple(seal.RunDirPath, seal.sys.user.Uid)
// parse string UID // parse string UID
if u, err := strconv.Atoi(seal.sys.Uid); err != nil { if u, err := strconv.Atoi(seal.sys.user.Uid); err != nil {
// unreachable unless kernel bug // unreachable unless kernel bug
panic("uid parse") panic("uid parse")
} else { } else {
seal.sys.uid = u seal.sys.I = system.New(u)
} }
// pass through enablements // pass through enablements
seal.et = config.Confinement.Enablements seal.et = config.Confinement.Enablements
// this method calls all share methods in sequence // this method calls all share methods in sequence
if err := seal.shareAll([2]*dbus.Config{config.Confinement.SessionBus, config.Confinement.SystemBus}); err != nil { if err := seal.shareAll([2]*dbus.Config{config.Confinement.SessionBus, config.Confinement.SystemBus}, a.os); err != nil {
return err return err
} }
// verbose log seal information // verbose log seal information
verbose.Println("created application seal as user", fmsg.VPrintln("created application seal as user",
seal.sys.Username, "("+seal.sys.Uid+"),", seal.sys.user.Username, "("+seal.sys.user.Uid+"),",
"method:", config.Method+",", "method:", config.Method+",",
"launcher:", seal.toolPath+",", "launcher:", seal.toolPath+",",
"command:", config.Command) "command:", config.Command)

View File

@ -1,15 +1,11 @@
package app package app
import ( import (
"errors"
"fmt"
"os"
"path" "path"
"git.ophivana.moe/cat/fortify/acl" "git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/cat/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/security/fortify/internal/system"
"git.ophivana.moe/cat/fortify/internal/verbose"
) )
const ( const (
@ -17,122 +13,30 @@ const (
dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS" dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
) )
var (
ErrDBusConfig = errors.New("dbus config not supplied")
)
type (
SealDBusError BaseError
LookupDBusError BaseError
StartDBusError BaseError
CloseDBusError BaseError
)
func (seal *appSeal) shareDBus(config [2]*dbus.Config) error { func (seal *appSeal) shareDBus(config [2]*dbus.Config) error {
if !seal.et.Has(state.EnableDBus) { if !seal.et.Has(system.EDBus) {
return nil return nil
} }
// session bus is mandatory
if config[0] == nil {
return (*SealDBusError)(wrapError(ErrDBusConfig, "attempted to seal session bus proxy with nil config"))
}
// system bus is optional
seal.sys.dbusSystem = config[1] != nil
// upstream address, downstream socket path
var sessionBus, systemBus [2]string
// downstream socket paths // downstream socket paths
sessionBus[1] = path.Join(seal.share, "bus") sessionPath, systemPath := path.Join(seal.share, "bus"), path.Join(seal.share, "system_bus_socket")
systemBus[1] = path.Join(seal.share, "system_bus_socket")
// resolve upstream bus addresses // configure dbus proxy
sessionBus[0], systemBus[0] = dbus.Address() if err := seal.sys.ProxyDBus(config[0], config[1], sessionPath, systemPath); err != nil {
return err
// create proxy instance
seal.sys.dbus = dbus.New(sessionBus, systemBus)
// seal dbus proxy
if err := seal.sys.dbus.Seal(config[0], config[1]); err != nil {
return (*SealDBusError)(wrapError(err, "cannot seal message bus proxy:", err))
} }
// store addresses for cleanup and logging
seal.sys.dbusAddr = &[2][2]string{sessionBus, systemBus}
// share proxy sockets // share proxy sockets
sessionInner := path.Join(seal.sys.runtime, "bus") sessionInner := path.Join(seal.sys.runtime, "bus")
seal.sys.setEnv(dbusSessionBusAddress, "unix:path="+sessionInner) seal.sys.bwrap.SetEnv[dbusSessionBusAddress] = "unix:path=" + sessionInner
seal.sys.bind(sessionBus[1], sessionInner, true) seal.sys.bwrap.Bind(sessionPath, sessionInner)
seal.sys.updatePerm(sessionBus[1], acl.Read, acl.Write) seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
if seal.sys.dbusSystem { if config[1] != nil {
systemInner := "/run/dbus/system_bus_socket" systemInner := "/run/dbus/system_bus_socket"
seal.sys.setEnv(dbusSystemBusAddress, "unix:path="+systemInner) seal.sys.bwrap.SetEnv[dbusSystemBusAddress] = "unix:path=" + systemInner
seal.sys.bind(systemBus[1], systemInner, true) seal.sys.bwrap.Bind(systemPath, systemInner)
seal.sys.updatePerm(systemBus[1], acl.Read, acl.Write) seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
} }
return nil return nil
} }
func (tx *appSealTx) startDBus() error {
// ready channel passed to dbus package
ready := make(chan error, 1)
// used by waiting goroutine to notify process return
tx.dbusWait = make(chan struct{})
// background dbus proxy start
if err := tx.dbus.Start(ready, os.Stderr, true); err != nil {
return (*StartDBusError)(wrapError(err, "cannot start message bus proxy:", err))
}
verbose.Println("starting message bus proxy:", tx.dbus)
verbose.Println("message bus proxy bwrap args:", tx.dbus.Bwrap())
// background wait for proxy instance and notify completion
go func() {
if err := tx.dbus.Wait(); err != nil {
fmt.Println("fortify: warn: message bus proxy returned error:", err)
go func() { ready <- err }()
} else {
verbose.Println("message bus proxy exit")
}
// ensure socket removal so ephemeral directory is empty at revert
if err := os.Remove(tx.dbusAddr[0][1]); err != nil && !errors.Is(err, os.ErrNotExist) {
fmt.Println("fortify: cannot remove dangling session bus socket:", err)
}
if tx.dbusSystem {
if err := os.Remove(tx.dbusAddr[1][1]); err != nil && !errors.Is(err, os.ErrNotExist) {
fmt.Println("fortify: cannot remove dangling system bus socket:", err)
}
}
// notify proxy completion
tx.dbusWait <- struct{}{}
}()
// ready is not nil if the proxy process faulted
if err := <-ready; err != nil {
// note that err here is either an I/O related error or a predetermined unexpected behaviour error
return (*StartDBusError)(wrapError(err, "message bus proxy fault after start:", err))
}
verbose.Println("message bus proxy ready")
return nil
}
func (tx *appSealTx) stopDBus() error {
if err := tx.dbus.Close(); err != nil {
if errors.Is(err, os.ErrClosed) {
return (*CloseDBusError)(wrapError(err, "message bus proxy already closed"))
} else {
return (*CloseDBusError)(wrapError(err, "cannot close message bus proxy:", err))
}
}
// block until proxy wait returns
<-tx.dbusWait
return nil
}

View File

@ -2,11 +2,12 @@ package app
import ( import (
"errors" "errors"
"os"
"path" "path"
"git.ophivana.moe/cat/fortify/acl" "git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/system"
) )
const ( const (
@ -22,44 +23,44 @@ var (
ErrXDisplay = errors.New(display + " unset") ErrXDisplay = errors.New(display + " unset")
) )
type ErrDisplayEnv BaseError func (seal *appSeal) shareDisplay(os internal.System) error {
func (seal *appSeal) shareDisplay() error {
// pass $TERM to launcher // pass $TERM to launcher
if t, ok := os.LookupEnv(term); ok { if t, ok := os.LookupEnv(term); ok {
seal.sys.setEnv(term, t) seal.sys.bwrap.SetEnv[term] = t
} }
// set up wayland // set up wayland
if seal.et.Has(state.EnableWayland) { if seal.et.Has(system.EWayland) {
if wd, ok := os.LookupEnv(waylandDisplay); !ok { if wd, ok := os.LookupEnv(waylandDisplay); !ok {
return (*ErrDisplayEnv)(wrapError(ErrWayland, "WAYLAND_DISPLAY is not set")) return fmsg.WrapError(ErrWayland,
} else if seal.wlDone == nil { "WAYLAND_DISPLAY is not set")
} else if seal.wl == nil {
// hardlink wayland socket // hardlink wayland socket
wp := path.Join(seal.RuntimePath, wd) wp := path.Join(seal.RuntimePath, wd)
wpi := path.Join(seal.shareLocal, "wayland") wpi := path.Join(seal.shareLocal, "wayland")
w := path.Join(seal.sys.runtime, "wayland-0") w := path.Join(seal.sys.runtime, "wayland-0")
seal.sys.link(wp, wpi) seal.sys.Link(wp, wpi)
seal.sys.setEnv(waylandDisplay, w) seal.sys.bwrap.SetEnv[waylandDisplay] = w
seal.sys.bind(wpi, w, true) seal.sys.bwrap.Bind(wpi, w)
// ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`) // ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
seal.sys.updatePermTag(state.EnableWayland, wp, acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.EWayland, wp, acl.Read, acl.Write, acl.Execute)
} else { } else {
// set wayland socket path (e.g. `/run/user/%d/wayland-%d`) // set wayland socket path for mediation (e.g. `/run/user/%d/wayland-%d`)
seal.wl = path.Join(seal.RuntimePath, wd) seal.wl.Path = path.Join(seal.RuntimePath, wd)
} }
} }
// set up X11 // set up X11
if seal.et.Has(state.EnableX) { if seal.et.Has(system.EX11) {
// discover X11 and grant user permission via the `ChangeHosts` command // discover X11 and grant user permission via the `ChangeHosts` command
if d, ok := os.LookupEnv(display); !ok { if d, ok := os.LookupEnv(display); !ok {
return (*ErrDisplayEnv)(wrapError(ErrXDisplay, "DISPLAY is not set")) return fmsg.WrapError(ErrXDisplay,
"DISPLAY is not set")
} else { } else {
seal.sys.changeHosts(seal.sys.Username) seal.sys.ChangeHosts(seal.sys.user.Username)
seal.sys.setEnv(display, d) seal.sys.bwrap.SetEnv[display] = d
seal.sys.bind("/tmp/.X11-unix", "/tmp/.X11-unix", true) seal.sys.bwrap.Bind("/tmp/.X11-unix", "/tmp/.X11-unix")
} }
} }

View File

@ -4,10 +4,11 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"os"
"path" "path"
"git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/system"
) )
const ( const (
@ -24,13 +25,8 @@ var (
ErrPulseMode = errors.New("unexpected pulse socket mode") ErrPulseMode = errors.New("unexpected pulse socket mode")
) )
type ( func (seal *appSeal) sharePulse(os internal.System) error {
PulseCookieAccessError BaseError if !seal.et.Has(system.EPulse) {
PulseSocketAccessError BaseError
)
func (seal *appSeal) sharePulse() error {
if !seal.et.Has(state.EnablePulse) {
return nil return nil
} }
@ -39,49 +35,50 @@ func (seal *appSeal) sharePulse() error {
ps := path.Join(pd, "native") ps := path.Join(pd, "native")
if _, err := os.Stat(pd); err != nil { if _, err := os.Stat(pd); err != nil {
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return (*PulseSocketAccessError)(wrapError(err, return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio directory '%s':", pd), err)) fmt.Sprintf("cannot access PulseAudio directory %q:", pd))
} }
return (*PulseSocketAccessError)(wrapError(ErrPulseSocket, return fmsg.WrapError(ErrPulseSocket,
fmt.Sprintf("PulseAudio directory '%s' not found", pd))) fmt.Sprintf("PulseAudio directory %q not found", pd))
} }
// check PulseAudio socket permission (e.g. `/run/user/%d/pulse/native`) // check PulseAudio socket permission (e.g. `/run/user/%d/pulse/native`)
if s, err := os.Stat(ps); err != nil { if s, err := os.Stat(ps); err != nil {
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return (*PulseSocketAccessError)(wrapError(err, return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio socket '%s':", ps), err)) fmt.Sprintf("cannot access PulseAudio socket %q:", ps))
} }
return (*PulseSocketAccessError)(wrapError(ErrPulseSocket, return fmsg.WrapError(ErrPulseSocket,
fmt.Sprintf("PulseAudio directory '%s' found but socket does not exist", pd))) fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pd))
} else { } else {
if m := s.Mode(); m&0o006 != 0o006 { if m := s.Mode(); m&0o006 != 0o006 {
return (*PulseSocketAccessError)(wrapError(ErrPulseMode, return fmsg.WrapError(ErrPulseMode,
fmt.Sprintf("unexpected permissions on '%s':", ps), m)) fmt.Sprintf("unexpected permissions on %q:", ps), m)
} }
} }
// hard link pulse socket into target-executable share // hard link pulse socket into target-executable share
psi := path.Join(seal.shareLocal, "pulse") psi := path.Join(seal.shareLocal, "pulse")
p := path.Join(seal.sys.runtime, "pulse", "native") p := path.Join(seal.sys.runtime, "pulse", "native")
seal.sys.link(ps, psi) seal.sys.Link(ps, psi)
seal.sys.bind(psi, p, true) seal.sys.bwrap.Bind(psi, p)
seal.sys.setEnv(pulseServer, "unix:"+p) seal.sys.bwrap.SetEnv[pulseServer] = "unix:" + p
// publish current user's pulse cookie for target user // publish current user's pulse cookie for target user
if src, err := discoverPulseCookie(); err != nil { if src, err := discoverPulseCookie(os); err != nil {
return err return err
} else { } else {
dst := path.Join(seal.share, "pulse-cookie") dst := path.Join(seal.share, "pulse-cookie")
seal.sys.setEnv(pulseCookie, dst) seal.sys.bwrap.SetEnv[pulseCookie] = dst
seal.sys.copyFile(dst, src) seal.sys.CopyFile(dst, src)
seal.sys.bwrap.Bind(dst, dst)
} }
return nil return nil
} }
// discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie // discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie
func discoverPulseCookie() (string, error) { func discoverPulseCookie(os internal.System) (string, error) {
if p, ok := os.LookupEnv(pulseCookie); ok { if p, ok := os.LookupEnv(pulseCookie); ok {
return p, nil return p, nil
} }
@ -91,8 +88,8 @@ func discoverPulseCookie() (string, error) {
p = path.Join(p, ".pulse-cookie") p = path.Join(p, ".pulse-cookie")
if s, err := os.Stat(p); err != nil { if s, err := os.Stat(p); err != nil {
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return p, (*PulseCookieAccessError)(wrapError(err, return p, fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio cookie '%s':", p), err)) fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
} }
// not found, try next method // not found, try next method
} else if !s.IsDir() { } else if !s.IsDir() {
@ -105,7 +102,8 @@ func discoverPulseCookie() (string, error) {
p = path.Join(p, "pulse", "cookie") p = path.Join(p, "pulse", "cookie")
if s, err := os.Stat(p); err != nil { if s, err := os.Stat(p); err != nil {
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return p, (*PulseCookieAccessError)(wrapError(err, "cannot access PulseAudio cookie", p+":", err)) return p, fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
} }
// not found, try next method // not found, try next method
} else if !s.IsDir() { } else if !s.IsDir() {
@ -113,7 +111,7 @@ func discoverPulseCookie() (string, error) {
} }
} }
return "", (*PulseCookieAccessError)(wrapError(ErrPulseCookie, return "", fmsg.WrapError(ErrPulseCookie,
fmt.Sprintf("cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)", fmt.Sprintf("cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
pulseCookie, xdgConfigHome, home))) pulseCookie, xdgConfigHome, home))
} }

View File

@ -3,9 +3,8 @@ package app
import ( import (
"path" "path"
"git.ophivana.moe/cat/fortify/acl" "git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/cat/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/internal/system"
"git.ophivana.moe/cat/fortify/internal/state"
) )
const ( const (
@ -17,58 +16,24 @@ const (
// shareRuntime queues actions for sharing/ensuring the runtime and share directories // shareRuntime queues actions for sharing/ensuring the runtime and share directories
func (seal *appSeal) shareRuntime() { func (seal *appSeal) shareRuntime() {
// mount tmpfs on inner runtime (e.g. `/run/user/%d`) // mount tmpfs on inner runtime (e.g. `/run/user/%d`)
seal.sys.bwrap.Tmpfs = append(seal.sys.bwrap.Tmpfs, seal.sys.bwrap.Tmpfs("/run/user", 1*1024*1024)
bwrap.PermConfig[bwrap.TmpfsConfig]{ seal.sys.bwrap.Tmpfs(seal.sys.runtime, 8*1024*1024)
Path: bwrap.TmpfsConfig{
Size: 1 * 1024 * 1024, // point to inner runtime path `/run/user/%d`
Dir: "/run/user", seal.sys.bwrap.SetEnv[xdgRuntimeDir] = seal.sys.runtime
}, seal.sys.bwrap.SetEnv[xdgSessionClass] = "user"
}, seal.sys.bwrap.SetEnv[xdgSessionType] = "tty"
bwrap.PermConfig[bwrap.TmpfsConfig]{
Path: bwrap.TmpfsConfig{
Size: 8 * 1024 * 1024,
Dir: seal.sys.runtime,
},
},
)
// ensure RunDir (e.g. `/run/user/%d/fortify`) // ensure RunDir (e.g. `/run/user/%d/fortify`)
seal.sys.ensure(seal.RunDirPath, 0700) seal.sys.Ensure(seal.RunDirPath, 0700)
seal.sys.updatePermTag(state.EnableLength, seal.RunDirPath, acl.Execute) seal.sys.UpdatePermType(system.User, seal.RunDirPath, acl.Execute)
// ensure runtime directory ACL (e.g. `/run/user/%d`) // ensure runtime directory ACL (e.g. `/run/user/%d`)
seal.sys.updatePermTag(state.EnableLength, seal.RuntimePath, acl.Execute) seal.sys.Ensure(seal.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
seal.sys.UpdatePermType(system.User, seal.RuntimePath, acl.Execute)
// ensure Share (e.g. `/tmp/fortify.%d`)
// acl is unnecessary as this directory is world executable
seal.sys.ensure(seal.SharePath, 0701)
// ensure process-specific share (e.g. `/tmp/fortify.%d/%s`)
// acl is unnecessary as this directory is world executable
seal.share = path.Join(seal.SharePath, seal.id.String())
seal.sys.ensureEphemeral(seal.share, 0701)
// ensure process-specific share local to XDG_RUNTIME_DIR (e.g. `/run/user/%d/fortify/%s`) // ensure process-specific share local to XDG_RUNTIME_DIR (e.g. `/run/user/%d/fortify/%s`)
seal.shareLocal = path.Join(seal.RunDirPath, seal.id.String()) seal.shareLocal = path.Join(seal.RunDirPath, seal.id)
seal.sys.ensureEphemeral(seal.shareLocal, 0700) seal.sys.Ephemeral(system.Process, seal.shareLocal, 0700)
seal.sys.updatePerm(seal.shareLocal, acl.Execute) seal.sys.UpdatePerm(seal.shareLocal, acl.Execute)
}
func (seal *appSeal) shareRuntimeChild() string {
// ensure child runtime parent directory (e.g. `/tmp/fortify.%d/runtime`)
targetRuntimeParent := path.Join(seal.SharePath, "runtime")
seal.sys.ensure(targetRuntimeParent, 0700)
seal.sys.updatePermTag(state.EnableLength, targetRuntimeParent, acl.Execute)
// ensure child runtime directory (e.g. `/tmp/fortify.%d/runtime/%d`)
targetRuntime := path.Join(targetRuntimeParent, seal.sys.Uid)
seal.sys.ensure(targetRuntime, 0700)
seal.sys.updatePermTag(state.EnableLength, targetRuntime, acl.Read, acl.Write, acl.Execute)
// point to ensured runtime path
seal.sys.setEnv(xdgRuntimeDir, targetRuntime)
seal.sys.setEnv(xdgSessionClass, "user")
seal.sys.setEnv(xdgSessionType, "tty")
return targetRuntime
} }

View File

@ -1,8 +1,11 @@
package app package app
import ( import (
"os"
"path" "path"
"git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/system"
) )
const ( const (
@ -11,33 +14,58 @@ const (
// shareSystem queues various system-related actions // shareSystem queues various system-related actions
func (seal *appSeal) shareSystem() { func (seal *appSeal) shareSystem() {
// ensure Share (e.g. `/tmp/fortify.%d`)
// acl is unnecessary as this directory is world executable
seal.sys.Ensure(seal.SharePath, 0701)
// ensure process-specific share (e.g. `/tmp/fortify.%d/%s`)
// acl is unnecessary as this directory is world executable
seal.share = path.Join(seal.SharePath, seal.id)
seal.sys.Ephemeral(system.Process, seal.share, 0701)
// ensure child tmpdir parent directory (e.g. `/tmp/fortify.%d/tmpdir`)
targetTmpdirParent := path.Join(seal.SharePath, "tmpdir")
seal.sys.Ensure(targetTmpdirParent, 0700)
seal.sys.UpdatePermType(system.User, targetTmpdirParent, acl.Execute)
// ensure child tmpdir (e.g. `/tmp/fortify.%d/tmpdir/%d`)
targetTmpdir := path.Join(targetTmpdirParent, seal.sys.user.Uid)
seal.sys.Ensure(targetTmpdir, 01700)
seal.sys.UpdatePermType(system.User, targetTmpdir, acl.Read, acl.Write, acl.Execute)
seal.sys.bwrap.Bind(targetTmpdir, "/tmp", false, true)
// mount tmpfs on inner shared directory (e.g. `/tmp/fortify.%d`)
seal.sys.bwrap.Tmpfs(seal.SharePath, 1*1024*1024)
}
func (seal *appSeal) sharePasswd(os internal.System) {
// look up shell // look up shell
sh := "/bin/sh" sh := "/bin/sh"
if s, ok := os.LookupEnv(shell); ok { if s, ok := os.LookupEnv(shell); ok {
seal.sys.setEnv(shell, s) seal.sys.bwrap.SetEnv[shell] = s
sh = s sh = s
} }
// generate /etc/passwd // generate /etc/passwd
passwdPath := path.Join(seal.share, "passwd") passwdPath := path.Join(seal.share, "passwd")
username := "chronos" username := "chronos"
if seal.sys.Username != "" { if seal.sys.user.Username != "" {
username = seal.sys.Username username = seal.sys.user.Username
seal.sys.setEnv("USER", seal.sys.Username) seal.sys.bwrap.SetEnv["USER"] = seal.sys.user.Username
} }
homeDir := "/var/empty" homeDir := "/var/empty"
if seal.sys.HomeDir != "" { if seal.sys.user.HomeDir != "" {
homeDir = seal.sys.HomeDir homeDir = seal.sys.user.HomeDir
seal.sys.setEnv("HOME", seal.sys.HomeDir) seal.sys.bwrap.SetEnv["HOME"] = seal.sys.user.HomeDir
} }
passwd := username + ":x:65534:65534:Fortify:" + homeDir + ":" + sh + "\n" passwd := username + ":x:65534:65534:Fortify:" + homeDir + ":" + sh + "\n"
seal.sys.writeFile(passwdPath, []byte(passwd)) seal.sys.Write(passwdPath, passwd)
// write /etc/group // write /etc/group
groupPath := path.Join(seal.share, "group") groupPath := path.Join(seal.share, "group")
seal.sys.writeFile(groupPath, []byte("fortify:x:65534:\n")) seal.sys.Write(groupPath, "fortify:x:65534:\n")
// bind /etc/passwd and /etc/group // bind /etc/passwd and /etc/group
seal.sys.bind(passwdPath, "/etc/passwd", true) seal.sys.bwrap.Bind(passwdPath, "/etc/passwd")
seal.sys.bind(groupPath, "/etc/group", true) seal.sys.bwrap.Bind(groupPath, "/etc/group")
} }

View File

@ -3,27 +3,20 @@ package app
import ( import (
"errors" "errors"
"fmt" "fmt"
"os"
"os/exec" "os/exec"
"path" "path"
"path/filepath" "path/filepath"
"strconv" "strings"
"time"
"git.ophivana.moe/cat/fortify/helper" "git.ophivana.moe/security/fortify/helper"
"git.ophivana.moe/cat/fortify/internal/shim" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/security/fortify/internal/shim"
"git.ophivana.moe/cat/fortify/internal/verbose" "git.ophivana.moe/security/fortify/internal/state"
"git.ophivana.moe/security/fortify/internal/system"
) )
type ( // Start selects a user switcher and starts shim.
// ProcessError encapsulates errors returned by starting *exec.Cmd // Note that Wait must be called regardless of error returned by Start.
ProcessError BaseError
// ShimError encapsulates errors returned by shim.ServeConfig.
ShimError BaseError
)
// Start starts the fortified child
func (a *app) Start() error { func (a *app) Start() error {
a.lock.Lock() a.lock.Lock()
defer a.lock.Unlock() defer a.lock.Unlock()
@ -41,17 +34,14 @@ func (a *app) Start() error {
if s, err := exec.LookPath(n); err == nil { if s, err := exec.LookPath(n); err == nil {
shimExec[i] = s shimExec[i] = s
} else { } else {
return (*ProcessError)(wrapError(err, fmt.Sprintf("cannot find %q: %v", n, err))) return fmsg.WrapError(err,
fmt.Sprintf("executable file %q not found in $PATH", n))
} }
} }
} }
if err := a.seal.sys.commit(); err != nil {
return err
}
// select command builder // select command builder
var commandBuilder func(shimEnv string) (args []string) var commandBuilder shim.CommandBuilder
switch a.seal.launchOption { switch a.seal.launchOption {
case LaunchMethodSudo: case LaunchMethodSudo:
commandBuilder = a.commandBuilderSudo commandBuilder = a.commandBuilderSudo
@ -61,51 +51,48 @@ func (a *app) Start() error {
panic("unreachable") panic("unreachable")
} }
// configure child process // construct shim manager
confSockPath := path.Join(a.seal.share, "shim") a.shim = shim.New(a.seal.toolPath, uint32(a.seal.sys.UID()), path.Join(a.seal.share, "shim"), a.seal.wl,
a.cmd = exec.Command(a.seal.toolPath, commandBuilder(shim.EnvShim+"="+confSockPath)...) &shim.Payload{
a.cmd.Env = []string{}
a.cmd.Stdin = os.Stdin
a.cmd.Stdout = os.Stdout
a.cmd.Stderr = os.Stderr
a.cmd.Dir = a.seal.RunDirPath
if wls, err := shim.ServeConfig(confSockPath, &shim.Payload{
Argv: a.seal.command, Argv: a.seal.command,
Exec: shimExec, Exec: shimExec,
Bwrap: a.seal.sys.bwrap, Bwrap: a.seal.sys.bwrap,
WL: a.seal.wlDone != nil, WL: a.seal.wl != nil,
Verbose: verbose.Get(), Verbose: fmsg.Verbose(),
}, a.seal.wl, a.seal.wlDone); err != nil { },
return (*ShimError)(wrapError(err, "cannot listen on shim socket:", err)) // checkPid is impossible at the moment since there is no reliable way to obtain shim's pid
// this feature is disabled here until sudo is replaced by fortify suid wrapper
false,
)
// startup will go ahead, commit system setup
if err := a.seal.sys.Commit(); err != nil {
return err
}
a.seal.sys.needRevert = true
if startTime, err := a.shim.Start(commandBuilder); err != nil {
return err
} else { } else {
a.wayland = wls // shim start and setup success, create process state
}
// start shim
verbose.Println("starting shim as target user:", a.cmd)
if err := a.cmd.Start(); err != nil {
return (*ProcessError)(wrapError(err, "cannot start process:", err))
}
startTime := time.Now().UTC()
// create process state
sd := state.State{ sd := state.State{
PID: a.cmd.Process.Pid, PID: a.shim.Unwrap().Process.Pid,
Command: a.seal.command, Command: a.seal.command,
Capability: a.seal.et, Capability: a.seal.et,
Launcher: a.seal.toolPath, Method: method[a.seal.launchOption],
Argv: a.cmd.Args, Argv: a.shim.Unwrap().Args,
Time: startTime, Time: *startTime,
} }
// register process state // register process state
var err = new(StateStoreError) var err0 = new(StateStoreError)
err.Inner, err.DoErr = a.seal.store.Do(func(b state.Backend) { err0.Inner, err0.DoErr = a.seal.store.Do(func(b state.Backend) {
err.InnerErr = b.Save(&sd) err0.InnerErr = b.Save(&sd)
}) })
return err.equiv("cannot save process state:", err) a.seal.sys.saveState = true
return err0.equiv("cannot save process state:")
}
} }
// StateStoreError is returned for a failed state save // StateStoreError is returned for a failed state save
@ -121,10 +108,10 @@ type StateStoreError struct {
} }
func (e *StateStoreError) equiv(a ...any) error { func (e *StateStoreError) equiv(a ...any) error {
if e.Inner == true && e.DoErr == nil && e.InnerErr == nil && e.Err == nil { if e.Inner && e.DoErr == nil && e.InnerErr == nil && e.Err == nil {
return nil return nil
} else { } else {
return wrapError(e, a...) return fmsg.WrapErrorSuffix(e, a...)
} }
} }
@ -167,29 +154,40 @@ func (a *app) Wait() (int, error) {
a.lock.Lock() a.lock.Lock()
defer a.lock.Unlock() defer a.lock.Unlock()
if a.shim == nil {
fmsg.VPrintln("shim not initialised, skipping cleanup")
return 1, nil
}
var r int var r int
if cmd := a.shim.Unwrap(); cmd == nil {
// failure prior to process start
r = 255
} else {
// wait for process and resolve exit code // wait for process and resolve exit code
if err := a.cmd.Wait(); err != nil { if err := cmd.Wait(); err != nil {
var exitError *exec.ExitError var exitError *exec.ExitError
if !errors.As(err, &exitError) { if !errors.As(err, &exitError) {
// should be unreachable // should be unreachable
a.wait = err a.waitErr = err
} }
// store non-zero return code // store non-zero return code
r = exitError.ExitCode() r = exitError.ExitCode()
} else { } else {
r = a.cmd.ProcessState.ExitCode() r = cmd.ProcessState.ExitCode()
}
fmsg.VPrintf("process %d exited with exit code %d", cmd.Process.Pid, r)
} }
verbose.Println("process", strconv.Itoa(a.cmd.Process.Pid), "exited with exit code", r) // child process exited, resume output
fmsg.Resume()
// close wayland connection // close wayland connection
if a.wayland != nil { if a.seal.wl != nil {
close(a.seal.wlDone) if err := a.seal.wl.Close(); err != nil {
if err := a.wayland.Close(); err != nil { fmsg.Println("cannot close wayland connection:", err)
fmt.Println("fortify: cannot close wayland connection:", err)
} }
} }
@ -198,22 +196,25 @@ func (a *app) Wait() (int, error) {
e.Inner, e.DoErr = a.seal.store.Do(func(b state.Backend) { e.Inner, e.DoErr = a.seal.store.Do(func(b state.Backend) {
e.InnerErr = func() error { e.InnerErr = func() error {
// destroy defunct state entry // destroy defunct state entry
if err := b.Destroy(a.cmd.Process.Pid); err != nil { if cmd := a.shim.Unwrap(); cmd != nil && a.seal.sys.saveState {
if err := b.Destroy(cmd.Process.Pid); err != nil {
return err return err
} }
}
// enablements of remaining launchers // enablements of remaining launchers
rt, tags := new(state.Enablements), new(state.Enablements) rt, ec := new(system.Enablements), new(system.Criteria)
tags.Set(state.EnableLength + 1) ec.Enablements = new(system.Enablements)
ec.Set(system.Process)
if states, err := b.Load(); err != nil { if states, err := b.Load(); err != nil {
return err return err
} else { } else {
if l := len(states); l == 0 { if l := len(states); l == 0 {
// cleanup globals as the final launcher // cleanup globals as the final launcher
verbose.Println("no other launchers active, will clean up globals") fmsg.VPrintln("no other launchers active, will clean up globals")
tags.Set(state.EnableLength) ec.Set(system.User)
} else { } else {
verbose.Printf("found %d active launchers, cleaning up without globals\n", l) fmsg.VPrintf("found %d active launchers, cleaning up without globals", l)
} }
// accumulate capabilities of other launchers // accumulate capabilities of other launchers
@ -222,24 +223,25 @@ func (a *app) Wait() (int, error) {
} }
} }
// invert accumulated enablements for cleanup // invert accumulated enablements for cleanup
for i := state.Enablement(0); i < state.EnableLength; i++ { for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
if !rt.Has(i) { if !rt.Has(i) {
tags.Set(i) ec.Set(i)
} }
} }
if verbose.Get() { if fmsg.Verbose() {
ct := make([]state.Enablement, 0, state.EnableLength) labels := make([]string, 0, system.ELen+1)
for i := state.Enablement(0); i < state.EnableLength; i++ { for i := system.Enablement(0); i < system.Enablement(system.ELen+2); i++ {
if tags.Has(i) { if ec.Has(i) {
ct = append(ct, i) labels = append(labels, system.TypeString(i))
} }
} }
if len(ct) > 0 { if len(labels) > 0 {
verbose.Println("will revert operations tagged", ct, "as no remaining launchers hold these enablements") fmsg.VPrintln("reverting operations labelled", strings.Join(labels, ", "))
} }
} }
if err := a.seal.sys.revert(tags); err != nil { a.shim.AbortWait(errors.New("shim exited"))
if err := a.seal.sys.Revert(ec); err != nil {
return err.(RevertCompoundError) return err.(RevertCompoundError)
} }

View File

@ -1,429 +1,48 @@
package app package app
import ( import (
"errors"
"fmt"
"io/fs"
"os"
"os/user" "os/user"
"git.ophivana.moe/cat/fortify/acl" "git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/cat/fortify/dbus" "git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/cat/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/cat/fortify/internal" "git.ophivana.moe/security/fortify/internal/system"
"git.ophivana.moe/cat/fortify/internal/state"
"git.ophivana.moe/cat/fortify/internal/verbose"
"git.ophivana.moe/cat/fortify/xcb"
) )
// appSeal seals the application with child-related information // appSealSys encapsulates app seal behaviour with OS interactions
type appSeal struct { type appSealSys struct {
// application unique identifier
id *appID
// wayland socket path if mediated wayland is enabled
wl string
// wait for wayland client to exit if mediated wayland is enabled,
// (wlDone == nil) determines whether mediated wayland setup is performed
wlDone chan struct{}
// freedesktop application ID
fid string
// argv to start process with in the final confined environment
command []string
// persistent process state store
store state.Store
// uint8 representation of launch method sealed from config
launchOption uint8
// process-specific share directory path
share string
// process-specific share directory path local to XDG_RUNTIME_DIR
shareLocal string
// path to launcher program
toolPath string
// pass-through enablement tracking from config
et state.Enablements
// prevents sharing from happening twice
shared bool
// seal system-level component
sys *appSealTx
// used in various sealing operations
internal.SystemConstants
// protected by upstream mutex
}
// appSealTx contains the system-level component of the app seal
type appSealTx struct {
bwrap *bwrap.Config bwrap *bwrap.Config
// paths to override by mounting tmpfs over them
// reference to D-Bus proxy instance, nil if disabled override []string
dbus *dbus.Proxy
// notification from goroutine waiting for dbus.Proxy
dbusWait chan struct{}
// upstream address/downstream path used to initialise dbus.Proxy
dbusAddr *[2][2]string
// whether system bus proxy is enabled
dbusSystem bool
// paths to append/strip ACLs (of target user) from
acl []*appACLEntry
// X11 ChangeHosts commands to perform
xhost []string
// paths of directories to ensure
mkdir []appEnsureEntry
// dst, data pairs of temporarily available files
files [][2]string
// dst, src pairs of temporarily shared files
tmpfiles [][2]string
// dst, src pairs of temporarily hard linked files
hardlinks [][2]string
// default formatted XDG_RUNTIME_DIR of User // default formatted XDG_RUNTIME_DIR of User
runtime string runtime string
// sealed path to fortify executable, used by shim // sealed path to fortify executable, used by shim
executable string executable string
// target user UID as an integer
uid int
// target user sealed from config // target user sealed from config
*user.User user *user.User
// prevents commit from happening twice needRevert bool
complete bool saveState bool
// prevents cleanup from happening twice *system.I
closed bool
// protected by upstream mutex // protected by upstream mutex
} }
type appEnsureEntry struct {
path string
perm os.FileMode
remove bool
}
// setEnv sets an environment variable for the child process
func (tx *appSealTx) setEnv(k, v string) {
tx.bwrap.SetEnv[k] = v
}
// bind mounts a directory within the sandbox
func (tx *appSealTx) bind(src, dest string, ro bool) {
if !ro {
tx.bwrap.Bind = append(tx.bwrap.Bind, [2]string{src, dest})
} else {
tx.bwrap.ROBind = append(tx.bwrap.ROBind, [2]string{src, dest})
}
}
// ensure appends a directory ensure action
func (tx *appSealTx) ensure(path string, perm os.FileMode) {
tx.mkdir = append(tx.mkdir, appEnsureEntry{path, perm, false})
}
// ensureEphemeral appends a directory ensure action with removal in rollback
func (tx *appSealTx) ensureEphemeral(path string, perm os.FileMode) {
tx.mkdir = append(tx.mkdir, appEnsureEntry{path, perm, true})
}
// appACLEntry contains information for applying/reverting an ACL entry
type appACLEntry struct {
tag state.Enablement
path string
perms []acl.Perm
}
func (e *appACLEntry) ts() string {
switch e.tag {
case state.EnableLength:
return "Global"
case state.EnableLength + 1:
return "Process"
default:
return e.tag.String()
}
}
func (e *appACLEntry) String() string {
var s = []byte("---")
for _, p := range e.perms {
switch p {
case acl.Read:
s[0] = 'r'
case acl.Write:
s[1] = 'w'
case acl.Execute:
s[2] = 'x'
}
}
return string(s)
}
// updatePerm appends an untagged acl update action
func (tx *appSealTx) updatePerm(path string, perms ...acl.Perm) {
tx.updatePermTag(state.EnableLength+1, path, perms...)
}
// updatePermTag appends an acl update action
// Tagging with state.EnableLength sets cleanup to happen at final active launcher exit,
// while tagging with state.EnableLength+1 will unconditionally clean up on exit.
func (tx *appSealTx) updatePermTag(tag state.Enablement, path string, perms ...acl.Perm) {
tx.acl = append(tx.acl, &appACLEntry{tag, path, perms})
}
// changeHosts appends target username of an X11 ChangeHosts action
func (tx *appSealTx) changeHosts(username string) {
tx.xhost = append(tx.xhost, username)
}
// writeFile appends a files action
func (tx *appSealTx) writeFile(dst string, data []byte) {
tx.files = append(tx.files, [2]string{dst, string(data)})
tx.updatePerm(dst, acl.Read)
tx.bind(dst, dst, true)
}
// copyFile appends a tmpfiles action
func (tx *appSealTx) copyFile(dst, src string) {
tx.tmpfiles = append(tx.tmpfiles, [2]string{dst, src})
tx.updatePerm(dst, acl.Read)
tx.bind(dst, dst, true)
}
// link appends a hardlink action
func (tx *appSealTx) link(oldname, newname string) {
tx.hardlinks = append(tx.hardlinks, [2]string{oldname, newname})
}
type (
ChangeHostsError BaseError
EnsureDirError BaseError
TmpfileError BaseError
DBusStartError BaseError
ACLUpdateError BaseError
)
// commit applies recorded actions
// order: xhost, mkdir, files, tmpfiles, hardlinks, dbus, acl
func (tx *appSealTx) commit() error {
if tx.complete {
panic("seal transaction committed twice")
}
tx.complete = true
txp := &appSealTx{User: tx.User, bwrap: &bwrap.Config{SetEnv: make(map[string]string)}}
defer func() {
// rollback partial commit
if txp != nil {
// global changes (x11, ACLs) are always repeated and check for other launchers cannot happen here
// attempting cleanup here will cause other fortified processes to lose access to them
// a better (and more secure) fix is to proxy access to these resources and eliminate the ACLs altogether
tags := new(state.Enablements)
for e := state.Enablement(0); e < state.EnableLength+2; e++ {
tags.Set(e)
}
if err := txp.revert(tags); err != nil {
fmt.Println("fortify: errors returned reverting partial commit:", err)
}
}
}()
// insert xhost entries
for _, username := range tx.xhost {
verbose.Printf("inserting XHost entry SI:localuser:%s\n", username)
if err := xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+username); err != nil {
return (*ChangeHostsError)(wrapError(err,
fmt.Sprintf("cannot insert XHost entry SI:localuser:%s, %s", username, err)))
} else {
// register partial commit
txp.changeHosts(username)
}
}
// ensure directories
for _, dir := range tx.mkdir {
verbose.Println("ensuring directory mode:", dir.perm.String(), "path:", dir.path)
if err := os.Mkdir(dir.path, dir.perm); err != nil && !errors.Is(err, fs.ErrExist) {
return (*EnsureDirError)(wrapError(err,
fmt.Sprintf("cannot create directory '%s': %s", dir.path, err)))
} else {
// only ephemeral dirs require rollback
if dir.remove {
// register partial commit
txp.ensureEphemeral(dir.path, dir.perm)
}
}
}
// write files
for _, file := range tx.files {
verbose.Println("writing", len(file[1]), "bytes of data to", file[0])
if err := os.WriteFile(file[0], []byte(file[1]), 0600); err != nil {
return (*TmpfileError)(wrapError(err,
fmt.Sprintf("cannot write file '%s': %s", file[0], err)))
} else {
// register partial commit
txp.writeFile(file[0], make([]byte, 0)) // data not necessary for revert
}
}
// publish tmpfiles
for _, tmpfile := range tx.tmpfiles {
verbose.Println("publishing tmpfile", tmpfile[0], "from", tmpfile[1])
if err := copyFile(tmpfile[0], tmpfile[1]); err != nil {
return (*TmpfileError)(wrapError(err,
fmt.Sprintf("cannot publish tmpfile '%s' from '%s': %s", tmpfile[0], tmpfile[1], err)))
} else {
// register partial commit
txp.copyFile(tmpfile[0], tmpfile[1])
}
}
// create hardlinks
for _, link := range tx.hardlinks {
verbose.Println("creating hardlink", link[1], "from", link[0])
if err := os.Link(link[0], link[1]); err != nil {
return (*TmpfileError)(wrapError(err,
fmt.Sprintf("cannot create hardlink '%s' from '%s': %s", link[1], link[0], err)))
} else {
// register partial commit
txp.link(link[0], link[1])
}
}
if tx.dbus != nil {
// start dbus proxy
verbose.Printf("session bus proxy on '%s' for upstream '%s'\n", tx.dbusAddr[0][1], tx.dbusAddr[0][0])
if tx.dbusSystem {
verbose.Printf("system bus proxy on '%s' for upstream '%s'\n", tx.dbusAddr[1][1], tx.dbusAddr[1][0])
}
if err := tx.startDBus(); err != nil {
return (*DBusStartError)(wrapError(err, "cannot start message bus proxy:", err))
} else {
txp.dbus = tx.dbus
txp.dbusAddr = tx.dbusAddr
txp.dbusSystem = tx.dbusSystem
txp.dbusWait = tx.dbusWait
}
}
// apply ACLs
for _, e := range tx.acl {
verbose.Println("applying ACL", e, "uid:", tx.Uid, "tag:", e.ts(), "path:", e.path)
if err := acl.UpdatePerm(e.path, tx.uid, e.perms...); err != nil {
return (*ACLUpdateError)(wrapError(err,
fmt.Sprintf("cannot apply ACL to '%s': %s", e.path, err)))
} else {
// register partial commit
txp.updatePermTag(e.tag, e.path, e.perms...)
}
}
// disarm partial commit rollback
txp = nil
return nil
}
// revert rolls back recorded actions
// order: acl, dbus, hardlinks, tmpfiles, files, mkdir, xhost
// errors are printed but not treated as fatal
func (tx *appSealTx) revert(tags *state.Enablements) error {
if tx.closed {
panic("seal transaction reverted twice")
}
tx.closed = true
// will be slightly over-sized with ephemeral dirs
errs := make([]error, 0, len(tx.acl)+1+len(tx.tmpfiles)+len(tx.mkdir)+len(tx.xhost))
joinError := func(err error, a ...any) {
var e error
if err != nil {
e = wrapError(err, a...)
}
errs = append(errs, e)
}
// revert ACLs
for _, e := range tx.acl {
if tags.Has(e.tag) {
verbose.Println("stripping ACL", e, "uid:", tx.Uid, "tag:", e.ts(), "path:", e.path)
err := acl.UpdatePerm(e.path, tx.uid)
joinError(err, fmt.Sprintf("cannot strip ACL entry from '%s': %s", e.path, err))
} else {
verbose.Println("skipping ACL", e, "uid:", tx.Uid, "tag:", e.ts(), "path:", e.path)
}
}
if tx.dbus != nil {
// stop dbus proxy
verbose.Println("terminating message bus proxy")
err := tx.stopDBus()
joinError(err, "cannot stop message bus proxy:", err)
}
// remove hardlinks
for _, link := range tx.hardlinks {
verbose.Println("removing hardlink", link[1])
err := os.Remove(link[1])
joinError(err, fmt.Sprintf("cannot remove hardlink '%s': %s", link[1], err))
}
// remove tmpfiles
for _, tmpfile := range tx.tmpfiles {
verbose.Println("removing tmpfile", tmpfile[0])
err := os.Remove(tmpfile[0])
joinError(err, fmt.Sprintf("cannot remove tmpfile '%s': %s", tmpfile[0], err))
}
// remove files
for _, file := range tx.files {
verbose.Println("removing file", file[0])
err := os.Remove(file[0])
joinError(err, fmt.Sprintf("cannot remove file '%s': %s", file[0], err))
}
// remove (empty) ephemeral directories
for i := len(tx.mkdir); i > 0; i-- {
dir := tx.mkdir[i-1]
if !dir.remove {
continue
}
verbose.Println("destroying ephemeral directory mode:", dir.perm.String(), "path:", dir.path)
err := os.Remove(dir.path)
joinError(err, fmt.Sprintf("cannot remove ephemeral directory '%s': %s", dir.path, err))
}
if tags.Has(state.EnableX) {
// rollback xhost insertions
for _, username := range tx.xhost {
verbose.Printf("deleting XHost entry SI:localuser:%s\n", username)
err := xcb.ChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+username)
joinError(err, "cannot remove XHost entry:", err)
}
}
return errors.Join(errs...)
}
// shareAll calls all share methods in sequence // shareAll calls all share methods in sequence
func (seal *appSeal) shareAll(bus [2]*dbus.Config) error { func (seal *appSeal) shareAll(bus [2]*dbus.Config, os internal.System) error {
if seal.shared { if seal.shared {
panic("seal shared twice") panic("seal shared twice")
} }
seal.shared = true seal.shared = true
seal.shareRuntime()
seal.shareSystem() seal.shareSystem()
targetRuntime := seal.shareRuntimeChild() seal.shareRuntime()
verbose.Printf("child runtime data dir '%s' configured\n", targetRuntime) seal.sharePasswd(os)
if err := seal.shareDisplay(); err != nil { if err := seal.shareDisplay(os); err != nil {
return err return err
} }
if err := seal.sharePulse(); err != nil { if err := seal.sharePulse(os); err != nil {
return err return err
} }
@ -434,12 +53,11 @@ func (seal *appSeal) shareAll(bus [2]*dbus.Config) error {
if err := seal.shareDBus(bus); err != nil { if err := seal.shareDBus(bus); err != nil {
return err return err
} else if seal.sys.dbusAddr != nil { // set if D-Bus enabled and share successful
verbose.Println("sealed session proxy", bus[0].Args(seal.sys.dbusAddr[0]))
if bus[1] != nil {
verbose.Println("sealed system proxy", bus[1].Args(seal.sys.dbusAddr[1]))
} }
verbose.Println("message bus proxy final args:", seal.sys.dbus)
// queue overriding tmpfs at the end of seal.sys.bwrap.Filesystem
for _, dest := range seal.sys.override {
seal.sys.bwrap.Tmpfs(dest, 8*1024)
} }
return nil return nil

View File

@ -1,34 +0,0 @@
package internal
import (
"errors"
"fmt"
"io/fs"
"os"
)
const (
systemdCheckPath = "/run/systemd/system"
)
var SdBootedV = func() bool {
if v, err := SdBooted(); err != nil {
fmt.Println("warn: read systemd marker:", err)
return false
} else {
return v
}
}()
// SdBooted implements https://www.freedesktop.org/software/systemd/man/sd_booted.html
func SdBooted() (bool, error) {
_, err := os.Stat(systemdCheckPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
err = nil
}
return false, err
}
return true, nil
}

View File

@ -1,59 +0,0 @@
package internal
import (
"fmt"
"os"
"path"
"strconv"
"sync"
"git.ophivana.moe/cat/fortify/internal/verbose"
)
// state that remain constant for the lifetime of the process
// fetched and cached here
const (
xdgRuntimeDir = "XDG_RUNTIME_DIR"
)
// SystemConstants contains state from the operating system
type SystemConstants struct {
// path to shared directory e.g. /tmp/fortify.%d
SharePath string `json:"share_path"`
// XDG_RUNTIME_DIR value e.g. /run/user/%d
RuntimePath string `json:"runtime_path"`
// application runtime directory e.g. /run/user/%d/fortify
RunDirPath string `json:"run_dir_path"`
}
var (
scVal SystemConstants
scOnce sync.Once
)
func copySC() {
sc := SystemConstants{
SharePath: path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Geteuid())),
}
verbose.Println("process share directory at", sc.SharePath)
// runtimePath, runDirPath
if r, ok := os.LookupEnv(xdgRuntimeDir); !ok {
fmt.Println("Env variable", xdgRuntimeDir, "unset")
os.Exit(1)
} else {
sc.RuntimePath = r
sc.RunDirPath = path.Join(sc.RuntimePath, "fortify")
verbose.Println("XDG runtime directory at", sc.RunDirPath)
}
scVal = sc
}
// GetSC returns a populated SystemConstants value
func GetSC() SystemConstants {
scOnce.Do(copySC)
return scVal
}

72
internal/fmsg/defer.go Normal file
View File

@ -0,0 +1,72 @@
package fmsg
import (
"os"
"sync"
"sync/atomic"
)
var (
wstate atomic.Bool
withhold = make(chan struct{}, 1)
msgbuf = make(chan dOp, 64) // these ops are tiny so a large buffer is allocated for withholding output
dequeueOnce sync.Once
queueSync sync.WaitGroup
)
func dequeue() {
go func() {
for {
select {
case op := <-msgbuf:
op.Do()
queueSync.Done()
case <-withhold:
<-withhold
}
}
}()
}
type dOp interface{ Do() }
func Exit(code int) {
queueSync.Wait()
os.Exit(code)
}
func Withhold() {
dequeueOnce.Do(dequeue)
if wstate.CompareAndSwap(false, true) {
withhold <- struct{}{}
}
}
func Resume() {
dequeueOnce.Do(dequeue)
if wstate.CompareAndSwap(true, false) {
withhold <- struct{}{}
}
}
type dPrint []any
func (v dPrint) Do() {
std.Print(v...)
}
type dPrintf struct {
format string
v []any
}
func (d *dPrintf) Do() {
std.Printf(d.format, d.v...)
}
type dPrintln []any
func (v dPrintln) Do() {
std.Println(v...)
}

View File

@ -1,4 +1,4 @@
package app package fmsg
import ( import (
"fmt" "fmt"
@ -29,11 +29,32 @@ func (e *BaseError) Message() string {
return e.message return e.message
} }
func wrapError(err error, a ...any) *BaseError { // WrapError wraps an error with a corresponding message.
return &BaseError{ func WrapError(err error, a ...any) error {
message: fmt.Sprintln(a...), if err == nil {
baseError: baseError{err}, return nil
} }
return wrapError(err, fmt.Sprintln(a...))
}
// WrapErrorSuffix wraps an error with a corresponding message with err at the end of the message.
func WrapErrorSuffix(err error, a ...any) error {
if err == nil {
return nil
}
return wrapError(err, fmt.Sprintln(append(a, err)...))
}
// WrapErrorFunc wraps an error with a corresponding message returned by f.
func WrapErrorFunc(err error, f func(err error) string) error {
if err == nil {
return nil
}
return wrapError(err, f(err))
}
func wrapError(err error, message string) *BaseError {
return &BaseError{message, baseError{err}}
} }
var ( var (

43
internal/fmsg/fmsg.go Normal file
View File

@ -0,0 +1,43 @@
// Package fmsg provides various functions for output messages.
package fmsg
import (
"log"
"os"
)
var std = log.New(os.Stderr, "fortify: ", 0)
func SetPrefix(prefix string) {
prefix += ": "
std.SetPrefix(prefix)
std.SetPrefix(prefix)
}
func Print(v ...any) {
dequeueOnce.Do(dequeue)
queueSync.Add(1)
msgbuf <- dPrint(v)
}
func Printf(format string, v ...any) {
dequeueOnce.Do(dequeue)
queueSync.Add(1)
msgbuf <- &dPrintf{format, v}
}
func Println(v ...any) {
dequeueOnce.Do(dequeue)
queueSync.Add(1)
msgbuf <- dPrintln(v)
}
func Fatal(v ...any) {
Print(v...)
Exit(1)
}
func Fatalf(format string, v ...any) {
Printf(format, v...)
Exit(1)
}

25
internal/fmsg/verbose.go Normal file
View File

@ -0,0 +1,25 @@
package fmsg
import "sync/atomic"
var verbose = new(atomic.Bool)
func Verbose() bool {
return verbose.Load()
}
func SetVerbose(v bool) {
verbose.Store(v)
}
func VPrintf(format string, v ...any) {
if verbose.Load() {
Printf(format, v...)
}
}
func VPrintln(v ...any) {
if verbose.Load() {
Println(v...)
}
}

View File

@ -4,64 +4,66 @@ import (
"encoding/gob" "encoding/gob"
"errors" "errors"
"flag" "flag"
"fmt"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"path" "path"
"strconv" "strconv"
"syscall" "syscall"
"time"
"git.ophivana.moe/cat/fortify/internal/verbose" "git.ophivana.moe/security/fortify/internal/fmsg"
)
const (
// time to wait for linger processes after death initial process
residualProcessTimeout = 5 * time.Second
) )
// everything beyond this point runs within pid namespace // everything beyond this point runs within pid namespace
// proceed with caution! // proceed with caution!
func doInit(fd uintptr) { func doInit(fd uintptr) {
fmsg.SetPrefix("init")
// re-exec // re-exec
if len(os.Args) > 0 && os.Args[0] != "fortify" && path.IsAbs(os.Args[0]) { if len(os.Args) > 0 && os.Args[0] != "fortify" && path.IsAbs(os.Args[0]) {
if err := syscall.Exec(os.Args[0], []string{"fortify", "init"}, os.Environ()); err != nil { if err := syscall.Exec(os.Args[0], []string{"fortify", "init"}, os.Environ()); err != nil {
fmt.Println("fortify-init: cannot re-exec self:", err) fmsg.Println("cannot re-exec self:", err)
// continue anyway // continue anyway
} }
} }
verbose.Prefix = "fortify-init:"
var payload Payload var payload Payload
p := os.NewFile(fd, "config-stream") p := os.NewFile(fd, "config-stream")
if p == nil { if p == nil {
fmt.Println("fortify-init: invalid config descriptor") fmsg.Fatal("invalid config descriptor")
os.Exit(1)
} }
if err := gob.NewDecoder(p).Decode(&payload); err != nil { if err := gob.NewDecoder(p).Decode(&payload); err != nil {
fmt.Println("fortify-init: cannot decode init payload:", err) fmsg.Fatal("cannot decode init payload:", err)
os.Exit(1)
} else { } else {
// sharing stdout with parent // sharing stdout with parent
// USE WITH CAUTION // USE WITH CAUTION
verbose.Set(payload.Verbose) fmsg.SetVerbose(payload.Verbose)
// child does not need to see this // child does not need to see this
if err = os.Unsetenv(EnvInit); err != nil { if err = os.Unsetenv(EnvInit); err != nil {
fmt.Println("fortify-init: cannot unset", EnvInit+":", err) fmsg.Println("cannot unset", EnvInit+":", err)
// not fatal // not fatal
} else { } else {
verbose.Println("received configuration") fmsg.VPrintln("received configuration")
} }
} }
// close config fd // close config fd
if err := p.Close(); err != nil { if err := p.Close(); err != nil {
fmt.Println("fortify-init: cannot close config fd:", err) fmsg.Println("cannot close config fd:", err)
// not fatal // not fatal
} }
// die with parent // die with parent
if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGKILL), 0); errno != 0 { if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGKILL), 0); errno != 0 {
fmt.Println("fortify-init: prctl(PR_SET_PDEATHSIG, SIGKILL):", errno.Error()) fmsg.Fatal("prctl(PR_SET_PDEATHSIG, SIGKILL):", errno.Error())
os.Exit(1)
} }
cmd := exec.Command(payload.Argv0) cmd := exec.Command(payload.Argv0)
@ -78,8 +80,7 @@ func doInit(fd uintptr) {
} }
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
fmt.Printf("fortify-init: cannot start %q: %v", payload.Argv0, err) fmsg.Fatalf("cannot start %q: %v", payload.Argv0, err)
os.Exit(1)
} }
sig := make(chan os.Signal, 2) sig := make(chan os.Signal, 2)
@ -115,18 +116,20 @@ func doInit(fd uintptr) {
} }
} }
if !errors.Is(err, syscall.ECHILD) { if !errors.Is(err, syscall.ECHILD) {
fmt.Println("fortify-init: unexpected wait4 response:", err) fmsg.Println("unexpected wait4 response:", err)
} }
close(done) close(done)
}() }()
timeout := make(chan struct{})
r := 2 r := 2
for { for {
select { select {
case s := <-sig: case s := <-sig:
verbose.Println("received", s.String()) fmsg.VPrintln("received", s.String())
os.Exit(0) fmsg.Exit(0)
case w := <-info: case w := <-info:
if w.wpid == cmd.Process.Pid { if w.wpid == cmd.Process.Pid {
switch { switch {
@ -137,9 +140,17 @@ func doInit(fd uintptr) {
default: default:
r = 255 r = 255
} }
go func() {
time.Sleep(residualProcessTimeout)
close(timeout)
}()
} }
case <-done: case <-done:
os.Exit(r) fmsg.Exit(r)
case <-timeout:
fmsg.Println("timeout exceeded waiting for lingering processes")
fmsg.Exit(r)
} }
} }
} }
@ -153,8 +164,7 @@ func Try() {
if args := flag.Args(); len(args) == 1 && args[0] == "init" { if args := flag.Args(); len(args) == 1 && args[0] == "init" {
if s, ok := os.LookupEnv(EnvInit); ok { if s, ok := os.LookupEnv(EnvInit); ok {
if fd, err := strconv.Atoi(s); err != nil { if fd, err := strconv.Atoi(s); err != nil {
fmt.Printf("fortify-init: cannot parse %q: %v", s, err) fmsg.Fatalf("cannot parse %q: %v", s, err)
os.Exit(1)
} else { } else {
doInit(uintptr(fd)) doInit(uintptr(fd))
} }

View File

@ -4,37 +4,36 @@ import (
"encoding/gob" "encoding/gob"
"errors" "errors"
"flag" "flag"
"fmt"
"net" "net"
"os" "os"
"path" "path"
"strconv" "strconv"
"syscall" "syscall"
"git.ophivana.moe/cat/fortify/helper" "git.ophivana.moe/security/fortify/helper"
init0 "git.ophivana.moe/cat/fortify/internal/init" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/cat/fortify/internal/verbose" init0 "git.ophivana.moe/security/fortify/internal/init"
) )
// everything beyond this point runs as target user // everything beyond this point runs as target user
// proceed with caution! // proceed with caution!
func doShim(socket string) { func doShim(socket string) {
fmsg.SetPrefix("shim")
// re-exec // re-exec
if len(os.Args) > 0 && os.Args[0] != "fortify" && path.IsAbs(os.Args[0]) { if len(os.Args) > 0 && os.Args[0] != "fortify" && path.IsAbs(os.Args[0]) {
if err := syscall.Exec(os.Args[0], []string{"fortify", "shim"}, os.Environ()); err != nil { if err := syscall.Exec(os.Args[0], []string{"fortify", "shim"}, os.Environ()); err != nil {
fmt.Println("fortify-shim: cannot re-exec self:", err) fmsg.Println("cannot re-exec self:", err)
// continue anyway // continue anyway
} }
} }
verbose.Prefix = "fortify-shim:"
// dial setup socket // dial setup socket
var conn *net.UnixConn var conn *net.UnixConn
if c, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: socket, Net: "unix"}); err != nil { if c, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: socket, Net: "unix"}); err != nil {
fmt.Println("fortify-shim: cannot dial setup socket:", err) fmsg.Fatal("cannot dial setup socket:", err)
os.Exit(1) panic("unreachable")
} else { } else {
conn = c conn = c
} }
@ -42,25 +41,22 @@ func doShim(socket string) {
// decode payload gob stream // decode payload gob stream
var payload Payload var payload Payload
if err := gob.NewDecoder(conn).Decode(&payload); err != nil { if err := gob.NewDecoder(conn).Decode(&payload); err != nil {
fmt.Println("fortify-shim: cannot decode shim payload:", err) fmsg.Fatal("cannot decode shim payload:", err)
os.Exit(1)
} else { } else {
// sharing stdout with parent // sharing stdout with parent
// USE WITH CAUTION // USE WITH CAUTION
verbose.Set(payload.Verbose) fmsg.SetVerbose(payload.Verbose)
} }
if payload.Bwrap == nil { if payload.Bwrap == nil {
fmt.Println("fortify-shim: bwrap config not supplied") fmsg.Fatal("bwrap config not supplied")
os.Exit(1)
} }
// receive wayland fd over socket // receive wayland fd over socket
wfd := -1 wfd := -1
if payload.WL { if payload.WL {
if fd, err := receiveWLfd(conn); err != nil { if fd, err := receiveWLfd(conn); err != nil {
fmt.Println("fortify-shim: cannot receive wayland fd:", err) fmsg.Fatal("cannot receive wayland fd:", err)
os.Exit(1)
} else { } else {
wfd = fd wfd = fd
} }
@ -68,7 +64,7 @@ func doShim(socket string) {
// close setup socket // close setup socket
if err := conn.Close(); err != nil { if err := conn.Close(); err != nil {
fmt.Println("fortify-shim: cannot close setup socket:", err) fmsg.Println("cannot close setup socket:", err)
// not fatal // not fatal
} }
@ -83,8 +79,7 @@ func doShim(socket string) {
// no argv, look up shell instead // no argv, look up shell instead
var ok bool var ok bool
if ic.Argv0, ok = os.LookupEnv("SHELL"); !ok { if ic.Argv0, ok = os.LookupEnv("SHELL"); !ok {
fmt.Println("fortify-shim: no command was specified and $SHELL was unset") fmsg.Fatal("no command was specified and $SHELL was unset")
os.Exit(1)
} }
ic.Argv = []string{ic.Argv0} ic.Argv = []string{ic.Argv0}
@ -106,46 +101,42 @@ func doShim(socket string) {
// share config pipe // share config pipe
if r, w, err := os.Pipe(); err != nil { if r, w, err := os.Pipe(); err != nil {
fmt.Println("fortify-shim: cannot pipe:", err) fmsg.Fatal("cannot pipe:", err)
os.Exit(1)
} else { } else {
conf.SetEnv[init0.EnvInit] = strconv.Itoa(3 + len(extraFiles)) conf.SetEnv[init0.EnvInit] = strconv.Itoa(3 + len(extraFiles))
extraFiles = append(extraFiles, r) extraFiles = append(extraFiles, r)
verbose.Println("transmitting config to init") fmsg.VPrintln("transmitting config to init")
go func() { go func() {
// stream config to pipe // stream config to pipe
if err = gob.NewEncoder(w).Encode(&ic); err != nil { if err = gob.NewEncoder(w).Encode(&ic); err != nil {
fmt.Println("fortify-shim: cannot transmit init config:", err) fmsg.Fatal("cannot transmit init config:", err)
os.Exit(1)
} }
}() }()
} }
helper.BubblewrapName = payload.Exec[1] // resolved bwrap path by parent helper.BubblewrapName = payload.Exec[1] // resolved bwrap path by parent
if b, err := helper.NewBwrap(conf, nil, payload.Exec[0], func(int, int) []string { return []string{"init"} }); err != nil { if b, err := helper.NewBwrap(conf, nil, payload.Exec[0], func(int, int) []string { return []string{"init"} }); err != nil {
fmt.Println("fortify-shim: malformed sandbox config:", err) fmsg.Fatal("malformed sandbox config:", err)
os.Exit(1)
} else { } else {
cmd := b.Unwrap() cmd := b.Unwrap()
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.ExtraFiles = extraFiles cmd.ExtraFiles = extraFiles
if verbose.Get() { if fmsg.Verbose() {
verbose.Println("bwrap args:", conf.Args()) fmsg.VPrintln("bwrap args:", conf.Args())
} }
// run and pass through exit code // run and pass through exit code
if err = b.Start(); err != nil { if err = b.Start(); err != nil {
fmt.Println("fortify-shim: cannot start target process:", err) fmsg.Fatal("cannot start target process:", err)
os.Exit(1)
} else if err = b.Wait(); err != nil { } else if err = b.Wait(); err != nil {
verbose.Println("wait:", err) fmsg.VPrintln("wait:", err)
} }
if b.Unwrap().ProcessState != nil { if b.Unwrap().ProcessState != nil {
os.Exit(b.Unwrap().ProcessState.ExitCode()) fmsg.Exit(b.Unwrap().ProcessState.ExitCode())
} else { } else {
os.Exit(127) fmsg.Exit(127)
} }
} }
} }

View File

@ -1,82 +1,200 @@
package shim package shim
import ( import (
"encoding/gob"
"errors" "errors"
"fmt"
"net" "net"
"os" "os"
"os/exec"
"sync"
"sync/atomic"
"syscall" "syscall"
"time"
"git.ophivana.moe/cat/fortify/internal/verbose" "git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/internal/fmsg"
) )
// called in the parent process // used by the parent process
func ServeConfig(socket string, payload *Payload, wl string, done chan struct{}) (*net.UnixConn, error) { type Shim struct {
var ws *net.UnixConn // user switcher process
if payload.WL { cmd *exec.Cmd
if f, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: wl, Net: "unix"}); err != nil { // uid of shim target user
return nil, err uid uint32
} else { // whether to check shim pid
verbose.Println("connected to wayland at", wl) checkPid bool
ws = f // user switcher executable path
} executable string
// path to setup socket
socket string
// shim setup abort reason and completion
abort chan error
abortErr atomic.Pointer[error]
abortOnce sync.Once
// wayland mediation, nil if disabled
wl *Wayland
// shim setup payload
payload *Payload
} }
if c, err := net.ListenUnix("unix", &net.UnixAddr{Name: socket, Net: "unix"}); err != nil { func New(executable string, uid uint32, socket string, wl *Wayland, payload *Payload, checkPid bool) *Shim {
return nil, err return &Shim{uid: uid, executable: executable, socket: socket, wl: wl, payload: payload, checkPid: checkPid}
}
func (s *Shim) String() string {
if s.cmd == nil {
return "(unused shim manager)"
}
return s.cmd.String()
}
func (s *Shim) Unwrap() *exec.Cmd {
return s.cmd
}
func (s *Shim) Abort(err error) {
s.abortOnce.Do(func() {
s.abortErr.Store(&err)
// s.abort is buffered so this will never block
s.abort <- err
})
}
func (s *Shim) AbortWait(err error) {
s.Abort(err)
<-s.abort
}
type CommandBuilder func(shimEnv string) (args []string)
func (s *Shim) Start(f CommandBuilder) (*time.Time, error) {
var (
cf chan *net.UnixConn
accept func()
)
// listen on setup socket
if c, a, err := s.serve(); err != nil {
return nil, fmsg.WrapErrorSuffix(err,
"cannot listen on shim setup socket:")
} else { } else {
verbose.Println("configuring shim on socket", socket) // accepts a connection after each call to accept
if err = os.Chmod(socket, 0777); err != nil { // connections are sent to the channel cf
fmt.Println("fortify: cannot change permissions of shim setup socket:", err) cf, accept = c, a
}
// start user switcher process and save time
s.cmd = exec.Command(s.executable, f(EnvShim+"="+s.socket)...)
s.cmd.Env = []string{}
s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
s.cmd.Dir = "/"
fmsg.VPrintln("starting shim via user switcher:", s.cmd)
fmsg.Withhold() // withhold messages to stderr
if err := s.cmd.Start(); err != nil {
return nil, fmsg.WrapErrorSuffix(err,
"cannot start user switcher:")
}
startTime := time.Now().UTC()
// kill shim if something goes wrong and an error is returned
killShim := func() {
if err := s.cmd.Process.Signal(os.Interrupt); err != nil {
fmsg.Println("cannot terminate shim on faulted setup:", err)
}
}
defer func() { killShim() }()
accept()
conn := <-cf
if conn == nil {
return &startTime, fmsg.WrapErrorSuffix(*s.abortErr.Load(), "cannot accept call on setup socket:")
}
// authenticate against called provided uid and shim pid
if cred, err := peerCred(conn); err != nil {
return &startTime, fmsg.WrapErrorSuffix(*s.abortErr.Load(), "cannot retrieve shim credentials:")
} else if cred.Uid != s.uid {
fmsg.Printf("process %d owned by user %d tried to connect, expecting %d",
cred.Pid, cred.Uid, s.uid)
err = errors.New("compromised fortify build")
s.Abort(err)
return &startTime, err
} else if s.checkPid && cred.Pid != int32(s.cmd.Process.Pid) {
fmsg.Printf("process %d tried to connect to shim setup socket, expecting shim %d",
cred.Pid, s.cmd.Process.Pid)
err = errors.New("compromised target user")
s.Abort(err)
return &startTime, err
}
// serve payload and wayland fd if enabled
// this also closes the connection
err := s.payload.serve(conn, s.wl)
if err == nil {
killShim = func() {}
}
s.Abort(err) // aborting with nil indicates success
return &startTime, err
}
func (s *Shim) serve() (chan *net.UnixConn, func(), error) {
if s.abort != nil {
panic("attempted to serve shim setup twice")
}
s.abort = make(chan error, 1)
cf := make(chan *net.UnixConn)
accept := make(chan struct{}, 1)
if l, err := net.ListenUnix("unix", &net.UnixAddr{Name: s.socket, Net: "unix"}); err != nil {
return nil, nil, err
} else {
l.SetUnlinkOnClose(true)
fmsg.VPrintf("listening on shim setup socket %q", s.socket)
if err = acl.UpdatePerm(s.socket, int(s.uid), acl.Read, acl.Write, acl.Execute); err != nil {
fmsg.Println("cannot append ACL entry to shim setup socket:", err)
s.Abort(err) // ensures setup socket cleanup
} }
go func() { go func() {
var conn *net.UnixConn for {
if conn, err = c.AcceptUnix(); err != nil { select {
fmt.Println("fortify: cannot accept connection from shim:", err) case err = <-s.abort:
} else { if err != nil {
if err = gob.NewEncoder(conn).Encode(*payload); err != nil { fmsg.VPrintln("aborting shim setup, reason:", err)
fmt.Println("fortify: cannot stream shim payload:", err) }
return if err = l.Close(); err != nil {
fmsg.Println("cannot close setup socket:", err)
}
close(s.abort)
close(cf)
return
case <-accept:
if conn, err0 := l.AcceptUnix(); err0 != nil {
s.Abort(err0) // does not block, breaks loop
cf <- nil // receiver sees nil value and loads err0 stored during abort
} else {
cf <- conn
} }
if payload.WL {
// get raw connection
var rc syscall.RawConn
if rc, err = ws.SyscallConn(); err != nil {
fmt.Println("fortify: cannot obtain raw wayland connection:", err)
return
} else {
go func() {
// pass wayland socket fd
if err = rc.Control(func(fd uintptr) {
if _, _, err = conn.WriteMsgUnix(nil, syscall.UnixRights(int(fd)), nil); err != nil {
fmt.Println("fortify: cannot pass wayland connection to shim:", err)
return
} }
_ = conn.Close()
// block until shim exits
<-done
verbose.Println("releasing wayland connection")
}); err != nil {
fmt.Println("fortify: cannot obtain wayland connection fd:", err)
} }
}() }()
} }
} else {
_ = conn.Close() return cf, func() { accept <- struct{}{} }, nil
} }
// peerCred fetches peer credentials of conn
func peerCred(conn *net.UnixConn) (ucred *syscall.Ucred, err error) {
var raw syscall.RawConn
if raw, err = conn.SyscallConn(); err != nil {
return
} }
if err = c.Close(); err != nil {
fmt.Println("fortify: cannot close shim socket:", err) err0 := raw.Control(func(fd uintptr) {
} ucred, err = syscall.GetsockoptUcred(int(fd), syscall.SOL_SOCKET, syscall.SO_PEERCRED)
if err = os.Remove(socket); err != nil && !errors.Is(err, os.ErrNotExist) { })
fmt.Println("fortify: cannot remove dangling shim socket:", err) err = errors.Join(err, err0)
} return
}()
return ws, nil
}
} }

View File

@ -1,6 +1,13 @@
package shim package shim
import "git.ophivana.moe/cat/fortify/helper/bwrap" import (
"encoding/gob"
"errors"
"net"
"git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
const EnvShim = "FORTIFY_SHIM" const EnvShim = "FORTIFY_SHIM"
@ -17,3 +24,19 @@ type Payload struct {
// verbosity pass through // verbosity pass through
Verbose bool Verbose bool
} }
func (p *Payload) serve(conn *net.UnixConn, wl *Wayland) error {
if err := gob.NewEncoder(conn).Encode(*p); err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot stream shim payload:")
}
if wl != nil {
if err := wl.WriteUnix(conn); err != nil {
return errors.Join(err, conn.Close())
}
}
return fmsg.WrapErrorSuffix(conn.Close(),
"cannot close setup connection:")
}

75
internal/shim/wayland.go Normal file
View File

@ -0,0 +1,75 @@
package shim
import (
"fmt"
"net"
"sync"
"syscall"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
// Wayland implements wayland mediation.
type Wayland struct {
// wayland socket path
Path string
// wayland connection
conn *net.UnixConn
connErr error
sync.Once
// wait for wayland client to exit
done chan struct{}
}
func (wl *Wayland) WriteUnix(conn *net.UnixConn) error {
// connect to host wayland socket
if f, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: wl.Path, Net: "unix"}); err != nil {
return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot connect to wayland at %q:", wl.Path))
} else {
fmsg.VPrintf("connected to wayland at %q", wl.Path)
wl.conn = f
}
// set up for passing wayland socket
if rc, err := wl.conn.SyscallConn(); err != nil {
return fmsg.WrapErrorSuffix(err, "cannot obtain raw wayland connection:")
} else {
ec := make(chan error)
go func() {
// pass wayland connection fd
if err = rc.Control(func(fd uintptr) {
if _, _, err = conn.WriteMsgUnix(nil, syscall.UnixRights(int(fd)), nil); err != nil {
ec <- fmsg.WrapErrorSuffix(err, "cannot pass wayland connection to shim:")
return
}
ec <- nil
// block until shim exits
<-wl.done
fmsg.VPrintln("releasing wayland connection")
}); err != nil {
ec <- fmsg.WrapErrorSuffix(err, "cannot obtain wayland connection fd:")
return
}
}()
return <-ec
}
}
func (wl *Wayland) Close() error {
wl.Do(func() {
close(wl.done)
wl.connErr = wl.conn.Close()
})
return wl.connErr
}
func NewWayland() *Wayland {
wl := new(Wayland)
wl.done = make(chan struct{})
return wl
}

View File

@ -10,7 +10,8 @@ import (
"text/tabwriter" "text/tabwriter"
"time" "time"
"git.ophivana.moe/cat/fortify/internal/verbose" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/system"
) )
// MustPrintLauncherStateSimpleGlobal prints active launcher states of all simple stores // MustPrintLauncherStateSimpleGlobal prints active launcher states of all simple stores
@ -20,19 +21,18 @@ func MustPrintLauncherStateSimpleGlobal(w **tabwriter.Writer, runDir string) {
// read runtime directory to get all UIDs // read runtime directory to get all UIDs
if dirs, err := os.ReadDir(path.Join(runDir, "state")); err != nil && !errors.Is(err, os.ErrNotExist) { if dirs, err := os.ReadDir(path.Join(runDir, "state")); err != nil && !errors.Is(err, os.ErrNotExist) {
fmt.Println("cannot read runtime directory:", err) fmsg.Fatal("cannot read runtime directory:", err)
os.Exit(1)
} else { } else {
for _, e := range dirs { for _, e := range dirs {
// skip non-directories // skip non-directories
if !e.IsDir() { if !e.IsDir() {
verbose.Println("skipped non-directory entry", e.Name()) fmsg.VPrintf("skipped non-directory entry %q", e.Name())
continue continue
} }
// skip non-numerical names // skip non-numerical names
if _, err = strconv.Atoi(e.Name()); err != nil { if _, err = strconv.Atoi(e.Name()); err != nil {
verbose.Println("skipped non-uid entry", e.Name()) fmsg.VPrintf("skipped non-uid entry %q", e.Name())
continue continue
} }
@ -44,7 +44,7 @@ func MustPrintLauncherStateSimpleGlobal(w **tabwriter.Writer, runDir string) {
// mustPrintLauncherState causes store activity so store needs to be closed // mustPrintLauncherState causes store activity so store needs to be closed
if err = s.Close(); err != nil { if err = s.Close(); err != nil {
fmt.Printf("warn: error closing store for user %s: %s\n", e.Name(), err) fmsg.Printf("cannot close store for user %q: %s", e.Name(), err)
} }
} }
} }
@ -66,8 +66,8 @@ func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time
*w = tabwriter.NewWriter(os.Stdout, 0, 1, 4, ' ', 0) *w = tabwriter.NewWriter(os.Stdout, 0, 1, 4, ' ', 0)
// write header when initialising // write header when initialising
if !verbose.Get() { if !fmsg.Verbose() {
_, _ = fmt.Fprintln(*w, "\tUID\tPID\tUptime\tEnablements\tLauncher\tCommand") _, _ = fmt.Fprintln(*w, "\tUID\tPID\tUptime\tEnablements\tMethod\tCommand")
} else { } else {
// argv is emitted in body when verbose // argv is emitted in body when verbose
_, _ = fmt.Fprintln(*w, "\tUID\tPID\tArgv") _, _ = fmt.Fprintln(*w, "\tUID\tPID\tArgv")
@ -85,7 +85,7 @@ func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time
// build enablements string // build enablements string
ets := strings.Builder{} ets := strings.Builder{}
// append enablement strings in order // append enablement strings in order
for i := Enablement(0); i < EnableLength; i++ { for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
if state.Capability.Has(i) { if state.Capability.Has(i) {
ets.WriteString(", " + i.String()) ets.WriteString(", " + i.String())
} }
@ -95,9 +95,9 @@ func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time
ets.WriteString("(No enablements)") ets.WriteString("(No enablements)")
} }
if !verbose.Get() { if !fmsg.Verbose() {
_, _ = fmt.Fprintf(*w, "\t%s\t%d\t%s\t%s\t%s\t%s\n", _, _ = fmt.Fprintf(*w, "\t%s\t%d\t%s\t%s\t%s\t%s\n",
s.path[len(s.path)-1], state.PID, now.Sub(state.Time).Round(time.Second).String(), strings.TrimPrefix(ets.String(), ", "), state.Launcher, s.path[len(s.path)-1], state.PID, now.Sub(state.Time).Round(time.Second).String(), strings.TrimPrefix(ets.String(), ", "), state.Method,
state.Command) state.Command)
} else { } else {
// emit argv instead when verbose // emit argv instead when verbose
@ -109,15 +109,13 @@ func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time
return nil return nil
}() }()
}); err != nil { }); err != nil {
fmt.Printf("cannot perform action on store '%s': %s\n", path.Join(s.path...), err) fmsg.Printf("cannot perform action on store %q: %s", path.Join(s.path...), err)
if !ok { if !ok {
fmt.Println("warn: store faulted before printing") fmsg.Fatal("store faulted before printing")
os.Exit(1)
} }
} }
if innerErr != nil { if innerErr != nil {
fmt.Printf("cannot print launcher state for store '%s': %s\n", path.Join(s.path...), innerErr) fmsg.Fatalf("cannot print launcher state for store %q: %s", path.Join(s.path...), innerErr)
os.Exit(1)
} }
} }

View File

@ -2,6 +2,8 @@ package state
import ( import (
"time" "time"
"git.ophivana.moe/security/fortify/internal/system"
) )
type Store interface { type Store interface {
@ -29,10 +31,10 @@ type State struct {
// command used to seal the app // command used to seal the app
Command []string Command []string
// capability enablements applied to child // capability enablements applied to child
Capability Enablements Capability system.Enablements
// resolved launcher path // user switch method
Launcher string Method string
// full argv whe launching // full argv whe launching
Argv []string Argv []string
// process start time // process start time

126
internal/system.go Normal file
View File

@ -0,0 +1,126 @@
package internal
import (
"errors"
"io/fs"
"os"
"os/exec"
"os/user"
"path"
"strconv"
"sync"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
// System provides safe access to operating system resources.
type System interface {
// Geteuid provides [os.Geteuid].
Geteuid() int
// LookupEnv provides [os.LookupEnv].
LookupEnv(key string) (string, bool)
// TempDir provides [os.TempDir].
TempDir() string
// LookPath provides [exec.LookPath].
LookPath(file string) (string, error)
// Executable provides [os.Executable].
Executable() (string, error)
// Lookup provides [user.Lookup].
Lookup(username string) (*user.User, error)
// ReadDir provides [os.ReadDir].
ReadDir(name string) ([]fs.DirEntry, error)
// Stat provides [os.Stat].
Stat(name string) (fs.FileInfo, error)
// Open provides [os.Open]
Open(name string) (fs.File, error)
// Exit provides [os.Exit].
Exit(code int)
// Paths returns a populated [Paths] struct.
Paths() Paths
// SdBooted implements https://www.freedesktop.org/software/systemd/man/sd_booted.html
SdBooted() bool
}
// Paths contains environment dependent paths used by fortify.
type Paths struct {
// path to shared directory e.g. /tmp/fortify.%d
SharePath string `json:"share_path"`
// XDG_RUNTIME_DIR value e.g. /run/user/%d
RuntimePath string `json:"runtime_path"`
// application runtime directory e.g. /run/user/%d/fortify
RunDirPath string `json:"run_dir_path"`
}
// CopyPaths is a generic implementation of [System.Paths].
func CopyPaths(os System, v *Paths) {
v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Geteuid()))
fmsg.VPrintf("process share directory at %q", v.SharePath)
if r, ok := os.LookupEnv(xdgRuntimeDir); !ok || r == "" || !path.IsAbs(r) {
// fall back to path in share since fortify has no hard XDG dependency
v.RunDirPath = path.Join(v.SharePath, "run")
v.RuntimePath = path.Join(v.RunDirPath, "compat")
} else {
v.RuntimePath = r
v.RunDirPath = path.Join(v.RuntimePath, "fortify")
}
fmsg.VPrintf("runtime directory at %q", v.RunDirPath)
}
// Std implements System using the standard library.
type Std struct {
paths Paths
pathsOnce sync.Once
sdBooted bool
sdBootedOnce sync.Once
}
func (s *Std) Geteuid() int { return os.Geteuid() }
func (s *Std) LookupEnv(key string) (string, bool) { return os.LookupEnv(key) }
func (s *Std) TempDir() string { return os.TempDir() }
func (s *Std) LookPath(file string) (string, error) { return exec.LookPath(file) }
func (s *Std) Executable() (string, error) { return os.Executable() }
func (s *Std) Lookup(username string) (*user.User, error) { return user.Lookup(username) }
func (s *Std) ReadDir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) }
func (s *Std) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) }
func (s *Std) Open(name string) (fs.File, error) { return os.Open(name) }
func (s *Std) Exit(code int) { fmsg.Exit(code) }
const xdgRuntimeDir = "XDG_RUNTIME_DIR"
func (s *Std) Paths() Paths {
s.pathsOnce.Do(func() { CopyPaths(s, &s.paths) })
return s.paths
}
func (s *Std) SdBooted() bool {
s.sdBootedOnce.Do(func() { s.sdBooted = copySdBooted() })
return s.sdBooted
}
const systemdCheckPath = "/run/systemd/system"
func copySdBooted() bool {
if v, err := sdBooted(); err != nil {
fmsg.Println("cannot read systemd marker:", err)
return false
} else {
return v
}
}
func sdBooted() (bool, error) {
_, err := os.Stat(systemdCheckPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
err = nil
}
return false, err
}
return true, nil
}

70
internal/system/acl.go Normal file
View File

@ -0,0 +1,70 @@
package system
import (
"fmt"
"slices"
"git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
// UpdatePerm appends an ephemeral acl update Op.
func (sys *I) UpdatePerm(path string, perms ...acl.Perm) *I {
sys.UpdatePermType(Process, path, perms...)
return sys
}
// UpdatePermType appends an acl update Op.
func (sys *I) UpdatePermType(et Enablement, path string, perms ...acl.Perm) *I {
sys.lock.Lock()
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &ACL{et, path, perms})
return sys
}
type ACL struct {
et Enablement
path string
perms acl.Perms
}
func (a *ACL) Type() Enablement {
return a.et
}
func (a *ACL) apply(sys *I) error {
fmsg.VPrintln("applying ACL", a)
return fmsg.WrapErrorSuffix(acl.UpdatePerm(a.path, sys.uid, a.perms...),
fmt.Sprintf("cannot apply ACL entry to %q:", a.path))
}
func (a *ACL) revert(sys *I, ec *Criteria) error {
if ec.hasType(a) {
fmsg.VPrintln("stripping ACL", a)
return fmsg.WrapErrorSuffix(acl.UpdatePerm(a.path, sys.uid),
fmt.Sprintf("cannot strip ACL entry from %q:", a.path))
} else {
fmsg.VPrintln("skipping ACL", a)
return nil
}
}
func (a *ACL) Is(o Op) bool {
a0, ok := o.(*ACL)
return ok && a0 != nil &&
a.et == a0.et &&
a.path == a0.path &&
slices.Equal(a.perms, a0.perms)
}
func (a *ACL) Path() string {
return a.path
}
func (a *ACL) String() string {
return fmt.Sprintf("%s type: %s path: %q",
a.perms, TypeString(a.et), a.path)
}

View File

@ -0,0 +1,90 @@
package system
import (
"testing"
"git.ophivana.moe/security/fortify/acl"
)
func TestUpdatePerm(t *testing.T) {
testCases := []struct {
path string
perms []acl.Perm
}{
{"/run/user/1971/fortify", []acl.Perm{acl.Execute}},
{"/tmp/fortify.1971/tmpdir/150", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
}
for _, tc := range testCases {
t.Run(tc.path+permSubTestSuffix(tc.perms), func(t *testing.T) {
sys := New(150)
sys.UpdatePerm(tc.path, tc.perms...)
(&tcOp{Process, tc.path}).test(t, sys.ops, []Op{&ACL{Process, tc.path, tc.perms}}, "UpdatePerm")
})
}
}
func TestUpdatePermType(t *testing.T) {
testCases := []struct {
perms []acl.Perm
tcOp
}{
{[]acl.Perm{acl.Execute}, tcOp{User, "/tmp/fortify.1971/tmpdir"}},
{[]acl.Perm{acl.Read, acl.Write, acl.Execute}, tcOp{User, "/tmp/fortify.1971/tmpdir/150"}},
{[]acl.Perm{acl.Execute}, tcOp{Process, "/run/user/1971/fortify/fcb8a12f7c482d183ade8288c3de78b5"}},
{[]acl.Perm{acl.Read}, tcOp{Process, "/tmp/fortify.1971/fcb8a12f7c482d183ade8288c3de78b5/passwd"}},
{[]acl.Perm{acl.Read}, tcOp{Process, "/tmp/fortify.1971/fcb8a12f7c482d183ade8288c3de78b5/group"}},
{[]acl.Perm{acl.Read, acl.Write, acl.Execute}, tcOp{EWayland, "/run/user/1971/wayland-0"}},
}
for _, tc := range testCases {
t.Run(tc.path+"_"+TypeString(tc.et)+permSubTestSuffix(tc.perms), func(t *testing.T) {
sys := New(150)
sys.UpdatePermType(tc.et, tc.path, tc.perms...)
tc.test(t, sys.ops, []Op{&ACL{tc.et, tc.path, tc.perms}}, "UpdatePermType")
})
}
}
func TestACL_String(t *testing.T) {
testCases := []struct {
want string
et Enablement
perms []acl.Perm
}{
{`--- type: Process path: "/nonexistent"`, Process, []acl.Perm{}},
{`r-- type: User path: "/nonexistent"`, User, []acl.Perm{acl.Read}},
{`-w- type: Wayland path: "/nonexistent"`, EWayland, []acl.Perm{acl.Write}},
{`--x type: X11 path: "/nonexistent"`, EX11, []acl.Perm{acl.Execute}},
{`rw- type: D-Bus path: "/nonexistent"`, EDBus, []acl.Perm{acl.Read, acl.Write}},
{`r-x type: PulseAudio path: "/nonexistent"`, EPulse, []acl.Perm{acl.Read, acl.Execute}},
{`rwx type: User path: "/nonexistent"`, User, []acl.Perm{acl.Read, acl.Write, acl.Execute}},
{`rwx type: Process path: "/nonexistent"`, Process, []acl.Perm{acl.Read, acl.Write, acl.Write, acl.Execute}},
}
for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) {
a := &ACL{et: tc.et, perms: tc.perms, path: "/nonexistent"}
if got := a.String(); got != tc.want {
t.Errorf("String() = %v, want %v",
got, tc.want)
}
})
}
}
func permSubTestSuffix(perms []acl.Perm) (suffix string) {
for _, perm := range perms {
switch perm {
case acl.Read:
suffix += "_read"
case acl.Write:
suffix += "_write"
case acl.Execute:
suffix += "_execute"
default:
panic("unreachable")
}
}
return
}

166
internal/system/dbus.go Normal file
View File

@ -0,0 +1,166 @@
package system
import (
"errors"
"os"
"git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
var (
ErrDBusConfig = errors.New("dbus config not supplied")
)
func (sys *I) MustProxyDBus(sessionPath string, session *dbus.Config, systemPath string, system *dbus.Config) *I {
if err := sys.ProxyDBus(session, system, sessionPath, systemPath); err != nil {
panic(err.Error())
} else {
return sys
}
}
func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath string) error {
d := new(DBus)
// used by waiting goroutine to notify process exit
d.done = make(chan struct{})
// session bus is mandatory
if session == nil {
return fmsg.WrapError(ErrDBusConfig,
"attempted to seal message bus proxy without session bus config")
}
// system bus is optional
d.system = system == nil
// upstream address, downstream socket path
var sessionBus, systemBus [2]string
// resolve upstream bus addresses
sessionBus[0], systemBus[0] = dbus.Address()
// set paths from caller
sessionBus[1], systemBus[1] = sessionPath, systemPath
// create proxy instance
d.proxy = dbus.New(sessionBus, systemBus)
defer func() {
if fmsg.Verbose() && d.proxy.Sealed() {
fmsg.VPrintln("sealed session proxy", session.Args(sessionBus))
if system != nil {
fmsg.VPrintln("sealed system proxy", system.Args(systemBus))
}
fmsg.VPrintln("message bus proxy final args:", d.proxy)
}
}()
// queue operation
sys.ops = append(sys.ops, d)
// seal dbus proxy
return fmsg.WrapErrorSuffix(d.proxy.Seal(session, system),
"cannot seal message bus proxy:")
}
type DBus struct {
proxy *dbus.Proxy
// whether system bus proxy is enabled
system bool
// notification from goroutine waiting for dbus.Proxy
done chan struct{}
}
func (d *DBus) Type() Enablement {
return Process
}
func (d *DBus) apply(_ *I) error {
fmsg.VPrintf("session bus proxy on %q for upstream %q", d.proxy.Session()[1], d.proxy.Session()[0])
if d.system {
fmsg.VPrintf("system bus proxy on %q for upstream %q", d.proxy.System()[1], d.proxy.System()[0])
}
// ready channel passed to dbus package
ready := make(chan error, 1)
// background dbus proxy start
if err := d.proxy.Start(ready, os.Stderr, true); err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot start message bus proxy:")
}
fmsg.VPrintln("starting message bus proxy:", d.proxy)
if fmsg.Verbose() { // save the extra bwrap arg build when verbose logging is off
fmsg.VPrintln("message bus proxy bwrap args:", d.proxy.Bwrap())
}
// background wait for proxy instance and notify completion
go func() {
if err := d.proxy.Wait(); err != nil {
fmsg.Println("message bus proxy exited with error:", err)
go func() { ready <- err }()
} else {
fmsg.VPrintln("message bus proxy exit")
}
// ensure socket removal so ephemeral directory is empty at revert
if err := os.Remove(d.proxy.Session()[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
fmsg.Println("cannot remove dangling session bus socket:", err)
}
if d.system {
if err := os.Remove(d.proxy.System()[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
fmsg.Println("cannot remove dangling system bus socket:", err)
}
}
// notify proxy completion
close(d.done)
}()
// ready is not nil if the proxy process faulted
if err := <-ready; err != nil {
// note that err here is either an I/O error or a predetermined unexpected behaviour error
return fmsg.WrapErrorSuffix(err,
"message bus proxy fault after start:")
}
fmsg.VPrintln("message bus proxy ready")
return nil
}
func (d *DBus) revert(_ *I, _ *Criteria) error {
// criteria ignored here since dbus is always process-scoped
fmsg.VPrintln("terminating message bus proxy")
if err := d.proxy.Close(); err != nil {
if errors.Is(err, os.ErrClosed) {
return fmsg.WrapError(err,
"message bus proxy already closed")
} else {
return fmsg.WrapErrorSuffix(err,
"cannot stop message bus proxy:")
}
}
// block until proxy wait returns
<-d.done
return nil
}
func (d *DBus) Is(o Op) bool {
d0, ok := o.(*DBus)
return ok && d0 != nil &&
((d.proxy == nil && d0.proxy == nil) ||
(d.proxy != nil && d0.proxy != nil && d.proxy.String() == d0.proxy.String()))
}
func (d *DBus) Path() string {
return "(dbus proxy)"
}
func (d *DBus) String() string {
return d.proxy.String()
}

View File

@ -1,4 +1,4 @@
package state package system
type ( type (
// Enablement represents an optional system resource // Enablement represents an optional system resource
@ -8,22 +8,25 @@ type (
) )
const ( const (
EnableWayland Enablement = iota EWayland Enablement = iota
EnableX EX11
EnableDBus EDBus
EnablePulse EPulse
EnableLength
) )
var enablementString = [EnableLength]string{ var enablementString = [...]string{
"Wayland", EWayland: "Wayland",
"X11", EX11: "X11",
"D-Bus", EDBus: "D-Bus",
"PulseAudio", EPulse: "PulseAudio",
} }
const ELen = len(enablementString)
func (e Enablement) String() string { func (e Enablement) String() string {
if int(e) >= ELen {
return "<invalid enablement>"
}
return enablementString[e] return enablementString[e]
} }

88
internal/system/mkdir.go Normal file
View File

@ -0,0 +1,88 @@
package system
import (
"errors"
"fmt"
"os"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
// Ensure the existence and mode of a directory.
func (sys *I) Ensure(name string, perm os.FileMode) *I {
sys.lock.Lock()
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &Mkdir{User, name, perm, false})
return sys
}
// Ephemeral ensures the temporary existence and mode of a directory through the life of et.
func (sys *I) Ephemeral(et Enablement, name string, perm os.FileMode) *I {
sys.lock.Lock()
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &Mkdir{et, name, perm, true})
return sys
}
type Mkdir struct {
et Enablement
path string
perm os.FileMode
ephemeral bool
}
func (m *Mkdir) Type() Enablement {
return m.et
}
func (m *Mkdir) apply(_ *I) error {
fmsg.VPrintln("ensuring directory", m)
// create directory
err := os.Mkdir(m.path, m.perm)
if !errors.Is(err, os.ErrExist) {
return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot create directory %q:", m.path))
}
// directory exists, ensure mode
return fmsg.WrapErrorSuffix(os.Chmod(m.path, m.perm),
fmt.Sprintf("cannot change mode of %q to %s:", m.path, m.perm))
}
func (m *Mkdir) revert(_ *I, ec *Criteria) error {
if !m.ephemeral {
// skip non-ephemeral dir and do not log anything
return nil
}
if ec.hasType(m) {
fmsg.VPrintln("destroying ephemeral directory", m)
return fmsg.WrapErrorSuffix(os.Remove(m.path),
fmt.Sprintf("cannot remove ephemeral directory %q:", m.path))
} else {
fmsg.VPrintln("skipping ephemeral directory", m)
return nil
}
}
func (m *Mkdir) Is(o Op) bool {
m0, ok := o.(*Mkdir)
return ok && m0 != nil && *m == *m0
}
func (m *Mkdir) Path() string {
return m.path
}
func (m *Mkdir) String() string {
t := "Ensure"
if m.ephemeral {
t = TypeString(m.Type())
}
return fmt.Sprintf("mode: %s type: %s path: %q", m.perm.String(), t, m.path)
}

View File

@ -0,0 +1,73 @@
package system
import (
"os"
"testing"
)
func TestEnsure(t *testing.T) {
testCases := []struct {
name string
perm os.FileMode
}{
{"/tmp/fortify.1971", 0701},
{"/tmp/fortify.1971/tmpdir", 0700},
{"/tmp/fortify.1971/tmpdir/150", 0700},
{"/run/user/1971/fortify", 0700},
}
for _, tc := range testCases {
t.Run(tc.name+"_"+tc.perm.String(), func(t *testing.T) {
sys := New(150)
sys.Ensure(tc.name, tc.perm)
(&tcOp{User, tc.name}).test(t, sys.ops, []Op{&Mkdir{User, tc.name, tc.perm, false}}, "Ensure")
})
}
}
func TestEphemeral(t *testing.T) {
testCases := []struct {
perm os.FileMode
tcOp
}{
{0700, tcOp{Process, "/run/user/1971/fortify/ec07546a772a07cde87389afc84ffd13"}},
{0701, tcOp{Process, "/tmp/fortify.1971/ec07546a772a07cde87389afc84ffd13"}},
}
for _, tc := range testCases {
t.Run(tc.path+"_"+tc.perm.String()+"_"+TypeString(tc.et), func(t *testing.T) {
sys := New(150)
sys.Ephemeral(tc.et, tc.path, tc.perm)
tc.test(t, sys.ops, []Op{&Mkdir{tc.et, tc.path, tc.perm, true}}, "Ephemeral")
})
}
}
func TestMkdir_String(t *testing.T) {
testCases := []struct {
want string
ephemeral bool
et Enablement
}{
{"Ensure", false, User},
{"Ensure", false, Process},
{"Ensure", false, EWayland},
{"Wayland", true, EWayland},
{"X11", true, EX11},
{"D-Bus", true, EDBus},
{"PulseAudio", true, EPulse},
}
for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) {
m := &Mkdir{
et: tc.et,
path: "/nonexistent",
perm: 0701,
ephemeral: tc.ephemeral,
}
want := "mode: " + os.FileMode(0701).String() + " type: " + tc.want + " path: \"/nonexistent\""
if got := m.String(); got != want {
t.Errorf("String() = %v, want %v", got, want)
}
})
}
}

140
internal/system/op.go Normal file
View File

@ -0,0 +1,140 @@
package system
import (
"errors"
"sync"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
const (
// User type is reverted at final launcher exit.
User = Enablement(ELen)
// Process type is unconditionally reverted on exit.
Process = Enablement(ELen + 1)
)
type Criteria struct {
*Enablements
}
func (ec *Criteria) hasType(o Op) bool {
// nil criteria: revert everything except User
if ec.Enablements == nil {
return o.Type() != User
}
return ec.Has(o.Type())
}
// Op is a reversible system operation.
type Op interface {
// Type returns Op's enablement type.
Type() Enablement
// apply the Op
apply(sys *I) error
// revert reverses the Op if criteria is met
revert(sys *I, ec *Criteria) error
Is(o Op) bool
Path() string
String() string
}
func TypeString(e Enablement) string {
switch e {
case User:
return "User"
case Process:
return "Process"
default:
return e.String()
}
}
type I struct {
uid int
ops []Op
state [2]bool
lock sync.Mutex
}
func (sys *I) UID() int {
return sys.uid
}
func (sys *I) Equal(v *I) bool {
if v == nil || sys.uid != v.uid || len(sys.ops) != len(v.ops) {
return false
}
for i, o := range sys.ops {
if !o.Is(v.ops[i]) {
return false
}
}
return true
}
func (sys *I) Commit() error {
sys.lock.Lock()
defer sys.lock.Unlock()
if sys.state[0] {
panic("sys instance committed twice")
}
sys.state[0] = true
sp := New(sys.uid)
sp.ops = make([]Op, 0, len(sys.ops)) // prevent copies during commits
defer func() {
// sp is set to nil when all ops are applied
if sp != nil {
// rollback partial commit
fmsg.VPrintf("commit faulted after %d ops, rolling back partial commit", len(sp.ops))
if err := sp.Revert(&Criteria{nil}); err != nil {
fmsg.Println("errors returned reverting partial commit:", err)
}
}
}()
for _, o := range sys.ops {
if err := o.apply(sys); err != nil {
return err
} else {
// register partial commit
sp.ops = append(sp.ops, o)
}
}
// disarm partial commit rollback
sp = nil
return nil
}
func (sys *I) Revert(ec *Criteria) error {
sys.lock.Lock()
defer sys.lock.Unlock()
if sys.state[1] {
panic("sys instance reverted twice")
}
sys.state[1] = true
// collect errors
errs := make([]error, len(sys.ops))
for i := range sys.ops {
errs[i] = sys.ops[len(sys.ops)-i-1].revert(sys, ec)
}
// errors.Join filters nils
return errors.Join(errs...)
}
func New(uid int) *I {
return &I{uid: uid}
}

View File

@ -0,0 +1,79 @@
package system
import "testing"
type tcOp struct {
et Enablement
path string
}
// test an instance of the Op interface
func (ptc tcOp) test(t *testing.T, gotOps []Op, wantOps []Op, fn string) {
if len(gotOps) != len(wantOps) {
t.Errorf("%s: inserted %v Ops, want %v", fn,
len(gotOps), len(wantOps))
return
}
t.Run("path", func(t *testing.T) {
if len(gotOps) > 0 {
if got := gotOps[0].Path(); got != ptc.path {
t.Errorf("Path() = %q, want %q",
got, ptc.path)
return
}
}
})
for i := range gotOps {
o := gotOps[i]
t.Run("is", func(t *testing.T) {
if !o.Is(o) {
t.Errorf("Is returned false on self")
return
}
if !o.Is(wantOps[i]) {
t.Errorf("%s: inserted %#v, want %#v",
fn,
o, wantOps[i])
return
}
})
t.Run("criteria", func(t *testing.T) {
testCases := []struct {
name string
ec *Criteria
want bool
}{
{"nil", newCriteria(), ptc.et != User},
{"self", newCriteria(ptc.et), true},
{"all", newCriteria(EWayland, EX11, EDBus, EPulse, User, Process), true},
{"enablements", newCriteria(EWayland, EX11, EDBus, EPulse), ptc.et != User && ptc.et != Process},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := tc.ec.hasType(o); got != tc.want {
t.Errorf("hasType: got %v, want %v",
got, tc.want)
}
})
}
})
}
}
func newCriteria(labels ...Enablement) *Criteria {
ec := new(Criteria)
if len(labels) == 0 {
return ec
}
ec.Enablements = new(Enablements)
for _, e := range labels {
ec.Set(e)
}
return ec
}

129
internal/system/op_test.go Normal file
View File

@ -0,0 +1,129 @@
package system_test
import (
"strconv"
"testing"
"git.ophivana.moe/security/fortify/internal/system"
)
func TestNew(t *testing.T) {
testCases := []struct {
uid int
}{
{150},
{149},
{148},
{147},
}
for _, tc := range testCases {
t.Run("sys initialised with uid "+strconv.Itoa(tc.uid), func(t *testing.T) {
if got := system.New(tc.uid); got.UID() != tc.uid {
t.Errorf("New(%d) uid = %d, want %d",
tc.uid,
got.UID(), tc.uid)
}
})
}
}
func TestTypeString(t *testing.T) {
testCases := []struct {
e system.Enablement
want string
}{
{system.EWayland, system.EWayland.String()},
{system.EX11, system.EX11.String()},
{system.EDBus, system.EDBus.String()},
{system.EPulse, system.EPulse.String()},
{system.User, "User"},
{system.Process, "Process"},
}
for _, tc := range testCases {
t.Run("label type string "+tc.want, func(t *testing.T) {
if got := system.TypeString(tc.e); got != tc.want {
t.Errorf("TypeString(%d) = %v, want %v",
tc.e,
got, tc.want)
}
})
}
}
func TestI_Equal(t *testing.T) {
testCases := []struct {
name string
sys *system.I
v *system.I
want bool
}{
{
"simple UID",
system.New(150),
system.New(150),
true,
},
{
"simple UID differ",
system.New(150),
system.New(151),
false,
},
{
"simple UID nil",
system.New(150),
nil,
false,
},
{
"op length mismatch",
system.New(150).
ChangeHosts("chronos"),
system.New(150).
ChangeHosts("chronos").
Ensure("/run", 0755),
false,
},
{
"op value mismatch",
system.New(150).
ChangeHosts("chronos").
Ensure("/run", 0644),
system.New(150).
ChangeHosts("chronos").
Ensure("/run", 0755),
false,
},
{
"op type mismatch",
system.New(150).
ChangeHosts("chronos").
CopyFile("/tmp/fortify.1971/30c9543e0a2c9621a8bfecb9d874c347/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie"),
system.New(150).
ChangeHosts("chronos").
Ensure("/run", 0755),
false,
},
{
"op equals",
system.New(150).
ChangeHosts("chronos").
Ensure("/run", 0755),
system.New(150).
ChangeHosts("chronos").
Ensure("/run", 0755),
true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.sys.Equal(tc.v) != tc.want {
t.Errorf("Equal: got %v; want %v",
!tc.want, tc.want)
}
})
}
}

145
internal/system/tmpfiles.go Normal file
View File

@ -0,0 +1,145 @@
package system
import (
"errors"
"fmt"
"io"
"os"
"strconv"
"git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
// CopyFile registers an Op that copies path dst from src.
func (sys *I) CopyFile(dst, src string) *I {
return sys.CopyFileType(Process, dst, src)
}
// CopyFileType registers a file copying Op labelled with type et.
func (sys *I) CopyFileType(et Enablement, dst, src string) *I {
sys.lock.Lock()
sys.ops = append(sys.ops, &Tmpfile{et, tmpfileCopy, dst, src})
sys.lock.Unlock()
sys.UpdatePermType(et, dst, acl.Read)
return sys
}
// Link registers an Op that links dst to src.
func (sys *I) Link(oldname, newname string) *I {
return sys.LinkFileType(Process, oldname, newname)
}
// LinkFileType registers a file linking Op labelled with type et.
func (sys *I) LinkFileType(et Enablement, oldname, newname string) *I {
sys.lock.Lock()
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &Tmpfile{et, tmpfileLink, newname, oldname})
return sys
}
// Write registers an Op that writes dst with the contents of src.
func (sys *I) Write(dst, src string) *I {
return sys.WriteType(Process, dst, src)
}
// WriteType registers a file writing Op labelled with type et.
func (sys *I) WriteType(et Enablement, dst, src string) *I {
sys.lock.Lock()
sys.ops = append(sys.ops, &Tmpfile{et, tmpfileWrite, dst, src})
sys.lock.Unlock()
sys.UpdatePermType(et, dst, acl.Read)
return sys
}
const (
tmpfileCopy uint8 = iota
tmpfileLink
tmpfileWrite
)
type Tmpfile struct {
et Enablement
method uint8
dst, src string
}
func (t *Tmpfile) Type() Enablement {
return t.et
}
func (t *Tmpfile) apply(_ *I) error {
switch t.method {
case tmpfileCopy:
fmsg.VPrintln("publishing tmpfile", t)
return fmsg.WrapErrorSuffix(copyFile(t.dst, t.src),
fmt.Sprintf("cannot copy tmpfile %q:", t.dst))
case tmpfileLink:
fmsg.VPrintln("linking tmpfile", t)
return fmsg.WrapErrorSuffix(os.Link(t.src, t.dst),
fmt.Sprintf("cannot link tmpfile %q:", t.dst))
case tmpfileWrite:
fmsg.VPrintln("writing", t)
return fmsg.WrapErrorSuffix(os.WriteFile(t.dst, []byte(t.src), 0600),
fmt.Sprintf("cannot write tmpfile %q:", t.dst))
default:
panic("invalid tmpfile method " + strconv.Itoa(int(t.method)))
}
}
func (t *Tmpfile) revert(_ *I, ec *Criteria) error {
if ec.hasType(t) {
fmsg.VPrintf("removing tmpfile %q", t.dst)
return fmsg.WrapErrorSuffix(os.Remove(t.dst),
fmt.Sprintf("cannot remove tmpfile %q:", t.dst))
} else {
fmsg.VPrintf("skipping tmpfile %q", t.dst)
return nil
}
}
func (t *Tmpfile) Is(o Op) bool {
t0, ok := o.(*Tmpfile)
return ok && t0 != nil && *t == *t0
}
func (t *Tmpfile) Path() string {
if t.method == tmpfileWrite {
return fmt.Sprintf("(%d bytes of data)", len(t.src))
}
return t.src
}
func (t *Tmpfile) String() string {
switch t.method {
case tmpfileCopy:
return fmt.Sprintf("%q from %q", t.dst, t.src)
case tmpfileLink:
return fmt.Sprintf("%q from %q", t.dst, t.src)
case tmpfileWrite:
return fmt.Sprintf("%d bytes of data to %q", len(t.src), t.dst)
default:
panic("invalid tmpfile method " + strconv.Itoa(int(t.method)))
}
}
func copyFile(dst, src string) error {
dstD, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
srcD, err := os.Open(src)
if err != nil {
return errors.Join(err, dstD.Close())
}
_, err = io.Copy(dstD, srcD)
return errors.Join(err, dstD.Close(), srcD.Close())
}

View File

@ -0,0 +1,167 @@
package system
import (
"strconv"
"testing"
"git.ophivana.moe/security/fortify/acl"
)
func TestCopyFile(t *testing.T) {
testCases := []struct {
dst, src string
}{
{"/tmp/fortify.1971/f587afe9fce3c8e1ad5b64deb6c41ad5/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie"},
{"/tmp/fortify.1971/62154f708b5184ab01f9dcc2bbe7a33b/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie"},
}
for _, tc := range testCases {
t.Run("copy file "+tc.dst+" from "+tc.src, func(t *testing.T) {
sys := New(150)
sys.CopyFile(tc.dst, tc.src)
(&tcOp{Process, tc.src}).test(t, sys.ops, []Op{
&Tmpfile{Process, tmpfileCopy, tc.dst, tc.src},
&ACL{Process, tc.dst, []acl.Perm{acl.Read}},
}, "CopyFile")
})
}
}
func TestCopyFileType(t *testing.T) {
testCases := []struct {
tcOp
dst string
}{
{tcOp{User, "/tmp/fortify.1971/f587afe9fce3c8e1ad5b64deb6c41ad5/pulse-cookie"}, "/home/ophestra/xdg/config/pulse/cookie"},
{tcOp{Process, "/tmp/fortify.1971/62154f708b5184ab01f9dcc2bbe7a33b/pulse-cookie"}, "/home/ophestra/xdg/config/pulse/cookie"},
}
for _, tc := range testCases {
t.Run("copy file "+tc.dst+" from "+tc.path+" with type "+TypeString(tc.et), func(t *testing.T) {
sys := New(150)
sys.CopyFileType(tc.et, tc.dst, tc.path)
tc.test(t, sys.ops, []Op{
&Tmpfile{tc.et, tmpfileCopy, tc.dst, tc.path},
&ACL{tc.et, tc.dst, []acl.Perm{acl.Read}},
}, "CopyFileType")
})
}
}
func TestLink(t *testing.T) {
testCases := []struct {
dst, src string
}{
{"/tmp/fortify.1971/f587afe9fce3c8e1ad5b64deb6c41ad5/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie"},
{"/tmp/fortify.1971/62154f708b5184ab01f9dcc2bbe7a33b/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie"},
}
for _, tc := range testCases {
t.Run("link file "+tc.dst+" from "+tc.src, func(t *testing.T) {
sys := New(150)
sys.Link(tc.src, tc.dst)
(&tcOp{Process, tc.src}).test(t, sys.ops, []Op{
&Tmpfile{Process, tmpfileLink, tc.dst, tc.src},
}, "Link")
})
}
}
func TestLinkFileType(t *testing.T) {
testCases := []struct {
tcOp
dst string
}{
{tcOp{User, "/tmp/fortify.1971/f587afe9fce3c8e1ad5b64deb6c41ad5/pulse-cookie"}, "/home/ophestra/xdg/config/pulse/cookie"},
{tcOp{Process, "/tmp/fortify.1971/62154f708b5184ab01f9dcc2bbe7a33b/pulse-cookie"}, "/home/ophestra/xdg/config/pulse/cookie"},
}
for _, tc := range testCases {
t.Run("link file "+tc.dst+" from "+tc.path+" with type "+TypeString(tc.et), func(t *testing.T) {
sys := New(150)
sys.LinkFileType(tc.et, tc.path, tc.dst)
tc.test(t, sys.ops, []Op{
&Tmpfile{tc.et, tmpfileLink, tc.dst, tc.path},
}, "LinkFileType")
})
}
}
func TestWrite(t *testing.T) {
testCases := []struct {
dst, src string
}{
{"/etc/passwd", "chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n"},
{"/etc/group", "fortify:x:65534:\n"},
}
for _, tc := range testCases {
t.Run("write "+strconv.Itoa(len(tc.src))+" bytes to "+tc.dst, func(t *testing.T) {
sys := New(150)
sys.Write(tc.dst, tc.src)
(&tcOp{Process, "(" + strconv.Itoa(len(tc.src)) + " bytes of data)"}).test(t, sys.ops, []Op{
&Tmpfile{Process, tmpfileWrite, tc.dst, tc.src},
&ACL{Process, tc.dst, []acl.Perm{acl.Read}},
}, "Write")
})
}
}
func TestWriteType(t *testing.T) {
testCases := []struct {
et Enablement
dst, src string
}{
{Process, "/etc/passwd", "chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n"},
{Process, "/etc/group", "fortify:x:65534:\n"},
{User, "/etc/passwd", "chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n"},
{User, "/etc/group", "fortify:x:65534:\n"},
}
for _, tc := range testCases {
t.Run("write "+strconv.Itoa(len(tc.src))+" bytes to "+tc.dst+" with type "+TypeString(tc.et), func(t *testing.T) {
sys := New(150)
sys.WriteType(tc.et, tc.dst, tc.src)
(&tcOp{tc.et, "(" + strconv.Itoa(len(tc.src)) + " bytes of data)"}).test(t, sys.ops, []Op{
&Tmpfile{tc.et, tmpfileWrite, tc.dst, tc.src},
&ACL{tc.et, tc.dst, []acl.Perm{acl.Read}},
}, "WriteType")
})
}
}
func TestTmpfile_String(t *testing.T) {
t.Run("invalid method panic", func(t *testing.T) {
defer func() {
wantPanic := "invalid tmpfile method 255"
if r := recover(); r != wantPanic {
t.Errorf("String() panic = %v, want %v",
r, wantPanic)
}
}()
_ = (&Tmpfile{method: 255}).String()
})
testCases := []struct {
method uint8
dst, src string
want string
}{
{tmpfileCopy, "/tmp/fortify.1971/4b6bdc9182fb2f1d3a965c5fa8b9b66e/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie",
`"/tmp/fortify.1971/4b6bdc9182fb2f1d3a965c5fa8b9b66e/pulse-cookie" from "/home/ophestra/xdg/config/pulse/cookie"`},
{tmpfileLink, "/run/user/1971/fortify/4b6bdc9182fb2f1d3a965c5fa8b9b66e/wayland", "/run/user/1971/wayland-0",
`"/run/user/1971/fortify/4b6bdc9182fb2f1d3a965c5fa8b9b66e/wayland" from "/run/user/1971/wayland-0"`},
{tmpfileLink, "/run/user/1971/fortify/4b6bdc9182fb2f1d3a965c5fa8b9b66e/pulse", "/run/user/1971/pulse/native",
`"/run/user/1971/fortify/4b6bdc9182fb2f1d3a965c5fa8b9b66e/pulse" from "/run/user/1971/pulse/native"`},
{tmpfileWrite, "/tmp/fortify.1971/4b6bdc9182fb2f1d3a965c5fa8b9b66e/passwd", "chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n",
`75 bytes of data to "/tmp/fortify.1971/4b6bdc9182fb2f1d3a965c5fa8b9b66e/passwd"`},
{tmpfileWrite, "/tmp/fortify.1971/4b6bdc9182fb2f1d3a965c5fa8b9b66e/group", "fortify:x:65534:\n",
`17 bytes of data to "/tmp/fortify.1971/4b6bdc9182fb2f1d3a965c5fa8b9b66e/group"`},
}
for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) {
if got := (&Tmpfile{
method: tc.method,
dst: tc.dst,
src: tc.src,
}).String(); got != tc.want {
t.Errorf("String() = %v, want %v", got, tc.want)
}
})
}
}

54
internal/system/xhost.go Normal file
View File

@ -0,0 +1,54 @@
package system
import (
"fmt"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/xcb"
)
// ChangeHosts appends an X11 ChangeHosts command Op.
func (sys *I) ChangeHosts(username string) *I {
sys.lock.Lock()
defer sys.lock.Unlock()
sys.ops = append(sys.ops, XHost(username))
return sys
}
type XHost string
func (x XHost) Type() Enablement {
return EX11
}
func (x XHost) apply(_ *I) error {
fmsg.VPrintf("inserting entry %s to X11", x)
return fmsg.WrapErrorSuffix(xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)),
fmt.Sprintf("cannot insert entry %s to X11:", x))
}
func (x XHost) revert(_ *I, ec *Criteria) error {
if ec.hasType(x) {
fmsg.VPrintf("deleting entry %s from X11", x)
return fmsg.WrapErrorSuffix(xcb.ChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)),
fmt.Sprintf("cannot delete entry %s from X11:", x))
} else {
fmsg.VPrintf("skipping entry %s in X11", x)
return nil
}
}
func (x XHost) Is(o Op) bool {
x0, ok := o.(XHost)
return ok && x == x0
}
func (x XHost) Path() string {
return string(x)
}
func (x XHost) String() string {
return string("SI:localuser:" + x)
}

View File

@ -0,0 +1,34 @@
package system
import (
"testing"
)
func TestChangeHosts(t *testing.T) {
testCases := []string{"chronos", "keyring", "cat", "kbd", "yonah"}
for _, tc := range testCases {
t.Run("append ChangeHosts operation for "+tc, func(t *testing.T) {
sys := New(150)
sys.ChangeHosts(tc)
(&tcOp{EX11, tc}).test(t, sys.ops, []Op{
XHost(tc),
}, "ChangeHosts")
})
}
}
func TestXHost_String(t *testing.T) {
testCases := []struct {
username string
want string
}{
{"chronos", "SI:localuser:chronos"},
}
for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) {
if got := XHost(tc.username).String(); got != tc.want {
t.Errorf("String() = %v, want %v", got, tc.want)
}
})
}
}

View File

@ -1,19 +0,0 @@
package verbose
import (
"fmt"
)
var Prefix = "fortify:"
func Println(a ...any) {
if verbose.Load() {
fmt.Println(append([]any{Prefix}, a...)...)
}
}
func Printf(format string, a ...any) {
if verbose.Load() {
fmt.Printf(Prefix+" "+format, a...)
}
}

View File

@ -1,67 +0,0 @@
package verbose_test
import (
"os"
"os/exec"
"strconv"
"strings"
"testing"
"git.ophivana.moe/cat/fortify/internal/verbose"
)
const (
testVerbose = "GO_TEST_VERBOSE"
wantStdout = "fortify: println\nfortify: printf"
)
func TestPrinter(t *testing.T) {
switch os.Getenv(testVerbose) {
case "0":
verbose.Set(false)
case "1":
verbose.Set(true)
default:
return
}
verbose.Println("println")
verbose.Printf("%s", "printf")
}
func TestPrintf_Println(t *testing.T) {
testPrintfPrintln(t, false)
testPrintfPrintln(t, true)
// make -cover happy
stdout := os.Stdout
t.Cleanup(func() {
os.Stdout = stdout
})
os.Stdout = nil
verbose.Set(true)
verbose.Printf("")
verbose.Println()
}
func testPrintfPrintln(t *testing.T, v bool) {
t.Run("start verbose printer with verbose "+strconv.FormatBool(v), func(t *testing.T) {
stdout, stderr := new(strings.Builder), new(strings.Builder)
stdout.Grow(len(wantStdout))
cmd := exec.Command(os.Args[0], "-test.run=TestPrinter")
cmd.Stdout, cmd.Stderr = stdout, stderr
if v {
cmd.Env = append(cmd.Env, testVerbose+"=1")
} else {
cmd.Env = append(cmd.Env, testVerbose+"=0")
}
if err := cmd.Run(); err != nil {
panic("cannot run printer process: " + err.Error() + " stderr: " + stderr.String())
}
if got := stdout.String(); strings.Contains(got, wantStdout) != v {
t.Errorf("Print: got %v; want %t",
got, v)
}
})
}

View File

@ -1,13 +0,0 @@
package verbose
import "sync/atomic"
var verbose = new(atomic.Bool)
func Get() bool {
return verbose.Load()
}
func Set(v bool) {
verbose.Store(v)
}

View File

@ -1,20 +0,0 @@
package verbose_test
import (
"testing"
"git.ophivana.moe/cat/fortify/internal/verbose"
)
func TestGetSet(t *testing.T) {
verbose.Set(false)
if verbose.Get() {
t.Errorf("Get() = true, want false")
}
verbose.Set(true)
if !verbose.Get() {
t.Errorf("Get() = false, want true")
}
}

View File

@ -5,14 +5,37 @@ import (
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
"git.ophivana.moe/security/fortify/helper"
"git.ophivana.moe/security/fortify/helper/bwrap"
) )
func Exec(p string) ([]*Entry, error) { func Exec(p string) ([]*Entry, error) {
t := exec.Command("ldd", p) var (
t.Stdout, t.Stderr = new(strings.Builder), os.Stderr h helper.Helper
if err := t.Run(); err != nil { cmd *exec.Cmd
)
if b, err := helper.NewBwrap((&bwrap.Config{
Hostname: "fortify-ldd",
Chdir: "/",
NewSession: true,
DieWithParent: true,
}).Bind("/", "/").DevTmpfs("/dev"),
nil, "ldd", func(_, _ int) []string { return []string{p} }); err != nil {
return nil, err
} else {
cmd = b.Unwrap()
h = b
}
cmd.Stdout, cmd.Stderr = new(strings.Builder), os.Stderr
if err := h.Start(); err != nil {
return nil, err
}
if err := h.Wait(); err != nil {
return nil, err return nil, err
} }
return Parse(t.Stdout.(fmt.Stringer)) return Parse(cmd.Stdout.(fmt.Stringer))
} }

View File

@ -6,7 +6,7 @@ import (
"strings" "strings"
"testing" "testing"
"git.ophivana.moe/cat/fortify/ldd" "git.ophivana.moe/security/fortify/ldd"
) )
func TestParseError(t *testing.T) { func TestParseError(t *testing.T) {

View File

@ -4,7 +4,6 @@ import (
_ "embed" _ "embed"
"flag" "flag"
"fmt" "fmt"
"os"
) )
var ( var (

56
main.go
View File

@ -2,15 +2,13 @@ package main
import ( import (
"flag" "flag"
"fmt"
"os"
"syscall" "syscall"
"git.ophivana.moe/cat/fortify/internal" "git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/cat/fortify/internal/app" "git.ophivana.moe/security/fortify/internal/app"
init0 "git.ophivana.moe/cat/fortify/internal/init" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/cat/fortify/internal/shim" init0 "git.ophivana.moe/security/fortify/internal/init"
"git.ophivana.moe/cat/fortify/internal/verbose" "git.ophivana.moe/security/fortify/internal/shim"
) )
var ( var (
@ -21,17 +19,19 @@ func init() {
flag.BoolVar(&flagVerbose, "v", false, "Verbose output") flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
} }
func main() { var os = new(internal.Std)
flag.Parse()
verbose.Set(flagVerbose)
func main() {
// linux/sched/coredump.h // linux/sched/coredump.h
if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, 0, 0); errno != 0 { if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, 0, 0); errno != 0 {
fmt.Printf("fortify: cannot set SUID_DUMP_DISABLE: %s", errno.Error()) fmsg.Printf("fortify: cannot set SUID_DUMP_DISABLE: %s", errno.Error())
} }
if internal.SdBootedV { flag.Parse()
verbose.Println("system booted with systemd as init system") fmsg.SetVerbose(flagVerbose)
if os.SdBooted() {
fmsg.VPrintln("system booted with systemd as init system")
} }
// shim/init early exit // shim/init early exit
@ -39,9 +39,9 @@ func main() {
shim.Try() shim.Try()
// root check // root check
if os.Getuid() == 0 { if os.Geteuid() == 0 {
fmt.Println("fortify: this program must not run as root") fmsg.Fatal("this program must not run as root")
os.Exit(1) panic("unreachable")
} }
// version/license/template command early exit // version/license/template command early exit
@ -53,20 +53,26 @@ func main() {
tryState() tryState()
// invoke app // invoke app
r := 1 a, err := app.New(os)
a := app.New() if err != nil {
if err := a.Seal(loadConfig()); err != nil { fmsg.Fatalf("cannot create app: %s\n", err)
logBaseError(err, "fortify: cannot seal app:") } else if err = a.Seal(loadConfig()); err != nil {
logBaseError(err, "cannot seal app:")
fmsg.Exit(1)
} else if err = a.Start(); err != nil { } else if err = a.Start(); err != nil {
logBaseError(err, "fortify: cannot start app:") logBaseError(err, "cannot start app:")
} else if r, err = a.Wait(); err != nil { }
var r int
// wait must be called regardless of result of start
if r, err = a.Wait(); err != nil {
if r < 1 { if r < 1 {
r = 1 r = 1
} }
logWaitError(err) logWaitError(err)
} }
if err := a.WaitErr(); err != nil { if err = a.WaitErr(); err != nil {
fmt.Println("fortify: inner wait failed:", err) fmsg.Println("inner wait failed:", err)
} }
os.Exit(r) fmsg.Exit(r)
} }

View File

@ -10,7 +10,7 @@
buildGoModule rec { buildGoModule rec {
pname = "fortify"; pname = "fortify";
version = "0.0.3"; version = "0.0.10";
src = ./.; src = ./.;
vendorHash = null; vendorHash = null;
@ -20,6 +20,8 @@ buildGoModule rec {
"-w" "-w"
"-X" "-X"
"main.Version=v${version}" "main.Version=v${version}"
"-X"
"main.FortifyPath=${placeholder "out"}/bin/.fortify-wrapped"
]; ];
buildInputs = [ buildInputs = [
@ -36,5 +38,7 @@ buildGoModule rec {
xdg-dbus-proxy xdg-dbus-proxy
] ]
} }
mv $out/bin/fsu $out/bin/.fsu
''; '';
} }

View File

@ -3,11 +3,10 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"os"
"text/tabwriter" "text/tabwriter"
"git.ophivana.moe/cat/fortify/internal" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/security/fortify/internal/state"
) )
var ( var (
@ -22,15 +21,15 @@ func init() {
func tryState() { func tryState() {
if stateActionEarly { if stateActionEarly {
var w *tabwriter.Writer var w *tabwriter.Writer
state.MustPrintLauncherStateSimpleGlobal(&w, internal.GetSC().RunDirPath) state.MustPrintLauncherStateSimpleGlobal(&w, os.Paths().RunDirPath)
if w != nil { if w != nil {
if err := w.Flush(); err != nil { if err := w.Flush(); err != nil {
fmt.Println("warn: error formatting output:", err) fmsg.Println("cannot format output:", err)
} }
} else { } else {
fmt.Println("No information available") fmt.Println("No information available")
} }
os.Exit(0) fmsg.Exit(0)
} }
} }

View File

@ -3,7 +3,6 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"os"
) )
var ( var (