Compare commits
11 Commits
7106b00968
...
21735a8abe
Author | SHA1 | Date | |
---|---|---|---|
21735a8abe | |||
34272672b1 | |||
7b96cd6ded | |||
163f15e93f | |||
016da20443 | |||
37780456a7 | |||
efacaa40fa | |||
ad6d0ee55f | |||
cf791469d8 | |||
be14421775 | |||
045983d7f4 |
@ -1,50 +0,0 @@
|
|||||||
name: Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
- push
|
|
||||||
- pull_request
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
dist:
|
|
||||||
name: Create distribution
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
|
|
||||||
- name: Install Nix
|
|
||||||
uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
|
|
||||||
with:
|
|
||||||
# explicitly enable sandbox
|
|
||||||
install_options: --daemon
|
|
||||||
extra_nix_config: |
|
|
||||||
sandbox = true
|
|
||||||
system-features = nixos-test benchmark big-parallel kvm
|
|
||||||
enable_kvm: true
|
|
||||||
|
|
||||||
- name: Ensure environment
|
|
||||||
run: >-
|
|
||||||
apt-get update && apt-get install -y sqlite3
|
|
||||||
if: ${{ runner.os == 'Linux' }}
|
|
||||||
|
|
||||||
- name: Restore Nix store
|
|
||||||
uses: nix-community/cache-nix-action@v5
|
|
||||||
with:
|
|
||||||
primary-key: nix-small-${{ runner.os }}-${{ hashFiles('**/*.nix') }}
|
|
||||||
restore-prefixes-first-match: nix-small-${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Build for test
|
|
||||||
id: build-test
|
|
||||||
run: >-
|
|
||||||
export FORTIFY_REV="$(git rev-parse --short HEAD)" &&
|
|
||||||
sed -i.old 's/version = /version = "0.0.0-'$FORTIFY_REV'"; # version = /' package.nix &&
|
|
||||||
nix build --print-out-paths --print-build-logs .#dist &&
|
|
||||||
mv package.nix.old package.nix &&
|
|
||||||
echo "rev=$FORTIFY_REV" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Upload test build
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: "fortify-${{ steps.build-test.outputs.rev }}"
|
|
||||||
path: result/*
|
|
||||||
retention-days: 1
|
|
@ -5,7 +5,7 @@ on:
|
|||||||
- pull_request
|
- pull_request
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
test:
|
||||||
name: Run NixOS test
|
name: Run NixOS test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@ -30,8 +30,13 @@ jobs:
|
|||||||
- name: Restore Nix store
|
- name: Restore Nix store
|
||||||
uses: nix-community/cache-nix-action@v5
|
uses: nix-community/cache-nix-action@v5
|
||||||
with:
|
with:
|
||||||
primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix') }}
|
primary-key: flake-check-${{ runner.os }}-${{ hashFiles('**/*.nix') }}
|
||||||
restore-prefixes-first-match: nix-${{ runner.os }}-
|
restore-prefixes-first-match: flake-check-${{ runner.os }}-
|
||||||
|
gc-max-store-size-linux: 1073741824
|
||||||
|
purge: true
|
||||||
|
purge-prefixes: flake-check-${{ runner.os }}-
|
||||||
|
purge-created: 60
|
||||||
|
purge-primary-key: never
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
@ -41,6 +46,55 @@ jobs:
|
|||||||
- name: Upload test output
|
- name: Upload test output
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: "result"
|
name: "nixos-vm-output"
|
||||||
|
path: result/*
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
dist:
|
||||||
|
name: Create distribution
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
|
- name: Install Nix
|
||||||
|
uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
|
||||||
|
with:
|
||||||
|
# explicitly enable sandbox
|
||||||
|
install_options: --daemon
|
||||||
|
extra_nix_config: |
|
||||||
|
sandbox = true
|
||||||
|
system-features = nixos-test benchmark big-parallel kvm
|
||||||
|
enable_kvm: true
|
||||||
|
|
||||||
|
- name: Ensure environment
|
||||||
|
run: >-
|
||||||
|
apt-get update && apt-get install -y sqlite3
|
||||||
|
if: ${{ runner.os == 'Linux' }}
|
||||||
|
|
||||||
|
- name: Restore Nix store
|
||||||
|
uses: nix-community/cache-nix-action@v5
|
||||||
|
with:
|
||||||
|
primary-key: build-dist-${{ runner.os }}-${{ hashFiles('**/*.nix') }}
|
||||||
|
restore-prefixes-first-match: build-dist-${{ runner.os }}-
|
||||||
|
gc-max-store-size-linux: 1073741824
|
||||||
|
purge: true
|
||||||
|
purge-prefixes: build-dist-${{ runner.os }}-
|
||||||
|
purge-created: 60
|
||||||
|
purge-primary-key: never
|
||||||
|
|
||||||
|
- name: Build for test
|
||||||
|
id: build-test
|
||||||
|
run: >-
|
||||||
|
export FORTIFY_REV="$(git rev-parse --short HEAD)" &&
|
||||||
|
sed -i.old 's/version = /version = "0.0.0-'$FORTIFY_REV'"; # version = /' package.nix &&
|
||||||
|
nix build --print-out-paths --print-build-logs .#dist &&
|
||||||
|
mv package.nix.old package.nix &&
|
||||||
|
echo "rev=$FORTIFY_REV" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Upload test build
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: "fortify-${{ steps.build-test.outputs.rev }}"
|
||||||
path: result/*
|
path: result/*
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
@ -1,95 +0,0 @@
|
|||||||
package bwrap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SyscallPolicy struct {
|
|
||||||
DenyDevel bool `json:"deny_devel"`
|
|
||||||
Multiarch bool `json:"multiarch"`
|
|
||||||
Linux32 bool `json:"linux32"`
|
|
||||||
Can bool `json:"can"`
|
|
||||||
Bluetooth bool `json:"bluetooth"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type seccompBuilder struct {
|
|
||||||
config *Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *seccompBuilder) Len() int {
|
|
||||||
if s == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *seccompBuilder) Append(args *[]string, extraFiles *[]*os.File) error {
|
|
||||||
if s == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if f, err := s.config.resolveSeccomp(); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
extraFile(args, extraFiles, positionalArgs[Seccomp], f)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) resolveSeccomp() (*os.File, error) {
|
|
||||||
if c.Syscall == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolve seccomp filter opts
|
|
||||||
var (
|
|
||||||
opts syscallOpts
|
|
||||||
optd []string
|
|
||||||
optCond = [...]struct {
|
|
||||||
v bool
|
|
||||||
o syscallOpts
|
|
||||||
d string
|
|
||||||
}{
|
|
||||||
{!c.UserNS, flagDenyNS, "denyns"},
|
|
||||||
{c.NewSession, flagDenyTTY, "denytty"},
|
|
||||||
{c.Syscall.DenyDevel, flagDenyDevel, "denydevel"},
|
|
||||||
{c.Syscall.Multiarch, flagMultiarch, "multiarch"},
|
|
||||||
{c.Syscall.Linux32, flagLinux32, "linux32"},
|
|
||||||
{c.Syscall.Can, flagCan, "can"},
|
|
||||||
{c.Syscall.Bluetooth, flagBluetooth, "bluetooth"},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if CPrintln != nil {
|
|
||||||
optd = make([]string, 1, len(optCond)+1)
|
|
||||||
optd[0] = "common"
|
|
||||||
}
|
|
||||||
for _, opt := range optCond {
|
|
||||||
if opt.v {
|
|
||||||
opts |= opt.o
|
|
||||||
if fmsg.Verbose() {
|
|
||||||
optd = append(optd, opt.d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if CPrintln != nil {
|
|
||||||
CPrintln(fmt.Sprintf("seccomp flags: %s", optd))
|
|
||||||
}
|
|
||||||
|
|
||||||
// export seccomp filter to tmpfile
|
|
||||||
if f, err := tmpfile(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return f, exportAndSeek(f, opts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func exportAndSeek(f *os.File, opts syscallOpts) error {
|
|
||||||
if err := exportFilter(f.Fd(), opts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err := f.Seek(0, io.SeekStart)
|
|
||||||
return err
|
|
||||||
}
|
|
@ -1,83 +1,90 @@
|
|||||||
package bwrap
|
package bwrap
|
||||||
|
|
||||||
/*
|
|
||||||
#cgo linux pkg-config: --static libseccomp
|
|
||||||
|
|
||||||
#include "seccomp-export.h"
|
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
|
||||||
|
"git.gensokyo.uk/security/fortify/helper/seccomp"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
)
|
)
|
||||||
|
|
||||||
var CPrintln func(v ...any)
|
type SyscallPolicy struct {
|
||||||
|
// disable fortify extensions
|
||||||
var resErr = [...]error{
|
Compat bool `json:"compat"`
|
||||||
0: nil,
|
// deny development syscalls
|
||||||
1: errors.New("seccomp_init failed"),
|
DenyDevel bool `json:"deny_devel"`
|
||||||
2: errors.New("seccomp_arch_add failed"),
|
// deny multiarch/emulation syscalls
|
||||||
3: errors.New("seccomp_arch_add failed (multiarch)"),
|
Multiarch bool `json:"multiarch"`
|
||||||
4: errors.New("internal libseccomp failure"),
|
// allow PER_LINUX32
|
||||||
5: errors.New("seccomp_rule_add failed"),
|
Linux32 bool `json:"linux32"`
|
||||||
6: errors.New("seccomp_export_bpf failed"),
|
// allow AF_CAN
|
||||||
|
Can bool `json:"can"`
|
||||||
|
// allow AF_BLUETOOTH
|
||||||
|
Bluetooth bool `json:"bluetooth"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type seccompBuilder struct {
|
||||||
syscallOpts = C.f_syscall_opts
|
config *Config
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
flagDenyNS syscallOpts = C.F_DENY_NS
|
|
||||||
flagDenyTTY syscallOpts = C.F_DENY_TTY
|
|
||||||
flagDenyDevel syscallOpts = C.F_DENY_DEVEL
|
|
||||||
flagMultiarch syscallOpts = C.F_MULTIARCH
|
|
||||||
flagLinux32 syscallOpts = C.F_LINUX32
|
|
||||||
flagCan syscallOpts = C.F_CAN
|
|
||||||
flagBluetooth syscallOpts = C.F_BLUETOOTH
|
|
||||||
)
|
|
||||||
|
|
||||||
func tmpfile() (*os.File, error) {
|
|
||||||
fd, err := C.f_tmpfile_fd()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return os.NewFile(uintptr(fd), "tmpfile"), err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func exportFilter(fd uintptr, opts syscallOpts) error {
|
func (s *seccompBuilder) Len() int {
|
||||||
var (
|
if s == nil {
|
||||||
arch C.uint32_t = 0
|
return 0
|
||||||
multiarch C.uint32_t = 0
|
}
|
||||||
)
|
return 2
|
||||||
switch runtime.GOARCH {
|
|
||||||
case "386":
|
|
||||||
arch = C.SCMP_ARCH_X86
|
|
||||||
case "amd64":
|
|
||||||
arch = C.SCMP_ARCH_X86_64
|
|
||||||
multiarch = C.SCMP_ARCH_X86
|
|
||||||
case "arm":
|
|
||||||
arch = C.SCMP_ARCH_ARM
|
|
||||||
case "arm64":
|
|
||||||
arch = C.SCMP_ARCH_AARCH64
|
|
||||||
multiarch = C.SCMP_ARCH_ARM
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := C.f_export_bpf(C.int(fd), arch, multiarch, opts)
|
func (s *seccompBuilder) Append(args *[]string, extraFiles *[]*os.File) error {
|
||||||
if re := resErr[res]; re != nil {
|
if s == nil {
|
||||||
if err == nil {
|
return nil
|
||||||
return re
|
|
||||||
}
|
|
||||||
return fmt.Errorf("%s: %v", re.Error(), err)
|
|
||||||
}
|
}
|
||||||
|
if f, err := s.config.resolveSeccomp(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
} else {
|
||||||
|
extraFile(args, extraFiles, positionalArgs[Seccomp], f)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//export F_println
|
func (c *Config) resolveSeccomp() (*os.File, error) {
|
||||||
func F_println(v *C.char) {
|
if c.Syscall == nil {
|
||||||
if CPrintln != nil {
|
return nil, nil
|
||||||
CPrintln(C.GoString(v))
|
}
|
||||||
|
|
||||||
|
// resolve seccomp filter opts
|
||||||
|
var (
|
||||||
|
opts seccomp.SyscallOpts
|
||||||
|
optd []string
|
||||||
|
optCond = [...]struct {
|
||||||
|
v bool
|
||||||
|
o seccomp.SyscallOpts
|
||||||
|
d string
|
||||||
|
}{
|
||||||
|
{!c.Syscall.Compat, seccomp.FlagExt, "fortify"},
|
||||||
|
{!c.UserNS, seccomp.FlagDenyNS, "denyns"},
|
||||||
|
{c.NewSession, seccomp.FlagDenyTTY, "denytty"},
|
||||||
|
{c.Syscall.DenyDevel, seccomp.FlagDenyDevel, "denydevel"},
|
||||||
|
{c.Syscall.Multiarch, seccomp.FlagMultiarch, "multiarch"},
|
||||||
|
{c.Syscall.Linux32, seccomp.FlagLinux32, "linux32"},
|
||||||
|
{c.Syscall.Can, seccomp.FlagCan, "can"},
|
||||||
|
{c.Syscall.Bluetooth, seccomp.FlagBluetooth, "bluetooth"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if seccomp.CPrintln != nil {
|
||||||
|
optd = make([]string, 1, len(optCond)+1)
|
||||||
|
optd[0] = "common"
|
||||||
|
}
|
||||||
|
for _, opt := range optCond {
|
||||||
|
if opt.v {
|
||||||
|
opts |= opt.o
|
||||||
|
if fmsg.Verbose() {
|
||||||
|
optd = append(optd, opt.d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if seccomp.CPrintln != nil {
|
||||||
|
seccomp.CPrintln(fmt.Sprintf("seccomp flags: %s", optd))
|
||||||
|
}
|
||||||
|
|
||||||
|
return seccomp.Export(opts)
|
||||||
|
}
|
||||||
|
17
helper/seccomp/export.go
Normal file
17
helper/seccomp/export.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package seccomp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Export(opts SyscallOpts) (f *os.File, err error) {
|
||||||
|
if f, err = tmpfile(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = exportFilter(f.Fd(), opts); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = f.Seek(0, io.SeekStart)
|
||||||
|
return
|
||||||
|
}
|
@ -28,7 +28,7 @@ struct f_syscall_act {
|
|||||||
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
|
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
|
||||||
|
|
||||||
#define SECCOMP_RULESET_ADD(ruleset) do { \
|
#define SECCOMP_RULESET_ADD(ruleset) do { \
|
||||||
F_println("adding seccomp ruleset \"" #ruleset "\""); \
|
if (opts & F_VERBOSE) F_println("adding seccomp ruleset \"" #ruleset "\""); \
|
||||||
for (int i = 0; i < LEN(ruleset); i++) { \
|
for (int i = 0; i < LEN(ruleset); i++) { \
|
||||||
assert(ruleset[i].m_errno == EPERM || ruleset[i].m_errno == ENOSYS); \
|
assert(ruleset[i].m_errno == EPERM || ruleset[i].m_errno == ENOSYS); \
|
||||||
\
|
\
|
||||||
@ -89,6 +89,31 @@ int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts o
|
|||||||
{SCMP_SYS(migrate_pages), EPERM},
|
{SCMP_SYS(migrate_pages), EPERM},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// fortify: project-specific extensions
|
||||||
|
struct f_syscall_act deny_common_ext[] = {
|
||||||
|
// system calls for changing the system clock
|
||||||
|
{SCMP_SYS(adjtimex), EPERM},
|
||||||
|
{SCMP_SYS(clock_adjtime), EPERM},
|
||||||
|
{SCMP_SYS(clock_adjtime64), EPERM},
|
||||||
|
{SCMP_SYS(clock_settime), EPERM},
|
||||||
|
{SCMP_SYS(clock_settime64), EPERM},
|
||||||
|
{SCMP_SYS(settimeofday), EPERM},
|
||||||
|
|
||||||
|
// loading and unloading of kernel modules
|
||||||
|
{SCMP_SYS(delete_module), EPERM},
|
||||||
|
{SCMP_SYS(finit_module), EPERM},
|
||||||
|
{SCMP_SYS(init_module), EPERM},
|
||||||
|
|
||||||
|
// system calls for rebooting and reboot preparation
|
||||||
|
{SCMP_SYS(kexec_file_load), EPERM},
|
||||||
|
{SCMP_SYS(kexec_load), EPERM},
|
||||||
|
{SCMP_SYS(reboot), EPERM},
|
||||||
|
|
||||||
|
// system calls for enabling/disabling swap devices
|
||||||
|
{SCMP_SYS(swapoff), EPERM},
|
||||||
|
{SCMP_SYS(swapon), EPERM},
|
||||||
|
};
|
||||||
|
|
||||||
struct f_syscall_act deny_ns[] = {
|
struct f_syscall_act deny_ns[] = {
|
||||||
// Don't allow subnamespace setups:
|
// Don't allow subnamespace setups:
|
||||||
{SCMP_SYS(unshare), EPERM},
|
{SCMP_SYS(unshare), EPERM},
|
||||||
@ -126,6 +151,34 @@ int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts o
|
|||||||
{SCMP_SYS(mount_setattr), ENOSYS},
|
{SCMP_SYS(mount_setattr), ENOSYS},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// fortify: project-specific extensions
|
||||||
|
struct f_syscall_act deny_ns_ext[] = {
|
||||||
|
// changing file ownership
|
||||||
|
{SCMP_SYS(chown), EPERM},
|
||||||
|
{SCMP_SYS(chown32), EPERM},
|
||||||
|
{SCMP_SYS(fchown), EPERM},
|
||||||
|
{SCMP_SYS(fchown32), EPERM},
|
||||||
|
{SCMP_SYS(fchownat), EPERM},
|
||||||
|
{SCMP_SYS(lchown), EPERM},
|
||||||
|
{SCMP_SYS(lchown32), EPERM},
|
||||||
|
|
||||||
|
// system calls for changing user ID and group ID credentials
|
||||||
|
{SCMP_SYS(setgid), EPERM},
|
||||||
|
{SCMP_SYS(setgid32), EPERM},
|
||||||
|
{SCMP_SYS(setgroups), EPERM},
|
||||||
|
{SCMP_SYS(setgroups32), EPERM},
|
||||||
|
{SCMP_SYS(setregid), EPERM},
|
||||||
|
{SCMP_SYS(setregid32), EPERM},
|
||||||
|
{SCMP_SYS(setresgid), EPERM},
|
||||||
|
{SCMP_SYS(setresgid32), EPERM},
|
||||||
|
{SCMP_SYS(setresuid), EPERM},
|
||||||
|
{SCMP_SYS(setresuid32), EPERM},
|
||||||
|
{SCMP_SYS(setreuid), EPERM},
|
||||||
|
{SCMP_SYS(setreuid32), EPERM},
|
||||||
|
{SCMP_SYS(setuid), EPERM},
|
||||||
|
{SCMP_SYS(setuid32), EPERM},
|
||||||
|
};
|
||||||
|
|
||||||
struct f_syscall_act deny_tty[] = {
|
struct f_syscall_act deny_tty[] = {
|
||||||
// Don't allow faking input to the controlling tty (CVE-2017-5226)
|
// Don't allow faking input to the controlling tty (CVE-2017-5226)
|
||||||
{SCMP_SYS(ioctl), EPERM, &SCMP_A1(SCMP_CMP_MASKED_EQ, 0xFFFFFFFFu, (int)TIOCSTI)},
|
{SCMP_SYS(ioctl), EPERM, &SCMP_A1(SCMP_CMP_MASKED_EQ, 0xFFFFFFFFu, (int)TIOCSTI)},
|
||||||
@ -145,6 +198,22 @@ int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts o
|
|||||||
{SCMP_SYS(ptrace), EPERM}
|
{SCMP_SYS(ptrace), EPERM}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct f_syscall_act deny_emu[] = {
|
||||||
|
// modify_ldt is a historic source of interesting information leaks,
|
||||||
|
// so it's disabled as a hardening measure.
|
||||||
|
// However, it is required to run old 16-bit applications
|
||||||
|
// as well as some Wine patches, so it's allowed in multiarch.
|
||||||
|
{SCMP_SYS(modify_ldt), EPERM},
|
||||||
|
};
|
||||||
|
|
||||||
|
// fortify: project-specific extensions
|
||||||
|
struct f_syscall_act deny_emu_ext[] = {
|
||||||
|
{SCMP_SYS(subpage_prot), ENOSYS},
|
||||||
|
{SCMP_SYS(switch_endian), ENOSYS},
|
||||||
|
{SCMP_SYS(vm86), ENOSYS},
|
||||||
|
{SCMP_SYS(vm86old), ENOSYS},
|
||||||
|
};
|
||||||
|
|
||||||
// Blocklist all but unix, inet, inet6 and netlink
|
// Blocklist all but unix, inet, inet6 and netlink
|
||||||
struct
|
struct
|
||||||
{
|
{
|
||||||
@ -199,26 +268,11 @@ int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts o
|
|||||||
if (opts & F_DENY_NS) SECCOMP_RULESET_ADD(deny_ns);
|
if (opts & F_DENY_NS) SECCOMP_RULESET_ADD(deny_ns);
|
||||||
if (opts & F_DENY_TTY) SECCOMP_RULESET_ADD(deny_tty);
|
if (opts & F_DENY_TTY) SECCOMP_RULESET_ADD(deny_tty);
|
||||||
if (opts & F_DENY_DEVEL) SECCOMP_RULESET_ADD(deny_devel);
|
if (opts & F_DENY_DEVEL) SECCOMP_RULESET_ADD(deny_devel);
|
||||||
|
if (!allow_multiarch) SECCOMP_RULESET_ADD(deny_emu);
|
||||||
if (!allow_multiarch) {
|
if (opts & F_EXT) {
|
||||||
F_println("disabling modify_ldt");
|
SECCOMP_RULESET_ADD(deny_common_ext);
|
||||||
|
if (opts & F_DENY_NS) SECCOMP_RULESET_ADD(deny_ns_ext);
|
||||||
// modify_ldt is a historic source of interesting information leaks,
|
if (!allow_multiarch) SECCOMP_RULESET_ADD(deny_emu_ext);
|
||||||
// so it's disabled as a hardening measure.
|
|
||||||
// However, it is required to run old 16-bit applications
|
|
||||||
// as well as some Wine patches, so it's allowed in multiarch.
|
|
||||||
ret = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(modify_ldt), 0);
|
|
||||||
|
|
||||||
// See above for the meaning of EFAULT.
|
|
||||||
if (ret == -EFAULT) {
|
|
||||||
// call fmsg here?
|
|
||||||
res = 4;
|
|
||||||
goto out;
|
|
||||||
} else if (ret < 0) {
|
|
||||||
res = 5;
|
|
||||||
errno = -ret;
|
|
||||||
goto out;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Socket filtering doesn't work on e.g. i386, so ignore failures here
|
// Socket filtering doesn't work on e.g. i386, so ignore failures here
|
@ -8,13 +8,15 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
F_DENY_NS = 1 << 0,
|
F_VERBOSE = 1 << 0,
|
||||||
F_DENY_TTY = 1 << 1,
|
F_EXT = 1 << 1,
|
||||||
F_DENY_DEVEL = 1 << 2,
|
F_DENY_NS = 1 << 2,
|
||||||
F_MULTIARCH = 1 << 3,
|
F_DENY_TTY = 1 << 3,
|
||||||
F_LINUX32 = 1 << 4,
|
F_DENY_DEVEL = 1 << 4,
|
||||||
F_CAN = 1 << 5,
|
F_MULTIARCH = 1 << 5,
|
||||||
F_BLUETOOTH = 1 << 6,
|
F_LINUX32 = 1 << 6,
|
||||||
|
F_CAN = 1 << 7,
|
||||||
|
F_BLUETOOTH = 1 << 8,
|
||||||
} f_syscall_opts;
|
} f_syscall_opts;
|
||||||
|
|
||||||
extern void F_println(char *v);
|
extern void F_println(char *v);
|
89
helper/seccomp/seccomp.go
Normal file
89
helper/seccomp/seccomp.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package seccomp
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo linux pkg-config: --static libseccomp
|
||||||
|
|
||||||
|
#include "seccomp-export.h"
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
var CPrintln func(v ...any)
|
||||||
|
|
||||||
|
var resErr = [...]error{
|
||||||
|
0: nil,
|
||||||
|
1: errors.New("seccomp_init failed"),
|
||||||
|
2: errors.New("seccomp_arch_add failed"),
|
||||||
|
3: errors.New("seccomp_arch_add failed (multiarch)"),
|
||||||
|
4: errors.New("internal libseccomp failure"),
|
||||||
|
5: errors.New("seccomp_rule_add failed"),
|
||||||
|
6: errors.New("seccomp_export_bpf failed"),
|
||||||
|
}
|
||||||
|
|
||||||
|
type SyscallOpts = C.f_syscall_opts
|
||||||
|
|
||||||
|
const (
|
||||||
|
flagVerbose SyscallOpts = C.F_VERBOSE
|
||||||
|
FlagExt SyscallOpts = C.F_EXT
|
||||||
|
FlagDenyNS SyscallOpts = C.F_DENY_NS
|
||||||
|
FlagDenyTTY SyscallOpts = C.F_DENY_TTY
|
||||||
|
FlagDenyDevel SyscallOpts = C.F_DENY_DEVEL
|
||||||
|
FlagMultiarch SyscallOpts = C.F_MULTIARCH
|
||||||
|
FlagLinux32 SyscallOpts = C.F_LINUX32
|
||||||
|
FlagCan SyscallOpts = C.F_CAN
|
||||||
|
FlagBluetooth SyscallOpts = C.F_BLUETOOTH
|
||||||
|
)
|
||||||
|
|
||||||
|
func tmpfile() (*os.File, error) {
|
||||||
|
fd, err := C.f_tmpfile_fd()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return os.NewFile(uintptr(fd), "tmpfile"), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func exportFilter(fd uintptr, opts SyscallOpts) error {
|
||||||
|
var (
|
||||||
|
arch C.uint32_t = 0
|
||||||
|
multiarch C.uint32_t = 0
|
||||||
|
)
|
||||||
|
switch runtime.GOARCH {
|
||||||
|
case "386":
|
||||||
|
arch = C.SCMP_ARCH_X86
|
||||||
|
case "amd64":
|
||||||
|
arch = C.SCMP_ARCH_X86_64
|
||||||
|
multiarch = C.SCMP_ARCH_X86
|
||||||
|
case "arm":
|
||||||
|
arch = C.SCMP_ARCH_ARM
|
||||||
|
case "arm64":
|
||||||
|
arch = C.SCMP_ARCH_AARCH64
|
||||||
|
multiarch = C.SCMP_ARCH_ARM
|
||||||
|
}
|
||||||
|
|
||||||
|
// this removes repeated transitions between C and Go execution
|
||||||
|
// when producing log output via F_println and CPrintln is nil
|
||||||
|
if CPrintln != nil {
|
||||||
|
opts |= flagVerbose
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := C.f_export_bpf(C.int(fd), arch, multiarch, opts)
|
||||||
|
if re := resErr[res]; re != nil {
|
||||||
|
if err == nil {
|
||||||
|
return re
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s: %v", re.Error(), err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
//export F_println
|
||||||
|
func F_println(v *C.char) {
|
||||||
|
if CPrintln != nil {
|
||||||
|
CPrintln(C.GoString(v))
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
"git.gensokyo.uk/security/fortify/helper"
|
"git.gensokyo.uk/security/fortify/helper"
|
||||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
"git.gensokyo.uk/security/fortify/helper/seccomp"
|
||||||
"git.gensokyo.uk/security/fortify/internal"
|
"git.gensokyo.uk/security/fortify/internal"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
"git.gensokyo.uk/security/fortify/internal/proc"
|
"git.gensokyo.uk/security/fortify/internal/proc"
|
||||||
@ -128,7 +128,7 @@ func Main() {
|
|||||||
|
|
||||||
helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
|
helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
|
||||||
if fmsg.Verbose() {
|
if fmsg.Verbose() {
|
||||||
bwrap.CPrintln = fmsg.Println
|
seccomp.CPrintln = fmsg.Println
|
||||||
}
|
}
|
||||||
if b, err := helper.NewBwrap(
|
if b, err := helper.NewBwrap(
|
||||||
conf, innerInit,
|
conf, innerInit,
|
||||||
|
4
main.go
4
main.go
@ -16,7 +16,7 @@ import (
|
|||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/dbus"
|
"git.gensokyo.uk/security/fortify/dbus"
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
"git.gensokyo.uk/security/fortify/helper/seccomp"
|
||||||
"git.gensokyo.uk/security/fortify/internal"
|
"git.gensokyo.uk/security/fortify/internal"
|
||||||
"git.gensokyo.uk/security/fortify/internal/app"
|
"git.gensokyo.uk/security/fortify/internal/app"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
@ -310,7 +310,7 @@ func runApp(config *fst.Config) {
|
|||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
if fmsg.Verbose() {
|
if fmsg.Verbose() {
|
||||||
bwrap.CPrintln = fmsg.Println
|
seccomp.CPrintln = fmsg.Println
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle signals for graceful shutdown
|
// handle signals for graceful shutdown
|
||||||
|
@ -118,7 +118,8 @@ in
|
|||||||
env
|
env
|
||||||
;
|
;
|
||||||
syscall = {
|
syscall = {
|
||||||
inherit (app) devel multiarch bluetooth;
|
inherit (app) compat multiarch bluetooth;
|
||||||
|
deny_devel = !app.devel;
|
||||||
};
|
};
|
||||||
map_real_uid = app.mapRealUid;
|
map_real_uid = app.mapRealUid;
|
||||||
no_new_session = app.tty;
|
no_new_session = app.tty;
|
||||||
|
26
options.md
26
options.md
@ -36,7 +36,7 @@ package
|
|||||||
|
|
||||||
|
|
||||||
*Default:*
|
*Default:*
|
||||||
` <derivation fortify-0.2.11> `
|
` <derivation fortify-0.2.12> `
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -198,6 +198,30 @@ null or string
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## environment\.fortify\.apps\.\*\.compat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Whether to enable disable syscall filter extensions\.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
*Type:*
|
||||||
|
boolean
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
*Default:*
|
||||||
|
` false `
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
*Example:*
|
||||||
|
` true `
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## environment\.fortify\.apps\.\*\.dbus\.session
|
## environment\.fortify\.apps\.\*\.dbus\.session
|
||||||
|
|
||||||
|
|
||||||
|
@ -151,6 +151,7 @@ in
|
|||||||
default = true;
|
default = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
compat = mkEnableOption "disable syscall filter extensions";
|
||||||
devel = mkEnableOption "development kernel APIs";
|
devel = mkEnableOption "development kernel APIs";
|
||||||
multiarch = mkEnableOption "multiarch kernel support";
|
multiarch = mkEnableOption "multiarch kernel support";
|
||||||
bluetooth = mkEnableOption "AF_BLUETOOTH socket operations";
|
bluetooth = mkEnableOption "AF_BLUETOOTH socket operations";
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
buildGoModule rec {
|
buildGoModule rec {
|
||||||
pname = "fortify";
|
pname = "fortify";
|
||||||
version = "0.2.11";
|
version = "0.2.12";
|
||||||
|
|
||||||
src = builtins.path {
|
src = builtins.path {
|
||||||
name = "fortify-src";
|
name = "fortify-src";
|
||||||
@ -63,7 +63,7 @@ buildGoModule rec {
|
|||||||
makeBinaryWrapper
|
makeBinaryWrapper
|
||||||
];
|
];
|
||||||
|
|
||||||
preConfigure = ''
|
preBuild = ''
|
||||||
HOME=$(mktemp -d) go generate ./...
|
HOME=$(mktemp -d) go generate ./...
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
9
test.nix
9
test.nix
@ -237,8 +237,8 @@ nixosTest {
|
|||||||
machine.succeed("rm -rf /tmp/src && cp -a '${self.packages.${system}.fortify.src}' /tmp/src")
|
machine.succeed("rm -rf /tmp/src && cp -a '${self.packages.${system}.fortify.src}' /tmp/src")
|
||||||
machine.succeed("fortify-fhs -c '(cd /tmp/src && go generate ./... && go test ./... && touch /tmp/success-gotest)' &> /tmp/gotest &")
|
machine.succeed("fortify-fhs -c '(cd /tmp/src && go generate ./... && go test ./... && touch /tmp/success-gotest)' &> /tmp/gotest &")
|
||||||
|
|
||||||
# To check sway's version:
|
# To check fortify's version:
|
||||||
print(machine.succeed("sway --version"))
|
print(machine.succeed("sudo -u alice -i fortify version"))
|
||||||
|
|
||||||
# Wait for Sway to complete startup:
|
# Wait for Sway to complete startup:
|
||||||
machine.wait_for_file("/run/user/1000/wayland-1")
|
machine.wait_for_file("/run/user/1000/wayland-1")
|
||||||
@ -254,6 +254,11 @@ nixosTest {
|
|||||||
print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare"))
|
print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare"))
|
||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-bare")
|
machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-bare")
|
||||||
|
|
||||||
|
# Verify silent output permissive defaults:
|
||||||
|
output = machine.succeed("sudo -u alice -i fortify run -a 0 true &>/dev/stdout")
|
||||||
|
if output != "":
|
||||||
|
raise Exception(f"unexpected output\n{output}")
|
||||||
|
|
||||||
# Start fortify permissive defaults within Wayland session:
|
# Start fortify permissive defaults within Wayland session:
|
||||||
fortify('-v run --wayland --dbus notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-done')
|
fortify('-v run --wayland --dbus notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-done')
|
||||||
machine.wait_for_file("/tmp/dbus-done")
|
machine.wait_for_file("/tmp/dbus-done")
|
||||||
|
@ -1,17 +1,7 @@
|
|||||||
package wl
|
#include "wayland-bind.h"
|
||||||
|
|
||||||
//go:generate sh -c "wayland-scanner client-header `pkg-config --variable=datarootdir wayland-protocols`/wayland-protocols/staging/security-context/security-context-v1.xml security-context-v1-protocol.h"
|
|
||||||
//go:generate sh -c "wayland-scanner private-code `pkg-config --variable=datarootdir wayland-protocols`/wayland-protocols/staging/security-context/security-context-v1.xml security-context-v1-protocol.c"
|
|
||||||
|
|
||||||
/*
|
|
||||||
#cgo linux pkg-config: --static wayland-client
|
|
||||||
#cgo freebsd openbsd LDFLAGS: -lwayland-client
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
#include <sys/un.h>
|
#include <sys/un.h>
|
||||||
@ -33,7 +23,7 @@ static const struct wl_registry_listener registry_listener = {
|
|||||||
.global_remove = registry_handle_global_remove,
|
.global_remove = registry_handle_global_remove,
|
||||||
};
|
};
|
||||||
|
|
||||||
static int32_t bind_wayland_fd(char *socket_path, int fd, const char *app_id, const char *instance_id, int sync_fd) {
|
int32_t bind_wayland_fd(char *socket_path, int fd, const char *app_id, const char *instance_id, int sync_fd) {
|
||||||
int32_t res = 0; // refer to resErr for meaning
|
int32_t res = 0; // refer to resErr for meaning
|
||||||
|
|
||||||
struct wl_display *display;
|
struct wl_display *display;
|
||||||
@ -96,17 +86,3 @@ out:
|
|||||||
free((void *)instance_id);
|
free((void *)instance_id);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var resErr = [...]error{
|
|
||||||
0: nil,
|
|
||||||
1: errors.New("wl_display_connect_to_fd() failed"),
|
|
||||||
2: errors.New("wp_security_context_v1 not available"),
|
|
||||||
}
|
|
||||||
|
|
||||||
func bindWaylandFd(socketPath string, fd uintptr, appID, instanceID string, syncFD uintptr) error {
|
|
||||||
res := C.bind_wayland_fd(C.CString(socketPath), C.int(fd), C.CString(appID), C.CString(instanceID), C.int(syncFD))
|
|
||||||
return resErr[int32(res)]
|
|
||||||
}
|
|
3
wl/wayland-bind.h
Normal file
3
wl/wayland-bind.h
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
int32_t bind_wayland_fd(char *socket_path, int fd, const char *app_id, const char *instance_id, int sync_fd);
|
24
wl/wl.go
Normal file
24
wl/wl.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package wl
|
||||||
|
|
||||||
|
//go:generate sh -c "wayland-scanner client-header `pkg-config --variable=datarootdir wayland-protocols`/wayland-protocols/staging/security-context/security-context-v1.xml security-context-v1-protocol.h"
|
||||||
|
//go:generate sh -c "wayland-scanner private-code `pkg-config --variable=datarootdir wayland-protocols`/wayland-protocols/staging/security-context/security-context-v1.xml security-context-v1-protocol.c"
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo linux pkg-config: --static wayland-client
|
||||||
|
#cgo freebsd openbsd LDFLAGS: -lwayland-client
|
||||||
|
|
||||||
|
#include "wayland-bind.h"
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var resErr = [...]error{
|
||||||
|
0: nil,
|
||||||
|
1: errors.New("wl_display_connect_to_fd() failed"),
|
||||||
|
2: errors.New("wp_security_context_v1 not available"),
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindWaylandFd(socketPath string, fd uintptr, appID, instanceID string, syncFD uintptr) error {
|
||||||
|
res := C.bind_wayland_fd(C.CString(socketPath), C.int(fd), C.CString(appID), C.CString(instanceID), C.int(syncFD))
|
||||||
|
return resErr[int32(res)]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user