Compare commits
No commits in common. "48f634d04634f9b4051fec416ca9e08cf36308f6" and "2a4e2724a35f322af3ae26c637c74161c4884542" have entirely different histories.
48f634d046
...
2a4e2724a3
@ -22,57 +22,6 @@ jobs:
|
|||||||
path: result/*
|
path: result/*
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
race:
|
|
||||||
name: Fortify (race detector)
|
|
||||||
runs-on: nix
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Run NixOS test
|
|
||||||
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.race
|
|
||||||
|
|
||||||
- name: Upload test output
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: "fortify-race-vm-output"
|
|
||||||
path: result/*
|
|
||||||
retention-days: 1
|
|
||||||
|
|
||||||
sandbox:
|
|
||||||
name: Sandbox
|
|
||||||
runs-on: nix
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Run NixOS test
|
|
||||||
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sandbox
|
|
||||||
|
|
||||||
- name: Upload test output
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: "sandbox-vm-output"
|
|
||||||
path: result/*
|
|
||||||
retention-days: 1
|
|
||||||
|
|
||||||
sandbox-race:
|
|
||||||
name: Sandbox (race detector)
|
|
||||||
runs-on: nix
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Run NixOS test
|
|
||||||
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sandbox-race
|
|
||||||
|
|
||||||
- name: Upload test output
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: "sandbox-race-vm-output"
|
|
||||||
path: result/*
|
|
||||||
retention-days: 1
|
|
||||||
|
|
||||||
fpkg:
|
fpkg:
|
||||||
name: Fpkg
|
name: Fpkg
|
||||||
runs-on: nix
|
runs-on: nix
|
||||||
@ -90,14 +39,29 @@ jobs:
|
|||||||
path: result/*
|
path: result/*
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
|
race:
|
||||||
|
name: Data race detector
|
||||||
|
runs-on: nix
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run NixOS test
|
||||||
|
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.race
|
||||||
|
|
||||||
|
- name: Upload test output
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: "fortify-race-vm-output"
|
||||||
|
path: result/*
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
check:
|
check:
|
||||||
name: Flake checks
|
name: Flake checks
|
||||||
needs:
|
needs:
|
||||||
- fortify
|
- fortify
|
||||||
- race
|
|
||||||
- sandbox
|
|
||||||
- sandbox-race
|
|
||||||
- fpkg
|
- fpkg
|
||||||
|
- race
|
||||||
runs-on: nix
|
runs-on: nix
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -72,7 +71,7 @@ func TestProxy_Seal(t *testing.T) {
|
|||||||
for id, tc := range testCasePairs() {
|
for id, tc := range testCasePairs() {
|
||||||
t.Run("create seal for "+id, func(t *testing.T) {
|
t.Run("create seal for "+id, func(t *testing.T) {
|
||||||
p := dbus.New(tc[0].bus, tc[1].bus)
|
p := dbus.New(tc[0].bus, tc[1].bus)
|
||||||
if err := p.Seal(tc[0].c, tc[1].c); (errors.Is(err, syscall.EINVAL)) != tc[0].wantErr {
|
if err := p.Seal(tc[0].c, tc[1].c); (errors.Is(err, helper.ErrContainsNull)) != tc[0].wantErr {
|
||||||
t.Errorf("Seal(%p, %p) error = %v, wantErr %v",
|
t.Errorf("Seal(%p, %p) error = %v, wantErr %v",
|
||||||
tc[0].c, tc[1].c,
|
tc[0].c, tc[1].c,
|
||||||
err, tc[0].wantErr)
|
err, tc[0].wantErr)
|
||||||
|
@ -58,19 +58,12 @@
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
fortify = callPackage ./test { inherit system self; };
|
fortify = callPackage ./test { inherit system self; };
|
||||||
|
fpkg = callPackage ./cmd/fpkg/test { inherit system self; };
|
||||||
race = callPackage ./test {
|
race = callPackage ./test {
|
||||||
inherit system self;
|
inherit system self;
|
||||||
withRace = true;
|
withRace = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
sandbox = callPackage ./test/sandbox { inherit self; };
|
|
||||||
sandbox-race = callPackage ./test/sandbox {
|
|
||||||
inherit self;
|
|
||||||
withRace = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
fpkg = callPackage ./cmd/fpkg/test { inherit system self; };
|
|
||||||
|
|
||||||
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
|
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
|
||||||
cd ${./.}
|
cd ${./.}
|
||||||
|
|
||||||
|
@ -97,10 +97,6 @@ func (s *SandboxConfig) ToContainer(sys SandboxSys, uid, gid *int) (*sandbox.Par
|
|||||||
Seccomp: s.Seccomp,
|
Seccomp: s.Seccomp,
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.Multiarch {
|
|
||||||
container.Seccomp |= seccomp.FlagMultiarch
|
|
||||||
}
|
|
||||||
|
|
||||||
/* this is only 4 KiB of memory on a 64-bit system,
|
/* this is only 4 KiB of memory on a 64-bit system,
|
||||||
permissive defaults on NixOS results in around 100 entries
|
permissive defaults on NixOS results in around 100 entries
|
||||||
so this capacity should eliminate copies for most setups */
|
so this capacity should eliminate copies for most setups */
|
||||||
|
@ -1,17 +1,38 @@
|
|||||||
package helper
|
package helper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"syscall"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type argsWt [][]byte
|
var (
|
||||||
|
ErrContainsNull = errors.New("argument contains null character")
|
||||||
|
)
|
||||||
|
|
||||||
|
type argsWt []string
|
||||||
|
|
||||||
|
// checks whether any element contains the null character
|
||||||
|
// must be called before args use and args must not be modified after call
|
||||||
|
func (a argsWt) check() error {
|
||||||
|
for _, arg := range a {
|
||||||
|
for _, b := range arg {
|
||||||
|
if b == '\x00' {
|
||||||
|
return ErrContainsNull
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a argsWt) WriteTo(w io.Writer) (int64, error) {
|
func (a argsWt) WriteTo(w io.Writer) (int64, error) {
|
||||||
|
// assuming already checked
|
||||||
|
|
||||||
nt := 0
|
nt := 0
|
||||||
|
// write null terminated arguments
|
||||||
for _, arg := range a {
|
for _, arg := range a {
|
||||||
n, err := w.Write(arg)
|
n, err := w.Write([]byte(arg + "\x00"))
|
||||||
nt += n
|
nt += n
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -23,32 +44,18 @@ func (a argsWt) WriteTo(w io.Writer) (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a argsWt) String() string {
|
func (a argsWt) String() string {
|
||||||
return string(
|
return strings.Join(a, " ")
|
||||||
bytes.TrimSuffix(
|
|
||||||
bytes.ReplaceAll(
|
|
||||||
bytes.Join(a, nil),
|
|
||||||
[]byte{0}, []byte{' '},
|
|
||||||
),
|
|
||||||
[]byte{' '},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
|
// NewCheckedArgs returns a checked argument writer for args.
|
||||||
func NewCheckedArgs(args []string) (wt io.WriterTo, err error) {
|
// Callers must not retain any references to args.
|
||||||
a := make(argsWt, len(args))
|
func NewCheckedArgs(args []string) (io.WriterTo, error) {
|
||||||
for i, arg := range args {
|
a := argsWt(args)
|
||||||
a[i], err = syscall.ByteSliceFromString(arg)
|
return a, a.check()
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wt = a
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustNewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
|
// MustNewCheckedArgs returns a checked argument writer for args and panics if check fails.
|
||||||
// If s contains a NUL byte this function panics instead of returning an error.
|
// Callers must not retain any references to args.
|
||||||
func MustNewCheckedArgs(args []string) io.WriterTo {
|
func MustNewCheckedArgs(args []string) io.WriterTo {
|
||||||
a, err := NewCheckedArgs(args)
|
a, err := NewCheckedArgs(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -4,33 +4,34 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper"
|
"git.gensokyo.uk/security/fortify/helper"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestArgsString(t *testing.T) {
|
func Test_argsFd_String(t *testing.T) {
|
||||||
wantString := strings.Join(wantArgs, " ")
|
wantString := strings.Join(wantArgs, " ")
|
||||||
if got := argsWt.(fmt.Stringer).String(); got != wantString {
|
if got := argsWt.(fmt.Stringer).String(); got != wantString {
|
||||||
t.Errorf("String: %q, want %q",
|
t.Errorf("String(): got %v; want %v",
|
||||||
got, wantString)
|
got, wantString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewCheckedArgs(t *testing.T) {
|
func TestNewCheckedArgs(t *testing.T) {
|
||||||
args := []string{"\x00"}
|
args := []string{"\x00"}
|
||||||
if _, err := helper.NewCheckedArgs(args); !errors.Is(err, syscall.EINVAL) {
|
if _, err := helper.NewCheckedArgs(args); !errors.Is(err, helper.ErrContainsNull) {
|
||||||
t.Errorf("NewCheckedArgs: error = %v, wantErr %v",
|
t.Errorf("NewCheckedArgs(%q) error = %v, wantErr %v",
|
||||||
err, syscall.EINVAL)
|
args,
|
||||||
|
err, helper.ErrContainsNull)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("must panic", func(t *testing.T) {
|
t.Run("must panic", func(t *testing.T) {
|
||||||
badPayload := []string{"\x00"}
|
badPayload := []string{"\x00"}
|
||||||
defer func() {
|
defer func() {
|
||||||
wantPanic := "invalid argument"
|
wantPanic := "argument contains null character"
|
||||||
if r := recover(); r != wantPanic {
|
if r := recover(); r != wantPanic {
|
||||||
t.Errorf("MustNewCheckedArgs: panic = %v, wantPanic %v",
|
t.Errorf("MustNewCheckedArgs(%q) panic = %v, wantPanic %v",
|
||||||
|
badPayload,
|
||||||
r, wantPanic)
|
r, wantPanic)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@ -56,8 +55,8 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput f
|
|||||||
|
|
||||||
t.Run("start helper with status channel and wait", func(t *testing.T) {
|
t.Run("start helper with status channel and wait", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
stdout := new(strings.Builder)
|
stdout, stderr := new(strings.Builder), new(strings.Builder)
|
||||||
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, true)
|
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, stderr }, true)
|
||||||
|
|
||||||
t.Run("wait not yet started helper", func(t *testing.T) {
|
t.Run("wait not yet started helper", func(t *testing.T) {
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -89,8 +88,8 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput f
|
|||||||
|
|
||||||
t.Log("waiting on helper")
|
t.Log("waiting on helper")
|
||||||
if err := h.Wait(); !errors.Is(err, context.Canceled) {
|
if err := h.Wait(); !errors.Is(err, context.Canceled) {
|
||||||
t.Errorf("Wait: error = %v",
|
t.Errorf("Wait() err = %v stderr = %s",
|
||||||
err)
|
err, stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("wait already finalised helper", func(t *testing.T) {
|
t.Run("wait already finalised helper", func(t *testing.T) {
|
||||||
@ -102,8 +101,8 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput f
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if got := trimStdout(stdout); got != wantPayload {
|
if got := stderr.String(); got != wantPayload {
|
||||||
t.Errorf("Start: stdout = %q, want %q",
|
t.Errorf("Start: stderr = %v, want %v",
|
||||||
got, wantPayload)
|
got, wantPayload)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -111,27 +110,23 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput f
|
|||||||
t.Run("start helper and wait", func(t *testing.T) {
|
t.Run("start helper and wait", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
stdout := new(strings.Builder)
|
stdout, stderr := new(strings.Builder), new(strings.Builder)
|
||||||
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, false)
|
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, stderr }, false)
|
||||||
|
|
||||||
if err := h.Start(); err != nil {
|
if err := h.Start(); err != nil {
|
||||||
t.Errorf("Start: error = %v",
|
t.Errorf("Start() error = %v",
|
||||||
err)
|
err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.Wait(); err != nil {
|
if err := h.Wait(); err != nil {
|
||||||
t.Errorf("Wait: error = %v stdout = %q",
|
t.Errorf("Wait() err = %v stdout = %s stderr = %s",
|
||||||
err, stdout)
|
err, stdout, stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if got := trimStdout(stdout); got != wantPayload {
|
if got := stderr.String(); got != wantPayload {
|
||||||
t.Errorf("Start: stdout = %q, want %q",
|
t.Errorf("Start() stderr = %v, want %v",
|
||||||
got, wantPayload)
|
got, wantPayload)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func trimStdout(stdout fmt.Stringer) string {
|
|
||||||
return strings.TrimPrefix(stdout.String(), "=== RUN TestHelperInit\n")
|
|
||||||
}
|
|
||||||
|
@ -63,7 +63,7 @@ func flagRestoreFiles(offset int, ap, sp string) (argsFile, statFile *os.File) {
|
|||||||
func genericStub(argsFile, statFile *os.File) {
|
func genericStub(argsFile, statFile *os.File) {
|
||||||
if argsFile != nil {
|
if argsFile != nil {
|
||||||
// this output is checked by parent
|
// this output is checked by parent
|
||||||
if _, err := io.Copy(os.Stdout, argsFile); err != nil {
|
if _, err := io.Copy(os.Stderr, argsFile); err != nil {
|
||||||
panic("cannot read args: " + err.Error())
|
panic("cannot read args: " + err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,7 +101,6 @@ var testCasesNixos = []sealTestCase{
|
|||||||
"HOME=/var/lib/persist/module/fortify/0/1",
|
"HOME=/var/lib/persist/module/fortify/0/1",
|
||||||
"PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie",
|
"PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie",
|
||||||
"PULSE_SERVER=unix:/run/user/1971/pulse/native",
|
"PULSE_SERVER=unix:/run/user/1971/pulse/native",
|
||||||
"SHELL=/run/current-system/sw/bin/zsh",
|
|
||||||
"TERM=xterm-256color",
|
"TERM=xterm-256color",
|
||||||
"USER=u0_a1",
|
"USER=u0_a1",
|
||||||
"WAYLAND_DISPLAY=wayland-0",
|
"WAYLAND_DISPLAY=wayland-0",
|
||||||
|
@ -41,7 +41,6 @@ var testCasesPd = []sealTestCase{
|
|||||||
Args: []string{"/run/current-system/sw/bin/zsh"},
|
Args: []string{"/run/current-system/sw/bin/zsh"},
|
||||||
Env: []string{
|
Env: []string{
|
||||||
"HOME=/home/chronos",
|
"HOME=/home/chronos",
|
||||||
"SHELL=/run/current-system/sw/bin/zsh",
|
|
||||||
"TERM=xterm-256color",
|
"TERM=xterm-256color",
|
||||||
"USER=chronos",
|
"USER=chronos",
|
||||||
"XDG_RUNTIME_DIR=/run/user/65534",
|
"XDG_RUNTIME_DIR=/run/user/65534",
|
||||||
@ -260,7 +259,6 @@ var testCasesPd = []sealTestCase{
|
|||||||
"HOME=/home/chronos",
|
"HOME=/home/chronos",
|
||||||
"PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie",
|
"PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie",
|
||||||
"PULSE_SERVER=unix:/run/user/65534/pulse/native",
|
"PULSE_SERVER=unix:/run/user/65534/pulse/native",
|
||||||
"SHELL=/run/current-system/sw/bin/zsh",
|
|
||||||
"TERM=xterm-256color",
|
"TERM=xterm-256color",
|
||||||
"USER=chronos",
|
"USER=chronos",
|
||||||
"WAYLAND_DISPLAY=wayland-0",
|
"WAYLAND_DISPLAY=wayland-0",
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"maps"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
@ -254,9 +255,8 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
|
|||||||
mapuid = newInt(uid)
|
mapuid = newInt(uid)
|
||||||
mapgid = newInt(gid)
|
mapgid = newInt(gid)
|
||||||
if seal.env == nil {
|
if seal.env == nil {
|
||||||
seal.env = make(map[string]string, 1<<6)
|
seal.env = make(map[string]string)
|
||||||
}
|
}
|
||||||
seal.env[shell] = shellPath
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -504,13 +504,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
|
|||||||
|
|
||||||
// flatten and sort env for deterministic behaviour
|
// flatten and sort env for deterministic behaviour
|
||||||
seal.container.Env = make([]string, 0, len(seal.env))
|
seal.container.Env = make([]string, 0, len(seal.env))
|
||||||
for k, v := range seal.env {
|
maps.All(seal.env)(func(k string, v string) bool { seal.container.Env = append(seal.container.Env, k+"="+v); return true })
|
||||||
if strings.IndexByte(k, '=') != -1 {
|
|
||||||
return fmsg.WrapError(syscall.EINVAL,
|
|
||||||
fmt.Sprintf("invalid environment variable %s", k))
|
|
||||||
}
|
|
||||||
seal.container.Env = append(seal.container.Env, k+"="+v)
|
|
||||||
}
|
|
||||||
slices.Sort(seal.container.Env)
|
slices.Sort(seal.container.Env)
|
||||||
|
|
||||||
fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s",
|
fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s",
|
||||||
|
@ -33,10 +33,10 @@ func (s *multiStore) Do(aid int, f func(c Cursor)) (bool, error) {
|
|||||||
|
|
||||||
// load or initialise new backend
|
// load or initialise new backend
|
||||||
b := new(multiBackend)
|
b := new(multiBackend)
|
||||||
b.lock.Lock()
|
|
||||||
if v, ok := s.backends.LoadOrStore(aid, b); ok {
|
if v, ok := s.backends.LoadOrStore(aid, b); ok {
|
||||||
b = v.(*multiBackend)
|
b = v.(*multiBackend)
|
||||||
} else {
|
} else {
|
||||||
|
b.lock.Lock()
|
||||||
b.path = path.Join(s.base, strconv.Itoa(aid))
|
b.path = path.Join(s.base, strconv.Itoa(aid))
|
||||||
|
|
||||||
// ensure directory
|
// ensure directory
|
||||||
|
@ -47,7 +47,7 @@ type State interface {
|
|||||||
Uid(aid int) (int, error)
|
Uid(aid int) (int, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CopyPaths is a generic implementation of [fst.Paths].
|
// CopyPaths is a generic implementation of [System.Paths].
|
||||||
func CopyPaths(os State, v *fst.Paths) {
|
func CopyPaths(os State, v *fst.Paths) {
|
||||||
v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Getuid()))
|
v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Getuid()))
|
||||||
|
|
||||||
|
22
nixos.nix
22
nixos.nix
@ -88,15 +88,11 @@ in
|
|||||||
|
|
||||||
conf = {
|
conf = {
|
||||||
inherit (app) id;
|
inherit (app) id;
|
||||||
path =
|
path = pkgs.writeScript "${app.name}-start" ''
|
||||||
if app.path == null then
|
#!${pkgs.zsh}${pkgs.zsh.shellPath}
|
||||||
pkgs.writeScript "${app.name}-start" ''
|
${script}
|
||||||
#!${pkgs.zsh}${pkgs.zsh.shellPath}
|
'';
|
||||||
${script}
|
args = [ "${app.name}-start" ];
|
||||||
''
|
|
||||||
else
|
|
||||||
app.path;
|
|
||||||
args = if app.args == null then [ "${app.name}-start" ] else app.args;
|
|
||||||
|
|
||||||
confinement = {
|
confinement = {
|
||||||
app_id = aid;
|
app_id = aid;
|
||||||
@ -201,11 +197,9 @@ in
|
|||||||
${copy "${pkg}/share/icons"}
|
${copy "${pkg}/share/icons"}
|
||||||
${copy "${pkg}/share/man"}
|
${copy "${pkg}/share/man"}
|
||||||
|
|
||||||
if test -d "$out/share/applications"; then
|
substituteInPlace $out/share/applications/* \
|
||||||
substituteInPlace $out/share/applications/* \
|
--replace-warn '${pkg}/bin/' "" \
|
||||||
--replace-warn '${pkg}/bin/' "" \
|
--replace-warn '${pkg}/libexec/' ""
|
||||||
--replace-warn '${pkg}/libexec/' ""
|
|
||||||
fi
|
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
++ acc
|
++ acc
|
||||||
|
42
options.md
42
options.md
@ -35,7 +35,7 @@ package
|
|||||||
|
|
||||||
|
|
||||||
*Default:*
|
*Default:*
|
||||||
` <derivation fortify-static-x86_64-unknown-linux-musl-0.3.2> `
|
` <derivation fortify-static-x86_64-unknown-linux-musl-0.3.1> `
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -73,25 +73,6 @@ list of package
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
## environment\.fortify\.apps\.\*\.args
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Custom args\.
|
|
||||||
Setting this to null will default to script name\.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
*Type:*
|
|
||||||
null or (list of string)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
*Default:*
|
|
||||||
` null `
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## environment\.fortify\.apps\.\*\.capability\.dbus
|
## environment\.fortify\.apps\.\*\.capability\.dbus
|
||||||
|
|
||||||
|
|
||||||
@ -505,25 +486,6 @@ boolean
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
## environment\.fortify\.apps\.\*\.path
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Custom executable path\.
|
|
||||||
Setting this to null will default to the start script\.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
*Type:*
|
|
||||||
null or string
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
*Default:*
|
|
||||||
` null `
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## environment\.fortify\.apps\.\*\.script
|
## environment\.fortify\.apps\.\*\.script
|
||||||
|
|
||||||
|
|
||||||
@ -644,7 +606,7 @@ package
|
|||||||
|
|
||||||
|
|
||||||
*Default:*
|
*Default:*
|
||||||
` <derivation fortify-fsu-0.3.2> `
|
` <derivation fortify-fsu-0.3.1> `
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
18
options.nix
18
options.nix
@ -94,24 +94,6 @@ in
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
path = mkOption {
|
|
||||||
type = nullOr str;
|
|
||||||
default = null;
|
|
||||||
description = ''
|
|
||||||
Custom executable path.
|
|
||||||
Setting this to null will default to the start script.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
args = mkOption {
|
|
||||||
type = nullOr (listOf str);
|
|
||||||
default = null;
|
|
||||||
description = ''
|
|
||||||
Custom args.
|
|
||||||
Setting this to null will default to script name.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
script = mkOption {
|
script = mkOption {
|
||||||
type = nullOr str;
|
type = nullOr str;
|
||||||
default = null;
|
default = null;
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
buildGoModule rec {
|
buildGoModule rec {
|
||||||
pname = "fortify";
|
pname = "fortify";
|
||||||
version = "0.3.2";
|
version = "0.3.1";
|
||||||
|
|
||||||
src = builtins.path {
|
src = builtins.path {
|
||||||
name = "${pname}-src";
|
name = "${pname}-src";
|
||||||
|
@ -99,8 +99,6 @@ type (
|
|||||||
// Permission bits of newly created parent directories.
|
// Permission bits of newly created parent directories.
|
||||||
// The zero value is interpreted as 0755.
|
// The zero value is interpreted as 0755.
|
||||||
ParentPerm os.FileMode
|
ParentPerm os.FileMode
|
||||||
// Retain CAP_SYS_ADMIN.
|
|
||||||
Privileged bool
|
|
||||||
|
|
||||||
Flags HardeningFlags
|
Flags HardeningFlags
|
||||||
}
|
}
|
||||||
|
@ -223,30 +223,17 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
|||||||
if _, _, errno := syscall.Syscall(PR_SET_NO_NEW_PRIVS, 1, 0, 0); errno != 0 {
|
if _, _, errno := syscall.Syscall(PR_SET_NO_NEW_PRIVS, 1, 0, 0); errno != 0 {
|
||||||
log.Fatalf("prctl(PR_SET_NO_NEW_PRIVS): %v", errno)
|
log.Fatalf("prctl(PR_SET_NO_NEW_PRIVS): %v", errno)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0); errno != 0 {
|
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0); errno != 0 {
|
||||||
log.Fatalf("cannot clear the ambient capability set: %v", errno)
|
log.Fatalf("cannot clear the ambient capability set: %v", errno)
|
||||||
}
|
}
|
||||||
for i := uintptr(0); i <= LastCap(); i++ {
|
for i := uintptr(0); i <= LastCap(); i++ {
|
||||||
if params.Privileged && i == CAP_SYS_ADMIN {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_CAPBSET_DROP, i, 0); errno != 0 {
|
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_CAPBSET_DROP, i, 0); errno != 0 {
|
||||||
log.Fatalf("cannot drop capability from bonding set: %v", errno)
|
log.Fatalf("cannot drop capability from bonding set: %v", errno)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var keep [2]uint32
|
|
||||||
if params.Privileged {
|
|
||||||
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
|
|
||||||
|
|
||||||
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_SYS_ADMIN); errno != 0 {
|
|
||||||
log.Fatalf("cannot raise CAP_SYS_ADMIN: %v", errno)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := capset(
|
if err := capset(
|
||||||
&capHeader{_LINUX_CAPABILITY_VERSION_3, 0},
|
&capHeader{_LINUX_CAPABILITY_VERSION_3, 0},
|
||||||
&[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}},
|
&[2]capData{{0, 0, 0}, {0, 0, 0}},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.Fatalf("cannot capset: %v", err)
|
log.Fatalf("cannot capset: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -73,16 +73,6 @@ func TestExport(t *testing.T) {
|
|||||||
0x80, 0x8b, 0x1a, 0x6f, 0x84, 0xf3, 0x2b, 0xbd,
|
0x80, 0x8b, 0x1a, 0x6f, 0x84, 0xf3, 0x2b, 0xbd,
|
||||||
0xe1, 0xaa, 0x02, 0xae, 0x30, 0xee, 0xdc, 0xfa,
|
0xe1, 0xaa, 0x02, 0xae, 0x30, 0xee, 0xdc, 0xfa,
|
||||||
}, false},
|
}, false},
|
||||||
{"fortify default", seccomp.FlagExt | seccomp.FlagDenyDevel, []byte{
|
|
||||||
0xc6, 0x98, 0xb0, 0x81, 0xff, 0x95, 0x7a, 0xfe,
|
|
||||||
0x17, 0xa6, 0xd9, 0x43, 0x74, 0x53, 0x7d, 0x37,
|
|
||||||
0xf2, 0xa6, 0x3f, 0x6f, 0x9d, 0xd7, 0x5d, 0xa7,
|
|
||||||
0x54, 0x65, 0x42, 0x40, 0x7a, 0x9e, 0x32, 0x47,
|
|
||||||
0x6e, 0xbd, 0xa3, 0x31, 0x2b, 0xa7, 0x78, 0x5d,
|
|
||||||
0x7f, 0x61, 0x85, 0x42, 0xbc, 0xfa, 0xf2, 0x7c,
|
|
||||||
0xa2, 0x7d, 0xcc, 0x2d, 0xdd, 0xba, 0x85, 0x20,
|
|
||||||
0x69, 0xd2, 0x8b, 0xcf, 0xe8, 0xca, 0xd3, 0x9a,
|
|
||||||
}, false},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := make([]byte, 8)
|
buf := make([]byte, 8)
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
// Package seccomp provides filter presets and high level wrappers around libseccomp.
|
|
||||||
package seccomp
|
package seccomp
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -11,7 +11,6 @@ const (
|
|||||||
PR_SET_NO_NEW_PRIVS = 0x26
|
PR_SET_NO_NEW_PRIVS = 0x26
|
||||||
|
|
||||||
CAP_SYS_ADMIN = 0x15
|
CAP_SYS_ADMIN = 0x15
|
||||||
CAP_SETPCAP = 0x8
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -31,9 +30,10 @@ func SetDumpable(dumpable uintptr) error {
|
|||||||
const (
|
const (
|
||||||
_LINUX_CAPABILITY_VERSION_3 = 0x20080522
|
_LINUX_CAPABILITY_VERSION_3 = 0x20080522
|
||||||
|
|
||||||
PR_CAP_AMBIENT = 0x2f
|
PR_CAP_AMBIENT = 47
|
||||||
PR_CAP_AMBIENT_RAISE = 0x2
|
PR_CAP_AMBIENT_CLEAR_ALL = 4
|
||||||
PR_CAP_AMBIENT_CLEAR_ALL = 0x4
|
|
||||||
|
CAP_SETPCAP = 8
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@ -49,12 +49,6 @@ type (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// See CAP_TO_INDEX in linux/capability.h:
|
|
||||||
func capToIndex(cap uintptr) uintptr { return cap >> 5 }
|
|
||||||
|
|
||||||
// See CAP_TO_MASK in linux/capability.h:
|
|
||||||
func capToMask(cap uintptr) uint32 { return 1 << uint(cap&31) }
|
|
||||||
|
|
||||||
func capset(hdrp *capHeader, datap *[2]capData) error {
|
func capset(hdrp *capHeader, datap *[2]capData) error {
|
||||||
if _, _, errno := syscall.Syscall(syscall.SYS_CAPSET,
|
if _, _, errno := syscall.Syscall(syscall.SYS_CAPSET,
|
||||||
uintptr(unsafe.Pointer(hdrp)),
|
uintptr(unsafe.Pointer(hdrp)),
|
||||||
|
@ -4,6 +4,12 @@
|
|||||||
config,
|
config,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
|
let
|
||||||
|
testCases = import ./sandbox/case {
|
||||||
|
inherit (pkgs) lib callPackage foot;
|
||||||
|
inherit (config.environment.fortify.package) version;
|
||||||
|
};
|
||||||
|
in
|
||||||
{
|
{
|
||||||
users.users = {
|
users.users = {
|
||||||
alice = {
|
alice = {
|
||||||
@ -102,6 +108,10 @@
|
|||||||
home-manager = _: _: { home.stateVersion = "23.05"; };
|
home-manager = _: _: { home.stateVersion = "23.05"; };
|
||||||
|
|
||||||
apps = [
|
apps = [
|
||||||
|
testCases.preset
|
||||||
|
testCases.tty
|
||||||
|
testCases.mapuid
|
||||||
|
|
||||||
{
|
{
|
||||||
name = "ne-foot";
|
name = "ne-foot";
|
||||||
verbose = true;
|
verbose = true;
|
||||||
|
@ -7,15 +7,10 @@ in the public sandbox/vfs package. Files in this package are excluded by the bui
|
|||||||
package sandbox
|
package sandbox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha512"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -28,7 +23,6 @@ func printf(format string, v ...any) { printfFunc(format, v...) }
|
|||||||
func fatalf(format string, v ...any) { fatalfFunc(format, v...) }
|
func fatalf(format string, v ...any) { fatalfFunc(format, v...) }
|
||||||
|
|
||||||
type TestCase struct {
|
type TestCase struct {
|
||||||
Env []string `json:"env"`
|
|
||||||
FS *FS `json:"fs"`
|
FS *FS `json:"fs"`
|
||||||
Mount []*MountinfoEntry `json:"mount"`
|
Mount []*MountinfoEntry `json:"mount"`
|
||||||
Seccomp bool `json:"seccomp"`
|
Seccomp bool `json:"seccomp"`
|
||||||
@ -40,46 +34,13 @@ type T struct {
|
|||||||
MountsPath string
|
MountsPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *T) MustCheckFile(wantFilePath, markerPath string) {
|
func (t *T) MustCheckFile(wantFilePath string) {
|
||||||
var want *TestCase
|
var want *TestCase
|
||||||
mustDecode(wantFilePath, &want)
|
mustDecode(wantFilePath, &want)
|
||||||
t.MustCheck(want)
|
t.MustCheck(want)
|
||||||
if _, err := os.Create(markerPath); err != nil {
|
|
||||||
fatalf("cannot create success marker: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *T) MustCheck(want *TestCase) {
|
func (t *T) MustCheck(want *TestCase) {
|
||||||
if want.Env != nil {
|
|
||||||
var (
|
|
||||||
fail bool
|
|
||||||
i int
|
|
||||||
got string
|
|
||||||
)
|
|
||||||
for i, got = range os.Environ() {
|
|
||||||
if i == len(want.Env) {
|
|
||||||
fatalf("got more than %d environment variables", len(want.Env))
|
|
||||||
}
|
|
||||||
if got != want.Env[i] {
|
|
||||||
fail = true
|
|
||||||
printf("[FAIL] %s", got)
|
|
||||||
} else {
|
|
||||||
printf("[ OK ] %s", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
i++
|
|
||||||
if i != len(want.Env) {
|
|
||||||
fatalf("got %d environment variables, want %d", i, len(want.Env))
|
|
||||||
}
|
|
||||||
|
|
||||||
if fail {
|
|
||||||
fatalf("[FAIL] some environment variables did not match")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
printf("[SKIP] skipping environ check")
|
|
||||||
}
|
|
||||||
|
|
||||||
if want.FS != nil && t.FS != nil {
|
if want.FS != nil && t.FS != nil {
|
||||||
if err := want.FS.Compare(".", t.FS); err != nil {
|
if err := want.FS.Compare(".", t.FS); err != nil {
|
||||||
fatalf("%v", err)
|
fatalf("%v", err)
|
||||||
@ -121,7 +82,7 @@ func (t *T) MustCheck(want *TestCase) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if want.Seccomp {
|
if want.Seccomp {
|
||||||
if trySyscalls() != nil {
|
if TrySyscalls() != nil {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -129,81 +90,6 @@ func (t *T) MustCheck(want *TestCase) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func MustCheckFilter(pid int, want string) {
|
|
||||||
err := CheckFilter(pid, want)
|
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var perr *ptraceError
|
|
||||||
if !errors.As(err, &perr) {
|
|
||||||
fatalf("%s", err)
|
|
||||||
}
|
|
||||||
switch perr.op {
|
|
||||||
case "PTRACE_ATTACH":
|
|
||||||
fatalf("cannot attach to process %d: %v", pid, err)
|
|
||||||
case "PTRACE_SECCOMP_GET_FILTER":
|
|
||||||
if perr.errno == syscall.ENOENT {
|
|
||||||
fatalf("seccomp filter not installed for process %d", pid)
|
|
||||||
}
|
|
||||||
fatalf("cannot get filter: %v", err)
|
|
||||||
default:
|
|
||||||
fatalf("cannot check filter: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
*(*int)(nil) = 0 // not reached
|
|
||||||
}
|
|
||||||
|
|
||||||
func CheckFilter(pid int, want string) error {
|
|
||||||
if err := ptraceAttach(pid); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := ptraceDetach(pid); err != nil {
|
|
||||||
printf("cannot detach from process %d: %v", pid, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
h := sha512.New()
|
|
||||||
{
|
|
||||||
getFilter:
|
|
||||||
buf, err := getFilter[[8]byte](pid, 0)
|
|
||||||
/* this is not how ESRCH should be handled: the manpage advises the
|
|
||||||
use of waitpid, however that is not applicable for attaching to an
|
|
||||||
arbitrary process, and spawning target process here is not easily
|
|
||||||
possible under the current testing framework;
|
|
||||||
|
|
||||||
despite checking for /proc/pid/status indicating state t (tracing stop),
|
|
||||||
it does not appear to be directly related to the internal state used to
|
|
||||||
determine whether a process is ready to accept ptrace operations, it also
|
|
||||||
introduces a TOCTOU that is irrelevant in the testing vm; this behaviour
|
|
||||||
is kept anyway as it reduces the average iterations required here;
|
|
||||||
|
|
||||||
since this code is only ever compiled into the test program, whatever
|
|
||||||
implications this ugliness might have should not hurt anyone */
|
|
||||||
if errors.Is(err, syscall.ESRCH) {
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
goto getFilter
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, b := range buf {
|
|
||||||
h.Write(b[:])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := hex.EncodeToString(h.Sum(nil)); got != want {
|
|
||||||
printf("[FAIL] %s", got)
|
|
||||||
return syscall.ENOTRECOVERABLE
|
|
||||||
} else {
|
|
||||||
printf("[ OK ] %s", got)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustDecode(wantFilePath string, v any) {
|
func mustDecode(wantFilePath string, v any) {
|
||||||
if f, err := os.Open(wantFilePath); err != nil {
|
if f, err := os.Open(wantFilePath); err != nil {
|
||||||
fatalf("cannot open %q: %v", wantFilePath, err)
|
fatalf("cannot open %q: %v", wantFilePath, err)
|
||||||
|
30
test/sandbox/assert.nix
Normal file
30
test/sandbox/assert.nix
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
writeText,
|
||||||
|
buildGoModule,
|
||||||
|
pkg-config,
|
||||||
|
util-linux,
|
||||||
|
|
||||||
|
version,
|
||||||
|
}:
|
||||||
|
buildGoModule {
|
||||||
|
pname = "check-sandbox";
|
||||||
|
inherit version;
|
||||||
|
|
||||||
|
src = ../.;
|
||||||
|
vendorHash = null;
|
||||||
|
|
||||||
|
buildInputs = [ util-linux ];
|
||||||
|
nativeBuildInputs = [ pkg-config ];
|
||||||
|
|
||||||
|
preBuild = ''
|
||||||
|
go mod init git.gensokyo.uk/security/fortify/test >& /dev/null
|
||||||
|
cp ${writeText "main.go" ''
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
import "git.gensokyo.uk/security/fortify/test/sandbox"
|
||||||
|
|
||||||
|
func main() { (&sandbox.T{FS: os.DirFS("/")}).MustCheckFile(os.Args[1]) }
|
||||||
|
''} main.go
|
||||||
|
'';
|
||||||
|
}
|
@ -1,4 +1,10 @@
|
|||||||
lib: testProgram:
|
{
|
||||||
|
lib,
|
||||||
|
callPackage,
|
||||||
|
foot,
|
||||||
|
|
||||||
|
version,
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
fs = mode: dir: data: {
|
fs = mode: dir: data: {
|
||||||
mode = lib.fromHexString mode;
|
mode = lib.fromHexString mode;
|
||||||
@ -23,6 +29,8 @@ let
|
|||||||
;
|
;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
checkSandbox = callPackage ../. { inherit version; };
|
||||||
|
|
||||||
callTestCase =
|
callTestCase =
|
||||||
path:
|
path:
|
||||||
let
|
let
|
||||||
@ -38,13 +46,9 @@ let
|
|||||||
name = "check-sandbox-${tc.name}";
|
name = "check-sandbox-${tc.name}";
|
||||||
verbose = true;
|
verbose = true;
|
||||||
inherit (tc) tty mapRealUid;
|
inherit (tc) tty mapRealUid;
|
||||||
share = testProgram;
|
share = foot;
|
||||||
packages = [ ];
|
packages = [ ];
|
||||||
path = "${testProgram}/bin/fortify-test";
|
command = builtins.toString (checkSandbox tc.name tc.want);
|
||||||
args = [
|
|
||||||
"test"
|
|
||||||
(toString (builtins.toFile "fortify-${tc.name}-want.json" (builtins.toJSON tc.want)))
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
@ -9,19 +9,6 @@
|
|||||||
mapRealUid = true;
|
mapRealUid = true;
|
||||||
|
|
||||||
want = {
|
want = {
|
||||||
env = [
|
|
||||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus"
|
|
||||||
"HOME=/var/lib/fortify/u0/a3"
|
|
||||||
"PULSE_SERVER=unix:/run/user/1000/pulse/native"
|
|
||||||
"SHELL=/run/current-system/sw/bin/bash"
|
|
||||||
"TERM=linux"
|
|
||||||
"USER=u0_a3"
|
|
||||||
"WAYLAND_DISPLAY=wayland-0"
|
|
||||||
"XDG_RUNTIME_DIR=/run/user/1000"
|
|
||||||
"XDG_SESSION_CLASS=user"
|
|
||||||
"XDG_SESSION_TYPE=tty"
|
|
||||||
];
|
|
||||||
|
|
||||||
fs = fs "dead" {
|
fs = fs "dead" {
|
||||||
".fortify" = fs "800001ed" {
|
".fortify" = fs "800001ed" {
|
||||||
etc = fs "800001ed" null null;
|
etc = fs "800001ed" null null;
|
||||||
@ -97,6 +84,7 @@
|
|||||||
"pki" = fs "80001ff" null null;
|
"pki" = fs "80001ff" null null;
|
||||||
"polkit-1" = fs "80001ff" null null;
|
"polkit-1" = fs "80001ff" null null;
|
||||||
"profile" = fs "80001ff" null null;
|
"profile" = fs "80001ff" null null;
|
||||||
|
"profiles" = fs "80001ff" null null;
|
||||||
"protocols" = fs "80001ff" null null;
|
"protocols" = fs "80001ff" null null;
|
||||||
"resolv.conf" = fs "80001ff" null null;
|
"resolv.conf" = fs "80001ff" null null;
|
||||||
"resolvconf.conf" = fs "80001ff" null null;
|
"resolvconf.conf" = fs "80001ff" null null;
|
||||||
|
@ -9,19 +9,6 @@
|
|||||||
mapRealUid = false;
|
mapRealUid = false;
|
||||||
|
|
||||||
want = {
|
want = {
|
||||||
env = [
|
|
||||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
|
|
||||||
"HOME=/var/lib/fortify/u0/a1"
|
|
||||||
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
|
|
||||||
"SHELL=/run/current-system/sw/bin/bash"
|
|
||||||
"TERM=linux"
|
|
||||||
"USER=u0_a1"
|
|
||||||
"WAYLAND_DISPLAY=wayland-0"
|
|
||||||
"XDG_RUNTIME_DIR=/run/user/65534"
|
|
||||||
"XDG_SESSION_CLASS=user"
|
|
||||||
"XDG_SESSION_TYPE=tty"
|
|
||||||
];
|
|
||||||
|
|
||||||
fs = fs "dead" {
|
fs = fs "dead" {
|
||||||
".fortify" = fs "800001ed" {
|
".fortify" = fs "800001ed" {
|
||||||
etc = fs "800001ed" null null;
|
etc = fs "800001ed" null null;
|
||||||
@ -97,6 +84,7 @@
|
|||||||
"pki" = fs "80001ff" null null;
|
"pki" = fs "80001ff" null null;
|
||||||
"polkit-1" = fs "80001ff" null null;
|
"polkit-1" = fs "80001ff" null null;
|
||||||
"profile" = fs "80001ff" null null;
|
"profile" = fs "80001ff" null null;
|
||||||
|
"profiles" = fs "80001ff" null null;
|
||||||
"protocols" = fs "80001ff" null null;
|
"protocols" = fs "80001ff" null null;
|
||||||
"resolv.conf" = fs "80001ff" null null;
|
"resolv.conf" = fs "80001ff" null null;
|
||||||
"resolvconf.conf" = fs "80001ff" null null;
|
"resolvconf.conf" = fs "80001ff" null null;
|
||||||
|
@ -9,19 +9,6 @@
|
|||||||
mapRealUid = false;
|
mapRealUid = false;
|
||||||
|
|
||||||
want = {
|
want = {
|
||||||
env = [
|
|
||||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
|
|
||||||
"HOME=/var/lib/fortify/u0/a2"
|
|
||||||
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
|
|
||||||
"SHELL=/run/current-system/sw/bin/bash"
|
|
||||||
"TERM=linux"
|
|
||||||
"USER=u0_a2"
|
|
||||||
"WAYLAND_DISPLAY=wayland-0"
|
|
||||||
"XDG_RUNTIME_DIR=/run/user/65534"
|
|
||||||
"XDG_SESSION_CLASS=user"
|
|
||||||
"XDG_SESSION_TYPE=tty"
|
|
||||||
];
|
|
||||||
|
|
||||||
fs = fs "dead" {
|
fs = fs "dead" {
|
||||||
".fortify" = fs "800001ed" {
|
".fortify" = fs "800001ed" {
|
||||||
etc = fs "800001ed" null null;
|
etc = fs "800001ed" null null;
|
||||||
@ -98,6 +85,7 @@
|
|||||||
"pki" = fs "80001ff" null null;
|
"pki" = fs "80001ff" null null;
|
||||||
"polkit-1" = fs "80001ff" null null;
|
"polkit-1" = fs "80001ff" null null;
|
||||||
"profile" = fs "80001ff" null null;
|
"profile" = fs "80001ff" null null;
|
||||||
|
"profiles" = fs "80001ff" null null;
|
||||||
"protocols" = fs "80001ff" null null;
|
"protocols" = fs "80001ff" null null;
|
||||||
"resolv.conf" = fs "80001ff" null null;
|
"resolv.conf" = fs "80001ff" null null;
|
||||||
"resolvconf.conf" = fs "80001ff" null null;
|
"resolvconf.conf" = fs "80001ff" null null;
|
||||||
|
@ -1,76 +0,0 @@
|
|||||||
{
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
config,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
testProgram = pkgs.callPackage ./tool/package.nix { inherit (config.environment.fortify.package) version; };
|
|
||||||
testCases = import ./case lib testProgram;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
users.users = {
|
|
||||||
alice = {
|
|
||||||
isNormalUser = true;
|
|
||||||
description = "Alice Foobar";
|
|
||||||
password = "foobar";
|
|
||||||
uid = 1000;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
home-manager.users.alice.home.stateVersion = "24.11";
|
|
||||||
|
|
||||||
# Automatically login on tty1 as a normal user:
|
|
||||||
services.getty.autologinUser = "alice";
|
|
||||||
|
|
||||||
environment = {
|
|
||||||
systemPackages = with pkgs; [
|
|
||||||
# For checking seccomp outcome:
|
|
||||||
testProgram
|
|
||||||
];
|
|
||||||
|
|
||||||
variables = {
|
|
||||||
SWAYSOCK = "/tmp/sway-ipc.sock";
|
|
||||||
WLR_RENDERER = "pixman";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# Automatically configure and start Sway when logging in on tty1:
|
|
||||||
programs.bash.loginShellInit = ''
|
|
||||||
if [ "$(tty)" = "/dev/tty1" ]; then
|
|
||||||
set -e
|
|
||||||
|
|
||||||
mkdir -p ~/.config/sway
|
|
||||||
(sed s/Mod4/Mod1/ /etc/sway/config &&
|
|
||||||
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=session sway && touch /tmp/sway-exit-ok
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
|
|
||||||
programs.sway.enable = true;
|
|
||||||
|
|
||||||
virtualisation.qemu.options = [
|
|
||||||
# Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch:
|
|
||||||
"-vga none -device virtio-gpu-pci"
|
|
||||||
|
|
||||||
# Increase performance:
|
|
||||||
"-smp 8"
|
|
||||||
];
|
|
||||||
|
|
||||||
environment.fortify = {
|
|
||||||
enable = true;
|
|
||||||
stateDir = "/var/lib/fortify";
|
|
||||||
users.alice = 0;
|
|
||||||
|
|
||||||
home-manager = _: _: { home.stateVersion = "23.05"; };
|
|
||||||
|
|
||||||
apps = [
|
|
||||||
testCases.preset
|
|
||||||
testCases.tty
|
|
||||||
testCases.mapuid
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,39 +1,14 @@
|
|||||||
{
|
{
|
||||||
lib,
|
writeShellScript,
|
||||||
nixosTest,
|
writeText,
|
||||||
|
callPackage,
|
||||||
|
|
||||||
self,
|
version,
|
||||||
withRace ? false,
|
|
||||||
}:
|
}:
|
||||||
|
name: want:
|
||||||
nixosTest {
|
writeShellScript "fortify-${name}-check-sandbox-script" ''
|
||||||
name = "fortify-sandbox" + (if withRace then "-race" else "");
|
set -e
|
||||||
nodes.machine =
|
${callPackage ./assert.nix { inherit version; }}/bin/test \
|
||||||
{ options, pkgs, ... }:
|
${writeText "fortify-${name}-want.json" (builtins.toJSON want)}
|
||||||
{
|
touch /tmp/sandbox-ok
|
||||||
# Run with Go race detector:
|
''
|
||||||
environment.fortify = lib.mkIf withRace rec {
|
|
||||||
# race detector does not support static linking
|
|
||||||
package = (pkgs.callPackage ../../package.nix { }).overrideAttrs (previousAttrs: {
|
|
||||||
GOFLAGS = previousAttrs.GOFLAGS ++ [ "-race" ];
|
|
||||||
});
|
|
||||||
fsuPackage = options.environment.fortify.fsuPackage.default.override { fortify = package; };
|
|
||||||
};
|
|
||||||
|
|
||||||
imports = [
|
|
||||||
./configuration.nix
|
|
||||||
|
|
||||||
self.nixosModules.fortify
|
|
||||||
self.inputs.home-manager.nixosModules.home-manager
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
# adapted from nixos sway integration tests
|
|
||||||
|
|
||||||
# testScriptWithTypes:49: error: Cannot call function of unknown type
|
|
||||||
# (machine.succeed if succeed else machine.execute)(
|
|
||||||
# ^
|
|
||||||
# Found 1 error in 1 file (checked 1 source file)
|
|
||||||
skipTypeCheck = true;
|
|
||||||
testScript = builtins.readFile ./test.py;
|
|
||||||
}
|
|
||||||
|
@ -1,119 +0,0 @@
|
|||||||
package sandbox
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
NULL = 0
|
|
||||||
|
|
||||||
PTRACE_ATTACH = 16
|
|
||||||
PTRACE_DETACH = 17
|
|
||||||
PTRACE_SECCOMP_GET_FILTER = 0x420c
|
|
||||||
)
|
|
||||||
|
|
||||||
type ptraceError struct {
|
|
||||||
op string
|
|
||||||
errno syscall.Errno
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ptraceError) Error() string { return fmt.Sprintf("%s: %v", p.op, p.errno) }
|
|
||||||
|
|
||||||
func (p *ptraceError) Unwrap() error {
|
|
||||||
if p.errno == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return p.errno
|
|
||||||
}
|
|
||||||
|
|
||||||
func ptrace(op uintptr, pid, addr int, data unsafe.Pointer) (r uintptr, errno syscall.Errno) {
|
|
||||||
r, _, errno = syscall.Syscall6(syscall.SYS_PTRACE, op, uintptr(pid), uintptr(addr), uintptr(data), NULL, NULL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func ptraceAttach(pid int) error {
|
|
||||||
const (
|
|
||||||
statePrefix = "State:"
|
|
||||||
stateSuffix = "t (tracing stop)"
|
|
||||||
)
|
|
||||||
|
|
||||||
var r io.ReadSeekCloser
|
|
||||||
if f, err := os.Open(fmt.Sprintf("/proc/%d/status", pid)); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
r = f
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, errno := ptrace(PTRACE_ATTACH, pid, 0, nil); errno != 0 {
|
|
||||||
return &ptraceError{"PTRACE_ATTACH", errno}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ugly! but there does not appear to be another way
|
|
||||||
for {
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
|
|
||||||
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s := bufio.NewScanner(r)
|
|
||||||
|
|
||||||
var found bool
|
|
||||||
for s.Scan() {
|
|
||||||
found = strings.HasPrefix(s.Text(), statePrefix)
|
|
||||||
if found {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := s.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
return syscall.EBADE
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasSuffix(s.Text(), stateSuffix) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ptraceDetach(pid int) error {
|
|
||||||
if _, errno := ptrace(PTRACE_DETACH, pid, 0, nil); errno != 0 {
|
|
||||||
return &ptraceError{"PTRACE_DETACH", errno}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type sockFilter struct { /* Filter block */
|
|
||||||
code uint16 /* Actual filter code */
|
|
||||||
jt uint8 /* Jump true */
|
|
||||||
jf uint8 /* Jump false */
|
|
||||||
k uint32 /* Generic multiuse field */
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFilter[T comparable](pid, index int) ([]T, error) {
|
|
||||||
if s := unsafe.Sizeof(*new(T)); s != 8 {
|
|
||||||
panic(fmt.Sprintf("invalid filter block size %d", s))
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf []T
|
|
||||||
if n, errno := ptrace(PTRACE_SECCOMP_GET_FILTER, pid, index, nil); errno != 0 {
|
|
||||||
return nil, &ptraceError{"PTRACE_SECCOMP_GET_FILTER", errno}
|
|
||||||
} else {
|
|
||||||
buf = make([]T, n)
|
|
||||||
}
|
|
||||||
if _, errno := ptrace(PTRACE_SECCOMP_GET_FILTER, pid, index, unsafe.Pointer(&buf[0])); errno != 0 {
|
|
||||||
return nil, &ptraceError{"PTRACE_SECCOMP_GET_FILTER", errno}
|
|
||||||
}
|
|
||||||
return buf, nil
|
|
||||||
}
|
|
@ -10,7 +10,9 @@ import (
|
|||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
func trySyscalls() error {
|
const NULL = 0
|
||||||
|
|
||||||
|
func TrySyscalls() error {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
errno syscall.Errno
|
errno syscall.Errno
|
||||||
|
@ -1,71 +0,0 @@
|
|||||||
import json
|
|
||||||
import shlex
|
|
||||||
|
|
||||||
q = shlex.quote
|
|
||||||
|
|
||||||
|
|
||||||
def swaymsg(command: str = "", succeed=True, type="command"):
|
|
||||||
assert command != "" or type != "command", "Must specify command or type"
|
|
||||||
shell = q(f"swaymsg -t {q(type)} -- {q(command)}")
|
|
||||||
with machine.nested(
|
|
||||||
f"sending swaymsg {shell!r}" + " (allowed to fail)" * (not succeed)
|
|
||||||
):
|
|
||||||
ret = (machine.succeed if succeed else machine.execute)(
|
|
||||||
f"su - alice -c {shell}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# execute also returns a status code, but disregard.
|
|
||||||
if not succeed:
|
|
||||||
_, ret = ret
|
|
||||||
|
|
||||||
if not succeed and not ret:
|
|
||||||
return None
|
|
||||||
|
|
||||||
parsed = json.loads(ret)
|
|
||||||
return parsed
|
|
||||||
|
|
||||||
|
|
||||||
start_all()
|
|
||||||
machine.wait_for_unit("multi-user.target")
|
|
||||||
|
|
||||||
# To check fortify's version:
|
|
||||||
print(machine.succeed("sudo -u alice -i fortify version"))
|
|
||||||
|
|
||||||
# Wait for Sway to complete startup:
|
|
||||||
machine.wait_for_file("/run/user/1000/wayland-1")
|
|
||||||
machine.wait_for_file("/tmp/sway-ipc.sock")
|
|
||||||
|
|
||||||
# Check seccomp outcome:
|
|
||||||
swaymsg("exec fortify run cat")
|
|
||||||
pid = int(machine.wait_until_succeeds("pgrep -U 1000000 -x cat", timeout=5))
|
|
||||||
print(machine.succeed(f"fortify-test filter {pid} c698b081ff957afe17a6d94374537d37f2a63f6f9dd75da7546542407a9e32476ebda3312ba7785d7f618542bcfaf27ca27dcc2dddba852069d28bcfe8cad39a &>/dev/stdout", timeout=5))
|
|
||||||
machine.succeed(f"kill -TERM {pid}")
|
|
||||||
|
|
||||||
# Verify capabilities/securebits in user namespace:
|
|
||||||
print(machine.succeed("sudo -u alice -i fortify run capsh --print"))
|
|
||||||
print(machine.succeed("sudo -u alice -i fortify run capsh --has-no-new-privs"))
|
|
||||||
print(machine.fail("sudo -u alice -i fortify run capsh --has-a=CAP_SYS_ADMIN"))
|
|
||||||
print(machine.fail("sudo -u alice -i fortify run capsh --has-b=CAP_SYS_ADMIN"))
|
|
||||||
print(machine.fail("sudo -u alice -i fortify run capsh --has-i=CAP_SYS_ADMIN"))
|
|
||||||
print(machine.fail("sudo -u alice -i fortify run capsh --has-p=CAP_SYS_ADMIN"))
|
|
||||||
print(machine.fail("sudo -u alice -i fortify run umount -R /dev"))
|
|
||||||
|
|
||||||
# Check sandbox outcome:
|
|
||||||
check_offset = 0
|
|
||||||
def check_sandbox(name):
|
|
||||||
global check_offset
|
|
||||||
check_offset += 1
|
|
||||||
swaymsg(f"exec script /dev/null -E always -qec check-sandbox-{name}")
|
|
||||||
machine.wait_for_file(f"/tmp/fortify.1000/tmpdir/{check_offset}/sandbox-ok", timeout=15)
|
|
||||||
|
|
||||||
|
|
||||||
check_sandbox("preset")
|
|
||||||
check_sandbox("tty")
|
|
||||||
check_sandbox("mapuid")
|
|
||||||
|
|
||||||
# Exit Sway and verify process exit status 0:
|
|
||||||
swaymsg("exit", succeed=False)
|
|
||||||
machine.wait_for_file("/tmp/sway-exit-ok")
|
|
||||||
|
|
||||||
# Print fortify runDir contents:
|
|
||||||
print(machine.succeed("find /run/user/1000/fortify"))
|
|
@ -1,39 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/test/sandbox"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
log.SetFlags(0)
|
|
||||||
log.SetPrefix("test: ")
|
|
||||||
|
|
||||||
if len(os.Args) < 2 {
|
|
||||||
log.Fatal("invalid argument")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch os.Args[1] {
|
|
||||||
case "filter":
|
|
||||||
if len(os.Args) != 4 {
|
|
||||||
log.Fatal("invalid argument")
|
|
||||||
}
|
|
||||||
|
|
||||||
if pid, err := strconv.Atoi(strings.TrimSpace(os.Args[2])); err != nil {
|
|
||||||
log.Fatalf("%s", err)
|
|
||||||
} else if pid < 1 {
|
|
||||||
log.Fatalf("%d out of range", pid)
|
|
||||||
} else {
|
|
||||||
sandbox.MustCheckFilter(pid, os.Args[3])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
(&sandbox.T{FS: os.DirFS("/")}).MustCheckFile(os.Args[1], "/tmp/sandbox-ok")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
lib,
|
|
||||||
buildGoModule,
|
|
||||||
pkg-config,
|
|
||||||
util-linux,
|
|
||||||
|
|
||||||
version,
|
|
||||||
}:
|
|
||||||
buildGoModule rec {
|
|
||||||
pname = "check-sandbox";
|
|
||||||
inherit version;
|
|
||||||
|
|
||||||
src = builtins.path {
|
|
||||||
name = "${pname}-src";
|
|
||||||
path = lib.cleanSource ../.;
|
|
||||||
filter = path: type: (type == "directory") || (type == "regular" && lib.hasSuffix ".go" path);
|
|
||||||
};
|
|
||||||
vendorHash = null;
|
|
||||||
|
|
||||||
buildInputs = [ util-linux ];
|
|
||||||
nativeBuildInputs = [ pkg-config ];
|
|
||||||
|
|
||||||
preBuild = ''
|
|
||||||
go mod init git.gensokyo.uk/security/fortify/test/sandbox >& /dev/null
|
|
||||||
'';
|
|
||||||
|
|
||||||
postInstall = ''
|
|
||||||
mv $out/bin/tool $out/bin/fortify-test
|
|
||||||
'';
|
|
||||||
}
|
|
19
test/test.py
19
test/test.py
@ -99,15 +99,34 @@ print(denyOutputVerbose)
|
|||||||
# Fail direct fsu call:
|
# Fail direct fsu call:
|
||||||
print(machine.fail("sudo -u alice -i fsu"))
|
print(machine.fail("sudo -u alice -i fsu"))
|
||||||
|
|
||||||
|
# Verify capabilities/securebits in user namespace:
|
||||||
|
print(machine.succeed("sudo -u alice -i fortify run capsh --print"))
|
||||||
|
print(machine.succeed("sudo -u alice -i fortify run capsh --has-no-new-privs"))
|
||||||
|
print(machine.fail("sudo -u alice -i fortify run capsh --has-a=CAP_SYS_ADMIN"))
|
||||||
|
print(machine.fail("sudo -u alice -i fortify run capsh --has-b=CAP_SYS_ADMIN"))
|
||||||
|
print(machine.fail("sudo -u alice -i fortify run capsh --has-i=CAP_SYS_ADMIN"))
|
||||||
|
print(machine.fail("sudo -u alice -i fortify run capsh --has-p=CAP_SYS_ADMIN"))
|
||||||
|
print(machine.fail("sudo -u alice -i fortify run umount -R /dev"))
|
||||||
|
|
||||||
# Verify PrintBaseError behaviour:
|
# Verify PrintBaseError behaviour:
|
||||||
if denyOutput != "fsu: uid 1001 is not in the fsurc file\n":
|
if denyOutput != "fsu: uid 1001 is not in the fsurc file\n":
|
||||||
raise Exception(f"unexpected deny output:\n{denyOutput}")
|
raise Exception(f"unexpected deny output:\n{denyOutput}")
|
||||||
if denyOutputVerbose != "fsu: uid 1001 is not in the fsurc file\nfortify: *cannot obtain uid from fsu: permission denied\n":
|
if denyOutputVerbose != "fsu: uid 1001 is not in the fsurc file\nfortify: *cannot obtain uid from fsu: permission denied\n":
|
||||||
raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}")
|
raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}")
|
||||||
|
|
||||||
|
# Check sandbox outcome:
|
||||||
check_offset = 0
|
check_offset = 0
|
||||||
|
def check_sandbox(name):
|
||||||
|
global check_offset
|
||||||
|
check_offset += 1
|
||||||
|
swaymsg(f"exec script /dev/null -E always -qec check-sandbox-{name}")
|
||||||
|
machine.wait_for_file(f"/tmp/fortify.1000/tmpdir/{check_offset}/sandbox-ok", timeout=15)
|
||||||
|
|
||||||
|
|
||||||
|
check_sandbox("preset")
|
||||||
|
check_sandbox("tty")
|
||||||
|
check_sandbox("mapuid")
|
||||||
|
|
||||||
def aid(offset):
|
def aid(offset):
|
||||||
return 1+check_offset+offset
|
return 1+check_offset+offset
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user