Compare commits

..

No commits in common. "master" and "v0.0.11" have entirely different histories.

103 changed files with 2626 additions and 5093 deletions

View File

@ -1,46 +0,0 @@
name: Nix
on:
- push
- pull_request
jobs:
tests:
name: NixOS tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Nix
uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
with:
# explicitly enable sandbox
install_options: --daemon
extra_nix_config: |
sandbox = true
system-features = nixos-test benchmark big-parallel kvm
enable_kvm: true
- name: Ensure environment
run: >-
apt-get update && apt-get install -y sqlite3
if: ${{ runner.os == 'Linux' }}
- name: Restore Nix store
uses: nix-community/cache-nix-action@v5
with:
primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix') }}
restore-prefixes-first-match: nix-${{ runner.os }}-
- name: Run tests
run: |
nix --print-build-logs --experimental-features 'nix-command flakes' flake check --all-systems
nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.nixos-tests
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "result"
path: result/*
retention-days: 1

View File

@ -1,4 +1,4 @@
name: Create distribution name: release
on: on:
push: push:
@ -7,26 +7,8 @@ on:
jobs: jobs:
release: release:
name: Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
container:
image: node:16-bookworm-slim
steps: steps:
- name: Get dependencies
run: >-
echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list.d/backports.list &&
apt-get update &&
apt-get install -y
acl
git
gcc
pkg-config
libwayland-dev
wayland-protocols/bookworm-backports
libxcb1-dev
libacl1-dev
if: ${{ runner.os == 'Linux' }}
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
@ -37,17 +19,30 @@ jobs:
with: with:
go-version: '>=1.23.0' go-version: '>=1.23.0'
- name: Go generate - name: Get dependencies
run: >- run: >-
go generate ./... apt-get update &&
apt-get install -y
gcc
pkg-config
libacl1-dev
if: ${{ runner.os == 'Linux' }}
- name: Build for release - name: Build for Linux
run: FORTIFY_VERSION='${{ github.ref_name }}' ./dist/release.sh run: >-
go build -v -ldflags '-s -w
-X git.ophivana.moe/security/fortify/internal.Version=${{ github.ref_name }}
-X git.ophivana.moe/security/fortify/internal.Fsu=/usr/bin/fsu
-X git.ophivana.moe/security/fortify/internal.Fshim=/usr/libexec/fortify/fshim
-X git.ophivana.moe/security/fortify/internal.Finit=/usr/libexec/fortify/finit
-X main.Fmain=/usr/bin/fortify'
-o bin/ ./... &&
(cd bin && sha512sum --tag -b * > sha512sums)
- 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
with: with:
files: |- files: |-
dist/fortify-** bin/**
api_key: '${{secrets.RELEASE_TOKEN}}' api_key: '${{secrets.RELEASE_TOKEN}}'

View File

@ -1,4 +1,4 @@
name: Tests name: test
on: on:
- push - push
@ -6,29 +6,8 @@ on:
jobs: jobs:
test: test:
name: Go tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
container:
image: node:16-bookworm-slim
steps: steps:
- name: Enable backports
run: >-
echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list.d/backports.list
if: ${{ runner.os == 'Linux' }}
- name: Ensure environment
run: >-
apt-get update && apt-get install -y curl wget sudo libxml2
if: ${{ runner.os == 'Linux' }}
- name: Get dependencies
uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: acl git gcc pkg-config libwayland-dev wayland-protocols/bookworm-backports libxcb1-dev libacl1-dev
version: 1.0
#execute_install_scripts: true
if: ${{ runner.os == 'Linux' }}
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
@ -39,24 +18,26 @@ jobs:
with: with:
go-version: '>=1.23.0' go-version: '>=1.23.0'
- name: Go generate - name: Get dependencies
run: >- run: >-
go generate ./... apt-get update &&
apt-get install -y
gcc
pkg-config
libacl1-dev
if: ${{ runner.os == 'Linux' }}
- name: Run tests - name: Run tests
run: >- run: >-
go test ./... go test ./...
- name: Build for test - name: Build for Linux
id: build-test
run: >- run: >-
FORTIFY_VERSION="$(git rev-parse --short HEAD)" go build -v -ldflags '-s -w
bash -c './dist/release.sh && -X git.ophivana.moe/security/fortify/internal.Version=${{ github.ref_name }}
echo "rev=$FORTIFY_VERSION" >> $GITHUB_OUTPUT' -X git.ophivana.moe/security/fortify/internal.Fsu=/usr/bin/fsu
-X git.ophivana.moe/security/fortify/internal.Fshim=/usr/libexec/fortify/fshim
- name: Upload test build -X git.ophivana.moe/security/fortify/internal.Finit=/usr/libexec/fortify/finit
uses: actions/upload-artifact@v3 -X main.Fmain=/usr/bin/fortify'
with: -o bin/ ./... &&
name: "fortify-${{ steps.build-test.outputs.rev }}" (cd bin && sha512sum --tag -b * > sha512sums)
path: dist/fortify-*
retention-days: 1

6
.gitignore vendored
View File

@ -23,9 +23,3 @@ go.work.sum
.env .env
.idea .idea
.vscode .vscode
# go generate
security-context-v1-protocol.*
# release
/dist/fortify-*

161
README.md
View File

@ -1,8 +1,7 @@
Fortify Fortify
======= =======
[![Go Reference](https://pkg.go.dev/badge/git.gensokyo.uk/security/fortify.svg)](https://pkg.go.dev/git.gensokyo.uk/security/fortify) [![Go Reference](https://pkg.go.dev/badge/git.ophivana.moe/security/fortify.svg)](https://pkg.go.dev/git.ophivana.moe/security/fortify)
[![Go Report Card](https://goreportcard.com/badge/git.gensokyo.uk/security/fortify)](https://goreportcard.com/report/git.gensokyo.uk/security/fortify)
Lets you run graphical applications as another user in a confined environment 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.
@ -15,17 +14,24 @@ Why would you want this?
- It provides UID isolation on top of the standard application sandbox. - It provides UID isolation on top of the standard application sandbox.
There are a few different things to set up for this to work:
- A set of users, each for a group of applications that should be allowed access to each other
- A tool to switch users, currently sudo and machinectl are supported.
- If you are running NixOS, the module in this repository can take care of launchers and desktop files in the privileged
user's environment, as well as packages and extra home-manager configuration for target users.
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.gensokyo.uk/security/fortify -- help nix run git+https://git.ophivana.moe/security/fortify -- -h
``` ```
## Module usage ## Module usage
The NixOS module currently requires home-manager to function correctly. The NixOS module currently requires home-manager and impermanence to function correctly.
Full module documentation can be found [here](options.md).
To use the module, import it into your configuration with To use the module, import it into your configuration with
@ -35,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.gensokyo.uk/security/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";
@ -62,21 +68,21 @@ This adds the `environment.fortify` option:
{ {
environment.fortify = { environment.fortify = {
enable = true; enable = true;
stateDir = "/var/lib/persist/module/fortify"; user = "nixos";
users = { stateDir = "/var/lib/persist/module";
alice = 0; target = {
nixos = 10; chronos = {
launchers = {
weechat.method = "sudo";
claws-mail.capability.pulse = false;
discord = {
command = "vesktop --ozone-platform-hint=wayland";
share = pkgs.vesktop;
}; };
apps = [ chromium.dbus = {
{ configSystem = {
name = "chromium";
id = "org.chromium.Chromium";
packages = [ pkgs.chromium ];
userns = true;
mapRealUid = true;
dbus = {
system = {
filter = true; filter = true;
talk = [ talk = [
"org.bluez" "org.bluez"
@ -84,10 +90,10 @@ This adds the `environment.fortify` option:
"org.freedesktop.UPower" "org.freedesktop.UPower"
]; ];
}; };
session = config = {
f: filter = true;
f {
talk = [ talk = [
"org.freedesktop.DBus"
"org.freedesktop.FileManager1" "org.freedesktop.FileManager1"
"org.freedesktop.Notifications" "org.freedesktop.Notifications"
"org.freedesktop.ScreenSaver" "org.freedesktop.ScreenSaver"
@ -100,63 +106,74 @@ This adds the `environment.fortify` option:
"org.mpris.MediaPlayer2.org.chromium.Chromium.*" "org.mpris.MediaPlayer2.org.chromium.Chromium.*"
"org.mpris.MediaPlayer2.chromium.*" "org.mpris.MediaPlayer2.chromium.*"
]; ];
call = { }; call = {
broadcast = { }; "org.freedesktop.portal.*" = "*";
};
broadcast = {
"org.freedesktop.portal.*" = "@/org/freedesktop/portal/*";
}; };
}; };
}
{
name = "claws-mail";
id = "org.claws_mail.Claws-Mail";
packages = [ pkgs.claws-mail ];
gpu = false;
capability.pulse = false;
}
{
name = "weechat";
packages = [ pkgs.weechat ];
capability = {
wayland = false;
x11 = false;
dbus = true;
pulse = false;
}; };
}
{
name = "discord";
id = "dev.vencord.Vesktop";
packages = [ pkgs.vesktop ];
share = pkgs.vesktop;
command = "vesktop --ozone-platform-hint=wayland";
userns = true;
mapRealUid = true;
capability.x11 = true;
dbus = {
session =
f:
f {
talk = [ "org.kde.StatusNotifierWatcher" ];
own = [ ];
call = { };
broadcast = { };
}; };
system.filter = true; packages = with pkgs; [
}; weechat
} claws-mail
{ vesktop
name = "looking-glass-client"; chromium
groups = [ "plugdev" ]; ];
extraPaths = [ persistence.directories = [
{ ".config/weechat"
src = "/dev/shm/looking-glass"; ".claws-mail"
write = true; ".config/vesktop"
}
]; ];
extraConfig = { extraConfig = {
programs.looking-glass-client.enable = true; programs.looking-glass-client.enable = true;
}; };
} };
]; };
}; };
} }
``` ```
* `enable` determines whether the module should be enabled or not. Useful when sharing configurations between graphical
and headless systems. Defaults to `false`.
* `user` specifies the privileged user with access to fortified applications.
* `stateDir` is the path to your persistent storage location. It is directly passed through to the impermanence module.
* `target` is an attribute set of submodules, where the attribute name is the username of the unprivileged target user.
The available options are:
* `packages`, the list of packages to make available in the target user's environment.
* `persistence`, user persistence attribute set passed to impermanence.
* `extraConfig`, extra home-manager configuration for the target user.
* `launchers`, attribute set where the attribute name is the name of the launcher.
The available options are:
* `command`, the command to run as the target user. Defaults to launcher name.
* `dbus.config`, D-Bus proxy custom configuration.
* `dbus.configSystem`, D-Bus system bus custom configuration, null to disable.
* `dbus.id`, D-Bus application id, has no effect if `dbus.config` is set.
* `dbus.mpris`, whether to enable MPRIS defaults, has no effect if `dbus.config` is set.
* `capability.wayland`, whether to share the Wayland socket.
* `capability.x11`, whether to share the X11 socket and allow connection.
* `capability.dbus`, whether to proxy D-Bus.
* `capability.pulse`, whether to share the PulseAudio socket and cookie.
* `share`, package containing desktop/icon files. Defaults to launcher name.
* `method`, the launch method for the sandboxed program, can be `"fortify"`, `"fortify-sudo"`, `"sudo"`.

View File

@ -1,19 +0,0 @@
// Package acl implements simple ACL manipulation via libacl.
package acl
type 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)
}

View File

@ -1,156 +0,0 @@
package acl_test
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os/exec"
"strconv"
)
type (
getFAclInvocation struct {
cmd *exec.Cmd
val []*getFAclResp
pe []error
}
getFAclResp struct {
typ fAclType
cred int32
val fAclPerm
raw []byte
}
fAclPerm uintptr
fAclType uint8
)
const fAclBufSize = 16
const (
fAclPermRead fAclPerm = 1 << iota
fAclPermWrite
fAclPermExecute
)
const (
fAclTypeUser fAclType = iota
fAclTypeGroup
fAclTypeMask
fAclTypeOther
)
func (c *getFAclInvocation) run(name string) error {
if c.cmd != nil {
panic("attempted to run twice")
}
c.cmd = exec.Command("getfacl", "--omit-header", "--absolute-names", "--numeric", name)
scanErr := make(chan error, 1)
if p, err := c.cmd.StdoutPipe(); err != nil {
return err
} else {
go c.parse(p, scanErr)
}
if err := c.cmd.Start(); err != nil {
return err
}
return errors.Join(<-scanErr, c.cmd.Wait())
}
func (c *getFAclInvocation) parse(pipe io.Reader, scanErr chan error) {
c.val = make([]*getFAclResp, 0, 4+fAclBufSize)
s := bufio.NewScanner(pipe)
for s.Scan() {
fields := bytes.SplitN(s.Bytes(), []byte{':'}, 3)
if len(fields) != 3 {
continue
}
resp := getFAclResp{}
switch string(fields[0]) {
case "user":
resp.typ = fAclTypeUser
case "group":
resp.typ = fAclTypeGroup
case "mask":
resp.typ = fAclTypeMask
case "other":
resp.typ = fAclTypeOther
default:
c.pe = append(c.pe, fmt.Errorf("unknown type %s", string(fields[0])))
continue
}
if len(fields[1]) == 0 {
resp.cred = -1
} else {
if cred, err := strconv.Atoi(string(fields[1])); err != nil {
c.pe = append(c.pe, err)
continue
} else {
resp.cred = int32(cred)
if resp.cred < 0 {
c.pe = append(c.pe, fmt.Errorf("credential %d out of range", resp.cred))
continue
}
}
}
if len(fields[2]) != 3 {
c.pe = append(c.pe, fmt.Errorf("invalid perm length %d", len(fields[2])))
continue
} else {
switch fields[2][0] {
case 'r':
resp.val |= fAclPermRead
case '-':
default:
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][0]))
continue
}
switch fields[2][1] {
case 'w':
resp.val |= fAclPermWrite
case '-':
default:
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][1]))
continue
}
switch fields[2][2] {
case 'x':
resp.val |= fAclPermExecute
case '-':
default:
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][2]))
continue
}
}
resp.raw = make([]byte, len(s.Bytes()))
copy(resp.raw, s.Bytes())
c.val = append(c.val, &resp)
}
scanErr <- s.Err()
}
func (r *getFAclResp) String() string {
if r.raw != nil && len(r.raw) > 0 {
return string(r.raw)
}
return "(user-initialised resp value)"
}
func (r *getFAclResp) equals(typ fAclType, cred int32, val fAclPerm) bool {
return r.typ == typ && r.cred == cred && r.val == val
}

View File

@ -1,125 +0,0 @@
package acl_test
import (
"errors"
"os"
"path"
"reflect"
"testing"
"git.gensokyo.uk/security/fortify/acl"
)
const testFileName = "acl.test"
var (
uid = os.Geteuid()
cred = int32(os.Geteuid())
)
func TestUpdatePerm(t *testing.T) {
if os.Getenv("GO_TEST_SKIP_ACL") == "1" {
t.Log("acl test skipped")
t.SkipNow()
}
testFilePath := path.Join(t.TempDir(), testFileName)
if f, err := os.Create(testFilePath); err != nil {
t.Fatalf("Create: error = %v", err)
} else {
if err = f.Close(); err != nil {
t.Fatalf("Close: error = %v", err)
}
}
defer func() {
if err := os.Remove(testFilePath); err != nil {
t.Fatalf("Remove: error = %v", err)
}
}()
cur := getfacl(t, testFilePath)
t.Run("default entry count", func(t *testing.T) {
if len(cur) != 3 {
t.Fatalf("unexpected test file acl length %d", len(cur))
}
})
t.Run("default clear mask", func(t *testing.T) {
if err := acl.UpdatePerm(testFilePath, uid); err != nil {
t.Fatalf("UpdatePerm: error = %v", err)
}
if cur = getfacl(t, testFilePath); len(cur) != 4 {
t.Fatalf("UpdatePerm: %v", cur)
}
})
t.Run("default clear consistency", func(t *testing.T) {
if err := acl.UpdatePerm(testFilePath, uid); err != nil {
t.Fatalf("UpdatePerm: error = %v", err)
}
if val := getfacl(t, testFilePath); !reflect.DeepEqual(val, cur) {
t.Fatalf("UpdatePerm: %v, want %v", val, cur)
}
})
testUpdate(t, testFilePath, "r--", cur, fAclPermRead, acl.Read)
testUpdate(t, testFilePath, "-w-", cur, fAclPermWrite, acl.Write)
testUpdate(t, testFilePath, "--x", cur, fAclPermExecute, acl.Execute)
testUpdate(t, testFilePath, "-wx", cur, fAclPermWrite|fAclPermExecute, acl.Write, acl.Execute)
testUpdate(t, testFilePath, "r-x", cur, fAclPermRead|fAclPermExecute, acl.Read, acl.Execute)
testUpdate(t, testFilePath, "rw-", cur, fAclPermRead|fAclPermWrite, acl.Read, acl.Write)
testUpdate(t, testFilePath, "rwx", cur, fAclPermRead|fAclPermWrite|fAclPermExecute, acl.Read, acl.Write, acl.Execute)
}
func testUpdate(t *testing.T, testFilePath, name string, cur []*getFAclResp, val fAclPerm, perms ...acl.Perm) {
t.Run(name, func(t *testing.T) {
t.Cleanup(func() {
if err := acl.UpdatePerm(testFilePath, uid); err != nil {
t.Fatalf("UpdatePerm: error = %v", err)
}
if v := getfacl(t, testFilePath); !reflect.DeepEqual(v, cur) {
t.Fatalf("UpdatePerm: %v, want %v", v, cur)
}
})
if err := acl.UpdatePerm(testFilePath, uid, perms...); err != nil {
t.Fatalf("UpdatePerm: error = %v", err)
}
r := respByCred(getfacl(t, testFilePath), fAclTypeUser, cred)
if r == nil {
t.Fatalf("UpdatePerm did not add an ACL entry")
}
if !r.equals(fAclTypeUser, cred, val) {
t.Fatalf("UpdatePerm(%s) = %s", name, r)
}
})
}
func getfacl(t *testing.T, name string) []*getFAclResp {
c := new(getFAclInvocation)
if err := c.run(name); err != nil {
t.Fatalf("getfacl: error = %v", err)
}
if len(c.pe) != 0 {
t.Errorf("errors encountered parsing getfacl output\n%s", errors.Join(c.pe...).Error())
}
return c.val
}
func respByCred(v []*getFAclResp, typ fAclType, cred int32) *getFAclResp {
j := -1
for i, r := range v {
if r.typ == typ && r.cred == cred {
if j != -1 {
panic("invalid acl")
}
j = i
}
}
if j == -1 {
return nil
}
return v[j]
}

163
acl/c.go
View File

@ -1,95 +1,50 @@
package acl package acl
import "C"
import ( import (
"errors" "errors"
"runtime" "fmt"
"syscall" "syscall"
"unsafe" "unsafe"
) )
/* //#include <stdlib.h>
#cgo linux pkg-config: libacl //#include <sys/acl.h>
//#include <acl/libacl.h>
#include <stdlib.h> //#cgo linux LDFLAGS: -lacl
#include <sys/acl.h>
#include <acl/libacl.h>
static acl_t _go_acl_get_file(const char *path_p, acl_type_t type) {
acl_t acl = acl_get_file(path_p, type);
free((void *)path_p);
return acl;
}
static int _go_acl_set_file(const char *path_p, acl_type_t type, acl_t acl) {
if (acl_valid(acl) != 0) {
return -1;
}
int ret = acl_set_file(path_p, type, acl);
free((void *)path_p);
return ret;
}
*/
import "C" import "C"
func getFile(name string, t C.acl_type_t) (*ACL, error) { type acl struct {
a, err := C._go_acl_get_file(C.CString(name), t) val C.acl_t
freed bool
}
func aclGetFile(path string, t C.acl_type_t) (*acl, error) {
p := C.CString(path)
a, err := C.acl_get_file(p, t)
C.free(unsafe.Pointer(p))
if errors.Is(err, syscall.ENODATA) { if errors.Is(err, syscall.ENODATA) {
err = nil err = nil
} }
return &acl{val: a, freed: false}, err
return newACL(a), err
} }
func (acl *ACL) setFile(name string, t C.acl_type_t) error { func (a *acl) setFile(path string, t C.acl_type_t) error {
_, err := C._go_acl_set_file(C.CString(name), t, acl.acl) if C.acl_valid(a.val) != 0 {
return fmt.Errorf("invalid acl")
}
p := C.CString(path)
_, err := C.acl_set_file(p, t, a.val)
C.free(unsafe.Pointer(p))
return err return err
} }
func newACL(a C.acl_t) *ACL { func (a *acl) removeEntry(tt C.acl_tag_t, tq int) error {
acl := &ACL{a}
runtime.SetFinalizer(acl, (*ACL).free)
return acl
}
type ACL struct {
acl C.acl_t
}
func (acl *ACL) free() {
C.acl_free(unsafe.Pointer(acl.acl))
// no need for a finalizer anymore
runtime.SetFinalizer(acl, nil)
}
const (
Read = C.ACL_READ
Write = C.ACL_WRITE
Execute = C.ACL_EXECUTE
TypeDefault = C.ACL_TYPE_DEFAULT
TypeAccess = C.ACL_TYPE_ACCESS
UndefinedTag = C.ACL_UNDEFINED_TAG
UserObj = C.ACL_USER_OBJ
User = C.ACL_USER
GroupObj = C.ACL_GROUP_OBJ
Group = C.ACL_GROUP
Mask = C.ACL_MASK
Other = C.ACL_OTHER
)
type (
Perm C.acl_perm_t
)
func (acl *ACL) removeEntry(tt C.acl_tag_t, tq int) error {
var e C.acl_entry_t var e C.acl_entry_t
// get first entry // get first entry
if r, err := C.acl_get_entry(acl.acl, C.ACL_FIRST_ENTRY, &e); err != nil { if r, err := C.acl_get_entry(a.val, C.ACL_FIRST_ENTRY, &e); err != nil {
return err return err
} else if r == 0 { } else if r == 0 {
// return on acl with no entries // return on acl with no entries
@ -97,7 +52,7 @@ func (acl *ACL) removeEntry(tt C.acl_tag_t, tq int) error {
} }
for { for {
if r, err := C.acl_get_entry(acl.acl, C.ACL_NEXT_ENTRY, &e); err != nil { if r, err := C.acl_get_entry(a.val, C.ACL_NEXT_ENTRY, &e); err != nil {
return err return err
} else if r == 0 { } else if r == 0 {
// return on drained acl // return on drained acl
@ -129,68 +84,16 @@ func (acl *ACL) removeEntry(tt C.acl_tag_t, tq int) error {
// delete on match // delete on match
if t == tt && q == tq { if t == tt && q == tq {
_, err := C.acl_delete_entry(acl.acl, e) _, err := C.acl_delete_entry(a.val, e)
return err return err
} }
} }
} }
func UpdatePerm(name string, uid int, perms ...Perm) error { func (a *acl) free() {
// read acl from file if a.freed {
a, err := getFile(name, TypeAccess) panic("acl already freed")
if err != nil {
return err
} }
// free acl on return if get is successful C.acl_free(unsafe.Pointer(a.val))
defer a.free() a.freed = true
// remove existing entry
if err = a.removeEntry(User, uid); err != nil {
return err
}
// create new entry if perms are passed
if len(perms) > 0 {
// create new acl entry
var e C.acl_entry_t
if _, err = C.acl_create_entry(&a.acl, &e); err != nil {
return err
}
// get perm set of new entry
var p C.acl_permset_t
if _, err = C.acl_get_permset(e, &p); err != nil {
return err
}
// add target perms
for _, perm := range perms {
if _, err = C.acl_add_perm(p, C.acl_perm_t(perm)); err != nil {
return err
}
}
// set perm set to new entry
if _, err = C.acl_set_permset(e, p); err != nil {
return err
}
// set user tag to new entry
if _, err = C.acl_set_tag_type(e, User); err != nil {
return err
}
// set qualifier (uid) to new entry
if _, err = C.acl_set_qualifier(e, unsafe.Pointer(&uid)); err != nil {
return err
}
}
// calculate mask after update
if _, err = C.acl_calc_mask(&a.acl); err != nil {
return err
}
// write acl to file
return a.setFile(name, TypeAccess)
} }

107
acl/export.go Normal file
View File

@ -0,0 +1,107 @@
// Package acl implements simple ACL manipulation via libacl.
package acl
import "unsafe"
//#include <stdlib.h>
//#include <sys/acl.h>
//#include <acl/libacl.h>
//#cgo linux LDFLAGS: -lacl
import "C"
const (
Read = C.ACL_READ
Write = C.ACL_WRITE
Execute = C.ACL_EXECUTE
TypeDefault = C.ACL_TYPE_DEFAULT
TypeAccess = C.ACL_TYPE_ACCESS
UndefinedTag = C.ACL_UNDEFINED_TAG
UserObj = C.ACL_USER_OBJ
User = C.ACL_USER
GroupObj = C.ACL_GROUP_OBJ
Group = C.ACL_GROUP
Mask = C.ACL_MASK
Other = C.ACL_OTHER
)
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 {
// read acl from file
a, err := aclGetFile(path, TypeAccess)
if err != nil {
return err
}
// free acl on return if get is successful
defer a.free()
// remove existing entry
if err = a.removeEntry(User, uid); err != nil {
return err
}
// create new entry if perms are passed
if len(perms) > 0 {
// create new acl entry
var e C.acl_entry_t
if _, err = C.acl_create_entry(&a.val, &e); err != nil {
return err
}
// get perm set of new entry
var p C.acl_permset_t
if _, err = C.acl_get_permset(e, &p); err != nil {
return err
}
// add target perms
for _, perm := range perms {
if _, err = C.acl_add_perm(p, C.acl_perm_t(perm)); err != nil {
return err
}
}
// set perm set to new entry
if _, err = C.acl_set_permset(e, p); err != nil {
return err
}
// set user tag to new entry
if _, err = C.acl_set_tag_type(e, User); err != nil {
return err
}
// set qualifier (uid) to new entry
if _, err = C.acl_set_qualifier(e, unsafe.Pointer(&uid)); err != nil {
return err
}
}
// calculate mask after update
if _, err = C.acl_calc_mask(&a.val); err != nil {
return err
}
// write acl to file
return a.setFile(path, TypeAccess)
}

View File

@ -7,6 +7,8 @@ type Payload struct {
Argv0 string Argv0 string
// child full argv // child full argv
Argv []string Argv []string
// wayland fd, -1 to disable
WL int
// verbosity pass through // verbosity pass through
Verbose bool Verbose bool

View File

@ -1,18 +1,19 @@
package main package main
import ( import (
"encoding/gob"
"errors" "errors"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"path" "path"
"strconv"
"syscall" "syscall"
"time" "time"
init0 "git.gensokyo.uk/security/fortify/cmd/finit/ipc" init0 "git.ophivana.moe/security/fortify/cmd/finit/ipc"
"git.gensokyo.uk/security/fortify/internal" "git.ophivana.moe/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/proc"
) )
const ( const (
@ -47,24 +48,30 @@ func main() {
} }
} }
// receive setup payload // setup pipe fd from environment
var ( var setup *os.File
payload init0.Payload if s, ok := os.LookupEnv(init0.Env); !ok {
closeSetup func() error
)
if f, err := proc.Receive(init0.Env, &payload); err != nil {
if errors.Is(err, proc.ErrInvalid) {
fmsg.Fatal("invalid config descriptor")
}
if errors.Is(err, proc.ErrNotSet) {
fmsg.Fatal("FORTIFY_INIT not set") fmsg.Fatal("FORTIFY_INIT not set")
panic("unreachable")
} else {
if fd, err := strconv.Atoi(s); err != nil {
fmsg.Fatalf("cannot parse %q: %v", s, err)
panic("unreachable")
} else {
setup = os.NewFile(uintptr(fd), "setup")
if setup == nil {
fmsg.Fatal("invalid config descriptor")
panic("unreachable")
}
}
} }
fmsg.Fatalf("cannot decode init setup payload: %v", err) var payload init0.Payload
if err := gob.NewDecoder(setup).Decode(&payload); err != nil {
fmsg.Fatal("cannot decode init setup payload:", err)
panic("unreachable") panic("unreachable")
} else { } else {
fmsg.SetVerbose(payload.Verbose) fmsg.SetVerbose(payload.Verbose)
closeSetup = f
// child does not need to see this // child does not need to see this
if err = os.Unsetenv(init0.Env); err != nil { if err = os.Unsetenv(init0.Env); err != nil {
@ -85,13 +92,21 @@ func main() {
cmd.Args = payload.Argv cmd.Args = payload.Argv
cmd.Env = os.Environ() cmd.Env = os.Environ()
// pass wayland fd
if payload.WL != -1 {
if f := os.NewFile(uintptr(payload.WL), "wayland"); f != nil {
cmd.Env = append(cmd.Env, "WAYLAND_SOCKET="+strconv.Itoa(3+len(cmd.ExtraFiles)))
cmd.ExtraFiles = append(cmd.ExtraFiles, f)
}
}
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
fmsg.Fatalf("cannot start %q: %v", payload.Argv0, err) fmsg.Fatalf("cannot start %q: %v", payload.Argv0, err)
} }
fmsg.Suspend() fmsg.Withhold()
// close setup pipe as setup is now complete // close setup pipe as setup is now complete
if err := closeSetup(); err != nil { if err := setup.Close(); err != nil {
fmsg.Println("cannot close setup pipe:", err) fmsg.Println("cannot close setup pipe:", err)
// not fatal // not fatal
} }

View File

@ -1,7 +1,12 @@
package shim0 package shim0
import ( import (
"git.gensokyo.uk/security/fortify/helper/bwrap" "encoding/gob"
"errors"
"net"
"git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal/fmsg"
) )
const Env = "FORTIFY_SHIM" const Env = "FORTIFY_SHIM"
@ -13,9 +18,25 @@ type Payload struct {
Exec [2]string Exec [2]string
// bwrap config // bwrap config
Bwrap *bwrap.Config Bwrap *bwrap.Config
// sync fd // whether to pass wayland fd
Sync *uintptr WL bool
// 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:")
}

View File

@ -1,20 +1,18 @@
package shim package shim
import ( import (
"encoding/gob"
"errors" "errors"
"net"
"os" "os"
"os/exec" "os/exec"
"os/signal" "sync"
"strconv" "sync/atomic"
"strings"
"syscall" "syscall"
"time" "time"
shim0 "git.gensokyo.uk/security/fortify/cmd/fshim/ipc" "git.ophivana.moe/security/fortify/acl"
"git.gensokyo.uk/security/fortify/internal" shim0 "git.ophivana.moe/security/fortify/cmd/fshim/ipc"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/proc"
) )
const shimSetupTimeout = 5 * time.Second const shimSetupTimeout = 5 * time.Second
@ -26,18 +24,24 @@ type Shim struct {
cmd *exec.Cmd cmd *exec.Cmd
// uid of shim target user // uid of shim target user
uid uint32 uid uint32
// string representation of application id // whether to check shim pid
aid string checkPid bool
// string representation of supplementary group ids // user switcher executable path
supp []string executable string
// fallback exit notifier with error returned killing the process // path to setup socket
killFallback chan error socket string
// shim setup abort reason and completion
abort chan error
abortErr atomic.Pointer[error]
abortOnce sync.Once
// wayland mediation, nil if disabled
wl *shim0.Wayland
// shim setup payload // shim setup payload
payload *shim0.Payload payload *shim0.Payload
} }
func New(uid uint32, aid string, supp []string, payload *shim0.Payload) *Shim { func New(executable string, uid uint32, socket string, wl *shim0.Wayland, payload *shim0.Payload, checkPid bool) *Shim {
return &Shim{uid: uid, aid: aid, supp: supp, payload: payload} return &Shim{uid: uid, executable: executable, socket: socket, wl: wl, payload: payload, checkPid: checkPid}
} }
func (s *Shim) String() string { func (s *Shim) String() string {
@ -51,87 +55,169 @@ func (s *Shim) Unwrap() *exec.Cmd {
return s.cmd return s.cmd
} }
func (s *Shim) WaitFallback() chan error { func (s *Shim) Abort(err error) {
return s.killFallback s.abortOnce.Do(func() {
s.abortErr.Store(&err)
// s.abort is buffered so this will never block
s.abort <- err
})
} }
func (s *Shim) Start() (*time.Time, error) { func (s *Shim) AbortWait(err error) {
// start user switcher process and save time s.Abort(err)
var fsu string <-s.abort
if p, ok := internal.Check(internal.Fsu); !ok { }
fmsg.Fatal("invalid fsu path, this copy of fshim is not compiled correctly")
panic("unreachable")
} else {
fsu = p
}
s.cmd = exec.Command(fsu)
var encoder *gob.Encoder type CommandBuilder func(shimEnv string) (args []string)
if fd, e, err := proc.Setup(&s.cmd.ExtraFiles); err != nil {
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, return nil, fmsg.WrapErrorSuffix(err,
"cannot create shim setup pipe:") "cannot listen on shim setup socket:")
} else { } else {
encoder = e // accepts a connection after each call to accept
s.cmd.Env = []string{ // connections are sent to the channel cf
shim0.Env + "=" + strconv.Itoa(fd), cf, accept = c, a
"FORTIFY_APP_ID=" + s.aid,
}
} }
if len(s.supp) > 0 { // start user switcher process and save time
fmsg.VPrintf("attaching supplementary group ids %s", s.supp) s.cmd = exec.Command(s.executable, f(shim0.Env+"="+s.socket)...)
s.cmd.Env = append(s.cmd.Env, "FORTIFY_GROUPS="+strings.Join(s.supp, " ")) s.cmd.Env = []string{}
}
s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
s.cmd.Dir = "/" s.cmd.Dir = "/"
fmsg.VPrintln("starting shim via user switcher:", s.cmd)
// pass sync fd if set fmsg.Withhold() // withhold messages to stderr
if s.payload.Bwrap.Sync() != nil {
fd := proc.ExtraFile(s.cmd, s.payload.Bwrap.Sync())
s.payload.Sync = &fd
}
fmsg.VPrintln("starting shim via fsu:", s.cmd)
fmsg.Suspend() // withhold messages to stderr
if err := s.cmd.Start(); err != nil { if err := s.cmd.Start(); err != nil {
return nil, fmsg.WrapErrorSuffix(err, return nil, fmsg.WrapErrorSuffix(err,
"cannot start fsu:") "cannot start user switcher:")
} }
startTime := time.Now().UTC() startTime := time.Now().UTC()
// kill shim if something goes wrong and an error is returned // kill shim if something goes wrong and an error is returned
s.killFallback = make(chan error, 1)
killShim := func() { killShim := func() {
if err := s.cmd.Process.Signal(os.Interrupt); err != nil { if err := s.cmd.Process.Signal(os.Interrupt); err != nil {
s.killFallback <- err fmsg.Println("cannot terminate shim on faulted setup:", err)
} }
} }
defer func() { killShim() }() defer func() { killShim() }()
// take alternative exit path on signal accept()
sig := make(chan os.Signal, 2) var conn *net.UnixConn
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
go func() {
v := <-sig
fmsg.Printf("got %s after program start", v)
s.killFallback <- nil
signal.Ignore(syscall.SIGINT, syscall.SIGTERM)
}()
shimErr := make(chan error)
go func() { shimErr <- encoder.Encode(s.payload) }()
select { select {
case err := <-shimErr: case c := <-cf:
if err != nil { if c == nil {
return &startTime, fmsg.WrapErrorSuffix(err, return &startTime, fmsg.WrapErrorSuffix(*s.abortErr.Load(), "cannot accept call on setup socket:")
"cannot transmit shim config:") } else {
conn = c
} }
killShim = func() {}
case <-time.After(shimSetupTimeout): case <-time.After(shimSetupTimeout):
return &startTime, fmsg.WrapError(errors.New("timed out waiting for shim"), err := fmsg.WrapError(errors.New("timed out waiting for shim"),
"timed out waiting for shim") "timed out waiting for shim to connect")
s.AbortWait(err)
return &startTime, err
} }
return &startTime, nil // 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() {
cfWg := new(sync.WaitGroup)
for {
select {
case err = <-s.abort:
if err != nil {
fmsg.VPrintln("aborting shim setup, reason:", err)
}
if err = l.Close(); err != nil {
fmsg.Println("cannot close setup socket:", err)
}
close(s.abort)
go func() {
cfWg.Wait()
close(cf)
}()
return
case <-accept:
cfWg.Add(1)
go func() {
defer cfWg.Done()
if conn, err0 := l.AcceptUnix(); err0 != nil {
// breaks loop
s.Abort(err0)
// receiver sees nil value and loads err0 stored during abort
cf <- nil
} else {
cf <- conn
}
}()
}
}
}()
}
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
}
err0 := raw.Control(func(fd uintptr) {
ucred, err = syscall.GetsockoptUcred(int(fd), syscall.SOL_SOCKET, syscall.SO_PEERCRED)
})
err = errors.Join(err, err0)
return
} }

75
cmd/fshim/ipc/wayland.go Normal file
View File

@ -0,0 +1,75 @@
package shim0
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

@ -1,18 +1,19 @@
package main package main
import ( import (
"encoding/gob"
"errors" "errors"
"net"
"os" "os"
"path" "path"
"strconv" "strconv"
"syscall" "syscall"
init0 "git.gensokyo.uk/security/fortify/cmd/finit/ipc" init0 "git.ophivana.moe/security/fortify/cmd/finit/ipc"
shim "git.gensokyo.uk/security/fortify/cmd/fshim/ipc" shim "git.ophivana.moe/security/fortify/cmd/fshim/ipc"
"git.gensokyo.uk/security/fortify/helper" "git.ophivana.moe/security/fortify/helper"
"git.gensokyo.uk/security/fortify/internal" "git.ophivana.moe/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/proc"
) )
// everything beyond this point runs as unconstrained target user // everything beyond this point runs as unconstrained target user
@ -37,6 +38,15 @@ func main() {
} }
} }
// lookup socket path from environment
var socketPath string
if s, ok := os.LookupEnv(shim.Env); !ok {
fmsg.Fatal("FORTIFY_SHIM not set")
panic("unreachable")
} else {
socketPath = s
}
// check path to finit // check path to finit
var finitPath string var finitPath string
if p, ok := internal.Path(internal.Finit); !ok { if p, ok := internal.Path(internal.Finit); !ok {
@ -45,38 +55,40 @@ func main() {
finitPath = p finitPath = p
} }
// receive setup payload // dial setup socket
var ( var conn *net.UnixConn
payload shim.Payload if c, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: socketPath, Net: "unix"}); err != nil {
closeSetup func() error fmsg.Fatal("cannot dial setup socket:", err)
)
if f, err := proc.Receive(shim.Env, &payload); err != nil {
if errors.Is(err, proc.ErrInvalid) {
fmsg.Fatal("invalid config descriptor")
}
if errors.Is(err, proc.ErrNotSet) {
fmsg.Fatal("FORTIFY_SHIM not set")
}
fmsg.Fatalf("cannot decode shim setup payload: %v", err)
panic("unreachable") panic("unreachable")
} else {
conn = c
}
// decode payload gob stream
var payload shim.Payload
if err := gob.NewDecoder(conn).Decode(&payload); err != nil {
fmsg.Fatal("cannot decode shim payload:", err)
} else { } else {
fmsg.SetVerbose(payload.Verbose) fmsg.SetVerbose(payload.Verbose)
closeSetup = f
} }
if payload.Bwrap == nil { if payload.Bwrap == nil {
fmsg.Fatal("bwrap config not supplied") fmsg.Fatal("bwrap config not supplied")
} }
// restore bwrap sync fd // receive wayland fd over socket
if payload.Sync != nil { wfd := -1
payload.Bwrap.SetSync(os.NewFile(*payload.Sync, "sync")) if payload.WL {
if fd, err := receiveWLfd(conn); err != nil {
fmsg.Fatal("cannot receive wayland fd:", err)
} else {
wfd = fd
}
} }
// close setup socket // close setup socket
if err := closeSetup(); err != nil { if err := conn.Close(); err != nil {
fmsg.Println("cannot close setup pipe:", err) fmsg.Println("cannot close setup socket:", err)
// not fatal // not fatal
} }
@ -90,10 +102,7 @@ func main() {
} else { } else {
// no argv, look up shell instead // no argv, look up shell instead
var ok bool var ok bool
if payload.Bwrap.SetEnv == nil { if ic.Argv0, ok = os.LookupEnv("SHELL"); !ok {
fmsg.Fatal("no command was specified and environment is unset")
}
if ic.Argv0, ok = payload.Bwrap.SetEnv["SHELL"]; !ok {
fmsg.Fatal("no command was specified and $SHELL was unset") fmsg.Fatal("no command was specified and $SHELL was unset")
} }
@ -104,15 +113,28 @@ func main() {
var extraFiles []*os.File var extraFiles []*os.File
// serve setup payload // pass wayland fd
if fd, encoder, err := proc.Setup(&extraFiles); err != nil { if wfd != -1 {
fmsg.Fatalf("cannot pipe: %v", err) if f := os.NewFile(uintptr(wfd), "wayland"); f != nil {
ic.WL = 3 + len(extraFiles)
extraFiles = append(extraFiles, f)
}
} else { } else {
conf.SetEnv[init0.Env] = strconv.Itoa(fd) ic.WL = -1
go func() { }
// share config pipe
if r, w, err := os.Pipe(); err != nil {
fmsg.Fatal("cannot pipe:", err)
} else {
conf.SetEnv[init0.Env] = strconv.Itoa(3 + len(extraFiles))
extraFiles = append(extraFiles, r)
fmsg.VPrintln("transmitting config to init") fmsg.VPrintln("transmitting config to init")
if err = encoder.Encode(&ic); err != nil { go func() {
fmsg.Fatalf("cannot transmit init config: %v", err) // stream config to pipe
if err = gob.NewEncoder(w).Encode(&ic); err != nil {
fmsg.Fatal("cannot transmit init config:", err)
} }
}() }()
} }
@ -120,7 +142,7 @@ func main() {
helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
if b, err := helper.NewBwrap(conf, nil, finitPath, if b, err := helper.NewBwrap(conf, nil, finitPath,
func(int, int) []string { return make([]string, 0) }); err != nil { func(int, int) []string { return make([]string, 0) }); err != nil {
fmsg.Fatalf("malformed sandbox config: %v", err) fmsg.Fatal("malformed sandbox config:", err)
} 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
@ -132,7 +154,7 @@ func main() {
// run and pass through exit code // run and pass through exit code
if err = b.Start(); err != nil { if err = b.Start(); err != nil {
fmsg.Fatalf("cannot start target process: %v", err) fmsg.Fatal("cannot start target process:", err)
} else if err = b.Wait(); err != nil { } else if err = b.Wait(); err != nil {
fmsg.VPrintln("wait:", err) fmsg.VPrintln("wait:", err)
} }
@ -143,3 +165,30 @@ func main() {
} }
} }
} }
func receiveWLfd(conn *net.UnixConn) (int, error) {
oob := make([]byte, syscall.CmsgSpace(4)) // single fd
if _, oobn, _, _, err := conn.ReadMsgUnix(nil, oob); err != nil {
return -1, err
} else if len(oob) != oobn {
return -1, errors.New("invalid message length")
}
var msg syscall.SocketControlMessage
if messages, err := syscall.ParseSocketControlMessage(oob); err != nil {
return -1, err
} else if len(messages) != 1 {
return -1, errors.New("unexpected message count")
} else {
msg = messages[0]
}
if fds, err := syscall.ParseUnixRights(&msg); err != nil {
return -1, err
} else if len(fds) != 1 {
return -1, errors.New("unexpected fd count")
} else {
return fds[0], nil
}
}

View File

@ -1,12 +1,10 @@
package main package main
import ( import (
"bytes" "bufio"
"fmt"
"log" "log"
"os" "os"
"path" "path"
"slices"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
@ -17,15 +15,9 @@ const (
fsuConfFile = "/etc/fsurc" fsuConfFile = "/etc/fsurc"
envShim = "FORTIFY_SHIM" envShim = "FORTIFY_SHIM"
envAID = "FORTIFY_APP_ID" envAID = "FORTIFY_APP_ID"
envGroups = "FORTIFY_GROUPS"
PR_SET_NO_NEW_PRIVS = 0x26
) )
var ( var Fmain = compPoison
Fmain = compPoison
Fshim = compPoison
)
func main() { func main() {
log.SetFlags(0) log.SetFlags(0)
@ -41,17 +33,12 @@ func main() {
log.Fatal("this program must not be started by root") log.Fatal("this program must not be started by root")
} }
var fmain, fshim string var fmain string
if p, ok := checkPath(Fmain); !ok { if p, ok := checkPath(Fmain); !ok {
log.Fatal("invalid fortify path, this copy of fsu is not compiled correctly") log.Fatal("invalid fortify path, this copy of fsu is not compiled correctly")
} else { } else {
fmain = p fmain = p
} }
if p, ok := checkPath(Fshim); !ok {
log.Fatal("invalid fshim path, this copy of fsu is not compiled correctly")
} else {
fshim = p
}
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe") pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
if p, err := os.Readlink(pexe); err != nil { if p, err := os.Readlink(pexe); err != nil {
@ -74,81 +61,84 @@ func main() {
uid += fid * 10000 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 // allowed aid range 0 to 9999
if as, ok := os.LookupEnv(envAID); !ok { if as, ok := os.LookupEnv(envAID); !ok {
log.Fatal("FORTIFY_APP_ID not set") log.Fatal("FORTIFY_APP_ID not set")
} else if aid, err := parseUint32Fast(as); err != nil || aid < 0 || aid > 9999 { } else if aid, err := strconv.Atoi(as); err != nil || aid < 0 || aid > 9999 {
log.Fatal("invalid aid") log.Fatal("invalid aid")
} else { } else {
uid += aid uid += aid
} }
// pass through setup fd to shim
var shimSetupFd string
if s, ok := os.LookupEnv(envShim); !ok {
// fortify requests target uid
// print resolved uid and exit
fmt.Print(uid)
os.Exit(0)
} else if len(s) != 1 || s[0] > '9' || s[0] < '3' {
log.Fatal("FORTIFY_SHIM holds an invalid value")
} else {
shimSetupFd = s
}
// supplementary groups
var suppGroups, suppCurrent []int
if gs, ok := os.LookupEnv(envGroups); ok {
if cur, err := os.Getgroups(); err != nil {
log.Fatalf("cannot get groups: %v", err)
} else {
suppCurrent = cur
}
// parse space-separated list of group ids
gss := bytes.Split([]byte(gs), []byte{' '})
suppGroups = make([]int, len(gss)+1)
for i, s := range gss {
if gid, err := strconv.Atoi(string(s)); err != nil {
log.Fatalf("cannot parse %q: %v", string(s), err)
} else if gid > 0 && gid != uid && gid != os.Getgid() && slices.Contains(suppCurrent, gid) {
suppGroups[i] = gid
} else {
log.Fatalf("invalid gid %d", gid)
}
}
suppGroups[len(suppGroups)-1] = uid
} else {
suppGroups = []int{uid}
}
// final bounds check to catch any bugs
if uid < 1000000 || uid >= 2000000 {
panic("uid out of bounds")
}
// careful! users in the allowlist is effectively allowed to drop groups via fsu
if err := syscall.Setresgid(uid, uid, uid); err != nil { if err := syscall.Setresgid(uid, uid, uid); err != nil {
log.Fatalf("cannot set gid: %v", err) log.Fatalf("cannot set gid: %v", err)
} }
if err := syscall.Setgroups(suppGroups); err != nil {
log.Fatalf("cannot set supplementary groups: %v", err)
}
if err := syscall.Setresuid(uid, uid, uid); err != nil { if err := syscall.Setresuid(uid, uid, uid); err != nil {
log.Fatalf("cannot set uid: %v", err) log.Fatalf("cannot set uid: %v", err)
} }
if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 { if err := syscall.Exec(fmain, []string{"fortify", "shim"}, []string{envShim + "=" + shimSetupPath}); err != nil {
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
}
if err := syscall.Exec(fshim, []string{"fshim"}, []string{envShim + "=" + shimSetupFd}); err != nil {
log.Fatalf("cannot start shim: %v", err) log.Fatalf("cannot start shim: %v", err)
} }
panic("unreachable") 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
}
}
func checkPath(p string) (string, bool) { func checkPath(p string) (string, bool) {
return p, p != compPoison && p != "" && path.IsAbs(p) return p, p != compPoison && p != "" && path.IsAbs(p)
} }

View File

@ -1,77 +0,0 @@
package main
import (
"bufio"
"errors"
"fmt"
"log"
"os"
"strings"
"syscall"
)
func parseUint32Fast(s string) (int, error) {
sLen := len(s)
if sLen < 1 {
return -1, errors.New("zero length string")
}
if sLen > 10 {
return -1, errors.New("string too long")
}
n := 0
for i, ch := range []byte(s) {
ch -= '0'
if ch > 9 {
return -1, fmt.Errorf("invalid character '%s' at index %d", string([]byte{ch}), i)
}
n = n*10 + int(ch)
}
return n, nil
}
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 = parseUint32Fast(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 = parseUint32Fast(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

@ -1,69 +0,0 @@
package main
import (
"bytes"
"errors"
"flag"
"fmt"
"os"
"path"
"strconv"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
func main() {
fmsg.SetPrefix("fuserdb")
const varEmpty = "/var/empty"
out := flag.String("o", "userdb", "output directory")
homeDir := flag.String("d", varEmpty, "parent of home directories")
shell := flag.String("s", "/sbin/nologin", "absolute path to subordinate user shell")
flag.Parse()
type user struct {
name string
fid int
}
users := make([]user, len(flag.Args()))
for i, s := range flag.Args() {
f := bytes.SplitN([]byte(s), []byte{':'}, 2)
if len(f) != 2 {
fmsg.Fatalf("invalid entry at index %d", i)
}
users[i].name = string(f[0])
if fid, err := strconv.Atoi(string(f[1])); err != nil {
fmsg.Fatal(err.Error())
} else {
users[i].fid = fid
}
}
if err := os.MkdirAll(*out, 0755); err != nil && !errors.Is(err, os.ErrExist) {
fmsg.Fatalf("cannot create output: %v", err)
}
for _, u := range users {
fidString := strconv.Itoa(u.fid)
for aid := 0; aid < 10000; aid++ {
userName := fmt.Sprintf("u%d_a%d", u.fid, aid)
uid := 1000000 + u.fid*10000 + aid
us := strconv.Itoa(uid)
realName := fmt.Sprintf("Fortify subordinate user %d (%s)", aid, u.name)
var homeDirectory string
if *homeDir != varEmpty {
homeDirectory = path.Join(*homeDir, "u"+fidString, "a"+strconv.Itoa(aid))
} else {
homeDirectory = varEmpty
}
writeUser(userName, uid, us, realName, homeDirectory, *shell, *out)
writeGroup(userName, uid, us, nil, *out)
}
}
fmsg.Printf("created %d entries", len(users)*2*10000)
fmsg.Exit(0)
}

View File

@ -1,64 +0,0 @@
package main
import (
"encoding/json"
"os"
"path"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
type payloadU struct {
UserName string `json:"userName"`
Uid int `json:"uid"`
Gid int `json:"gid"`
MemberOf []string `json:"memberOf,omitempty"`
RealName string `json:"realName"`
HomeDirectory string `json:"homeDirectory"`
Shell string `json:"shell"`
}
func writeUser(userName string, uid int, us string, realName, homeDirectory, shell string, out string) {
userFileName := userName + ".user"
if f, err := os.OpenFile(path.Join(out, userFileName), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
fmsg.Fatalf("cannot create %s: %v", userName, err)
} else if err = json.NewEncoder(f).Encode(&payloadU{
UserName: userName,
Uid: uid,
Gid: uid,
RealName: realName,
HomeDirectory: homeDirectory,
Shell: shell,
}); err != nil {
fmsg.Fatalf("cannot serialise %s: %v", userName, err)
} else if err = f.Close(); err != nil {
fmsg.Printf("cannot close %s: %v", userName, err)
}
if err := os.Symlink(userFileName, path.Join(out, us+".user")); err != nil {
fmsg.Fatalf("cannot link %s: %v", userName, err)
}
}
type payloadG struct {
GroupName string `json:"groupName"`
Gid int `json:"gid"`
Members []string `json:"members,omitempty"`
}
func writeGroup(groupName string, gid int, gs string, members []string, out string) {
groupFileName := groupName + ".group"
if f, err := os.OpenFile(path.Join(out, groupFileName), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
fmsg.Fatalf("cannot create %s: %v", groupName, err)
} else if err = json.NewEncoder(f).Encode(&payloadG{
GroupName: groupName,
Gid: gid,
Members: members,
}); err != nil {
fmsg.Fatalf("cannot serialise %s: %v", groupName, err)
} else if err = f.Close(); err != nil {
fmsg.Printf("cannot close %s: %v", groupName, err)
}
if err := os.Symlink(groupFileName, path.Join(out, gs+".group")); err != nil {
fmsg.Fatalf("cannot link %s: %v", groupName, err)
}
}

View File

@ -1,51 +0,0 @@
#compdef fortify
_fortify_app() {
_path_files -g "*.(json|ftfy)"
}
_fortify_run() {
_arguments \
'--id[App ID, leave empty to disable security context app_id]:id' \
'-a[Fortify application ID]: :_numbers' \
'-g[Groups inherited by the app process]: :_groups' \
'-d[Application home directory]: :_files -/' \
'-u[Passwd name within sandbox]: :_users' \
'--wayland[Share Wayland socket]' \
'-X[Share X11 socket and allow connection]' \
'--dbus[Proxy D-Bus connection]' \
'--pulse[Share PulseAudio socket and cookie]' \
'--dbus-config[Path to D-Bus proxy config file]: :_files -g "*.json"' \
'--dbus-system[Path to system D-Bus proxy config file]: :_files -g "*.json"' \
'--mpris[Allow owning MPRIS D-Bus path]' \
'--dbus-log[Force logging in the D-Bus proxy]'
}
(( $+functions[_fortify_commands] )) || _fortify_commands()
{
local -a _fortify_cmds
_fortify_cmds=(
"app:Launch app defined by the specified config file"
"run:Configure and start a permissive default sandbox"
"ps:List active apps and their state"
"version:Show fortify version"
"license:Show full license text"
"template:Produce a config template"
"help:Show help message"
)
if (( CURRENT == 1 )); then
_describe -t commands 'fortify command' _fortify_cmds || compadd "$@"
else
local curcontext="$curcontext"
cmd="${${_fortify_cmds[(r)$words[1]:*]%%:*}}"
if (( $+functions[_fortify_$cmd] )); then
_fortify_$cmd
else
_message "no more options"
fi
fi
}
_arguments -C \
'-v[Verbose output]' \
'*::fortify command:_fortify_commands'

135
config.go Normal file
View File

@ -0,0 +1,135 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/internal/app"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/system"
)
var (
printTemplate bool
confPath string
dbusConfigSession string
dbusConfigSystem string
dbusID string
mpris bool
dbusVerbose bool
userName string
enablements [system.ELen]bool
launchMethodText string
)
func init() {
flag.BoolVar(&printTemplate, "template", false, "Print a full config template and exit")
// config file, disables every other flag here
flag.StringVar(&confPath, "c", "nil", "Path to full app configuration, or \"nil\" to configure from flags")
flag.StringVar(&dbusConfigSession, "dbus-config", "builtin", "Path to D-Bus proxy config file, or \"builtin\" for defaults")
flag.StringVar(&dbusConfigSystem, "dbus-system", "nil", "Path to system D-Bus proxy config file, or \"nil\" to disable")
flag.StringVar(&dbusID, "dbus-id", "", "D-Bus ID of application, leave empty to disable own paths, has no effect if custom config is available")
flag.BoolVar(&mpris, "mpris", false, "Allow owning MPRIS D-Bus path, has no effect if custom config is available")
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.BoolVar(&enablements[system.EWayland], "wayland", false, "Share Wayland socket")
flag.BoolVar(&enablements[system.EX11], "X", false, "Share X11 socket and allow connection")
flag.BoolVar(&enablements[system.EDBus], "dbus", false, "Proxy D-Bus connection")
flag.BoolVar(&enablements[system.EPulse], "pulse", false, "Share PulseAudio socket and cookie")
}
func init() {
methodHelpString := "Method of launching the child process, can be one of \"sudo\""
if os.SdBooted() {
methodHelpString += ", \"systemd\""
}
flag.StringVar(&launchMethodText, "method", "sudo", methodHelpString)
}
func tryTemplate() {
if printTemplate {
if s, err := json.MarshalIndent(app.Template(), "", " "); err != nil {
fmsg.Fatalf("cannot generate template: %v", err)
panic("unreachable")
} else {
fmt.Println(string(s))
}
fmsg.Exit(0)
}
}
func loadConfig() *app.Config {
if confPath == "nil" {
// config from flags
return configFromFlags()
} else {
// config from file
c := new(app.Config)
if f, err := os.Open(confPath); err != nil {
fmsg.Fatalf("cannot access config file %q: %s", confPath, err)
panic("unreachable")
} else if err = json.NewDecoder(f).Decode(&c); err != nil {
fmsg.Fatalf("cannot parse config file %q: %s", confPath, err)
panic("unreachable")
} else {
return c
}
}
}
func configFromFlags() (config *app.Config) {
// initialise config from flags
config = &app.Config{
ID: dbusID,
User: userName,
Command: flag.Args(),
Method: launchMethodText,
}
// enablements from flags
for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
if enablements[i] {
config.Confinement.Enablements.Set(i)
}
}
// parse D-Bus config file from flags if applicable
if enablements[system.EDBus] {
if dbusConfigSession == "builtin" {
config.Confinement.SessionBus = dbus.NewConfig(dbusID, true, mpris)
} else {
if c, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
fmsg.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
} else {
config.Confinement.SessionBus = c
}
}
// system bus proxy is optional
if dbusConfigSystem != "nil" {
if c, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
fmsg.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
} else {
config.Confinement.SystemBus = c
}
}
// override log from configuration
if dbusVerbose {
config.Confinement.SessionBus.Log = true
config.Confinement.SystemBus.Log = true
}
}
return
}

View File

@ -9,7 +9,7 @@ import (
"strings" "strings"
"testing" "testing"
"git.gensokyo.uk/security/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.gensokyo.uk/security/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
"git.gensokyo.uk/security/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.gensokyo.uk/security/fortify/helper" "git.ophivana.moe/security/fortify/helper"
"git.gensokyo.uk/security/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.

View File

@ -9,9 +9,9 @@ import (
"strconv" "strconv"
"strings" "strings"
"git.gensokyo.uk/security/fortify/helper" "git.ophivana.moe/security/fortify/helper"
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/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.

View File

@ -3,7 +3,7 @@ package dbus_test
import ( import (
"sync" "sync"
"git.gensokyo.uk/security/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.gensokyo.uk/security/fortify/helper" "git.ophivana.moe/security/fortify/helper"
) )
func TestHelperChildStub(t *testing.T) { func TestHelperChildStub(t *testing.T) {

1
dist/fsurc.default vendored
View File

@ -1 +0,0 @@
1000 0

10
dist/install.sh vendored
View File

@ -1,10 +0,0 @@
#!/bin/sh
cd "$(dirname -- "$0")" || exit 1
install -vDm0755 "bin/fortify" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fortify"
install -vDm0755 "bin/fshim" "${FORTIFY_INSTALL_PREFIX}/usr/libexec/fortify/fshim"
install -vDm0755 "bin/finit" "${FORTIFY_INSTALL_PREFIX}/usr/libexec/fortify/finit"
install -vDm0755 "bin/fuserdb" "${FORTIFY_INSTALL_PREFIX}/usr/libexec/fortify/fuserdb"
install -vDm6511 "bin/fsu" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fsu"
install -vDm0400 "fsurc.default" "${FORTIFY_INSTALL_PREFIX}/etc/fsurc"

19
dist/release.sh vendored
View File

@ -1,19 +0,0 @@
#!/bin/sh -e
cd "$(dirname -- "$0")/.."
VERSION="${FORTIFY_VERSION:-untagged}"
pname="fortify-${VERSION}"
out="dist/${pname}"
mkdir -p "${out}"
cp "README.md" "dist/fsurc.default" "dist/install.sh" "${out}"
go build -v -o "${out}/bin/" -ldflags "-s -w
-X git.gensokyo.uk/security/fortify/internal.Version=${VERSION}
-X git.gensokyo.uk/security/fortify/internal.Fsu=/usr/bin/fsu
-X git.gensokyo.uk/security/fortify/internal.Finit=/usr/libexec/fortify/finit
-X main.Fmain=/usr/bin/fortify
-X main.Fshim=/usr/libexec/fortify/fshim" ./...
rm -f "./${out}.tar.gz" && tar -C dist -czf "${out}.tar.gz" "${pname}"
rm -rf "./${out}"
sha512sum "${out}.tar.gz" > "${out}.tar.gz.sha512"

View File

@ -3,8 +3,8 @@ package main
import ( import (
"errors" "errors"
"git.gensokyo.uk/security/fortify/internal/app" "git.ophivana.moe/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
func logWaitError(err error) { func logWaitError(err error) {

30
flake.lock generated
View File

@ -1,45 +1,23 @@
{ {
"nodes": { "nodes": {
"home-manager": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1733951536,
"narHash": "sha256-Zb5ZCa7Xj+0gy5XVXINTSr71fCfAv+IKtmIXNrykT54=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "1318c3f3b068cdcea922fa7c1a0a1f0c96c22f5f",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-24.11",
"repo": "home-manager",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1734298236, "lastModified": 1725361206,
"narHash": "sha256-aWhhqY44xBjMoO9r5fyPp5u8tqUNWRZ/m/P+abMSs5c=", "narHash": "sha256-/HTUg+kMaqBPGrcQBYboAMsQHIWIkuKRDldss/035Hc=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "eb919d9300b6a18f8583f58aef16db458fbd7bec", "rev": "2830c7c930311397d94c0b86a359c865c081c875",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-24.11-small", "ref": "nixos-unstable-small",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
}, },
"root": { "root": {
"inputs": { "inputs": {
"home-manager": "home-manager",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
} }
} }

109
flake.nix
View File

@ -2,20 +2,11 @@
description = "fortify sandbox tool and nixos module"; description = "fortify sandbox tool and nixos module";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11-small"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable-small";
home-manager = {
url = "github:nix-community/home-manager/release-24.11";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = outputs =
{ { self, nixpkgs }:
self,
nixpkgs,
home-manager,
}:
let let
supportedSystems = [ supportedSystems = [
"aarch64-linux" "aarch64-linux"
@ -29,55 +20,6 @@
{ {
nixosModules.fortify = import ./nixos.nix; nixosModules.fortify = import ./nixos.nix;
checks = forAllSystems (
system:
let
pkgs = nixpkgsFor.${system};
inherit (pkgs)
runCommandLocal
callPackage
nixfmt-rfc-style
deadnix
statix
;
in
{
check-formatting =
runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; }
''
cd ${./.}
echo "running nixfmt..."
nixfmt --check .
touch $out
'';
check-lint =
runCommandLocal "check-lint"
{
nativeBuildInputs = [
deadnix
statix
];
}
''
cd ${./.}
echo "running deadnix..."
deadnix --fail
echo "running statix..."
statix check .
touch $out
'';
nixos-tests = callPackage ./test.nix { inherit system self home-manager; };
}
);
packages = forAllSystems ( packages = forAllSystems (
system: system:
let let
@ -95,58 +37,11 @@
buildInputs = with nixpkgsFor.${system}; self.packages.${system}.fortify.buildInputs; buildInputs = with nixpkgsFor.${system}; self.packages.${system}.fortify.buildInputs;
}; };
fhs = nixpkgsFor.${system}.buildFHSEnv {
pname = "fortify-fhs";
inherit (self.packages.${system}.fortify) version;
targetPkgs =
pkgs: with pkgs; [
go
gcc
pkg-config
acl
wayland
wayland-scanner
wayland-protocols
xorg.libxcb
];
extraOutputsToInstall = [ "dev" ];
profile = ''
export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
'';
};
withPackage = nixpkgsFor.${system}.mkShell { 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 ];
}; };
generateDoc =
let
pkgs = nixpkgsFor.${system};
inherit (pkgs) lib;
doc =
let
eval = lib.evalModules {
specialArgs = {
inherit pkgs;
};
modules = [ ./options.nix ];
};
cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval;
in
pkgs.nixosOptionsDoc { inherit (cleanEval) options; };
docText = pkgs.runCommand "fortify-module-docs.md" { } ''
cat ${doc.optionsCommonMark} > $out
sed -i '/*Declared by:*/,+1 d' $out
'';
in
nixpkgsFor.${system}.mkShell {
shellHook = ''
exec cat ${docText} > options.md
'';
};
}); });
}; };
} }

View File

@ -1,48 +0,0 @@
package fst
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
)
type ID [16]byte
var (
ErrInvalidLength = errors.New("string representation must have a length of 32")
)
func (a *ID) String() string {
return hex.EncodeToString(a[:])
}
func NewAppID(id *ID) error {
_, err := rand.Read(id[:])
return err
}
func ParseAppID(id *ID, s string) error {
if len(s) != 32 {
return ErrInvalidLength
}
for i, b := range s {
if b < '0' || b > 'f' {
return fmt.Errorf("invalid char %q at byte %d", b, i)
}
v := uint8(b)
if v > '9' {
v = 10 + v - 'a'
} else {
v -= '0'
}
if i%2 == 0 {
v <<= 4
}
id[i/2] += v
}
return nil
}

View File

@ -1,63 +0,0 @@
package fst_test
import (
"errors"
"testing"
"git.gensokyo.uk/security/fortify/fst"
)
func TestParseAppID(t *testing.T) {
t.Run("bad length", func(t *testing.T) {
if err := fst.ParseAppID(new(fst.ID), "meow"); !errors.Is(err, fst.ErrInvalidLength) {
t.Errorf("ParseAppID: error = %v, wantErr = %v", err, fst.ErrInvalidLength)
}
})
t.Run("bad byte", func(t *testing.T) {
wantErr := "invalid char '\\n' at byte 15"
if err := fst.ParseAppID(new(fst.ID), "02bc7f8936b2af6\n\ne2535cd71ef0bb7"); err == nil || err.Error() != wantErr {
t.Errorf("ParseAppID: error = %v, wantErr = %v", err, wantErr)
}
})
t.Run("fuzz 16 iterations", func(t *testing.T) {
for i := 0; i < 16; i++ {
testParseAppIDWithRandom(t)
}
})
}
func FuzzParseAppID(f *testing.F) {
for i := 0; i < 16; i++ {
id := new(fst.ID)
if err := fst.NewAppID(id); err != nil {
panic(err.Error())
}
f.Add(id[0], id[1], id[2], id[3], id[4], id[5], id[6], id[7], id[8], id[9], id[10], id[11], id[12], id[13], id[14], id[15])
}
f.Fuzz(func(t *testing.T, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15 byte) {
testParseAppID(t, &fst.ID{b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15})
})
}
func testParseAppIDWithRandom(t *testing.T) {
id := new(fst.ID)
if err := fst.NewAppID(id); err != nil {
t.Fatalf("cannot generate app ID: %v", err)
}
testParseAppID(t, id)
}
func testParseAppID(t *testing.T, id *fst.ID) {
s := id.String()
got := new(fst.ID)
if err := fst.ParseAppID(got, s); err != nil {
t.Fatalf("cannot parse app ID: %v", err)
}
if *got != *id {
t.Fatalf("ParseAppID(%#v) = \n%#v, want \n%#v", s, got, id)
}
}

View File

@ -1,2 +0,0 @@
// Package fst exports shared fortify types.
package fst

2
go.mod
View File

@ -1,3 +1,3 @@
module git.gensokyo.uk/security/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.gensokyo.uk/security/fortify/helper" "git.ophivana.moe/security/fortify/helper"
) )
func Test_argsFD_String(t *testing.T) { func Test_argsFD_String(t *testing.T) {

View File

@ -3,13 +3,11 @@ package helper
import ( import (
"errors" "errors"
"io" "io"
"os"
"os/exec" "os/exec"
"strconv" "strconv"
"sync" "sync"
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/proc"
) )
// BubblewrapName is the file name or path to bubblewrap. // BubblewrapName is the file name or path to bubblewrap.
@ -21,8 +19,6 @@ type bubblewrap struct {
// bwrap pipes // bwrap pipes
p *pipes p *pipes
// sync pipe
sync *os.File
// returns an array of arguments passed directly // returns an array of arguments passed directly
// to the child process spawned by bwrap // to the child process spawned by bwrap
argF func(argsFD, statFD int) []string argF func(argsFD, statFD int) []string
@ -76,10 +72,6 @@ func (b *bubblewrap) StartNotify(ready chan error) error {
b.Cmd.Env = append(b.Cmd.Env, FortifyHelper+"=1", FortifyStatus+"=-1") b.Cmd.Env = append(b.Cmd.Env, FortifyHelper+"=1", FortifyStatus+"=-1")
} }
if b.sync != nil {
b.Cmd.Args = append(b.Cmd.Args, "--sync-fd", strconv.Itoa(int(proc.ExtraFile(b.Cmd, b.sync))))
}
if err := b.Cmd.Start(); err != nil { if err := b.Cmd.Start(); err != nil {
return err return err
} }
@ -139,7 +131,6 @@ func NewBwrap(conf *bwrap.Config, wt io.WriterTo, name string, argF func(argsFD,
b.p = &pipes{args: args} b.p = &pipes{args: args}
} }
b.sync = conf.Sync()
b.argF = argF b.argF = argF
b.name = name b.name = name
if wt != nil { if wt != nil {

View File

@ -68,16 +68,13 @@ type Config struct {
// (--as-pid-1) // (--as-pid-1)
AsInit bool `json:"as_init"` AsInit bool `json:"as_init"`
// keep this fd open while sandbox is running
// (--sync-fd FD)
sync *os.File
/* unmapped options include: /* unmapped options include:
--unshare-user-try Create new user namespace if possible else continue by skipping it --unshare-user-try Create new user namespace if possible else continue by skipping it
--unshare-cgroup-try Create new cgroup namespace if possible else continue by skipping it --unshare-cgroup-try Create new cgroup namespace if possible else continue by skipping it
--userns FD Use this user namespace (cannot combine with --unshare-user) --userns FD Use this user namespace (cannot combine with --unshare-user)
--userns2 FD After setup switch to this user namespace, only useful with --userns --userns2 FD After setup switch to this user namespace, only useful with --userns
--pidns FD Use this pid namespace (as parent namespace if using --unshare-pid) --pidns FD Use this pid namespace (as parent namespace if using --unshare-pid)
--sync-fd FD Keep this fd open while sandbox is running
--exec-label LABEL Exec label for the sandbox --exec-label LABEL Exec label for the sandbox
--file-label LABEL File label for temporary sandbox content --file-label LABEL File label for temporary sandbox content
--file FD DEST Copy from FD to destination DEST --file FD DEST Copy from FD to destination DEST
@ -95,12 +92,6 @@ type Config struct {
among which --args is used internally for passing arguments */ among which --args is used internally for passing arguments */
} }
// Sync keep this fd open while sandbox is running
// (--sync-fd FD)
func (c *Config) Sync() *os.File {
return c.sync
}
type UnshareConfig struct { type UnshareConfig struct {
// (--unshare-user) // (--unshare-user)
// create new user namespace // create new user namespace

View File

@ -136,10 +136,3 @@ func (c *Config) SetGID(gid int) *Config {
} }
return c return c
} }
// SetSync sets the sync pipe kept open while sandbox is running
// (--sync-fd FD)
func (c *Config) SetSync(s *os.File) *Config {
c.sync = s
return c
}

View File

@ -7,8 +7,8 @@ import (
"strings" "strings"
"testing" "testing"
"git.gensokyo.uk/security/fortify/helper" "git.ophivana.moe/security/fortify/helper"
"git.gensokyo.uk/security/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.gensokyo.uk/security/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.gensokyo.uk/security/fortify/helper" "git.ophivana.moe/security/fortify/helper"
) )
var ( var (

View File

@ -5,8 +5,6 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"git.gensokyo.uk/security/fortify/internal/proc"
) )
type pipes struct { type pipes struct {
@ -49,21 +47,24 @@ func (p *pipes) pipe() error {
} }
// calls pipe to create pipes and sets them up as ExtraFiles, returning their fd // calls pipe to create pipes and sets them up as ExtraFiles, returning their fd
func (p *pipes) prepareCmd(cmd *exec.Cmd) (argsFd, statFd int, err error) { func (p *pipes) prepareCmd(cmd *exec.Cmd) (int, int, error) {
argsFd, statFd = -1, -1 if err := p.pipe(); err != nil {
if err = p.pipe(); err != nil { return -1, -1, err
return
} }
// save a reference of cmd for future use // save a reference of cmd for future use
p.cmd = cmd p.cmd = cmd
argsFd = int(proc.ExtraFile(cmd, p.argsP[0])) // ExtraFiles: If non-nil, entry i becomes file descriptor 3+i.
if p.ready != nil { argsFd := 3 + len(cmd.ExtraFiles)
statFd = int(proc.ExtraFile(cmd, p.statP[1])) cmd.ExtraFiles = append(cmd.ExtraFiles, p.argsP[0])
}
return if p.ready != nil {
cmd.ExtraFiles = append(cmd.ExtraFiles, p.statP[1])
return argsFd, argsFd + 1, nil
} else {
return argsFd, -1, nil
}
} }
func (p *pipes) readyWriteArgs() error { func (p *pipes) readyWriteArgs() error {

View File

@ -10,8 +10,8 @@ import (
"syscall" "syscall"
"testing" "testing"
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/fmsg" "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;

View File

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

View File

@ -2,16 +2,14 @@ package app
import ( import (
"sync" "sync"
"sync/atomic"
"git.gensokyo.uk/security/fortify/cmd/fshim/ipc/shim" "git.ophivana.moe/security/fortify/cmd/fshim/ipc/shim"
"git.gensokyo.uk/security/fortify/fst" "git.ophivana.moe/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/linux"
) )
type App interface { type App interface {
// ID returns a copy of App's unique ID. // ID returns a copy of App's unique ID.
ID() fst.ID ID() ID
// Start sets up the system and starts the App. // 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 waits for App's process to exit and reverts system setup.
@ -19,16 +17,13 @@ type App interface {
// WaitErr returns error returned by the underlying wait syscall. // WaitErr returns error returned by the underlying wait syscall.
WaitErr() error WaitErr() error
Seal(config *fst.Config) error Seal(config *Config) error
String() string String() string
} }
type app struct { type app struct {
// single-use config reference
ct *appCt
// application unique identifier // application unique identifier
id *fst.ID id *ID
// operating system interface // operating system interface
os linux.System os linux.System
// shim process manager // shim process manager
@ -41,7 +36,7 @@ type app struct {
lock sync.RWMutex lock sync.RWMutex
} }
func (a *app) ID() fst.ID { func (a *app) ID() ID {
return *a.id return *a.id
} }
@ -58,7 +53,7 @@ func (a *app) String() string {
} }
if a.seal != nil { if a.seal != nil {
return "(sealed fortified app as uid " + a.seal.sys.user.us + ")" return "(sealed fortified app as uid " + a.seal.sys.user.Uid + ")"
} }
return "(unsealed fortified app)" return "(unsealed fortified app)"
@ -70,28 +65,7 @@ func (a *app) WaitErr() error {
func New(os linux.System) (App, error) { func New(os linux.System) (App, error) {
a := new(app) a := new(app)
a.id = new(fst.ID) a.id = new(ID)
a.os = os a.os = os
return a, fst.NewAppID(a.id) return a, newAppID(a.id)
}
// appCt ensures its wrapped val is only accessed once
type appCt struct {
val *fst.Config
done *atomic.Bool
}
func (a *appCt) Unwrap() *fst.Config {
if !a.done.Load() {
defer a.done.Store(true)
return a.val
}
panic("attempted to access config reference twice")
}
func newAppCt(config *fst.Config) (ct *appCt) {
ct = new(appCt)
ct.done = new(atomic.Bool)
ct.val = config
return ct
} }

View File

@ -1,87 +1,278 @@
package app_test package app_test
import ( import (
"git.gensokyo.uk/security/fortify/acl" "fmt"
"git.gensokyo.uk/security/fortify/dbus" "io/fs"
"git.gensokyo.uk/security/fortify/fst" "os/user"
"git.gensokyo.uk/security/fortify/helper/bwrap" "strconv"
"git.gensokyo.uk/security/fortify/internal/system"
"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/app"
"git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/system"
) )
var testCasesNixos = []sealTestCase{ var testCasesNixos = []sealTestCase{
{ {
"nixos chromium direct wayland", new(stubNixOS), "nixos permissive defaults no enablements", new(stubNixOS),
&fst.Config{ &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/65534",
"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/65534", 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", ID: "org.chromium.Chromium",
Command: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"}, User: "chronos",
Confinement: fst.ConfinementConfig{ Command: []string{"/run/current-system/sw/bin/zsh", "-c", "exec chromium "},
AppID: 1, Groups: []string{}, Username: "u0_a1", Confinement: app.ConfinementConfig{
Outer: "/var/lib/persist/module/fortify/0/1",
Sandbox: &fst.SandboxConfig{
UserNS: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil,
Filesystem: []*fst.FilesystemConfig{
{Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true},
{Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true},
{Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"},
{Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true},
}, AutoEtc: true,
Override: []string{"/var/run/nscd"},
},
SystemBus: &dbus.Config{
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
Filter: true,
},
SessionBus: &dbus.Config{ SessionBus: &dbus.Config{
Talk: []string{ Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications", "org.freedesktop.Notifications",
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets", "org.freedesktop.FileManager1",
"org.kde.kwalletd5", "org.kde.kwalletd6", "org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager",
}, },
Own: []string{ Own: []string{
"org.chromium.Chromium.*", "org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*", "org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*", "org.mpris.MediaPlayer2.chromium.*",
}, },
Call: map[string]string{}, Broadcast: map[string]string{}, 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, Filter: true,
}, },
Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(), Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(),
}, },
Method: "systemd",
}, },
fst.ID{ app.ID{
0x8e, 0x2c, 0x76, 0xb0, 0xeb, 0xf0, 0x83, 0xd1,
0x66, 0xda, 0xbe, 0x57, 0xb1, 0x75, 0x91, 0x17,
0x4c, 0xf0, 0x73, 0xbd, 0x82, 0xd4, 0x13, 0x36,
0xb4, 0x6e, 0xb5, 0xc1, 0x9b, 0x64, 0xce, 0x7c,
}, },
system.New(1000001). system.New(150).
Ensure("/tmp/fortify.1971", 0711). Ensure("/tmp/fortify.1971", 0701).
Ephemeral(system.Process, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1", 0711). 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", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/1", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/1", acl.Read, acl.Write, 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/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 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/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute). 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/8e2c76b066dabe574cf073bdb46eb5c1/passwd", "u0_a1:x:1971:1971:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n"). 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/8e2c76b066dabe574cf073bdb46eb5c1/group", "fortify:x:1971:\n"). WriteType(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/group", "fortify:x:65534:\n").
Link("/run/user/1971/wayland-0", "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/wayland"). 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). UpdatePermType(system.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute).
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse"). Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse").
CopyFile("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie"). CopyFile("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie").
MustProxyDBus("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", &dbus.Config{ MustProxyDBus("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", &dbus.Config{
Talk: []string{ Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications", "org.freedesktop.Notifications",
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets", "org.freedesktop.FileManager1",
"org.kde.kwalletd5", "org.kde.kwalletd6", "org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager",
}, },
Own: []string{ Own: []string{
"org.chromium.Chromium.*", "org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*", "org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*", "org.mpris.MediaPlayer2.chromium.*",
}, },
Call: map[string]string{}, Broadcast: map[string]string{}, Call: map[string]string{
"org.freedesktop.portal.*": "*",
},
Broadcast: map[string]string{
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
},
Filter: true, Filter: true,
}, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", &dbus.Config{ }, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", &dbus.Config{
Talk: []string{ Talk: []string{
"org.bluez", "org.bluez",
"org.freedesktop.Avahi", "org.freedesktop.Avahi",
@ -89,137 +280,317 @@ var testCasesNixos = []sealTestCase{
}, },
Filter: true, Filter: true,
}). }).
UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write). UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write), UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
(&bwrap.Config{ (&bwrap.Config{
Net: true, Net: true,
UserNS: true, UserNS: true,
Chdir: "/var/lib/persist/module/fortify/0/1",
Clearenv: true, Clearenv: true,
SetEnv: map[string]string{ SetEnv: map[string]string{
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1971/bus", "DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/65534/bus",
"DBUS_SYSTEM_BUS_ADDRESS": "unix:path=/run/dbus/system_bus_socket", "DBUS_SYSTEM_BUS_ADDRESS": "unix:path=/run/dbus/system_bus_socket",
"HOME": "/var/lib/persist/module/fortify/0/1", "HOME": "/home/chronos",
"PULSE_COOKIE": "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/pulse-cookie", "PULSE_COOKIE": "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie",
"PULSE_SERVER": "unix:/run/user/1971/pulse/native", "PULSE_SERVER": "unix:/run/user/65534/pulse/native",
"SHELL": "/run/current-system/sw/bin/zsh", "SHELL": "/run/current-system/sw/bin/zsh",
"TERM": "xterm-256color", "TERM": "xterm-256color",
"USER": "u0_a1", "USER": "chronos",
"WAYLAND_DISPLAY": "/run/user/1971/wayland-0", "WAYLAND_DISPLAY": "/run/user/65534/wayland-0",
"XDG_RUNTIME_DIR": "/run/user/1971", "XDG_RUNTIME_DIR": "/run/user/65534",
"XDG_SESSION_CLASS": "user", "XDG_SESSION_CLASS": "user",
"XDG_SESSION_TYPE": "tty", "XDG_SESSION_TYPE": "tty",
}, },
Chmod: make(bwrap.ChmodConfig), Chmod: make(bwrap.ChmodConfig),
NewSession: true,
DieWithParent: true, DieWithParent: true,
AsInit: true, AsInit: true,
}).SetUID(1971).SetGID(1971). }).SetUID(65534).SetGID(65534).
Procfs("/proc"). Procfs("/proc").DevTmpfs("/dev").Mqueue("/dev/mqueue").
Tmpfs("/fortify", 4096). Tmpfs("/dev/fortify", 4096).
DevTmpfs("/dev").Mqueue("/dev/mqueue"). Bind("/bin", "/bin", false, true).
Bind("/bin", "/bin"). Bind("/boot", "/boot", false, true).
Bind("/usr/bin", "/usr/bin"). Bind("/etc", "/dev/fortify/etc").
Bind("/nix/store", "/nix/store"). Bind("/home", "/home", false, true).
Bind("/run/current-system", "/run/current-system"). Bind("/lib", "/lib", false, true).
Bind("/sys/block", "/sys/block", true). Bind("/lib64", "/lib64", false, true).
Bind("/sys/bus", "/sys/bus", true). Bind("/nix", "/nix", false, true).
Bind("/sys/class", "/sys/class", true). Bind("/root", "/root", false, true).
Bind("/sys/dev", "/sys/dev", true). Bind("/srv", "/srv", false, true).
Bind("/sys/devices", "/sys/devices", true). Bind("/sys", "/sys", false, true).
Bind("/run/opengl-driver", "/run/opengl-driver"). 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). Bind("/dev/dri", "/dev/dri", true, true, true).
Bind("/etc", "/fortify/etc"). Symlink("/dev/fortify/etc/alsa", "/etc/alsa").
Symlink("/fortify/etc/alsa", "/etc/alsa"). Symlink("/dev/fortify/etc/bashrc", "/etc/bashrc").
Symlink("/fortify/etc/bashrc", "/etc/bashrc"). Symlink("/dev/fortify/etc/binfmt.d", "/etc/binfmt.d").
Symlink("/fortify/etc/binfmt.d", "/etc/binfmt.d"). Symlink("/dev/fortify/etc/dbus-1", "/etc/dbus-1").
Symlink("/fortify/etc/dbus-1", "/etc/dbus-1"). Symlink("/dev/fortify/etc/default", "/etc/default").
Symlink("/fortify/etc/default", "/etc/default"). Symlink("/dev/fortify/etc/ethertypes", "/etc/ethertypes").
Symlink("/fortify/etc/ethertypes", "/etc/ethertypes"). Symlink("/dev/fortify/etc/fonts", "/etc/fonts").
Symlink("/fortify/etc/fonts", "/etc/fonts"). Symlink("/dev/fortify/etc/fstab", "/etc/fstab").
Symlink("/fortify/etc/fstab", "/etc/fstab"). Symlink("/dev/fortify/etc/fuse.conf", "/etc/fuse.conf").
Symlink("/fortify/etc/fuse.conf", "/etc/fuse.conf"). Symlink("/dev/fortify/etc/host.conf", "/etc/host.conf").
Symlink("/fortify/etc/host.conf", "/etc/host.conf"). Symlink("/dev/fortify/etc/hostid", "/etc/hostid").
Symlink("/fortify/etc/hostid", "/etc/hostid"). Symlink("/dev/fortify/etc/hostname", "/etc/hostname").
Symlink("/fortify/etc/hostname", "/etc/hostname"). Symlink("/dev/fortify/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink("/fortify/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM"). Symlink("/dev/fortify/etc/hosts", "/etc/hosts").
Symlink("/fortify/etc/hosts", "/etc/hosts"). Symlink("/dev/fortify/etc/inputrc", "/etc/inputrc").
Symlink("/fortify/etc/inputrc", "/etc/inputrc"). Symlink("/dev/fortify/etc/ipsec.d", "/etc/ipsec.d").
Symlink("/fortify/etc/ipsec.d", "/etc/ipsec.d"). Symlink("/dev/fortify/etc/issue", "/etc/issue").
Symlink("/fortify/etc/issue", "/etc/issue"). Symlink("/dev/fortify/etc/kbd", "/etc/kbd").
Symlink("/fortify/etc/kbd", "/etc/kbd"). Symlink("/dev/fortify/etc/libblockdev", "/etc/libblockdev").
Symlink("/fortify/etc/libblockdev", "/etc/libblockdev"). Symlink("/dev/fortify/etc/locale.conf", "/etc/locale.conf").
Symlink("/fortify/etc/locale.conf", "/etc/locale.conf"). Symlink("/dev/fortify/etc/localtime", "/etc/localtime").
Symlink("/fortify/etc/localtime", "/etc/localtime"). Symlink("/dev/fortify/etc/login.defs", "/etc/login.defs").
Symlink("/fortify/etc/login.defs", "/etc/login.defs"). Symlink("/dev/fortify/etc/lsb-release", "/etc/lsb-release").
Symlink("/fortify/etc/lsb-release", "/etc/lsb-release"). Symlink("/dev/fortify/etc/lvm", "/etc/lvm").
Symlink("/fortify/etc/lvm", "/etc/lvm"). Symlink("/dev/fortify/etc/machine-id", "/etc/machine-id").
Symlink("/fortify/etc/machine-id", "/etc/machine-id"). Symlink("/dev/fortify/etc/man_db.conf", "/etc/man_db.conf").
Symlink("/fortify/etc/man_db.conf", "/etc/man_db.conf"). Symlink("/dev/fortify/etc/modprobe.d", "/etc/modprobe.d").
Symlink("/fortify/etc/modprobe.d", "/etc/modprobe.d"). Symlink("/dev/fortify/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/fortify/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab"). Symlink("/proc/mounts", "/etc/mtab").
Symlink("/fortify/etc/nanorc", "/etc/nanorc"). Symlink("/dev/fortify/etc/nanorc", "/etc/nanorc").
Symlink("/fortify/etc/netgroup", "/etc/netgroup"). Symlink("/dev/fortify/etc/netgroup", "/etc/netgroup").
Symlink("/fortify/etc/NetworkManager", "/etc/NetworkManager"). Symlink("/dev/fortify/etc/NetworkManager", "/etc/NetworkManager").
Symlink("/fortify/etc/nix", "/etc/nix"). Symlink("/dev/fortify/etc/nix", "/etc/nix").
Symlink("/fortify/etc/nixos", "/etc/nixos"). Symlink("/dev/fortify/etc/nixos", "/etc/nixos").
Symlink("/fortify/etc/NIXOS", "/etc/NIXOS"). Symlink("/dev/fortify/etc/NIXOS", "/etc/NIXOS").
Symlink("/fortify/etc/nscd.conf", "/etc/nscd.conf"). Symlink("/dev/fortify/etc/nscd.conf", "/etc/nscd.conf").
Symlink("/fortify/etc/nsswitch.conf", "/etc/nsswitch.conf"). Symlink("/dev/fortify/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink("/fortify/etc/opensnitchd", "/etc/opensnitchd"). Symlink("/dev/fortify/etc/opensnitchd", "/etc/opensnitchd").
Symlink("/fortify/etc/os-release", "/etc/os-release"). Symlink("/dev/fortify/etc/os-release", "/etc/os-release").
Symlink("/fortify/etc/pam", "/etc/pam"). Symlink("/dev/fortify/etc/pam", "/etc/pam").
Symlink("/fortify/etc/pam.d", "/etc/pam.d"). Symlink("/dev/fortify/etc/pam.d", "/etc/pam.d").
Symlink("/fortify/etc/pipewire", "/etc/pipewire"). Symlink("/dev/fortify/etc/pipewire", "/etc/pipewire").
Symlink("/fortify/etc/pki", "/etc/pki"). Symlink("/dev/fortify/etc/pki", "/etc/pki").
Symlink("/fortify/etc/polkit-1", "/etc/polkit-1"). Symlink("/dev/fortify/etc/polkit-1", "/etc/polkit-1").
Symlink("/fortify/etc/profile", "/etc/profile"). Symlink("/dev/fortify/etc/profile", "/etc/profile").
Symlink("/fortify/etc/protocols", "/etc/protocols"). Symlink("/dev/fortify/etc/protocols", "/etc/protocols").
Symlink("/fortify/etc/qemu", "/etc/qemu"). Symlink("/dev/fortify/etc/qemu", "/etc/qemu").
Symlink("/fortify/etc/resolv.conf", "/etc/resolv.conf"). Symlink("/dev/fortify/etc/resolv.conf", "/etc/resolv.conf").
Symlink("/fortify/etc/resolvconf.conf", "/etc/resolvconf.conf"). Symlink("/dev/fortify/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink("/fortify/etc/rpc", "/etc/rpc"). Symlink("/dev/fortify/etc/rpc", "/etc/rpc").
Symlink("/fortify/etc/samba", "/etc/samba"). Symlink("/dev/fortify/etc/samba", "/etc/samba").
Symlink("/fortify/etc/sddm.conf", "/etc/sddm.conf"). Symlink("/dev/fortify/etc/sddm.conf", "/etc/sddm.conf").
Symlink("/fortify/etc/secureboot", "/etc/secureboot"). Symlink("/dev/fortify/etc/secureboot", "/etc/secureboot").
Symlink("/fortify/etc/services", "/etc/services"). Symlink("/dev/fortify/etc/services", "/etc/services").
Symlink("/fortify/etc/set-environment", "/etc/set-environment"). Symlink("/dev/fortify/etc/set-environment", "/etc/set-environment").
Symlink("/fortify/etc/shadow", "/etc/shadow"). Symlink("/dev/fortify/etc/shadow", "/etc/shadow").
Symlink("/fortify/etc/shells", "/etc/shells"). Symlink("/dev/fortify/etc/shells", "/etc/shells").
Symlink("/fortify/etc/ssh", "/etc/ssh"). Symlink("/dev/fortify/etc/ssh", "/etc/ssh").
Symlink("/fortify/etc/ssl", "/etc/ssl"). Symlink("/dev/fortify/etc/ssl", "/etc/ssl").
Symlink("/fortify/etc/static", "/etc/static"). Symlink("/dev/fortify/etc/static", "/etc/static").
Symlink("/fortify/etc/subgid", "/etc/subgid"). Symlink("/dev/fortify/etc/subgid", "/etc/subgid").
Symlink("/fortify/etc/subuid", "/etc/subuid"). Symlink("/dev/fortify/etc/subuid", "/etc/subuid").
Symlink("/fortify/etc/sudoers", "/etc/sudoers"). Symlink("/dev/fortify/etc/sudoers", "/etc/sudoers").
Symlink("/fortify/etc/sysctl.d", "/etc/sysctl.d"). Symlink("/dev/fortify/etc/sysctl.d", "/etc/sysctl.d").
Symlink("/fortify/etc/systemd", "/etc/systemd"). Symlink("/dev/fortify/etc/systemd", "/etc/systemd").
Symlink("/fortify/etc/terminfo", "/etc/terminfo"). Symlink("/dev/fortify/etc/terminfo", "/etc/terminfo").
Symlink("/fortify/etc/tmpfiles.d", "/etc/tmpfiles.d"). Symlink("/dev/fortify/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink("/fortify/etc/udev", "/etc/udev"). Symlink("/dev/fortify/etc/udev", "/etc/udev").
Symlink("/fortify/etc/udisks2", "/etc/udisks2"). Symlink("/dev/fortify/etc/udisks2", "/etc/udisks2").
Symlink("/fortify/etc/UPower", "/etc/UPower"). Symlink("/dev/fortify/etc/UPower", "/etc/UPower").
Symlink("/fortify/etc/vconsole.conf", "/etc/vconsole.conf"). Symlink("/dev/fortify/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink("/fortify/etc/X11", "/etc/X11"). Symlink("/dev/fortify/etc/X11", "/etc/X11").
Symlink("/fortify/etc/zfs", "/etc/zfs"). Symlink("/dev/fortify/etc/zfs", "/etc/zfs").
Symlink("/fortify/etc/zinputrc", "/etc/zinputrc"). Symlink("/dev/fortify/etc/zinputrc", "/etc/zinputrc").
Symlink("/fortify/etc/zoneinfo", "/etc/zoneinfo"). Symlink("/dev/fortify/etc/zoneinfo", "/etc/zoneinfo").
Symlink("/fortify/etc/zprofile", "/etc/zprofile"). Symlink("/dev/fortify/etc/zprofile", "/etc/zprofile").
Symlink("/fortify/etc/zshenv", "/etc/zshenv"). Symlink("/dev/fortify/etc/zshenv", "/etc/zshenv").
Symlink("/fortify/etc/zshrc", "/etc/zshrc"). Symlink("/dev/fortify/etc/zshrc", "/etc/zshrc").
Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", false, true). Bind("/tmp/fortify.1971/tmpdir/150", "/tmp", false, true).
Tmpfs("/tmp/fortify.1971", 1048576). Tmpfs("/tmp/fortify.1971", 1048576).
Tmpfs("/run/user", 1048576). Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/1971", 8388608). Tmpfs("/run/user/65534", 8388608).
Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", false, true). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/passwd", "/etc/passwd").
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/passwd", "/etc/passwd"). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/group", "/etc/group").
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/group", "/etc/group"). Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/65534/wayland-0").
Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/wayland", "/run/user/1971/wayland-0"). Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native").
Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native"). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie", "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie").
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/pulse-cookie", "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/pulse-cookie"). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus").
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus"). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket").
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket").
Tmpfs("/var/run/nscd", 8192), 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) FshimPath() string {
return "/nix/store/00000000000000000000000000000000-fortify-0.0.10/bin/.fshim"
}
func (s *stubNixOS) Paths() linux.Paths {
return linux.Paths{
SharePath: "/tmp/fortify.1971",
RuntimePath: "/run/user/1971",
RunDirPath: "/run/user/1971/fortify",
}
}
func (s *stubNixOS) SdBooted() bool {
return true
}

View File

@ -1,452 +0,0 @@
package app_test
import (
"git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/system"
)
var testCasesPd = []sealTestCase{
{
"nixos permissive defaults no enablements", new(stubNixOS),
&fst.Config{
Command: make([]string, 0),
Confinement: fst.ConfinementConfig{
AppID: 0,
Username: "chronos",
Outer: "/home/chronos",
},
},
fst.ID{
0x4a, 0x45, 0x0b, 0x65,
0x96, 0xd7, 0xbc, 0x15,
0xbd, 0x01, 0x78, 0x0e,
0xb9, 0xa6, 0x07, 0xac,
},
system.New(1000000).
Ensure("/tmp/fortify.1971", 0711).
Ephemeral(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac", 0711).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute).
Ensure("/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,
Chdir: "/home/chronos",
SetEnv: map[string]string{
"HOME": "/home/chronos",
"SHELL": "/run/current-system/sw/bin/zsh",
"TERM": "xterm-256color",
"USER": "chronos",
"XDG_RUNTIME_DIR": "/run/user/65534",
"XDG_SESSION_CLASS": "user",
"XDG_SESSION_TYPE": "tty"},
Chmod: make(bwrap.ChmodConfig),
DieWithParent: true,
AsInit: true,
}).SetUID(65534).SetGID(65534).
Procfs("/proc").
Tmpfs("/fortify", 4096).
DevTmpfs("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", false, true).
Bind("/boot", "/boot", false, true).
Bind("/home", "/home", false, true).
Bind("/lib", "/lib", false, true).
Bind("/lib64", "/lib64", false, true).
Bind("/nix", "/nix", false, true).
Bind("/root", "/root", false, true).
Bind("/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("/etc", "/fortify/etc").
Symlink("/fortify/etc/alsa", "/etc/alsa").
Symlink("/fortify/etc/bashrc", "/etc/bashrc").
Symlink("/fortify/etc/binfmt.d", "/etc/binfmt.d").
Symlink("/fortify/etc/dbus-1", "/etc/dbus-1").
Symlink("/fortify/etc/default", "/etc/default").
Symlink("/fortify/etc/ethertypes", "/etc/ethertypes").
Symlink("/fortify/etc/fonts", "/etc/fonts").
Symlink("/fortify/etc/fstab", "/etc/fstab").
Symlink("/fortify/etc/fuse.conf", "/etc/fuse.conf").
Symlink("/fortify/etc/host.conf", "/etc/host.conf").
Symlink("/fortify/etc/hostid", "/etc/hostid").
Symlink("/fortify/etc/hostname", "/etc/hostname").
Symlink("/fortify/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink("/fortify/etc/hosts", "/etc/hosts").
Symlink("/fortify/etc/inputrc", "/etc/inputrc").
Symlink("/fortify/etc/ipsec.d", "/etc/ipsec.d").
Symlink("/fortify/etc/issue", "/etc/issue").
Symlink("/fortify/etc/kbd", "/etc/kbd").
Symlink("/fortify/etc/libblockdev", "/etc/libblockdev").
Symlink("/fortify/etc/locale.conf", "/etc/locale.conf").
Symlink("/fortify/etc/localtime", "/etc/localtime").
Symlink("/fortify/etc/login.defs", "/etc/login.defs").
Symlink("/fortify/etc/lsb-release", "/etc/lsb-release").
Symlink("/fortify/etc/lvm", "/etc/lvm").
Symlink("/fortify/etc/machine-id", "/etc/machine-id").
Symlink("/fortify/etc/man_db.conf", "/etc/man_db.conf").
Symlink("/fortify/etc/modprobe.d", "/etc/modprobe.d").
Symlink("/fortify/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab").
Symlink("/fortify/etc/nanorc", "/etc/nanorc").
Symlink("/fortify/etc/netgroup", "/etc/netgroup").
Symlink("/fortify/etc/NetworkManager", "/etc/NetworkManager").
Symlink("/fortify/etc/nix", "/etc/nix").
Symlink("/fortify/etc/nixos", "/etc/nixos").
Symlink("/fortify/etc/NIXOS", "/etc/NIXOS").
Symlink("/fortify/etc/nscd.conf", "/etc/nscd.conf").
Symlink("/fortify/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink("/fortify/etc/opensnitchd", "/etc/opensnitchd").
Symlink("/fortify/etc/os-release", "/etc/os-release").
Symlink("/fortify/etc/pam", "/etc/pam").
Symlink("/fortify/etc/pam.d", "/etc/pam.d").
Symlink("/fortify/etc/pipewire", "/etc/pipewire").
Symlink("/fortify/etc/pki", "/etc/pki").
Symlink("/fortify/etc/polkit-1", "/etc/polkit-1").
Symlink("/fortify/etc/profile", "/etc/profile").
Symlink("/fortify/etc/protocols", "/etc/protocols").
Symlink("/fortify/etc/qemu", "/etc/qemu").
Symlink("/fortify/etc/resolv.conf", "/etc/resolv.conf").
Symlink("/fortify/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink("/fortify/etc/rpc", "/etc/rpc").
Symlink("/fortify/etc/samba", "/etc/samba").
Symlink("/fortify/etc/sddm.conf", "/etc/sddm.conf").
Symlink("/fortify/etc/secureboot", "/etc/secureboot").
Symlink("/fortify/etc/services", "/etc/services").
Symlink("/fortify/etc/set-environment", "/etc/set-environment").
Symlink("/fortify/etc/shadow", "/etc/shadow").
Symlink("/fortify/etc/shells", "/etc/shells").
Symlink("/fortify/etc/ssh", "/etc/ssh").
Symlink("/fortify/etc/ssl", "/etc/ssl").
Symlink("/fortify/etc/static", "/etc/static").
Symlink("/fortify/etc/subgid", "/etc/subgid").
Symlink("/fortify/etc/subuid", "/etc/subuid").
Symlink("/fortify/etc/sudoers", "/etc/sudoers").
Symlink("/fortify/etc/sysctl.d", "/etc/sysctl.d").
Symlink("/fortify/etc/systemd", "/etc/systemd").
Symlink("/fortify/etc/terminfo", "/etc/terminfo").
Symlink("/fortify/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink("/fortify/etc/udev", "/etc/udev").
Symlink("/fortify/etc/udisks2", "/etc/udisks2").
Symlink("/fortify/etc/UPower", "/etc/UPower").
Symlink("/fortify/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink("/fortify/etc/X11", "/etc/X11").
Symlink("/fortify/etc/zfs", "/etc/zfs").
Symlink("/fortify/etc/zinputrc", "/etc/zinputrc").
Symlink("/fortify/etc/zoneinfo", "/etc/zoneinfo").
Symlink("/fortify/etc/zprofile", "/etc/zprofile").
Symlink("/fortify/etc/zshenv", "/etc/zshenv").
Symlink("/fortify/etc/zshrc", "/etc/zshrc").
Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", false, true).
Tmpfs("/tmp/fortify.1971", 1048576).
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/65534", 8388608).
Bind("/home/chronos", "/home/chronos", false, true).
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),
&fst.Config{
ID: "org.chromium.Chromium",
Command: []string{"/run/current-system/sw/bin/zsh", "-c", "exec chromium "},
Confinement: fst.ConfinementConfig{
AppID: 9,
Groups: []string{"video"},
Username: "chronos",
Outer: "/home/chronos",
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(),
},
},
fst.ID{
0xeb, 0xf0, 0x83, 0xd1,
0xb1, 0x75, 0x91, 0x17,
0x82, 0xd4, 0x13, 0x36,
0x9b, 0x64, 0xce, 0x7c,
},
system.New(1000009).
Ensure("/tmp/fortify.1971", 0711).
Ephemeral(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0711).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/9", 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").
Ensure("/tmp/fortify.1971/wayland", 0711).
Wayland("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
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,
Chdir: "/home/chronos",
Clearenv: true,
SetEnv: map[string]string{
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/65534/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/65534/pulse/native",
"SHELL": "/run/current-system/sw/bin/zsh",
"TERM": "xterm-256color",
"USER": "chronos",
"WAYLAND_DISPLAY": "/run/user/65534/wayland-0",
"XDG_RUNTIME_DIR": "/run/user/65534",
"XDG_SESSION_CLASS": "user",
"XDG_SESSION_TYPE": "tty",
},
Chmod: make(bwrap.ChmodConfig),
DieWithParent: true,
AsInit: true,
}).SetUID(65534).SetGID(65534).
Procfs("/proc").
Tmpfs("/fortify", 4096).
DevTmpfs("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", false, true).
Bind("/boot", "/boot", false, true).
Bind("/home", "/home", false, true).
Bind("/lib", "/lib", false, true).
Bind("/lib64", "/lib64", false, true).
Bind("/nix", "/nix", false, true).
Bind("/root", "/root", false, true).
Bind("/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).
Bind("/etc", "/fortify/etc").
Symlink("/fortify/etc/alsa", "/etc/alsa").
Symlink("/fortify/etc/bashrc", "/etc/bashrc").
Symlink("/fortify/etc/binfmt.d", "/etc/binfmt.d").
Symlink("/fortify/etc/dbus-1", "/etc/dbus-1").
Symlink("/fortify/etc/default", "/etc/default").
Symlink("/fortify/etc/ethertypes", "/etc/ethertypes").
Symlink("/fortify/etc/fonts", "/etc/fonts").
Symlink("/fortify/etc/fstab", "/etc/fstab").
Symlink("/fortify/etc/fuse.conf", "/etc/fuse.conf").
Symlink("/fortify/etc/host.conf", "/etc/host.conf").
Symlink("/fortify/etc/hostid", "/etc/hostid").
Symlink("/fortify/etc/hostname", "/etc/hostname").
Symlink("/fortify/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink("/fortify/etc/hosts", "/etc/hosts").
Symlink("/fortify/etc/inputrc", "/etc/inputrc").
Symlink("/fortify/etc/ipsec.d", "/etc/ipsec.d").
Symlink("/fortify/etc/issue", "/etc/issue").
Symlink("/fortify/etc/kbd", "/etc/kbd").
Symlink("/fortify/etc/libblockdev", "/etc/libblockdev").
Symlink("/fortify/etc/locale.conf", "/etc/locale.conf").
Symlink("/fortify/etc/localtime", "/etc/localtime").
Symlink("/fortify/etc/login.defs", "/etc/login.defs").
Symlink("/fortify/etc/lsb-release", "/etc/lsb-release").
Symlink("/fortify/etc/lvm", "/etc/lvm").
Symlink("/fortify/etc/machine-id", "/etc/machine-id").
Symlink("/fortify/etc/man_db.conf", "/etc/man_db.conf").
Symlink("/fortify/etc/modprobe.d", "/etc/modprobe.d").
Symlink("/fortify/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab").
Symlink("/fortify/etc/nanorc", "/etc/nanorc").
Symlink("/fortify/etc/netgroup", "/etc/netgroup").
Symlink("/fortify/etc/NetworkManager", "/etc/NetworkManager").
Symlink("/fortify/etc/nix", "/etc/nix").
Symlink("/fortify/etc/nixos", "/etc/nixos").
Symlink("/fortify/etc/NIXOS", "/etc/NIXOS").
Symlink("/fortify/etc/nscd.conf", "/etc/nscd.conf").
Symlink("/fortify/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink("/fortify/etc/opensnitchd", "/etc/opensnitchd").
Symlink("/fortify/etc/os-release", "/etc/os-release").
Symlink("/fortify/etc/pam", "/etc/pam").
Symlink("/fortify/etc/pam.d", "/etc/pam.d").
Symlink("/fortify/etc/pipewire", "/etc/pipewire").
Symlink("/fortify/etc/pki", "/etc/pki").
Symlink("/fortify/etc/polkit-1", "/etc/polkit-1").
Symlink("/fortify/etc/profile", "/etc/profile").
Symlink("/fortify/etc/protocols", "/etc/protocols").
Symlink("/fortify/etc/qemu", "/etc/qemu").
Symlink("/fortify/etc/resolv.conf", "/etc/resolv.conf").
Symlink("/fortify/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink("/fortify/etc/rpc", "/etc/rpc").
Symlink("/fortify/etc/samba", "/etc/samba").
Symlink("/fortify/etc/sddm.conf", "/etc/sddm.conf").
Symlink("/fortify/etc/secureboot", "/etc/secureboot").
Symlink("/fortify/etc/services", "/etc/services").
Symlink("/fortify/etc/set-environment", "/etc/set-environment").
Symlink("/fortify/etc/shadow", "/etc/shadow").
Symlink("/fortify/etc/shells", "/etc/shells").
Symlink("/fortify/etc/ssh", "/etc/ssh").
Symlink("/fortify/etc/ssl", "/etc/ssl").
Symlink("/fortify/etc/static", "/etc/static").
Symlink("/fortify/etc/subgid", "/etc/subgid").
Symlink("/fortify/etc/subuid", "/etc/subuid").
Symlink("/fortify/etc/sudoers", "/etc/sudoers").
Symlink("/fortify/etc/sysctl.d", "/etc/sysctl.d").
Symlink("/fortify/etc/systemd", "/etc/systemd").
Symlink("/fortify/etc/terminfo", "/etc/terminfo").
Symlink("/fortify/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink("/fortify/etc/udev", "/etc/udev").
Symlink("/fortify/etc/udisks2", "/etc/udisks2").
Symlink("/fortify/etc/UPower", "/etc/UPower").
Symlink("/fortify/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink("/fortify/etc/X11", "/etc/X11").
Symlink("/fortify/etc/zfs", "/etc/zfs").
Symlink("/fortify/etc/zinputrc", "/etc/zinputrc").
Symlink("/fortify/etc/zoneinfo", "/etc/zoneinfo").
Symlink("/fortify/etc/zprofile", "/etc/zprofile").
Symlink("/fortify/etc/zshenv", "/etc/zshenv").
Symlink("/fortify/etc/zshrc", "/etc/zshrc").
Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", false, true).
Tmpfs("/tmp/fortify.1971", 1048576).
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/65534", 8388608).
Bind("/home/chronos", "/home/chronos", false, true).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/passwd", "/etc/passwd").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/group", "/etc/group").
Bind("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/65534/wayland-0").
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie", "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket").
Tmpfs("/var/run/nscd", 8192),
},
}

View File

@ -1,153 +0,0 @@
package app_test
import (
"fmt"
"io"
"io/fs"
"os/user"
"strconv"
"git.gensokyo.uk/security/fortify/internal/linux"
)
// 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) LookupGroup(name string) (*user.Group, error) {
switch name {
case "video":
return &user.Group{Gid: "26", Name: "video"}, nil
default:
return nil, user.UnknownGroupError(name)
}
}
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) Stdout() io.Writer {
panic("requested stdout")
}
func (s *stubNixOS) Paths() linux.Paths {
return linux.Paths{
SharePath: "/tmp/fortify.1971",
RuntimePath: "/run/user/1971",
RunDirPath: "/run/user/1971/fortify",
}
}
func (s *stubNixOS) Uid(aid int) (int, error) {
return 1000000 + 0*10000 + aid, nil
}
func (s *stubNixOS) SdBooted() bool {
return true
}

View File

@ -6,24 +6,23 @@ import (
"testing" "testing"
"time" "time"
"git.gensokyo.uk/security/fortify/fst" "git.ophivana.moe/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/app" "git.ophivana.moe/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/system"
"git.gensokyo.uk/security/fortify/internal/system"
) )
type sealTestCase struct { type sealTestCase struct {
name string name string
os linux.System os linux.System
config *fst.Config config *app.Config
id fst.ID id app.ID
wantSys *system.I wantSys *system.I
wantBwrap *bwrap.Config wantBwrap *bwrap.Config
} }
func TestApp(t *testing.T) { func TestApp(t *testing.T) {
testCases := append(testCasesPd, testCasesNixos...) testCases := append(testCasesNixos)
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {

View File

@ -1,22 +1,23 @@
package fst package app
import ( import (
"errors" "os"
"git.gensokyo.uk/security/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/system"
"git.gensokyo.uk/security/fortify/internal/system"
) )
const fTmp = "/fortify"
// Config is used to seal an *App // Config is used to seal an *App
type Config struct { type Config struct {
// D-Bus application ID // D-Bus application ID
ID string `json:"id"` ID string `json:"id"`
// username of the target user to switch to
User string `json:"user"`
// value passed through to the child process as its argv // value passed through to the child process as its argv
Command []string `json:"command"` Command []string `json:"command"`
// string representation of the child's launch method
Method string `json:"method"`
// child confinement configuration // child confinement configuration
Confinement ConfinementConfig `json:"confinement"` Confinement ConfinementConfig `json:"confinement"`
@ -24,16 +25,6 @@ type Config struct {
// ConfinementConfig defines fortified child's confinement // ConfinementConfig defines fortified child's confinement
type ConfinementConfig struct { type ConfinementConfig struct {
// numerical application id, determines uid in the init namespace
AppID int `json:"app_id"`
// list of supplementary groups to inherit
Groups []string `json:"groups"`
// passwd username in the sandbox, defaults to chronos
Username string `json:"username,omitempty"`
// home directory in sandbox, empty for outer
Inner string `json:"home_inner"`
// home directory in init namespace
Outer string `json:"home"`
// bwrap sandbox confinement configuration // bwrap sandbox confinement configuration
Sandbox *SandboxConfig `json:"sandbox"` Sandbox *SandboxConfig `json:"sandbox"`
@ -56,14 +47,12 @@ type SandboxConfig struct {
UserNS bool `json:"userns,omitempty"` UserNS bool `json:"userns,omitempty"`
// share net namespace // share net namespace
Net bool `json:"net,omitempty"` Net bool `json:"net,omitempty"`
// share all devices
Dev bool `json:"dev,omitempty"`
// do not run in new session // do not run in new session
NoNewSession bool `json:"no_new_session,omitempty"` NoNewSession bool `json:"no_new_session,omitempty"`
// map target user uid to privileged user uid in the user namespace // map target user uid to privileged user uid in the user namespace
MapRealUID bool `json:"map_real_uid"` UseRealUID bool `json:"use_real_uid"`
// direct access to wayland socket // mediated access to wayland socket
DirectWayland bool `json:"direct_wayland,omitempty"` Wayland bool `json:"wayland,omitempty"`
// final environment variables // final environment variables
Env map[string]string `json:"env"` Env map[string]string `json:"env"`
@ -71,8 +60,6 @@ type SandboxConfig struct {
Filesystem []*FilesystemConfig `json:"filesystem"` Filesystem []*FilesystemConfig `json:"filesystem"`
// symlinks created inside the sandbox // symlinks created inside the sandbox
Link [][2]string `json:"symlink"` Link [][2]string `json:"symlink"`
// automatically set up /etc symlinks
AutoEtc bool `json:"auto_etc"`
// paths to override by mounting tmpfs over them // paths to override by mounting tmpfs over them
Override []string `json:"override"` Override []string `json:"override"`
} }
@ -92,16 +79,13 @@ type FilesystemConfig struct {
// Bwrap returns the address of the corresponding bwrap.Config to s. // 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. // Note that remaining tmpfs entries must be queued by the caller prior to launch.
func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) { func (s *SandboxConfig) Bwrap(uid int) *bwrap.Config {
if s == nil { if s == nil {
return nil, errors.New("nil sandbox config") return nil
} }
var uid int if !s.UseRealUID {
if !s.MapRealUID {
uid = 65534 uid = 65534
} else {
uid = os.Geteuid()
} }
conf := (&bwrap.Config{ conf := (&bwrap.Config{
@ -115,21 +99,11 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
AsInit: true, AsInit: true,
// initialise map // initialise map
Chmod: make(bwrap.ChmodConfig), Chmod: make(map[string]os.FileMode),
}). }).
SetUID(uid).SetGID(uid). SetUID(uid).SetGID(uid).
Procfs("/proc"). Procfs("/proc").DevTmpfs("/dev").Mqueue("/dev/mqueue").
Tmpfs(fTmp, 4*1024) Tmpfs("/dev/fortify", 4*1024)
if !s.Dev {
conf.DevTmpfs("/dev").Mqueue("/dev/mqueue")
} else {
conf.Bind("/dev", "/dev", false, true, true)
}
if !s.AutoEtc {
conf.Dir("/etc")
}
for _, c := range s.Filesystem { for _, c := range s.Filesystem {
if c == nil { if c == nil {
@ -147,35 +121,14 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
conf.Symlink(l[0], l[1]) conf.Symlink(l[0], l[1])
} }
if s.AutoEtc { return conf
conf.Bind("/etc", fTmp+"/etc")
// link host /etc contents to prevent passwd/group from being overwritten
if d, err := os.ReadDir("/etc"); err != nil {
return nil, err
} else {
for _, ent := range d {
name := ent.Name()
switch name {
case "passwd":
case "group":
case "mtab":
conf.Symlink("/proc/mounts", "/etc/"+name)
default:
conf.Symlink(fTmp+"/etc/"+name, "/etc/"+name)
}
}
}
}
return conf, nil
} }
// Template returns a fully populated instance of Config. // Template returns a fully populated instance of Config.
func Template() *Config { func Template() *Config {
return &Config{ return &Config{
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
User: "chronos",
Command: []string{ Command: []string{
"chromium", "chromium",
"--ignore-gpu-blocklist", "--ignore-gpu-blocklist",
@ -183,20 +136,14 @@ func Template() *Config {
"--enable-features=UseOzonePlatform", "--enable-features=UseOzonePlatform",
"--ozone-platform=wayland", "--ozone-platform=wayland",
}, },
Method: "sudo",
Confinement: ConfinementConfig{ Confinement: ConfinementConfig{
AppID: 9,
Groups: []string{"video"},
Username: "chronos",
Outer: "/var/lib/persist/home/org.chromium.Chromium",
Inner: "/var/lib/fortify",
Sandbox: &SandboxConfig{ Sandbox: &SandboxConfig{
Hostname: "localhost", Hostname: "localhost",
UserNS: true, UserNS: true,
Net: true, Net: true,
NoNewSession: true, NoNewSession: true,
MapRealUID: true, Wayland: false,
Dev: true,
DirectWayland: false,
// example API credentials pulled from Google Chrome // example API credentials pulled from Google Chrome
// DO NOT USE THESE IN A REAL BROWSER // DO NOT USE THESE IN A REAL BROWSER
Env: map[string]string{ Env: map[string]string{
@ -205,15 +152,12 @@ func Template() *Config {
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT", "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT",
}, },
Filesystem: []*FilesystemConfig{ Filesystem: []*FilesystemConfig{
{Src: "/nix/store"}, {Src: "/nix"},
{Src: "/run/current-system"}, {Src: "/storage/emulated/0", Write: true, Must: true},
{Src: "/run/opengl-driver"}, {Src: "/data/user/0", Dst: "/data/data", Write: true, Must: true},
{Src: "/var/db/nix-channels"}, {Src: "/var/tmp", Write: true},
{Src: "/home/chronos", Write: true, Must: true},
{Src: "/dev/dri", Device: true},
}, },
Link: [][2]string{{"/run/user/65534", "/run/user/150"}}, Link: [][2]string{{"/dev/fortify/etc", "/etc"}},
AutoEtc: true,
Override: []string{"/var/run/nscd"}, Override: []string{"/var/run/nscd"},
}, },
SystemBus: &dbus.Config{ SystemBus: &dbus.Config{

View File

@ -1,13 +1,12 @@
package app package app
import ( import (
"git.gensokyo.uk/security/fortify/fst" "git.ophivana.moe/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/system"
"git.gensokyo.uk/security/fortify/internal/system"
) )
func NewWithID(id fst.ID, os linux.System) App { func NewWithID(id ID, os linux.System) App {
a := new(app) a := new(app)
a.id = &id a.id = &id
a.os = os a.os = os

17
internal/app/id.go Normal file
View File

@ -0,0 +1,17 @@
package app
import (
"crypto/rand"
"encoding/hex"
)
type ID [16]byte
func (a *ID) String() string {
return hex.EncodeToString(a[:])
}
func newAppID(id *ID) error {
_, err := rand.Read(id[:])
return err
}

View File

@ -0,0 +1,57 @@
package app
import (
"strings"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
func (a *app) commandBuilderMachineCtl(shimEnv string) (args []string) {
args = make([]string, 0, 9+len(a.seal.sys.bwrap.SetEnv))
// shell --uid=$USER
args = append(args, "shell", "--uid="+a.seal.sys.user.Username)
// --quiet
if !fmsg.Verbose() {
args = append(args, "--quiet")
}
// environ
envQ := make([]string, 0, len(a.seal.sys.bwrap.SetEnv)+1)
for k, v := range a.seal.sys.bwrap.SetEnv {
envQ = append(envQ, "-E"+k+"="+v)
}
// add shim payload to environment for shim path
envQ = append(envQ, "-E"+shimEnv)
args = append(args, envQ...)
// -- .host
args = append(args, "--", ".host")
// /bin/sh -c
if sh, err := a.os.LookPath("sh"); err != nil {
// hardcode /bin/sh path since it exists more often than not
args = append(args, "/bin/sh", "-c")
} else {
args = append(args, sh, "-c")
}
// build inner command expression ran as target user
innerCommand := strings.Builder{}
// apply custom environment variables to activation environment
innerCommand.WriteString("dbus-update-activation-environment --systemd")
for k := range a.seal.sys.bwrap.SetEnv {
innerCommand.WriteString(" " + k)
}
innerCommand.WriteString("; ")
// launch fortify shim
innerCommand.WriteString("exec " + a.os.FshimPath())
// append inner command
args = append(args, innerCommand.String())
return
}

View File

@ -0,0 +1,30 @@
package app
import (
"git.ophivana.moe/security/fortify/internal/fmsg"
)
const (
sudoAskPass = "SUDO_ASKPASS"
)
func (a *app) commandBuilderSudo(shimEnv string) (args []string) {
args = make([]string, 0, 8)
// -Hiu $USER
args = append(args, "-Hiu", a.seal.sys.user.Username)
// -A?
if _, ok := a.os.LookupEnv(sudoAskPass); ok {
fmsg.VPrintln(sudoAskPass, "set, adding askpass flag")
args = append(args, "-A")
}
// shim payload
args = append(args, shimEnv)
// -- $@
args = append(args, "--", a.os.FshimPath())
return
}

View File

@ -2,33 +2,45 @@ package app
import ( import (
"errors" "errors"
"fmt"
"io/fs" "io/fs"
"os/user"
"path" "path"
"regexp"
"strconv" "strconv"
"git.gensokyo.uk/security/fortify/dbus" shim "git.ophivana.moe/security/fortify/cmd/fshim/ipc"
"git.gensokyo.uk/security/fortify/fst" "git.ophivana.moe/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/state" "git.ophivana.moe/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
const (
LaunchMethodSudo uint8 = iota
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("invalid aid") ErrUser = errors.New("unknown user")
ErrHome = errors.New("invalid home directory") ErrLaunch = errors.New("invalid launch method")
ErrName = errors.New("invalid username")
)
var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z0-9_-]{0,30}\\$)$") ErrSudo = errors.New("sudo not available")
ErrSystemd = errors.New("systemd not available")
ErrMachineCtl = errors.New("machinectl not available")
)
// appSeal seals the application with child-related information // appSeal seals the application with child-related information
type appSeal struct { type appSeal struct {
// app unique ID string representation // app unique ID string representation
id string id string
// wayland mediation, disabled if nil
wl *shim.Wayland
// dbus proxy message buffer retriever // dbus proxy message buffer retriever
dbusMsg func(f func(msgbuf []string)) dbusMsg func(f func(msgbuf []string))
@ -39,15 +51,17 @@ type appSeal struct {
// persistent process state store // persistent process state store
store state.Store store state.Store
// uint8 representation of launch method sealed from config
launchOption uint8
// process-specific share directory path // process-specific share directory path
share string share string
// process-specific share directory path local to XDG_RUNTIME_DIR // process-specific share directory path local to XDG_RUNTIME_DIR
shareLocal string shareLocal string
// path to launcher program
toolPath string
// pass-through enablement tracking from config // pass-through enablement tracking from config
et system.Enablements et system.Enablements
// wayland socket direct access
directWayland bool
// prevents sharing from happening twice // prevents sharing from happening twice
shared bool shared bool
@ -60,7 +74,7 @@ type appSeal struct {
} }
// Seal seals the app launch context // Seal seals the app launch context
func (a *app) Seal(config *fst.Config) error { func (a *app) Seal(config *Config) error {
a.lock.Lock() a.lock.Lock()
defer a.lock.Unlock() defer a.lock.Unlock()
@ -84,11 +98,39 @@ func (a *app) Seal(config *fst.Config) error {
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
switch config.Method {
case method[LaunchMethodSudo]:
seal.launchOption = LaunchMethodSudo
if sudoPath, err := a.os.LookPath("sudo"); err != nil {
return fmsg.WrapError(ErrSudo,
"sudo not found")
} else {
seal.toolPath = sudoPath
}
case method[LaunchMethodMachineCtl]:
seal.launchOption = LaunchMethodMachineCtl
if !a.os.SdBooted() {
return fmsg.WrapError(ErrSystemd,
"system has not been booted with systemd as init system")
}
if machineCtlPath, err := a.os.LookPath("machinectl"); err != nil {
return fmsg.WrapError(ErrMachineCtl,
"machinectl not found")
} else {
seal.toolPath = machineCtlPath
}
default:
return fmsg.WrapError(ErrLaunch,
"invalid launch method")
}
// create seal system component // create seal system component
seal.sys = new(appSealSys) seal.sys = new(appSealSys)
// mapped uid // mapped uid
if config.Confinement.Sandbox != nil && config.Confinement.Sandbox.MapRealUID { if config.Confinement.Sandbox != nil && config.Confinement.Sandbox.UseRealUID {
seal.sys.mappedID = a.os.Geteuid() seal.sys.mappedID = a.os.Geteuid()
} else { } else {
seal.sys.mappedID = 65534 seal.sys.mappedID = 65534
@ -96,51 +138,16 @@ func (a *app) Seal(config *fst.Config) error {
seal.sys.mappedIDString = strconv.Itoa(seal.sys.mappedID) seal.sys.mappedIDString = strconv.Itoa(seal.sys.mappedID)
seal.sys.runtime = path.Join("/run/user", seal.sys.mappedIDString) seal.sys.runtime = path.Join("/run/user", seal.sys.mappedIDString)
// validate uid and set user info // look up user from system
if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 { if u, err := a.os.Lookup(config.User); err != nil {
return fmsg.WrapError(ErrUser, if errors.As(err, new(user.UnknownUserError)) {
fmt.Sprintf("aid %d out of range", config.Confinement.AppID)) return fmsg.WrapError(ErrUser, "unknown user", config.User)
} else { } else {
seal.sys.user = appUser{ // unreachable
aid: config.Confinement.AppID, panic(err)
as: strconv.Itoa(config.Confinement.AppID),
data: config.Confinement.Outer,
home: config.Confinement.Inner,
username: config.Confinement.Username,
} }
if seal.sys.user.username == "" {
seal.sys.user.username = "chronos"
} else if !posixUsername.MatchString(seal.sys.user.username) {
return fmsg.WrapError(ErrName,
fmt.Sprintf("invalid user name %q", seal.sys.user.username))
}
if seal.sys.user.data == "" || !path.IsAbs(seal.sys.user.data) {
return fmsg.WrapError(ErrHome,
fmt.Sprintf("invalid home directory %q", seal.sys.user.data))
}
if seal.sys.user.home == "" {
seal.sys.user.home = seal.sys.user.data
}
// invoke fsu for full uid
if u, err := a.os.Uid(seal.sys.user.aid); err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot obtain uid from fsu:")
} else { } else {
seal.sys.user.uid = u seal.sys.user = u
seal.sys.user.us = strconv.Itoa(u)
}
// resolve supplementary group ids from names
seal.sys.user.supp = make([]string, len(config.Confinement.Groups))
for i, name := range config.Confinement.Groups {
if g, err := a.os.LookupGroup(name); err != nil {
return fmsg.WrapError(err,
fmt.Sprintf("unknown group %q", name))
} else {
seal.sys.user.supp[i] = g.Gid
}
}
} }
// map sandbox config to bwrap // map sandbox config to bwrap
@ -148,17 +155,16 @@ func (a *app) Seal(config *fst.Config) error {
fmsg.VPrintln("sandbox configuration not supplied, PROCEED WITH CAUTION") fmsg.VPrintln("sandbox configuration not supplied, PROCEED WITH CAUTION")
// permissive defaults // permissive defaults
conf := &fst.SandboxConfig{ conf := &SandboxConfig{
UserNS: true, UserNS: true,
Net: true, Net: true,
NoNewSession: true, NoNewSession: true,
AutoEtc: true,
} }
// bind entries in / // bind entries in /
if d, err := a.os.ReadDir("/"); err != nil { if d, err := a.os.ReadDir("/"); err != nil {
return err return err
} else { } else {
b := make([]*fst.FilesystemConfig, 0, len(d)) b := make([]*FilesystemConfig, 0, len(d))
for _, ent := range d { for _, ent := range d {
p := "/" + ent.Name() p := "/" + ent.Name()
switch p { switch p {
@ -167,10 +173,11 @@ func (a *app) Seal(config *fst.Config) error {
case "/run": case "/run":
case "/tmp": case "/tmp":
case "/mnt": case "/mnt":
case "/etc":
case "/etc":
b = append(b, &FilesystemConfig{Src: p, Dst: "/dev/fortify/etc", Write: false, Must: true})
default: default:
b = append(b, &fst.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...)
@ -179,7 +186,7 @@ func (a *app) Seal(config *fst.Config) error {
if d, err := a.os.ReadDir("/run"); err != nil { if d, err := a.os.ReadDir("/run"); err != nil {
return err return err
} else { } else {
b := make([]*fst.FilesystemConfig, 0, len(d)) b := make([]*FilesystemConfig, 0, len(d))
for _, ent := range d { for _, ent := range d {
name := ent.Name() name := ent.Name()
switch name { switch name {
@ -187,7 +194,7 @@ func (a *app) Seal(config *fst.Config) error {
case "dbus": case "dbus":
default: default:
p := "/run/" + name p := "/run/" + name
b = append(b, &fst.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...)
@ -199,29 +206,60 @@ func (a *app) Seal(config *fst.Config) error {
} }
// bind GPU stuff // bind GPU stuff
if config.Confinement.Enablements.Has(system.EX11) || config.Confinement.Enablements.Has(system.EWayland) { if config.Confinement.Enablements.Has(system.EX11) || config.Confinement.Enablements.Has(system.EWayland) {
conf.Filesystem = append(conf.Filesystem, &fst.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.directWayland = config.Confinement.Sandbox.DirectWayland seal.sys.bwrap = config.Confinement.Sandbox.Bwrap(a.os.Geteuid())
if b, err := config.Confinement.Sandbox.Bwrap(a.os); err != nil {
return err
} else {
seal.sys.bwrap = b
}
seal.sys.override = config.Confinement.Sandbox.Override 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 struct and client wait channel if mediated wayland is enabled
// this field being set enables mediated wayland setup later on
if config.Confinement.Sandbox.Wayland {
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.NewMulti(seal.RunDirPath) seal.store = state.NewSimple(seal.RunDirPath, seal.sys.user.Uid)
// initialise system interface with full uid // parse string UID
seal.sys.I = system.New(seal.sys.user.uid) if u, err := strconv.Atoi(seal.sys.user.Uid); err != nil {
// unreachable unless kernel bug
panic("uid parse")
} else {
seal.sys.I = system.New(u)
}
// pass through enablements // pass through enablements
seal.et = config.Confinement.Enablements seal.et = config.Confinement.Enablements
@ -232,11 +270,13 @@ func (a *app) Seal(config *fst.Config) error {
} }
// verbose log seal information // verbose log seal information
fmsg.VPrintf("created application seal for uid %s (%s) groups: %v, command: %s", fmsg.VPrintln("created application seal as user",
seal.sys.user.us, seal.sys.user.username, config.Confinement.Groups, config.Command) seal.sys.user.Username, "("+seal.sys.user.Uid+"),",
"method:", config.Method+",",
"launcher:", seal.toolPath+",",
"command:", config.Command)
// seal app and release lock // seal app and release lock
a.seal = seal a.seal = seal
a.ct = newAppCt(config)
return nil return nil
} }

View File

@ -3,9 +3,9 @@ package app
import ( import (
"path" "path"
"git.gensokyo.uk/security/fortify/acl" "git.ophivana.moe/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
const ( const (

View File

@ -4,10 +4,10 @@ import (
"errors" "errors"
"path" "path"
"git.gensokyo.uk/security/fortify/acl" "git.ophivana.moe/security/fortify/acl"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
const ( const (
@ -31,36 +31,23 @@ func (seal *appSeal) shareDisplay(os linux.System) error {
// set up wayland // set up wayland
if seal.et.Has(system.EWayland) { if seal.et.Has(system.EWayland) {
var wp string
if wd, ok := os.LookupEnv(waylandDisplay); !ok { if wd, ok := os.LookupEnv(waylandDisplay); !ok {
return fmsg.WrapError(ErrWayland, return fmsg.WrapError(ErrWayland,
"WAYLAND_DISPLAY is not set") "WAYLAND_DISPLAY is not set")
} else { } else if seal.wl == nil {
wp = path.Join(seal.RuntimePath, wd)
}
w := path.Join(seal.sys.runtime, "wayland-0")
seal.sys.bwrap.SetEnv[waylandDisplay] = w
if seal.directWayland {
// hardlink wayland socket // hardlink wayland socket
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")
seal.sys.Link(wp, wpi) seal.sys.Link(wp, wpi)
seal.sys.bwrap.SetEnv[waylandDisplay] = w
seal.sys.bwrap.Bind(wpi, w) 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.UpdatePermType(system.EWayland, wp, acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.EWayland, wp, acl.Read, acl.Write, acl.Execute)
} else { } else {
wc := path.Join(seal.SharePath, "wayland") // set wayland socket path for mediation (e.g. `/run/user/%d/wayland-%d`)
wt := path.Join(wc, seal.id) seal.wl.Path = path.Join(seal.RuntimePath, wd)
seal.sys.Ensure(wc, 0711)
appID := seal.fid
if appID == "" {
// use instance ID in case app id is not set
appID = "moe.ophivana.fortify." + seal.id
}
seal.sys.Wayland(wt, wp, appID, seal.id)
seal.sys.bwrap.Bind(wt, w)
} }
} }
@ -71,7 +58,7 @@ func (seal *appSeal) shareDisplay(os linux.System) error {
return fmsg.WrapError(ErrXDisplay, return fmsg.WrapError(ErrXDisplay,
"DISPLAY is not set") "DISPLAY is not set")
} else { } else {
seal.sys.ChangeHosts("#" + seal.sys.user.us) seal.sys.ChangeHosts(seal.sys.user.Username)
seal.sys.bwrap.SetEnv[display] = d seal.sys.bwrap.SetEnv[display] = d
seal.sys.bwrap.Bind("/tmp/.X11-unix", "/tmp/.X11-unix") seal.sys.bwrap.Bind("/tmp/.X11-unix", "/tmp/.X11-unix")
} }

View File

@ -6,9 +6,9 @@ import (
"io/fs" "io/fs"
"path" "path"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
const ( const (

View File

@ -3,8 +3,8 @@ package app
import ( import (
"path" "path"
"git.gensokyo.uk/security/fortify/acl" "git.ophivana.moe/security/fortify/acl"
"git.gensokyo.uk/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
const ( const (

View File

@ -3,9 +3,9 @@ package app
import ( import (
"path" "path"
"git.gensokyo.uk/security/fortify/acl" "git.ophivana.moe/security/fortify/acl"
"git.gensokyo.uk/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
const ( const (
@ -16,12 +16,12 @@ const (
func (seal *appSeal) shareSystem() { func (seal *appSeal) shareSystem() {
// ensure Share (e.g. `/tmp/fortify.%d`) // ensure Share (e.g. `/tmp/fortify.%d`)
// acl is unnecessary as this directory is world executable // acl is unnecessary as this directory is world executable
seal.sys.Ensure(seal.SharePath, 0711) seal.sys.Ensure(seal.SharePath, 0701)
// ensure process-specific share (e.g. `/tmp/fortify.%d/%s`) // ensure process-specific share (e.g. `/tmp/fortify.%d/%s`)
// acl is unnecessary as this directory is world executable // acl is unnecessary as this directory is world executable
seal.share = path.Join(seal.SharePath, seal.id) seal.share = path.Join(seal.SharePath, seal.id)
seal.sys.Ephemeral(system.Process, seal.share, 0711) seal.sys.Ephemeral(system.Process, seal.share, 0701)
// ensure child tmpdir parent directory (e.g. `/tmp/fortify.%d/tmpdir`) // ensure child tmpdir parent directory (e.g. `/tmp/fortify.%d/tmpdir`)
targetTmpdirParent := path.Join(seal.SharePath, "tmpdir") targetTmpdirParent := path.Join(seal.SharePath, "tmpdir")
@ -29,7 +29,7 @@ func (seal *appSeal) shareSystem() {
seal.sys.UpdatePermType(system.User, targetTmpdirParent, acl.Execute) seal.sys.UpdatePermType(system.User, targetTmpdirParent, acl.Execute)
// ensure child tmpdir (e.g. `/tmp/fortify.%d/tmpdir/%d`) // ensure child tmpdir (e.g. `/tmp/fortify.%d/tmpdir/%d`)
targetTmpdir := path.Join(targetTmpdirParent, seal.sys.user.as) targetTmpdir := path.Join(targetTmpdirParent, seal.sys.user.Uid)
seal.sys.Ensure(targetTmpdir, 01700) seal.sys.Ensure(targetTmpdir, 01700)
seal.sys.UpdatePermType(system.User, targetTmpdir, acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.User, targetTmpdir, acl.Read, acl.Write, acl.Execute)
seal.sys.bwrap.Bind(targetTmpdir, "/tmp", false, true) seal.sys.bwrap.Bind(targetTmpdir, "/tmp", false, true)
@ -49,21 +49,15 @@ func (seal *appSeal) sharePasswd(os linux.System) {
// 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.user.username != "" { if seal.sys.user.Username != "" {
username = seal.sys.user.username username = seal.sys.user.Username
seal.sys.bwrap.SetEnv["USER"] = seal.sys.user.Username
} }
homeDir := "/var/empty" homeDir := "/var/empty"
if seal.sys.user.home != "" { if seal.sys.user.HomeDir != "" {
homeDir = seal.sys.user.home homeDir = seal.sys.user.HomeDir
seal.sys.bwrap.SetEnv["HOME"] = seal.sys.user.HomeDir
} }
// bind home directory
seal.sys.bwrap.Bind(seal.sys.user.data, homeDir, false, true)
seal.sys.bwrap.Chdir = homeDir
seal.sys.bwrap.SetEnv["USER"] = username
seal.sys.bwrap.SetEnv["HOME"] = homeDir
passwd := username + ":x:" + seal.sys.mappedIDString + ":" + seal.sys.mappedIDString + ":Fortify:" + homeDir + ":" + sh + "\n" passwd := username + ":x:" + seal.sys.mappedIDString + ":" + seal.sys.mappedIDString + ":Fortify:" + homeDir + ":" + sh + "\n"
seal.sys.Write(passwdPath, passwd) seal.sys.Write(passwdPath, passwd)

View File

@ -4,15 +4,16 @@ import (
"errors" "errors"
"fmt" "fmt"
"os/exec" "os/exec"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
shim0 "git.gensokyo.uk/security/fortify/cmd/fshim/ipc" shim0 "git.ophivana.moe/security/fortify/cmd/fshim/ipc"
"git.gensokyo.uk/security/fortify/cmd/fshim/ipc/shim" "git.ophivana.moe/security/fortify/cmd/fshim/ipc/shim"
"git.gensokyo.uk/security/fortify/helper" "git.ophivana.moe/security/fortify/helper"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/state" "git.ophivana.moe/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
// Start selects a user switcher and starts shim. // Start selects a user switcher and starts shim.
@ -40,18 +41,30 @@ func (a *app) Start() error {
} }
} }
// select command builder
var commandBuilder shim.CommandBuilder
switch a.seal.launchOption {
case LaunchMethodSudo:
commandBuilder = a.commandBuilderSudo
case LaunchMethodMachineCtl:
commandBuilder = a.commandBuilderMachineCtl
default:
panic("unreachable")
}
// construct shim manager // construct shim manager
a.shim = shim.New( a.shim = shim.New(a.seal.toolPath, uint32(a.seal.sys.UID()), path.Join(a.seal.share, "shim"), a.seal.wl,
uint32(a.seal.sys.UID()),
a.seal.sys.user.as,
a.seal.sys.user.supp,
&shim0.Payload{ &shim0.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.wl != nil,
Verbose: fmsg.Verbose(), Verbose: fmsg.Verbose(),
}, },
// 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 // startup will go ahead, commit system setup
@ -60,24 +73,23 @@ func (a *app) Start() error {
} }
a.seal.sys.needRevert = true a.seal.sys.needRevert = true
// export sync pipe from sys if startTime, err := a.shim.Start(commandBuilder); err != nil {
a.seal.sys.bwrap.SetSync(a.seal.sys.Sync())
if startTime, err := a.shim.Start(); err != nil {
return err return err
} else { } else {
// shim start and setup success, create process state // shim start and setup success, create process state
sd := state.State{ sd := state.State{
ID: *a.id,
PID: a.shim.Unwrap().Process.Pid, PID: a.shim.Unwrap().Process.Pid,
Config: a.ct.Unwrap(), Command: a.seal.command,
Capability: a.seal.et,
Method: method[a.seal.launchOption],
Argv: a.shim.Unwrap().Args,
Time: *startTime, Time: *startTime,
} }
// register process state // register process state
var err0 = new(StateStoreError) var err0 = new(StateStoreError)
err0.Inner, err0.DoErr = a.seal.store.Do(a.seal.sys.user.aid, func(c state.Cursor) { err0.Inner, err0.DoErr = a.seal.store.Do(func(b state.Backend) {
err0.InnerErr = c.Save(&sd) err0.InnerErr = b.Save(&sd)
}) })
a.seal.sys.saveState = true a.seal.sys.saveState = true
return err0.equiv("cannot save process state:") return err0.equiv("cannot save process state:")
@ -154,13 +166,8 @@ func (a *app) Wait() (int, error) {
// failure prior to process start // failure prior to process start
r = 255 r = 255
} else { } else {
wait := make(chan error, 1)
go func() { wait <- cmd.Wait() }()
select {
// wait for process and resolve exit code // wait for process and resolve exit code
case err := <-wait: if err := cmd.Wait(); err != nil {
if 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
@ -173,16 +180,6 @@ func (a *app) Wait() (int, error) {
r = cmd.ProcessState.ExitCode() r = cmd.ProcessState.ExitCode()
} }
fmsg.VPrintf("process %d exited with exit code %d", cmd.Process.Pid, r) fmsg.VPrintf("process %d exited with exit code %d", cmd.Process.Pid, r)
// alternative exit path when kill was unsuccessful
case err := <-a.shim.WaitFallback():
r = 255
if err != nil {
fmsg.Printf("cannot terminate shim on faulted setup: %v", err)
} else {
fmsg.VPrintln("alternative exit path selected")
}
}
} }
// child process exited, resume output // child process exited, resume output
@ -197,13 +194,20 @@ func (a *app) Wait() (int, error) {
}) })
} }
// close wayland connection
if a.seal.wl != nil {
if err := a.seal.wl.Close(); err != nil {
fmsg.Println("cannot close wayland connection:", err)
}
}
// update store and revert app setup transaction // update store and revert app setup transaction
e := new(StateStoreError) e := new(StateStoreError)
e.Inner, e.DoErr = a.seal.store.Do(a.seal.sys.user.aid, func(b state.Cursor) { 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 cmd := a.shim.Unwrap(); cmd != nil && a.seal.sys.saveState { if cmd := a.shim.Unwrap(); cmd != nil && a.seal.sys.saveState {
if err := b.Destroy(*a.id); err != nil { if err := b.Destroy(cmd.Process.Pid); err != nil {
return err return err
} }
} }
@ -224,12 +228,8 @@ func (a *app) Wait() (int, error) {
} }
// accumulate capabilities of other launchers // accumulate capabilities of other launchers
for i, s := range states { for _, s := range states {
if s.Config != nil { *rt |= s.Capability
*rt |= s.Config.Confinement.Enablements
} else {
fmsg.Printf("state entry %d does not contain config", i)
}
} }
} }
// invert accumulated enablements for cleanup // invert accumulated enablements for cleanup
@ -250,11 +250,10 @@ func (a *app) Wait() (int, error) {
} }
} }
if a.seal.sys.needRevert { a.shim.AbortWait(errors.New("shim exited"))
if err := a.seal.sys.Revert(ec); err != nil { if err := a.seal.sys.Revert(ec); err != nil {
return err.(RevertCompoundError) return err.(RevertCompoundError)
} }
}
return nil return nil
}() }()

View File

@ -1,10 +1,12 @@
package app package app
import ( import (
"git.gensokyo.uk/security/fortify/dbus" "os/user"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/internal/system" "git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/system"
) )
// appSealSys encapsulates app seal behaviour with OS interactions // appSealSys encapsulates app seal behaviour with OS interactions
@ -16,7 +18,7 @@ type appSealSys struct {
// default formatted XDG_RUNTIME_DIR of User // default formatted XDG_RUNTIME_DIR of User
runtime string runtime string
// target user sealed from config // target user sealed from config
user appUser user *user.User
// mapped uid and gid in user namespace // mapped uid and gid in user namespace
mappedID int mappedID int
@ -30,28 +32,6 @@ type appSealSys struct {
// protected by upstream mutex // protected by upstream mutex
} }
type appUser struct {
// full uid resolved by fsu
uid int
// string representation of uid
us string
// supplementary group ids
supp []string
// application id
aid int
// string representation of aid
as string
// home directory host path
data string
// app user home directory
home string
// passwd database username
username string
}
// shareAll calls all share methods in sequence // shareAll calls all share methods in sequence
func (seal *appSeal) shareAll(bus [2]*dbus.Config, os linux.System) error { func (seal *appSeal) shareAll(bus [2]*dbus.Config, os linux.System) error {
if seal.shared { if seal.shared {

View File

@ -33,17 +33,16 @@ func dequeue() {
// queue submits ops to msgbuf but drops messages // queue submits ops to msgbuf but drops messages
// when the buffer is full and dequeue is withholding // when the buffer is full and dequeue is withholding
func queue(op dOp) { func queue(op dOp) {
queueSync.Add(1)
select { select {
case msgbuf <- op: case msgbuf <- op:
queueSync.Add(1)
default: default:
// send the op anyway if not withholding // send the op anyway if not withholding
// as dequeue will get to it eventually // as dequeue will get to it eventually
if !wstate.Load() { if !wstate.Load() {
queueSync.Add(1)
msgbuf <- op msgbuf <- op
} else { } else {
queueSync.Done()
// increment dropped message count // increment dropped message count
dropped.Add(1) dropped.Add(1)
} }
@ -57,10 +56,9 @@ func Exit(code int) {
os.Exit(code) os.Exit(code)
} }
func Suspend() { func Withhold() {
dequeueOnce.Do(dequeue) dequeueOnce.Do(dequeue)
if wstate.CompareAndSwap(false, true) { if wstate.CompareAndSwap(false, true) {
queueSync.Wait()
withhold <- struct{}{} withhold <- struct{}{}
} }
} }

View File

@ -1,13 +1,12 @@
package linux package linux
import ( import (
"io"
"io/fs" "io/fs"
"os/user" "os/user"
"path" "path"
"strconv" "strconv"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
// System provides safe access to operating system resources. // System provides safe access to operating system resources.
@ -22,8 +21,8 @@ type System interface {
LookPath(file string) (string, error) LookPath(file string) (string, error)
// Executable provides [os.Executable]. // Executable provides [os.Executable].
Executable() (string, error) Executable() (string, error)
// LookupGroup provides [user.LookupGroup]. // Lookup provides [user.Lookup].
LookupGroup(name string) (*user.Group, error) Lookup(username string) (*user.User, error)
// ReadDir provides [os.ReadDir]. // ReadDir provides [os.ReadDir].
ReadDir(name string) ([]fs.DirEntry, error) ReadDir(name string) ([]fs.DirEntry, error)
// Stat provides [os.Stat]. // Stat provides [os.Stat].
@ -32,13 +31,13 @@ type System interface {
Open(name string) (fs.File, error) Open(name string) (fs.File, error)
// Exit provides [os.Exit]. // Exit provides [os.Exit].
Exit(code int) Exit(code int)
// Stdout provides [os.Stdout].
Stdout() io.Writer
// FshimPath returns an absolute path to the fshim binary.
FshimPath() string
// Paths returns a populated [Paths] struct. // Paths returns a populated [Paths] struct.
Paths() Paths Paths() Paths
// Uid invokes fsu and returns target uid. // SdBooted implements https://www.freedesktop.org/software/systemd/man/sd_booted.html
Uid(aid int) (int, error) SdBooted() bool
} }
// Paths contains environment dependent paths used by fortify. // Paths contains environment dependent paths used by fortify.

View File

@ -1,16 +1,15 @@
package linux package linux
import ( import (
"io" "errors"
"io/fs" "io/fs"
"os" "os"
"os/exec" "os/exec"
"os/user" "os/user"
"strconv"
"sync" "sync"
"git.gensokyo.uk/security/fortify/internal" "git.ophivana.moe/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
// Std implements System using the standard library. // Std implements System using the standard library.
@ -18,12 +17,11 @@ type Std struct {
paths Paths paths Paths
pathsOnce sync.Once pathsOnce sync.Once
uidOnce sync.Once sdBooted bool
uidCopy map[int]struct { sdBootedOnce sync.Once
uid int
err error fshim string
} fshimOnce sync.Once
uidMu sync.RWMutex
} }
func (s *Std) Geteuid() int { return os.Geteuid() } func (s *Std) Geteuid() int { return os.Geteuid() }
@ -31,58 +29,55 @@ func (s *Std) LookupEnv(key string) (string, bool) { return os.LookupEn
func (s *Std) TempDir() string { return os.TempDir() } func (s *Std) TempDir() string { return os.TempDir() }
func (s *Std) LookPath(file string) (string, error) { return exec.LookPath(file) } func (s *Std) LookPath(file string) (string, error) { return exec.LookPath(file) }
func (s *Std) Executable() (string, error) { return os.Executable() } func (s *Std) Executable() (string, error) { return os.Executable() }
func (s *Std) LookupGroup(name string) (*user.Group, error) { return user.LookupGroup(name) } 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) 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) 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) Open(name string) (fs.File, error) { return os.Open(name) }
func (s *Std) Exit(code int) { fmsg.Exit(code) } func (s *Std) Exit(code int) { fmsg.Exit(code) }
func (s *Std) Stdout() io.Writer { return os.Stdout }
const xdgRuntimeDir = "XDG_RUNTIME_DIR" const xdgRuntimeDir = "XDG_RUNTIME_DIR"
func (s *Std) FshimPath() string {
s.fshimOnce.Do(func() {
p, ok := internal.Path(internal.Fshim)
if !ok {
fmsg.Fatal("invalid fshim path, this copy of fortify is not compiled correctly")
}
s.fshim = p
})
return s.fshim
}
func (s *Std) Paths() Paths { func (s *Std) Paths() Paths {
s.pathsOnce.Do(func() { CopyPaths(s, &s.paths) }) s.pathsOnce.Do(func() { CopyPaths(s, &s.paths) })
return s.paths return s.paths
} }
func (s *Std) Uid(aid int) (int, error) { func (s *Std) SdBooted() bool {
s.uidOnce.Do(func() { s.sdBootedOnce.Do(func() { s.sdBooted = copySdBooted() })
s.uidCopy = make(map[int]struct { return s.sdBooted
uid int }
err error
})
})
s.uidMu.RLock() const systemdCheckPath = "/run/systemd/system"
if u, ok := s.uidCopy[aid]; ok {
s.uidMu.RUnlock()
return u.uid, u.err
}
s.uidMu.RUnlock() func copySdBooted() bool {
s.uidMu.Lock() if v, err := sdBooted(); err != nil {
defer s.uidMu.Unlock() fmsg.Println("cannot read systemd marker:", err)
return false
u := struct {
uid int
err error
}{}
defer func() { s.uidCopy[aid] = u }()
u.uid = -1
if fsu, ok := internal.Check(internal.Fsu); !ok {
fmsg.Fatal("invalid fsu path, this copy of fshim is not compiled correctly")
panic("unreachable")
} else { } else {
cmd := exec.Command(fsu) return v
cmd.Path = fsu
cmd.Stderr = os.Stderr // pass through fatal messages
cmd.Env = []string{"FORTIFY_APP_ID=" + strconv.Itoa(aid)}
cmd.Dir = "/"
var p []byte
if p, u.err = cmd.Output(); u.err == nil {
u.uid, u.err = strconv.Atoi(string(p))
}
return u.uid, u.err
} }
} }
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

@ -4,6 +4,7 @@ import "path"
var ( var (
Fsu = compPoison Fsu = compPoison
Fshim = compPoison
Finit = compPoison Finit = compPoison
) )

View File

@ -1,42 +0,0 @@
package proc
import (
"encoding/gob"
"errors"
"os"
"strconv"
)
var (
ErrNotSet = errors.New("environment variable not set")
ErrInvalid = errors.New("bad file descriptor")
)
func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
if r, w, err := os.Pipe(); err != nil {
return -1, nil, err
} else {
fd := 3 + len(*extraFiles)
*extraFiles = append(*extraFiles, r)
return fd, gob.NewEncoder(w), nil
}
}
func Receive(key string, e any) (func() error, error) {
var setup *os.File
if s, ok := os.LookupEnv(key); !ok {
return nil, ErrNotSet
} else {
if fd, err := strconv.Atoi(s); err != nil {
return nil, err
} else {
setup = os.NewFile(uintptr(fd), "setup")
if setup == nil {
return nil, ErrInvalid
}
}
}
return func() error { return setup.Close() }, gob.NewDecoder(setup).Decode(e)
}

View File

@ -1,13 +0,0 @@
package proc
import (
"os"
"os/exec"
)
func ExtraFile(cmd *exec.Cmd, f *os.File) (fd uintptr) {
// ExtraFiles: If non-nil, entry i becomes file descriptor 3+i.
fd = uintptr(3 + len(cmd.ExtraFiles))
cmd.ExtraFiles = append(cmd.ExtraFiles, f)
return
}

View File

@ -1,292 +0,0 @@
package state
import (
"encoding/gob"
"errors"
"fmt"
"io/fs"
"os"
"path"
"strconv"
"sync"
"syscall"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
// fine-grained locking and access
type multiStore struct {
base string
// initialised backends
backends *sync.Map
lock sync.RWMutex
}
func (s *multiStore) Do(aid int, f func(c Cursor)) (bool, error) {
s.lock.RLock()
defer s.lock.RUnlock()
// load or initialise new backend
b := new(multiBackend)
if v, ok := s.backends.LoadOrStore(aid, b); ok {
b = v.(*multiBackend)
} else {
b.lock.Lock()
b.path = path.Join(s.base, strconv.Itoa(aid))
// ensure directory
if err := os.MkdirAll(b.path, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
s.backends.CompareAndDelete(aid, b)
return false, err
}
// open locker file
if l, err := os.OpenFile(b.path+".lock", os.O_RDWR|os.O_CREATE, 0600); err != nil {
s.backends.CompareAndDelete(aid, b)
return false, err
} else {
b.lockfile = l
}
b.lock.Unlock()
}
// lock backend
if err := b.lockFile(); err != nil {
return false, err
}
// expose backend methods without exporting the pointer
c := new(struct{ *multiBackend })
c.multiBackend = b
f(b)
// disable access to the backend on a best-effort basis
c.multiBackend = nil
// unlock backend
return true, b.unlockFile()
}
func (s *multiStore) List() ([]int, error) {
var entries []os.DirEntry
// read base directory to get all aids
if v, err := os.ReadDir(s.base); err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, err
} else {
entries = v
}
aidsBuf := make([]int, 0, len(entries))
for _, e := range entries {
// skip non-directories
if !e.IsDir() {
fmsg.VPrintf("skipped non-directory entry %q", e.Name())
continue
}
// skip non-numerical names
if v, err := strconv.Atoi(e.Name()); err != nil {
fmsg.VPrintf("skipped non-aid entry %q", e.Name())
continue
} else {
if v < 0 || v > 9999 {
fmsg.VPrintf("skipped out of bounds entry %q", e.Name())
continue
}
aidsBuf = append(aidsBuf, v)
}
}
return append([]int(nil), aidsBuf...), nil
}
func (s *multiStore) Close() error {
s.lock.Lock()
defer s.lock.Unlock()
var errs []error
s.backends.Range(func(_, value any) bool {
b := value.(*multiBackend)
errs = append(errs, b.close())
return true
})
return errors.Join(errs...)
}
type multiBackend struct {
path string
// created/opened by prepare
lockfile *os.File
lock sync.RWMutex
}
func (b *multiBackend) filename(id *fst.ID) string {
return path.Join(b.path, id.String())
}
func (b *multiBackend) lockFileAct(lt int) (err error) {
op := "LockAct"
switch lt {
case syscall.LOCK_EX:
op = "Lock"
case syscall.LOCK_UN:
op = "Unlock"
}
for {
err = syscall.Flock(int(b.lockfile.Fd()), lt)
if !errors.Is(err, syscall.EINTR) {
break
}
}
if err != nil {
return &fs.PathError{
Op: op,
Path: b.lockfile.Name(),
Err: err,
}
}
return nil
}
func (b *multiBackend) lockFile() error {
return b.lockFileAct(syscall.LOCK_EX)
}
func (b *multiBackend) unlockFile() error {
return b.lockFileAct(syscall.LOCK_UN)
}
// reads all launchers in simpleBackend
// file contents are ignored if decode is false
func (b *multiBackend) load(decode bool) (Entries, error) {
b.lock.RLock()
defer b.lock.RUnlock()
// read directory contents, should only contain files named after ids
var entries []os.DirEntry
if pl, err := os.ReadDir(b.path); err != nil {
return nil, err
} else {
entries = pl
}
// allocate as if every entry is valid
// since that should be the case assuming no external interference happens
r := make(Entries, len(entries))
for _, e := range entries {
if e.IsDir() {
return nil, fmt.Errorf("unexpected directory %q in store", e.Name())
}
id := new(fst.ID)
if err := fst.ParseAppID(id, e.Name()); err != nil {
return nil, err
}
// run in a function to better handle file closing
if err := func() error {
// open state file for reading
if f, err := os.Open(path.Join(b.path, e.Name())); err != nil {
return err
} else {
defer func() {
if f.Close() != nil {
// unreachable
panic("foreign state file closed prematurely")
}
}()
s := new(State)
r[*id] = s
// append regardless, but only parse if required, used to implement Len
if decode {
if err = gob.NewDecoder(f).Decode(s); err != nil {
return err
}
if s.ID != *id {
return fmt.Errorf("state entry %s has unexpected id %s", id, &s.ID)
}
}
return nil
}
}(); err != nil {
return nil, err
}
}
return r, nil
}
// Save writes process state to filesystem
func (b *multiBackend) Save(state *State) error {
b.lock.Lock()
defer b.lock.Unlock()
if state.Config == nil {
return errors.New("state does not contain config")
}
statePath := b.filename(&state.ID)
// create and open state data file
if f, err := os.OpenFile(statePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600); err != nil {
return err
} else {
defer func() {
if f.Close() != nil {
// unreachable
panic("state file closed prematurely")
}
}()
// encode into state file
return gob.NewEncoder(f).Encode(state)
}
}
func (b *multiBackend) Destroy(id fst.ID) error {
b.lock.Lock()
defer b.lock.Unlock()
return os.Remove(b.filename(&id))
}
func (b *multiBackend) Load() (Entries, error) {
return b.load(true)
}
func (b *multiBackend) Len() (int, error) {
// rn consists of only nil entries but has the correct length
rn, err := b.load(false)
return len(rn), err
}
func (b *multiBackend) close() error {
b.lock.Lock()
defer b.lock.Unlock()
err := b.lockfile.Close()
if err == nil || errors.Is(err, os.ErrInvalid) || errors.Is(err, os.ErrClosed) {
return nil
}
return err
}
// NewMulti returns an instance of the multi-file store.
func NewMulti(runDir string) Store {
b := new(multiStore)
b.base = path.Join(runDir, "state")
b.backends = new(sync.Map)
return b
}

View File

@ -1,11 +0,0 @@
package state_test
import (
"testing"
"git.gensokyo.uk/security/fortify/internal/state"
)
func TestMulti(t *testing.T) {
testStore(t, state.NewMulti(t.TempDir()))
}

View File

@ -1,45 +1,62 @@
package state package state
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"path"
"strconv"
"strings" "strings"
"text/tabwriter" "text/tabwriter"
"time" "time"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/system" "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
// in an implementation-specific way. // in an implementation-specific way.
func MustPrintLauncherStateSimpleGlobal(w **tabwriter.Writer, runDir string) { func MustPrintLauncherStateSimpleGlobal(w **tabwriter.Writer, runDir string) {
now := time.Now().UTC() now := time.Now().UTC()
s := NewMulti(runDir)
// read runtime directory to get all UIDs // read runtime directory to get all UIDs
if aids, err := s.List(); err != nil { if dirs, err := os.ReadDir(path.Join(runDir, "state")); err != nil && !errors.Is(err, os.ErrNotExist) {
fmsg.Fatal("cannot list store:", err) fmsg.Fatal("cannot read runtime directory:", err)
} else { } else {
for _, aid := range aids { for _, e := range dirs {
// print states belonging to this store // skip non-directories
s.(*multiStore).mustPrintLauncherState(aid, w, now) if !e.IsDir() {
} fmsg.VPrintf("skipped non-directory entry %q", e.Name())
continue
} }
// skip non-numerical names
if _, err = strconv.Atoi(e.Name()); err != nil {
fmsg.VPrintf("skipped non-uid entry %q", e.Name())
continue
}
// obtain temporary store
s := NewSimple(runDir, e.Name()).(*simpleStore)
// print states belonging to this store
s.mustPrintLauncherState(w, now)
// 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 {
fmsg.Printf("cannot close store: %v", err) fmsg.Printf("cannot close store for user %q: %s", e.Name(), err)
}
}
} }
} }
func (s *multiStore) mustPrintLauncherState(aid int, w **tabwriter.Writer, now time.Time) { func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time) {
var innerErr error var innerErr error
if ok, err := s.Do(aid, func(c Cursor) { if ok, err := s.Do(func(b Backend) {
innerErr = func() error { innerErr = func() error {
// read launcher states // read launcher states
states, err := c.Load() states, err := b.Load()
if err != nil { if err != nil {
return err return err
} }
@ -50,10 +67,10 @@ func (s *multiStore) mustPrintLauncherState(aid int, w **tabwriter.Writer, now t
// write header when initialising // write header when initialising
if !fmsg.Verbose() { if !fmsg.Verbose() {
_, _ = fmt.Fprintln(*w, "\tPID\tApp\tUptime\tEnablements\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, "\tPID\tApp\tArgv") _, _ = fmt.Fprintln(*w, "\tUID\tPID\tArgv")
} }
} }
@ -65,54 +82,40 @@ func (s *multiStore) mustPrintLauncherState(aid int, w **tabwriter.Writer, now t
continue continue
} }
// build enablements and command string // build enablements string
var ( ets := strings.Builder{}
ets *strings.Builder
cs = "(No command information)"
)
// check if enablements are provided
if state.Config != nil {
ets = new(strings.Builder)
// append enablement strings in order // append enablement strings in order
for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ { for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
if state.Config.Confinement.Enablements.Has(i) { if state.Capability.Has(i) {
ets.WriteString(", " + i.String()) ets.WriteString(", " + i.String())
} }
} }
// prevent an empty string when
cs = fmt.Sprintf("%q", state.Config.Command)
}
if ets != nil {
// prevent an empty string
if ets.Len() == 0 { if ets.Len() == 0 {
ets.WriteString("(No enablements)") ets.WriteString("(No enablements)")
} }
} else {
ets = new(strings.Builder)
ets.WriteString("(No confinement information)")
}
if !fmsg.Verbose() { if !fmsg.Verbose() {
_, _ = fmt.Fprintf(*w, "\t%d\t%d\t%s\t%s\t%s\n", _, _ = fmt.Fprintf(*w, "\t%s\t%d\t%s\t%s\t%s\t%s\n",
state.PID, aid, now.Sub(state.Time).Round(time.Second).String(), strings.TrimPrefix(ets.String(), ", "), cs) s.path[len(s.path)-1], state.PID, now.Sub(state.Time).Round(time.Second).String(), strings.TrimPrefix(ets.String(), ", "), state.Method,
state.Command)
} else { } else {
// emit argv instead when verbose // emit argv instead when verbose
_, _ = fmt.Fprintf(*w, "\t%d\t%d\t%s\n", _, _ = fmt.Fprintf(*w, "\t%s\t%d\t%s\n",
state.PID, aid, state.ID) s.path[len(s.path)-1], state.PID, state.Argv)
} }
} }
return nil return nil
}() }()
}); err != nil { }); err != nil {
fmsg.Printf("cannot perform action on app %d: %v", aid, err) fmsg.Printf("cannot perform action on store %q: %s", path.Join(s.path...), err)
if !ok { if !ok {
fmsg.Fatal("store faulted before printing") fmsg.Fatal("store faulted before printing")
} }
} }
if innerErr != nil { if innerErr != nil {
fmsg.Fatalf("cannot print launcher state of app %d: %s", aid, innerErr) fmsg.Fatalf("cannot print launcher state for store %q: %s", path.Join(s.path...), innerErr)
} }
} }

218
internal/state/simple.go Normal file
View File

@ -0,0 +1,218 @@
package state
import (
"encoding/gob"
"errors"
"io/fs"
"os"
"path"
"strconv"
"sync"
"syscall"
)
// file-based locking
type simpleStore struct {
path []string
// created/opened by prepare
lockfile *os.File
// enforce prepare method
init sync.Once
// error returned by prepare
initErr error
lock sync.Mutex
}
func (s *simpleStore) Do(f func(b Backend)) (bool, error) {
s.init.Do(s.prepare)
if s.initErr != nil {
return false, s.initErr
}
s.lock.Lock()
defer s.lock.Unlock()
// lock store
if err := s.lockFile(); err != nil {
return false, err
}
// initialise new backend for caller
b := new(simpleBackend)
b.path = path.Join(s.path...)
f(b)
// disable backend
b.lock.Lock()
// unlock store
return true, s.unlockFile()
}
func (s *simpleStore) lockFileAct(lt int) (err error) {
op := "LockAct"
switch lt {
case syscall.LOCK_EX:
op = "Lock"
case syscall.LOCK_UN:
op = "Unlock"
}
for {
err = syscall.Flock(int(s.lockfile.Fd()), lt)
if !errors.Is(err, syscall.EINTR) {
break
}
}
if err != nil {
return &fs.PathError{
Op: op,
Path: s.lockfile.Name(),
Err: err,
}
}
return nil
}
func (s *simpleStore) lockFile() error {
return s.lockFileAct(syscall.LOCK_EX)
}
func (s *simpleStore) unlockFile() error {
return s.lockFileAct(syscall.LOCK_UN)
}
func (s *simpleStore) prepare() {
s.initErr = func() error {
prefix := path.Join(s.path...)
// ensure directory
if err := os.MkdirAll(prefix, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
return err
}
// open locker file
if f, err := os.OpenFile(prefix+".lock", os.O_RDWR|os.O_CREATE, 0600); err != nil {
return err
} else {
s.lockfile = f
}
return nil
}()
}
func (s *simpleStore) Close() error {
s.lock.Lock()
defer s.lock.Unlock()
err := s.lockfile.Close()
if err == nil || errors.Is(err, os.ErrInvalid) || errors.Is(err, os.ErrClosed) {
return nil
}
return err
}
type simpleBackend struct {
path string
lock sync.RWMutex
}
func (b *simpleBackend) filename(pid int) string {
return path.Join(b.path, strconv.Itoa(pid))
}
// reads all launchers in simpleBackend
// file contents are ignored if decode is false
func (b *simpleBackend) load(decode bool) ([]*State, error) {
b.lock.RLock()
defer b.lock.RUnlock()
var (
r []*State
f *os.File
)
// read directory contents, should only contain files named after PIDs
if pl, err := os.ReadDir(b.path); err != nil {
return nil, err
} else {
for _, e := range pl {
// run in a function to better handle file closing
if err = func() error {
// open state file for reading
if f, err = os.Open(path.Join(b.path, e.Name())); err != nil {
return err
} else {
defer func() {
if f.Close() != nil {
// unreachable
panic("foreign state file closed prematurely")
}
}()
var s State
r = append(r, &s)
// append regardless, but only parse if required, used to implement Len
if decode {
return gob.NewDecoder(f).Decode(&s)
} else {
return nil
}
}
}(); err != nil {
return nil, err
}
}
}
return r, nil
}
// Save writes process state to filesystem
func (b *simpleBackend) Save(state *State) error {
b.lock.Lock()
defer b.lock.Unlock()
statePath := b.filename(state.PID)
// create and open state data file
if f, err := os.OpenFile(statePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600); err != nil {
return err
} else {
defer func() {
if f.Close() != nil {
// unreachable
panic("state file closed prematurely")
}
}()
// encode into state file
return gob.NewEncoder(f).Encode(state)
}
}
func (b *simpleBackend) Destroy(pid int) error {
b.lock.Lock()
defer b.lock.Unlock()
return os.Remove(b.filename(pid))
}
func (b *simpleBackend) Load() ([]*State, error) {
return b.load(true)
}
func (b *simpleBackend) Len() (int, error) {
// rn consists of only nil entries but has the correct length
rn, err := b.load(false)
return len(rn), err
}
// NewSimple returns an instance of a file-based store.
func NewSimple(runDir string, prefix ...string) Store {
b := new(simpleStore)
b.path = append([]string{runDir, "state"}, prefix...)
return b
}

View File

@ -3,42 +3,40 @@ package state
import ( import (
"time" "time"
"git.gensokyo.uk/security/fortify/fst" "git.ophivana.moe/security/fortify/internal/system"
) )
type Entries map[fst.ID]*State
type Store interface { type Store interface {
// Do calls f exactly once and ensures store exclusivity until f returns. // Do calls f exactly once and ensures store exclusivity until f returns.
// Returns whether f is called and any errors during the locking process. // Returns whether f is called and any errors during the locking process.
// Cursor provided to f becomes invalid as soon as f returns. // Backend provided to f becomes invalid as soon as f returns.
Do(aid int, f func(c Cursor)) (ok bool, err error) Do(f func(b Backend)) (bool, error)
// List queries the store and returns a list of aids known to the store.
// Note that some or all returned aids might not have any active apps.
List() (aids []int, err error)
// Close releases any resources held by Store. // Close releases any resources held by Store.
Close() error Close() error
} }
// Cursor provides access to the store // Backend provides access to the store
type Cursor interface { type Backend interface {
Save(state *State) error Save(state *State) error
Destroy(id fst.ID) error Destroy(pid int) error
Load() (Entries, error) Load() ([]*State, error)
Len() (int, error) Len() (int, error)
} }
// State is the on-disk format for a fortified process's state information // State is the on-disk format for a fortified process's state information
type State struct { type State struct {
// fortify instance id
ID fst.ID `json:"instance"`
// child process PID value // child process PID value
PID int `json:"pid"` PID int
// sealed app configuration // command used to seal the app
Config *fst.Config `json:"config"` Command []string
// capability enablements applied to child
Capability system.Enablements
// user switch method
Method string
// full argv whe launching
Argv []string
// process start time // process start time
Time time.Time Time time.Time
} }

View File

@ -1,126 +0,0 @@
package state_test
import (
"math/rand/v2"
"reflect"
"slices"
"testing"
"time"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/state"
)
func testStore(t *testing.T, s state.Store) {
t.Run("list empty store", func(t *testing.T) {
if aids, err := s.List(); err != nil {
t.Fatalf("List: error = %v", err)
} else if len(aids) != 0 {
t.Fatalf("List: aids = %#v", aids)
}
})
const (
insertEntryChecked = iota
insertEntryNoCheck
insertEntryOtherApp
tl
)
var tc [tl]state.State
for i := 0; i < tl; i++ {
makeState(t, &tc[i])
}
do := func(aid int, f func(c state.Cursor)) {
if ok, err := s.Do(aid, f); err != nil {
t.Fatalf("Do: ok = %v, error = %v", ok, err)
}
}
insert := func(i, aid int) {
do(aid, func(c state.Cursor) {
if err := c.Save(&tc[i]); err != nil {
t.Fatalf("Save(&tc[%v]): error = %v", i, err)
}
})
}
check := func(i, aid int) {
do(aid, func(c state.Cursor) {
if entries, err := c.Load(); err != nil {
t.Fatalf("Load: error = %v", err)
} else if got, ok := entries[tc[i].ID]; !ok {
t.Fatalf("Load: entry %s missing",
&tc[i].ID)
} else {
got.Time = tc[i].Time
if !reflect.DeepEqual(got, &tc[i]) {
t.Fatalf("Load: entry %s got %#v, want %#v",
&tc[i].ID, got, &tc[i])
}
}
})
}
t.Run("insert entry checked", func(t *testing.T) {
insert(insertEntryChecked, 0)
check(insertEntryChecked, 0)
})
t.Run("insert entry unchecked", func(t *testing.T) {
insert(insertEntryNoCheck, 0)
})
t.Run("insert entry different aid", func(t *testing.T) {
insert(insertEntryOtherApp, 1)
check(insertEntryOtherApp, 1)
})
t.Run("check previous insertion", func(t *testing.T) {
check(insertEntryNoCheck, 0)
})
t.Run("list aids", func(t *testing.T) {
if aids, err := s.List(); err != nil {
t.Fatalf("List: error = %v", err)
} else {
slices.Sort(aids)
want := []int{0, 1}
if slices.Compare(aids, want) != 0 {
t.Fatalf("List() = %#v, want %#v", aids, want)
}
}
})
t.Run("clear aid 1", func(t *testing.T) {
do(1, func(c state.Cursor) {
if err := c.Destroy(tc[insertEntryOtherApp].ID); err != nil {
t.Fatalf("Destroy: error = %v", err)
}
})
do(1, func(c state.Cursor) {
if l, err := c.Len(); err != nil {
t.Fatalf("Len: error = %v", err)
} else if l != 0 {
t.Fatalf("Len() = %d, want 0", l)
}
})
})
t.Run("close store", func(t *testing.T) {
if err := s.Close(); err != nil {
t.Fatalf("Close: error = %v", err)
}
})
}
func makeState(t *testing.T, s *state.State) {
if err := fst.NewAppID(&s.ID); err != nil {
t.Fatalf("cannot create dummy state: %v", err)
}
s.Config = fst.Template()
s.PID = rand.Int()
s.Time = time.Now()
}

View File

@ -4,8 +4,8 @@ import (
"fmt" "fmt"
"slices" "slices"
"git.gensokyo.uk/security/fortify/acl" "git.ophivana.moe/security/fortify/acl"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
// UpdatePerm appends an ephemeral acl update Op. // UpdatePerm appends an ephemeral acl update Op.

View File

@ -3,7 +3,7 @@ package system
import ( import (
"testing" "testing"
"git.gensokyo.uk/security/fortify/acl" "git.ophivana.moe/security/fortify/acl"
) )
func TestUpdatePerm(t *testing.T) { func TestUpdatePerm(t *testing.T) {

View File

@ -7,8 +7,8 @@ import (
"strings" "strings"
"sync" "sync"
"git.gensokyo.uk/security/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
var ( var (

View File

@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
// Ensure the existence and mode of a directory. // Ensure the existence and mode of a directory.

View File

@ -2,10 +2,9 @@ package system
import ( import (
"errors" "errors"
"os"
"sync" "sync"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
const ( const (
@ -57,7 +56,6 @@ func TypeString(e Enablement) string {
type I struct { type I struct {
uid int uid int
ops []Op ops []Op
sp *os.File
state [2]bool state [2]bool
lock sync.Mutex lock sync.Mutex
@ -67,10 +65,6 @@ func (sys *I) UID() int {
return sys.uid return sys.uid
} }
func (sys *I) Sync() *os.File {
return sys.sp
}
func (sys *I) Equal(v *I) bool { func (sys *I) Equal(v *I) bool {
if v == nil || sys.uid != v.uid || len(sys.ops) != len(v.ops) { if v == nil || sys.uid != v.uid || len(sys.ops) != len(v.ops) {
return false return false

View File

@ -4,7 +4,7 @@ import (
"strconv" "strconv"
"testing" "testing"
"git.gensokyo.uk/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
func TestNew(t *testing.T) { func TestNew(t *testing.T) {

View File

@ -7,8 +7,8 @@ import (
"os" "os"
"strconv" "strconv"
"git.gensokyo.uk/security/fortify/acl" "git.ophivana.moe/security/fortify/acl"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
// CopyFile registers an Op that copies path dst from src. // CopyFile registers an Op that copies path dst from src.

View File

@ -4,7 +4,7 @@ import (
"strconv" "strconv"
"testing" "testing"
"git.gensokyo.uk/security/fortify/acl" "git.ophivana.moe/security/fortify/acl"
) )
func TestCopyFile(t *testing.T) { func TestCopyFile(t *testing.T) {

View File

@ -1,85 +0,0 @@
package system
import (
"errors"
"fmt"
"os"
"git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/wl"
)
// Wayland sets up a wayland socket with a security context attached.
func (sys *I) Wayland(dst, src, appID, instanceID string) *I {
sys.lock.Lock()
defer sys.lock.Unlock()
sys.ops = append(sys.ops, Wayland{[2]string{dst, src}, new(wl.Conn), appID, instanceID})
return sys
}
type Wayland struct {
pair [2]string
conn *wl.Conn
appID, instanceID string
}
func (w Wayland) Type() Enablement {
return Process
}
func (w Wayland) apply(sys *I) error {
// the Wayland op is not repeatable
if sys.sp != nil {
return errors.New("attempted to attach multiple wayland sockets")
}
if err := w.conn.Attach(w.pair[1]); err != nil {
return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot attach to wayland on %q:", w.pair[1]))
} else {
fmsg.VPrintf("wayland attached on %q", w.pair[1])
}
if sp, err := w.conn.Bind(w.pair[0], w.appID, w.instanceID); err != nil {
return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot bind to socket on %q:", w.pair[0]))
} else {
sys.sp = sp
fmsg.VPrintf("wayland listening on %q", w.pair[0])
return fmsg.WrapErrorSuffix(errors.Join(os.Chmod(w.pair[0], 0), acl.UpdatePerm(w.pair[0], sys.uid, acl.Read, acl.Write, acl.Execute)),
fmt.Sprintf("cannot chmod socket on %q:", w.pair[0]))
}
}
func (w Wayland) revert(_ *I, ec *Criteria) error {
if ec.hasType(w) {
fmsg.VPrintf("removing wayland socket on %q", w.pair[0])
if err := os.Remove(w.pair[0]); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
fmsg.VPrintf("detaching from wayland on %q", w.pair[1])
return fmsg.WrapErrorSuffix(w.conn.Close(),
fmt.Sprintf("cannot detach from wayland on %q:", w.pair[1]))
} else {
fmsg.VPrintf("skipping wayland cleanup on %q", w.pair[0])
return nil
}
}
func (w Wayland) Is(o Op) bool {
w0, ok := o.(Wayland)
return ok && w.pair == w0.pair
}
func (w Wayland) Path() string {
return w.pair[0]
}
func (w Wayland) String() string {
return fmt.Sprintf("wayland socket at %q", w.pair[0])
}

View File

@ -3,8 +3,8 @@ package system
import ( import (
"fmt" "fmt"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/xcb" "git.ophivana.moe/security/fortify/xcb"
) )
// ChangeHosts appends an X11 ChangeHosts command Op. // ChangeHosts appends an X11 ChangeHosts command Op.

View File

@ -6,8 +6,8 @@ import (
"os/exec" "os/exec"
"strings" "strings"
"git.gensokyo.uk/security/fortify/helper" "git.ophivana.moe/security/fortify/helper"
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/helper/bwrap"
) )
func Exec(p string) ([]*Entry, error) { func Exec(p string) ([]*Entry, error) {

View File

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

25
license.go Normal file
View File

@ -0,0 +1,25 @@
package main
import (
_ "embed"
"flag"
"fmt"
)
var (
//go:embed LICENSE
license string
printLicense bool
)
func init() {
flag.BoolVar(&printLicense, "license", false, "Print license")
}
func tryLicense() {
if printLicense {
fmt.Println(license)
os.Exit(0)
}
}

269
main.go
View File

@ -1,31 +1,16 @@
package main package main
import ( import (
_ "embed"
"encoding/json"
"flag" "flag"
"fmt"
"os/user"
"strconv"
"strings"
"sync"
"text/tabwriter"
"git.gensokyo.uk/security/fortify/dbus" "git.ophivana.moe/security/fortify/internal"
"git.gensokyo.uk/security/fortify/fst" "git.ophivana.moe/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/app" "git.ophivana.moe/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/internal/system"
) )
var ( var (
flagVerbose bool flagVerbose bool
//go:embed LICENSE
license string
) )
func init() { func init() {
@ -34,253 +19,38 @@ func init() {
var os = new(linux.Std) var os = new(linux.Std)
type gl []string
func (g *gl) String() string {
if g == nil {
return "<nil>"
}
return strings.Join(*g, " ")
}
func (g *gl) Set(v string) error {
*g = append(*g, v)
return nil
}
func main() { func main() {
if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil { if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
fmsg.Printf("cannot set SUID_DUMP_DISABLE: %s", err) fmsg.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
// not fatal: this program runs as the privileged user // not fatal: this program runs as the privileged user
} }
flag.Parse()
fmsg.SetVerbose(flagVerbose)
if os.SdBooted() {
fmsg.VPrintln("system booted with systemd as init system")
}
// root check
if os.Geteuid() == 0 { if os.Geteuid() == 0 {
fmsg.Fatal("this program must not run as root") fmsg.Fatal("this program must not run as root")
panic("unreachable") panic("unreachable")
} }
flag.CommandLine.Usage = func() { // version/license/template command early exit
fmt.Println() tryVersion()
fmt.Println("Usage:\tfortify [-v] COMMAND [OPTIONS]") tryLicense()
fmt.Println() tryTemplate()
fmt.Println("Commands:")
w := tabwriter.NewWriter(os.Stdout(), 0, 1, 4, ' ', 0)
commands := [][2]string{
{"app", "Launch app defined by the specified config file"},
{"run", "Configure and start a permissive default sandbox"},
{"ps", "List active apps and their state"},
{"version", "Show fortify version"},
{"license", "Show full license text"},
{"template", "Produce a config template"},
{"help", "Show this help message"},
}
for _, c := range commands {
_, _ = fmt.Fprintf(w, "\t%s\t%s\n", c[0], c[1])
}
if err := w.Flush(); err != nil {
fmt.Printf("fortify: cannot write command list: %v\n", err)
}
fmt.Println()
}
flag.Parse()
fmsg.SetVerbose(flagVerbose)
args := flag.Args() // state query command early exit
if len(args) == 0 { tryState()
flag.CommandLine.Usage()
fmsg.Exit(0)
}
switch args[0] {
case "version": // print version string
if v, ok := internal.Check(internal.Version); ok {
fmt.Println(v)
} else {
fmt.Println("impure")
}
fmsg.Exit(0)
case "license": // print embedded license
fmt.Println(license)
fmsg.Exit(0)
case "template": // print full template configuration
if s, err := json.MarshalIndent(fst.Template(), "", " "); err != nil {
fmsg.Fatalf("cannot generate template: %v", err)
panic("unreachable")
} else {
fmt.Println(string(s))
}
fmsg.Exit(0)
case "help": // print help message
flag.CommandLine.Usage()
fmsg.Exit(0)
case "ps": // print all state info
var w *tabwriter.Writer
state.MustPrintLauncherStateSimpleGlobal(&w, os.Paths().RunDirPath)
if w != nil {
if err := w.Flush(); err != nil {
fmsg.Println("cannot format output:", err)
}
} else {
fmt.Println("No information available")
}
fmsg.Exit(0)
case "app": // launch app from configuration file
if len(args) < 2 {
fmsg.Fatal("app requires at least 1 argument")
}
config := new(fst.Config)
if f, err := os.Open(args[1]); err != nil {
fmsg.Fatalf("cannot access config file %q: %s", args[1], err)
panic("unreachable")
} else if err = json.NewDecoder(f).Decode(&config); err != nil {
fmsg.Fatalf("cannot parse config file %q: %s", args[1], err)
panic("unreachable")
}
// append extra args
config.Command = append(config.Command, args[2:]...)
// invoke app // invoke app
runApp(config)
case "run": // run app in permissive defaults usage pattern
set := flag.NewFlagSet("run", flag.ExitOnError)
var (
dbusConfigSession string
dbusConfigSystem string
mpris bool
dbusVerbose bool
fid string
aid int
groups gl
homeDir string
userName string
enablements [system.ELen]bool
)
set.StringVar(&dbusConfigSession, "dbus-config", "builtin", "Path to D-Bus proxy config file, or \"builtin\" for defaults")
set.StringVar(&dbusConfigSystem, "dbus-system", "nil", "Path to system D-Bus proxy config file, or \"nil\" to disable")
set.BoolVar(&mpris, "mpris", false, "Allow owning MPRIS D-Bus path, has no effect if custom config is available")
set.BoolVar(&dbusVerbose, "dbus-log", false, "Force logging in the D-Bus proxy")
set.StringVar(&fid, "id", "", "App ID, leave empty to disable security context app_id")
set.IntVar(&aid, "a", 0, "Fortify application ID")
set.Var(&groups, "g", "Groups inherited by the app process")
set.StringVar(&homeDir, "d", "os", "Application home directory")
set.StringVar(&userName, "u", "chronos", "Passwd name within sandbox")
set.BoolVar(&enablements[system.EWayland], "wayland", false, "Allow Wayland connections")
set.BoolVar(&enablements[system.EX11], "X", false, "Share X11 socket and allow connection")
set.BoolVar(&enablements[system.EDBus], "dbus", false, "Proxy D-Bus connection")
set.BoolVar(&enablements[system.EPulse], "pulse", false, "Share PulseAudio socket and cookie")
// Ignore errors; set is set for ExitOnError.
_ = set.Parse(args[1:])
// initialise config from flags
config := &fst.Config{
ID: fid,
Command: set.Args(),
}
if aid < 0 || aid > 9999 {
fmsg.Fatalf("aid %d out of range", aid)
panic("unreachable")
}
// resolve home/username from os when flag is unset
var (
passwd *user.User
passwdOnce sync.Once
passwdFunc = func() {
var us string
if uid, err := os.Uid(aid); err != nil {
fmsg.Fatalf("cannot obtain uid from fsu: %v", err)
} else {
us = strconv.Itoa(uid)
}
if u, err := user.LookupId(us); err != nil {
fmsg.VPrintf("cannot look up uid %s", us)
passwd = &user.User{
Uid: us,
Gid: us,
Username: "chronos",
Name: "Fortify",
HomeDir: "/var/empty",
}
} else {
passwd = u
}
}
)
if homeDir == "os" {
passwdOnce.Do(passwdFunc)
homeDir = passwd.HomeDir
}
if userName == "chronos" {
passwdOnce.Do(passwdFunc)
userName = passwd.Username
}
config.Confinement.AppID = aid
config.Confinement.Groups = groups
config.Confinement.Outer = homeDir
config.Confinement.Username = userName
// enablements from flags
for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
if enablements[i] {
config.Confinement.Enablements.Set(i)
}
}
// parse D-Bus config file from flags if applicable
if enablements[system.EDBus] {
if dbusConfigSession == "builtin" {
config.Confinement.SessionBus = dbus.NewConfig(fid, true, mpris)
} else {
if c, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
fmsg.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
} else {
config.Confinement.SessionBus = c
}
}
// system bus proxy is optional
if dbusConfigSystem != "nil" {
if c, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
fmsg.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
} else {
config.Confinement.SystemBus = c
}
}
// override log from configuration
if dbusVerbose {
config.Confinement.SessionBus.Log = true
config.Confinement.SystemBus.Log = true
}
}
// invoke app
runApp(config)
default:
fmsg.Fatalf("%q is not a valid command", args[0])
}
panic("unreachable")
}
func runApp(config *fst.Config) {
a, err := app.New(os) a, err := app.New(os)
if err != nil { if err != nil {
fmsg.Fatalf("cannot create app: %s\n", err) fmsg.Fatalf("cannot create app: %s\n", err)
} else if err = a.Seal(config); err != nil { } else if err = a.Seal(loadConfig()); err != nil {
logBaseError(err, "cannot seal app:") logBaseError(err, "cannot seal app:")
fmsg.Exit(1) fmsg.Exit(1)
} else if err = a.Start(); err != nil { } else if err = a.Start(); err != nil {
@ -299,5 +69,4 @@ func runApp(config *fst.Config) {
fmsg.Println("inner wait failed:", err) fmsg.Println("inner wait failed:", err)
} }
fmsg.Exit(r) fmsg.Exit(r)
panic("unreachable")
} }

440
nixos.nix
View File

@ -7,214 +7,290 @@
let let
inherit (lib) inherit (lib)
types
mkOption
mkEnableOption
mkIf mkIf
mkDefault
mapAttrs mapAttrs
mergeAttrsList mapAttrsToList
imap1
foldr
foldlAttrs foldlAttrs
optional optional
optionals
; ;
cfg = config.environment.fortify; cfg = config.environment.fortify;
in in
{ {
imports = [ ./options.nix ]; options = {
environment.fortify = {
enable = mkEnableOption "fortify";
config = mkIf cfg.enable { target = mkOption {
security.wrappers.fsu = { default = { };
source = "${cfg.package}/libexec/fsu"; type =
setuid = true; let
owner = "root"; inherit (types)
setgid = true; str
group = "root"; enum
}; bool
package
environment.etc = { anything
fsurc = { submodule
mode = "0400"; listOf
text = foldlAttrs ( attrsOf
acc: username: fid: nullOr
"${toString config.users.users.${username}.uid} ${toString fid}\n" + acc ;
) "" cfg.users; in
}; attrsOf (submodule {
options = {
userdb.source = pkgs.runCommand "fortify-userdb" { } '' packages = mkOption {
${cfg.package}/libexec/fuserdb -o $out ${ type = listOf package;
foldlAttrs ( default = [ ];
acc: username: fid: description = ''
acc + " ${username}:${toString fid}" List of extra packages to install via home-manager.
) "-s /run/current-system/sw/bin/nologin -d ${cfg.stateDir}" cfg.users
}
''; '';
}; };
systemd.services.nix-daemon.unitConfig.RequiresMountsFor = [ "/etc/userdb" ]; launchers = mkOption {
type = attrsOf (submodule {
services.userdbd.enable = mkDefault true; options = {
command = mkOption {
home-manager = type = nullOr str;
let default = null;
privPackages = mapAttrs (username: fid: { description = ''
home.packages = Command to run as the target user.
let Setting this to null will default command to wrapper name.
# aid 0 is reserved '';
wrappers = imap1 (
aid: app:
let
extendDBusDefault = id: ext: {
filter = true;
talk = [ "org.freedesktop.Notifications" ] ++ ext.talk;
own =
(optionals (app.id != null) [
"${id}.*"
"org.mpris.MediaPlayer2.${id}.*"
])
++ ext.own;
inherit (ext) call broadcast;
}; };
dbusConfig =
let dbus = {
default = { config = mkOption {
talk = [ ]; type = nullOr anything;
own = [ ]; default = null;
call = { }; description = ''
broadcast = { }; D-Bus custom configuration.
Setting this to null will enable built-in defaults.
'';
}; };
in
{ configSystem = mkOption {
session_bus = type = nullOr anything;
if app.dbus.session != null then default = null;
(app.dbus.session (extendDBusDefault app.id)) description = ''
else D-Bus system bus custom configuration.
(extendDBusDefault app.id default); Setting this to null will disable the system bus proxy.
system_bus = app.dbus.system; '';
}; };
command = if app.command == null then app.name else app.command;
script = if app.script == null then ("exec " + command + " $@") else app.script; id = mkOption {
enablements = type = nullOr str;
with app.capability; default = null;
(if wayland then 1 else 0) description = ''
+ (if x11 then 2 else 0) D-Bus application id.
+ (if dbus then 4 else 0) Setting this to null will disable own path in defaults.
+ (if pulse then 8 else 0); Has no effect if custom configuration is set.
conf = { '';
inherit (app) id; };
command = [
(pkgs.writeScript "${app.name}-start" '' mpris = mkOption {
#!${pkgs.zsh}${pkgs.zsh.shellPath} type = bool;
${script} default = false;
'') description = ''
Whether to enable MPRIS in D-Bus defaults.
'';
};
};
capability = {
wayland = mkOption {
type = bool;
default = true;
description = ''
Whether to share the Wayland socket.
'';
};
x11 = mkOption {
type = bool;
default = false;
description = ''
Whether to share the X11 socket and allow connection.
'';
};
dbus = mkOption {
type = bool;
default = true;
description = ''
Whether to proxy D-Bus.
'';
};
pulse = mkOption {
type = bool;
default = true;
description = ''
Whether to share the PulseAudio socket and cookie.
'';
};
};
share = mkOption {
type = nullOr package;
default = null;
description = ''
Package containing share files.
Setting this to null will default package name to wrapper name.
'';
};
method = mkOption {
type = enum [
"simple"
"sudo"
"systemd"
]; ];
confinement = { default = "systemd";
app_id = aid; description = ''
inherit (app) groups; Launch method for the sandboxed program.
username = "u${toString fid}_a${toString aid}"; '';
home = "${cfg.stateDir}/u${toString fid}/a${toString aid}";
sandbox = {
inherit (app)
userns
net
dev
env
;
map_real_uid = app.mapRealUid;
no_new_session = app.tty;
filesystem =
[
{ src = "/bin"; }
{ src = "/usr/bin"; }
{ src = "/nix/store"; }
{ src = "/run/current-system"; }
{
src = "/sys/block";
require = false;
}
{
src = "/sys/bus";
require = false;
}
{
src = "/sys/class";
require = false;
}
{
src = "/sys/dev";
require = false;
}
{
src = "/sys/devices";
require = false;
}
]
++ optionals app.nix [
{ src = "/nix/var"; }
{ src = "/var/db/nix-channels"; }
]
++ optionals (if app.gpu != null then app.gpu else app.capability.wayland || app.capability.x11) [
{ src = "/run/opengl-driver"; }
{
src = "/dev/dri";
dev = true;
}
]
++ app.extraPaths;
auto_etc = true;
override = [ "/var/run/nscd" ];
};
inherit enablements;
inherit (dbusConfig) session_bus system_bus;
}; };
}; };
in });
pkgs.writeShellScriptBin app.name '' default = { };
exec fortify app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@ };
''
) cfg.apps; persistence = mkOption {
in type = submodule {
foldr ( options = {
app: acc: directories = mkOption {
type = listOf anything;
default = [ ];
};
files = mkOption {
type = listOf anything;
default = [ ];
};
};
};
description = ''
Per-user state passed to github:nix-community/impermanence.
'';
};
extraConfig = mkOption {
type = anything;
default = { };
description = "Extra home-manager configuration.";
};
};
});
};
package = mkOption {
type = types.package;
default = pkgs.callPackage ./package.nix { };
description = "Package providing fortify.";
};
user = mkOption {
type = types.str;
description = "Privileged user account.";
};
stateDir = mkOption {
type = types.str;
description = ''
The path to persistent storage where per-user state should be stored.
'';
};
};
};
config = mkIf cfg.enable {
environment.persistence.${cfg.stateDir}.users = mapAttrs (_: target: target.persistence) cfg.target;
home-manager.users =
mapAttrs (_: target: target.extraConfig // { home.packages = target.packages; }) cfg.target
// {
${cfg.user}.home.packages =
let let
pkg = if app.share != null then app.share else pkgs.${app.name}; wrap =
copy = source: "[ -d '${source}' ] && cp -Lrv '${source}' $out/share || true"; user: launchers:
mapAttrsToList (
name: launcher:
with launcher.capability;
let
command = if launcher.command == null then name else launcher.command;
dbusConfig =
if launcher.dbus.config != null then
pkgs.writeText "${name}-dbus.json" (builtins.toJSON launcher.dbus.config)
else
null;
dbusSystem =
if launcher.dbus.configSystem != null then
pkgs.writeText "${name}-dbus-system.json" (builtins.toJSON launcher.dbus.configSystem)
else
null;
capArgs =
(if wayland then " --wayland" else "")
+ (if x11 then " -X" else "")
+ (if dbus then " --dbus" else "")
+ (if pulse then " --pulse" else "")
+ (if launcher.dbus.mpris then " --mpris" else "")
+ (if launcher.dbus.id != null then " --dbus-id ${launcher.dbus.id}" else "")
+ (if dbusConfig != null then " --dbus-config ${dbusConfig}" else "")
+ (if dbusSystem != null then " --dbus-system ${dbusSystem}" else "");
in in
optional (app.capability.wayland || app.capability.x11) ( pkgs.writeShellScriptBin name (
pkgs.runCommand "${app.name}-share" { } '' if launcher.method == "simple" then
mkdir -p $out/share ''
${copy "${pkg}/share/applications"} exec sudo -u ${user} -i ${command} $@
${copy "${pkg}/share/pixmaps"} ''
${copy "${pkg}/share/icons"} else
${copy "${pkg}/share/man"} ''
exec fortify${capArgs} --method ${launcher.method} -u ${user} $SHELL -c "exec ${command} $@"
substituteInPlace $out/share/applications/* \
--replace-warn '${pkg}/bin/' "" \
--replace-warn '${pkg}/libexec/' ""
'' ''
) )
++ acc ) launchers;
) (wrappers ++ [ cfg.package ]) cfg.apps;
}) cfg.users;
in in
{ foldlAttrs (
useUserPackages = false; # prevent users.users entries from being added acc: user: target:
acc
users = foldlAttrs ( ++ (foldlAttrs (
acc: _: fid: shares: name: launcher:
mergeAttrsList ( let
# aid 0 is reserved pkg = if launcher.share != null then launcher.share else pkgs.${name};
imap1 (aid: app: { link = source: "[ -d '${source}' ] && ln -sv '${source}' $out/share || true";
"u${toString fid}_a${toString aid}" = app.extraConfig // { in
home.packages = app.packages; shares
}; ++
}) cfg.apps optional (launcher.method != "simple" && (launcher.capability.wayland || launcher.capability.x11))
(
pkgs.runCommand "${name}-share" { } ''
mkdir -p $out/share
${link "${pkg}/share/applications"}
${link "${pkg}/share/icons"}
${link "${pkg}/share/man"}
''
) )
// acc ) (wrap user target.launchers) target.launchers)
) privPackages cfg.users; ) [ cfg.package ] cfg.target;
}; };
security.polkit.extraConfig =
let
allowList = builtins.toJSON (mapAttrsToList (name: _: name) cfg.target);
in
''
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.machine1.host-shell" &&
${allowList}.indexOf(action.lookup("user")) > -1 &&
subject.user == "${cfg.user}") {
return polkit.Result.YES;
}
});
'';
}; };
} }

View File

@ -1,555 +0,0 @@
## environment\.fortify\.enable
Whether to enable fortify\.
*Type:*
boolean
*Default:*
` false `
*Example:*
` true `
## environment\.fortify\.package
The fortify package to use\.
*Type:*
package
*Default:*
` <derivation fortify-0.2.5> `
## environment\.fortify\.apps
Declarative fortify apps\.
*Type:*
list of (submodule)
*Default:*
` [ ] `
## environment\.fortify\.apps\.\*\.packages
List of extra packages to install via home-manager\.
*Type:*
list of package
*Default:*
` [ ] `
## environment\.fortify\.apps\.\*\.capability\.dbus
Whether to proxy D-Bus\.
*Type:*
boolean
*Default:*
` true `
## environment\.fortify\.apps\.\*\.capability\.pulse
Whether to share the PulseAudio socket and cookie\.
*Type:*
boolean
*Default:*
` true `
## environment\.fortify\.apps\.\*\.capability\.wayland
Whether to share the Wayland socket\.
*Type:*
boolean
*Default:*
` true `
## environment\.fortify\.apps\.\*\.capability\.x11
Whether to share the X11 socket and allow connection\.
*Type:*
boolean
*Default:*
` false `
## environment\.fortify\.apps\.\*\.command
Command to run as the target user\.
Setting this to null will default command to launcher name\.
Has no effect when script is set\.
*Type:*
null or string
*Default:*
` null `
## environment\.fortify\.apps\.\*\.dbus\.session
D-Bus session bus custom configuration\.
Setting this to null will enable built-in defaults\.
*Type:*
null or (function that evaluates to a(n) anything)
*Default:*
` null `
## environment\.fortify\.apps\.\*\.dbus\.system
D-Bus system bus custom configuration\.
Setting this to null will disable the system bus proxy\.
*Type:*
null or anything
*Default:*
` null `
## environment\.fortify\.apps\.\*\.dev
Whether to enable access to all devices within the sandbox\.
*Type:*
boolean
*Default:*
` false `
*Example:*
` true `
## environment\.fortify\.apps\.\*\.env
Environment variables to set for the initial process in the sandbox\.
*Type:*
null or (attribute set of string)
*Default:*
` null `
## environment\.fortify\.apps\.\*\.extraConfig
Extra home-manager configuration\.
*Type:*
anything
*Default:*
` { } `
## environment\.fortify\.apps\.\*\.extraPaths
Extra paths to make available to the sandbox\.
*Type:*
list of anything
*Default:*
` [ ] `
## environment\.fortify\.apps\.\*\.gpu
Target process GPU and driver access\.
Setting this to null will enable GPU whenever X or Wayland is enabled\.
*Type:*
null or boolean
*Default:*
` null `
## environment\.fortify\.apps\.\*\.groups
List of groups to inherit from the privileged user\.
*Type:*
list of string
*Default:*
` [ ] `
## environment\.fortify\.apps\.\*\.id
Freedesktop application ID\.
*Type:*
null or string
*Default:*
` null `
## environment\.fortify\.apps\.\*\.mapRealUid
Whether to enable mapping to fortifys real UID within the sandbox\.
*Type:*
boolean
*Default:*
` false `
*Example:*
` true `
## environment\.fortify\.apps\.\*\.name
Name of the apps launcher script\.
*Type:*
string
## environment\.fortify\.apps\.\*\.net
Whether to enable network access within the sandbox\.
*Type:*
boolean
*Default:*
` true `
*Example:*
` true `
## environment\.fortify\.apps\.\*\.nix
Whether to enable nix daemon access within the sandbox\.
*Type:*
boolean
*Default:*
` false `
*Example:*
` true `
## environment\.fortify\.apps\.\*\.script
Application launch script\.
*Type:*
null or string
*Default:*
` null `
## environment\.fortify\.apps\.\*\.share
Package containing share files\.
Setting this to null will default package name to wrapper name\.
*Type:*
null or package
*Default:*
` null `
## environment\.fortify\.apps\.\*\.tty
Whether to enable allow access to the controlling terminal\.
*Type:*
boolean
*Default:*
` false `
*Example:*
` true `
## environment\.fortify\.apps\.\*\.userns
Whether to enable userns within the sandbox\.
*Type:*
boolean
*Default:*
` false `
*Example:*
` true `
## environment\.fortify\.stateDir
The state directory where app home directories are stored\.
*Type:*
string
## environment\.fortify\.users
Users allowed to spawn fortify apps and their corresponding fortify fid\.
*Type:*
attribute set of integer between 0 and 99 (both inclusive)

View File

@ -1,214 +0,0 @@
{ lib, pkgs, ... }:
let
inherit (lib) types mkOption mkEnableOption;
in
{
options = {
environment.fortify = {
enable = mkEnableOption "fortify";
package = mkOption {
type = types.package;
default = pkgs.callPackage ./package.nix { };
description = "The fortify package to use.";
};
users = mkOption {
type =
let
inherit (types) attrsOf ints;
in
attrsOf (ints.between 0 99);
description = ''
Users allowed to spawn fortify apps and their corresponding fortify fid.
'';
};
apps = mkOption {
type =
let
inherit (types)
str
bool
package
anything
submodule
listOf
attrsOf
nullOr
functionTo
;
in
listOf (submodule {
options = {
name = mkOption {
type = str;
description = ''
Name of the app's launcher script.
'';
};
id = mkOption {
type = nullOr str;
default = null;
description = ''
Freedesktop application ID.
'';
};
packages = mkOption {
type = listOf package;
default = [ ];
description = ''
List of extra packages to install via home-manager.
'';
};
extraConfig = mkOption {
type = anything;
default = { };
description = ''
Extra home-manager configuration.
'';
};
script = mkOption {
type = nullOr str;
default = null;
description = ''
Application launch script.
'';
};
command = mkOption {
type = nullOr str;
default = null;
description = ''
Command to run as the target user.
Setting this to null will default command to launcher name.
Has no effect when script is set.
'';
};
groups = mkOption {
type = listOf str;
default = [ ];
description = ''
List of groups to inherit from the privileged user.
'';
};
dbus = {
session = mkOption {
type = nullOr (functionTo anything);
default = null;
description = ''
D-Bus session bus custom configuration.
Setting this to null will enable built-in defaults.
'';
};
system = mkOption {
type = nullOr anything;
default = null;
description = ''
D-Bus system bus custom configuration.
Setting this to null will disable the system bus proxy.
'';
};
};
env = mkOption {
type = nullOr (attrsOf str);
default = null;
description = ''
Environment variables to set for the initial process in the sandbox.
'';
};
nix = mkEnableOption "nix daemon access within the sandbox";
userns = mkEnableOption "userns within the sandbox";
mapRealUid = mkEnableOption "mapping to fortify's real UID within the sandbox";
dev = mkEnableOption "access to all devices within the sandbox";
tty = mkEnableOption "allow access to the controlling terminal";
net = mkEnableOption "network access within the sandbox" // {
default = true;
};
gpu = mkOption {
type = nullOr bool;
default = null;
description = ''
Target process GPU and driver access.
Setting this to null will enable GPU whenever X or Wayland is enabled.
'';
};
extraPaths = mkOption {
type = listOf anything;
default = [ ];
description = ''
Extra paths to make available to the sandbox.
'';
};
capability = {
wayland = mkOption {
type = bool;
default = true;
description = ''
Whether to share the Wayland socket.
'';
};
x11 = mkOption {
type = bool;
default = false;
description = ''
Whether to share the X11 socket and allow connection.
'';
};
dbus = mkOption {
type = bool;
default = true;
description = ''
Whether to proxy D-Bus.
'';
};
pulse = mkOption {
type = bool;
default = true;
description = ''
Whether to share the PulseAudio socket and cookie.
'';
};
};
share = mkOption {
type = nullOr package;
default = null;
description = ''
Package containing share files.
Setting this to null will default package name to wrapper name.
'';
};
};
});
default = [ ];
description = "Declarative fortify apps.";
};
stateDir = mkOption {
type = types.str;
description = ''
The state directory where app home directories are stored.
'';
};
};
};
}

View File

@ -4,17 +4,13 @@
makeBinaryWrapper, makeBinaryWrapper,
xdg-dbus-proxy, xdg-dbus-proxy,
bubblewrap, bubblewrap,
pkg-config,
acl, acl,
wayland,
wayland-scanner,
wayland-protocols,
xorg, xorg,
}: }:
buildGoModule rec { buildGoModule rec {
pname = "fortify"; pname = "fortify";
version = "0.2.5"; version = "0.0.11";
src = ./.; src = ./.;
vendorHash = null; vendorHash = null;
@ -26,55 +22,39 @@ buildGoModule rec {
ldflags ldflags
++ [ ++ [
"-X" "-X"
"git.gensokyo.uk/security/fortify/internal.${name}=${value}" "git.ophivana.moe/security/fortify/internal.${name}=${value}"
] ]
) )
[ [
"-s" "-s"
"-w" "-w"
"-X" "-X"
"main.Fmain=${placeholder "out"}/libexec/fortify" "main.Fmain=${placeholder "out"}/bin/.fortify-wrapped"
"-X"
"main.Fshim=${placeholder "out"}/libexec/fshim"
] ]
{ {
Version = "v${version}"; Version = "v${version}";
Fsu = "/run/wrappers/bin/fsu"; Fsu = "/run/wrappers/bin/fsu";
Finit = "${placeholder "out"}/libexec/finit"; Fshim = "${placeholder "out"}/bin/.fshim";
Finit = "${placeholder "out"}/bin/.finit";
}; };
# nix build environment does not allow acls
GO_TEST_SKIP_ACL = 1;
buildInputs = [ buildInputs = [
acl acl
wayland
wayland-protocols
xorg.libxcb xorg.libxcb
]; ];
nativeBuildInputs = [ nativeBuildInputs = [ makeBinaryWrapper ];
pkg-config
wayland-scanner
makeBinaryWrapper
];
preConfigure = ''
HOME=$(mktemp -d) go generate ./...
'';
postInstall = '' postInstall = ''
install -D --target-directory=$out/share/zsh/site-functions comp/* wrapProgram $out/bin/${pname} --prefix PATH : ${
mkdir "$out/libexec"
mv "$out"/bin/* "$out/libexec/"
makeBinaryWrapper "$out/libexec/fortify" "$out/bin/fortify" \
--inherit-argv0 --prefix PATH : ${
lib.makeBinPath [ lib.makeBinPath [
bubblewrap bubblewrap
xdg-dbus-proxy xdg-dbus-proxy
] ]
} }
mv $out/bin/fsu $out/bin/.fsu
mv $out/bin/fshim $out/bin/.fshim
mv $out/bin/finit $out/bin/.finit
''; '';
} }

35
state.go Normal file
View File

@ -0,0 +1,35 @@
package main
import (
"flag"
"fmt"
"text/tabwriter"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/state"
)
var (
stateActionEarly bool
)
func init() {
flag.BoolVar(&stateActionEarly, "state", false, "print state information of active launchers")
}
// tryState is called after app initialisation
func tryState() {
if stateActionEarly {
var w *tabwriter.Writer
state.MustPrintLauncherStateSimpleGlobal(&w, os.Paths().RunDirPath)
if w != nil {
if err := w.Flush(); err != nil {
fmsg.Println("cannot format output:", err)
}
} else {
fmt.Println("No information available")
}
fmsg.Exit(0)
}
}

221
test.nix
View File

@ -1,221 +0,0 @@
{
system,
self,
home-manager,
nixosTest,
}:
nixosTest {
name = "fortify";
# adapted from nixos sway integration tests
# testScriptWithTypes:49: error: Cannot call function of unknown type
# (machine.succeed if succeed else machine.execute)(
# ^
# Found 1 error in 1 file (checked 1 source file)
skipTypeCheck = true;
nodes.machine =
{ lib, pkgs, ... }:
{
users.users.alice = {
isNormalUser = true;
description = "Alice Foobar";
password = "foobar";
uid = 1000;
};
home-manager.users.alice.home.stateVersion = "24.11";
# Automatically login on tty1 as a normal user:
services.getty.autologinUser = "alice";
environment = {
systemPackages = with pkgs; [
# For glinfo and wayland-info:
mesa-demos
wayland-utils
alacritty
# For go tests:
self.devShells.${system}.fhs
];
variables = {
SWAYSOCK = "/tmp/sway-ipc.sock";
WLR_RENDERER = "pixman";
};
# To help with OCR:
etc."xdg/foot/foot.ini".text = lib.generators.toINI { } {
main = {
font = "inconsolata:size=14";
};
colors = rec {
foreground = "000000";
background = "ffffff";
regular2 = foreground;
};
};
};
fonts.packages = [ pkgs.inconsolata ];
# Automatically configure and start Sway when logging in on tty1:
programs.bash.loginShellInit = ''
if [ "$(tty)" = "/dev/tty1" ]; then
set -e
mkdir -p ~/.config/sway
sed s/Mod4/Mod1/ /etc/sway/config > ~/.config/sway/config
sway --validate
sway && touch /tmp/sway-exit-ok
fi
'';
programs.sway.enable = true;
virtualisation.qemu.options = [
# Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch:
"-vga none -device virtio-gpu-pci"
# Increase Go test compiler performance:
"-smp 8"
];
environment.fortify = {
enable = true;
stateDir = "/var/lib/fortify";
users.alice = 0;
};
imports = [
self.nixosModules.fortify
home-manager.nixosModules.home-manager
];
};
testScript = ''
import shlex
import json
q = shlex.quote
NODE_GROUPS = ["nodes", "floating_nodes"]
def swaymsg(command: str = "", succeed=True, type="command"):
assert command != "" or type != "command", "Must specify command or type"
shell = q(f"swaymsg -t {q(type)} -- {q(command)}")
with machine.nested(
f"sending swaymsg {shell!r}" + " (allowed to fail)" * (not succeed)
):
ret = (machine.succeed if succeed else machine.execute)(
f"su - alice -c {shell}"
)
# execute also returns a status code, but disregard.
if not succeed:
_, ret = ret
if not succeed and not ret:
return None
parsed = json.loads(ret)
return parsed
def walk(tree):
yield tree
for group in NODE_GROUPS:
for node in tree.get(group, []):
yield from walk(node)
def wait_for_window(pattern):
def func(last_chance):
nodes = (node["name"] for node in walk(swaymsg(type="get_tree")))
if last_chance:
nodes = list(nodes)
machine.log(f"Last call! Current list of windows: {nodes}")
return any(pattern in name for name in nodes)
retry(func)
def collect_state_ui(name):
swaymsg(f"exec fortify ps > '/tmp/{name}.ps'")
machine.copy_from_vm(f"/tmp/{name}.ps", "")
machine.screenshot(name)
start_all()
machine.wait_for_unit("multi-user.target")
# Run fortify Go tests outside of nix build:
machine.succeed("rm -rf /tmp/src && cp -a '${self.packages.${system}.fortify.src}' /tmp/src")
print(machine.succeed("fortify-fhs -c '(cd /tmp/src && go generate ./... && go test ./...)'"))
# To check sway's version:
print(machine.succeed("sway --version"))
# Wait for Sway to complete startup:
machine.wait_for_file("/run/user/1000/wayland-1")
machine.wait_for_file("/tmp/sway-ipc.sock")
# Create fortify aid 0 home directory:
machine.succeed("install -dm 0700 -o 1000000 -g 1000000 /var/lib/fortify/u0/a0")
# Start fortify outside Wayland session:
print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare"))
machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-bare")
# Start fortify within Wayland session:
swaymsg("exec fortify -v run --wayland touch /tmp/success-session")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-session")
# Start a terminal (foot) within fortify on workspace 3:
machine.send_key("alt-3")
machine.sleep(3)
swaymsg("exec fortify run --wayland foot")
wait_for_window("u0_a0@machine")
machine.send_chars("wayland-info && touch /tmp/success-client\n")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-client")
collect_state_ui("foot_wayland_permissive")
# Verify acl on XDG_RUNTIME_DIR:
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000000"))
machine.send_chars("exit\n")
machine.wait_until_fails("pgrep foot")
# Verify acl cleanup on XDG_RUNTIME_DIR:
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000000")
# Start a terminal (foot) within fortify from a terminal on workspace 4:
machine.send_key("alt-4")
machine.sleep(3)
swaymsg("exec foot fortify run --wayland foot")
wait_for_window("u0_a0@machine")
machine.send_chars("wayland-info && touch /tmp/success-client-term\n")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-client-term")
collect_state_ui("foot_wayland_permissive_term")
machine.send_chars("exit\n")
machine.wait_until_fails("pgrep foot")
# Test XWayland (foot does not support X):
swaymsg("exec fortify run -X alacritty")
wait_for_window("u0_a0@machine")
machine.send_chars("glinfo && touch /tmp/success-client-x11\n")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-client-x11")
collect_state_ui("alacritty_x11_permissive")
machine.send_chars("exit\n")
machine.wait_until_fails("pgrep alacritty")
# Exit Sway and verify process exit status 0:
swaymsg("exit", succeed=False)
machine.wait_until_fails("pgrep -x sway")
machine.wait_for_file("/tmp/sway-exit-ok")
# Print fortify runDir contents:
print(machine.succeed("find /run/user/1000/fortify"))
'';
}

27
version.go Normal file
View File

@ -0,0 +1,27 @@
package main
import (
"flag"
"fmt"
"git.ophivana.moe/security/fortify/internal"
)
var (
printVersion bool
)
func init() {
flag.BoolVar(&printVersion, "V", false, "Print version")
}
func tryVersion() {
if printVersion {
if v, ok := internal.Check(internal.Version); ok {
fmt.Println(v)
} else {
fmt.Println("impure")
}
os.Exit(0)
}
}

111
wl/c.go
View File

@ -1,111 +0,0 @@
package wl
//go:generate sh -c "wayland-scanner client-header `pkg-config --variable=datarootdir wayland-protocols`/wayland-protocols/staging/security-context/security-context-v1.xml security-context-v1-protocol.h"
//go:generate sh -c "wayland-scanner private-code `pkg-config --variable=datarootdir wayland-protocols`/wayland-protocols/staging/security-context/security-context-v1.xml security-context-v1-protocol.c"
/*
#cgo linux pkg-config: wayland-client
#cgo freebsd openbsd LDFLAGS: -lwayland-client
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <wayland-client.h>
#include "security-context-v1-protocol.h"
static void registry_handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) {
struct wp_security_context_manager_v1 **out = data;
if (strcmp(interface, wp_security_context_manager_v1_interface.name) == 0)
*out = wl_registry_bind(registry, name, &wp_security_context_manager_v1_interface, 1);
}
static void registry_handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) { } // no-op
static const struct wl_registry_listener registry_listener = {
.global = registry_handle_global,
.global_remove = registry_handle_global_remove,
};
static int32_t bind_wayland_fd(char *socket_path, int fd, const char *app_id, const char *instance_id, int sync_fd) {
int32_t res = 0; // refer to resErr for meaning
struct wl_display *display;
display = wl_display_connect_to_fd(fd);
if (!display) {
res = 1;
goto out;
};
struct wl_registry *registry;
registry = wl_display_get_registry(display);
struct wp_security_context_manager_v1 *security_context_manager = NULL;
wl_registry_add_listener(registry, &registry_listener, &security_context_manager);
int ret;
ret = wl_display_roundtrip(display);
wl_registry_destroy(registry);
if (ret < 0)
goto out;
if (!security_context_manager) {
res = 2;
goto out;
}
int listen_fd = -1;
listen_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (listen_fd < 0)
goto out;
struct sockaddr_un sockaddr = {0};
sockaddr.sun_family = AF_UNIX;
snprintf(sockaddr.sun_path, sizeof(sockaddr.sun_path), "%s", socket_path);
if (bind(listen_fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) != 0)
goto out;
if (listen(listen_fd, 0) != 0)
goto out;
struct wp_security_context_v1 *security_context;
security_context = wp_security_context_manager_v1_create_listener(security_context_manager, listen_fd, sync_fd);
wp_security_context_v1_set_sandbox_engine(security_context, "moe.ophivana.fortify");
wp_security_context_v1_set_app_id(security_context, app_id);
wp_security_context_v1_set_instance_id(security_context, instance_id);
wp_security_context_v1_commit(security_context);
wp_security_context_v1_destroy(security_context);
if (wl_display_roundtrip(display) < 0)
goto out;
out:
if (listen_fd >= 0)
close(listen_fd);
if (security_context_manager)
wp_security_context_manager_v1_destroy(security_context_manager);
if (display)
wl_display_disconnect(display);
free((void *)socket_path);
free((void *)app_id);
free((void *)instance_id);
return res;
}
*/
import "C"
import "errors"
var resErr = [...]error{
0: nil,
1: errors.New("wl_display_connect_to_fd() failed"),
2: errors.New("wp_security_context_v1 not available"),
}
func bindWaylandFd(socketPath string, fd uintptr, appID, instanceID string, syncFD uintptr) error {
res := C.bind_wayland_fd(C.CString(socketPath), C.int(fd), C.CString(appID), C.CString(instanceID), C.int(syncFD))
return resErr[int32(res)]
}

Some files were not shown because too many files have changed in this diff Show More