Compare commits

..

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

86 changed files with 998 additions and 1930 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,7 +7,6 @@ on:
jobs: jobs:
release: release:
name: Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: node:16-bookworm-slim image: node:16-bookworm-slim
@ -17,7 +16,6 @@ jobs:
echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list.d/backports.list && echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list.d/backports.list &&
apt-get update && apt-get update &&
apt-get install -y apt-get install -y
acl
git git
gcc gcc
pkg-config pkg-config
@ -41,13 +39,21 @@ jobs:
run: >- run: >-
go generate ./... go generate ./...
- 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,27 +6,22 @@ on:
jobs: jobs:
test: test:
name: Go tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: node:16-bookworm-slim 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 - name: Get dependencies
uses: awalsh128/cache-apt-pkgs-action@latest run: >-
with: echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list.d/backports.list &&
packages: acl git gcc pkg-config libwayland-dev wayland-protocols/bookworm-backports libxcb1-dev libacl1-dev apt-get update &&
version: 1.0 apt-get install -y
#execute_install_scripts: true git
gcc
pkg-config
libwayland-dev
wayland-protocols/bookworm-backports
libxcb1-dev
libacl1-dev
if: ${{ runner.os == 'Linux' }} if: ${{ runner.os == 'Linux' }}
- name: Checkout - name: Checkout
@ -47,16 +42,13 @@ jobs:
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

3
.gitignore vendored
View File

@ -26,6 +26,3 @@ go.work.sum
# go generate # go generate
security-context-v1-protocol.* 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.
@ -18,7 +18,7 @@ Why would you want this?
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 -- help
``` ```
## Module usage ## Module usage
@ -35,7 +35,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";

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

@ -1,18 +1,19 @@
package main package main
import ( import (
"encoding/gob"
"errors" "errors"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"path" "path"
"strconv"
"syscall" "syscall"
"time" "time"
init0 "git.gensokyo.uk/security/fortify/cmd/finit/ipc" init0 "git.ophivana.moe/security/fortify/cmd/finit/ipc"
"git.gensokyo.uk/security/fortify/internal" "git.ophivana.moe/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/proc"
) )
const ( const (
@ -47,24 +48,30 @@ func main() {
} }
} }
// receive setup payload // setup pipe fd from environment
var ( var setup *os.File
payload init0.Payload if s, ok := os.LookupEnv(init0.Env); !ok {
closeSetup func() error fmsg.Fatal("FORTIFY_INIT not set")
) panic("unreachable")
if f, err := proc.Receive(init0.Env, &payload); err != nil { } else {
if errors.Is(err, proc.ErrInvalid) { if fd, err := strconv.Atoi(s); err != nil {
fmsg.Fatal("invalid config descriptor") fmsg.Fatalf("cannot parse %q: %v", s, err)
} panic("unreachable")
if errors.Is(err, proc.ErrNotSet) { } else {
fmsg.Fatal("FORTIFY_INIT not set") setup = os.NewFile(uintptr(fd), "setup")
if setup == nil {
fmsg.Fatal("invalid config descriptor")
panic("unreachable")
}
} }
}
fmsg.Fatalf("cannot decode init setup payload: %v", err) var payload init0.Payload
if err := gob.NewDecoder(setup).Decode(&payload); err != nil {
fmsg.Fatal("cannot decode init setup payload:", err)
panic("unreachable") panic("unreachable")
} else { } else {
fmsg.SetVerbose(payload.Verbose) fmsg.SetVerbose(payload.Verbose)
closeSetup = f
// child does not need to see this // child does not need to see this
if err = os.Unsetenv(init0.Env); err != nil { if err = os.Unsetenv(init0.Env); err != nil {
@ -91,7 +98,7 @@ func main() {
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,11 @@
package shim0 package shim0
import ( import (
"git.gensokyo.uk/security/fortify/helper/bwrap" "encoding/gob"
"net"
"git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal/fmsg"
) )
const Env = "FORTIFY_SHIM" const Env = "FORTIFY_SHIM"
@ -19,3 +23,13 @@ type Payload struct {
// verbosity pass through // verbosity pass through
Verbose bool Verbose bool
} }
func (p *Payload) Serve(conn *net.UnixConn) error {
if err := gob.NewEncoder(conn).Encode(*p); err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot stream shim payload:")
}
return fmsg.WrapErrorSuffix(conn.Close(),
"cannot close setup connection:")
}

View File

@ -1,20 +1,22 @@
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"
"git.ophivana.moe/security/fortify/internal/proc"
) )
const shimSetupTimeout = 5 * time.Second const shimSetupTimeout = 5 * time.Second
@ -30,14 +32,20 @@ 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
// 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, payload *shim0.Payload) *Shim {
return &Shim{uid: uid, aid: aid, supp: supp, payload: payload} return &Shim{uid: uid, aid: aid, supp: supp, socket: socket, payload: payload}
} }
func (s *Shim) String() string { func (s *Shim) String() string {
@ -51,11 +59,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,19 +101,10 @@ func (s *Shim) Start() (*time.Time, error) {
fsu = p fsu = p
} }
s.cmd = exec.Command(fsu) s.cmd = exec.Command(fsu)
s.cmd.Env = []string{
var encoder *gob.Encoder shim0.Env + "=" + s.socket,
if fd, e, err := proc.Setup(&s.cmd.ExtraFiles); err != nil { "FORTIFY_APP_ID=" + s.aid,
return nil, fmsg.WrapErrorSuffix(err,
"cannot create shim setup pipe:")
} else {
encoder = e
s.cmd.Env = []string{
shim0.Env + "=" + strconv.Itoa(fd),
"FORTIFY_APP_ID=" + s.aid,
}
} }
if len(s.supp) > 0 { if len(s.supp) > 0 {
fmsg.VPrintf("attaching supplementary group ids %s", s.supp) fmsg.VPrintf("attaching supplementary group ids %s", s.supp)
s.cmd.Env = append(s.cmd.Env, "FORTIFY_GROUPS="+strings.Join(s.supp, " ")) s.cmd.Env = append(s.cmd.Env, "FORTIFY_GROUPS="+strings.Join(s.supp, " "))
@ -118,20 +145,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
// this also closes the connection
err := s.payload.Serve(conn)
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
} }

View File

@ -1,18 +1,18 @@
package main package main
import ( import (
"errors" "encoding/gob"
"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 +37,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,24 +54,21 @@ 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 {
@ -75,8 +81,8 @@ func main() {
} }
// 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 +110,17 @@ func main() {
var extraFiles []*os.File var extraFiles []*os.File
// serve setup payload // share config pipe
if fd, encoder, err := proc.Setup(&extraFiles); err != nil { if r, w, err := os.Pipe(); err != nil {
fmsg.Fatalf("cannot pipe: %v", err) fmsg.Fatalf("cannot pipe: %v", err)
} else { } else {
conf.SetEnv[init0.Env] = strconv.Itoa(fd) conf.SetEnv[init0.Env] = strconv.Itoa(3 + len(extraFiles))
extraFiles = append(extraFiles, r)
fmsg.VPrintln("transmitting config to init")
go func() { go func() {
fmsg.VPrintln("transmitting config to init") // stream config to pipe
if err = encoder.Encode(&ic); err != nil { if err = gob.NewEncoder(w).Encode(&ic); err != nil {
fmsg.Fatalf("cannot transmit init config: %v", err) fmsg.Fatalf("cannot transmit init config: %v", err)
} }
}() }()

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

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

@ -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) {

28
flake.lock generated
View File

@ -1,33 +1,12 @@
{ {
"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": 1733348545,
"narHash": "sha256-aWhhqY44xBjMoO9r5fyPp5u8tqUNWRZ/m/P+abMSs5c=", "narHash": "sha256-b4JrUmqT0vFNx42aEN9LTWOHomkTKL/ayLopflVf81U=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "eb919d9300b6a18f8583f58aef16db458fbd7bec", "rev": "9ecb50d2fae8680be74c08bb0a995c5383747f89",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -39,7 +18,6 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"home-manager": "home-manager",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
} }
} }

View File

@ -3,19 +3,10 @@
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11-small"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11-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

@ -8,8 +8,8 @@ import (
"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" "git.ophivana.moe/security/fortify/internal/proc"
) )
// BubblewrapName is the file name or path to bubblewrap. // BubblewrapName is the file name or path to bubblewrap.

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

@ -6,7 +6,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"git.gensokyo.uk/security/fortify/internal/proc" "git.ophivana.moe/security/fortify/internal/proc"
) )
type pipes struct { type pipes struct {

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,25 +1,25 @@
package app_test package app_test
import ( import (
"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/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/system" "git.ophivana.moe/security/fortify/internal/system"
) )
var testCasesNixos = []sealTestCase{ var testCasesNixos = []sealTestCase{
{ {
"nixos chromium direct wayland", new(stubNixOS), "nixos chromium direct wayland", new(stubNixOS),
&fst.Config{ &app.Config{
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Command: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"}, Command: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
Confinement: fst.ConfinementConfig{ Confinement: app.ConfinementConfig{
AppID: 1, Groups: []string{}, Username: "u0_a1", AppID: 1, Groups: []string{}, Username: "u0_a1",
Outer: "/var/lib/persist/module/fortify/0/1", Outer: "/var/lib/persist/module/fortify/0/1",
Sandbox: &fst.SandboxConfig{ Sandbox: &app.SandboxConfig{
UserNS: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, UserNS: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil,
Filesystem: []*fst.FilesystemConfig{ Filesystem: []*app.FilesystemConfig{
{Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true}, {Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true},
{Src: "/nix/store", Must: true}, {Src: "/run/current-system", 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: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"},
@ -48,7 +48,7 @@ var testCasesNixos = []sealTestCase{
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, 0x8e, 0x2c, 0x76, 0xb0,
0x66, 0xda, 0xbe, 0x57, 0x66, 0xda, 0xbe, 0x57,
0x4c, 0xf0, 0x73, 0xbd, 0x4c, 0xf0, 0x73, 0xbd,

View File

@ -1,25 +1,25 @@
package app_test package app_test
import ( import (
"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/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/system" "git.ophivana.moe/security/fortify/internal/system"
) )
var testCasesPd = []sealTestCase{ var testCasesPd = []sealTestCase{
{ {
"nixos permissive defaults no enablements", new(stubNixOS), "nixos permissive defaults no enablements", new(stubNixOS),
&fst.Config{ &app.Config{
Command: make([]string, 0), Command: make([]string, 0),
Confinement: fst.ConfinementConfig{ Confinement: app.ConfinementConfig{
AppID: 0, AppID: 0,
Username: "chronos", Username: "chronos",
Outer: "/home/chronos", Outer: "/home/chronos",
}, },
}, },
fst.ID{ app.ID{
0x4a, 0x45, 0x0b, 0x65, 0x4a, 0x45, 0x0b, 0x65,
0x96, 0xd7, 0xbc, 0x15, 0x96, 0xd7, 0xbc, 0x15,
0xbd, 0x01, 0x78, 0x0e, 0xbd, 0x01, 0x78, 0x0e,
@ -190,10 +190,10 @@ var testCasesPd = []sealTestCase{
}, },
{ {
"nixos permissive defaults chromium", new(stubNixOS), "nixos permissive defaults chromium", new(stubNixOS),
&fst.Config{ &app.Config{
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Command: []string{"/run/current-system/sw/bin/zsh", "-c", "exec chromium "}, Command: []string{"/run/current-system/sw/bin/zsh", "-c", "exec chromium "},
Confinement: fst.ConfinementConfig{ Confinement: app.ConfinementConfig{
AppID: 9, AppID: 9,
Groups: []string{"video"}, Groups: []string{"video"},
Username: "chronos", Username: "chronos",
@ -232,7 +232,7 @@ var testCasesPd = []sealTestCase{
Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(), Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(),
}, },
}, },
fst.ID{ app.ID{
0xeb, 0xf0, 0x83, 0xd1, 0xeb, 0xf0, 0x83, 0xd1,
0xb1, 0x75, 0x91, 0x17, 0xb1, 0x75, 0x91, 0x17,
0x82, 0xd4, 0x13, 0x36, 0x82, 0xd4, 0x13, 0x36,

View File

@ -7,7 +7,7 @@ import (
"os/user" "os/user"
"strconv" "strconv"
"git.gensokyo.uk/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/linux"
) )
// fs methods are not implemented using a real FS // fs methods are not implemented using a real FS

View File

@ -6,18 +6,17 @@ 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
} }

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"

View File

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

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

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

View File

@ -8,12 +8,11 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"git.gensokyo.uk/security/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/internal/state" "git.ophivana.moe/security/fortify/internal/system"
"git.gensokyo.uk/security/fortify/internal/system"
) )
var ( var (
@ -60,7 +59,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()
@ -148,7 +147,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 +157,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 +169,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 +178,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 +186,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,7 +198,7 @@ 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
@ -218,7 +217,7 @@ func (a *app) Seal(config *fst.Config) error {
// 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 +236,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 (

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,6 +46,7 @@ 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"),
&shim0.Payload{ &shim0.Payload{
Argv: a.seal.command, Argv: a.seal.command,
Exec: shimExec, Exec: shimExec,
@ -68,16 +70,17 @@ func (a *app) Start() error {
} else { } else {
// shim start and setup success, create process state // shim start and setup success, create process state
sd := state.State{ sd := state.State{
ID: *a.id, PID: a.shim.Unwrap().Process.Pid,
PID: a.shim.Unwrap().Process.Pid, Command: a.seal.command,
Config: a.ct.Unwrap(), Capability: a.seal.et,
Time: *startTime, Argv: a.shim.Unwrap().Args,
Time: *startTime,
} }
// register process state // register process state
var err0 = new(StateStoreError) var err0 = new(StateStoreError)
err0.Inner, err0.DoErr = a.seal.store.Do(a.seal.sys.user.aid, func(c state.Cursor) { err0.Inner, err0.DoErr = a.seal.store.Do(func(b state.Backend) {
err0.InnerErr = c.Save(&sd) err0.InnerErr = b.Save(&sd)
}) })
a.seal.sys.saveState = true a.seal.sys.saveState = true
return err0.equiv("cannot save process state:") return err0.equiv("cannot save process state:")
@ -199,11 +202,11 @@ func (a *app) Wait() (int, error) {
// 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 +227,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 +249,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,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
}
// mustPrintLauncherState causes store activity so store needs to be closed // skip non-numerical names
if err := s.Close(); err != nil { if _, err = strconv.Atoi(e.Name()); err != nil {
fmsg.Printf("cannot close store: %v", err) fmsg.VPrintf("skipped non-uid entry %q", e.Name())
continue
}
// obtain temporary store
s := NewSimple(runDir, e.Name()).(*simpleStore)
// print states belonging to this store
s.mustPrintLauncherState(w, now)
// mustPrintLauncherState causes store activity so store needs to be closed
if err = s.Close(); err != nil {
fmsg.Printf("cannot close store for user %q: %s", e.Name(), err)
}
}
} }
} }
func (s *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 // append enablement strings in order
cs = "(No command information)" for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
) if state.Capability.Has(i) {
ets.WriteString(", " + i.String())
// check if enablements are provided
if state.Config != nil {
ets = new(strings.Builder)
// append enablement strings in order
for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
if state.Config.Confinement.Enablements.Has(i) {
ets.WriteString(", " + i.String())
}
} }
cs = fmt.Sprintf("%q", state.Config.Command)
} }
if ets != nil { // prevent an empty string when
// 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

@ -5,7 +5,7 @@ import (
"os" "os"
"sync" "sync"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
const ( const (

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

@ -5,9 +5,9 @@ import (
"fmt" "fmt"
"os" "os"
"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/wl" "git.ophivana.moe/security/fortify/wl"
) )
// Wayland sets up a wayland socket with a security context attached. // Wayland sets up a wayland socket with a security context attached.

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

27
main.go
View File

@ -11,14 +11,13 @@ import (
"sync" "sync"
"text/tabwriter" "text/tabwriter"
"git.gensokyo.uk/security/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst" "git.ophivana.moe/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal" "git.ophivana.moe/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/app" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/internal/state" "git.ophivana.moe/security/fortify/internal/system"
"git.gensokyo.uk/security/fortify/internal/system"
) )
var ( var (
@ -103,7 +102,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 +129,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")
@ -180,7 +179,7 @@ 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: fid,
Command: set.Args(), Command: set.Args(),
} }
@ -276,7 +275,11 @@ func main() {
panic("unreachable") panic("unreachable")
} }
func runApp(config *fst.Config) { func runApp(config *app.Config) {
if os.SdBooted() {
fmsg.VPrintln("system booted with systemd as init system")
}
a, err := app.New(os) a, err := app.New(os)
if err != nil { if err != nil {
fmsg.Fatalf("cannot create app: %s\n", err) fmsg.Fatalf("cannot create app: %s\n", err)

View File

@ -10,6 +10,7 @@ let
mkIf mkIf
mkDefault mkDefault
mapAttrs mapAttrs
mapAttrsToList
mergeAttrsList mergeAttrsList
imap1 imap1
foldr foldr

View File

@ -36,7 +36,7 @@ package
*Default:* *Default:*
` <derivation fortify-0.2.5> ` ` <derivation fortify-0.2.1> `

View File

@ -31,6 +31,7 @@ in
let let
inherit (types) inherit (types)
str str
enum
bool bool
package package
anything anything

View File

@ -14,7 +14,7 @@
buildGoModule rec { buildGoModule rec {
pname = "fortify"; pname = "fortify";
version = "0.2.5"; version = "0.2.2";
src = ./.; src = ./.;
vendorHash = null; vendorHash = null;
@ -26,7 +26,7 @@ buildGoModule rec {
ldflags ldflags
++ [ ++ [
"-X" "-X"
"git.gensokyo.uk/security/fortify/internal.${name}=${value}" "git.ophivana.moe/security/fortify/internal.${name}=${value}"
] ]
) )
[ [
@ -43,9 +43,6 @@ 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

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"))
'';
}

137
xcb/c.go
View File

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

View File

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