Compare commits

..

7 Commits

Author SHA1 Message Date
e85be67fd9
acl: implement Update in C
All checks were successful
Test / Create distribution (push) Successful in 18s
Test / Run NixOS test (push) Successful in 46s
The original implementation was effectively just writing C in Go.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-17 21:39:14 +09:00
7e69893264
acl: rename UpdatePerms to Update
All checks were successful
Test / Create distribution (push) Successful in 24s
Test / Run NixOS test (push) Successful in 3m21s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-17 20:33:18 +09:00
38a3e6af03
system: make xcb internal
All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Run NixOS test (push) Successful in 3m29s
This package is hauntingly ugly. Move this to internal until it is removed or replaced.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-17 19:07:53 +09:00
90cb01b274
system: move out of internal
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m17s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-17 19:00:43 +09:00
b1e1d5627e
system: wrap console output functions
All checks were successful
Test / Create distribution (push) Successful in 24s
Test / Run NixOS test (push) Successful in 3m13s
This eliminates all fmsg imports from internal/system.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-17 18:17:55 +09:00
3ae2ab652e
system/wayland: sync file at caller specified address
All checks were successful
Test / Create distribution (push) Successful in 24s
Test / Run NixOS test (push) Successful in 3m14s
Storing this in sys is incredibly ugly: sys should be stateless and Ops must keep track of their state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-17 13:24:17 +09:00
db71fbe22b
system/tmpfiles: fail gracefully in API misuse
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m25s
Panicking here leaves garbage behind. Not ideal if this package is going to be exported.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-17 12:17:01 +09:00
35 changed files with 324 additions and 406 deletions

69
acl/acl-update.c Normal file
View File

@ -0,0 +1,69 @@
#include "acl-update.h"
#include <stdlib.h>
#include <stdbool.h>
#include <sys/acl.h>
#include <acl/libacl.h>
int f_acl_update_file_by_uid(const char *path_p, uid_t uid, acl_perm_t *perms, size_t plen) {
int ret = -1;
bool v;
int i;
acl_t acl;
acl_entry_t entry;
acl_tag_t tag_type;
void *qualifier_p;
acl_permset_t permset;
acl = acl_get_file(path_p, ACL_TYPE_ACCESS);
if (acl == NULL)
goto out;
// prune entries by uid
for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1; i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
if (acl_get_tag_type(entry, &tag_type) != 0)
return -1;
if (tag_type != ACL_USER)
continue;
qualifier_p = acl_get_qualifier(entry);
if (qualifier_p == NULL)
return -1;
v = *(uid_t *)qualifier_p == uid;
acl_free(qualifier_p);
if (!v)
continue;
acl_delete_entry(acl, entry);
}
if (plen == 0)
goto set;
if (acl_create_entry(&acl, &entry) != 0)
goto out;
if (acl_get_permset(entry, &permset) != 0)
goto out;
for (i = 0; i < plen; i++) {
if (acl_add_perm(permset, perms[i]) != 0)
goto out;
}
if (acl_set_tag_type(entry, ACL_USER) != 0)
goto out;
if (acl_set_qualifier(entry, (void *)&uid) != 0)
goto out;
set:
if (acl_calc_mask(&acl) != 0)
goto out;
if (acl_valid(acl) != 0)
goto out;
if (acl_set_file(path_p, ACL_TYPE_ACCESS, acl) == 0)
ret = 0;
out:
free((void *)path_p);
if (acl != NULL)
acl_free((void *)acl);
return ret;
}

3
acl/acl-update.h Normal file
View File

@ -0,0 +1,3 @@
#include <sys/acl.h>
int f_acl_update_file_by_uid(const char *path_p, uid_t uid, acl_perm_t *perms, size_t plen);

View File

@ -1,19 +1,36 @@
// Package acl implements simple ACL manipulation via libacl. // Package acl implements simple ACL manipulation via libacl.
package acl package acl
type Perms []Perm /*
#cgo linux pkg-config: --static libacl
func (ps Perms) String() string { #include "acl-update.h"
var s = []byte("---") */
for _, p := range ps { import "C"
switch p {
case Read: type Perm C.acl_perm_t
s[0] = 'r'
case Write: const (
s[1] = 'w' Read Perm = C.ACL_READ
case Execute: Write Perm = C.ACL_WRITE
s[2] = 'x' Execute Perm = C.ACL_EXECUTE
} )
// Update replaces ACL_USER entry with qualifier uid.
func Update(name string, uid int, perms ...Perm) error {
var p *Perm
if len(perms) > 0 {
p = &perms[0]
} }
return string(s)
r, err := C.f_acl_update_file_by_uid(
C.CString(name),
C.uid_t(uid),
(*C.acl_perm_t)(p),
C.size_t(len(perms)),
)
if r == 0 {
return nil
}
return err
} }

View File

@ -47,7 +47,7 @@ func TestUpdatePerm(t *testing.T) {
}) })
t.Run("default clear mask", func(t *testing.T) { t.Run("default clear mask", func(t *testing.T) {
if err := acl.UpdatePerm(testFilePath, uid); err != nil { if err := acl.Update(testFilePath, uid); err != nil {
t.Fatalf("UpdatePerm: error = %v", err) t.Fatalf("UpdatePerm: error = %v", err)
} }
if cur = getfacl(t, testFilePath); len(cur) != 4 { if cur = getfacl(t, testFilePath); len(cur) != 4 {
@ -56,7 +56,7 @@ func TestUpdatePerm(t *testing.T) {
}) })
t.Run("default clear consistency", func(t *testing.T) { t.Run("default clear consistency", func(t *testing.T) {
if err := acl.UpdatePerm(testFilePath, uid); err != nil { if err := acl.Update(testFilePath, uid); err != nil {
t.Fatalf("UpdatePerm: error = %v", err) t.Fatalf("UpdatePerm: error = %v", err)
} }
if val := getfacl(t, testFilePath); !reflect.DeepEqual(val, cur) { if val := getfacl(t, testFilePath); !reflect.DeepEqual(val, cur) {
@ -76,7 +76,7 @@ func TestUpdatePerm(t *testing.T) {
func testUpdate(t *testing.T, testFilePath, name string, cur []*getFAclResp, val fAclPerm, perms ...acl.Perm) { func testUpdate(t *testing.T, testFilePath, name string, cur []*getFAclResp, val fAclPerm, perms ...acl.Perm) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
t.Cleanup(func() { t.Cleanup(func() {
if err := acl.UpdatePerm(testFilePath, uid); err != nil { if err := acl.Update(testFilePath, uid); err != nil {
t.Fatalf("UpdatePerm: error = %v", err) t.Fatalf("UpdatePerm: error = %v", err)
} }
if v := getfacl(t, testFilePath); !reflect.DeepEqual(v, cur) { if v := getfacl(t, testFilePath); !reflect.DeepEqual(v, cur) {
@ -84,7 +84,7 @@ func testUpdate(t *testing.T, testFilePath, name string, cur []*getFAclResp, val
} }
}) })
if err := acl.UpdatePerm(testFilePath, uid, perms...); err != nil { if err := acl.Update(testFilePath, uid, perms...); err != nil {
t.Fatalf("UpdatePerm: error = %v", err) t.Fatalf("UpdatePerm: error = %v", err)
} }
r := respByCred(getfacl(t, testFilePath), fAclTypeUser, cred) r := respByCred(getfacl(t, testFilePath), fAclTypeUser, cred)

196
acl/c.go
View File

@ -1,196 +0,0 @@
package acl
import "C"
import (
"errors"
"runtime"
"syscall"
"unsafe"
)
/*
#cgo linux pkg-config: --static libacl
#include <stdlib.h>
#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"
func getFile(name string, t C.acl_type_t) (*ACL, error) {
a, err := C._go_acl_get_file(C.CString(name), t)
if errors.Is(err, syscall.ENODATA) {
err = nil
}
return newACL(a), err
}
func (acl *ACL) setFile(name string, t C.acl_type_t) error {
_, err := C._go_acl_set_file(C.CString(name), t, acl.acl)
return err
}
func newACL(a C.acl_t) *ACL {
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
// get first entry
if r, err := C.acl_get_entry(acl.acl, C.ACL_FIRST_ENTRY, &e); err != nil {
return err
} else if r == 0 {
// return on acl with no entries
return nil
}
for {
if r, err := C.acl_get_entry(acl.acl, C.ACL_NEXT_ENTRY, &e); err != nil {
return err
} else if r == 0 {
// return on drained acl
return nil
}
var (
q int
t C.acl_tag_t
)
// get current entry tag type
if _, err := C.acl_get_tag_type(e, &t); err != nil {
return err
}
// get current entry qualifier
if rq, err := C.acl_get_qualifier(e); err != nil {
// neither ACL_USER nor ACL_GROUP
if errors.Is(err, syscall.EINVAL) {
continue
}
return err
} else {
q = *(*int)(rq)
C.acl_free(rq)
}
// delete on match
if t == tt && q == tq {
_, err := C.acl_delete_entry(acl.acl, e)
return err
}
}
}
func UpdatePerm(name string, uid int, perms ...Perm) error {
// read acl from file
a, err := getFile(name, 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.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)
}

18
acl/perms.go Normal file
View File

@ -0,0 +1,18 @@
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

@ -6,7 +6,7 @@ import (
"os" "os"
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/internal/system" "git.gensokyo.uk/security/fortify/system"
) )
type bundleInfo struct { type bundleInfo struct {

View File

@ -3,7 +3,7 @@ package fst
import ( import (
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/system" "git.gensokyo.uk/security/fortify/system"
) )
const Tmp = "/.fortify" const Tmp = "/.fortify"

View File

@ -5,7 +5,7 @@ import (
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/system" "git.gensokyo.uk/security/fortify/system"
) )
var testCasesNixos = []sealTestCase{ var testCasesNixos = []sealTestCase{

View File

@ -1,11 +1,13 @@
package app_test package app_test
import ( import (
"os"
"git.gensokyo.uk/security/fortify/acl" "git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/system" "git.gensokyo.uk/security/fortify/system"
) )
var testCasesPd = []sealTestCase{ var testCasesPd = []sealTestCase{
@ -217,7 +219,7 @@ var testCasesPd = []sealTestCase{
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", acl.Execute). Ephemeral(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", acl.Execute).
Ensure("/tmp/fortify.1971/wayland", 0711). Ensure("/tmp/fortify.1971/wayland", 0711).
Wayland("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"). Wayland(new(*os.File), "/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse"). Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse").
CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 256, 256). CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
MustProxyDBus("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", &dbus.Config{ MustProxyDBus("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", &dbus.Config{

View File

@ -11,7 +11,7 @@ import (
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/app" "git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/linux" "git.gensokyo.uk/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/system" "git.gensokyo.uk/security/fortify/system"
) )
type sealTestCase struct { type sealTestCase struct {

View File

@ -4,7 +4,7 @@ import (
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/linux" "git.gensokyo.uk/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/system" "git.gensokyo.uk/security/fortify/system"
) )
func NewWithID(id fst.ID, os linux.System) App { func NewWithID(id fst.ID, os linux.System) App {

View File

@ -19,7 +19,7 @@ import (
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/linux" "git.gensokyo.uk/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/state" "git.gensokyo.uk/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/internal/system" "git.gensokyo.uk/security/fortify/system"
) )
var ( var (
@ -252,8 +252,12 @@ func (a *app) Seal(config *fst.Config) error {
// 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.NewMulti(seal.RunDirPath)
// initialise system interface with full uid // initialise system interface with os uid
seal.sys.I = system.New(seal.sys.user.uid) seal.sys.I = system.New(seal.sys.user.uid)
seal.sys.I.IsVerbose = fmsg.Load
seal.sys.I.Verbose = fmsg.Verbose
seal.sys.I.Verbosef = fmsg.Verbosef
seal.sys.I.WrapErr = fmsg.WrapError
// pass through enablements // pass through enablements
seal.et = config.Confinement.Enablements seal.et = config.Confinement.Enablements

View File

@ -12,7 +12,7 @@ import (
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/linux" "git.gensokyo.uk/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/system" "git.gensokyo.uk/security/fortify/system"
"git.gensokyo.uk/security/fortify/wl" "git.gensokyo.uk/security/fortify/wl"
) )
@ -163,7 +163,7 @@ func (seal *appSeal) setupShares(bus [2]*dbus.Config, os linux.System) error {
// use instance ID in case app id is not set // use instance ID in case app id is not set
appID = "uk.gensokyo.fortify." + seal.id appID = "uk.gensokyo.fortify." + seal.id
} }
seal.sys.Wayland(outerPath, socketPath, appID, seal.id) seal.sys.Wayland(&seal.sys.sp, outerPath, socketPath, appID, seal.id)
seal.sys.bwrap.Bind(outerPath, innerPath) seal.sys.bwrap.Bind(outerPath, innerPath)
} else { // bind mount wayland socket (insecure) } else { // bind mount wayland socket (insecure)
fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION") fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION")

View File

@ -14,7 +14,7 @@ import (
"git.gensokyo.uk/security/fortify/internal/app/shim" "git.gensokyo.uk/security/fortify/internal/app/shim"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/state" "git.gensokyo.uk/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/internal/system" "git.gensokyo.uk/security/fortify/system"
) )
const shimSetupTimeout = 5 * time.Second const shimSetupTimeout = 5 * time.Second
@ -58,7 +58,7 @@ func (a *app) Run(ctx context.Context, rs *RunState) error {
if startTime, err := a.shim.Start( if startTime, err := a.shim.Start(
a.seal.sys.user.as, a.seal.sys.user.as,
a.seal.sys.user.supp, a.seal.sys.user.supp,
a.seal.sys.Sync(), a.seal.sys.sp,
); err != nil { ); err != nil {
return err return err
} else { } else {

View File

@ -1,13 +1,17 @@
package app package app
import ( import (
"os"
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/system" "git.gensokyo.uk/security/fortify/system"
) )
// appSealSys encapsulates app seal behaviour with OS interactions // appSealSys encapsulates app seal behaviour with OS interactions
type appSealSys struct { type appSealSys struct {
bwrap *bwrap.Config bwrap *bwrap.Config
// bwrap sync fd
sp *os.File
// paths to override by mounting tmpfs over them // paths to override by mounting tmpfs over them
override []string override []string

View File

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

View File

@ -26,7 +26,7 @@ import (
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/linux" "git.gensokyo.uk/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/state" "git.gensokyo.uk/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/internal/system" "git.gensokyo.uk/security/fortify/system"
) )
var ( var (

View File

@ -5,7 +5,6 @@ import (
"slices" "slices"
"git.gensokyo.uk/security/fortify/acl" "git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/internal/fmsg"
) )
// UpdatePerm appends an ephemeral acl update Op. // UpdatePerm appends an ephemeral acl update Op.
@ -31,23 +30,21 @@ type ACL struct {
perms acl.Perms perms acl.Perms
} }
func (a *ACL) Type() Enablement { func (a *ACL) Type() Enablement { return a.et }
return a.et
}
func (a *ACL) apply(sys *I) error { func (a *ACL) apply(sys *I) error {
fmsg.Verbose("applying ACL", a) sys.println("applying ACL", a)
return fmsg.WrapErrorSuffix(acl.UpdatePerm(a.path, sys.uid, a.perms...), return sys.wrapErrSuffix(acl.Update(a.path, sys.uid, a.perms...),
fmt.Sprintf("cannot apply ACL entry to %q:", a.path)) fmt.Sprintf("cannot apply ACL entry to %q:", a.path))
} }
func (a *ACL) revert(sys *I, ec *Criteria) error { func (a *ACL) revert(sys *I, ec *Criteria) error {
if ec.hasType(a) { if ec.hasType(a) {
fmsg.Verbose("stripping ACL", a) sys.println("stripping ACL", a)
return fmsg.WrapErrorSuffix(acl.UpdatePerm(a.path, sys.uid), return sys.wrapErrSuffix(acl.Update(a.path, sys.uid),
fmt.Sprintf("cannot strip ACL entry from %q:", a.path)) fmt.Sprintf("cannot strip ACL entry from %q:", a.path))
} else { } else {
fmsg.Verbose("skipping ACL", a) sys.println("skipping ACL", a)
return nil return nil
} }
} }
@ -60,9 +57,7 @@ func (a *ACL) Is(o Op) bool {
slices.Equal(a.perms, a0.perms) slices.Equal(a.perms, a0.perms)
} }
func (a *ACL) Path() string { func (a *ACL) Path() string { return a.path }
return a.path
}
func (a *ACL) String() string { func (a *ACL) String() string {
return fmt.Sprintf("%s type: %s path: %q", return fmt.Sprintf("%s type: %s path: %q",

View File

@ -8,7 +8,6 @@ import (
"sync" "sync"
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/internal/fmsg"
) )
var ( var (
@ -28,7 +27,7 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st
// session bus is mandatory // session bus is mandatory
if session == nil { if session == nil {
return nil, fmsg.WrapError(ErrDBusConfig, return nil, sys.wrapErr(ErrDBusConfig,
"attempted to seal message bus proxy without session bus config") "attempted to seal message bus proxy without session bus config")
} }
@ -48,12 +47,12 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st
d.proxy = dbus.New(sessionBus, systemBus) d.proxy = dbus.New(sessionBus, systemBus)
defer func() { defer func() {
if fmsg.Load() && d.proxy.Sealed() { if sys.IsVerbose() && d.proxy.Sealed() {
fmsg.Verbose("sealed session proxy", session.Args(sessionBus)) sys.println("sealed session proxy", session.Args(sessionBus))
if system != nil { if system != nil {
fmsg.Verbose("sealed system proxy", system.Args(systemBus)) sys.println("sealed system proxy", system.Args(systemBus))
} }
fmsg.Verbose("message bus proxy final args:", d.proxy) sys.println("message bus proxy final args:", d.proxy)
} }
}() }()
@ -62,7 +61,7 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st
// seal dbus proxy // seal dbus proxy
d.out = &scanToFmsg{msg: new(strings.Builder)} d.out = &scanToFmsg{msg: new(strings.Builder)}
return d.out.Dump, fmsg.WrapErrorSuffix(d.proxy.Seal(session, system), return d.out.Dump, sys.wrapErrSuffix(d.proxy.Seal(session, system),
"cannot seal message bus proxy:") "cannot seal message bus proxy:")
} }
@ -74,32 +73,30 @@ type DBus struct {
system bool system bool
} }
func (d *DBus) Type() Enablement { func (d *DBus) Type() Enablement { return Process }
return Process
}
func (d *DBus) apply(sys *I) error { func (d *DBus) apply(sys *I) error {
fmsg.Verbosef("session bus proxy on %q for upstream %q", d.proxy.Session()[1], d.proxy.Session()[0]) sys.printf("session bus proxy on %q for upstream %q", d.proxy.Session()[1], d.proxy.Session()[0])
if d.system { if d.system {
fmsg.Verbosef("system bus proxy on %q for upstream %q", d.proxy.System()[1], d.proxy.System()[0]) sys.printf("system bus proxy on %q for upstream %q", d.proxy.System()[1], d.proxy.System()[0])
} }
// this starts the process and blocks until ready // this starts the process and blocks until ready
if err := d.proxy.Start(sys.ctx, d.out, true); err != nil { if err := d.proxy.Start(sys.ctx, d.out, true); err != nil {
d.out.Dump() d.out.Dump()
return fmsg.WrapErrorSuffix(err, return sys.wrapErrSuffix(err,
"cannot start message bus proxy:") "cannot start message bus proxy:")
} }
fmsg.Verbose("starting message bus proxy:", d.proxy) sys.println("starting message bus proxy:", d.proxy)
return nil return nil
} }
func (d *DBus) revert(_ *I, _ *Criteria) error { func (d *DBus) revert(sys *I, _ *Criteria) error {
// criteria ignored here since dbus is always process-scoped // criteria ignored here since dbus is always process-scoped
fmsg.Verbose("terminating message bus proxy") sys.println("terminating message bus proxy")
d.proxy.Close() d.proxy.Close()
defer fmsg.Verbose("message bus proxy exit") defer sys.println("message bus proxy exit")
return fmsg.WrapErrorSuffix(d.proxy.Wait(), "message bus proxy error:") return sys.wrapErrSuffix(d.proxy.Wait(), "message bus proxy error:")
} }
func (d *DBus) Is(o Op) bool { func (d *DBus) Is(o Op) bool {

View File

@ -3,8 +3,6 @@ package system
import ( import (
"fmt" "fmt"
"os" "os"
"git.gensokyo.uk/security/fortify/internal/fmsg"
) )
// Link registers an Op that links dst to src. // Link registers an Op that links dst to src.
@ -27,27 +25,23 @@ type Hardlink struct {
func (l *Hardlink) Type() Enablement { return l.et } func (l *Hardlink) Type() Enablement { return l.et }
func (l *Hardlink) apply(_ *I) error { func (l *Hardlink) apply(sys *I) error {
fmsg.Verbose("linking", l) sys.println("linking", l)
return fmsg.WrapErrorSuffix(os.Link(l.src, l.dst), return sys.wrapErrSuffix(os.Link(l.src, l.dst),
fmt.Sprintf("cannot link %q:", l.dst)) fmt.Sprintf("cannot link %q:", l.dst))
} }
func (l *Hardlink) revert(_ *I, ec *Criteria) error { func (l *Hardlink) revert(sys *I, ec *Criteria) error {
if ec.hasType(l) { if ec.hasType(l) {
fmsg.Verbosef("removing hard link %q", l.dst) sys.printf("removing hard link %q", l.dst)
return fmsg.WrapErrorSuffix(os.Remove(l.dst), return sys.wrapErrSuffix(os.Remove(l.dst),
fmt.Sprintf("cannot remove hard link %q:", l.dst)) fmt.Sprintf("cannot remove hard link %q:", l.dst))
} else { } else {
fmsg.Verbosef("skipping hard link %q", l.dst) sys.printf("skipping hard link %q", l.dst)
return nil return nil
} }
} }
func (l *Hardlink) Is(o Op) bool { func (l *Hardlink) Is(o Op) bool { l0, ok := o.(*Hardlink); return ok && l0 != nil && *l == *l0 }
l0, ok := o.(*Hardlink)
return ok && l0 != nil && *l == *l0
}
func (l *Hardlink) Path() string { return l.src } func (l *Hardlink) Path() string { return l.src }
func (l *Hardlink) String() string { return fmt.Sprintf("%q from %q", l.dst, l.src) } func (l *Hardlink) String() string { return fmt.Sprintf("%q from %q", l.dst, l.src) }

View File

@ -4,8 +4,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"git.gensokyo.uk/security/fortify/internal/fmsg"
) )
// Ensure the existence and mode of a directory. // Ensure the existence and mode of a directory.
@ -39,33 +37,33 @@ func (m *Mkdir) Type() Enablement {
return m.et return m.et
} }
func (m *Mkdir) apply(_ *I) error { func (m *Mkdir) apply(sys *I) error {
fmsg.Verbose("ensuring directory", m) sys.println("ensuring directory", m)
// create directory // create directory
err := os.Mkdir(m.path, m.perm) err := os.Mkdir(m.path, m.perm)
if !errors.Is(err, os.ErrExist) { if !errors.Is(err, os.ErrExist) {
return fmsg.WrapErrorSuffix(err, return sys.wrapErrSuffix(err,
fmt.Sprintf("cannot create directory %q:", m.path)) fmt.Sprintf("cannot create directory %q:", m.path))
} }
// directory exists, ensure mode // directory exists, ensure mode
return fmsg.WrapErrorSuffix(os.Chmod(m.path, m.perm), return sys.wrapErrSuffix(os.Chmod(m.path, m.perm),
fmt.Sprintf("cannot change mode of %q to %s:", m.path, m.perm)) fmt.Sprintf("cannot change mode of %q to %s:", m.path, m.perm))
} }
func (m *Mkdir) revert(_ *I, ec *Criteria) error { func (m *Mkdir) revert(sys *I, ec *Criteria) error {
if !m.ephemeral { if !m.ephemeral {
// skip non-ephemeral dir and do not log anything // skip non-ephemeral dir and do not log anything
return nil return nil
} }
if ec.hasType(m) { if ec.hasType(m) {
fmsg.Verbose("destroying ephemeral directory", m) sys.println("destroying ephemeral directory", m)
return fmsg.WrapErrorSuffix(os.Remove(m.path), return sys.wrapErrSuffix(os.Remove(m.path),
fmt.Sprintf("cannot remove ephemeral directory %q:", m.path)) fmt.Sprintf("cannot remove ephemeral directory %q:", m.path))
} else { } else {
fmsg.Verbose("skipping ephemeral directory", m) sys.println("skipping ephemeral directory", m)
return nil return nil
} }
} }

View File

@ -4,10 +4,7 @@ import (
"context" "context"
"errors" "errors"
"log" "log"
"os"
"sync" "sync"
"git.gensokyo.uk/security/fortify/internal/fmsg"
) )
const ( const (
@ -56,12 +53,26 @@ func TypeString(e Enablement) string {
} }
} }
// New initialises sys with no-op verbose functions.
func New(uid int) (sys *I) {
sys = new(I)
sys.uid = uid
sys.IsVerbose = func() bool { return false }
sys.Verbose = func(...any) {}
sys.Verbosef = func(string, ...any) {}
sys.WrapErr = func(err error, _ ...any) error { return err }
return
}
type I struct { type I struct {
uid int uid int
ops []Op ops []Op
ctx context.Context ctx context.Context
// sync fd passed to bwrap
sp *os.File IsVerbose func() bool
Verbose func(v ...any)
Verbosef func(format string, v ...any)
WrapErr func(err error, a ...any) error
// whether sys has been reverted // whether sys has been reverted
state bool state bool
@ -69,12 +80,15 @@ type I struct {
lock sync.Mutex lock sync.Mutex
} }
func (sys *I) UID() int { func (sys *I) UID() int { return sys.uid }
return sys.uid func (sys *I) println(v ...any) { sys.Verbose(v...) }
} func (sys *I) printf(format string, v ...any) { sys.Verbosef(format, v...) }
func (sys *I) wrapErr(err error, a ...any) error { return sys.WrapErr(err, a...) }
func (sys *I) Sync() *os.File { func (sys *I) wrapErrSuffix(err error, a ...any) error {
return sys.sp if err == nil {
return nil
}
return sys.wrapErr(err, append(a, err)...)
} }
func (sys *I) Equal(v *I) bool { func (sys *I) Equal(v *I) bool {
@ -106,7 +120,7 @@ func (sys *I) Commit(ctx context.Context) error {
// sp is set to nil when all ops are applied // sp is set to nil when all ops are applied
if sp != nil { if sp != nil {
// rollback partial commit // rollback partial commit
fmsg.Verbosef("commit faulted after %d ops, rolling back partial commit", len(sp.ops)) sys.printf("commit faulted after %d ops, rolling back partial commit", len(sp.ops))
if err := sp.Revert(&Criteria{nil}); err != nil { if err := sp.Revert(&Criteria{nil}); err != nil {
log.Println("errors returned reverting partial commit:", err) log.Println("errors returned reverting partial commit:", err)
} }
@ -146,7 +160,3 @@ func (sys *I) Revert(ec *Criteria) error {
// errors.Join filters nils // errors.Join filters nils
return errors.Join(errs...) return errors.Join(errs...)
} }
func New(uid int) *I {
return &I{uid: uid}
}

View File

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

View File

@ -2,12 +2,11 @@ package system
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
"syscall" "syscall"
"git.gensokyo.uk/security/fortify/internal/fmsg"
) )
// CopyFile registers an Op that copies from src. // CopyFile registers an Op that copies from src.
@ -32,29 +31,34 @@ type Tmpfile struct {
} }
func (t *Tmpfile) Type() Enablement { return Process } func (t *Tmpfile) Type() Enablement { return Process }
func (t *Tmpfile) apply(_ *I) error { func (t *Tmpfile) apply(sys *I) error {
fmsg.Verbose("copying", t) sys.println("copying", t)
if t.payload == nil {
// this is a misuse of the API; do not return an error message
return errors.New("invalid payload")
}
if b, err := os.Stat(t.src); err != nil { if b, err := os.Stat(t.src); err != nil {
return fmsg.WrapErrorSuffix(err, return sys.wrapErrSuffix(err,
fmt.Sprintf("cannot stat %q:", t.src)) fmt.Sprintf("cannot stat %q:", t.src))
} else { } else {
if b.IsDir() { if b.IsDir() {
return fmsg.WrapErrorSuffix(syscall.EISDIR, return sys.wrapErrSuffix(syscall.EISDIR,
fmt.Sprintf("%q is a directory", t.src)) fmt.Sprintf("%q is a directory", t.src))
} }
if s := b.Size(); s > t.n { if s := b.Size(); s > t.n {
return fmsg.WrapErrorSuffix(syscall.ENOMEM, return sys.wrapErrSuffix(syscall.ENOMEM,
fmt.Sprintf("file %q is too long: %d > %d", fmt.Sprintf("file %q is too long: %d > %d",
t.src, s, t.n)) t.src, s, t.n))
} }
} }
if f, err := os.Open(t.src); err != nil { if f, err := os.Open(t.src); err != nil {
return fmsg.WrapErrorSuffix(err, return sys.wrapErrSuffix(err,
fmt.Sprintf("cannot open %q:", t.src)) fmt.Sprintf("cannot open %q:", t.src))
} else if _, err = io.CopyN(t.buf, f, t.n); err != nil { } else if _, err = io.CopyN(t.buf, f, t.n); err != nil {
return fmsg.WrapErrorSuffix(err, return sys.wrapErrSuffix(err,
fmt.Sprintf("cannot read from %q:", t.src)) fmt.Sprintf("cannot read from %q:", t.src))
} }

89
system/wayland.go Normal file
View File

@ -0,0 +1,89 @@
package system
import (
"errors"
"fmt"
"os"
"git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/wl"
)
// Wayland sets up a wayland socket with a security context attached.
func (sys *I) Wayland(syncFd **os.File, dst, src, appID, instanceID string) *I {
sys.lock.Lock()
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &Wayland{syncFd, dst, src, appID, instanceID, wl.Conn{}})
return sys
}
type Wayland struct {
sync **os.File
dst, src string
appID, instanceID string
conn wl.Conn
}
func (w *Wayland) Type() Enablement { return Process }
func (w *Wayland) apply(sys *I) error {
if w.sync == nil {
// this is a misuse of the API; do not return an error message
return errors.New("invalid sync")
}
// the Wayland op is not repeatable
if *w.sync != nil {
// this is a misuse of the API; do not return an error message
return errors.New("attempted to attach multiple wayland sockets")
}
if err := w.conn.Attach(w.src); err != nil {
// make console output less nasty
if errors.Is(err, os.ErrNotExist) {
err = os.ErrNotExist
}
return sys.wrapErrSuffix(err,
fmt.Sprintf("cannot attach to wayland on %q:", w.src))
} else {
sys.printf("wayland attached on %q", w.src)
}
if sp, err := w.conn.Bind(w.dst, w.appID, w.instanceID); err != nil {
return sys.wrapErrSuffix(err,
fmt.Sprintf("cannot bind to socket on %q:", w.dst))
} else {
*w.sync = sp
sys.printf("wayland listening on %q", w.dst)
return sys.wrapErrSuffix(errors.Join(os.Chmod(w.dst, 0), acl.Update(w.dst, sys.uid, acl.Read, acl.Write, acl.Execute)),
fmt.Sprintf("cannot chmod socket on %q:", w.dst))
}
}
func (w *Wayland) revert(sys *I, ec *Criteria) error {
if ec.hasType(w) {
sys.printf("removing wayland socket on %q", w.dst)
if err := os.Remove(w.dst); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
sys.printf("detaching from wayland on %q", w.src)
return sys.wrapErrSuffix(w.conn.Close(),
fmt.Sprintf("cannot detach from wayland on %q:", w.src))
} else {
sys.printf("skipping wayland cleanup on %q", w.dst)
return nil
}
}
func (w *Wayland) Is(o Op) bool {
w0, ok := o.(*Wayland)
return ok && w.dst == w0.dst && w.src == w0.src &&
w.appID == w0.appID && w.instanceID == w0.instanceID
}
func (w *Wayland) Path() string { return w.dst }
func (w *Wayland) String() string { return fmt.Sprintf("wayland socket at %q", w.dst) }

View File

@ -3,8 +3,7 @@ package system
import ( import (
"fmt" "fmt"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/system/internal/xcb"
"git.gensokyo.uk/security/fortify/xcb"
) )
// ChangeHosts appends an X11 ChangeHosts command Op. // ChangeHosts appends an X11 ChangeHosts command Op.
@ -23,19 +22,19 @@ func (x XHost) Type() Enablement {
return EX11 return EX11
} }
func (x XHost) apply(_ *I) error { func (x XHost) apply(sys *I) error {
fmsg.Verbosef("inserting entry %s to X11", x) sys.printf("inserting entry %s to X11", x)
return fmsg.WrapErrorSuffix(xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), return sys.wrapErrSuffix(xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)),
fmt.Sprintf("cannot insert entry %s to X11:", x)) fmt.Sprintf("cannot insert entry %s to X11:", x))
} }
func (x XHost) revert(_ *I, ec *Criteria) error { func (x XHost) revert(sys *I, ec *Criteria) error {
if ec.hasType(x) { if ec.hasType(x) {
fmsg.Verbosef("deleting entry %s from X11", x) sys.printf("deleting entry %s from X11", x)
return fmsg.WrapErrorSuffix(xcb.ChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), return sys.wrapErrSuffix(xcb.ChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)),
fmt.Sprintf("cannot delete entry %s from X11:", x)) fmt.Sprintf("cannot delete entry %s from X11:", x))
} else { } else {
fmsg.Verbosef("skipping entry %s in X11", x) sys.printf("skipping entry %s in X11", x)
return nil return nil
} }
} }