Compare commits

...

23 Commits

Author SHA1 Message Date
90b86a5531
release: 0.2.14
All checks were successful
Release / Create release (push) Successful in 24s
Test / Create distribution (push) Successful in 18s
Test / Run NixOS test (push) Successful in 50s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 23:05:02 +09:00
f545e154f0
workflows: use native nix runner
All checks were successful
Test / Create distribution (push) Successful in 20s
Test / Run NixOS test (push) Successful in 51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 22:58:04 +09:00
268a90f1a5
app: improve WAYLAND_DISPLAY correctness
All checks were successful
Test / Create distribution (push) Successful in 46s
Test / Run NixOS test (push) Successful in 3m35s
This now has identical behaviour as wayland C library.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 14:45:09 +09:00
3054527ca5
fortify: prevent exit status 0 on app failure
All checks were successful
Test / Create distribution (push) Successful in 46s
Test / Run NixOS test (push) Successful in 3m37s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 14:40:19 +09:00
ddb2f9c11b
app: remove wayland socket hard link
All checks were successful
Test / Create distribution (push) Successful in 49s
Test / Run NixOS test (push) Successful in 3m32s
This Op was not doing anything useful.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 10:54:00 +09:00
6ae02e72fa
nix: test direct_wayland behaviour
All checks were successful
Test / Create distribution (push) Successful in 47s
Test / Run NixOS test (push) Successful in 3m35s
This should never be used outside tests unless you absolutely know what you're doing or are using GNOME.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 10:45:27 +09:00
989fb5395f
nix: remove unused configuration
All checks were successful
Test / Create distribution (push) Successful in 49s
Test / Run NixOS test (push) Successful in 3m30s
User setup no longer depends on userdb.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 10:10:42 +09:00
f955b15b84
system: remove write mode tmpfiles
All checks were successful
Test / Create distribution (push) Successful in 57s
Test / Run NixOS test (push) Successful in 3m42s
This interface is ugly and bug-prone. This change removes its write mode which has been obsoleted by CopyBind.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 03:22:20 +09:00
0340c67995
app: port passwd and group files to copy
All checks were successful
Test / Create distribution (push) Successful in 49s
Test / Run NixOS test (push) Successful in 3m41s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 03:19:06 +09:00
72b0160aad
helper/bwrap: implement file copy flags
All checks were successful
Test / Create distribution (push) Successful in 49s
Test / Run NixOS test (push) Successful in 3m42s
These are significantly more efficient and less error-prone than mounting an external tmpfile. This should also reduce attack surface as the resulting files are private to its specific sandbox.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 03:13:15 +09:00
ea8d1c07df
priv/shim: move /sbin/init setup to app
All checks were successful
Test / Create distribution (push) Successful in 49s
Test / Run NixOS test (push) Successful in 3m36s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 03:06:10 +09:00
a0062d8275
fmsg: resume on exit
All checks were successful
Test / Create distribution (push) Successful in 47s
Test / Run NixOS test (push) Successful in 3m32s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 02:22:09 +09:00
43d2e4f5d7
nix: sway increase resolution
All checks were successful
Test / Create distribution (push) Successful in 50s
Test / Run NixOS test (push) Successful in 3m46s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 02:21:24 +09:00
be7d944b39
helper/bwrap: PositionalArg implement fmt.Stringer
All checks were successful
Test / Create distribution (push) Successful in 49s
Test / Run NixOS test (push) Successful in 3m28s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 00:11:48 +09:00
ace97952cc
helper/bwrap: merge Args and FDArgs
All checks were successful
Test / Create distribution (push) Successful in 1m13s
Test / Run NixOS test (push) Successful in 4m34s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-14 18:13:06 +09:00
73146ea7fa
dbus: remove BwrapStatic method
All checks were successful
Test / Create distribution (push) Successful in 54s
Test / Run NixOS test (push) Successful in 8m20s
This method does not do anything and is not called from anywhere. It also does not make any sense as a public interface since the argument builder is no longer stateless.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-14 18:09:59 +09:00
88040504b2
helper/bwrap: remove fmsg import
All checks were successful
Test / Create distribution (push) Successful in 57s
Test / Run NixOS test (push) Successful in 8m13s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-14 18:05:00 +09:00
1fd571d561
cmd/fsu: check parse behaviour
All checks were successful
Test / Run NixOS test (push) Successful in 10m33s
Test / Create distribution (push) Successful in 1m8s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-14 16:43:55 +09:00
be30e2f11e
cmd/fsu: revert offset in error message
All checks were successful
Test / Create distribution (push) Successful in 51s
Test / Run NixOS test (push) Successful in 3m39s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-14 15:31:39 +09:00
aaebb8f3ab
fortify: check print behaviour
All checks were successful
Test / Create distribution (push) Successful in 1m10s
Test / Run NixOS test (push) Successful in 3m59s
These output are supposed to be deterministic, so checking them is a good way to catch regressions.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-14 14:44:28 +09:00
1f74b636d3
state/join: use Join method when available
All checks were successful
Test / Create distribution (push) Successful in 1m4s
Test / Run NixOS test (push) Successful in 4m11s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-14 14:11:02 +09:00
e431ab3c24
app: check username length against LOGIN_NAME_MAX
All checks were successful
Test / Create distribution (push) Successful in 49s
Test / Run NixOS test (push) Successful in 3m46s
This limit is arbitrary, but it's good to enforce it anyway.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-14 12:44:55 +09:00
3fba33687b
fortify: print line after ps output
All checks were successful
Test / Create distribution (push) Successful in 1m43s
Test / Run NixOS test (push) Successful in 9m18s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-14 12:23:20 +09:00
37 changed files with 1283 additions and 548 deletions

View File

@ -8,39 +8,11 @@ on:
jobs:
release:
name: Create release
runs-on: ubuntu-latest
permissions:
actions: write
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup go
uses: actions/setup-go@v5
with:
go-version: '>=1.23.0'
- name: Install Nix
uses: cachix/install-nix-action@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: build-dist-${{ runner.os }}-${{ hashFiles('**/*.nix') }}
restore-prefixes-first-match: build-dist-${{ runner.os }}-
- name: Build for release
run: nix build --print-out-paths --print-build-logs .#dist

View File

@ -7,39 +7,11 @@ on:
jobs:
test:
name: Run NixOS test
runs-on: ubuntu-latest
permissions:
actions: write
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@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: flake-check-${{ runner.os }}-${{ hashFiles('**/*.nix') }}
restore-prefixes-first-match: flake-check-${{ runner.os }}-
gc-max-store-size-linux: 1073741824
purge: true
purge-prefixes: flake-check-${{ runner.os }}-
purge-created: 60
purge-primary-key: never
- name: Run tests
run: |
nix --print-build-logs --experimental-features 'nix-command flakes' flake check
@ -54,39 +26,11 @@ jobs:
dist:
name: Create distribution
runs-on: ubuntu-latest
permissions:
actions: write
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@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: build-dist-${{ runner.os }}-${{ hashFiles('**/*.nix') }}
restore-prefixes-first-match: build-dist-${{ runner.os }}-
gc-max-store-size-linux: 1073741824
purge: true
purge-prefixes: build-dist-${{ runner.os }}-
purge-created: 60
purge-primary-key: never
- name: Build for test
id: build-test
run: >-

View File

@ -61,8 +61,19 @@ func main() {
// aid
uid := 1000000
// refuse to run if fsurc is not protected correctly
if s, err := os.Stat(fsuConfFile); err != nil {
log.Fatal(err)
} else if s.Mode().Perm() != 0400 {
log.Fatal("bad fsurc perm")
} else if st := s.Sys().(*syscall.Stat_t); st.Uid != 0 || st.Gid != 0 {
log.Fatal("fsurc must be owned by uid 0")
}
// authenticate before accepting user input
if fid, ok := parseConfig(fsuConfFile, puid); !ok {
if f, err := os.Open(fsuConfFile); err != nil {
log.Fatal(err)
} else if fid, ok := mustParseConfig(f, puid); !ok {
log.Fatalf("uid %d is not in the fsurc file", puid)
} else {
uid += fid * 10000

View File

@ -4,10 +4,9 @@ import (
"bufio"
"errors"
"fmt"
"io"
"log"
"os"
"strings"
"syscall"
)
func parseUint32Fast(s string) (int, error) {
@ -23,55 +22,46 @@ func parseUint32Fast(s string) (int, error) {
for i, ch := range []byte(s) {
ch -= '0'
if ch > 9 {
return -1, fmt.Errorf("invalid character '%s' at index %d", string([]byte{ch}), i)
return -1, fmt.Errorf("invalid character '%s' at index %d", string(ch+'0'), i)
}
n = n*10 + int(ch)
}
return n, nil
}
func parseConfig(p string, puid int) (fid int, ok bool) {
// refuse to run if fsurc is not protected correctly
if s, err := os.Stat(p); err != nil {
log.Fatal(err)
} else if s.Mode().Perm() != 0400 {
log.Fatal("bad fsurc perm")
} else if st := s.Sys().(*syscall.Stat_t); st.Uid != 0 || st.Gid != 0 {
log.Fatal("fsurc must be owned by uid 0")
}
func parseConfig(r io.Reader, puid int) (fid int, ok bool, err error) {
s := bufio.NewScanner(r)
var line, puid0 int
for s.Scan() {
line++
if r, err := os.Open(p); err != nil {
log.Fatal(err)
return -1, false
} else {
s := bufio.NewScanner(r)
var line int
for s.Scan() {
line++
// <puid> <fid>
lf := strings.SplitN(s.Text(), " ", 2)
if len(lf) != 2 {
log.Fatalf("invalid entry on line %d", line)
}
var puid0 int
if puid0, err = parseUint32Fast(lf[0]); err != nil || puid0 < 1 {
log.Fatalf("invalid parent uid on line %d", line)
}
ok = puid0 == puid
if ok {
// allowed fid range 0 to 99
if fid, err = parseUint32Fast(lf[1]); err != nil || fid < 0 || fid > 99 {
log.Fatalf("invalid fortify uid on line %d", line)
}
return
}
// <puid> <fid>
lf := strings.SplitN(s.Text(), " ", 2)
if len(lf) != 2 {
return -1, false, fmt.Errorf("invalid entry on line %d", line)
}
if err = s.Err(); err != nil {
log.Fatalf("cannot read fsurc: %v", err)
puid0, err = parseUint32Fast(lf[0])
if err != nil || puid0 < 1 {
return -1, false, fmt.Errorf("invalid parent uid on line %d", line)
}
ok = puid0 == puid
if ok {
// allowed fid range 0 to 99
if fid, err = parseUint32Fast(lf[1]); err != nil || fid < 0 || fid > 99 {
return -1, false, fmt.Errorf("invalid fortify uid on line %d", line)
}
return
}
return -1, false
}
return -1, false, s.Err()
}
func mustParseConfig(r io.Reader, puid int) (int, bool) {
fid, ok, err := parseConfig(r, puid)
if err != nil {
log.Fatal(err)
}
return fid, ok
}

96
cmd/fsu/parse_test.go Normal file
View File

@ -0,0 +1,96 @@
package main
import (
"bytes"
"strconv"
"testing"
)
func Test_parseUint32Fast(t *testing.T) {
t.Run("zero-length", func(t *testing.T) {
if _, err := parseUint32Fast(""); err == nil || err.Error() != "zero length string" {
t.Errorf(`parseUint32Fast(""): error = %v`, err)
return
}
})
t.Run("overflow", func(t *testing.T) {
if _, err := parseUint32Fast("10000000000"); err == nil || err.Error() != "string too long" {
t.Errorf("parseUint32Fast: error = %v", err)
return
}
})
t.Run("invalid byte", func(t *testing.T) {
if _, err := parseUint32Fast("meow"); err == nil || err.Error() != "invalid character 'm' at index 0" {
t.Errorf(`parseUint32Fast("meow"): error = %v`, err)
return
}
})
t.Run("full range", func(t *testing.T) {
testRange := func(i, end int) {
for ; i < end; i++ {
s := strconv.Itoa(i)
w := i
t.Run("parse "+s, func(t *testing.T) {
t.Parallel()
v, err := parseUint32Fast(s)
if err != nil {
t.Errorf("parseUint32Fast(%q): error = %v",
s, err)
return
}
if v != w {
t.Errorf("parseUint32Fast(%q): got %v",
s, v)
return
}
})
}
}
testRange(0, 5000)
testRange(105000, 110000)
testRange(23005000, 23010000)
testRange(456005000, 456010000)
testRange(7890005000, 7890010000)
})
}
func Test_parseConfig(t *testing.T) {
testCases := []struct {
name string
puid, want int
wantErr string
rc string
}{
{"empty", 0, -1, "", ``},
{"invalid field", 0, -1, "invalid entry on line 1", `9`},
{"invalid puid", 0, -1, "invalid parent uid on line 1", `f 9`},
{"invalid fid", 1000, -1, "invalid fortify uid on line 1", `1000 f`},
{"match", 1000, 0, "", `1000 0`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
fid, ok, err := parseConfig(bytes.NewBufferString(tc.rc), tc.puid)
if err == nil && tc.wantErr != "" {
t.Errorf("parseConfig: error = %v; wantErr %q",
err, tc.wantErr)
return
}
if err != nil && err.Error() != tc.wantErr {
t.Errorf("parseConfig: error = %q; wantErr %q",
err, tc.wantErr)
return
}
if ok == (tc.want == -1) {
t.Errorf("parseConfig: ok = %v; want %v",
ok, tc.want)
return
}
if fid != tc.want {
t.Errorf("parseConfig: fid = %v; want %v",
fid, tc.want)
}
})
}
}

View File

@ -59,17 +59,6 @@ func (p *Proxy) String() string {
return "(unsealed dbus proxy)"
}
// BwrapStatic builds static bwrap args. This omits any fd-dependant args.
func (p *Proxy) BwrapStatic() []string {
p.lock.RLock()
defer p.lock.RUnlock()
if p.bwrap == nil {
return nil
}
return p.bwrap.Args()
}
// Seal seals the Proxy instance.
func (p *Proxy) Seal(session, system *Config) error {
p.lock.Lock()

View File

@ -75,9 +75,7 @@ func NewBwrap(
b.name = name
b.helperCmd = newHelperCmd(b, BubblewrapName, wt, argF, extraFiles)
args := conf.Args()
conf.FDArgs(syncFd, &args, b.extraFiles, &b.files)
if v, err := NewCheckedArgs(args); err != nil {
if v, err := NewCheckedArgs(conf.Args(syncFd, b.extraFiles, &b.files)); err != nil {
return nil, err
} else {
f := proc.NewWriterTo(v)

View File

@ -23,42 +23,22 @@ type FDBuilder interface {
}
// Args returns a slice of bwrap args corresponding to c.
func (c *Config) Args() (args []string) {
func (c *Config) Args(syncFd *os.File, extraFiles *proc.ExtraFilesPre, files *[]proc.File) (args []string) {
builders := []Builder{
c.boolArgs(),
c.intArgs(),
c.stringArgs(),
c.pairArgs(),
}
// copy FSBuilder slice to builder slice
fb := make([]Builder, len(c.Filesystem)+1)
for i, f := range c.Filesystem {
fb[i] = f
}
fb[len(fb)-1] = c.Chmod
builders = append(builders, fb...)
// accumulate arg count
argc := 0
for _, b := range builders {
argc += b.Len()
}
args = make([]string, 0, argc)
for _, b := range builders {
b.Append(&args)
}
return
}
func (c *Config) FDArgs(syncFd *os.File, args *[]string, extraFiles *proc.ExtraFilesPre, files *[]proc.File) {
builders := []FDBuilder{
c.seccompArgs(),
newFile(positionalArgs[SyncFd], syncFd),
newFile(SyncFd.String(), syncFd),
}
builders = slices.Grow(builders, len(c.Filesystem)+1)
for _, f := range c.Filesystem {
builders = append(builders, f)
}
builders = append(builders, c.Chmod)
argc := 0
fc := 0
for _, b := range builders {
@ -67,22 +47,26 @@ func (c *Config) FDArgs(syncFd *os.File, args *[]string, extraFiles *proc.ExtraF
continue
}
argc += l
fc++
proc.InitFile(b, extraFiles)
if f, ok := b.(FDBuilder); ok {
fc++
proc.InitFile(f, extraFiles)
}
}
fc++ // allocate extra slot for stat fd
*args = slices.Grow(*args, argc)
*files = slices.Grow(*files, fc)
args = make([]string, 0, argc)
*files = slices.Grow(*files, fc)
for _, b := range builders {
if b.Len() < 1 {
continue
}
b.Append(&args)
b.Append(args)
*files = append(*files, b)
if f, ok := b.(FDBuilder); ok {
*files = append(*files, f)
}
}
return
}

View File

@ -1,6 +1,8 @@
package bwrap
import "os"
import (
"os"
)
/*
Bind binds mount src on host to dest in sandbox.
@ -39,60 +41,83 @@ func (c *Config) Bind(src, dest string, opts ...bool) *Config {
if dev {
if try {
c.Filesystem = append(c.Filesystem, &pairF{DevBindTry.Unwrap(), src, dest})
c.Filesystem = append(c.Filesystem, &pairF{DevBindTry.String(), src, dest})
} else {
c.Filesystem = append(c.Filesystem, &pairF{DevBind.Unwrap(), src, dest})
c.Filesystem = append(c.Filesystem, &pairF{DevBind.String(), src, dest})
}
return c
} else if write {
if try {
c.Filesystem = append(c.Filesystem, &pairF{BindTry.Unwrap(), src, dest})
c.Filesystem = append(c.Filesystem, &pairF{BindTry.String(), src, dest})
} else {
c.Filesystem = append(c.Filesystem, &pairF{Bind.Unwrap(), src, dest})
c.Filesystem = append(c.Filesystem, &pairF{Bind.String(), src, dest})
}
return c
} else {
if try {
c.Filesystem = append(c.Filesystem, &pairF{ROBindTry.Unwrap(), src, dest})
c.Filesystem = append(c.Filesystem, &pairF{ROBindTry.String(), src, dest})
} else {
c.Filesystem = append(c.Filesystem, &pairF{ROBind.Unwrap(), src, dest})
c.Filesystem = append(c.Filesystem, &pairF{ROBind.String(), src, dest})
}
return c
}
}
// Write copy from FD to destination DEST
// (--file FD DEST)
func (c *Config) Write(dest string, payload []byte) *Config {
c.Filesystem = append(c.Filesystem, &DataConfig{Dest: dest, Data: payload, Type: DataWrite})
return c
}
/*
CopyBind copy from FD to file which is readonly bind-mounted on DEST
(--ro-bind-data FD DEST)
CopyBind(dest, payload, true) copy from FD to file which is bind-mounted on DEST
(--bind-data FD DEST)
*/
func (c *Config) CopyBind(dest string, payload []byte, opts ...bool) *Config {
t := DataROBind
if len(opts) > 0 && opts[0] {
t = DataBind
}
c.Filesystem = append(c.Filesystem, &DataConfig{Dest: dest, Data: payload, Type: t})
return c
}
// Dir create dir in sandbox
// (--dir DEST)
func (c *Config) Dir(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{Dir.Unwrap(), dest})
c.Filesystem = append(c.Filesystem, &stringF{Dir.String(), dest})
return c
}
// RemountRO remount path as readonly; does not recursively remount
// (--remount-ro DEST)
func (c *Config) RemountRO(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{RemountRO.Unwrap(), dest})
c.Filesystem = append(c.Filesystem, &stringF{RemountRO.String(), dest})
return c
}
// Procfs mount new procfs in sandbox
// (--proc DEST)
func (c *Config) Procfs(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{Procfs.Unwrap(), dest})
c.Filesystem = append(c.Filesystem, &stringF{Procfs.String(), dest})
return c
}
// DevTmpfs mount new dev in sandbox
// (--dev DEST)
func (c *Config) DevTmpfs(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{DevTmpfs.Unwrap(), dest})
c.Filesystem = append(c.Filesystem, &stringF{DevTmpfs.String(), dest})
return c
}
// Mqueue mount new mqueue in sandbox
// (--mqueue DEST)
func (c *Config) Mqueue(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{Mqueue.Unwrap(), dest})
c.Filesystem = append(c.Filesystem, &stringF{Mqueue.String(), dest})
return c
}

View File

@ -71,9 +71,6 @@ type Config struct {
--ro-bind-fd FD DEST Bind open directory or path fd read-only on DEST
--exec-label LABEL Exec label for the sandbox
--file-label LABEL File label for temporary sandbox content
--file FD DEST Copy from FD to destination DEST
--bind-data FD DEST Copy from FD to file which is bind-mounted on DEST
--ro-bind-data FD DEST Copy from FD to file which is readonly bind-mounted on DEST
--add-seccomp-fd FD Load and use seccomp rules from FD (repeatable)
--block-fd FD Block on FD until some data to read is available
--userns-block-fd FD Block on FD until the user namespace is ready

View File

@ -6,24 +6,29 @@ import (
"testing"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/helper/seccomp"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
func TestConfig_Args(t *testing.T) {
seccomp.CPrintln = fmsg.Println
t.Cleanup(func() { seccomp.CPrintln = nil })
testCases := []struct {
name string
conf *bwrap.Config
want []string
}{
{
name: "bind",
conf: (new(bwrap.Config)).
"bind", (new(bwrap.Config)).
Bind("/etc", "/.fortify/etc").
Bind("/etc", "/.fortify/etc", true).
Bind("/run", "/.fortify/run", false, true).
Bind("/sys/devices", "/.fortify/sys/devices", true, true).
Bind("/dev/dri", "/.fortify/dev/dri", false, true, true).
Bind("/dev/dri", "/.fortify/dev/dri", true, true, true),
want: []string{
[]string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
// Bind("/etc", "/.fortify/etc")
@ -41,14 +46,13 @@ func TestConfig_Args(t *testing.T) {
},
},
{
name: "dir remount-ro proc dev mqueue",
conf: (new(bwrap.Config)).
"dir remount-ro proc dev mqueue", (new(bwrap.Config)).
Dir("/.fortify").
RemountRO("/home").
Procfs("/proc").
DevTmpfs("/dev").
Mqueue("/dev/mqueue"),
want: []string{
[]string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
// Dir("/.fortify")
@ -64,11 +68,10 @@ func TestConfig_Args(t *testing.T) {
},
},
{
name: "tmpfs",
conf: (new(bwrap.Config)).
"tmpfs", (new(bwrap.Config)).
Tmpfs("/run/user", 8192).
Tmpfs("/run/dbus", 8192, 0755),
want: []string{
[]string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
// Tmpfs("/run/user", 8192)
@ -78,11 +81,10 @@ func TestConfig_Args(t *testing.T) {
},
},
{
name: "symlink",
conf: (new(bwrap.Config)).
"symlink", (new(bwrap.Config)).
Symlink("/.fortify/sbin/init", "/sbin/init").
Symlink("/.fortify/sbin/init", "/sbin/init", 0755),
want: []string{
[]string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
// Symlink("/.fortify/sbin/init", "/sbin/init")
@ -92,12 +94,11 @@ func TestConfig_Args(t *testing.T) {
},
},
{
name: "overlayfs",
conf: (new(bwrap.Config)).
"overlayfs", (new(bwrap.Config)).
Overlay("/etc", "/etc").
Join("/.fortify/bin", "/bin", "/usr/bin", "/usr/local/bin").
Persist("/nix", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/data/app/org.chromium.Chromium/nix"),
want: []string{
[]string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
// Overlay("/etc", "/etc")
@ -111,8 +112,23 @@ func TestConfig_Args(t *testing.T) {
},
},
{
name: "unshare",
conf: &bwrap.Config{Unshare: &bwrap.UnshareConfig{
"copy", (new(bwrap.Config)).
Write("/.fortify/version", make([]byte, 8)).
CopyBind("/etc/group", make([]byte, 8)).
CopyBind("/etc/passwd", make([]byte, 8), true),
[]string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
// Write("/.fortify/version", make([]byte, 8))
"--file", "3", "/.fortify/version",
// CopyBind("/etc/group", make([]byte, 8))
"--ro-bind-data", "4", "/etc/group",
// CopyBind("/etc/passwd", make([]byte, 8), true)
"--bind-data", "5", "/etc/passwd",
},
},
{
"unshare", &bwrap.Config{Unshare: &bwrap.UnshareConfig{
User: false,
IPC: false,
PID: false,
@ -120,14 +136,13 @@ func TestConfig_Args(t *testing.T) {
UTS: false,
CGroup: false,
}},
want: []string{"--disable-userns", "--assert-userns-disabled"},
[]string{"--disable-userns", "--assert-userns-disabled"},
},
{
name: "uid gid sync",
conf: (new(bwrap.Config)).
"uid gid sync", (new(bwrap.Config)).
SetUID(1971).
SetGID(100),
want: []string{
[]string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
// SetUID(1971)
@ -137,16 +152,16 @@ func TestConfig_Args(t *testing.T) {
},
},
{
name: "hostname chdir setenv unsetenv lockfile chmod",
conf: &bwrap.Config{
"hostname chdir setenv unsetenv lockfile chmod syscall", &bwrap.Config{
Hostname: "fortify",
Chdir: "/.fortify",
SetEnv: map[string]string{"FORTIFY_INIT": "/.fortify/sbin/init"},
UnsetEnv: []string{"HOME", "HOST"},
LockFile: []string{"/.fortify/lock"},
Syscall: new(bwrap.SyscallPolicy),
Chmod: map[string]os.FileMode{"/.fortify/sbin/init": 0755},
},
want: []string{
[]string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
// Hostname: "fortify"
@ -160,19 +175,15 @@ func TestConfig_Args(t *testing.T) {
"--lock-file", "/.fortify/lock",
// SetEnv: map[string]string{"FORTIFY_INIT": "/.fortify/sbin/init"}
"--setenv", "FORTIFY_INIT", "/.fortify/sbin/init",
// Syscall: new(bwrap.SyscallPolicy),
"--seccomp", "3",
// Chmod: map[string]os.FileMode{"/.fortify/sbin/init": 0755}
"--chmod", "755", "/.fortify/sbin/init",
},
},
{
name: "xdg-dbus-proxy constraint sample",
conf: (&bwrap.Config{
Unshare: nil,
UserNS: false,
Clearenv: true,
DieWithParent: true,
}).
"xdg-dbus-proxy constraint sample", (&bwrap.Config{Clearenv: true, DieWithParent: true}).
Symlink("usr/bin", "/bin").
Symlink("var/home", "/home").
Symlink("usr/lib", "/lib").
@ -195,7 +206,7 @@ func TestConfig_Args(t *testing.T) {
Bind("/sysroot", "/sysroot").
Bind("/usr", "/usr").
Bind("/etc", "/etc"),
want: []string{
[]string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
"--clearenv", "--die-with-parent",
@ -227,7 +238,7 @@ func TestConfig_Args(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := tc.conf.Args(); !slices.Equal(got, tc.want) {
if got := tc.conf.Args(nil, new(proc.ExtraFilesPre), new([]proc.File)); !slices.Equal(got, tc.want) {
t.Errorf("Args() = %#v, want %#v", got, tc.want)
}
})

View File

@ -6,7 +6,6 @@ import (
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/helper/seccomp"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
type SyscallPolicy struct {
@ -56,7 +55,7 @@ func (c *Config) seccompArgs() FDBuilder {
for _, opt := range optCond {
if opt.v {
opts |= opt.o
if fmsg.Verbose() {
if seccomp.CPrintln != nil {
optd = append(optd, opt.d)
}
}
@ -82,5 +81,5 @@ func (s *seccompBuilder) Append(args *[]string) {
return
}
*args = append(*args, positionalArgs[Seccomp], strconv.Itoa(int(s.Fd())))
*args = append(*args, Seccomp.String(), strconv.Itoa(int(s.Fd())))
}

View File

@ -2,21 +2,24 @@ package bwrap
import (
"encoding/gob"
"fmt"
"io"
"os"
"strconv"
"git.gensokyo.uk/security/fortify/helper/proc"
)
func init() {
gob.Register(new(PermConfig[SymlinkConfig]))
gob.Register(new(PermConfig[*TmpfsConfig]))
gob.Register(new(OverlayConfig))
gob.Register(new(DataConfig))
}
type PositionalArg int
func (p PositionalArg) Unwrap() string {
return positionalArgs[p]
}
func (p PositionalArg) String() string { return positionalArgs[p] }
const (
Tmpfs PositionalArg = iota
@ -46,6 +49,10 @@ const (
SyncFd
Seccomp
File
BindData
ROBindData
)
var positionalArgs = [...]string{
@ -76,6 +83,10 @@ var positionalArgs = [...]string{
SyncFd: "--sync-fd",
Seccomp: "--seccomp",
File: "--file",
BindData: "--bind-data",
ROBindData: "--ro-bind-data",
}
type PermConfig[T FSBuilder] struct {
@ -87,9 +98,7 @@ type PermConfig[T FSBuilder] struct {
Inner T `json:"path"`
}
func (p *PermConfig[T]) Path() string {
return p.Inner.Path()
}
func (p *PermConfig[T]) Path() string { return p.Inner.Path() }
func (p *PermConfig[T]) Len() int {
if p.Mode != nil {
@ -101,7 +110,7 @@ func (p *PermConfig[T]) Len() int {
func (p *PermConfig[T]) Append(args *[]string) {
if p.Mode != nil {
*args = append(*args, Perms.Unwrap(), strconv.FormatInt(int64(*p.Mode), 8))
*args = append(*args, Perms.String(), strconv.FormatInt(int64(*p.Mode), 8))
}
p.Inner.Append(args)
}
@ -115,9 +124,7 @@ type TmpfsConfig struct {
Dir string `json:"dir"`
}
func (t *TmpfsConfig) Path() string {
return t.Dir
}
func (t *TmpfsConfig) Path() string { return t.Dir }
func (t *TmpfsConfig) Len() int {
if t.Size > 0 {
@ -129,9 +136,9 @@ func (t *TmpfsConfig) Len() int {
func (t *TmpfsConfig) Append(args *[]string) {
if t.Size > 0 {
*args = append(*args, Size.Unwrap(), strconv.Itoa(t.Size))
*args = append(*args, Size.String(), strconv.Itoa(t.Size))
}
*args = append(*args, Tmpfs.Unwrap(), t.Dir)
*args = append(*args, Tmpfs.String(), t.Dir)
}
type OverlayConfig struct {
@ -164,9 +171,7 @@ type OverlayConfig struct {
Dest string `json:"dest"`
}
func (o *OverlayConfig) Path() string {
return o.Dest
}
func (o *OverlayConfig) Path() string { return o.Dest }
func (o *OverlayConfig) Len() int {
// (--tmp-overlay DEST) or (--ro-overlay DEST)
@ -182,20 +187,20 @@ func (o *OverlayConfig) Len() int {
func (o *OverlayConfig) Append(args *[]string) {
// --overlay-src SRC
for _, src := range o.Src {
*args = append(*args, OverlaySrc.Unwrap(), src)
*args = append(*args, OverlaySrc.String(), src)
}
if o.Persist != nil {
if o.Persist[0] != "" && o.Persist[1] != "" {
// --overlay RWSRC WORKDIR
*args = append(*args, Overlay.Unwrap(), o.Persist[0], o.Persist[1])
*args = append(*args, Overlay.String(), o.Persist[0], o.Persist[1])
} else {
// --ro-overlay
*args = append(*args, ROOverlay.Unwrap())
*args = append(*args, ROOverlay.String())
}
} else {
// --tmp-overlay
*args = append(*args, TmpOverlay.Unwrap())
*args = append(*args, TmpOverlay.String())
}
// DEST
@ -204,26 +209,65 @@ func (o *OverlayConfig) Append(args *[]string) {
type SymlinkConfig [2]string
func (s SymlinkConfig) Path() string {
return s[1]
}
func (s SymlinkConfig) Len() int {
return 3
}
func (s SymlinkConfig) Append(args *[]string) {
*args = append(*args, Symlink.Unwrap(), s[0], s[1])
}
func (s SymlinkConfig) Path() string { return s[1] }
func (s SymlinkConfig) Len() int { return 3 }
func (s SymlinkConfig) Append(args *[]string) { *args = append(*args, Symlink.String(), s[0], s[1]) }
type ChmodConfig map[string]os.FileMode
func (c ChmodConfig) Len() int {
return len(c)
}
func (c ChmodConfig) Len() int { return len(c) }
func (c ChmodConfig) Append(args *[]string) {
for path, mode := range c {
*args = append(*args, Chmod.Unwrap(), strconv.FormatInt(int64(mode), 8), path)
*args = append(*args, Chmod.String(), strconv.FormatInt(int64(mode), 8), path)
}
}
const (
DataWrite = iota
DataBind
DataROBind
)
type DataConfig struct {
Dest string `json:"dest"`
Data []byte `json:"data,omitempty"`
Type int `json:"type"`
proc.File
}
func (d *DataConfig) Path() string { return d.Dest }
func (d *DataConfig) Len() int {
if d == nil || d.Data == nil {
return 0
}
return 3
}
func (d *DataConfig) Init(fd uintptr, v **os.File) uintptr {
if d.File != nil {
panic("file initialised twice")
}
d.File = proc.NewWriterTo(d)
return d.File.Init(fd, v)
}
func (d *DataConfig) WriteTo(w io.Writer) (int64, error) {
n, err := w.Write(d.Data)
return int64(n), err
}
func (d *DataConfig) Append(args *[]string) {
if d == nil || d.Data == nil {
return
}
var a PositionalArg
switch d.Type {
case DataWrite:
a = File
case DataBind:
a = BindData
case DataROBind:
a = ROBindData
default:
panic(fmt.Sprintf("invalid type %d", a))
}
*args = append(*args, a.String(), strconv.Itoa(int(d.Fd())), d.Dest)
}

View File

@ -155,9 +155,8 @@ func bwrapStub() {
DieWithParent: true,
AsInit: true,
}
args := sc.Args()
sc.FDArgs(nil, &args, new(proc.ExtraFilesPre), new([]proc.File))
if _, err := MustNewCheckedArgs(args).WriteTo(want); err != nil {
if _, err := MustNewCheckedArgs(sc.Args(nil, new(proc.ExtraFilesPre), new([]proc.File))).
WriteTo(want); err != nil {
panic("cannot read want: " + err.Error())
}

View File

@ -62,9 +62,6 @@ var testCasesNixos = []sealTestCase{
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute).
WriteType(system.Process, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/passwd", "u0_a1:x:1971:1971:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n").
WriteType(system.Process, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/group", "fortify:x:1971:\n").
Link("/run/user/1971/wayland-0", "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/wayland").
UpdatePermType(system.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute).
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse").
CopyFile("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie").
@ -105,7 +102,7 @@ var testCasesNixos = []sealTestCase{
"SHELL": "/run/current-system/sw/bin/zsh",
"TERM": "xterm-256color",
"USER": "u0_a1",
"WAYLAND_DISPLAY": "/run/user/1971/wayland-0",
"WAYLAND_DISPLAY": "wayland-0",
"XDG_RUNTIME_DIR": "/run/user/1971",
"XDG_SESSION_CLASS": "user",
"XDG_SESSION_TYPE": "tty",
@ -212,13 +209,15 @@ var testCasesNixos = []sealTestCase{
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/1971", 8388608).
Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", false, true).
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/passwd", "/etc/passwd").
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/group", "/etc/group").
Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/wayland", "/run/user/1971/wayland-0").
CopyBind("/etc/passwd", []byte("u0_a1:x:1971:1971:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).
CopyBind("/etc/group", []byte("fortify:x:1971:\n")).
Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0").
Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native").
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/pulse-cookie", fst.Tmp+"/pulse-cookie").
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus").
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket").
Tmpfs("/var/run/nscd", 8192),
Tmpfs("/var/run/nscd", 8192).
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
Symlink("fortify", "/.fortify/sbin/init"),
},
}

View File

@ -32,9 +32,7 @@ var testCasesPd = []sealTestCase{
Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute).
WriteType(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/passwd", "chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n").
WriteType(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/group", "fortify:x:65534:\n"),
Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute),
(&bwrap.Config{
Net: true,
UserNS: true,
@ -154,9 +152,11 @@ var testCasesPd = []sealTestCase{
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/65534", 8388608).
Bind("/home/chronos", "/home/chronos", false, true).
Bind("/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/passwd", "/etc/passwd").
Bind("/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/group", "/etc/group").
Tmpfs("/var/run/nscd", 8192),
CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
CopyBind("/etc/group", []byte("fortify:x:65534:\n")).
Tmpfs("/var/run/nscd", 8192).
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
Symlink("fortify", "/.fortify/sbin/init"),
},
{
"nixos permissive defaults chromium", new(stubNixOS),
@ -216,8 +216,6 @@ var testCasesPd = []sealTestCase{
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", acl.Execute).
WriteType(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/passwd", "chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n").
WriteType(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/group", "fortify:x:65534:\n").
Ensure("/tmp/fortify.1971/wayland", 0711).
Wayland("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse").
@ -269,7 +267,7 @@ var testCasesPd = []sealTestCase{
"SHELL": "/run/current-system/sw/bin/zsh",
"TERM": "xterm-256color",
"USER": "chronos",
"WAYLAND_DISPLAY": "/run/user/65534/wayland-0",
"WAYLAND_DISPLAY": "wayland-0",
"XDG_RUNTIME_DIR": "/run/user/65534",
"XDG_SESSION_CLASS": "user",
"XDG_SESSION_TYPE": "tty",
@ -380,13 +378,15 @@ var testCasesPd = []sealTestCase{
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/65534", 8388608).
Bind("/home/chronos", "/home/chronos", false, true).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/passwd", "/etc/passwd").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/group", "/etc/group").
CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
CopyBind("/etc/group", []byte("fortify:x:65534:\n")).
Bind("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/65534/wayland-0").
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie", fst.Tmp+"/pulse-cookie").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket").
Tmpfs("/var/run/nscd", 8192),
Tmpfs("/var/run/nscd", 8192).
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
Symlink("fortify", "/.fortify/sbin/init"),
},
}

View File

@ -16,9 +16,12 @@ type stubNixOS struct {
usernameErr map[string]error
}
func (s *stubNixOS) Geteuid() int {
return 1971
}
func (s *stubNixOS) Geteuid() int { return 1971 }
func (s *stubNixOS) TempDir() string { return "/tmp" }
func (s *stubNixOS) MustExecutable() string { return "/run/wrappers/bin/fortify" }
func (s *stubNixOS) Exit(code int) { panic("called exit on stub with code " + strconv.Itoa(code)) }
func (s *stubNixOS) EvalSymlinks(path string) (string, error) { return path, nil }
func (s *stubNixOS) Uid(aid int) (int, error) { return 1000000 + 0*10000 + aid, nil }
func (s *stubNixOS) LookupEnv(key string) (string, bool) {
switch key {
@ -39,10 +42,6 @@ func (s *stubNixOS) LookupEnv(key string) (string, bool) {
}
}
func (s *stubNixOS) TempDir() string {
return "/tmp"
}
func (s *stubNixOS) LookPath(file string) (string, error) {
if s.lookPathErr != nil {
if err, ok := s.lookPathErr[file]; ok {
@ -60,10 +59,6 @@ func (s *stubNixOS) LookPath(file string) (string, error) {
}
}
func (s *stubNixOS) Executable() (string, error) {
return "/home/ophestra/.nix-profile/bin/fortify", nil
}
func (s *stubNixOS) LookupGroup(name string) (*user.Group, error) {
switch name {
case "video":
@ -127,14 +122,6 @@ func (s *stubNixOS) Open(name string) (fs.File, error) {
}
}
func (s *stubNixOS) EvalSymlinks(path string) (string, error) {
return path, nil
}
func (s *stubNixOS) Exit(code int) {
panic("called exit on stub with code " + strconv.Itoa(code))
}
func (s *stubNixOS) Paths() linux.Paths {
return linux.Paths{
SharePath: "/tmp/fortify.1971",
@ -142,11 +129,3 @@ func (s *stubNixOS) Paths() linux.Paths {
RunDirPath: "/run/user/1971/fortify",
}
}
func (s *stubNixOS) Uid(aid int) (int, error) {
return 1000000 + 0*10000 + aid, nil
}
func (s *stubNixOS) SdBooted() bool {
return true
}

View File

@ -15,6 +15,7 @@ import (
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/state"
@ -133,7 +134,8 @@ func (a *app) Seal(config *fst.Config) error {
}
if seal.sys.user.username == "" {
seal.sys.user.username = "chronos"
} else if !posixUsername.MatchString(seal.sys.user.username) {
} else if !posixUsername.MatchString(seal.sys.user.username) ||
len(seal.sys.user.username) >= internal.Sysconf_SC_LOGIN_NAME_MAX() {
return fmsg.WrapError(ErrName,
fmt.Sprintf("invalid user name %q", seal.sys.user.username))
}

View File

@ -13,6 +13,7 @@ import (
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/system"
"git.gensokyo.uk/security/fortify/wl"
)
const (
@ -27,9 +28,6 @@ const (
term = "TERM"
display = "DISPLAY"
// https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html
waylandDisplay = "WAYLAND_DISPLAY"
pulseServer = "PULSE_SERVER"
pulseCookie = "PULSE_COOKIE"
@ -38,7 +36,6 @@ const (
)
var (
ErrWayland = errors.New(waylandDisplay + " unset")
ErrXDisplay = errors.New(display + " unset")
ErrPulseCookie = errors.New("pulse cookie not present")
@ -113,34 +110,25 @@ func (seal *appSeal) setupShares(bus [2]*dbus.Config, os linux.System) error {
sh = s
}
// generate /etc/passwd
passwdPath := path.Join(seal.share, "passwd")
username := "chronos"
if seal.sys.user.username != "" {
username = seal.sys.user.username
}
// bind home directory
homeDir := "/var/empty"
if seal.sys.user.home != "" {
homeDir = seal.sys.user.home
}
// bind home directory
username := "chronos"
if seal.sys.user.username != "" {
username = seal.sys.user.username
}
seal.sys.bwrap.Bind(seal.sys.user.data, homeDir, false, true)
seal.sys.bwrap.Chdir = homeDir
seal.sys.bwrap.SetEnv["USER"] = username
seal.sys.bwrap.SetEnv["HOME"] = homeDir
seal.sys.bwrap.SetEnv["USER"] = username
passwd := username + ":x:" + seal.sys.mappedIDString + ":" + seal.sys.mappedIDString + ":Fortify:" + homeDir + ":" + sh + "\n"
seal.sys.Write(passwdPath, passwd)
// write /etc/group
groupPath := path.Join(seal.share, "group")
seal.sys.Write(groupPath, "fortify:x:"+seal.sys.mappedIDString+":\n")
// bind /etc/passwd and /etc/group
seal.sys.bwrap.Bind(passwdPath, "/etc/passwd")
seal.sys.bwrap.Bind(groupPath, "/etc/group")
// generate /etc/passwd and /etc/group
seal.sys.bwrap.CopyBind("/etc/passwd",
[]byte(username+":x:"+seal.sys.mappedIDString+":"+seal.sys.mappedIDString+":Fortify:"+homeDir+":"+sh+"\n"))
seal.sys.bwrap.CopyBind("/etc/group",
[]byte("fortify:x:"+seal.sys.mappedIDString+":\n"))
/*
Display servers
@ -153,36 +141,36 @@ func (seal *appSeal) setupShares(bus [2]*dbus.Config, os linux.System) error {
// set up wayland
if seal.et.Has(system.EWayland) {
var wp string
if wd, ok := os.LookupEnv(waylandDisplay); !ok {
return fmsg.WrapError(ErrWayland,
"WAYLAND_DISPLAY is not set")
var socketPath string
if name, ok := os.LookupEnv(wl.WaylandDisplay); !ok {
fmsg.VPrintln(wl.WaylandDisplay + " is not set, assuming " + wl.FallbackName)
socketPath = path.Join(seal.RuntimePath, wl.FallbackName)
} else if !path.IsAbs(name) {
socketPath = path.Join(seal.RuntimePath, name)
} else {
wp = path.Join(seal.RuntimePath, wd)
socketPath = name
}
w := path.Join(seal.sys.runtime, "wayland-0")
seal.sys.bwrap.SetEnv[waylandDisplay] = w
innerPath := path.Join(seal.sys.runtime, wl.FallbackName)
seal.sys.bwrap.SetEnv[wl.WaylandDisplay] = wl.FallbackName
if !seal.directWayland { // set up security-context-v1
wc := path.Join(seal.SharePath, "wayland")
wt := path.Join(wc, seal.id)
seal.sys.Ensure(wc, 0711)
socketDir := path.Join(seal.SharePath, "wayland")
outerPath := path.Join(socketDir, seal.id)
seal.sys.Ensure(socketDir, 0711)
appID := seal.fid
if appID == "" {
// use instance ID in case app id is not set
appID = "uk.gensokyo.fortify." + seal.id
}
seal.sys.Wayland(wt, wp, appID, seal.id)
seal.sys.bwrap.Bind(wt, w)
seal.sys.Wayland(outerPath, socketPath, appID, seal.id)
seal.sys.bwrap.Bind(outerPath, innerPath)
} else { // bind mount wayland socket (insecure)
// hardlink wayland socket
wpi := path.Join(seal.shareLocal, "wayland")
seal.sys.Link(wp, wpi)
seal.sys.bwrap.Bind(wpi, w)
fmsg.VPrintln("direct wayland access, PROCEED WITH CAUTION")
seal.sys.bwrap.Bind(socketPath, innerPath)
// ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
seal.sys.UpdatePermType(system.EWayland, wp, acl.Read, acl.Write, acl.Execute)
seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
}
}
@ -293,6 +281,10 @@ func (seal *appSeal) setupShares(bus [2]*dbus.Config, os linux.System) error {
seal.sys.bwrap.Tmpfs(dest, 8*1024)
}
// mount fortify in sandbox for init
seal.sys.bwrap.Bind(os.MustExecutable(), path.Join(fst.Tmp, "sbin/fortify"))
seal.sys.bwrap.Symlink("fortify", path.Join(fst.Tmp, "sbin/init"))
// append extra perms
for _, p := range seal.extraPerms {
if p == nil {

View File

@ -53,6 +53,7 @@ func queue(op dOp) {
type dOp interface{ Do() }
func Exit(code int) {
Resume() // resume here to avoid deadlock
queueSync.Wait()
os.Exit(code)
}

View File

@ -19,8 +19,8 @@ type System interface {
TempDir() string
// LookPath provides [exec.LookPath].
LookPath(file string) (string, error)
// Executable provides [os.Executable].
Executable() (string, error)
// MustExecutable provides [proc.MustExecutable].
MustExecutable() string
// LookupGroup provides [user.LookupGroup].
LookupGroup(name string) (*user.Group, error)
// ReadDir provides [os.ReadDir].

View File

@ -11,6 +11,7 @@ import (
"sync"
"syscall"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
@ -32,7 +33,7 @@ func (s *Std) Geteuid() int { return os.Geteuid(
func (s *Std) LookupEnv(key string) (string, bool) { return os.LookupEnv(key) }
func (s *Std) TempDir() string { return os.TempDir() }
func (s *Std) LookPath(file string) (string, error) { return exec.LookPath(file) }
func (s *Std) Executable() (string, error) { return os.Executable() }
func (s *Std) MustExecutable() string { return proc.MustExecutable() }
func (s *Std) LookupGroup(name string) (*user.Group, error) { return user.LookupGroup(name) }
func (s *Std) ReadDir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) }
func (s *Std) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) }

View File

@ -121,21 +121,12 @@ func Main() {
}()
}
// bind fortify inside sandbox
var (
innerSbin = path.Join(fst.Tmp, "sbin")
innerFortify = path.Join(innerSbin, "fortify")
innerInit = path.Join(innerSbin, "init")
)
conf.Bind(proc.MustExecutable(), innerFortify)
conf.Symlink("fortify", innerInit)
helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
if fmsg.Verbose() {
seccomp.CPrintln = fmsg.Println
}
if b, err := helper.NewBwrap(
conf, innerInit,
conf, path.Join(fst.Tmp, "sbin/init"),
nil, func(int, int) []string { return make([]string, 0) },
extraFiles,
syncFd,

View File

@ -9,8 +9,19 @@ var (
ErrDuplicate = errors.New("store contains duplicates")
)
/*
Joiner is the interface that wraps the Join method.
The Join function uses Joiner if available.
*/
type Joiner interface{ Join() (Entries, error) }
// Join returns joined state entries of all active aids.
func Join(s Store) (Entries, error) {
if j, ok := s.(Joiner); ok {
return j.Join()
}
var (
aids []int
entries = make(Entries)

6
internal/sysconf.go Normal file
View File

@ -0,0 +1,6 @@
package internal
//#include <unistd.h>
import "C"
func Sysconf_SC_LOGIN_NAME_MAX() int { return int(C.sysconf(C._SC_LOGIN_NAME_MAX)) }

View File

@ -42,26 +42,9 @@ func (sys *I) LinkFileType(et Enablement, oldname, newname string) *I {
return sys
}
// Write registers an Op that writes dst with the contents of src.
func (sys *I) Write(dst, src string) *I {
return sys.WriteType(Process, dst, src)
}
// WriteType registers a file writing Op labelled with type et.
func (sys *I) WriteType(et Enablement, dst, src string) *I {
sys.lock.Lock()
sys.ops = append(sys.ops, &Tmpfile{et, tmpfileWrite, dst, src})
sys.lock.Unlock()
sys.UpdatePermType(et, dst, acl.Read)
return sys
}
const (
tmpfileCopy uint8 = iota
tmpfileLink
tmpfileWrite
)
type Tmpfile struct {
@ -84,10 +67,6 @@ func (t *Tmpfile) apply(_ *I) error {
fmsg.VPrintln("linking tmpfile", t)
return fmsg.WrapErrorSuffix(os.Link(t.src, t.dst),
fmt.Sprintf("cannot link tmpfile %q:", t.dst))
case tmpfileWrite:
fmsg.VPrintln("writing", t)
return fmsg.WrapErrorSuffix(os.WriteFile(t.dst, []byte(t.src), 0600),
fmt.Sprintf("cannot write tmpfile %q:", t.dst))
default:
panic("invalid tmpfile method " + strconv.Itoa(int(t.method)))
}
@ -109,12 +88,7 @@ func (t *Tmpfile) Is(o Op) bool {
return ok && t0 != nil && *t == *t0
}
func (t *Tmpfile) Path() string {
if t.method == tmpfileWrite {
return fmt.Sprintf("(%d bytes of data)", len(t.src))
}
return t.src
}
func (t *Tmpfile) Path() string { return t.src }
func (t *Tmpfile) String() string {
switch t.method {
@ -122,8 +96,6 @@ func (t *Tmpfile) String() string {
return fmt.Sprintf("%q from %q", t.dst, t.src)
case tmpfileLink:
return fmt.Sprintf("%q from %q", t.dst, t.src)
case tmpfileWrite:
return fmt.Sprintf("%d bytes of data to %q", len(t.src), t.dst)
default:
panic("invalid tmpfile method " + strconv.Itoa(int(t.method)))
}

View File

@ -1,7 +1,6 @@
package system
import (
"strconv"
"testing"
"git.gensokyo.uk/security/fortify/acl"
@ -83,47 +82,6 @@ func TestLinkFileType(t *testing.T) {
}
}
func TestWrite(t *testing.T) {
testCases := []struct {
dst, src string
}{
{"/etc/passwd", "chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n"},
{"/etc/group", "fortify:x:65534:\n"},
}
for _, tc := range testCases {
t.Run("write "+strconv.Itoa(len(tc.src))+" bytes to "+tc.dst, func(t *testing.T) {
sys := New(150)
sys.Write(tc.dst, tc.src)
(&tcOp{Process, "(" + strconv.Itoa(len(tc.src)) + " bytes of data)"}).test(t, sys.ops, []Op{
&Tmpfile{Process, tmpfileWrite, tc.dst, tc.src},
&ACL{Process, tc.dst, []acl.Perm{acl.Read}},
}, "Write")
})
}
}
func TestWriteType(t *testing.T) {
testCases := []struct {
et Enablement
dst, src string
}{
{Process, "/etc/passwd", "chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n"},
{Process, "/etc/group", "fortify:x:65534:\n"},
{User, "/etc/passwd", "chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n"},
{User, "/etc/group", "fortify:x:65534:\n"},
}
for _, tc := range testCases {
t.Run("write "+strconv.Itoa(len(tc.src))+" bytes to "+tc.dst+" with type "+TypeString(tc.et), func(t *testing.T) {
sys := New(150)
sys.WriteType(tc.et, tc.dst, tc.src)
(&tcOp{tc.et, "(" + strconv.Itoa(len(tc.src)) + " bytes of data)"}).test(t, sys.ops, []Op{
&Tmpfile{tc.et, tmpfileWrite, tc.dst, tc.src},
&ACL{tc.et, tc.dst, []acl.Perm{acl.Read}},
}, "WriteType")
})
}
}
func TestTmpfile_String(t *testing.T) {
t.Run("invalid method panic", func(t *testing.T) {
defer func() {
@ -147,10 +105,6 @@ func TestTmpfile_String(t *testing.T) {
`"/run/user/1971/fortify/4b6bdc9182fb2f1d3a965c5fa8b9b66e/wayland" from "/run/user/1971/wayland-0"`},
{tmpfileLink, "/run/user/1971/fortify/4b6bdc9182fb2f1d3a965c5fa8b9b66e/pulse", "/run/user/1971/pulse/native",
`"/run/user/1971/fortify/4b6bdc9182fb2f1d3a965c5fa8b9b66e/pulse" from "/run/user/1971/pulse/native"`},
{tmpfileWrite, "/tmp/fortify.1971/4b6bdc9182fb2f1d3a965c5fa8b9b66e/passwd", "chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n",
`75 bytes of data to "/tmp/fortify.1971/4b6bdc9182fb2f1d3a965c5fa8b9b66e/passwd"`},
{tmpfileWrite, "/tmp/fortify.1971/4b6bdc9182fb2f1d3a965c5fa8b9b66e/group", "fortify:x:65534:\n",
`17 bytes of data to "/tmp/fortify.1971/4b6bdc9182fb2f1d3a965c5fa8b9b66e/group"`},
}
for _, tc := range testCases {

View File

@ -38,6 +38,10 @@ func (w Wayland) apply(sys *I) error {
}
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 {

18
main.go
View File

@ -13,6 +13,7 @@ import (
"sync"
"syscall"
"text/tabwriter"
"time"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
@ -23,6 +24,7 @@ import (
"git.gensokyo.uk/security/fortify/internal/linux"
init0 "git.gensokyo.uk/security/fortify/internal/priv/init"
"git.gensokyo.uk/security/fortify/internal/priv/shim"
"git.gensokyo.uk/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/internal/system"
)
@ -114,7 +116,7 @@ func main() {
fmt.Println(license)
fmsg.Exit(0)
case "template": // print full template configuration
printJSON(fst.Template())
printJSON(os.Stdout, false, fst.Template())
fmsg.Exit(0)
case "help": // print help message
flag.CommandLine.Usage()
@ -127,7 +129,7 @@ func main() {
// Ignore errors; set is set for ExitOnError.
_ = set.Parse(args[1:])
printPs(short)
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(sys.Paths().RunDirPath), short)
fmsg.Exit(0)
case "show": // pretty-print app info
set := flag.NewFlagSet("show", flag.ExitOnError)
@ -139,14 +141,14 @@ func main() {
switch len(set.Args()) {
case 0: // system
printShowSystem(short)
printShowSystem(os.Stdout, short)
case 1: // instance
name := set.Args()[0]
config, instance := tryShort(name)
if config == nil {
config = tryPath(name)
}
printShowInstance(instance, config, short)
printShowInstance(os.Stdout, time.Now().UTC(), instance, config, short)
default:
fmsg.Fatal("show requires 1 argument")
}
@ -326,14 +328,14 @@ func runApp(config *fst.Config) {
} else {
logWaitError(err)
}
if rs.ExitCode == 0 {
rs.ExitCode = 126
}
}
if rs.WaitErr != nil {
fmsg.Println("inner wait failed:", rs.WaitErr)
}
if rs.ExitCode < 0 {
fmsg.VPrintf("got negative exit %v", rs.ExitCode)
fmsg.Exit(1)
}
fmsg.Exit(rs.ExitCode)
panic("unreachable")
}

View File

@ -9,7 +9,6 @@ let
inherit (lib)
mkMerge
mkIf
mkDefault
mapAttrs
mergeAttrsList
imap1
@ -46,10 +45,6 @@ in
) "" cfg.users;
};
systemd.services.nix-daemon.unitConfig.RequiresMountsFor = [ "/etc/userdb" ];
services.userdbd.enable = mkDefault true;
home-manager =
let
privPackages = mapAttrs (username: fid: {
@ -123,6 +118,7 @@ in
};
map_real_uid = app.mapRealUid;
no_new_session = app.tty;
direct_wayland = app.insecureWayland;
filesystem =
let
bind = src: { inherit src; };

View File

@ -36,7 +36,7 @@ package
*Default:*
` <derivation fortify-0.2.13> `
` <derivation fortify-0.2.14> `
@ -425,6 +425,30 @@ null or string
## environment\.fortify\.apps\.\*\.insecureWayland
Whether to enable direct access to the Wayland socket\.
*Type:*
boolean
*Default:*
` false `
*Example:*
` true `
## environment\.fortify\.apps\.\*\.mapRealUid

View File

@ -146,6 +146,7 @@ in
mapRealUid = mkEnableOption "mapping to priv-user uid";
dev = mkEnableOption "access to all devices";
tty = mkEnableOption "access to the controlling terminal";
insecureWayland = mkEnableOption "direct access to the Wayland socket";
net = mkEnableOption "network access" // {
default = true;

View File

@ -16,7 +16,7 @@
buildGoModule rec {
pname = "fortify";
version = "0.2.13";
version = "0.2.14";
src = builtins.path {
name = "fortify-src";

195
print.go
View File

@ -3,7 +3,7 @@ package main
import (
"encoding/json"
"fmt"
direct "os"
"io"
"slices"
"strconv"
"strings"
@ -16,7 +16,10 @@ import (
"git.gensokyo.uk/security/fortify/internal/state"
)
func printShowSystem(short bool) {
func printShowSystem(output io.Writer, short bool) {
t := newPrinter(output)
defer t.MustFlush()
info := new(fst.Info)
// get fid by querying uid of aid 0
@ -27,58 +30,55 @@ func printShowSystem(short bool) {
}
if flagJSON {
printJSON(info)
printJSON(output, short, info)
return
}
w := tabwriter.NewWriter(direct.Stdout, 0, 1, 4, ' ', 0)
fmt.Fprintf(w, "User:\t%d\n", info.User)
if err := w.Flush(); err != nil {
fmsg.Fatalf("cannot flush tabwriter: %v", err)
}
t.Printf("User:\t%d\n", info.User)
}
func printShowInstance(instance *state.State, config *fst.Config, short bool) {
func printShowInstance(
output io.Writer, now time.Time,
instance *state.State, config *fst.Config,
short bool) {
if flagJSON {
if instance != nil {
printJSON(instance)
printJSON(output, short, instance)
} else {
printJSON(config)
printJSON(output, short, config)
}
return
}
now := time.Now().UTC()
w := tabwriter.NewWriter(direct.Stdout, 0, 1, 4, ' ', 0)
t := newPrinter(output)
defer t.MustFlush()
if config.Confinement.Sandbox == nil {
fmt.Print("Warning: this configuration uses permissive defaults!\n\n")
mustPrint(output, "Warning: this configuration uses permissive defaults!\n\n")
}
if instance != nil {
fmt.Fprintf(w, "State\n")
fmt.Fprintf(w, " Instance:\t%s (%d)\n", instance.ID.String(), instance.PID)
fmt.Fprintf(w, " Uptime:\t%s\n", now.Sub(instance.Time).Round(time.Second).String())
fmt.Fprintf(w, "\n")
t.Printf("State\n")
t.Printf(" Instance:\t%s (%d)\n", instance.ID.String(), instance.PID)
t.Printf(" Uptime:\t%s\n", now.Sub(instance.Time).Round(time.Second).String())
t.Printf("\n")
}
fmt.Fprintf(w, "App\n")
t.Printf("App\n")
if config.ID != "" {
fmt.Fprintf(w, " ID:\t%d (%s)\n", config.Confinement.AppID, config.ID)
t.Printf(" ID:\t%d (%s)\n", config.Confinement.AppID, config.ID)
} else {
fmt.Fprintf(w, " ID:\t%d\n", config.Confinement.AppID)
t.Printf(" ID:\t%d\n", config.Confinement.AppID)
}
fmt.Fprintf(w, " Enablements:\t%s\n", config.Confinement.Enablements.String())
t.Printf(" Enablements:\t%s\n", config.Confinement.Enablements.String())
if len(config.Confinement.Groups) > 0 {
fmt.Fprintf(w, " Groups:\t%q\n", config.Confinement.Groups)
t.Printf(" Groups:\t%q\n", config.Confinement.Groups)
}
fmt.Fprintf(w, " Directory:\t%s\n", config.Confinement.Outer)
t.Printf(" Directory:\t%s\n", config.Confinement.Outer)
if config.Confinement.Sandbox != nil {
sandbox := config.Confinement.Sandbox
if sandbox.Hostname != "" {
fmt.Fprintf(w, " Hostname:\t%q\n", sandbox.Hostname)
t.Printf(" Hostname:\t%q\n", sandbox.Hostname)
}
flags := make([]string, 0, 7)
writeFlag := func(name string, value bool) {
@ -96,27 +96,27 @@ func printShowInstance(instance *state.State, config *fst.Config, short bool) {
if len(flags) == 0 {
flags = append(flags, "none")
}
fmt.Fprintf(w, " Flags:\t%s\n", strings.Join(flags, " "))
t.Printf(" Flags:\t%s\n", strings.Join(flags, " "))
etc := sandbox.Etc
if etc == "" {
etc = "/etc"
}
fmt.Fprintf(w, " Etc:\t%s\n", etc)
t.Printf(" Etc:\t%s\n", etc)
if len(sandbox.Override) > 0 {
fmt.Fprintf(w, " Overrides:\t%s\n", strings.Join(sandbox.Override, " "))
t.Printf(" Overrides:\t%s\n", strings.Join(sandbox.Override, " "))
}
// Env map[string]string `json:"env"`
// Link [][2]string `json:"symlink"`
}
fmt.Fprintf(w, " Command:\t%s\n", strings.Join(config.Command, " "))
fmt.Fprintf(w, "\n")
t.Printf(" Command:\t%s\n", strings.Join(config.Command, " "))
t.Printf("\n")
if !short {
if config.Confinement.Sandbox != nil && len(config.Confinement.Sandbox.Filesystem) > 0 {
fmt.Fprintf(w, "Filesystem\n")
t.Printf("Filesystem\n")
for _, f := range config.Confinement.Sandbox.Filesystem {
if f == nil {
continue
@ -141,61 +141,54 @@ func printShowInstance(instance *state.State, config *fst.Config, short bool) {
if f.Dst != "" {
expr.WriteString(":" + f.Dst)
}
fmt.Fprintf(w, "%s\n", expr.String())
t.Printf("%s\n", expr.String())
}
fmt.Fprintf(w, "\n")
t.Printf("\n")
}
if len(config.Confinement.ExtraPerms) > 0 {
fmt.Fprintf(w, "Extra ACL\n")
t.Printf("Extra ACL\n")
for _, p := range config.Confinement.ExtraPerms {
if p == nil {
continue
}
fmt.Fprintf(w, " %s\n", p.String())
t.Printf(" %s\n", p.String())
}
fmt.Fprintf(w, "\n")
t.Printf("\n")
}
}
printDBus := func(c *dbus.Config) {
fmt.Fprintf(w, " Filter:\t%v\n", c.Filter)
t.Printf(" Filter:\t%v\n", c.Filter)
if len(c.See) > 0 {
fmt.Fprintf(w, " See:\t%q\n", c.See)
t.Printf(" See:\t%q\n", c.See)
}
if len(c.Talk) > 0 {
fmt.Fprintf(w, " Talk:\t%q\n", c.Talk)
t.Printf(" Talk:\t%q\n", c.Talk)
}
if len(c.Own) > 0 {
fmt.Fprintf(w, " Own:\t%q\n", c.Own)
t.Printf(" Own:\t%q\n", c.Own)
}
if len(c.Call) > 0 {
fmt.Fprintf(w, " Call:\t%q\n", c.Call)
t.Printf(" Call:\t%q\n", c.Call)
}
if len(c.Broadcast) > 0 {
fmt.Fprintf(w, " Broadcast:\t%q\n", c.Broadcast)
t.Printf(" Broadcast:\t%q\n", c.Broadcast)
}
}
if config.Confinement.SessionBus != nil {
fmt.Fprintf(w, "Session bus\n")
t.Printf("Session bus\n")
printDBus(config.Confinement.SessionBus)
fmt.Fprintf(w, "\n")
t.Printf("\n")
}
if config.Confinement.SystemBus != nil {
fmt.Fprintf(w, "System bus\n")
t.Printf("System bus\n")
printDBus(config.Confinement.SystemBus)
fmt.Fprintf(w, "\n")
}
if err := w.Flush(); err != nil {
fmsg.Fatalf("cannot flush tabwriter: %v", err)
t.Printf("\n")
}
}
func printPs(short bool) {
now := time.Now().UTC()
func printPs(output io.Writer, now time.Time, s state.Store, short bool) {
var entries state.Entries
s := state.NewMulti(sys.Paths().RunDirPath)
if e, err := state.Join(s); err != nil {
fmsg.Fatalf("cannot join store: %v", err)
} else {
@ -205,12 +198,12 @@ func printPs(short bool) {
fmsg.Printf("cannot close store: %v", err)
}
if flagJSON {
if !short && flagJSON {
es := make(map[string]*state.State, len(entries))
for id, instance := range entries {
es[id.String()] = instance
}
printJSON(es)
printJSON(output, short, es)
return
}
@ -225,7 +218,7 @@ func printPs(short bool) {
// gracefully skip inconsistent states
if id != instance.ID {
fmt.Printf("possible store corruption: entry %s has id %s",
fmsg.Printf("possible store corruption: entry %s has id %s",
id.String(), instance.ID.String())
continue
}
@ -239,24 +232,34 @@ func printPs(short bool) {
for i, e := range exp {
v[i] = e.s
}
printJSON(v)
printJSON(output, short, v)
} else {
for _, e := range exp {
fmt.Println(e.s[:8])
mustPrintln(output, e.s[:8])
}
}
return
}
// buffer output to reduce terminal activity
w := tabwriter.NewWriter(direct.Stdout, 0, 1, 4, ' ', 0)
fmt.Fprintln(w, "\tInstance\tPID\tApp\tUptime\tEnablements\tCommand")
t := newPrinter(output)
defer t.MustFlush()
t.Println("\tInstance\tPID\tApp\tUptime\tEnablements\tCommand")
for _, e := range exp {
printInstance(w, e, now)
}
if err := w.Flush(); err != nil {
fmsg.Fatalf("cannot flush tabwriter: %v", err)
var (
es = "(No confinement information)"
cs = "(No command information)"
as = "(No configuration information)"
)
if e.Config != nil {
es = e.Config.Confinement.Enablements.String()
cs = fmt.Sprintf("%q", e.Config.Command)
as = strconv.Itoa(e.Config.Confinement.AppID)
}
t.Printf("\t%s\t%d\t%s\t%s\t%s\t%s\n",
e.s[:8], e.PID, as, now.Sub(e.Time).Round(time.Second).String(), strings.TrimPrefix(es, ", "), cs)
}
t.Println()
}
type expandedStateEntry struct {
@ -264,26 +267,48 @@ type expandedStateEntry struct {
*state.State
}
func printInstance(w *tabwriter.Writer, e *expandedStateEntry, now time.Time) {
var (
es = "(No confinement information)"
cs = "(No command information)"
as = "(No configuration information)"
)
if e.Config != nil {
es = e.Config.Confinement.Enablements.String()
cs = fmt.Sprintf("%q", e.Config.Command)
as = strconv.Itoa(e.Config.Confinement.AppID)
func printJSON(output io.Writer, short bool, v any) {
encoder := json.NewEncoder(output)
if !short {
encoder.SetIndent("", " ")
}
fmt.Fprintf(w, "\t%s\t%d\t%s\t%s\t%s\t%s\n",
e.s[:8], e.PID, as, now.Sub(e.Time).Round(time.Second).String(), strings.TrimPrefix(es, ", "), cs)
}
func printJSON(v any) {
encoder := json.NewEncoder(direct.Stdout)
encoder.SetIndent("", " ")
if err := encoder.Encode(v); err != nil {
fmsg.Fatalf("cannot serialise: %v", err)
panic("unreachable")
}
}
func newPrinter(output io.Writer) *tp { return &tp{tabwriter.NewWriter(output, 0, 1, 4, ' ', 0)} }
type tp struct{ *tabwriter.Writer }
func (p *tp) Printf(format string, a ...any) {
if _, err := fmt.Fprintf(p, format, a...); err != nil {
fmsg.Fatalf("cannot write to tabwriter: %v", err)
panic("unreachable")
}
}
func (p *tp) Println(a ...any) {
if _, err := fmt.Fprintln(p, a...); err != nil {
fmsg.Fatalf("cannot write to tabwriter: %v", err)
panic("unreachable")
}
}
func (p *tp) MustFlush() {
if err := p.Writer.Flush(); err != nil {
fmsg.Fatalf("cannot flush tabwriter: %v", err)
panic("unreachable")
}
}
func mustPrint(output io.Writer, a ...any) {
if _, err := fmt.Fprint(output, a...); err != nil {
fmsg.Fatalf("cannot print: %v", err)
panic("unreachable")
}
}
func mustPrintln(output io.Writer, a ...any) {
if _, err := fmt.Fprintln(output, a...); err != nil {
fmsg.Fatalf("cannot print: %v", err)
panic("unreachable")
}
}

671
print_test.go Normal file
View File

@ -0,0 +1,671 @@
package main
import (
"strings"
"testing"
"time"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/state"
)
var (
testID = fst.ID{
0x8e, 0x2c, 0x76, 0xb0,
0x66, 0xda, 0xbe, 0x57,
0x4c, 0xf0, 0x73, 0xbd,
0xb4, 0x6e, 0xb5, 0xc1,
}
testState = &state.State{
ID: testID,
PID: 0xDEADBEEF,
Config: fst.Template(),
Time: testAppTime,
}
testTime = time.Unix(3752, 1).UTC()
testAppTime = time.Unix(0, 9).UTC()
)
func Test_printShowInstance(t *testing.T) {
testCases := []struct {
name string
instance *state.State
config *fst.Config
short, json bool
want string
}{
{"config", nil, fst.Template(), false, false, `App
ID: 9 (org.chromium.Chromium)
Enablements: Wayland, D-Bus, PulseAudio
Groups: ["video"]
Directory: /var/lib/persist/home/org.chromium.Chromium
Hostname: "localhost"
Flags: userns net dev tty mapuid autoetc
Etc: /etc
Overrides: /var/run/nscd
Command: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Filesystem
+/nix/store
+/run/current-system
+/run/opengl-driver
+/var/db/nix-channels
w*/var/lib/fortify/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
d+/dev/dri
Extra ACL
--x+:/var/lib/fortify/u0
rwx:/var/lib/fortify/u0/org.chromium.Chromium
Session bus
Filter: true
Talk: ["org.freedesktop.Notifications" "org.freedesktop.FileManager1" "org.freedesktop.ScreenSaver" "org.freedesktop.secrets" "org.kde.kwalletd5" "org.kde.kwalletd6" "org.gnome.SessionManager"]
Own: ["org.chromium.Chromium.*" "org.mpris.MediaPlayer2.org.chromium.Chromium.*" "org.mpris.MediaPlayer2.chromium.*"]
Call: map["org.freedesktop.portal.*":"*"]
Broadcast: map["org.freedesktop.portal.*":"@/org/freedesktop/portal/*"]
System bus
Filter: true
Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"]
`},
{"config pd", nil, new(fst.Config), false, false, `Warning: this configuration uses permissive defaults!
App
ID: 0
Enablements: (No enablements)
Directory:
Command:
`},
{"config flag none", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: new(fst.SandboxConfig)}}, false, false, `App
ID: 0
Enablements: (No enablements)
Directory:
Flags: none
Etc: /etc
Command:
`},
{"config nil entries", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: &fst.SandboxConfig{Filesystem: make([]*fst.FilesystemConfig, 1)}, ExtraPerms: make([]*fst.ExtraPermConfig, 1)}}, false, false, `App
ID: 0
Enablements: (No enablements)
Directory:
Flags: none
Etc: /etc
Command:
Filesystem
Extra ACL
`},
{"config pd dbus see", nil, &fst.Config{Confinement: fst.ConfinementConfig{SessionBus: &dbus.Config{See: []string{"org.example.test"}}}}, false, false, `Warning: this configuration uses permissive defaults!
App
ID: 0
Enablements: (No enablements)
Directory:
Command:
Session bus
Filter: false
See: ["org.example.test"]
`},
{"instance", testState, fst.Template(), false, false, `State
Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559)
Uptime: 1h2m32s
App
ID: 9 (org.chromium.Chromium)
Enablements: Wayland, D-Bus, PulseAudio
Groups: ["video"]
Directory: /var/lib/persist/home/org.chromium.Chromium
Hostname: "localhost"
Flags: userns net dev tty mapuid autoetc
Etc: /etc
Overrides: /var/run/nscd
Command: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Filesystem
+/nix/store
+/run/current-system
+/run/opengl-driver
+/var/db/nix-channels
w*/var/lib/fortify/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
d+/dev/dri
Extra ACL
--x+:/var/lib/fortify/u0
rwx:/var/lib/fortify/u0/org.chromium.Chromium
Session bus
Filter: true
Talk: ["org.freedesktop.Notifications" "org.freedesktop.FileManager1" "org.freedesktop.ScreenSaver" "org.freedesktop.secrets" "org.kde.kwalletd5" "org.kde.kwalletd6" "org.gnome.SessionManager"]
Own: ["org.chromium.Chromium.*" "org.mpris.MediaPlayer2.org.chromium.Chromium.*" "org.mpris.MediaPlayer2.chromium.*"]
Call: map["org.freedesktop.portal.*":"*"]
Broadcast: map["org.freedesktop.portal.*":"@/org/freedesktop/portal/*"]
System bus
Filter: true
Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"]
`},
{"instance pd", testState, new(fst.Config), false, false, `Warning: this configuration uses permissive defaults!
State
Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559)
Uptime: 1h2m32s
App
ID: 0
Enablements: (No enablements)
Directory:
Command:
`},
{"json nil", nil, nil, false, true, `null
`},
{"json instance", testState, nil, false, true, `{
"instance": [
142,
44,
118,
176,
102,
218,
190,
87,
76,
240,
115,
189,
180,
110,
181,
193
],
"pid": 3735928559,
"config": {
"id": "org.chromium.Chromium",
"command": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"confinement": {
"app_id": 9,
"groups": [
"video"
],
"username": "chronos",
"home_inner": "/var/lib/fortify",
"home": "/var/lib/persist/home/org.chromium.Chromium",
"sandbox": {
"hostname": "localhost",
"userns": true,
"net": true,
"dev": true,
"syscall": {
"compat": false,
"deny_devel": true,
"multiarch": true,
"linux32": false,
"can": false,
"bluetooth": false
},
"no_new_session": true,
"map_real_uid": true,
"env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"filesystem": [
{
"src": "/nix/store"
},
{
"src": "/run/current-system"
},
{
"src": "/run/opengl-driver"
},
{
"src": "/var/db/nix-channels"
},
{
"dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/fortify/u0/org.chromium.Chromium",
"write": true,
"require": true
},
{
"src": "/dev/dri",
"dev": true
}
],
"symlink": [
[
"/run/user/65534",
"/run/user/150"
]
],
"etc": "/etc",
"auto_etc": true,
"override": [
"/var/run/nscd"
]
},
"extra_perms": [
{
"ensure": true,
"path": "/var/lib/fortify/u0",
"x": true
},
{
"path": "/var/lib/fortify/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
}
],
"system_bus": {
"see": null,
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"own": null,
"call": null,
"broadcast": null,
"filter": true
},
"session_bus": {
"see": null,
"talk": [
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
],
"own": [
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"
],
"call": {
"org.freedesktop.portal.*": "*"
},
"broadcast": {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
},
"filter": true
},
"enablements": 13
}
},
"time": "1970-01-01T00:00:00.000000009Z"
}
`},
{"json config", nil, fst.Template(), false, true, `{
"id": "org.chromium.Chromium",
"command": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"confinement": {
"app_id": 9,
"groups": [
"video"
],
"username": "chronos",
"home_inner": "/var/lib/fortify",
"home": "/var/lib/persist/home/org.chromium.Chromium",
"sandbox": {
"hostname": "localhost",
"userns": true,
"net": true,
"dev": true,
"syscall": {
"compat": false,
"deny_devel": true,
"multiarch": true,
"linux32": false,
"can": false,
"bluetooth": false
},
"no_new_session": true,
"map_real_uid": true,
"env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"filesystem": [
{
"src": "/nix/store"
},
{
"src": "/run/current-system"
},
{
"src": "/run/opengl-driver"
},
{
"src": "/var/db/nix-channels"
},
{
"dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/fortify/u0/org.chromium.Chromium",
"write": true,
"require": true
},
{
"src": "/dev/dri",
"dev": true
}
],
"symlink": [
[
"/run/user/65534",
"/run/user/150"
]
],
"etc": "/etc",
"auto_etc": true,
"override": [
"/var/run/nscd"
]
},
"extra_perms": [
{
"ensure": true,
"path": "/var/lib/fortify/u0",
"x": true
},
{
"path": "/var/lib/fortify/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
}
],
"system_bus": {
"see": null,
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"own": null,
"call": null,
"broadcast": null,
"filter": true
},
"session_bus": {
"see": null,
"talk": [
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
],
"own": [
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"
],
"call": {
"org.freedesktop.portal.*": "*"
},
"broadcast": {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
},
"filter": true
},
"enablements": 13
}
}
`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
{
v := flagJSON
t.Cleanup(func() { flagJSON = v })
flagJSON = tc.json
}
output := new(strings.Builder)
printShowInstance(output, testTime, tc.instance, tc.config, tc.short)
if got := output.String(); got != tc.want {
t.Errorf("printShowInstance: got\n%s\nwant\n%s",
got, tc.want)
return
}
})
}
}
func Test_printPs(t *testing.T) {
testCases := []struct {
name string
entries state.Entries
short, json bool
want string
}{
{"no entries", make(state.Entries), false, false, ` Instance PID App Uptime Enablements Command
`},
{"no entries short", make(state.Entries), true, false, ``},
{"nil instance", state.Entries{testID: nil}, false, false, ` Instance PID App Uptime Enablements Command
`},
{"state corruption", state.Entries{fst.ID{}: testState}, false, false, ` Instance PID App Uptime Enablements Command
`},
{"valid", state.Entries{testID: testState}, false, false, ` Instance PID App Uptime Enablements Command
8e2c76b0 3735928559 9 1h2m32s Wayland, D-Bus, PulseAudio ["chromium" "--ignore-gpu-blocklist" "--disable-smooth-scrolling" "--enable-features=UseOzonePlatform" "--ozone-platform=wayland"]
`},
{"valid short", state.Entries{testID: testState}, true, false, `8e2c76b0
`},
{"valid json", state.Entries{testID: testState}, false, true, `{
"8e2c76b066dabe574cf073bdb46eb5c1": {
"instance": [
142,
44,
118,
176,
102,
218,
190,
87,
76,
240,
115,
189,
180,
110,
181,
193
],
"pid": 3735928559,
"config": {
"id": "org.chromium.Chromium",
"command": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"confinement": {
"app_id": 9,
"groups": [
"video"
],
"username": "chronos",
"home_inner": "/var/lib/fortify",
"home": "/var/lib/persist/home/org.chromium.Chromium",
"sandbox": {
"hostname": "localhost",
"userns": true,
"net": true,
"dev": true,
"syscall": {
"compat": false,
"deny_devel": true,
"multiarch": true,
"linux32": false,
"can": false,
"bluetooth": false
},
"no_new_session": true,
"map_real_uid": true,
"env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"filesystem": [
{
"src": "/nix/store"
},
{
"src": "/run/current-system"
},
{
"src": "/run/opengl-driver"
},
{
"src": "/var/db/nix-channels"
},
{
"dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/fortify/u0/org.chromium.Chromium",
"write": true,
"require": true
},
{
"src": "/dev/dri",
"dev": true
}
],
"symlink": [
[
"/run/user/65534",
"/run/user/150"
]
],
"etc": "/etc",
"auto_etc": true,
"override": [
"/var/run/nscd"
]
},
"extra_perms": [
{
"ensure": true,
"path": "/var/lib/fortify/u0",
"x": true
},
{
"path": "/var/lib/fortify/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
}
],
"system_bus": {
"see": null,
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"own": null,
"call": null,
"broadcast": null,
"filter": true
},
"session_bus": {
"see": null,
"talk": [
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
],
"own": [
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"
],
"call": {
"org.freedesktop.portal.*": "*"
},
"broadcast": {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
},
"filter": true
},
"enablements": 13
}
},
"time": "1970-01-01T00:00:00.000000009Z"
}
}
`},
{"valid short json", state.Entries{testID: testState}, true, true, `["8e2c76b066dabe574cf073bdb46eb5c1"]
`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
{
v := flagJSON
t.Cleanup(func() { flagJSON = v })
flagJSON = tc.json
}
output := new(strings.Builder)
printPs(output, testTime, stubStore(tc.entries), tc.short)
if got := output.String(); got != tc.want {
t.Errorf("printPs: got\n%s\nwant\n%s",
got, tc.want)
return
}
})
}
}
// stubStore implements [state.Store] and returns test samples via [state.Joiner].
type stubStore state.Entries
func (s stubStore) Join() (state.Entries, error) { return state.Entries(s), nil }
func (s stubStore) Do(int, func(c state.Cursor)) (bool, error) { panic("unreachable") }
func (s stubStore) List() ([]int, error) { panic("unreachable") }
func (s stubStore) Close() error { return nil }

View File

@ -79,8 +79,9 @@ nixosTest {
set -e
mkdir -p ~/.config/sway
sed s/Mod4/Mod1/ /etc/sway/config > ~/.config/sway/config
echo 'output * bg ${pkgs.nixos-artwork.wallpapers.simple-light-gray.gnomeFilePath} fill' >> ~/.config/sway/config
(sed s/Mod4/Mod1/ /etc/sway/config &&
echo 'output * bg ${pkgs.nixos-artwork.wallpapers.simple-light-gray.gnomeFilePath} fill' &&
echo 'output Virtual-1 res 1680x1050') > ~/.config/sway/config
sway --validate
systemd-cat --identifier=sway sway && touch /tmp/sway-exit-ok
@ -147,6 +148,18 @@ nixosTest {
pulse = false;
};
}
{
name = "da-foot";
verbose = true;
insecureWayland = true;
share = pkgs.foot;
packages = [ pkgs.foot ];
command = "foot";
capability = {
dbus = false;
pulse = false;
};
}
{
name = "strace-failure";
verbose = true;
@ -269,6 +282,9 @@ nixosTest {
if output != "":
raise Exception(f"unexpected output\n{output}")
# Verify graceful failure on bad Wayland display name:
print(machine.fail("sudo -u alice -i fortify -v run --wayland true"))
# Start fortify permissive defaults within Wayland session:
fortify('-v run --wayland --dbus notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-done')
machine.wait_for_file("/tmp/dbus-done")
@ -322,6 +338,20 @@ nixosTest {
machine.send_chars("exit\n")
machine.wait_until_fails("pgrep alacritty")
# Start app (foot) with direct Wayland access:
swaymsg("exec da-foot")
wait_for_window("u0_a4@machine")
machine.send_chars("clear; wayland-info && touch /tmp/success-direct\n")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/4/success-direct")
collect_state_ui("foot_direct")
check_state("da-foot", 1)
# Verify acl on XDG_RUNTIME_DIR:
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000004"))
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 1000004")
# Test syscall filter:
print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure"))

15
wl/consts.go Normal file
View File

@ -0,0 +1,15 @@
package wl
const (
// WaylandDisplay contains the name of the server socket
// (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1147)
// which is concatenated with XDG_RUNTIME_DIR
// (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1171)
// or used as-is if absolute
// (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1176).
WaylandDisplay = "WAYLAND_DISPLAY"
// FallbackName is used as the wayland socket name if WAYLAND_DISPLAY is unset
// (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1149).
FallbackName = "wayland-0"
)