Compare commits
20 Commits
5c82f1ed3e
...
48f634d046
Author | SHA1 | Date | |
---|---|---|---|
48f634d046 | |||
2a46f5bb12 | |||
7f2c0af5ad | |||
297b444dfb | |||
89a05909a4 | |||
f772940768 | |||
8886c40974 | |||
8b62e08b44 | |||
72c59f9229 | |||
ff3cfbb437 | |||
c13eb70d7d | |||
389402f955 | |||
660a2898dc | |||
faf59e12c0 | |||
d97a03c7c6 | |||
a102178019 | |||
e400862a12 | |||
184e9db2b2 | |||
605d018be2 | |||
78aaae7ee0 |
@ -22,6 +22,57 @@ 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
|
||||||
@ -39,29 +90,14 @@ 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
|
||||||
- fpkg
|
|
||||||
- race
|
- race
|
||||||
|
- sandbox
|
||||||
|
- sandbox-race
|
||||||
|
- fpkg
|
||||||
runs-on: nix
|
runs-on: nix
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -71,7 +72,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, helper.ErrContainsNull)) != tc[0].wantErr {
|
if err := p.Seal(tc[0].c, tc[1].c); (errors.Is(err, syscall.EINVAL)) != 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,12 +58,19 @@
|
|||||||
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,6 +97,10 @@ 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,38 +1,17 @@
|
|||||||
package helper
|
package helper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
type argsWt [][]byte
|
||||||
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([]byte(arg + "\x00"))
|
n, err := w.Write(arg)
|
||||||
nt += n
|
nt += n
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -44,18 +23,32 @@ func (a argsWt) WriteTo(w io.Writer) (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a argsWt) String() string {
|
func (a argsWt) String() string {
|
||||||
return strings.Join(a, " ")
|
return string(
|
||||||
|
bytes.TrimSuffix(
|
||||||
|
bytes.ReplaceAll(
|
||||||
|
bytes.Join(a, nil),
|
||||||
|
[]byte{0}, []byte{' '},
|
||||||
|
),
|
||||||
|
[]byte{' '},
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCheckedArgs returns a checked argument writer for args.
|
// NewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
|
||||||
// Callers must not retain any references to args.
|
func NewCheckedArgs(args []string) (wt io.WriterTo, err error) {
|
||||||
func NewCheckedArgs(args []string) (io.WriterTo, error) {
|
a := make(argsWt, len(args))
|
||||||
a := argsWt(args)
|
for i, arg := range args {
|
||||||
return a, a.check()
|
a[i], err = syscall.ByteSliceFromString(arg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wt = a
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustNewCheckedArgs returns a checked argument writer for args and panics if check fails.
|
// MustNewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
|
||||||
// Callers must not retain any references to args.
|
// If s contains a NUL byte this function panics instead of returning an error.
|
||||||
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,34 +4,33 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper"
|
"git.gensokyo.uk/security/fortify/helper"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_argsFd_String(t *testing.T) {
|
func TestArgsString(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(): got %v; want %v",
|
t.Errorf("String: %q, want %q",
|
||||||
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, helper.ErrContainsNull) {
|
if _, err := helper.NewCheckedArgs(args); !errors.Is(err, syscall.EINVAL) {
|
||||||
t.Errorf("NewCheckedArgs(%q) error = %v, wantErr %v",
|
t.Errorf("NewCheckedArgs: error = %v, wantErr %v",
|
||||||
args,
|
err, syscall.EINVAL)
|
||||||
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 := "argument contains null character"
|
wantPanic := "invalid argument"
|
||||||
if r := recover(); r != wantPanic {
|
if r := recover(); r != wantPanic {
|
||||||
t.Errorf("MustNewCheckedArgs(%q) panic = %v, wantPanic %v",
|
t.Errorf("MustNewCheckedArgs: panic = %v, wantPanic %v",
|
||||||
badPayload,
|
|
||||||
r, wantPanic)
|
r, wantPanic)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"maps"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
@ -505,7 +504,13 @@ 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))
|
||||||
maps.All(seal.env)(func(k string, v string) bool { seal.container.Env = append(seal.container.Env, k+"="+v); return true })
|
for k, v := range seal.env {
|
||||||
|
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 [System.Paths].
|
// CopyPaths is a generic implementation of [fst.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()))
|
||||||
|
|
||||||
|
@ -201,9 +201,11 @@ in
|
|||||||
${copy "${pkg}/share/icons"}
|
${copy "${pkg}/share/icons"}
|
||||||
${copy "${pkg}/share/man"}
|
${copy "${pkg}/share/man"}
|
||||||
|
|
||||||
substituteInPlace $out/share/applications/* \
|
if test -d "$out/share/applications"; then
|
||||||
--replace-warn '${pkg}/bin/' "" \
|
substituteInPlace $out/share/applications/* \
|
||||||
--replace-warn '${pkg}/libexec/' ""
|
--replace-warn '${pkg}/bin/' "" \
|
||||||
|
--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.1> `
|
` <derivation fortify-static-x86_64-unknown-linux-musl-0.3.2> `
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -73,6 +73,25 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@ -486,6 +505,25 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@ -606,7 +644,7 @@ package
|
|||||||
|
|
||||||
|
|
||||||
*Default:*
|
*Default:*
|
||||||
` <derivation fortify-fsu-0.3.1> `
|
` <derivation fortify-fsu-0.3.2> `
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
buildGoModule rec {
|
buildGoModule rec {
|
||||||
pname = "fortify";
|
pname = "fortify";
|
||||||
version = "0.3.1";
|
version = "0.3.2";
|
||||||
|
|
||||||
src = builtins.path {
|
src = builtins.path {
|
||||||
name = "${pname}-src";
|
name = "${pname}-src";
|
||||||
|
@ -99,6 +99,8 @@ 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,17 +223,30 @@ 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, 0, 0}, {0, 0, 0}},
|
&[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.Fatalf("cannot capset: %v", err)
|
log.Fatalf("cannot capset: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -73,6 +73,16 @@ 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,3 +1,4 @@
|
|||||||
|
// Package seccomp provides filter presets and high level wrappers around libseccomp.
|
||||||
package seccomp
|
package seccomp
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -11,6 +11,7 @@ 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 (
|
||||||
@ -30,10 +31,9 @@ func SetDumpable(dumpable uintptr) error {
|
|||||||
const (
|
const (
|
||||||
_LINUX_CAPABILITY_VERSION_3 = 0x20080522
|
_LINUX_CAPABILITY_VERSION_3 = 0x20080522
|
||||||
|
|
||||||
PR_CAP_AMBIENT = 47
|
PR_CAP_AMBIENT = 0x2f
|
||||||
PR_CAP_AMBIENT_CLEAR_ALL = 4
|
PR_CAP_AMBIENT_RAISE = 0x2
|
||||||
|
PR_CAP_AMBIENT_CLEAR_ALL = 0x4
|
||||||
CAP_SETPCAP = 8
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@ -49,6 +49,12 @@ 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,17 +4,6 @@
|
|||||||
config,
|
config,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
|
||||||
testCases = import ./sandbox/case {
|
|
||||||
inherit (pkgs)
|
|
||||||
lib
|
|
||||||
callPackage
|
|
||||||
writeText
|
|
||||||
foot
|
|
||||||
;
|
|
||||||
inherit (config.environment.fortify.package) version;
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
users.users = {
|
users.users = {
|
||||||
alice = {
|
alice = {
|
||||||
@ -113,10 +102,6 @@ in
|
|||||||
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,10 +7,15 @@ 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 (
|
||||||
@ -116,7 +121,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 {
|
||||||
@ -124,6 +129,81 @@ 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)
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
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], "/tmp/sandbox-ok") }
|
|
||||||
''} main.go
|
|
||||||
'';
|
|
||||||
}
|
|
@ -1,11 +1,4 @@
|
|||||||
{
|
lib: testProgram:
|
||||||
lib,
|
|
||||||
callPackage,
|
|
||||||
writeText,
|
|
||||||
foot,
|
|
||||||
|
|
||||||
version,
|
|
||||||
}:
|
|
||||||
let
|
let
|
||||||
fs = mode: dir: data: {
|
fs = mode: dir: data: {
|
||||||
mode = lib.fromHexString mode;
|
mode = lib.fromHexString mode;
|
||||||
@ -30,8 +23,6 @@ let
|
|||||||
;
|
;
|
||||||
};
|
};
|
||||||
|
|
||||||
checkSandbox = callPackage ../assert.nix { inherit version; };
|
|
||||||
|
|
||||||
callTestCase =
|
callTestCase =
|
||||||
path:
|
path:
|
||||||
let
|
let
|
||||||
@ -47,12 +38,12 @@ let
|
|||||||
name = "check-sandbox-${tc.name}";
|
name = "check-sandbox-${tc.name}";
|
||||||
verbose = true;
|
verbose = true;
|
||||||
inherit (tc) tty mapRealUid;
|
inherit (tc) tty mapRealUid;
|
||||||
share = foot;
|
share = testProgram;
|
||||||
packages = [ ];
|
packages = [ ];
|
||||||
path = "${checkSandbox}/bin/test";
|
path = "${testProgram}/bin/fortify-test";
|
||||||
args = [
|
args = [
|
||||||
"test"
|
"test"
|
||||||
(toString (writeText "fortify-${tc.name}-want.json" (builtins.toJSON tc.want)))
|
(toString (builtins.toFile "fortify-${tc.name}-want.json" (builtins.toJSON tc.want)))
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
|
@ -97,7 +97,6 @@
|
|||||||
"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;
|
||||||
|
@ -97,7 +97,6 @@
|
|||||||
"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;
|
||||||
|
@ -98,7 +98,6 @@
|
|||||||
"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;
|
||||||
|
76
test/sandbox/configuration.nix
Normal file
76
test/sandbox/configuration.nix
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
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
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
39
test/sandbox/default.nix
Normal file
39
test/sandbox/default.nix
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
lib,
|
||||||
|
nixosTest,
|
||||||
|
|
||||||
|
self,
|
||||||
|
withRace ? false,
|
||||||
|
}:
|
||||||
|
|
||||||
|
nixosTest {
|
||||||
|
name = "fortify-sandbox" + (if withRace then "-race" else "");
|
||||||
|
nodes.machine =
|
||||||
|
{ options, pkgs, ... }:
|
||||||
|
{
|
||||||
|
# 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;
|
||||||
|
}
|
119
test/sandbox/ptrace.go
Normal file
119
test/sandbox/ptrace.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
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,9 +10,7 @@ import (
|
|||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
const NULL = 0
|
func trySyscalls() error {
|
||||||
|
|
||||||
func TrySyscalls() error {
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
errno syscall.Errno
|
errno syscall.Errno
|
||||||
|
71
test/sandbox/test.py
Normal file
71
test/sandbox/test.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
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"))
|
39
test/sandbox/tool/main.go
Normal file
39
test/sandbox/tool/main.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
30
test/sandbox/tool/package.nix
Normal file
30
test/sandbox/tool/package.nix
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
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,34 +99,15 @@ 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