Compare commits

..

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

100 changed files with 2234 additions and 4265 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.Finit=/usr/libexec/fortify/finit
-X main.Fmain=/usr/bin/fortify
-X main.Fshim=/usr/libexec/fortify/fshim'
-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.Finit=/usr/libexec/fortify/finit
- name: Upload test build -X main.Fmain=/usr/bin/fortify
uses: actions/upload-artifact@v3 -X main.Fshim=/usr/libexec/fortify/fshim'
with: -o bin/ ./... &&
name: "fortify-${{ steps.build-test.outputs.rev }}" (cd bin && sha512sum --tag -b * > sha512sums)
path: dist/fortify-*
retention-days: 1

8
.gitignore vendored
View File

@ -22,10 +22,4 @@ go.work.sum
# env file # env file
.env .env
.idea .idea
.vscode .vscode
# go generate
security-context-v1-protocol.*
# release
/dist/fortify-*

View File

@ -1,8 +1,8 @@
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) [![Go Report Card](https://goreportcard.com/badge/git.ophivana.moe/security/fortify)](https://goreportcard.com/report/git.ophivana.moe/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,10 +15,19 @@ 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
@ -35,7 +44,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";
@ -88,6 +97,7 @@ This adds the `environment.fortify` option:
f: f:
f { f {
talk = [ talk = [
"org.freedesktop.DBus"
"org.freedesktop.FileManager1" "org.freedesktop.FileManager1"
"org.freedesktop.Notifications" "org.freedesktop.Notifications"
"org.freedesktop.ScreenSaver" "org.freedesktop.ScreenSaver"

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 fmsg.Fatal("FORTIFY_INIT not set")
) panic("unreachable")
if f, err := proc.Receive(init0.Env, &payload); err != nil { } else {
if errors.Is(err, proc.ErrInvalid) { if fd, err := strconv.Atoi(s); err != nil {
fmsg.Fatal("invalid config descriptor") fmsg.Fatalf("cannot parse %q: %v", s, err)
} panic("unreachable")
if errors.Is(err, proc.ErrNotSet) { } else {
fmsg.Fatal("FORTIFY_INIT not set") 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.Suspend()
// 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,11 +18,25 @@ type Payload struct {
Exec [2]string Exec [2]string
// bwrap config // bwrap config
Bwrap *bwrap.Config Bwrap *bwrap.Config
// path to outer home directory // whether to pass wayland fd
Home string WL bool
// sync fd
Sync *uintptr
// 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,21 @@
package shim package shim
import ( import (
"encoding/gob"
"errors" "errors"
"net"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"strconv"
"strings" "strings"
"sync"
"sync/atomic"
"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"
"git.gensokyo.uk/security/fortify/internal/proc" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
const shimSetupTimeout = 5 * time.Second const shimSetupTimeout = 5 * time.Second
@ -30,14 +31,22 @@ type Shim struct {
aid string aid string
// string representation of supplementary group ids // string representation of supplementary group ids
supp []string supp []string
// path to setup socket
socket string
// shim setup abort reason and completion
abort chan error
abortErr atomic.Pointer[error]
abortOnce sync.Once
// fallback exit notifier with error returned killing the process // fallback exit notifier with error returned killing the process
killFallback chan error killFallback chan error
// 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(uid uint32, aid string, supp []string, socket string, wl *shim0.Wayland, payload *shim0.Payload) *Shim {
return &Shim{uid: uid, aid: aid, supp: supp, payload: payload} return &Shim{uid: uid, aid: aid, supp: supp, socket: socket, wl: wl, payload: payload}
} }
func (s *Shim) String() string { func (s *Shim) String() string {
@ -51,11 +60,39 @@ func (s *Shim) Unwrap() *exec.Cmd {
return s.cmd return s.cmd
} }
func (s *Shim) Abort(err error) {
s.abortOnce.Do(func() {
s.abortErr.Store(&err)
// s.abort is buffered so this will never block
s.abort <- err
})
}
func (s *Shim) AbortWait(err error) {
s.Abort(err)
<-s.abort
}
func (s *Shim) WaitFallback() chan error { func (s *Shim) WaitFallback() chan error {
return s.killFallback return s.killFallback
} }
func (s *Shim) Start() (*time.Time, error) { func (s *Shim) Start() (*time.Time, error) {
var (
cf chan *net.UnixConn
accept func()
)
// listen on setup socket
if c, a, err := s.serve(); err != nil {
return nil, fmsg.WrapErrorSuffix(err,
"cannot listen on shim setup socket:")
} else {
// accepts a connection after each call to accept
// connections are sent to the channel cf
cf, accept = c, a
}
// start user switcher process and save time // start user switcher process and save time
var fsu string var fsu string
if p, ok := internal.Check(internal.Fsu); !ok { if p, ok := internal.Check(internal.Fsu); !ok {
@ -65,32 +102,16 @@ func (s *Shim) Start() (*time.Time, error) {
fsu = p fsu = p
} }
s.cmd = exec.Command(fsu) s.cmd = exec.Command(fsu)
s.cmd.Env = []string{
var encoder *gob.Encoder shim0.Env + "=" + s.socket,
if fd, e, err := proc.Setup(&s.cmd.ExtraFiles); err != nil { "FORTIFY_APP_ID=" + s.aid,
return nil, fmsg.WrapErrorSuffix(err,
"cannot create shim setup pipe:")
} else {
encoder = e
s.cmd.Env = []string{
shim0.Env + "=" + strconv.Itoa(fd),
"FORTIFY_APP_ID=" + s.aid,
}
} }
if len(s.supp) > 0 { if len(s.supp) > 0 {
fmsg.VPrintf("attaching supplementary group ids %s", s.supp) fmsg.VPrintf("attaching supplementary group ids %s", s.supp)
s.cmd.Env = append(s.cmd.Env, "FORTIFY_GROUPS="+strings.Join(s.supp, " ")) s.cmd.Env = append(s.cmd.Env, "FORTIFY_GROUPS="+strings.Join(s.supp, " "))
} }
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 = "/"
// pass sync fd if set
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.VPrintln("starting shim via fsu:", s.cmd)
fmsg.Suspend() // withhold messages to stderr fmsg.Suspend() // withhold messages to stderr
if err := s.cmd.Start(); err != nil { if err := s.cmd.Start(); err != nil {
@ -118,20 +139,117 @@ func (s *Shim) Start() (*time.Time, error) {
signal.Ignore(syscall.SIGINT, syscall.SIGTERM) signal.Ignore(syscall.SIGINT, syscall.SIGTERM)
}() }()
shimErr := make(chan error) accept()
go func() { shimErr <- encoder.Encode(s.payload) }() var conn *net.UnixConn
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 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,19 +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/fst" "git.ophivana.moe/security/fortify/helper"
"git.gensokyo.uk/security/fortify/helper" "git.ophivana.moe/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.gensokyo.uk/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
@ -38,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 {
@ -46,56 +55,43 @@ 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(err.Error())
)
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.Fatalf("cannot decode shim payload: %v", 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.Fatalf("cannot receive wayland fd: %v", 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
} }
// ensure home directory as target user
if s, err := os.Stat(payload.Home); err != nil {
if os.IsNotExist(err) {
if err = os.Mkdir(payload.Home, 0700); err != nil {
fmsg.Fatalf("cannot create home directory: %v", err)
}
} else {
fmsg.Fatalf("cannot access home directory: %v", err)
}
// home directory is created, proceed
} else if !s.IsDir() {
fmsg.Fatalf("data path %q is not a directory", payload.Home)
}
var ic init0.Payload var ic init0.Payload
// resolve argv0 // resolve argv0
@ -120,25 +116,34 @@ 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 {
if f := os.NewFile(uintptr(wfd), "wayland"); f != nil {
ic.WL = 3 + len(extraFiles)
extraFiles = append(extraFiles, f)
}
} else {
ic.WL = -1
}
// share config pipe
if r, w, err := os.Pipe(); err != nil {
fmsg.Fatalf("cannot pipe: %v", err) fmsg.Fatalf("cannot pipe: %v", err)
} else { } else {
conf.SetEnv[init0.Env] = strconv.Itoa(fd) conf.SetEnv[init0.Env] = strconv.Itoa(3 + len(extraFiles))
extraFiles = append(extraFiles, r)
fmsg.VPrintln("transmitting config to init")
go func() { go func() {
fmsg.VPrintln("transmitting config to init") // stream config to pipe
if err = encoder.Encode(&ic); err != nil { if err = gob.NewEncoder(w).Encode(&ic); err != nil {
fmsg.Fatalf("cannot transmit init config: %v", err) fmsg.Fatalf("cannot transmit init config: %v", err)
} }
}() }()
} }
// bind finit inside sandbox
finitInnerPath := path.Join(fst.Tmp, "sbin", "init")
conf.Bind(finitPath, finitInnerPath)
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, finitInnerPath, 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.Fatalf("malformed sandbox config: %v", err)
} else { } else {
@ -163,3 +168,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

@ -83,17 +83,17 @@ func main() {
uid += aid uid += aid
} }
// pass through setup fd to shim // pass through setup path to shim
var shimSetupFd string var shimSetupPath string
if s, ok := os.LookupEnv(envShim); !ok { if s, ok := os.LookupEnv(envShim); !ok {
// fortify requests target uid // fortify requests target uid
// print resolved uid and exit // print resolved uid and exit
fmt.Print(uid) fmt.Print(uid)
os.Exit(0) os.Exit(0)
} else if len(s) != 1 || s[0] > '9' || s[0] < '3' { } else if !path.IsAbs(s) {
log.Fatal("FORTIFY_SHIM holds an invalid value") log.Fatal("FORTIFY_SHIM is not absolute")
} else { } else {
shimSetupFd = s shimSetupPath = s
} }
// supplementary groups // supplementary groups
@ -123,11 +123,6 @@ func main() {
suppGroups = []int{uid} 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 // 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 {
@ -142,7 +137,7 @@ func main() {
if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 { if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error()) log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
} }
if err := syscall.Exec(fshim, []string{"fshim"}, []string{envShim + "=" + shimSetupFd}); err != nil { if err := syscall.Exec(fshim, []string{"fshim"}, []string{envShim + "=" + shimSetupPath}); err != nil {
log.Fatalf("cannot start shim: %v", err) log.Fatalf("cannot start shim: %v", err)
} }

View File

@ -9,7 +9,7 @@ import (
"path" "path"
"strconv" "strconv"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
func main() { func main() {
@ -54,7 +54,7 @@ func main() {
realName := fmt.Sprintf("Fortify subordinate user %d (%s)", aid, u.name) realName := fmt.Sprintf("Fortify subordinate user %d (%s)", aid, u.name)
var homeDirectory string var homeDirectory string
if *homeDir != varEmpty { if *homeDir != varEmpty {
homeDirectory = path.Join(*homeDir, "u"+fidString, "a"+strconv.Itoa(aid)) homeDirectory = path.Join(*homeDir, fidString, strconv.Itoa(aid))
} else { } else {
homeDirectory = varEmpty homeDirectory = varEmpty
} }

View File

@ -5,7 +5,7 @@ import (
"os" "os"
"path" "path"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
type payloadU struct { type payloadU struct {

View File

@ -1,82 +0,0 @@
#compdef fortify
_fortify_app() {
__fortify_files
return $?
}
_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]'
}
_fortify_ps() {
_arguments \
'--short[Print instance id]'
}
_fortify_show() {
_alternative \
'instances:domains:__fortify_instances' \
'files:files:__fortify_files'
}
__fortify_files() {
_files -g "*.(json|ftfy)"
return $?
}
__fortify_instances() {
local -a out
shift -p
out=( ${(f)"$(_call_program commands fortify ps --short 2>&1)"} )
if (( $#out == 0 )); then
_message "No active instances"
else
_describe "active instances" out
fi
return $?
}
(( $+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"
"show:Show the contents of an app configuration"
"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 'action' _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]' \
'--json[Format output in JSON when applicable]' \
'*::fortify command:_fortify_commands'

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) {
@ -124,8 +124,6 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
t.Run("proxy for "+id, func(t *testing.T) { t.Run("proxy for "+id, func(t *testing.T) {
helper.InternalReplaceExecCommand(t) helper.InternalReplaceExecCommand(t)
overridePath(t)
p := dbus.New(tc[0].bus, tc[1].bus) p := dbus.New(tc[0].bus, tc[1].bus)
output := new(strings.Builder) output := new(strings.Builder)
@ -176,7 +174,7 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
t.Run("sealed start of "+id, func(t *testing.T) { t.Run("sealed start of "+id, func(t *testing.T) {
if err := p.Start(nil, output, sandbox); err != nil { if err := p.Start(nil, output, sandbox); err != nil {
t.Fatalf("Start(nil, nil) error = %v", t.Errorf("Start(nil, nil) error = %v",
err) err)
} }
@ -215,11 +213,3 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
}) })
} }
} }
func overridePath(t *testing.T) {
proxyName := dbus.ProxyName
dbus.ProxyName = "/nonexistent-xdg-dbus-proxy"
t.Cleanup(func() {
dbus.ProxyName = proxyName
})
}

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.
@ -46,16 +46,14 @@ func (p *Proxy) Start(ready chan error, output io.Writer, sandbox bool) error {
// look up absolute path if name is just a file name // look up absolute path if name is just a file name
toolPath := p.name toolPath := p.name
if filepath.Base(p.name) == p.name { if filepath.Base(p.name) == p.name {
if s, err := exec.LookPath(p.name); err != nil { if s, err := exec.LookPath(p.name); err == nil {
return err
} else {
toolPath = s toolPath = s
} }
} }
// resolve libraries by parsing ldd output // resolve libraries by parsing ldd output
var proxyDeps []*ldd.Entry var proxyDeps []*ldd.Entry
if toolPath != "/nonexistent-xdg-dbus-proxy" { if path.IsAbs(toolPath) {
if l, err := ldd.Exec(toolPath); err != nil { if l, err := ldd.Exec(toolPath); err != nil {
return err return err
} else { } else {
@ -93,9 +91,6 @@ func (p *Proxy) Start(ready chan error, output io.Writer, sandbox bool) error {
if path.IsAbs(ent.Path) { if path.IsAbs(ent.Path) {
roBindTarget[path.Dir(ent.Path)] = struct{}{} roBindTarget[path.Dir(ent.Path)] = struct{}{}
} }
if path.IsAbs(ent.Name) {
roBindTarget[path.Dir(ent.Name)] = struct{}{}
}
} }
// resolve upstream bus directories // resolve upstream bus directories

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

14
dist/install.sh vendored
View File

@ -1,14 +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"
if [ ! -f "${FORTIFY_INSTALL_PREFIX}/etc/fsurc" ]; then
install -vDm0400 "fsurc.default" "${FORTIFY_INSTALL_PREFIX}/etc/fsurc"
fi
install -vDm0644 "comp/_fortify" "${FORTIFY_INSTALL_PREFIX}/usr/share/zsh/site-functions/_fortify"

20
dist/release.sh vendored
View File

@ -1,20 +0,0 @@
#!/bin/sh -e
cd "$(dirname -- "$0")/.."
VERSION="${FORTIFY_VERSION:-untagged}"
pname="fortify-${VERSION}"
out="dist/${pname}"
mkdir -p "${out}"
cp -v "README.md" "dist/fsurc.default" "dist/install.sh" "${out}"
cp -rv "comp" "${out}"
go build -trimpath -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}"
(cd dist && sha512sum "${pname}.tar.gz" > "${pname}.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"
} }
} }

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,26 +37,6 @@
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};
@ -134,7 +56,7 @@
}; };
modules = [ ./options.nix ]; modules = [ ./options.nix ];
}; };
cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval; cleanEval = lib.filterAttrsRecursive (n: v: n != "_module") eval;
in in
pkgs.nixosOptionsDoc { inherit (cleanEval) options; }; pkgs.nixosOptionsDoc { inherit (cleanEval) options; };
docText = pkgs.runCommand "fortify-module-docs.md" { } '' docText = pkgs.runCommand "fortify-module-docs.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

@ -106,7 +106,7 @@ func (c *Config) Mqueue(dest string) *Config {
// Dir create dir in sandbox // Dir create dir in sandbox
// (--dir DEST) // (--dir DEST)
func (c *Config) Dir(dest string) *Config { func (c *Config) Dir(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{awkwardArgs[Dir], dest}) c.Filesystem = append(c.Filesystem, &stringF{stringArgs[Dir], dest})
return c return c
} }
@ -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
} }
@ -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,287 @@
package app_test package app_test
import ( import (
"git.gensokyo.uk/security/fortify/acl" "fmt"
"git.gensokyo.uk/security/fortify/dbus" "io"
"git.gensokyo.uk/security/fortify/fst" "io/fs"
"git.gensokyo.uk/security/fortify/helper/bwrap" "os/user"
"git.gensokyo.uk/security/fortify/internal/system" "strconv"
"git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal/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{
Command: make([]string, 0),
Confinement: app.ConfinementConfig{
AppID: 0,
Username: "chronos",
Outer: "/home/chronos",
},
},
app.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),
&app.Config{
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Command: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"}, Command: []string{"/run/current-system/sw/bin/zsh", "-c", "exec chromium "},
Confinement: fst.ConfinementConfig{ Confinement: app.ConfinementConfig{
AppID: 1, Groups: []string{}, Username: "u0_a1", AppID: 9,
Outer: "/var/lib/persist/module/fortify/0/1", Groups: []string{"video"},
Sandbox: &fst.SandboxConfig{ Username: "chronos",
UserNS: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, Outer: "/home/chronos",
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(),
}, },
}, },
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(1000009).
Ensure("/tmp/fortify.1971", 0711). Ensure("/tmp/fortify.1971", 0711).
Ephemeral(system.Process, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1", 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", 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/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/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,136 +289,313 @@ 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", Chdir: "/home/chronos",
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": fst.Tmp + "/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").
Tmpfs(fst.Tmp, 4096). Tmpfs("/fortify", 4096).
DevTmpfs("/dev").Mqueue("/dev/mqueue"). DevTmpfs("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin"). Bind("/bin", "/bin", false, true).
Bind("/usr/bin", "/usr/bin"). Bind("/boot", "/boot", false, true).
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", fst.Tmp+"/etc"). Bind("/etc", "/fortify/etc").
Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa"). Symlink("/fortify/etc/alsa", "/etc/alsa").
Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc"). Symlink("/fortify/etc/bashrc", "/etc/bashrc").
Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d"). Symlink("/fortify/etc/binfmt.d", "/etc/binfmt.d").
Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1"). Symlink("/fortify/etc/dbus-1", "/etc/dbus-1").
Symlink(fst.Tmp+"/etc/default", "/etc/default"). Symlink("/fortify/etc/default", "/etc/default").
Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes"). Symlink("/fortify/etc/ethertypes", "/etc/ethertypes").
Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts"). Symlink("/fortify/etc/fonts", "/etc/fonts").
Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab"). Symlink("/fortify/etc/fstab", "/etc/fstab").
Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf"). Symlink("/fortify/etc/fuse.conf", "/etc/fuse.conf").
Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf"). Symlink("/fortify/etc/host.conf", "/etc/host.conf").
Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid"). Symlink("/fortify/etc/hostid", "/etc/hostid").
Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname"). Symlink("/fortify/etc/hostname", "/etc/hostname").
Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM"). Symlink("/fortify/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts"). Symlink("/fortify/etc/hosts", "/etc/hosts").
Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc"). Symlink("/fortify/etc/inputrc", "/etc/inputrc").
Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d"). Symlink("/fortify/etc/ipsec.d", "/etc/ipsec.d").
Symlink(fst.Tmp+"/etc/issue", "/etc/issue"). Symlink("/fortify/etc/issue", "/etc/issue").
Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd"). Symlink("/fortify/etc/kbd", "/etc/kbd").
Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev"). Symlink("/fortify/etc/libblockdev", "/etc/libblockdev").
Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf"). Symlink("/fortify/etc/locale.conf", "/etc/locale.conf").
Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime"). Symlink("/fortify/etc/localtime", "/etc/localtime").
Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs"). Symlink("/fortify/etc/login.defs", "/etc/login.defs").
Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release"). Symlink("/fortify/etc/lsb-release", "/etc/lsb-release").
Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm"). Symlink("/fortify/etc/lvm", "/etc/lvm").
Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id"). Symlink("/fortify/etc/machine-id", "/etc/machine-id").
Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf"). Symlink("/fortify/etc/man_db.conf", "/etc/man_db.conf").
Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d"). Symlink("/fortify/etc/modprobe.d", "/etc/modprobe.d").
Symlink(fst.Tmp+"/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(fst.Tmp+"/etc/nanorc", "/etc/nanorc"). Symlink("/fortify/etc/nanorc", "/etc/nanorc").
Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup"). Symlink("/fortify/etc/netgroup", "/etc/netgroup").
Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager"). Symlink("/fortify/etc/NetworkManager", "/etc/NetworkManager").
Symlink(fst.Tmp+"/etc/nix", "/etc/nix"). Symlink("/fortify/etc/nix", "/etc/nix").
Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos"). Symlink("/fortify/etc/nixos", "/etc/nixos").
Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS"). Symlink("/fortify/etc/NIXOS", "/etc/NIXOS").
Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf"). Symlink("/fortify/etc/nscd.conf", "/etc/nscd.conf").
Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf"). Symlink("/fortify/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd"). Symlink("/fortify/etc/opensnitchd", "/etc/opensnitchd").
Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release"). Symlink("/fortify/etc/os-release", "/etc/os-release").
Symlink(fst.Tmp+"/etc/pam", "/etc/pam"). Symlink("/fortify/etc/pam", "/etc/pam").
Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d"). Symlink("/fortify/etc/pam.d", "/etc/pam.d").
Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire"). Symlink("/fortify/etc/pipewire", "/etc/pipewire").
Symlink(fst.Tmp+"/etc/pki", "/etc/pki"). Symlink("/fortify/etc/pki", "/etc/pki").
Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1"). Symlink("/fortify/etc/polkit-1", "/etc/polkit-1").
Symlink(fst.Tmp+"/etc/profile", "/etc/profile"). Symlink("/fortify/etc/profile", "/etc/profile").
Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols"). Symlink("/fortify/etc/protocols", "/etc/protocols").
Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu"). Symlink("/fortify/etc/qemu", "/etc/qemu").
Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf"). Symlink("/fortify/etc/resolv.conf", "/etc/resolv.conf").
Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf"). Symlink("/fortify/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc"). Symlink("/fortify/etc/rpc", "/etc/rpc").
Symlink(fst.Tmp+"/etc/samba", "/etc/samba"). Symlink("/fortify/etc/samba", "/etc/samba").
Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf"). Symlink("/fortify/etc/sddm.conf", "/etc/sddm.conf").
Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot"). Symlink("/fortify/etc/secureboot", "/etc/secureboot").
Symlink(fst.Tmp+"/etc/services", "/etc/services"). Symlink("/fortify/etc/services", "/etc/services").
Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment"). Symlink("/fortify/etc/set-environment", "/etc/set-environment").
Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow"). Symlink("/fortify/etc/shadow", "/etc/shadow").
Symlink(fst.Tmp+"/etc/shells", "/etc/shells"). Symlink("/fortify/etc/shells", "/etc/shells").
Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh"). Symlink("/fortify/etc/ssh", "/etc/ssh").
Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl"). Symlink("/fortify/etc/ssl", "/etc/ssl").
Symlink(fst.Tmp+"/etc/static", "/etc/static"). Symlink("/fortify/etc/static", "/etc/static").
Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid"). Symlink("/fortify/etc/subgid", "/etc/subgid").
Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid"). Symlink("/fortify/etc/subuid", "/etc/subuid").
Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers"). Symlink("/fortify/etc/sudoers", "/etc/sudoers").
Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d"). Symlink("/fortify/etc/sysctl.d", "/etc/sysctl.d").
Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd"). Symlink("/fortify/etc/systemd", "/etc/systemd").
Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo"). Symlink("/fortify/etc/terminfo", "/etc/terminfo").
Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d"). Symlink("/fortify/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink(fst.Tmp+"/etc/udev", "/etc/udev"). Symlink("/fortify/etc/udev", "/etc/udev").
Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2"). Symlink("/fortify/etc/udisks2", "/etc/udisks2").
Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower"). Symlink("/fortify/etc/UPower", "/etc/UPower").
Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf"). Symlink("/fortify/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink(fst.Tmp+"/etc/X11", "/etc/X11"). Symlink("/fortify/etc/X11", "/etc/X11").
Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs"). Symlink("/fortify/etc/zfs", "/etc/zfs").
Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc"). Symlink("/fortify/etc/zinputrc", "/etc/zinputrc").
Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo"). Symlink("/fortify/etc/zoneinfo", "/etc/zoneinfo").
Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile"). Symlink("/fortify/etc/zprofile", "/etc/zprofile").
Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv"). Symlink("/fortify/etc/zshenv", "/etc/zshenv").
Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc"). Symlink("/fortify/etc/zshrc", "/etc/zshrc").
Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", false, true). Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", false, true).
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("/home/chronos", "/home/chronos", false, true).
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/passwd", "/etc/passwd"). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/passwd", "/etc/passwd").
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/group", "/etc/group"). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/group", "/etc/group").
Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/wayland", "/run/user/1971/wayland-0"). Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/65534/wayland-0").
Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native"). Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native").
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/pulse-cookie", fst.Tmp+"/pulse-cookie"). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie", "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie").
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus"). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus").
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket"). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/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) 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

@ -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(fst.Tmp, 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/kvm", "/dev/kvm", true, true, true).
Bind("/etc", fst.Tmp+"/etc").
Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
Symlink(fst.Tmp+"/etc/default", "/etc/default").
Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab").
Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
Symlink(fst.Tmp+"/etc/services", "/etc/services").
Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
Symlink(fst.Tmp+"/etc/static", "/etc/static").
Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", false, true).
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": fst.Tmp + "/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(fst.Tmp, 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("/dev/kvm", "/dev/kvm", true, true, true).
Bind("/etc", fst.Tmp+"/etc").
Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
Symlink(fst.Tmp+"/etc/default", "/etc/default").
Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab").
Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
Symlink(fst.Tmp+"/etc/services", "/etc/services").
Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
Symlink(fst.Tmp+"/etc/static", "/etc/static").
Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", false, true).
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", fst.Tmp+"/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,23 +1,24 @@
package fst package app
import ( import (
"errors" "errors"
"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/linux"
"git.gensokyo.uk/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
const Tmp = "/.fortify" const fTmp = "/fortify"
// Config is used to seal an *App // Config is used to seal an *App
type Config struct { type Config struct {
// application ID // D-Bus application ID
ID string `json:"id"` ID string `json:"id"`
// 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"`
// child confinement configuration
Confinement ConfinementConfig `json:"confinement"` Confinement ConfinementConfig `json:"confinement"`
} }
@ -27,7 +28,7 @@ type ConfinementConfig struct {
AppID int `json:"app_id"` AppID int `json:"app_id"`
// list of supplementary groups to inherit // list of supplementary groups to inherit
Groups []string `json:"groups"` Groups []string `json:"groups"`
// passwd username in the sandbox, defaults to passwd name of target uid or chronos // passwd username in the sandbox, defaults to chronos
Username string `json:"username,omitempty"` Username string `json:"username,omitempty"`
// home directory in sandbox, empty for outer // home directory in sandbox, empty for outer
Inner string `json:"home_inner"` Inner string `json:"home_inner"`
@ -35,8 +36,6 @@ type ConfinementConfig struct {
Outer string `json:"home"` Outer string `json:"home"`
// bwrap sandbox confinement configuration // bwrap sandbox confinement configuration
Sandbox *SandboxConfig `json:"sandbox"` Sandbox *SandboxConfig `json:"sandbox"`
// extra acl entries to append
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
// reference to a system D-Bus proxy configuration, // reference to a system D-Bus proxy configuration,
// nil value disables system bus proxy // nil value disables system bus proxy
@ -45,7 +44,7 @@ type ConfinementConfig struct {
// nil value makes session bus proxy assume built-in defaults // nil value makes session bus proxy assume built-in defaults
SessionBus *dbus.Config `json:"session_bus,omitempty"` SessionBus *dbus.Config `json:"session_bus,omitempty"`
// system resources to expose to the sandbox // child capability enablements
Enablements system.Enablements `json:"enablements"` Enablements system.Enablements `json:"enablements"`
} }
@ -53,7 +52,7 @@ type ConfinementConfig struct {
type SandboxConfig struct { type SandboxConfig struct {
// unix hostname within sandbox // unix hostname within sandbox
Hostname string `json:"hostname,omitempty"` Hostname string `json:"hostname,omitempty"`
// allow userns within sandbox // userns availability within sandbox
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"`
@ -63,8 +62,8 @@ type SandboxConfig struct {
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"` MapRealUID bool `json:"map_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"`
@ -72,42 +71,12 @@ 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"`
// read-only /etc directory
Etc string `json:"etc,omitempty"`
// automatically set up /etc symlinks // automatically set up /etc symlinks
AutoEtc bool `json:"auto_etc"` 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"`
} }
type ExtraPermConfig struct {
Ensure bool `json:"ensure,omitempty"`
Path string `json:"path"`
Read bool `json:"r,omitempty"`
Write bool `json:"w,omitempty"`
Execute bool `json:"x,omitempty"`
}
func (e *ExtraPermConfig) String() string {
buf := make([]byte, 0, 5+len(e.Path))
buf = append(buf, '-', '-', '-')
if e.Ensure {
buf = append(buf, '+')
}
buf = append(buf, ':')
buf = append(buf, []byte(e.Path)...)
if e.Read {
buf[0] = 'r'
}
if e.Write {
buf[1] = 'w'
}
if e.Execute {
buf[2] = 'x'
}
return string(buf)
}
type FilesystemConfig struct { type FilesystemConfig struct {
// mount point in sandbox, same as src if empty // mount point in sandbox, same as src if empty
Dst string `json:"dst,omitempty"` Dst string `json:"dst,omitempty"`
@ -117,7 +86,7 @@ type FilesystemConfig struct {
Write bool `json:"write,omitempty"` Write bool `json:"write,omitempty"`
// device access // device access
Device bool `json:"dev,omitempty"` Device bool `json:"dev,omitempty"`
// fail if mount fails // exit if unable to share
Must bool `json:"require,omitempty"` Must bool `json:"require,omitempty"`
} }
@ -150,7 +119,7 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
}). }).
SetUID(uid).SetGID(uid). SetUID(uid).SetGID(uid).
Procfs("/proc"). Procfs("/proc").
Tmpfs(Tmp, 4*1024) Tmpfs(fTmp, 4*1024)
if !s.Dev { if !s.Dev {
conf.DevTmpfs("/dev").Mqueue("/dev/mqueue") conf.DevTmpfs("/dev").Mqueue("/dev/mqueue")
@ -159,11 +128,7 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
} }
if !s.AutoEtc { if !s.AutoEtc {
if s.Etc == "" { conf.Dir("/etc")
conf.Dir("/etc")
} else {
conf.Bind(s.Etc, "/etc")
}
} }
for _, c := range s.Filesystem { for _, c := range s.Filesystem {
@ -183,14 +148,10 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
} }
if s.AutoEtc { if s.AutoEtc {
etc := s.Etc conf.Bind("/etc", fTmp+"/etc")
if etc == "" {
etc = "/etc"
}
conf.Bind(etc, Tmp+"/etc")
// link host /etc contents to prevent passwd/group from being overwritten // link host /etc contents to prevent passwd/group from being overwritten
if d, err := os.ReadDir(etc); err != nil { if d, err := os.ReadDir("/etc"); err != nil {
return nil, err return nil, err
} else { } else {
for _, ent := range d { for _, ent := range d {
@ -202,7 +163,7 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
case "mtab": case "mtab":
conf.Symlink("/proc/mounts", "/etc/"+name) conf.Symlink("/proc/mounts", "/etc/"+name)
default: default:
conf.Symlink(Tmp+"/etc/"+name, "/etc/"+name) conf.Symlink(fTmp+"/etc/"+name, "/etc/"+name)
} }
} }
} }
@ -229,13 +190,13 @@ func Template() *Config {
Outer: "/var/lib/persist/home/org.chromium.Chromium", Outer: "/var/lib/persist/home/org.chromium.Chromium",
Inner: "/var/lib/fortify", 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, MapRealUID: true,
Dev: true, Dev: true,
DirectWayland: false, Wayland: 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{
@ -252,7 +213,6 @@ func Template() *Config {
{Src: "/dev/dri", Device: true}, {Src: "/dev/dri", Device: true},
}, },
Link: [][2]string{{"/run/user/65534", "/run/user/150"}}, Link: [][2]string{{"/run/user/65534", "/run/user/150"}},
Etc: "/etc",
AutoEtc: true, AutoEtc: true,
Override: []string{"/var/run/nscd"}, Override: []string{"/var/run/nscd"},
}, },

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

@ -8,13 +8,12 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"git.gensokyo.uk/security/fortify/acl" shim "git.ophivana.moe/security/fortify/cmd/fshim/ipc"
"git.gensokyo.uk/security/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/internal/state" "git.ophivana.moe/security/fortify/internal/system"
"git.gensokyo.uk/security/fortify/internal/system"
) )
var ( var (
@ -30,6 +29,8 @@ var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z
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))
@ -47,10 +48,6 @@ type appSeal struct {
// pass-through enablement tracking from config // pass-through enablement tracking from config
et system.Enablements et system.Enablements
// wayland socket direct access
directWayland bool
// extra UpdatePerm ops
extraPerms []*sealedExtraPerm
// prevents sharing from happening twice // prevents sharing from happening twice
shared bool shared bool
@ -62,14 +59,8 @@ type appSeal struct {
// protected by upstream mutex // protected by upstream mutex
} }
type sealedExtraPerm struct {
name string
perms acl.Perms
ensure bool
}
// 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()
@ -109,68 +100,47 @@ func (a *app) Seal(config *fst.Config) error {
if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 { if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 {
return fmsg.WrapError(ErrUser, return fmsg.WrapError(ErrUser,
fmt.Sprintf("aid %d out of range", config.Confinement.AppID)) fmt.Sprintf("aid %d out of range", config.Confinement.AppID))
}
seal.sys.user = appUser{
aid: config.Confinement.AppID,
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 = appUser{
seal.sys.user.us = strconv.Itoa(u) aid: config.Confinement.AppID,
} 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
}
// resolve supplementary group ids from names // invoke fsu for full uid
seal.sys.user.supp = make([]string, len(config.Confinement.Groups)) if u, err := a.os.Uid(seal.sys.user.aid); err != nil {
for i, name := range config.Confinement.Groups { return fmsg.WrapErrorSuffix(err,
if g, err := a.os.LookupGroup(name); err != nil { "cannot obtain uid from fsu:")
return fmsg.WrapError(err,
fmt.Sprintf("unknown group %q", name))
} else { } else {
seal.sys.user.supp[i] = g.Gid seal.sys.user.uid = u
} seal.sys.user.us = strconv.Itoa(u)
}
// build extra perms
seal.extraPerms = make([]*sealedExtraPerm, len(config.Confinement.ExtraPerms))
for i, p := range config.Confinement.ExtraPerms {
if p == nil {
continue
} }
seal.extraPerms[i] = new(sealedExtraPerm) // resolve supplementary group ids from names
seal.extraPerms[i].name = p.Path seal.sys.user.supp = make([]string, len(config.Confinement.Groups))
seal.extraPerms[i].perms = make(acl.Perms, 0, 3) for i, name := range config.Confinement.Groups {
if p.Read { if g, err := a.os.LookupGroup(name); err != nil {
seal.extraPerms[i].perms = append(seal.extraPerms[i].perms, acl.Read) return fmsg.WrapError(err,
fmt.Sprintf("unknown group %q", name))
} else {
seal.sys.user.supp[i] = g.Gid
}
} }
if p.Write {
seal.extraPerms[i].perms = append(seal.extraPerms[i].perms, acl.Write)
}
if p.Execute {
seal.extraPerms[i].perms = append(seal.extraPerms[i].perms, acl.Execute)
}
seal.extraPerms[i].ensure = p.Ensure
} }
// map sandbox config to bwrap // map sandbox config to bwrap
@ -178,7 +148,7 @@ 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,
@ -188,7 +158,7 @@ func (a *app) Seal(config *fst.Config) error {
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 {
@ -200,7 +170,7 @@ func (a *app) Seal(config *fst.Config) error {
case "/etc": case "/etc":
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...)
@ -209,7 +179,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 {
@ -217,7 +187,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...)
@ -229,14 +199,11 @@ 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})
} }
// opportunistically bind kvm
conf.Filesystem = append(conf.Filesystem, &fst.FilesystemConfig{Src: "/dev/kvm", Device: true})
config.Confinement.Sandbox = conf config.Confinement.Sandbox = conf
} }
seal.directWayland = config.Confinement.Sandbox.DirectWayland
if b, err := config.Confinement.Sandbox.Bwrap(a.os); err != nil { if b, err := config.Confinement.Sandbox.Bwrap(a.os); err != nil {
return err return err
} else { } else {
@ -247,10 +214,16 @@ func (a *app) Seal(config *fst.Config) error {
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.as)
// initialise system interface with full uid // initialise system interface with full uid
seal.sys.I = system.New(seal.sys.user.uid) seal.sys.I = system.New(seal.sys.user.uid)
@ -259,7 +232,7 @@ func (a *app) Seal(config *fst.Config) error {
seal.et = config.Confinement.Enablements seal.et = config.Confinement.Enablements
// this method calls all share methods in sequence // this method calls all share methods in sequence
if err := seal.setupShares([2]*dbus.Config{config.Confinement.SessionBus, config.Confinement.SystemBus}, a.os); err != nil { if err := seal.shareAll([2]*dbus.Config{config.Confinement.SessionBus, config.Confinement.SystemBus}, a.os); err != nil {
return err return err
} }
@ -269,6 +242,5 @@ func (a *app) Seal(config *fst.Config) error {
// 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

@ -0,0 +1,44 @@
package app
import (
"path"
"git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/internal/system"
)
const (
dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
)
func (seal *appSeal) shareDBus(config [2]*dbus.Config) error {
if !seal.et.Has(system.EDBus) {
return nil
}
// downstream socket paths
sessionPath, systemPath := path.Join(seal.share, "bus"), path.Join(seal.share, "system_bus_socket")
// configure dbus proxy
if f, err := seal.sys.ProxyDBus(config[0], config[1], sessionPath, systemPath); err != nil {
return err
} else {
seal.dbusMsg = f
}
// share proxy sockets
sessionInner := path.Join(seal.sys.runtime, "bus")
seal.sys.bwrap.SetEnv[dbusSessionBusAddress] = "unix:path=" + sessionInner
seal.sys.bwrap.Bind(sessionPath, sessionInner)
seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
if config[1] != nil {
systemInner := "/run/dbus/system_bus_socket"
seal.sys.bwrap.SetEnv[dbusSystemBusAddress] = "unix:path=" + systemInner
seal.sys.bwrap.Bind(systemPath, systemInner)
seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
}
return nil
}

View File

@ -0,0 +1,68 @@
package app
import (
"errors"
"path"
"git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/system"
)
const (
term = "TERM"
display = "DISPLAY"
// https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html
waylandDisplay = "WAYLAND_DISPLAY"
)
var (
ErrWayland = errors.New(waylandDisplay + " unset")
ErrXDisplay = errors.New(display + " unset")
)
func (seal *appSeal) shareDisplay(os linux.System) error {
// pass $TERM to launcher
if t, ok := os.LookupEnv(term); ok {
seal.sys.bwrap.SetEnv[term] = t
}
// set up wayland
if seal.et.Has(system.EWayland) {
if wd, ok := os.LookupEnv(waylandDisplay); !ok {
return fmsg.WrapError(ErrWayland,
"WAYLAND_DISPLAY is not set")
} else if seal.wl == nil {
// hardlink wayland socket
wp := path.Join(seal.RuntimePath, wd)
wpi := path.Join(seal.shareLocal, "wayland")
w := path.Join(seal.sys.runtime, "wayland-0")
seal.sys.Link(wp, wpi)
seal.sys.bwrap.SetEnv[waylandDisplay] = w
seal.sys.bwrap.Bind(wpi, w)
// ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
seal.sys.UpdatePermType(system.EWayland, wp, acl.Read, acl.Write, acl.Execute)
} else {
// set wayland socket path for mediation (e.g. `/run/user/%d/wayland-%d`)
seal.wl.Path = path.Join(seal.RuntimePath, wd)
}
}
// set up X11
if seal.et.Has(system.EX11) {
// discover X11 and grant user permission via the `ChangeHosts` command
if d, ok := os.LookupEnv(display); !ok {
return fmsg.WrapError(ErrXDisplay,
"DISPLAY is not set")
} else {
seal.sys.ChangeHosts("#" + seal.sys.user.us)
seal.sys.bwrap.SetEnv[display] = d
seal.sys.bwrap.Bind("/tmp/.X11-unix", "/tmp/.X11-unix")
}
}
return nil
}

View File

@ -1,346 +0,0 @@
package app
import (
"errors"
"fmt"
"io/fs"
"path"
"git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/system"
)
const (
home = "HOME"
shell = "SHELL"
xdgConfigHome = "XDG_CONFIG_HOME"
xdgRuntimeDir = "XDG_RUNTIME_DIR"
xdgSessionClass = "XDG_SESSION_CLASS"
xdgSessionType = "XDG_SESSION_TYPE"
term = "TERM"
display = "DISPLAY"
// https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html
waylandDisplay = "WAYLAND_DISPLAY"
pulseServer = "PULSE_SERVER"
pulseCookie = "PULSE_COOKIE"
dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
)
var (
ErrWayland = errors.New(waylandDisplay + " unset")
ErrXDisplay = errors.New(display + " unset")
ErrPulseCookie = errors.New("pulse cookie not present")
ErrPulseSocket = errors.New("pulse socket not present")
ErrPulseMode = errors.New("unexpected pulse socket mode")
)
func (seal *appSeal) setupShares(bus [2]*dbus.Config, os linux.System) error {
if seal.shared {
panic("seal shared twice")
}
seal.shared = true
/*
Tmpdir-based share directory
*/
// ensure Share (e.g. `/tmp/fortify.%d`)
// acl is unnecessary as this directory is world executable
seal.sys.Ensure(seal.SharePath, 0711)
// ensure process-specific share (e.g. `/tmp/fortify.%d/%s`)
// acl is unnecessary as this directory is world executable
seal.share = path.Join(seal.SharePath, seal.id)
seal.sys.Ephemeral(system.Process, seal.share, 0711)
// ensure child tmpdir parent directory (e.g. `/tmp/fortify.%d/tmpdir`)
targetTmpdirParent := path.Join(seal.SharePath, "tmpdir")
seal.sys.Ensure(targetTmpdirParent, 0700)
seal.sys.UpdatePermType(system.User, targetTmpdirParent, acl.Execute)
// ensure child tmpdir (e.g. `/tmp/fortify.%d/tmpdir/%d`)
targetTmpdir := path.Join(targetTmpdirParent, seal.sys.user.as)
seal.sys.Ensure(targetTmpdir, 01700)
seal.sys.UpdatePermType(system.User, targetTmpdir, acl.Read, acl.Write, acl.Execute)
seal.sys.bwrap.Bind(targetTmpdir, "/tmp", false, true)
/*
XDG runtime directory
*/
// mount tmpfs on inner runtime (e.g. `/run/user/%d`)
seal.sys.bwrap.Tmpfs("/run/user", 1*1024*1024)
seal.sys.bwrap.Tmpfs(seal.sys.runtime, 8*1024*1024)
// point to inner runtime path `/run/user/%d`
seal.sys.bwrap.SetEnv[xdgRuntimeDir] = seal.sys.runtime
seal.sys.bwrap.SetEnv[xdgSessionClass] = "user"
seal.sys.bwrap.SetEnv[xdgSessionType] = "tty"
// ensure RunDir (e.g. `/run/user/%d/fortify`)
seal.sys.Ensure(seal.RunDirPath, 0700)
seal.sys.UpdatePermType(system.User, seal.RunDirPath, acl.Execute)
// ensure runtime directory ACL (e.g. `/run/user/%d`)
seal.sys.Ensure(seal.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
seal.sys.UpdatePermType(system.User, seal.RuntimePath, acl.Execute)
// ensure process-specific share local to XDG_RUNTIME_DIR (e.g. `/run/user/%d/fortify/%s`)
seal.shareLocal = path.Join(seal.RunDirPath, seal.id)
seal.sys.Ephemeral(system.Process, seal.shareLocal, 0700)
seal.sys.UpdatePerm(seal.shareLocal, acl.Execute)
/*
Inner passwd database
*/
// look up shell
sh := "/bin/sh"
if s, ok := os.LookupEnv(shell); ok {
seal.sys.bwrap.SetEnv[shell] = s
sh = s
}
// generate /etc/passwd
passwdPath := path.Join(seal.share, "passwd")
username := "chronos"
if seal.sys.user.username != "" {
username = seal.sys.user.username
}
homeDir := "/var/empty"
if seal.sys.user.home != "" {
homeDir = seal.sys.user.home
}
// 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"
seal.sys.Write(passwdPath, passwd)
// write /etc/group
groupPath := path.Join(seal.share, "group")
seal.sys.Write(groupPath, "fortify:x:"+seal.sys.mappedIDString+":\n")
// bind /etc/passwd and /etc/group
seal.sys.bwrap.Bind(passwdPath, "/etc/passwd")
seal.sys.bwrap.Bind(groupPath, "/etc/group")
/*
Display servers
*/
// pass $TERM to launcher
if t, ok := os.LookupEnv(term); ok {
seal.sys.bwrap.SetEnv[term] = t
}
// set up wayland
if seal.et.Has(system.EWayland) {
var wp string
if wd, ok := os.LookupEnv(waylandDisplay); !ok {
return fmsg.WrapError(ErrWayland,
"WAYLAND_DISPLAY is not set")
} else {
wp = path.Join(seal.RuntimePath, wd)
}
w := path.Join(seal.sys.runtime, "wayland-0")
seal.sys.bwrap.SetEnv[waylandDisplay] = w
if !seal.directWayland { // set up security-context-v1
wc := path.Join(seal.SharePath, "wayland")
wt := path.Join(wc, seal.id)
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)
} else { // bind mount wayland socket (insecure)
// hardlink wayland socket
wpi := path.Join(seal.shareLocal, "wayland")
seal.sys.Link(wp, wpi)
seal.sys.bwrap.Bind(wpi, w)
// ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
seal.sys.UpdatePermType(system.EWayland, wp, acl.Read, acl.Write, acl.Execute)
}
}
// set up X11
if seal.et.Has(system.EX11) {
// discover X11 and grant user permission via the `ChangeHosts` command
if d, ok := os.LookupEnv(display); !ok {
return fmsg.WrapError(ErrXDisplay,
"DISPLAY is not set")
} else {
seal.sys.ChangeHosts("#" + seal.sys.user.us)
seal.sys.bwrap.SetEnv[display] = d
seal.sys.bwrap.Bind("/tmp/.X11-unix", "/tmp/.X11-unix")
}
}
/*
PulseAudio server and authentication
*/
if seal.et.Has(system.EPulse) {
// check PulseAudio directory presence (e.g. `/run/user/%d/pulse`)
pd := path.Join(seal.RuntimePath, "pulse")
ps := path.Join(pd, "native")
if _, err := os.Stat(pd); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio directory %q:", pd))
}
return fmsg.WrapError(ErrPulseSocket,
fmt.Sprintf("PulseAudio directory %q not found", pd))
}
// check PulseAudio socket permission (e.g. `/run/user/%d/pulse/native`)
if s, err := os.Stat(ps); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio socket %q:", ps))
}
return fmsg.WrapError(ErrPulseSocket,
fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pd))
} else {
if m := s.Mode(); m&0o006 != 0o006 {
return fmsg.WrapError(ErrPulseMode,
fmt.Sprintf("unexpected permissions on %q:", ps), m)
}
}
// hard link pulse socket into target-executable share
psi := path.Join(seal.shareLocal, "pulse")
p := path.Join(seal.sys.runtime, "pulse", "native")
seal.sys.Link(ps, psi)
seal.sys.bwrap.Bind(psi, p)
seal.sys.bwrap.SetEnv[pulseServer] = "unix:" + p
// publish current user's pulse cookie for target user
if src, err := discoverPulseCookie(os); err != nil {
// not fatal
fmsg.VPrintln(err.(*fmsg.BaseError).Message())
} else {
dst := path.Join(seal.share, "pulse-cookie")
innerDst := fst.Tmp + "/pulse-cookie"
seal.sys.bwrap.SetEnv[pulseCookie] = innerDst
seal.sys.CopyFile(dst, src)
seal.sys.bwrap.Bind(dst, innerDst)
}
}
/*
D-Bus proxy
*/
if seal.et.Has(system.EDBus) {
// ensure dbus session bus defaults
if bus[0] == nil {
bus[0] = dbus.NewConfig(seal.fid, true, true)
}
// downstream socket paths
sessionPath, systemPath := path.Join(seal.share, "bus"), path.Join(seal.share, "system_bus_socket")
// configure dbus proxy
if f, err := seal.sys.ProxyDBus(bus[0], bus[1], sessionPath, systemPath); err != nil {
return err
} else {
seal.dbusMsg = f
}
// share proxy sockets
sessionInner := path.Join(seal.sys.runtime, "bus")
seal.sys.bwrap.SetEnv[dbusSessionBusAddress] = "unix:path=" + sessionInner
seal.sys.bwrap.Bind(sessionPath, sessionInner)
seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
if bus[1] != nil {
systemInner := "/run/dbus/system_bus_socket"
seal.sys.bwrap.SetEnv[dbusSystemBusAddress] = "unix:path=" + systemInner
seal.sys.bwrap.Bind(systemPath, systemInner)
seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
}
}
/*
Miscellaneous
*/
// queue overriding tmpfs at the end of seal.sys.bwrap.Filesystem
for _, dest := range seal.sys.override {
seal.sys.bwrap.Tmpfs(dest, 8*1024)
}
// append extra perms
for _, p := range seal.extraPerms {
if p == nil {
continue
}
if p.ensure {
seal.sys.Ensure(p.name, 0700)
}
seal.sys.UpdatePermType(system.User, p.name, p.perms...)
}
return nil
}
// discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie
func discoverPulseCookie(os linux.System) (string, error) {
if p, ok := os.LookupEnv(pulseCookie); ok {
return p, nil
}
// dotfile $HOME/.pulse-cookie
if p, ok := os.LookupEnv(home); ok {
p = path.Join(p, ".pulse-cookie")
if s, err := os.Stat(p); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return p, fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
}
// not found, try next method
} else if !s.IsDir() {
return p, nil
}
}
// $XDG_CONFIG_HOME/pulse/cookie
if p, ok := os.LookupEnv(xdgConfigHome); ok {
p = path.Join(p, "pulse", "cookie")
if s, err := os.Stat(p); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return p, fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
}
// not found, try next method
} else if !s.IsDir() {
return p, nil
}
}
return "", fmsg.WrapError(ErrPulseCookie,
fmt.Sprintf("cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
pulseCookie, xdgConfigHome, home))
}

117
internal/app/share.pulse.go Normal file
View File

@ -0,0 +1,117 @@
package app
import (
"errors"
"fmt"
"io/fs"
"path"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/system"
)
const (
pulseServer = "PULSE_SERVER"
pulseCookie = "PULSE_COOKIE"
home = "HOME"
xdgConfigHome = "XDG_CONFIG_HOME"
)
var (
ErrPulseCookie = errors.New("pulse cookie not present")
ErrPulseSocket = errors.New("pulse socket not present")
ErrPulseMode = errors.New("unexpected pulse socket mode")
)
func (seal *appSeal) sharePulse(os linux.System) error {
if !seal.et.Has(system.EPulse) {
return nil
}
// check PulseAudio directory presence (e.g. `/run/user/%d/pulse`)
pd := path.Join(seal.RuntimePath, "pulse")
ps := path.Join(pd, "native")
if _, err := os.Stat(pd); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio directory %q:", pd))
}
return fmsg.WrapError(ErrPulseSocket,
fmt.Sprintf("PulseAudio directory %q not found", pd))
}
// check PulseAudio socket permission (e.g. `/run/user/%d/pulse/native`)
if s, err := os.Stat(ps); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio socket %q:", ps))
}
return fmsg.WrapError(ErrPulseSocket,
fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pd))
} else {
if m := s.Mode(); m&0o006 != 0o006 {
return fmsg.WrapError(ErrPulseMode,
fmt.Sprintf("unexpected permissions on %q:", ps), m)
}
}
// hard link pulse socket into target-executable share
psi := path.Join(seal.shareLocal, "pulse")
p := path.Join(seal.sys.runtime, "pulse", "native")
seal.sys.Link(ps, psi)
seal.sys.bwrap.Bind(psi, p)
seal.sys.bwrap.SetEnv[pulseServer] = "unix:" + p
// publish current user's pulse cookie for target user
if src, err := discoverPulseCookie(os); err != nil {
return err
} else {
dst := path.Join(seal.share, "pulse-cookie")
seal.sys.bwrap.SetEnv[pulseCookie] = dst
seal.sys.CopyFile(dst, src)
seal.sys.bwrap.Bind(dst, dst)
}
return nil
}
// discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie
func discoverPulseCookie(os linux.System) (string, error) {
if p, ok := os.LookupEnv(pulseCookie); ok {
return p, nil
}
// dotfile $HOME/.pulse-cookie
if p, ok := os.LookupEnv(home); ok {
p = path.Join(p, ".pulse-cookie")
if s, err := os.Stat(p); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return p, fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
}
// not found, try next method
} else if !s.IsDir() {
return p, nil
}
}
// $XDG_CONFIG_HOME/pulse/cookie
if p, ok := os.LookupEnv(xdgConfigHome); ok {
p = path.Join(p, "pulse", "cookie")
if s, err := os.Stat(p); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return p, fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
}
// not found, try next method
} else if !s.IsDir() {
return p, nil
}
}
return "", fmsg.WrapError(ErrPulseCookie,
fmt.Sprintf("cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
pulseCookie, xdgConfigHome, home))
}

View File

@ -0,0 +1,39 @@
package app
import (
"path"
"git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/internal/system"
)
const (
xdgRuntimeDir = "XDG_RUNTIME_DIR"
xdgSessionClass = "XDG_SESSION_CLASS"
xdgSessionType = "XDG_SESSION_TYPE"
)
// shareRuntime queues actions for sharing/ensuring the runtime and share directories
func (seal *appSeal) shareRuntime() {
// mount tmpfs on inner runtime (e.g. `/run/user/%d`)
seal.sys.bwrap.Tmpfs("/run/user", 1*1024*1024)
seal.sys.bwrap.Tmpfs(seal.sys.runtime, 8*1024*1024)
// point to inner runtime path `/run/user/%d`
seal.sys.bwrap.SetEnv[xdgRuntimeDir] = seal.sys.runtime
seal.sys.bwrap.SetEnv[xdgSessionClass] = "user"
seal.sys.bwrap.SetEnv[xdgSessionType] = "tty"
// ensure RunDir (e.g. `/run/user/%d/fortify`)
seal.sys.Ensure(seal.RunDirPath, 0700)
seal.sys.UpdatePermType(system.User, seal.RunDirPath, acl.Execute)
// ensure runtime directory ACL (e.g. `/run/user/%d`)
seal.sys.Ensure(seal.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
seal.sys.UpdatePermType(system.User, seal.RuntimePath, acl.Execute)
// ensure process-specific share local to XDG_RUNTIME_DIR (e.g. `/run/user/%d/fortify/%s`)
seal.shareLocal = path.Join(seal.RunDirPath, seal.id)
seal.sys.Ephemeral(system.Process, seal.shareLocal, 0700)
seal.sys.UpdatePerm(seal.shareLocal, acl.Execute)
}

View File

@ -0,0 +1,77 @@
package app
import (
"path"
"git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/system"
)
const (
shell = "SHELL"
)
// shareSystem queues various system-related actions
func (seal *appSeal) shareSystem() {
// ensure Share (e.g. `/tmp/fortify.%d`)
// acl is unnecessary as this directory is world executable
seal.sys.Ensure(seal.SharePath, 0711)
// ensure process-specific share (e.g. `/tmp/fortify.%d/%s`)
// acl is unnecessary as this directory is world executable
seal.share = path.Join(seal.SharePath, seal.id)
seal.sys.Ephemeral(system.Process, seal.share, 0711)
// ensure child tmpdir parent directory (e.g. `/tmp/fortify.%d/tmpdir`)
targetTmpdirParent := path.Join(seal.SharePath, "tmpdir")
seal.sys.Ensure(targetTmpdirParent, 0700)
seal.sys.UpdatePermType(system.User, targetTmpdirParent, acl.Execute)
// ensure child tmpdir (e.g. `/tmp/fortify.%d/tmpdir/%d`)
targetTmpdir := path.Join(targetTmpdirParent, seal.sys.user.as)
seal.sys.Ensure(targetTmpdir, 01700)
seal.sys.UpdatePermType(system.User, targetTmpdir, acl.Read, acl.Write, acl.Execute)
seal.sys.bwrap.Bind(targetTmpdir, "/tmp", false, true)
// mount tmpfs on inner shared directory (e.g. `/tmp/fortify.%d`)
seal.sys.bwrap.Tmpfs(seal.SharePath, 1*1024*1024)
}
func (seal *appSeal) sharePasswd(os linux.System) {
// look up shell
sh := "/bin/sh"
if s, ok := os.LookupEnv(shell); ok {
seal.sys.bwrap.SetEnv[shell] = s
sh = s
}
// generate /etc/passwd
passwdPath := path.Join(seal.share, "passwd")
username := "chronos"
if seal.sys.user.username != "" {
username = seal.sys.user.username
}
homeDir := "/var/empty"
if seal.sys.user.home != "" {
homeDir = seal.sys.user.home
}
// 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"
seal.sys.Write(passwdPath, passwd)
// write /etc/group
groupPath := path.Join(seal.share, "group")
seal.sys.Write(groupPath, "fortify:x:"+seal.sys.mappedIDString+":\n")
// bind /etc/passwd and /etc/group
seal.sys.bwrap.Bind(passwdPath, "/etc/passwd")
seal.sys.bwrap.Bind(groupPath, "/etc/group")
}

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.
@ -45,11 +46,13 @@ func (a *app) Start() error {
uint32(a.seal.sys.UID()), uint32(a.seal.sys.UID()),
a.seal.sys.user.as, a.seal.sys.user.as,
a.seal.sys.user.supp, a.seal.sys.user.supp,
path.Join(a.seal.share, "shim"),
a.seal.wl,
&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,
Home: a.seal.sys.user.data, WL: a.seal.wl != nil,
Verbose: fmsg.Verbose(), Verbose: fmsg.Verbose(),
}, },
@ -61,24 +64,22 @@ func (a *app) Start() error {
} }
a.seal.sys.needRevert = true a.seal.sys.needRevert = true
// export sync pipe from sys
a.seal.sys.bwrap.SetSync(a.seal.sys.Sync())
if startTime, err := a.shim.Start(); err != nil { 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, Command: a.seal.command,
Config: a.ct.Unwrap(), Capability: a.seal.et,
Time: *startTime, Argv: a.shim.Unwrap().Args,
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:")
@ -198,13 +199,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
} }
} }
@ -225,12 +233,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
@ -251,6 +255,12 @@ func (a *app) Wait() (int, error) {
} }
} }
if a.shim.Unwrap() == nil {
fmsg.VPrintln("fault before shim start")
} else {
a.shim.AbortWait(errors.New("shim exited"))
}
if a.seal.sys.needRevert { if a.seal.sys.needRevert {
if err := a.seal.sys.Revert(ec); err != nil { if err := a.seal.sys.Revert(ec); err != nil {
return err.(RevertCompoundError) return err.(RevertCompoundError)

View File

@ -1,8 +1,10 @@
package app package app
import ( import (
"git.gensokyo.uk/security/fortify/helper/bwrap" "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
@ -49,3 +51,37 @@ type appUser struct {
// passwd database username // passwd database username
username string username string
} }
// shareAll calls all share methods in sequence
func (seal *appSeal) shareAll(bus [2]*dbus.Config, os linux.System) error {
if seal.shared {
panic("seal shared twice")
}
seal.shared = true
seal.shareSystem()
seal.shareRuntime()
seal.sharePasswd(os)
if err := seal.shareDisplay(os); err != nil {
return err
}
if err := seal.sharePulse(os); err != nil {
return err
}
// ensure dbus session bus defaults
if bus[0] == nil {
bus[0] = dbus.NewConfig(seal.fid, true, true)
}
if err := seal.shareDBus(bus); err != nil {
return err
}
// queue overriding tmpfs at the end of seal.sys.bwrap.Filesystem
for _, dest := range seal.sys.override {
seal.sys.bwrap.Tmpfs(dest, 8*1024)
}
return nil
}

View File

@ -7,7 +7,7 @@ import (
"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.
@ -39,6 +39,8 @@ type System interface {
Paths() Paths Paths() Paths
// Uid invokes fsu and returns target uid. // Uid invokes fsu and returns target uid.
Uid(aid int) (int, error) Uid(aid int) (int, error)
// SdBooted implements https://www.freedesktop.org/software/systemd/man/sd_booted.html
SdBooted() bool
} }
// Paths contains environment dependent paths used by fortify. // Paths contains environment dependent paths used by fortify.

View File

@ -1,6 +1,7 @@
package linux package linux
import ( import (
"errors"
"io" "io"
"io/fs" "io/fs"
"os" "os"
@ -9,8 +10,8 @@ import (
"strconv" "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,6 +19,9 @@ type Std struct {
paths Paths paths Paths
pathsOnce sync.Once pathsOnce sync.Once
sdBooted bool
sdBootedOnce sync.Once
uidOnce sync.Once uidOnce sync.Once
uidCopy map[int]struct { uidCopy map[int]struct {
uid int uid int
@ -86,3 +90,31 @@ func (s *Std) Uid(aid int) (int, error) {
return u.uid, u.err return u.uid, u.err
} }
} }
func (s *Std) SdBooted() bool {
s.sdBootedOnce.Do(func() { s.sdBooted = copySdBooted() })
return s.sdBooted
}
const systemdCheckPath = "/run/systemd/system"
func copySdBooted() bool {
if v, err := sdBooted(); err != nil {
fmsg.Println("cannot read systemd marker:", err)
return false
} else {
return v
}
}
func sdBooted() (bool, error) {
_, err := os.Stat(systemdCheckPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
err = nil
}
return false, err
}
return true, nil
}

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()))
}

121
internal/state/print.go Normal file
View File

@ -0,0 +1,121 @@
package state
import (
"errors"
"fmt"
"os"
"path"
"strconv"
"strings"
"text/tabwriter"
"time"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/system"
)
// MustPrintLauncherStateSimpleGlobal prints active launcher states of all simple stores
// in an implementation-specific way.
func MustPrintLauncherStateSimpleGlobal(w **tabwriter.Writer, runDir string) {
now := time.Now().UTC()
// read runtime directory to get all UIDs
if dirs, err := os.ReadDir(path.Join(runDir, "state")); err != nil && !errors.Is(err, os.ErrNotExist) {
fmsg.Fatal("cannot read runtime directory:", err)
} else {
for _, e := range dirs {
// skip non-directories
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
if err = s.Close(); err != nil {
fmsg.Printf("cannot close store for user %q: %s", e.Name(), err)
}
}
}
}
func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time) {
var innerErr error
if ok, err := s.Do(func(b Backend) {
innerErr = func() error {
// read launcher states
states, err := b.Load()
if err != nil {
return err
}
// initialise tabwriter if nil
if *w == nil {
*w = tabwriter.NewWriter(os.Stdout, 0, 1, 4, ' ', 0)
// write header when initialising
if !fmsg.Verbose() {
_, _ = fmt.Fprintln(*w, "\tPID\tApp\tUptime\tEnablements\tCommand")
} else {
// argv is emitted in body when verbose
_, _ = fmt.Fprintln(*w, "\tPID\tApp\tArgv")
}
}
// print each state
for _, state := range states {
// skip nil states
if state == nil {
_, _ = fmt.Fprintln(*w, "\tnil state entry")
continue
}
// build enablements string
ets := strings.Builder{}
// append enablement strings in order
for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
if state.Capability.Has(i) {
ets.WriteString(", " + i.String())
}
}
// prevent an empty string when
if ets.Len() == 0 {
ets.WriteString("(No enablements)")
}
if !fmsg.Verbose() {
_, _ = fmt.Fprintf(*w, "\t%d\t%s\t%s\t%s\t%s\n",
state.PID, s.path[len(s.path)-1], now.Sub(state.Time).Round(time.Second).String(), strings.TrimPrefix(ets.String(), ", "),
state.Command)
} else {
// emit argv instead when verbose
_, _ = fmt.Fprintf(*w, "\t%d\t%s\t%s\n",
state.PID, s.path[len(s.path)-1], state.Argv)
}
}
return nil
}()
}); err != nil {
fmsg.Printf("cannot perform action on store %q: %s", path.Join(s.path...), err)
if !ok {
fmsg.Fatal("store faulted before printing")
}
}
if innerErr != nil {
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,38 @@ 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
// full argv whe launching
Argv []string
// process start time // process start time
Time time.Time Time time.Time
} }

View File

@ -1,134 +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("join store", func(t *testing.T) {
if entries, err := state.Join(s); err != nil {
t.Fatalf("Join: error = %v", err)
} else if len(entries) != 3 {
t.Fatalf("Join(s) = %#v", entries)
}
})
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

@ -1,49 +0,0 @@
package state
import (
"errors"
"maps"
)
var (
ErrDuplicate = errors.New("store contains duplicates")
)
// Join returns joined state entries of all active aids.
func Join(s Store) (Entries, error) {
var (
aids []int
entries = make(Entries)
el int
res Entries
loadErr error
)
if ln, err := s.List(); err != nil {
return nil, err
} else {
aids = ln
}
for _, aid := range aids {
if _, err := s.Do(aid, func(c Cursor) {
res, loadErr = c.Load()
}); err != nil {
return nil, err
}
if loadErr != nil {
return nil, loadErr
}
// save expected length
el = len(entries) + len(res)
maps.Copy(entries, res)
if len(entries) != el {
return nil, ErrDuplicate
}
}
return entries, nil
}

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 (
@ -36,7 +36,7 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st
} }
// system bus is optional // system bus is optional
d.system = system != nil d.system = system == nil
// upstream address, downstream socket path // upstream address, downstream socket path
var sessionBus, systemBus [2]string var sessionBus, systemBus [2]string

View File

@ -1,9 +1,5 @@
package system package system
import (
"strings"
)
type ( type (
// Enablement represents an optional system resource // Enablement represents an optional system resource
Enablement uint8 Enablement uint8
@ -51,18 +47,3 @@ func (es *Enablements) Set(e Enablement) {
*es |= e.Mask() *es |= e.Mask()
} }
func (es *Enablements) String() string {
buf := new(strings.Builder)
for i := Enablement(0); i < Enablement(ELen); i++ {
if es.Has(i) {
buf.WriteString(", " + i.String())
}
}
if buf.Len() == 0 {
buf.WriteString("(No enablements)")
}
return strings.TrimPrefix(buf.String(), ", ")
}

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

@ -32,7 +32,7 @@ func Parse(stdout fmt.Stringer) ([]*Entry, error) {
switch len(segment) { switch len(segment) {
case 2: // /lib/ld-musl-x86_64.so.1 (0x7f04d14ef000) case 2: // /lib/ld-musl-x86_64.so.1 (0x7f04d14ef000)
iL = 1 iL = 1
result[i] = &Entry{Name: strings.TrimSpace(segment[0])} result[i] = &Entry{Name: segment[0]}
case 4: // libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7f04d14ef000) case 4: // libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7f04d14ef000)
iL = 3 iL = 3
if segment[1] != "=>" { if segment[1] != "=>" {
@ -42,7 +42,7 @@ func Parse(stdout fmt.Stringer) ([]*Entry, error) {
return nil, ErrPathNotAbsolute return nil, ErrPathNotAbsolute
} }
result[i] = &Entry{ result[i] = &Entry{
Name: strings.TrimSpace(segment[0]), Name: segment[0],
Path: segment[2], Path: segment[2],
} }
default: default:

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) {
@ -65,12 +65,12 @@ libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7ff71c0a4000)`,
{"libc.musl-x86_64.so.1", "/lib/ld-musl-x86_64.so.1", 0x7ff71c0a4000}, {"libc.musl-x86_64.so.1", "/lib/ld-musl-x86_64.so.1", 0x7ff71c0a4000},
}}, }},
{"glibc /nix/store/rc3n2r3nffpib2gqpxlkjx36frw6n34z-kmod-31/bin/kmod", ` {"glibc /nix/store/rc3n2r3nffpib2gqpxlkjx36frw6n34z-kmod-31/bin/kmod", `
linux-vdso.so.1 (0x00007ffed65be000) linux-vdso.so.1 (0x00007ffed65be000)
libzstd.so.1 => /nix/store/80pxmvb9q43kh9rkjagc4h41vf6dh1y6-zstd-1.5.6/lib/libzstd.so.1 (0x00007f3199cd1000) libzstd.so.1 => /nix/store/80pxmvb9q43kh9rkjagc4h41vf6dh1y6-zstd-1.5.6/lib/libzstd.so.1 (0x00007f3199cd1000)
liblzma.so.5 => /nix/store/g78jna1i5qhh8gqs4mr64648f0szqgw4-xz-5.4.7/lib/liblzma.so.5 (0x00007f3199ca2000) liblzma.so.5 => /nix/store/g78jna1i5qhh8gqs4mr64648f0szqgw4-xz-5.4.7/lib/liblzma.so.5 (0x00007f3199ca2000)
libc.so.6 => /nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib/libc.so.6 (0x00007f3199ab5000) libc.so.6 => /nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib/libc.so.6 (0x00007f3199ab5000)
libpthread.so.0 => /nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib/libpthread.so.0 (0x00007f3199ab0000) libpthread.so.0 => /nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib/libpthread.so.0 (0x00007f3199ab0000)
/nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib/ld-linux-x86-64.so.2 => /nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib64/ld-linux-x86-64.so.2 (0x00007f3199da5000)`, /nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib/ld-linux-x86-64.so.2 => /nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib64/ld-linux-x86-64.so.2 (0x00007f3199da5000)`,
[]*ldd.Entry{ []*ldd.Entry{
{"linux-vdso.so.1", "", 0x00007ffed65be000}, {"linux-vdso.so.1", "", 0x00007ffed65be000},
{"libzstd.so.1", "/nix/store/80pxmvb9q43kh9rkjagc4h41vf6dh1y6-zstd-1.5.6/lib/libzstd.so.1", 0x00007f3199cd1000}, {"libzstd.so.1", "/nix/store/80pxmvb9q43kh9rkjagc4h41vf6dh1y6-zstd-1.5.6/lib/libzstd.so.1", 0x00007f3199cd1000},
@ -79,35 +79,6 @@ libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7ff71c0a4000)`,
{"libpthread.so.0", "/nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib/libpthread.so.0", 0x00007f3199ab0000}, {"libpthread.so.0", "/nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib/libpthread.so.0", 0x00007f3199ab0000},
{"/nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib/ld-linux-x86-64.so.2", "/nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib64/ld-linux-x86-64.so.2", 0x00007f3199da5000}, {"/nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib/ld-linux-x86-64.so.2", "/nix/store/c10zhkbp6jmyh0xc5kd123ga8yy2p4hk-glibc-2.39-52/lib64/ld-linux-x86-64.so.2", 0x00007f3199da5000},
}}, }},
{"glibc /usr/bin/xdg-dbus-proxy", `
linux-vdso.so.1 (0x00007725f5772000)
libglib-2.0.so.0 => /usr/lib/libglib-2.0.so.0 (0x00007725f55d5000)
libgio-2.0.so.0 => /usr/lib/libgio-2.0.so.0 (0x00007725f5406000)
libgobject-2.0.so.0 => /usr/lib/libgobject-2.0.so.0 (0x00007725f53a6000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007725f5378000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007725f5187000)
libpcre2-8.so.0 => /usr/lib/libpcre2-8.so.0 (0x00007725f50e8000)
libgmodule-2.0.so.0 => /usr/lib/libgmodule-2.0.so.0 (0x00007725f50df000)
libz.so.1 => /usr/lib/libz.so.1 (0x00007725f50c6000)
libmount.so.1 => /usr/lib/libmount.so.1 (0x00007725f5076000)
libffi.so.8 => /usr/lib/libffi.so.8 (0x00007725f506b000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007725f5774000)
libblkid.so.1 => /usr/lib/libblkid.so.1 (0x00007725f5032000)`,
[]*ldd.Entry{
{"linux-vdso.so.1", "", 0x00007725f5772000},
{"libglib-2.0.so.0", "/usr/lib/libglib-2.0.so.0", 0x00007725f55d5000},
{"libgio-2.0.so.0", "/usr/lib/libgio-2.0.so.0", 0x00007725f5406000},
{"libgobject-2.0.so.0", "/usr/lib/libgobject-2.0.so.0", 0x00007725f53a6000},
{"libgcc_s.so.1", "/usr/lib/libgcc_s.so.1", 0x00007725f5378000},
{"libc.so.6", "/usr/lib/libc.so.6", 0x00007725f5187000},
{"libpcre2-8.so.0", "/usr/lib/libpcre2-8.so.0", 0x00007725f50e8000},
{"libgmodule-2.0.so.0", "/usr/lib/libgmodule-2.0.so.0", 0x00007725f50df000},
{"libz.so.1", "/usr/lib/libz.so.1", 0x00007725f50c6000},
{"libmount.so.1", "/usr/lib/libmount.so.1", 0x00007725f5076000},
{"libffi.so.8", "/usr/lib/libffi.so.8", 0x00007725f506b000},
{"/lib64/ld-linux-x86-64.so.2", "/usr/lib64/ld-linux-x86-64.so.2", 0x00007725f5774000},
{"libblkid.so.1", "/usr/lib/libblkid.so.1", 0x00007725f5032000},
}},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.file, func(t *testing.T) { t.Run(tc.file, func(t *testing.T) {

98
main.go
View File

@ -2,6 +2,7 @@ package main
import ( import (
_ "embed" _ "embed"
"encoding/json"
"flag" "flag"
"fmt" "fmt"
"os/user" "os/user"
@ -10,19 +11,17 @@ import (
"sync" "sync"
"text/tabwriter" "text/tabwriter"
"git.gensokyo.uk/security/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst" "git.ophivana.moe/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal" "git.ophivana.moe/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/app" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/internal/state" "git.ophivana.moe/security/fortify/internal/system"
"git.gensokyo.uk/security/fortify/internal/system"
) )
var ( var (
flagVerbose bool flagVerbose bool
flagJSON bool
//go:embed LICENSE //go:embed LICENSE
license string license string
@ -30,7 +29,6 @@ var (
func init() { func init() {
flag.BoolVar(&flagVerbose, "v", false, "Verbose output") flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
flag.BoolVar(&flagJSON, "json", false, "Format output in JSON when applicable")
} }
var os = new(linux.Std) var os = new(linux.Std)
@ -62,14 +60,13 @@ func main() {
flag.CommandLine.Usage = func() { flag.CommandLine.Usage = func() {
fmt.Println() fmt.Println()
fmt.Println("Usage:\tfortify [-v] [--json] COMMAND [OPTIONS]") fmt.Println("Usage:\tfortify [-v] COMMAND [OPTIONS]")
fmt.Println() fmt.Println()
fmt.Println("Commands:") fmt.Println("Commands:")
w := tabwriter.NewWriter(os.Stdout(), 0, 1, 4, ' ', 0) w := tabwriter.NewWriter(os.Stdout(), 0, 1, 4, ' ', 0)
commands := [][2]string{ commands := [][2]string{
{"app", "Launch app defined by the specified config file"}, {"app", "Launch app defined by the specified config file"},
{"run", "Configure and start a permissive default sandbox"}, {"run", "Configure and start a permissive default sandbox"},
{"show", "Show the contents of an app configuration"},
{"ps", "List active apps and their state"}, {"ps", "List active apps and their state"},
{"version", "Show fortify version"}, {"version", "Show fortify version"},
{"license", "Show full license text"}, {"license", "Show full license text"},
@ -105,70 +102,57 @@ func main() {
fmt.Println(license) fmt.Println(license)
fmsg.Exit(0) fmsg.Exit(0)
case "template": // print full template configuration case "template": // print full template configuration
printJSON(fst.Template()) 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) fmsg.Exit(0)
case "help": // print help message case "help": // print help message
flag.CommandLine.Usage() flag.CommandLine.Usage()
fmsg.Exit(0) fmsg.Exit(0)
case "ps": // print all state info case "ps": // print all state info
set := flag.NewFlagSet("ps", flag.ExitOnError) var w *tabwriter.Writer
var short bool state.MustPrintLauncherStateSimpleGlobal(&w, os.Paths().RunDirPath)
set.BoolVar(&short, "short", false, "Print instance id") if w != nil {
if err := w.Flush(); err != nil {
// Ignore errors; set is set for ExitOnError. fmsg.Println("cannot format output:", err)
_ = set.Parse(args[1:]) }
printPs(short)
fmsg.Exit(0)
case "show": // pretty-print app info
set := flag.NewFlagSet("show", flag.ExitOnError)
var short bool
set.BoolVar(&short, "short", false, "Omit filesystem information")
// Ignore errors; set is set for ExitOnError.
_ = set.Parse(args[1:])
var (
config *fst.Config
instance *state.State
name string
)
if len(set.Args()) != 1 {
fmsg.Fatal("show requires 1 argument")
} else { } else {
name = set.Args()[0] fmt.Println("No information available")
config, instance = tryShort(name)
} }
if config == nil {
config = tryPath(name)
}
printShow(instance, config, short)
fmsg.Exit(0) fmsg.Exit(0)
case "app": // launch app from configuration file case "app": // launch app from configuration file
if len(args) < 2 { if len(args) < 2 {
fmsg.Fatal("app requires at least 1 argument") fmsg.Fatal("app requires at least 1 argument")
} }
// config extraArgs... config := new(app.Config)
config := tryPath(args[1]) 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:]...) config.Command = append(config.Command, args[2:]...)
// invoke app // invoke app
runApp(config) runApp(config)
panic("unreachable")
case "run": // run app in permissive defaults usage pattern case "run": // run app in permissive defaults usage pattern
set := flag.NewFlagSet("run", flag.ExitOnError) set := flag.NewFlagSet("run", flag.ExitOnError)
var ( var (
dbusConfigSession string dbusConfigSession string
dbusConfigSystem string dbusConfigSystem string
dbusID string
mpris bool mpris bool
dbusVerbose bool dbusVerbose bool
fid string
aid int aid int
groups gl groups gl
homeDir string homeDir string
@ -178,15 +162,15 @@ func main() {
set.StringVar(&dbusConfigSession, "dbus-config", "builtin", "Path to D-Bus proxy config file, or \"builtin\" for defaults") 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.StringVar(&dbusConfigSystem, "dbus-system", "nil", "Path to system D-Bus proxy config file, or \"nil\" to disable")
set.StringVar(&dbusID, "dbus-id", "", "D-Bus ID of application, leave empty to disable own paths, has no effect if custom config is available")
set.BoolVar(&mpris, "mpris", false, "Allow owning MPRIS D-Bus path, has no effect if custom config is available") 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.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.IntVar(&aid, "a", 0, "Fortify application ID")
set.Var(&groups, "g", "Groups inherited by the app process") set.Var(&groups, "g", "Groups inherited by the app process")
set.StringVar(&homeDir, "d", "os", "Application home directory") set.StringVar(&homeDir, "d", "os", "Application home directory")
set.StringVar(&userName, "u", "chronos", "Passwd name within sandbox") set.StringVar(&userName, "u", "chronos", "Passwd name within sandbox")
set.BoolVar(&enablements[system.EWayland], "wayland", false, "Allow Wayland connections") set.BoolVar(&enablements[system.EWayland], "wayland", false, "Share Wayland socket")
set.BoolVar(&enablements[system.EX11], "X", false, "Share X11 socket and allow connection") 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.EDBus], "dbus", false, "Proxy D-Bus connection")
set.BoolVar(&enablements[system.EPulse], "pulse", false, "Share PulseAudio socket and cookie") set.BoolVar(&enablements[system.EPulse], "pulse", false, "Share PulseAudio socket and cookie")
@ -195,8 +179,8 @@ func main() {
_ = set.Parse(args[1:]) _ = set.Parse(args[1:])
// initialise config from flags // initialise config from flags
config := &fst.Config{ config := &app.Config{
ID: fid, ID: dbusID,
Command: set.Args(), Command: set.Args(),
} }
@ -257,7 +241,7 @@ func main() {
// parse D-Bus config file from flags if applicable // parse D-Bus config file from flags if applicable
if enablements[system.EDBus] { if enablements[system.EDBus] {
if dbusConfigSession == "builtin" { if dbusConfigSession == "builtin" {
config.Confinement.SessionBus = dbus.NewConfig(fid, true, mpris) config.Confinement.SessionBus = dbus.NewConfig(dbusID, true, mpris)
} else { } else {
if c, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil { if c, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
fmsg.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err) fmsg.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
@ -291,7 +275,11 @@ func main() {
panic("unreachable") panic("unreachable")
} }
func runApp(config *fst.Config) { func runApp(config *app.Config) {
if os.SdBooted() {
fmsg.VPrintln("system booted with systemd as init system")
}
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)

View File

@ -10,6 +10,7 @@ let
mkIf mkIf
mkDefault mkDefault
mapAttrs mapAttrs
mapAttrsToList
mergeAttrsList mergeAttrsList
imap1 imap1
foldr foldr
@ -52,8 +53,6 @@ in
''; '';
}; };
systemd.services.nix-daemon.unitConfig.RequiresMountsFor = [ "/etc/userdb" ];
services.userdbd.enable = mkDefault true; services.userdbd.enable = mkDefault true;
home-manager = home-manager =
@ -115,7 +114,7 @@ in
app_id = aid; app_id = aid;
inherit (app) groups; inherit (app) groups;
username = "u${toString fid}_a${toString aid}"; username = "u${toString fid}_a${toString aid}";
home = "${cfg.stateDir}/u${toString fid}/a${toString aid}"; home = "${cfg.stateDir}/${toString fid}/${toString aid}";
sandbox = { sandbox = {
inherit (app) inherit (app)
userns userns
@ -124,7 +123,6 @@ in
env env
; ;
map_real_uid = app.mapRealUid; map_real_uid = app.mapRealUid;
no_new_session = app.tty;
filesystem = filesystem =
[ [
{ src = "/bin"; } { src = "/bin"; }

View File

@ -36,7 +36,7 @@ package
*Default:* *Default:*
` <derivation fortify-0.2.8> ` ` <derivation fortify-0.1.0> `
@ -478,30 +478,6 @@ null or package
## environment\.fortify\.apps\.\*\.tty
Whether to enable allow access to the controlling terminal\.
*Type:*
boolean
*Default:*
` false `
*Example:*
` true `
## environment\.fortify\.apps\.\*\.userns ## environment\.fortify\.apps\.\*\.userns

View File

@ -31,6 +31,7 @@ in
let let
inherit (types) inherit (types)
str str
enum
bool bool
package package
anything anything
@ -132,7 +133,6 @@ in
userns = mkEnableOption "userns within the sandbox"; userns = mkEnableOption "userns within the sandbox";
mapRealUid = mkEnableOption "mapping to fortify's real UID within the sandbox"; mapRealUid = mkEnableOption "mapping to fortify's real UID within the sandbox";
dev = mkEnableOption "access to all devices 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" // { net = mkEnableOption "network access within the sandbox" // {
default = true; default = true;

View File

@ -4,23 +4,15 @@
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.8"; version = "0.2.1";
src = builtins.path { src = ./.;
name = "fortify-src";
path = lib.cleanSource ./.;
filter = path: type: !(type != "directory" && lib.hasSuffix ".nix" path);
};
vendorHash = null; vendorHash = null;
ldflags = ldflags =
@ -30,7 +22,7 @@ buildGoModule rec {
ldflags ldflags
++ [ ++ [
"-X" "-X"
"git.gensokyo.uk/security/fortify/internal.${name}=${value}" "git.ophivana.moe/security/fortify/internal.${name}=${value}"
] ]
) )
[ [
@ -47,29 +39,14 @@ buildGoModule rec {
Finit = "${placeholder "out"}/libexec/finit"; Finit = "${placeholder "out"}/libexec/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/*
mkdir "$out/libexec" mkdir "$out/libexec"
mv "$out"/bin/* "$out/libexec/" mv "$out"/bin/* "$out/libexec/"

108
parse.go
View File

@ -1,108 +0,0 @@
package main
import (
"encoding/json"
"errors"
"io"
direct "os"
"strconv"
"strings"
"syscall"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/state"
)
func tryPath(name string) (config *fst.Config) {
var r io.Reader
config = new(fst.Config)
if name != "-" {
r = tryFd(name)
if r == nil {
fmsg.VPrintln("load configuration from file")
if f, err := os.Open(name); err != nil {
fmsg.Fatalf("cannot access configuration file %q: %s", name, err)
panic("unreachable")
} else {
// finalizer closes f
r = f
}
} else {
defer func() {
if err := r.(io.ReadCloser).Close(); err != nil {
fmsg.Printf("cannot close config fd: %v", err)
}
}()
}
} else {
r = direct.Stdin
}
if err := json.NewDecoder(r).Decode(&config); err != nil {
fmsg.Fatalf("cannot load configuration: %v", err)
panic("unreachable")
}
return
}
func tryFd(name string) io.ReadCloser {
if v, err := strconv.Atoi(name); err != nil {
fmsg.VPrintf("name cannot be interpreted as int64: %v", err)
return nil
} else {
fd := uintptr(v)
if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
if errors.Is(errno, syscall.EBADF) {
return nil
}
fmsg.Fatalf("cannot get fd %d: %v", fd, errno)
}
return direct.NewFile(fd, strconv.Itoa(v))
}
}
func tryShort(name string) (config *fst.Config, instance *state.State) {
likePrefix := false
if len(name) <= 32 {
likePrefix = true
for _, c := range name {
if c >= '0' && c <= '9' {
continue
}
if c >= 'a' && c <= 'f' {
continue
}
likePrefix = false
break
}
}
// try to match from state store
if likePrefix && len(name) >= 8 {
fmsg.VPrintln("argument looks like prefix")
s := state.NewMulti(os.Paths().RunDirPath)
if entries, err := state.Join(s); err != nil {
fmsg.Printf("cannot join store: %v", err)
// drop to fetch from file
} else {
for id := range entries {
v := id.String()
if strings.HasPrefix(v, name) {
// match, use config from this state entry
instance = entries[id]
config = instance.Config
break
}
fmsg.VPrintf("instance %s skipped", v)
}
}
}
return
}

265
print.go
View File

@ -1,265 +0,0 @@
package main
import (
"encoding/json"
"fmt"
direct "os"
"slices"
"strconv"
"strings"
"text/tabwriter"
"time"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/state"
)
func printShow(instance *state.State, config *fst.Config, short bool) {
if flagJSON {
v := any(config)
if instance != nil {
v = instance
}
printJSON(v)
return
}
now := time.Now().UTC()
w := tabwriter.NewWriter(direct.Stdout, 0, 1, 4, ' ', 0)
if instance != nil {
fmt.Fprintf(w, "State\n")
fmt.Fprintf(w, " Instance:\t%s (%d)\n", instance.ID.String(), instance.PID)
fmt.Fprintf(w, " Uptime:\t%s\n", now.Sub(instance.Time).Round(time.Second).String())
fmt.Fprintf(w, "\n")
}
fmt.Fprintf(w, "App\n")
if config.ID != "" {
fmt.Fprintf(w, " ID:\t%d (%s)\n", config.Confinement.AppID, config.ID)
} else {
fmt.Fprintf(w, " ID:\t%d\n", config.Confinement.AppID)
}
fmt.Fprintf(w, " Enablements:\t%s\n", config.Confinement.Enablements.String())
if len(config.Confinement.Groups) > 0 {
fmt.Fprintf(w, " Groups:\t%q\n", config.Confinement.Groups)
}
fmt.Fprintf(w, " Directory:\t%s\n", config.Confinement.Outer)
if config.Confinement.Sandbox != nil {
sandbox := config.Confinement.Sandbox
if sandbox.Hostname != "" {
fmt.Fprintf(w, " Hostname:\t%q\n", sandbox.Hostname)
}
flags := make([]string, 0, 7)
writeFlag := func(name string, value bool) {
if value {
flags = append(flags, name)
}
}
writeFlag("userns", sandbox.UserNS)
writeFlag("net", sandbox.Net)
writeFlag("dev", sandbox.Dev)
writeFlag("tty", sandbox.NoNewSession)
writeFlag("mapuid", sandbox.MapRealUID)
writeFlag("directwl", sandbox.DirectWayland)
writeFlag("autoetc", sandbox.AutoEtc)
if len(flags) == 0 {
flags = append(flags, "none")
}
fmt.Fprintf(w, " Flags:\t%s\n", strings.Join(flags, " "))
etc := sandbox.Etc
if etc == "" {
etc = "/etc"
}
fmt.Fprintf(w, " Etc:\t%s\n", etc)
if len(sandbox.Override) > 0 {
fmt.Fprintf(w, " Overrides:\t%s\n", strings.Join(sandbox.Override, " "))
}
// Env map[string]string `json:"env"`
// Link [][2]string `json:"symlink"`
} else {
// this gets printed before everything else
fmt.Println("WARNING: current configuration uses permissive defaults!")
}
fmt.Fprintf(w, " Command:\t%s\n", strings.Join(config.Command, " "))
fmt.Fprintf(w, "\n")
if !short {
if config.Confinement.Sandbox != nil && len(config.Confinement.Sandbox.Filesystem) > 0 {
fmt.Fprintf(w, "Filesystem\n")
for _, f := range config.Confinement.Sandbox.Filesystem {
if f == nil {
continue
}
expr := new(strings.Builder)
expr.Grow(3 + len(f.Src) + 1 + len(f.Dst))
if f.Device {
expr.WriteString(" d")
} else if f.Write {
expr.WriteString(" w")
} else {
expr.WriteString(" ")
}
if f.Must {
expr.WriteString("*")
} else {
expr.WriteString("+")
}
expr.WriteString(f.Src)
if f.Dst != "" {
expr.WriteString(":" + f.Dst)
}
fmt.Fprintf(w, "%s\n", expr.String())
}
fmt.Fprintf(w, "\n")
}
if len(config.Confinement.ExtraPerms) > 0 {
fmt.Fprintf(w, "Extra ACL\n")
for _, p := range config.Confinement.ExtraPerms {
if p == nil {
continue
}
fmt.Fprintf(w, " %s\n", p.String())
}
fmt.Fprintf(w, "\n")
}
}
printDBus := func(c *dbus.Config) {
fmt.Fprintf(w, " Filter:\t%v\n", c.Filter)
if len(c.See) > 0 {
fmt.Fprintf(w, " See:\t%q\n", c.See)
}
if len(c.Talk) > 0 {
fmt.Fprintf(w, " Talk:\t%q\n", c.Talk)
}
if len(c.Own) > 0 {
fmt.Fprintf(w, " Own:\t%q\n", c.Own)
}
if len(c.Call) > 0 {
fmt.Fprintf(w, " Call:\t%q\n", c.Call)
}
if len(c.Broadcast) > 0 {
fmt.Fprintf(w, " Broadcast:\t%q\n", c.Broadcast)
}
}
if config.Confinement.SessionBus != nil {
fmt.Fprintf(w, "Session bus\n")
printDBus(config.Confinement.SessionBus)
fmt.Fprintf(w, "\n")
}
if config.Confinement.SystemBus != nil {
fmt.Fprintf(w, "System bus\n")
printDBus(config.Confinement.SystemBus)
fmt.Fprintf(w, "\n")
}
if err := w.Flush(); err != nil {
fmsg.Fatalf("cannot flush tabwriter: %v", err)
}
}
func printPs(short bool) {
now := time.Now().UTC()
var entries state.Entries
s := state.NewMulti(os.Paths().RunDirPath)
if e, err := state.Join(s); err != nil {
fmsg.Fatalf("cannot join store: %v", err)
} else {
entries = e
}
if err := s.Close(); err != nil {
fmsg.Printf("cannot close store: %v", err)
}
if flagJSON {
es := make(map[string]*state.State, len(entries))
for id, instance := range entries {
es[id.String()] = instance
}
printJSON(es)
return
}
// sort state entries by id string to ensure consistency between runs
exp := make([]*expandedStateEntry, 0, len(entries))
for id, instance := range entries {
// gracefully skip nil states
if instance == nil {
fmsg.Printf("got invalid state entry %s", id.String())
continue
}
// gracefully skip inconsistent states
if id != instance.ID {
fmt.Printf("possible store corruption: entry %s has id %s",
id.String(), instance.ID.String())
continue
}
exp = append(exp, &expandedStateEntry{s: id.String(), State: instance})
}
slices.SortFunc(exp, func(a, b *expandedStateEntry) int { return a.Time.Compare(b.Time) })
if short {
if flagJSON {
v := make([]string, len(exp))
for i, e := range exp {
v[i] = e.s
}
printJSON(v)
} else {
for _, e := range exp {
fmt.Println(e.s[:8])
}
}
return
}
// buffer output to reduce terminal activity
w := tabwriter.NewWriter(direct.Stdout, 0, 1, 4, ' ', 0)
fmt.Fprintln(w, "\tInstance\tPID\tApp\tUptime\tEnablements\tCommand")
for _, e := range exp {
printInstance(w, e, now)
}
if err := w.Flush(); err != nil {
fmsg.Fatalf("cannot flush tabwriter: %v", err)
}
}
type expandedStateEntry struct {
s string
*state.State
}
func printInstance(w *tabwriter.Writer, e *expandedStateEntry, now time.Time) {
var (
es = "(No confinement information)"
cs = "(No command information)"
as = "(No configuration information)"
)
if e.Config != nil {
es = e.Config.Confinement.Enablements.String()
cs = fmt.Sprintf("%q", e.Config.Command)
as = strconv.Itoa(e.Config.Confinement.AppID)
}
fmt.Fprintf(w, "\t%s\t%d\t%s\t%s\t%s\t%s\n",
e.s[:8], e.PID, as, now.Sub(e.Time).Round(time.Second).String(), strings.TrimPrefix(es, ", "), cs)
}
func printJSON(v any) {
encoder := json.NewEncoder(direct.Stdout)
encoder.SetIndent("", " ")
if err := encoder.Encode(v); err != nil {
fmsg.Fatalf("cannot serialise: %v", err)
panic("unreachable")
}
}

274
test.nix
View File

@ -1,274 +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 D-Bus tests:
libnotify
mako
# 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;
# For PulseAudio tests:
security.rtkit.enable = true;
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
jack.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", "")
swaymsg(f"exec fortify --json ps > '/tmp/{name}.json'")
machine.copy_from_vm(f"/tmp/{name}.json", "")
machine.screenshot(name)
def check_state(command, enablements):
instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 fortify --json ps"))
if len(instances) != 1:
raise Exception(f"unexpected state length {len(instances)}")
instance = next(iter(instances.values()))
if instance['config']['command'] != command:
raise Exception(f"unexpected command {instance['config']['command']}")
if instance['config']['confinement']['enablements'] != enablements:
raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
def fortify(command):
swaymsg(f"exec fortify {command}")
start_all()
machine.wait_for_unit("multi-user.target")
# Run fortify Go tests outside of nix build in the background:
machine.succeed("rm -rf /tmp/src && cp -a '${self.packages.${system}.fortify.src}' /tmp/src")
machine.succeed("fortify-fhs -c '(cd /tmp/src && go generate ./... && go test ./... && touch /tmp/success-gotest)' &> /tmp/gotest &")
# 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:
fortify('-v run --wayland --dbus notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-done')
machine.wait_for_file("/tmp/dbus-done")
collect_state_ui("dbus_notify_exited")
machine.succeed("pkill -9 mako")
# Start a terminal (foot) within fortify:
fortify("run --wayland foot")
wait_for_window("u0_a0@machine")
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-client")
collect_state_ui("foot_wayland_permissive")
check_state(["foot"], 1)
# 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:
swaymsg("exec foot $SHELL -c '(fortify run --wayland foot) & sleep 1 && fortify show --short $(fortify ps --short) && touch /tmp/ps-show-ok && cat'")
wait_for_window("u0_a0@machine")
machine.send_chars("clear; wayland-info && touch /tmp/success-client-term\n")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-client-term")
machine.wait_for_file("/tmp/ps-show-ok")
collect_state_ui("foot_wayland_permissive_term")
check_state(["foot"], 1)
machine.send_chars("exit\n")
wait_for_window("foot")
machine.send_key("ctrl-c")
machine.wait_until_fails("pgrep foot")
# Test PulseAudio (fortify does not support PipeWire yet):
fortify("run --wayland --pulse foot")
wait_for_window("u0_a0@machine")
machine.send_chars("clear; pactl info && touch /tmp/success-pulse\n")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-pulse")
collect_state_ui("pulse_wayland")
check_state(["foot"], 9)
machine.send_chars("exit\n")
machine.wait_until_fails("pgrep foot")
# Test XWayland (foot does not support X):
fortify("run -X alacritty")
wait_for_window("u0_a0@machine")
machine.send_chars("clear; 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")
check_state(["alacritty"], 2)
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"))
# Verify go test status:
machine.wait_for_file("/tmp/gotest")
print(machine.succeed("cat /tmp/gotest"))
machine.wait_for_file("/tmp/success-gotest")
'';
}

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)]
}

View File

@ -1,120 +0,0 @@
// Package wl implements Wayland security_context_v1 protocol.
package wl
import (
"errors"
"net"
"os"
"runtime"
"sync"
"syscall"
)
type Conn struct {
conn *net.UnixConn
done chan struct{}
doneOnce sync.Once
mu sync.Mutex
}
// Attach connects Conn to a wayland socket.
func (c *Conn) Attach(p string) (err error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.conn != nil {
return errors.New("attached")
}
c.conn, err = net.DialUnix("unix", nil, &net.UnixAddr{Name: p, Net: "unix"})
return
}
// Close releases resources and closes the connection to the wayland compositor.
func (c *Conn) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.done == nil {
return errors.New("no socket bound")
}
c.doneOnce.Do(func() {
c.done <- struct{}{}
<-c.done
})
// closed by wayland
runtime.SetFinalizer(c.conn, nil)
return nil
}
func (c *Conn) Bind(p, appID, instanceID string) (*os.File, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.conn == nil {
return nil, errors.New("not attached")
}
if c.done != nil {
return nil, errors.New("bound")
}
if rc, err := c.conn.SyscallConn(); err != nil {
// unreachable
return nil, err
} else {
c.done = make(chan struct{})
return bindRawConn(c.done, rc, p, appID, instanceID)
}
}
func bindRawConn(done chan struct{}, rc syscall.RawConn, p, appID, instanceID string) (*os.File, error) {
var syncPipe [2]*os.File
if r, w, err := os.Pipe(); err != nil {
return nil, err
} else {
syncPipe[0] = r
syncPipe[1] = w
}
setupDone := make(chan error, 1) // does not block with c.done
go func() {
if err := rc.Control(func(fd uintptr) {
// prevent runtime from closing the read end of sync fd
runtime.SetFinalizer(syncPipe[0], nil)
// allow the Bind method to return after setup
setupDone <- bind(fd, p, appID, instanceID, syncPipe[0].Fd())
close(setupDone)
// keep socket alive until done is requested
<-done
}); err != nil {
setupDone <- err
}
// notify Close that rc.Control has returned
close(done)
}()
// return write end of the pipe
return syncPipe[1], <-setupDone
}
func bind(fd uintptr, p, appID, instanceID string, syncFD uintptr) error {
// ensure p is available
if f, err := os.Create(p); err != nil {
return err
} else if err = f.Close(); err != nil {
return err
} else if err = os.Remove(p); err != nil {
return err
}
return bindWaylandFd(p, fd, appID, instanceID, syncFD)
}

137
xcb/c.go
View File

@ -1,124 +1,33 @@
package xcb package xcb
import ( import (
"runtime" "errors"
"unsafe"
) )
/* //#include <stdlib.h>
#cgo linux pkg-config: xcb //#include <xcb/xcb.h>
//#cgo linux LDFLAGS: -lxcb
#include <stdlib.h>
#include <xcb/xcb.h>
static int _go_xcb_change_hosts_checked(xcb_connection_t *c, uint8_t mode, uint8_t family, uint16_t address_len, const uint8_t *address) {
xcb_void_cookie_t cookie = xcb_change_hosts_checked(c, mode, family, address_len, address);
free((void *)address);
int errno = xcb_connection_has_error(c);
if (errno != 0)
return errno;
xcb_generic_error_t *e = xcb_request_check(c, cookie);
if (e != NULL) {
// don't want to deal with xcb errors
free((void *)e);
return -1;
}
return 0;
}
*/
import "C" import "C"
const ( func xcbHandleConnectionError(c *C.xcb_connection_t) error {
HostModeInsert = C.XCB_HOST_MODE_INSERT if errno := C.xcb_connection_has_error(c); errno != 0 {
HostModeDelete = C.XCB_HOST_MODE_DELETE switch errno {
case C.XCB_CONN_ERROR:
FamilyInternet = C.XCB_FAMILY_INTERNET return errors.New("connection error")
FamilyDecnet = C.XCB_FAMILY_DECNET case C.XCB_CONN_CLOSED_EXT_NOTSUPPORTED:
FamilyChaos = C.XCB_FAMILY_CHAOS return errors.New("extension not supported")
FamilyServerInterpreted = C.XCB_FAMILY_SERVER_INTERPRETED case C.XCB_CONN_CLOSED_MEM_INSUFFICIENT:
FamilyInternet6 = C.XCB_FAMILY_INTERNET_6 return errors.New("memory not available")
) case C.XCB_CONN_CLOSED_REQ_LEN_EXCEED:
return errors.New("request length exceeded")
type ( case C.XCB_CONN_CLOSED_PARSE_ERR:
HostMode = C.xcb_host_mode_t return errors.New("invalid display string")
Family = C.xcb_family_t case C.XCB_CONN_CLOSED_INVALID_SCREEN:
) return errors.New("server has no screen matching display")
default:
func (conn *connection) changeHostsChecked(mode HostMode, family Family, address string) error { return errors.New("generic X11 failure")
errno := C._go_xcb_change_hosts_checked( }
conn.c, } else {
C.uint8_t(mode),
C.uint8_t(family),
C.uint16_t(len(address)),
(*C.uint8_t)(unsafe.Pointer(C.CString(address))),
)
switch errno {
case 0:
return nil
case -1:
return ErrChangeHosts
default:
return &ConnectionError{errno}
}
}
type connection struct{ c *C.xcb_connection_t }
func connect() (*connection, error) {
conn := newConnection(C.xcb_connect(nil, nil))
return conn, conn.hasError()
}
func newConnection(c *C.xcb_connection_t) *connection {
conn := &connection{c}
runtime.SetFinalizer(conn, (*connection).disconnect)
return conn
}
const (
ConnError = C.XCB_CONN_ERROR
ConnClosedExtNotSupported = C.XCB_CONN_CLOSED_EXT_NOTSUPPORTED
ConnClosedMemInsufficient = C.XCB_CONN_CLOSED_MEM_INSUFFICIENT
ConnClosedReqLenExceed = C.XCB_CONN_CLOSED_REQ_LEN_EXCEED
ConnClosedParseErr = C.XCB_CONN_CLOSED_PARSE_ERR
ConnClosedInvalidScreen = C.XCB_CONN_CLOSED_INVALID_SCREEN
)
type ConnectionError struct{ errno C.int }
func (ce *ConnectionError) Error() string {
switch ce.errno {
case ConnError:
return "connection error"
case ConnClosedExtNotSupported:
return "extension not supported"
case ConnClosedMemInsufficient:
return "memory not available"
case ConnClosedReqLenExceed:
return "request length exceeded"
case ConnClosedParseErr:
return "invalid display string"
case ConnClosedInvalidScreen:
return "server has no screen matching display"
default:
return "generic X11 failure"
}
}
func (conn *connection) hasError() error {
errno := C.xcb_connection_has_error(conn.c)
if errno == 0 {
return nil return nil
} }
return &ConnectionError{errno}
}
func (conn *connection) disconnect() {
C.xcb_disconnect(conn.c)
// no need for a finalizer anymore
runtime.SetFinalizer(conn, nil)
} }

View File

@ -1,22 +1,63 @@
// Package xcb implements X11 ChangeHosts via libxcb. // Package xcb implements X11 ChangeHosts via libxcb.
package xcb package xcb
//#include <stdlib.h>
//#include <xcb/xcb.h>
//#cgo linux LDFLAGS: -lxcb
import "C"
import ( import (
"errors" "errors"
"unsafe"
) )
var ErrChangeHosts = errors.New("xcb_change_hosts() failed") const (
HostModeInsert = C.XCB_HOST_MODE_INSERT
HostModeDelete = C.XCB_HOST_MODE_DELETE
func ChangeHosts(mode HostMode, family Family, address string) error { FamilyInternet = C.XCB_FAMILY_INTERNET
var conn *connection FamilyDecnet = C.XCB_FAMILY_DECNET
FamilyChaos = C.XCB_FAMILY_CHAOS
FamilyServerInterpreted = C.XCB_FAMILY_SERVER_INTERPRETED
FamilyInternet6 = C.XCB_FAMILY_INTERNET_6
)
if c, err := connect(); err != nil { type ConnectionError struct {
c.disconnect() err error
return err }
} else {
defer c.disconnect() func (e *ConnectionError) Error() string {
conn = c return e.err.Error()
}
func (e *ConnectionError) Unwrap() error {
return e.err
}
var (
ErrChangeHosts = errors.New("xcb_change_hosts() failed")
)
func ChangeHosts(mode, family C.uint8_t, address string) error {
c := C.xcb_connect(nil, nil)
defer C.xcb_disconnect(c)
if err := xcbHandleConnectionError(c); err != nil {
return &ConnectionError{err}
} }
return conn.changeHostsChecked(mode, family, address) addr := C.CString(address)
cookie := C.xcb_change_hosts_checked(c, mode, family, C.ushort(len(address)), (*C.uchar)(unsafe.Pointer(addr)))
C.free(unsafe.Pointer(addr))
if err := xcbHandleConnectionError(c); err != nil {
return &ConnectionError{err}
}
e := C.xcb_request_check(c, cookie)
if e != nil {
defer C.free(unsafe.Pointer(e))
return ErrChangeHosts
}
return nil
} }