Compare commits

..

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

94 changed files with 1651 additions and 3182 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

6
.gitignore vendored
View File

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

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
)
if f, err := proc.Receive(init0.Env, &payload); err != nil {
if errors.Is(err, proc.ErrInvalid) {
fmsg.Fatal("invalid config descriptor")
}
if errors.Is(err, proc.ErrNotSet) {
fmsg.Fatal("FORTIFY_INIT not set") fmsg.Fatal("FORTIFY_INIT not set")
panic("unreachable")
} else {
if fd, err := strconv.Atoi(s); err != nil {
fmsg.Fatalf("cannot parse %q: %v", s, err)
panic("unreachable")
} else {
setup = os.NewFile(uintptr(fd), "setup")
if setup == nil {
fmsg.Fatal("invalid config descriptor")
panic("unreachable")
}
}
} }
fmsg.Fatalf("cannot decode init setup payload: %v", err) var payload init0.Payload
if err := gob.NewDecoder(setup).Decode(&payload); err != nil {
fmsg.Fatal("cannot decode init setup payload:", err)
panic("unreachable") panic("unreachable")
} else { } else {
fmsg.SetVerbose(payload.Verbose) fmsg.SetVerbose(payload.Verbose)
closeSetup = f
// child does not need to see this // child does not need to see this
if err = os.Unsetenv(init0.Env); err != nil { if err = os.Unsetenv(init0.Env); err != nil {
@ -85,13 +92,21 @@ func main() {
cmd.Args = payload.Argv cmd.Args = payload.Argv
cmd.Env = os.Environ() cmd.Env = os.Environ()
// pass wayland fd
if payload.WL != -1 {
if f := os.NewFile(uintptr(payload.WL), "wayland"); f != nil {
cmd.Env = append(cmd.Env, "WAYLAND_SOCKET="+strconv.Itoa(3+len(cmd.ExtraFiles)))
cmd.ExtraFiles = append(cmd.ExtraFiles, f)
}
}
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
fmsg.Fatalf("cannot start %q: %v", payload.Argv0, err) fmsg.Fatalf("cannot start %q: %v", payload.Argv0, err)
} }
fmsg.Suspend() fmsg.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,9 +18,25 @@ type Payload struct {
Exec [2]string Exec [2]string
// bwrap config // bwrap config
Bwrap *bwrap.Config Bwrap *bwrap.Config
// sync fd // whether to pass wayland fd
Sync *uintptr WL bool
// verbosity pass through // verbosity pass through
Verbose bool Verbose bool
} }
func (p *Payload) Serve(conn *net.UnixConn, wl *Wayland) error {
if err := gob.NewEncoder(conn).Encode(*p); err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot stream shim payload:")
}
if wl != nil {
if err := wl.WriteUnix(conn); err != nil {
return errors.Join(err, conn.Close())
}
}
return fmsg.WrapErrorSuffix(conn.Close(),
"cannot close setup connection:")
}

View File

@ -1,20 +1,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)
var encoder *gob.Encoder
if fd, e, err := proc.Setup(&s.cmd.ExtraFiles); err != nil {
return nil, fmsg.WrapErrorSuffix(err,
"cannot create shim setup pipe:")
} else {
encoder = e
s.cmd.Env = []string{ s.cmd.Env = []string{
shim0.Env + "=" + strconv.Itoa(fd), shim0.Env + "=" + s.socket,
"FORTIFY_APP_ID=" + s.aid, "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,18 +1,19 @@
package main package main
import ( import (
"encoding/gob"
"errors" "errors"
"net"
"os" "os"
"path" "path"
"strconv" "strconv"
"syscall" "syscall"
init0 "git.gensokyo.uk/security/fortify/cmd/finit/ipc" init0 "git.ophivana.moe/security/fortify/cmd/finit/ipc"
shim "git.gensokyo.uk/security/fortify/cmd/fshim/ipc" shim "git.ophivana.moe/security/fortify/cmd/fshim/ipc"
"git.gensokyo.uk/security/fortify/helper" "git.ophivana.moe/security/fortify/helper"
"git.gensokyo.uk/security/fortify/internal" "git.ophivana.moe/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/proc"
) )
// everything beyond this point runs as unconstrained target user // everything beyond this point runs as unconstrained target user
@ -37,6 +38,15 @@ func main() {
} }
} }
// lookup socket path from environment
var socketPath string
if s, ok := os.LookupEnv(shim.Env); !ok {
fmsg.Fatal("FORTIFY_SHIM not set")
panic("unreachable")
} else {
socketPath = s
}
// check path to finit // check path to finit
var finitPath string var finitPath string
if p, ok := internal.Path(internal.Finit); !ok { if p, ok := internal.Path(internal.Finit); !ok {
@ -45,38 +55,40 @@ func main() {
finitPath = p finitPath = p
} }
// receive setup payload // dial setup socket
var ( var conn *net.UnixConn
payload shim.Payload if c, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: socketPath, Net: "unix"}); err != nil {
closeSetup func() error fmsg.Fatal(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
} }
@ -104,14 +116,27 @@ 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))
go func() { extraFiles = append(extraFiles, r)
fmsg.VPrintln("transmitting config to init") fmsg.VPrintln("transmitting config to init")
if err = encoder.Encode(&ic); err != nil { go func() {
// stream config to pipe
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)
} }
}() }()
@ -143,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,51 +0,0 @@
#compdef fortify
_fortify_app() {
_path_files -g "*.(json|ftfy)"
}
_fortify_run() {
_arguments \
'--id[App ID, leave empty to disable security context app_id]:id' \
'-a[Fortify application ID]: :_numbers' \
'-g[Groups inherited by the app process]: :_groups' \
'-d[Application home directory]: :_files -/' \
'-u[Passwd name within sandbox]: :_users' \
'--wayland[Share Wayland socket]' \
'-X[Share X11 socket and allow connection]' \
'--dbus[Proxy D-Bus connection]' \
'--pulse[Share PulseAudio socket and cookie]' \
'--dbus-config[Path to D-Bus proxy config file]: :_files -g "*.json"' \
'--dbus-system[Path to system D-Bus proxy config file]: :_files -g "*.json"' \
'--mpris[Allow owning MPRIS D-Bus path]' \
'--dbus-log[Force logging in the D-Bus proxy]'
}
(( $+functions[_fortify_commands] )) || _fortify_commands()
{
local -a _fortify_cmds
_fortify_cmds=(
"app:Launch app defined by the specified config file"
"run:Configure and start a permissive default sandbox"
"ps:List active apps and their state"
"version:Show fortify version"
"license:Show full license text"
"template:Produce a config template"
"help:Show help message"
)
if (( CURRENT == 1 )); then
_describe -t commands 'fortify command' _fortify_cmds || compadd "$@"
else
local curcontext="$curcontext"
cmd="${${_fortify_cmds[(r)$words[1]:*]%%:*}}"
if (( $+functions[_fortify_$cmd] )); then
_fortify_$cmd
else
_message "no more options"
fi
fi
}
_arguments -C \
'-v[Verbose output]' \
'*::fortify command:_fortify_commands'

View File

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

View File

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

View File

@ -6,8 +6,8 @@ import (
"io" "io"
"sync" "sync"
"git.gensokyo.uk/security/fortify/helper" "git.ophivana.moe/security/fortify/helper"
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/helper/bwrap"
) )
// ProxyName is the file name or path to the proxy program. // ProxyName is the file name or path to the proxy program.

View File

@ -9,9 +9,9 @@ import (
"strconv" "strconv"
"strings" "strings"
"git.gensokyo.uk/security/fortify/helper" "git.ophivana.moe/security/fortify/helper"
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/ldd" "git.ophivana.moe/security/fortify/ldd"
) )
// Start launches the D-Bus proxy and sets up the Wait method. // Start launches the D-Bus proxy and sets up the Wait method.

View File

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

View File

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

1
dist/fsurc.default vendored
View File

@ -1 +0,0 @@
1000 0

10
dist/install.sh vendored
View File

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

19
dist/release.sh vendored
View File

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

View File

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

30
flake.lock generated
View File

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

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

@ -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,45 +289,79 @@ 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": "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/pulse-cookie", "PULSE_COOKIE": "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie",
"PULSE_SERVER": "unix:/run/user/1971/pulse/native", "PULSE_SERVER": "unix:/run/user/65534/pulse/native",
"SHELL": "/run/current-system/sw/bin/zsh", "SHELL": "/run/current-system/sw/bin/zsh",
"TERM": "xterm-256color", "TERM": "xterm-256color",
"USER": "u0_a1", "USER": "chronos",
"WAYLAND_DISPLAY": "/run/user/1971/wayland-0", "WAYLAND_DISPLAY": "/run/user/65534/wayland-0",
"XDG_RUNTIME_DIR": "/run/user/1971", "XDG_RUNTIME_DIR": "/run/user/65534",
"XDG_SESSION_CLASS": "user", "XDG_SESSION_CLASS": "user",
"XDG_SESSION_TYPE": "tty", "XDG_SESSION_TYPE": "tty",
}, },
Chmod: make(bwrap.ChmodConfig), Chmod: make(bwrap.ChmodConfig),
NewSession: true,
DieWithParent: true, DieWithParent: true,
AsInit: true, AsInit: true,
}).SetUID(1971).SetGID(1971). }).SetUID(65534).SetGID(65534).
Procfs("/proc"). Procfs("/proc").
Tmpfs("/fortify", 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", "/fortify/etc"). Bind("/etc", "/fortify/etc").
Symlink("/fortify/etc/alsa", "/etc/alsa"). Symlink("/fortify/etc/alsa", "/etc/alsa").
@ -208,18 +442,160 @@ var testCasesNixos = []sealTestCase{
Symlink("/fortify/etc/zprofile", "/etc/zprofile"). Symlink("/fortify/etc/zprofile", "/etc/zprofile").
Symlink("/fortify/etc/zshenv", "/etc/zshenv"). Symlink("/fortify/etc/zshenv", "/etc/zshenv").
Symlink("/fortify/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("/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", "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/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("/fortify", 4096).
DevTmpfs("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", false, true).
Bind("/boot", "/boot", false, true).
Bind("/home", "/home", false, true).
Bind("/lib", "/lib", false, true).
Bind("/lib64", "/lib64", false, true).
Bind("/nix", "/nix", false, true).
Bind("/root", "/root", false, true).
Bind("/srv", "/srv", false, true).
Bind("/sys", "/sys", false, true).
Bind("/usr", "/usr", false, true).
Bind("/var", "/var", false, true).
Bind("/run/agetty.reload", "/run/agetty.reload", false, true).
Bind("/run/binfmt", "/run/binfmt", false, true).
Bind("/run/booted-system", "/run/booted-system", false, true).
Bind("/run/credentials", "/run/credentials", false, true).
Bind("/run/cryptsetup", "/run/cryptsetup", false, true).
Bind("/run/current-system", "/run/current-system", false, true).
Bind("/run/host", "/run/host", false, true).
Bind("/run/keys", "/run/keys", false, true).
Bind("/run/libvirt", "/run/libvirt", false, true).
Bind("/run/libvirtd.pid", "/run/libvirtd.pid", false, true).
Bind("/run/lock", "/run/lock", false, true).
Bind("/run/log", "/run/log", false, true).
Bind("/run/lvm", "/run/lvm", false, true).
Bind("/run/mount", "/run/mount", false, true).
Bind("/run/NetworkManager", "/run/NetworkManager", false, true).
Bind("/run/nginx", "/run/nginx", false, true).
Bind("/run/nixos", "/run/nixos", false, true).
Bind("/run/nscd", "/run/nscd", false, true).
Bind("/run/opengl-driver", "/run/opengl-driver", false, true).
Bind("/run/pppd", "/run/pppd", false, true).
Bind("/run/resolvconf", "/run/resolvconf", false, true).
Bind("/run/sddm", "/run/sddm", false, true).
Bind("/run/store", "/run/store", false, true).
Bind("/run/syncoid", "/run/syncoid", false, true).
Bind("/run/system", "/run/system", false, true).
Bind("/run/systemd", "/run/systemd", false, true).
Bind("/run/tmpfiles.d", "/run/tmpfiles.d", false, true).
Bind("/run/udev", "/run/udev", false, true).
Bind("/run/udisks2", "/run/udisks2", false, true).
Bind("/run/utmp", "/run/utmp", false, true).
Bind("/run/virtlogd.pid", "/run/virtlogd.pid", false, true).
Bind("/run/wrappers", "/run/wrappers", false, true).
Bind("/run/zed.pid", "/run/zed.pid", false, true).
Bind("/run/zed.state", "/run/zed.state", false, true).
Bind("/etc", "/fortify/etc").
Symlink("/fortify/etc/alsa", "/etc/alsa").
Symlink("/fortify/etc/bashrc", "/etc/bashrc").
Symlink("/fortify/etc/binfmt.d", "/etc/binfmt.d").
Symlink("/fortify/etc/dbus-1", "/etc/dbus-1").
Symlink("/fortify/etc/default", "/etc/default").
Symlink("/fortify/etc/ethertypes", "/etc/ethertypes").
Symlink("/fortify/etc/fonts", "/etc/fonts").
Symlink("/fortify/etc/fstab", "/etc/fstab").
Symlink("/fortify/etc/fuse.conf", "/etc/fuse.conf").
Symlink("/fortify/etc/host.conf", "/etc/host.conf").
Symlink("/fortify/etc/hostid", "/etc/hostid").
Symlink("/fortify/etc/hostname", "/etc/hostname").
Symlink("/fortify/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink("/fortify/etc/hosts", "/etc/hosts").
Symlink("/fortify/etc/inputrc", "/etc/inputrc").
Symlink("/fortify/etc/ipsec.d", "/etc/ipsec.d").
Symlink("/fortify/etc/issue", "/etc/issue").
Symlink("/fortify/etc/kbd", "/etc/kbd").
Symlink("/fortify/etc/libblockdev", "/etc/libblockdev").
Symlink("/fortify/etc/locale.conf", "/etc/locale.conf").
Symlink("/fortify/etc/localtime", "/etc/localtime").
Symlink("/fortify/etc/login.defs", "/etc/login.defs").
Symlink("/fortify/etc/lsb-release", "/etc/lsb-release").
Symlink("/fortify/etc/lvm", "/etc/lvm").
Symlink("/fortify/etc/machine-id", "/etc/machine-id").
Symlink("/fortify/etc/man_db.conf", "/etc/man_db.conf").
Symlink("/fortify/etc/modprobe.d", "/etc/modprobe.d").
Symlink("/fortify/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab").
Symlink("/fortify/etc/nanorc", "/etc/nanorc").
Symlink("/fortify/etc/netgroup", "/etc/netgroup").
Symlink("/fortify/etc/NetworkManager", "/etc/NetworkManager").
Symlink("/fortify/etc/nix", "/etc/nix").
Symlink("/fortify/etc/nixos", "/etc/nixos").
Symlink("/fortify/etc/NIXOS", "/etc/NIXOS").
Symlink("/fortify/etc/nscd.conf", "/etc/nscd.conf").
Symlink("/fortify/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink("/fortify/etc/opensnitchd", "/etc/opensnitchd").
Symlink("/fortify/etc/os-release", "/etc/os-release").
Symlink("/fortify/etc/pam", "/etc/pam").
Symlink("/fortify/etc/pam.d", "/etc/pam.d").
Symlink("/fortify/etc/pipewire", "/etc/pipewire").
Symlink("/fortify/etc/pki", "/etc/pki").
Symlink("/fortify/etc/polkit-1", "/etc/polkit-1").
Symlink("/fortify/etc/profile", "/etc/profile").
Symlink("/fortify/etc/protocols", "/etc/protocols").
Symlink("/fortify/etc/qemu", "/etc/qemu").
Symlink("/fortify/etc/resolv.conf", "/etc/resolv.conf").
Symlink("/fortify/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink("/fortify/etc/rpc", "/etc/rpc").
Symlink("/fortify/etc/samba", "/etc/samba").
Symlink("/fortify/etc/sddm.conf", "/etc/sddm.conf").
Symlink("/fortify/etc/secureboot", "/etc/secureboot").
Symlink("/fortify/etc/services", "/etc/services").
Symlink("/fortify/etc/set-environment", "/etc/set-environment").
Symlink("/fortify/etc/shadow", "/etc/shadow").
Symlink("/fortify/etc/shells", "/etc/shells").
Symlink("/fortify/etc/ssh", "/etc/ssh").
Symlink("/fortify/etc/ssl", "/etc/ssl").
Symlink("/fortify/etc/static", "/etc/static").
Symlink("/fortify/etc/subgid", "/etc/subgid").
Symlink("/fortify/etc/subuid", "/etc/subuid").
Symlink("/fortify/etc/sudoers", "/etc/sudoers").
Symlink("/fortify/etc/sysctl.d", "/etc/sysctl.d").
Symlink("/fortify/etc/systemd", "/etc/systemd").
Symlink("/fortify/etc/terminfo", "/etc/terminfo").
Symlink("/fortify/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink("/fortify/etc/udev", "/etc/udev").
Symlink("/fortify/etc/udisks2", "/etc/udisks2").
Symlink("/fortify/etc/UPower", "/etc/UPower").
Symlink("/fortify/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink("/fortify/etc/X11", "/etc/X11").
Symlink("/fortify/etc/zfs", "/etc/zfs").
Symlink("/fortify/etc/zinputrc", "/etc/zinputrc").
Symlink("/fortify/etc/zoneinfo", "/etc/zoneinfo").
Symlink("/fortify/etc/zprofile", "/etc/zprofile").
Symlink("/fortify/etc/zshenv", "/etc/zshenv").
Symlink("/fortify/etc/zshrc", "/etc/zshrc").
Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", false, true).
Tmpfs("/tmp/fortify.1971", 1048576).
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/65534", 8388608).
Bind("/home/chronos", "/home/chronos", false, true).
Bind("/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/passwd", "/etc/passwd").
Bind("/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/group", "/etc/group").
Tmpfs("/var/run/nscd", 8192),
},
{
"nixos permissive defaults chromium", new(stubNixOS),
&fst.Config{
ID: "org.chromium.Chromium",
Command: []string{"/run/current-system/sw/bin/zsh", "-c", "exec chromium "},
Confinement: fst.ConfinementConfig{
AppID: 9,
Groups: []string{"video"},
Username: "chronos",
Outer: "/home/chronos",
SessionBus: &dbus.Config{
Talk: []string{
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{
"org.freedesktop.portal.*": "*",
},
Broadcast: map[string]string{
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
},
Filter: true,
},
SystemBus: &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
},
Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(),
},
},
fst.ID{
0xeb, 0xf0, 0x83, 0xd1,
0xb1, 0x75, 0x91, 0x17,
0x82, 0xd4, 0x13, 0x36,
0x9b, 0x64, 0xce, 0x7c,
},
system.New(1000009).
Ensure("/tmp/fortify.1971", 0711).
Ephemeral(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0711).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/9", acl.Read, acl.Write, acl.Execute).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", acl.Execute).
WriteType(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/passwd", "chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n").
WriteType(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/group", "fortify:x:65534:\n").
Ensure("/tmp/fortify.1971/wayland", 0711).
Wayland("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse").
CopyFile("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie").
MustProxyDBus("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", &dbus.Config{
Talk: []string{
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{
"org.freedesktop.portal.*": "*",
},
Broadcast: map[string]string{
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
},
Filter: true,
}, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
}).
UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
(&bwrap.Config{
Net: true,
UserNS: true,
Chdir: "/home/chronos",
Clearenv: true,
SetEnv: map[string]string{
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/65534/bus",
"DBUS_SYSTEM_BUS_ADDRESS": "unix:path=/run/dbus/system_bus_socket",
"HOME": "/home/chronos",
"PULSE_COOKIE": "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie",
"PULSE_SERVER": "unix:/run/user/65534/pulse/native",
"SHELL": "/run/current-system/sw/bin/zsh",
"TERM": "xterm-256color",
"USER": "chronos",
"WAYLAND_DISPLAY": "/run/user/65534/wayland-0",
"XDG_RUNTIME_DIR": "/run/user/65534",
"XDG_SESSION_CLASS": "user",
"XDG_SESSION_TYPE": "tty",
},
Chmod: make(bwrap.ChmodConfig),
DieWithParent: true,
AsInit: true,
}).SetUID(65534).SetGID(65534).
Procfs("/proc").
Tmpfs("/fortify", 4096).
DevTmpfs("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", false, true).
Bind("/boot", "/boot", false, true).
Bind("/home", "/home", false, true).
Bind("/lib", "/lib", false, true).
Bind("/lib64", "/lib64", false, true).
Bind("/nix", "/nix", false, true).
Bind("/root", "/root", false, true).
Bind("/srv", "/srv", false, true).
Bind("/sys", "/sys", false, true).
Bind("/usr", "/usr", false, true).
Bind("/var", "/var", false, true).
Bind("/run/agetty.reload", "/run/agetty.reload", false, true).
Bind("/run/binfmt", "/run/binfmt", false, true).
Bind("/run/booted-system", "/run/booted-system", false, true).
Bind("/run/credentials", "/run/credentials", false, true).
Bind("/run/cryptsetup", "/run/cryptsetup", false, true).
Bind("/run/current-system", "/run/current-system", false, true).
Bind("/run/host", "/run/host", false, true).
Bind("/run/keys", "/run/keys", false, true).
Bind("/run/libvirt", "/run/libvirt", false, true).
Bind("/run/libvirtd.pid", "/run/libvirtd.pid", false, true).
Bind("/run/lock", "/run/lock", false, true).
Bind("/run/log", "/run/log", false, true).
Bind("/run/lvm", "/run/lvm", false, true).
Bind("/run/mount", "/run/mount", false, true).
Bind("/run/NetworkManager", "/run/NetworkManager", false, true).
Bind("/run/nginx", "/run/nginx", false, true).
Bind("/run/nixos", "/run/nixos", false, true).
Bind("/run/nscd", "/run/nscd", false, true).
Bind("/run/opengl-driver", "/run/opengl-driver", false, true).
Bind("/run/pppd", "/run/pppd", false, true).
Bind("/run/resolvconf", "/run/resolvconf", false, true).
Bind("/run/sddm", "/run/sddm", false, true).
Bind("/run/store", "/run/store", false, true).
Bind("/run/syncoid", "/run/syncoid", false, true).
Bind("/run/system", "/run/system", false, true).
Bind("/run/systemd", "/run/systemd", false, true).
Bind("/run/tmpfiles.d", "/run/tmpfiles.d", false, true).
Bind("/run/udev", "/run/udev", false, true).
Bind("/run/udisks2", "/run/udisks2", false, true).
Bind("/run/utmp", "/run/utmp", false, true).
Bind("/run/virtlogd.pid", "/run/virtlogd.pid", false, true).
Bind("/run/wrappers", "/run/wrappers", false, true).
Bind("/run/zed.pid", "/run/zed.pid", false, true).
Bind("/run/zed.state", "/run/zed.state", false, true).
Bind("/dev/dri", "/dev/dri", true, true, true).
Bind("/etc", "/fortify/etc").
Symlink("/fortify/etc/alsa", "/etc/alsa").
Symlink("/fortify/etc/bashrc", "/etc/bashrc").
Symlink("/fortify/etc/binfmt.d", "/etc/binfmt.d").
Symlink("/fortify/etc/dbus-1", "/etc/dbus-1").
Symlink("/fortify/etc/default", "/etc/default").
Symlink("/fortify/etc/ethertypes", "/etc/ethertypes").
Symlink("/fortify/etc/fonts", "/etc/fonts").
Symlink("/fortify/etc/fstab", "/etc/fstab").
Symlink("/fortify/etc/fuse.conf", "/etc/fuse.conf").
Symlink("/fortify/etc/host.conf", "/etc/host.conf").
Symlink("/fortify/etc/hostid", "/etc/hostid").
Symlink("/fortify/etc/hostname", "/etc/hostname").
Symlink("/fortify/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink("/fortify/etc/hosts", "/etc/hosts").
Symlink("/fortify/etc/inputrc", "/etc/inputrc").
Symlink("/fortify/etc/ipsec.d", "/etc/ipsec.d").
Symlink("/fortify/etc/issue", "/etc/issue").
Symlink("/fortify/etc/kbd", "/etc/kbd").
Symlink("/fortify/etc/libblockdev", "/etc/libblockdev").
Symlink("/fortify/etc/locale.conf", "/etc/locale.conf").
Symlink("/fortify/etc/localtime", "/etc/localtime").
Symlink("/fortify/etc/login.defs", "/etc/login.defs").
Symlink("/fortify/etc/lsb-release", "/etc/lsb-release").
Symlink("/fortify/etc/lvm", "/etc/lvm").
Symlink("/fortify/etc/machine-id", "/etc/machine-id").
Symlink("/fortify/etc/man_db.conf", "/etc/man_db.conf").
Symlink("/fortify/etc/modprobe.d", "/etc/modprobe.d").
Symlink("/fortify/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab").
Symlink("/fortify/etc/nanorc", "/etc/nanorc").
Symlink("/fortify/etc/netgroup", "/etc/netgroup").
Symlink("/fortify/etc/NetworkManager", "/etc/NetworkManager").
Symlink("/fortify/etc/nix", "/etc/nix").
Symlink("/fortify/etc/nixos", "/etc/nixos").
Symlink("/fortify/etc/NIXOS", "/etc/NIXOS").
Symlink("/fortify/etc/nscd.conf", "/etc/nscd.conf").
Symlink("/fortify/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink("/fortify/etc/opensnitchd", "/etc/opensnitchd").
Symlink("/fortify/etc/os-release", "/etc/os-release").
Symlink("/fortify/etc/pam", "/etc/pam").
Symlink("/fortify/etc/pam.d", "/etc/pam.d").
Symlink("/fortify/etc/pipewire", "/etc/pipewire").
Symlink("/fortify/etc/pki", "/etc/pki").
Symlink("/fortify/etc/polkit-1", "/etc/polkit-1").
Symlink("/fortify/etc/profile", "/etc/profile").
Symlink("/fortify/etc/protocols", "/etc/protocols").
Symlink("/fortify/etc/qemu", "/etc/qemu").
Symlink("/fortify/etc/resolv.conf", "/etc/resolv.conf").
Symlink("/fortify/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink("/fortify/etc/rpc", "/etc/rpc").
Symlink("/fortify/etc/samba", "/etc/samba").
Symlink("/fortify/etc/sddm.conf", "/etc/sddm.conf").
Symlink("/fortify/etc/secureboot", "/etc/secureboot").
Symlink("/fortify/etc/services", "/etc/services").
Symlink("/fortify/etc/set-environment", "/etc/set-environment").
Symlink("/fortify/etc/shadow", "/etc/shadow").
Symlink("/fortify/etc/shells", "/etc/shells").
Symlink("/fortify/etc/ssh", "/etc/ssh").
Symlink("/fortify/etc/ssl", "/etc/ssl").
Symlink("/fortify/etc/static", "/etc/static").
Symlink("/fortify/etc/subgid", "/etc/subgid").
Symlink("/fortify/etc/subuid", "/etc/subuid").
Symlink("/fortify/etc/sudoers", "/etc/sudoers").
Symlink("/fortify/etc/sysctl.d", "/etc/sysctl.d").
Symlink("/fortify/etc/systemd", "/etc/systemd").
Symlink("/fortify/etc/terminfo", "/etc/terminfo").
Symlink("/fortify/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink("/fortify/etc/udev", "/etc/udev").
Symlink("/fortify/etc/udisks2", "/etc/udisks2").
Symlink("/fortify/etc/UPower", "/etc/UPower").
Symlink("/fortify/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink("/fortify/etc/X11", "/etc/X11").
Symlink("/fortify/etc/zfs", "/etc/zfs").
Symlink("/fortify/etc/zinputrc", "/etc/zinputrc").
Symlink("/fortify/etc/zoneinfo", "/etc/zoneinfo").
Symlink("/fortify/etc/zprofile", "/etc/zprofile").
Symlink("/fortify/etc/zshenv", "/etc/zshenv").
Symlink("/fortify/etc/zshrc", "/etc/zshrc").
Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", false, true).
Tmpfs("/tmp/fortify.1971", 1048576).
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/65534", 8388608).
Bind("/home/chronos", "/home/chronos", false, true).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/passwd", "/etc/passwd").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/group", "/etc/group").
Bind("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/65534/wayland-0").
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie", "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket").
Tmpfs("/var/run/nscd", 8192),
},
}

View File

@ -1,153 +0,0 @@
package app_test
import (
"fmt"
"io"
"io/fs"
"os/user"
"strconv"
"git.gensokyo.uk/security/fortify/internal/linux"
)
// fs methods are not implemented using a real FS
// to help better understand filesystem access behaviour
type stubNixOS struct {
lookPathErr map[string]error
usernameErr map[string]error
}
func (s *stubNixOS) Geteuid() int {
return 1971
}
func (s *stubNixOS) LookupEnv(key string) (string, bool) {
switch key {
case "SHELL":
return "/run/current-system/sw/bin/zsh", true
case "TERM":
return "xterm-256color", true
case "WAYLAND_DISPLAY":
return "wayland-0", true
case "PULSE_COOKIE":
return "", false
case "HOME":
return "/home/ophestra", true
case "XDG_CONFIG_HOME":
return "/home/ophestra/xdg/config", true
default:
panic(fmt.Sprintf("attempted to access unexpected environment variable %q", key))
}
}
func (s *stubNixOS) TempDir() string {
return "/tmp"
}
func (s *stubNixOS) LookPath(file string) (string, error) {
if s.lookPathErr != nil {
if err, ok := s.lookPathErr[file]; ok {
return "", err
}
}
switch file {
case "sudo":
return "/run/wrappers/bin/sudo", nil
case "machinectl":
return "/home/ophestra/.nix-profile/bin/machinectl", nil
default:
panic(fmt.Sprintf("attempted to look up unexpected executable %q", file))
}
}
func (s *stubNixOS) Executable() (string, error) {
return "/home/ophestra/.nix-profile/bin/fortify", nil
}
func (s *stubNixOS) LookupGroup(name string) (*user.Group, error) {
switch name {
case "video":
return &user.Group{Gid: "26", Name: "video"}, nil
default:
return nil, user.UnknownGroupError(name)
}
}
func (s *stubNixOS) ReadDir(name string) ([]fs.DirEntry, error) {
switch name {
case "/":
return stubDirEntries("bin", "boot", "dev", "etc", "home", "lib",
"lib64", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var")
case "/run":
return stubDirEntries("agetty.reload", "binfmt", "booted-system",
"credentials", "cryptsetup", "current-system", "dbus", "host", "keys",
"libvirt", "libvirtd.pid", "lock", "log", "lvm", "mount", "NetworkManager",
"nginx", "nixos", "nscd", "opengl-driver", "pppd", "resolvconf", "sddm",
"store", "syncoid", "system", "systemd", "tmpfiles.d", "udev", "udisks2",
"user", "utmp", "virtlogd.pid", "wrappers", "zed.pid", "zed.state")
case "/etc":
return stubDirEntries("alsa", "bashrc", "binfmt.d", "dbus-1", "default",
"ethertypes", "fonts", "fstab", "fuse.conf", "group", "host.conf", "hostid",
"hostname", "hostname.CHECKSUM", "hosts", "inputrc", "ipsec.d", "issue", "kbd",
"libblockdev", "locale.conf", "localtime", "login.defs", "lsb-release", "lvm",
"machine-id", "man_db.conf", "modprobe.d", "modules-load.d", "mtab", "nanorc",
"netgroup", "NetworkManager", "nix", "nixos", "NIXOS", "nscd.conf", "nsswitch.conf",
"opensnitchd", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1",
"profile", "protocols", "qemu", "resolv.conf", "resolvconf.conf", "rpc", "samba",
"sddm.conf", "secureboot", "services", "set-environment", "shadow", "shells", "ssh",
"ssl", "static", "subgid", "subuid", "sudoers", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "udisks2", "UPower", "vconsole.conf", "X11", "zfs", "zinputrc",
"zoneinfo", "zprofile", "zshenv", "zshrc")
default:
panic(fmt.Sprintf("attempted to read unexpected directory %q", name))
}
}
func (s *stubNixOS) Stat(name string) (fs.FileInfo, error) {
switch name {
case "/var/run/nscd":
return nil, nil
case "/run/user/1971/pulse":
return nil, nil
case "/run/user/1971/pulse/native":
return stubFileInfoMode(0666), nil
case "/home/ophestra/.pulse-cookie":
return stubFileInfoIsDir(true), nil
case "/home/ophestra/xdg/config/pulse/cookie":
return stubFileInfoIsDir(false), nil
default:
panic(fmt.Sprintf("attempted to stat unexpected path %q", name))
}
}
func (s *stubNixOS) Open(name string) (fs.File, error) {
switch name {
default:
panic(fmt.Sprintf("attempted to open unexpected file %q", name))
}
}
func (s *stubNixOS) Exit(code int) {
panic("called exit on stub with code " + strconv.Itoa(code))
}
func (s *stubNixOS) Stdout() io.Writer {
panic("requested stdout")
}
func (s *stubNixOS) Paths() linux.Paths {
return linux.Paths{
SharePath: "/tmp/fortify.1971",
RuntimePath: "/run/user/1971",
RunDirPath: "/run/user/1971/fortify",
}
}
func (s *stubNixOS) Uid(aid int) (int, error) {
return 1000000 + 0*10000 + aid, nil
}
func (s *stubNixOS) SdBooted() bool {
return true
}

View File

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

View File

@ -1,12 +1,12 @@
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 fTmp = "/fortify" const fTmp = "/fortify"
@ -62,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"`
@ -196,7 +196,7 @@ func Template() *Config {
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{

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

@ -5,30 +5,28 @@ import (
"fmt" "fmt"
"io/fs" "io/fs"
"path" "path"
"regexp"
"strconv" "strconv"
"git.gensokyo.uk/security/fortify/dbus" shim "git.ophivana.moe/security/fortify/cmd/fshim/ipc"
"git.gensokyo.uk/security/fortify/fst" "git.ophivana.moe/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/state" "git.ophivana.moe/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
var ( var (
ErrConfig = errors.New("no configuration to seal") ErrConfig = errors.New("no configuration to seal")
ErrUser = errors.New("invalid aid") ErrUser = errors.New("invalid aid")
ErrHome = errors.New("invalid home directory") ErrHome = errors.New("invalid home directory")
ErrName = errors.New("invalid username")
) )
var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z0-9_-]{0,30}\\$)$")
// appSeal seals the application with child-related information // appSeal seals the application with child-related information
type appSeal struct { type appSeal struct {
// app unique ID string representation // app unique ID string representation
id string id string
// wayland mediation, disabled if nil
wl *shim.Wayland
// dbus proxy message buffer retriever // dbus proxy message buffer retriever
dbusMsg func(f func(msgbuf []string)) dbusMsg func(f func(msgbuf []string))
@ -46,8 +44,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
// prevents sharing from happening twice // prevents sharing from happening twice
shared bool shared bool
@ -60,7 +56,7 @@ type appSeal struct {
} }
// Seal seals the app launch context // Seal seals the app launch context
func (a *app) Seal(config *fst.Config) error { func (a *app) Seal(config *Config) error {
a.lock.Lock() a.lock.Lock()
defer a.lock.Unlock() defer a.lock.Unlock()
@ -110,9 +106,6 @@ func (a *app) Seal(config *fst.Config) error {
} }
if seal.sys.user.username == "" { if seal.sys.user.username == "" {
seal.sys.user.username = "chronos" 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) { if seal.sys.user.data == "" || !path.IsAbs(seal.sys.user.data) {
return fmsg.WrapError(ErrHome, return fmsg.WrapError(ErrHome,
@ -148,7 +141,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,
@ -158,7 +151,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 {
@ -170,7 +163,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...)
@ -179,7 +172,7 @@ func (a *app) Seal(config *fst.Config) error {
if d, err := a.os.ReadDir("/run"); err != nil { if d, err := a.os.ReadDir("/run"); err != nil {
return err return err
} else { } else {
b := make([]*fst.FilesystemConfig, 0, len(d)) b := make([]*FilesystemConfig, 0, len(d))
for _, ent := range d { for _, ent := range d {
name := ent.Name() name := ent.Name()
switch name { switch name {
@ -187,7 +180,7 @@ func (a *app) Seal(config *fst.Config) error {
case "dbus": case "dbus":
default: default:
p := "/run/" + name p := "/run/" + name
b = append(b, &fst.FilesystemConfig{Src: p, Write: true, Must: true}) b = append(b, &FilesystemConfig{Src: p, Write: true, Must: true})
} }
} }
conf.Filesystem = append(conf.Filesystem, b...) conf.Filesystem = append(conf.Filesystem, b...)
@ -199,12 +192,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})
} }
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 {
@ -215,10 +207,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)
@ -237,6 +235,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

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

View File

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

View File

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

View File

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

View File

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

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,10 +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,
WL: a.seal.wl != nil,
Verbose: fmsg.Verbose(), Verbose: fmsg.Verbose(),
}, },
@ -60,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,
Config: a.ct.Unwrap(), Command: a.seal.command,
Capability: a.seal.et,
Argv: a.shim.Unwrap().Args,
Time: *startTime, Time: *startTime,
} }
// register process state // register process state
var err0 = new(StateStoreError) var err0 = new(StateStoreError)
err0.Inner, err0.DoErr = a.seal.store.Do(a.seal.sys.user.aid, func(c state.Cursor) { err0.Inner, err0.DoErr = a.seal.store.Do(func(b state.Backend) {
err0.InnerErr = c.Save(&sd) err0.InnerErr = b.Save(&sd)
}) })
a.seal.sys.saveState = true a.seal.sys.saveState = true
return err0.equiv("cannot save process state:") return err0.equiv("cannot save process state:")
@ -197,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
} }
} }
@ -224,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
@ -250,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,10 +1,10 @@
package app package app
import ( import (
"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"
) )
// appSealSys encapsulates app seal behaviour with OS interactions // appSealSys encapsulates app seal behaviour with OS interactions

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

67
main.go
View File

@ -8,17 +8,15 @@ import (
"os/user" "os/user"
"strconv" "strconv"
"strings" "strings"
"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 (
@ -103,7 +101,7 @@ 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
if s, err := json.MarshalIndent(fst.Template(), "", " "); err != nil { if s, err := json.MarshalIndent(app.Template(), "", " "); err != nil {
fmsg.Fatalf("cannot generate template: %v", err) fmsg.Fatalf("cannot generate template: %v", err)
panic("unreachable") panic("unreachable")
} else { } else {
@ -130,7 +128,7 @@ func main() {
fmsg.Fatal("app requires at least 1 argument") fmsg.Fatal("app requires at least 1 argument")
} }
config := new(fst.Config) config := new(app.Config)
if f, err := os.Open(args[1]); err != nil { if f, err := os.Open(args[1]); err != nil {
fmsg.Fatalf("cannot access config file %q: %s", args[1], err) fmsg.Fatalf("cannot access config file %q: %s", args[1], err)
panic("unreachable") panic("unreachable")
@ -150,10 +148,10 @@ func main() {
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
@ -163,15 +161,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")
@ -180,8 +178,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(),
} }
@ -190,42 +188,21 @@ func main() {
panic("unreachable") panic("unreachable")
} }
// resolve home/username from os when flag is unset // resolve home directory from os when flag is unset
var ( if homeDir == "os" {
passwd *user.User
passwdOnce sync.Once
passwdFunc = func() {
var us string var us string
if uid, err := os.Uid(aid); err != nil { if uid, err := os.Uid(aid); err != nil {
fmsg.Fatalf("cannot obtain uid from fsu: %v", err) fmsg.Fatalf("cannot obtain uid from fsu: %v", err)
} else { } else {
us = strconv.Itoa(uid) us = strconv.Itoa(uid)
} }
if u, err := user.LookupId(us); err != nil { if u, err := user.LookupId(us); err != nil {
fmsg.VPrintf("cannot look up uid %s", us) fmsg.VPrintf("cannot look up uid %s", us)
passwd = &user.User{ homeDir = "/var/empty"
Uid: us,
Gid: us,
Username: "chronos",
Name: "Fortify",
HomeDir: "/var/empty",
}
} else { } else {
passwd = u homeDir = u.HomeDir
} }
} }
)
if homeDir == "os" {
passwdOnce.Do(passwdFunc)
homeDir = passwd.HomeDir
}
if userName == "chronos" {
passwdOnce.Do(passwdFunc)
userName = passwd.Username
}
config.Confinement.AppID = aid config.Confinement.AppID = aid
config.Confinement.Groups = groups config.Confinement.Groups = groups
@ -242,7 +219,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)
@ -276,7 +253,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.5> ` ` <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,17 +4,13 @@
makeBinaryWrapper, makeBinaryWrapper,
xdg-dbus-proxy, xdg-dbus-proxy,
bubblewrap, bubblewrap,
pkg-config,
acl, acl,
wayland,
wayland-scanner,
wayland-protocols,
xorg, xorg,
}: }:
buildGoModule rec { buildGoModule rec {
pname = "fortify"; pname = "fortify";
version = "0.2.5"; version = "0.2.0";
src = ./.; src = ./.;
vendorHash = null; vendorHash = null;
@ -26,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}"
] ]
) )
[ [
@ -43,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/"

221
test.nix
View File

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

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

131
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
FamilyInternet = C.XCB_FAMILY_INTERNET
FamilyDecnet = C.XCB_FAMILY_DECNET
FamilyChaos = C.XCB_FAMILY_CHAOS
FamilyServerInterpreted = C.XCB_FAMILY_SERVER_INTERPRETED
FamilyInternet6 = C.XCB_FAMILY_INTERNET_6
)
type (
HostMode = C.xcb_host_mode_t
Family = C.xcb_family_t
)
func (conn *connection) changeHostsChecked(mode HostMode, family Family, address string) error {
errno := C._go_xcb_change_hosts_checked(
conn.c,
C.uint8_t(mode),
C.uint8_t(family),
C.uint16_t(len(address)),
(*C.uint8_t)(unsafe.Pointer(C.CString(address))),
)
switch errno { switch errno {
case 0: case C.XCB_CONN_ERROR:
return nil return errors.New("connection error")
case -1: case C.XCB_CONN_CLOSED_EXT_NOTSUPPORTED:
return ErrChangeHosts return errors.New("extension not supported")
case C.XCB_CONN_CLOSED_MEM_INSUFFICIENT:
return errors.New("memory not available")
case C.XCB_CONN_CLOSED_REQ_LEN_EXCEED:
return errors.New("request length exceeded")
case C.XCB_CONN_CLOSED_PARSE_ERR:
return errors.New("invalid display string")
case C.XCB_CONN_CLOSED_INVALID_SCREEN:
return errors.New("server has no screen matching display")
default: default:
return &ConnectionError{errno} return errors.New("generic X11 failure")
} }
} } else {
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
} }