Compare commits

...

7 Commits

Author SHA1 Message Date
195b717e01
release: 0.2.5
All checks were successful
Tests / Go tests (push) Successful in 49s
Create distribution / Release (push) Successful in 1m6s
Nix / NixOS tests (push) Successful in 1m23s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-20 00:28:48 +09:00
df6fc298f6
migrate to git.gensokyo.uk/security/fortify
All checks were successful
Tests / Go tests (push) Successful in 2m55s
Nix / NixOS tests (push) Successful in 5m10s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-20 00:20:02 +09:00
eae3034260
state: expose aids and use instance id as key
All checks were successful
Tests / Go tests (push) Successful in 39s
Nix / NixOS tests (push) Successful in 3m26s
Fortify state store instances was specific to aids due to outdated design decisions carried over from the ego rewrite. That no longer makes sense in the current application, so the interface now enables a single store object to manage all transient state.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-19 21:36:17 +09:00
5ea7333431
fst: implement app id parser
All checks were successful
Tests / Go tests (push) Successful in 40s
Nix / NixOS tests (push) Successful in 3m8s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-19 18:19:47 +09:00
f796622c35
state: rename simple store implementation
All checks were successful
Tests / Go tests (push) Successful in 42s
Nix / NixOS tests (push) Successful in 3m4s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-19 11:48:48 +09:00
5d25bee786
fortify: remove systemd check
All checks were successful
Tests / Go tests (push) Successful in 38s
Nix / NixOS tests (push) Successful in 3m3s
This is no longer necessary as fortify no longer integrates with external user switchers.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-19 11:14:31 +09:00
b48ece3bb0
acl: use test-managed tmpdir
All checks were successful
Tests / Go tests (push) Successful in 44s
Nix / NixOS tests (push) Successful in 3m7s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-19 11:08:13 +09:00
67 changed files with 732 additions and 476 deletions

View File

@ -1,8 +1,8 @@
Fortify Fortify
======= =======
[![Go Reference](https://pkg.go.dev/badge/git.ophivana.moe/security/fortify.svg)](https://pkg.go.dev/git.ophivana.moe/security/fortify) [![Go Reference](https://pkg.go.dev/badge/git.gensokyo.uk/security/fortify.svg)](https://pkg.go.dev/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) [![Go Report Card](https://goreportcard.com/badge/git.gensokyo.uk/security/fortify)](https://goreportcard.com/report/git.gensokyo.uk/security/fortify)
Lets you run graphical applications as another user in a confined environment with a nice NixOS Lets you run graphical applications as another user in a confined environment with a nice NixOS
module to configure target users and provide launchers and desktop files for your privileged user. module to configure target users and provide launchers and desktop files for your privileged user.
@ -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.ophivana.moe/security/fortify -- help nix run git+https://git.gensokyo.uk/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.ophivana.moe/security/fortify"; url = "git+https://git.gensokyo.uk/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

@ -7,7 +7,7 @@ import (
"reflect" "reflect"
"testing" "testing"
"git.ophivana.moe/security/fortify/acl" "git.gensokyo.uk/security/fortify/acl"
) )
const testFileName = "acl.test" const testFileName = "acl.test"
@ -15,8 +15,6 @@ const testFileName = "acl.test"
var ( var (
uid = os.Geteuid() uid = os.Geteuid()
cred = int32(os.Geteuid()) cred = int32(os.Geteuid())
testFilePath = path.Join(os.TempDir(), testFileName)
) )
func TestUpdatePerm(t *testing.T) { func TestUpdatePerm(t *testing.T) {
@ -25,6 +23,8 @@ func TestUpdatePerm(t *testing.T) {
t.SkipNow() t.SkipNow()
} }
testFilePath := path.Join(t.TempDir(), testFileName)
if f, err := os.Create(testFilePath); err != nil { if f, err := os.Create(testFilePath); err != nil {
t.Fatalf("Create: error = %v", err) t.Fatalf("Create: error = %v", err)
} else { } else {
@ -64,16 +64,16 @@ func TestUpdatePerm(t *testing.T) {
} }
}) })
testUpdate(t, "r--", cur, fAclPermRead, acl.Read) testUpdate(t, testFilePath, "r--", cur, fAclPermRead, acl.Read)
testUpdate(t, "-w-", cur, fAclPermWrite, acl.Write) testUpdate(t, testFilePath, "-w-", cur, fAclPermWrite, acl.Write)
testUpdate(t, "--x", cur, fAclPermExecute, acl.Execute) testUpdate(t, testFilePath, "--x", cur, fAclPermExecute, acl.Execute)
testUpdate(t, "-wx", cur, fAclPermWrite|fAclPermExecute, acl.Write, acl.Execute) testUpdate(t, testFilePath, "-wx", cur, fAclPermWrite|fAclPermExecute, acl.Write, acl.Execute)
testUpdate(t, "r-x", cur, fAclPermRead|fAclPermExecute, acl.Read, acl.Execute) testUpdate(t, testFilePath, "r-x", cur, fAclPermRead|fAclPermExecute, acl.Read, acl.Execute)
testUpdate(t, "rw-", cur, fAclPermRead|fAclPermWrite, acl.Read, acl.Write) testUpdate(t, testFilePath, "rw-", cur, fAclPermRead|fAclPermWrite, acl.Read, acl.Write)
testUpdate(t, "rwx", cur, fAclPermRead|fAclPermWrite|fAclPermExecute, acl.Read, acl.Write, acl.Execute) testUpdate(t, testFilePath, "rwx", cur, fAclPermRead|fAclPermWrite|fAclPermExecute, acl.Read, acl.Write, acl.Execute)
} }
func testUpdate(t *testing.T, 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.UpdatePerm(testFilePath, uid); err != nil {

View File

@ -9,10 +9,10 @@ import (
"syscall" "syscall"
"time" "time"
init0 "git.ophivana.moe/security/fortify/cmd/finit/ipc" init0 "git.gensokyo.uk/security/fortify/cmd/finit/ipc"
"git.ophivana.moe/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/proc" "git.gensokyo.uk/security/fortify/internal/proc"
) )
const ( const (

View File

@ -1,7 +1,7 @@
package shim0 package shim0
import ( import (
"git.ophivana.moe/security/fortify/helper/bwrap" "git.gensokyo.uk/security/fortify/helper/bwrap"
) )
const Env = "FORTIFY_SHIM" const Env = "FORTIFY_SHIM"

View File

@ -11,10 +11,10 @@ import (
"syscall" "syscall"
"time" "time"
shim0 "git.ophivana.moe/security/fortify/cmd/fshim/ipc" shim0 "git.gensokyo.uk/security/fortify/cmd/fshim/ipc"
"git.ophivana.moe/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/proc" "git.gensokyo.uk/security/fortify/internal/proc"
) )
const shimSetupTimeout = 5 * time.Second const shimSetupTimeout = 5 * time.Second

View File

@ -7,12 +7,12 @@ import (
"strconv" "strconv"
"syscall" "syscall"
init0 "git.ophivana.moe/security/fortify/cmd/finit/ipc" init0 "git.gensokyo.uk/security/fortify/cmd/finit/ipc"
shim "git.ophivana.moe/security/fortify/cmd/fshim/ipc" shim "git.gensokyo.uk/security/fortify/cmd/fshim/ipc"
"git.ophivana.moe/security/fortify/helper" "git.gensokyo.uk/security/fortify/helper"
"git.ophivana.moe/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/proc" "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

View File

@ -9,7 +9,7 @@ import (
"path" "path"
"strconv" "strconv"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
) )
func main() { func main() {

View File

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

View File

@ -9,7 +9,7 @@ import (
"strings" "strings"
"testing" "testing"
"git.ophivana.moe/security/fortify/dbus" "git.gensokyo.uk/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.ophivana.moe/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.ophivana.moe/security/fortify/helper" "git.gensokyo.uk/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.ophivana.moe/security/fortify/helper" "git.gensokyo.uk/security/fortify/helper"
"git.ophivana.moe/security/fortify/helper/bwrap" "git.gensokyo.uk/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.ophivana.moe/security/fortify/helper" "git.gensokyo.uk/security/fortify/helper"
"git.ophivana.moe/security/fortify/helper/bwrap" "git.gensokyo.uk/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/ldd" "git.gensokyo.uk/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.ophivana.moe/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
) )
var samples = []dbusTestCase{ var samples = []dbusTestCase{

View File

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

6
dist/release.sh vendored
View File

@ -8,9 +8,9 @@ mkdir -p "${out}"
cp "README.md" "dist/fsurc.default" "dist/install.sh" "${out}" cp "README.md" "dist/fsurc.default" "dist/install.sh" "${out}"
go build -v -o "${out}/bin/" -ldflags "-s -w go build -v -o "${out}/bin/" -ldflags "-s -w
-X git.ophivana.moe/security/fortify/internal.Version=${VERSION} -X git.gensokyo.uk/security/fortify/internal.Version=${VERSION}
-X git.ophivana.moe/security/fortify/internal.Fsu=/usr/bin/fsu -X git.gensokyo.uk/security/fortify/internal.Fsu=/usr/bin/fsu
-X git.ophivana.moe/security/fortify/internal.Finit=/usr/libexec/fortify/finit -X git.gensokyo.uk/security/fortify/internal.Finit=/usr/libexec/fortify/finit
-X main.Fmain=/usr/bin/fortify -X main.Fmain=/usr/bin/fortify
-X main.Fshim=/usr/libexec/fortify/fshim" ./... -X main.Fshim=/usr/libexec/fortify/fshim" ./...

View File

@ -3,8 +3,8 @@ package main
import ( import (
"errors" "errors"
"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"
) )
func logWaitError(err error) { func logWaitError(err error) {

View File

@ -3,10 +3,10 @@ package fst
import ( import (
"errors" "errors"
"git.ophivana.moe/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"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"
) )
const fTmp = "/fortify" const fTmp = "/fortify"

48
fst/id.go Normal file
View File

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

63
fst/id_test.go Normal file
View File

@ -0,0 +1,63 @@
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,18 +1,2 @@
// Package fst exports shared fortify types. // Package fst exports shared fortify types.
package fst package fst
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
}

2
go.mod
View File

@ -1,3 +1,3 @@
module git.ophivana.moe/security/fortify module git.gensokyo.uk/security/fortify
go 1.22 go 1.22

View File

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

View File

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

View File

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

View File

@ -10,8 +10,8 @@ import (
"syscall" "syscall"
"testing" "testing"
"git.ophivana.moe/security/fortify/helper/bwrap" "git.gensokyo.uk/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.gensokyo.uk/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.ophivana.moe/security/fortify/helper" "git.gensokyo.uk/security/fortify/helper"
) )
func TestHelperChildStub(t *testing.T) { func TestHelperChildStub(t *testing.T) {

View File

@ -4,9 +4,9 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"git.ophivana.moe/security/fortify/cmd/fshim/ipc/shim" "git.gensokyo.uk/security/fortify/cmd/fshim/ipc/shim"
"git.ophivana.moe/security/fortify/fst" "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 {

View File

@ -1,11 +1,11 @@
package app_test package app_test
import ( import (
"git.ophivana.moe/security/fortify/acl" "git.gensokyo.uk/security/fortify/acl"
"git.ophivana.moe/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.ophivana.moe/security/fortify/fst" "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/system" "git.gensokyo.uk/security/fortify/internal/system"
) )
var testCasesNixos = []sealTestCase{ var testCasesNixos = []sealTestCase{

View File

@ -1,11 +1,11 @@
package app_test package app_test
import ( import (
"git.ophivana.moe/security/fortify/acl" "git.gensokyo.uk/security/fortify/acl"
"git.ophivana.moe/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.ophivana.moe/security/fortify/fst" "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/system" "git.gensokyo.uk/security/fortify/internal/system"
) )
var testCasesPd = []sealTestCase{ var testCasesPd = []sealTestCase{

View File

@ -7,7 +7,7 @@ import (
"os/user" "os/user"
"strconv" "strconv"
"git.ophivana.moe/security/fortify/internal/linux" "git.gensokyo.uk/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,11 +6,11 @@ import (
"testing" "testing"
"time" "time"
"git.ophivana.moe/security/fortify/fst" "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 {

View File

@ -1,10 +1,10 @@
package app package app
import ( import (
"git.ophivana.moe/security/fortify/fst" "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 fst.ID, os linux.System) App {

View File

@ -8,12 +8,12 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"git.ophivana.moe/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.ophivana.moe/security/fortify/fst" "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 (
@ -218,7 +218,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.NewSimple(seal.RunDirPath, seal.sys.user.as) seal.store = state.NewMulti(seal.RunDirPath)
// 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)

View File

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

View File

@ -4,10 +4,10 @@ import (
"errors" "errors"
"path" "path"
"git.ophivana.moe/security/fortify/acl" "git.gensokyo.uk/security/fortify/acl"
"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/system" "git.gensokyo.uk/security/fortify/internal/system"
) )
const ( const (

View File

@ -6,9 +6,9 @@ import (
"io/fs" "io/fs"
"path" "path"
"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/system" "git.gensokyo.uk/security/fortify/internal/system"
) )
const ( const (

View File

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

View File

@ -3,9 +3,9 @@ package app
import ( import (
"path" "path"
"git.ophivana.moe/security/fortify/acl" "git.gensokyo.uk/security/fortify/acl"
"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"
) )
const ( const (

View File

@ -7,12 +7,12 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
shim0 "git.ophivana.moe/security/fortify/cmd/fshim/ipc" shim0 "git.gensokyo.uk/security/fortify/cmd/fshim/ipc"
"git.ophivana.moe/security/fortify/cmd/fshim/ipc/shim" "git.gensokyo.uk/security/fortify/cmd/fshim/ipc/shim"
"git.ophivana.moe/security/fortify/helper" "git.gensokyo.uk/security/fortify/helper"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"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"
) )
// Start selects a user switcher and starts shim. // Start selects a user switcher and starts shim.
@ -76,8 +76,8 @@ func (a *app) Start() error {
// register process state // register process state
var err0 = new(StateStoreError) var err0 = new(StateStoreError)
err0.Inner, err0.DoErr = a.seal.store.Do(func(b state.Backend) { err0.Inner, err0.DoErr = a.seal.store.Do(a.seal.sys.user.aid, func(c state.Cursor) {
err0.InnerErr = b.Save(&sd) err0.InnerErr = c.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 +199,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(func(b state.Backend) { e.Inner, e.DoErr = a.seal.store.Do(a.seal.sys.user.aid, func(b state.Cursor) {
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(cmd.Process.Pid); err != nil { if err := b.Destroy(*a.id); err != nil {
return err return err
} }
} }

View File

@ -1,10 +1,10 @@
package app package app
import ( import (
"git.ophivana.moe/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"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"
) )
// 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.ophivana.moe/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
) )
// System provides safe access to operating system resources. // System provides safe access to operating system resources.
@ -39,8 +39,6 @@ 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,7 +1,6 @@
package linux package linux
import ( import (
"errors"
"io" "io"
"io/fs" "io/fs"
"os" "os"
@ -10,8 +9,8 @@ import (
"strconv" "strconv"
"sync" "sync"
"git.ophivana.moe/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
) )
// Std implements System using the standard library. // Std implements System using the standard library.
@ -19,9 +18,6 @@ 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
@ -90,31 +86,3 @@ 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
}

292
internal/state/multi.go Normal file
View File

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

@ -0,0 +1,11 @@
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,62 +1,45 @@
package state package state
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"path"
"strconv"
"strings" "strings"
"text/tabwriter" "text/tabwriter"
"time" "time"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/system" "git.gensokyo.uk/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 dirs, err := os.ReadDir(path.Join(runDir, "state")); err != nil && !errors.Is(err, os.ErrNotExist) { if aids, err := s.List(); err != nil {
fmsg.Fatal("cannot read runtime directory:", err) fmsg.Fatal("cannot list store:", err)
} else { } else {
for _, e := range dirs { for _, aid := range aids {
// skip non-directories
if !e.IsDir() {
fmsg.VPrintf("skipped non-directory entry %q", e.Name())
continue
}
// skip non-numerical names
if _, err = strconv.Atoi(e.Name()); err != nil {
fmsg.VPrintf("skipped non-uid entry %q", e.Name())
continue
}
// obtain temporary store
s := NewSimple(runDir, e.Name()).(*simpleStore)
// print states belonging to this store // print states belonging to this store
s.mustPrintLauncherState(w, now) s.(*multiStore).mustPrintLauncherState(aid, 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)
}
} }
} }
// mustPrintLauncherState causes store activity so store needs to be closed
if err := s.Close(); err != nil {
fmsg.Printf("cannot close store: %v", err)
}
} }
func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time) { func (s *multiStore) mustPrintLauncherState(aid int, w **tabwriter.Writer, now time.Time) {
var innerErr error var innerErr error
if ok, err := s.Do(func(b Backend) { if ok, err := s.Do(aid, func(c Cursor) {
innerErr = func() error { innerErr = func() error {
// read launcher states // read launcher states
states, err := b.Load() states, err := c.Load()
if err != nil { if err != nil {
return err return err
} }
@ -111,25 +94,25 @@ func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time
} }
if !fmsg.Verbose() { if !fmsg.Verbose() {
_, _ = fmt.Fprintf(*w, "\t%d\t%s\t%s\t%s\t%s\n", _, _ = fmt.Fprintf(*w, "\t%d\t%d\t%s\t%s\t%s\n",
state.PID, s.path[len(s.path)-1], now.Sub(state.Time).Round(time.Second).String(), strings.TrimPrefix(ets.String(), ", "), cs) state.PID, aid, now.Sub(state.Time).Round(time.Second).String(), strings.TrimPrefix(ets.String(), ", "), cs)
} else { } else {
// emit argv instead when verbose // emit argv instead when verbose
_, _ = fmt.Fprintf(*w, "\t%d\t%s\t%s\n", _, _ = fmt.Fprintf(*w, "\t%d\t%d\t%s\n",
state.PID, s.path[len(s.path)-1], state.ID) state.PID, aid, state.ID)
} }
} }
return nil return nil
}() }()
}); err != nil { }); err != nil {
fmsg.Printf("cannot perform action on store %q: %s", path.Join(s.path...), err) fmsg.Printf("cannot perform action on app %d: %v", aid, 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 for store %q: %s", path.Join(s.path...), innerErr) fmsg.Fatalf("cannot print launcher state of app %d: %s", aid, innerErr)
} }
} }

View File

@ -1,222 +0,0 @@
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()
if state.Config == nil {
return errors.New("state does not contain config")
}
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,24 +3,30 @@ package state
import ( import (
"time" "time"
"git.ophivana.moe/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
) )
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.
// Backend provided to f becomes invalid as soon as f returns. // Cursor provided to f becomes invalid as soon as f returns.
Do(f func(b Backend)) (bool, error) Do(aid int, f func(c Cursor)) (ok bool, err 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
} }
// Backend provides access to the store // Cursor provides access to the store
type Backend interface { type Cursor interface {
Save(state *State) error Save(state *State) error
Destroy(pid int) error Destroy(id fst.ID) error
Load() ([]*State, error) Load() (Entries, error)
Len() (int, error) Len() (int, error)
} }

View File

@ -0,0 +1,126 @@
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.ophivana.moe/security/fortify/acl" "git.gensokyo.uk/security/fortify/acl"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.gensokyo.uk/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.ophivana.moe/security/fortify/acl" "git.gensokyo.uk/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.ophivana.moe/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
) )
var ( var (

View File

@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.gensokyo.uk/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.ophivana.moe/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
) )
const ( const (

View File

@ -4,7 +4,7 @@ import (
"strconv" "strconv"
"testing" "testing"
"git.ophivana.moe/security/fortify/internal/system" "git.gensokyo.uk/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.ophivana.moe/security/fortify/acl" "git.gensokyo.uk/security/fortify/acl"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.gensokyo.uk/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.ophivana.moe/security/fortify/acl" "git.gensokyo.uk/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.ophivana.moe/security/fortify/acl" "git.gensokyo.uk/security/fortify/acl"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/wl" "git.gensokyo.uk/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.ophivana.moe/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/xcb" "git.gensokyo.uk/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.ophivana.moe/security/fortify/helper" "git.gensokyo.uk/security/fortify/helper"
"git.ophivana.moe/security/fortify/helper/bwrap" "git.gensokyo.uk/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.ophivana.moe/security/fortify/ldd" "git.gensokyo.uk/security/fortify/ldd"
) )
func TestParseError(t *testing.T) { func TestParseError(t *testing.T) {

20
main.go
View File

@ -11,14 +11,14 @@ import (
"sync" "sync"
"text/tabwriter" "text/tabwriter"
"git.ophivana.moe/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.ophivana.moe/security/fortify/fst" "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 (
@ -277,10 +277,6 @@ func main() {
} }
func runApp(config *fst.Config) { func runApp(config *fst.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

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

View File

@ -14,7 +14,7 @@
buildGoModule rec { buildGoModule rec {
pname = "fortify"; pname = "fortify";
version = "0.2.4"; version = "0.2.5";
src = ./.; src = ./.;
vendorHash = null; vendorHash = null;
@ -26,7 +26,7 @@ buildGoModule rec {
ldflags ldflags
++ [ ++ [
"-X" "-X"
"git.ophivana.moe/security/fortify/internal.${name}=${value}" "git.gensokyo.uk/security/fortify/internal.${name}=${value}"
] ]
) )
[ [

View File

@ -214,5 +214,8 @@ nixosTest {
swaymsg("exit", succeed=False) swaymsg("exit", succeed=False)
machine.wait_until_fails("pgrep -x sway") machine.wait_until_fails("pgrep -x sway")
machine.wait_for_file("/tmp/sway-exit-ok") machine.wait_for_file("/tmp/sway-exit-ok")
# Print fortify runDir contents:
print(machine.succeed("find /run/user/1000/fortify"))
''; '';
} }