Natywny, w 100% napisany w H#, odpowiednik crate'a libseccomp z Rusta:
budowanie i instalowanie filtrów seccomp-bpf bez linkowania z prawdziwym
libseccomp.so, bez pliku C/Rust-shim kompilowanego obok, bez
extern static [rust, "..."] — jedyna granica FFI to extern static [c]
do prctl/syscall/ioctl/close z libc (src/ffi.h#), bo operacje na
seccomp są operacjami jądra i nie da się ich wykonać w czystej przestrzeni
użytkownika żadnym innym sposobem.
Program BPF (classic BPF, nie eBPF) jest składany ręcznie, instrukcja po
instrukcji, w H#, i pakowany bit-po-bicie do pamięci przy pomocy
wbudowanych w język prymitywów @pointers/arc_alloc/ptr_write_* —
zobacz nagłówkowy komentarz w src/bpf.h# po pełne wyjaśnienie, dlaczego
ani bytes, ani string nie nadają się do trzymania tego bufora.
cd twoj-projekt
bytes add libseccomplub w Bytes.hk:
[deps]
-> libseccomp => newest
Kod źródłowy jest podzielony na kilka plików w src/, połączonych przez
mod w src/lib.h# (punkt wejścia z manifestu Bytes.hk). W H# mod X
scala zawartość pliku bezpośrednio (bez prawdziwego zagnieżdżonego
namespace'u) — dlatego z zewnątrz każda funkcja jest dostępna po prostu
jako seccomp::nazwa_funkcji(...), niezależnie od tego, w którym pliku
faktycznie mieszka:
| Plik | Zawartość |
|---|---|
consts.h# |
stałe prctl/seccomp(2)/akcje/architektury/ioctl/opcode'y BPF |
errno.h# |
mapowanie nazwa↔numer errno (deny_errno, action_errno) |
ir.h# |
pośrednia reprezentacja instrukcji + łatanie skoków (dla komparatorów i bsearch) |
bpf.h# |
pack_instr + build_program (liniowy / multi-arch / bsearch) |
rules.h# |
typ Rule, buildery allow/deny_*, allow_baseline, validate_rules |
comparators.h# |
ArgCond/CondRule — filtrowanie po wartościach argumentów |
syscalls_x86_64.h# |
tabela syscalli x86_64 |
syscalls_aarch64.h# |
tabela syscalli aarch64 |
syscalls.h# |
dyspozytor number()/number_for_arch() |
export.h# |
export_hex/import_hex (serializacja), export_pfc (disasm) |
ffi.h# |
jedyny blok extern static [c] w całej bibliotece |
install.h# |
instalacja gotowego programu przez prctl |
notify.h# |
wsparcie dla SECCOMP_RET_USER_NOTIF |
use "bytes -> libseccomp" from "seccomp"
fn main() is
let mut rules: [seccomp::Rule] = seccomp::new_rules()
rules = seccomp::allow_baseline(rules) ;; read/write/mmap/exit/...
rules = seccomp::allow(rules, "openat")
rules = seccomp::deny_errno(rules, "ptrace", seccomp::EPERM)
let warnings: [string] = seccomp::validate_rules(rules)
;; (opcjonalnie: wypisz `warnings`, jeśli niepuste — literówki i
;; konflikty reguł, nigdy fatalne same w sobie)
let ok: bool = seccomp::install(rules, seccomp::ACTION_KILL_PROCESS)
if !ok is
write("install failed, errno=" + to_string(seccomp::last_errno()))
end
end
Kompilacja wyłącznie przez backend LLVM:
h# compile src/main.h# --release -o apph# preview / bytes run --tier interpreter nie obsługują extern,
arc_alloc ani ptr_* — działa wyłącznie logika czysta (bpf.h#,
rules.h#, comparators.h#'s encoding, obie tabele syscalli, errno.h#,
export.h#), którą pokrywa src/lib_test.h# uruchamiane przez
bytes test. install.h#/notify.h# (i ffi.h#, z którego korzystają)
wymagają realnie skompilowanej binarki.
| Funkcja | Opis |
|---|---|
new_rules() -> [Rule] |
pusta lista reguł |
allow(rules, name) -> [Rule] |
dopisz ALLOW dla syscalla (tabela x86_64) |
allow_arch(rules, name, arch) -> [Rule] |
jak wyżej, z tabelą wybranej architektury |
allow_nr(rules, nr) -> [Rule] |
jak allow, po surowym numerze |
deny_errno(rules, name, err) -> [Rule] |
zwróć err zamiast wykonać syscall |
deny_errno_arch(rules, name, err, arch) -> [Rule] |
jak wyżej, z tabelą wybranej architektury |
deny_errno_nr(rules, nr, err) -> [Rule] |
jak wyżej, po numerze |
deny_kill(rules, name) -> [Rule] |
zabij proces przy tym syscallu |
deny_trap(rules, name) -> [Rule] |
wyślij SIGSYS (do przechwycenia) |
deny_log(rules, name) -> [Rule] |
wpuść, ale zaloguj (audyt/dmesg) |
allow_baseline(rules) -> [Rule] |
gotowy zestaw dla zwykłego programu CLI |
validate_rules(rules) -> [string] |
ostrzeżenia: nierozwiązane nazwy, konflikty, duplikaty |
dedupe_rules(rules) -> [Rule] |
usuń duplikaty po nr (zachowuje pierwsze wystąpienie) |
Nieznana nazwa syscalla (literówka) nigdy nie zamienia się po cichu w
"zezwól na wszystko" — number() zwraca -1, a każdy budowniczy po prostu
nic wtedy nie dopisuje. validate_rules pozwala to wykryć jawnie zamiast
odkrywać dopiero w czasie działania.
Odpowiednik ScmpArgCompare/add_rule_conditional z Rustowego
libseccomp — filtrowanie nie tylko po numerze syscalla, ale i po
wartości jego argumentów:
;; openat(dirfd, path, flags, mode) — zezwól tylko na odczyt
let conds: [seccomp::ArgCond] = [seccomp::masked_eq(2, 0x3, 0)] ;; (flags & O_ACCMODE) == O_RDONLY
let mut rules: [seccomp::CondRule] = []
rules.push(seccomp::cond_rule(seccomp::number("openat"), seccomp::ACTION_ALLOW, conds))
rules.push(seccomp::cond_rule(seccomp::number("openat"), seccomp::action_errno(seccomp::EACCES), []))
seccomp::install_cond(rules, seccomp::ACTION_KILL_PROCESS)
| Funkcja | Warunek |
|---|---|
eq(idx, val) |
args[idx] == val — pełna precyzja 64-bit |
ne(idx, val) |
args[idx] != val — dolne 32 bity |
lt/le/gt/ge(idx, val) |
porównanie — dolne 32 bity |
masked_eq(idx, mask, val) |
(args[idx] & mask) == val — dolne 32 bity |
Tylko eq sprawdza pełne 64 bity seccomp_data.args[i] (osobne
porównanie górnego i dolnego słowa); reszta operuje na dolnych 32 bitach,
co pokrywa zdecydowaną większość realnych przypadków (fd, flagi, małe
rozmiary) bez komplikacji pełnego trójstronnego porównania 64-bit.
rule_to_cond(rule) podnosi zwykłą Rule do CondRule bez warunków, żeby
mieszać je w jednej liście [CondRule] z prawdziwymi regułami warunkowymi.
| Funkcja | Opis |
|---|---|
install(rules, default_action) -> bool |
dla ARCH_X86_64, schemat liniowy |
install_arch(rules, default_action, arch) -> bool |
wybrana architektura |
install_multi_arch(rules, default_action, arches) -> bool |
jeden filtr dla kilku ABI naraz |
install_bsearch(rules, default_action, arch) -> bool |
dyspozytor binarny O(log n) zamiast liniowego |
install_cond(rules, default_action) -> bool |
j.w., dla [CondRule] |
install_cond_arch(rules, default_action, arch) -> bool |
j.w. + architektura |
last_errno() -> int |
errno po nieudanej instalacji |
seccomp_mode() -> int |
0=brak, 1=strict, 2=filter (już aktywny) |
Filtr, raz zainstalowany, może być w tym procesie tylko zaostrzany — tak działa jądro. Druga instalacja dokłada kolejny filtr (logiczny AND), nie zastępuje poprzedniego.
install_bsearch sortuje reguły i dysponuje je drzewem porównań
BPF_JGE zamiast liniowego łańcucha — O(log n) zamiast O(n) porównań na
syscall, opłacalne od kilkudziesięciu–kilkuset reguł wzwyż. Domyślny,
liniowy schemat (install/install_arch) zostaje domyślny celowo: każdy
skok w nim ma stałą długość 0 lub 1, więc nigdy nie zbliża się do limitu
255 instrukcji klasycznego BPF, niezależnie od liczby reguł — jest
prościej zweryfikować go "na oko".
Zamiast sztywnego werdyktu z programu BPF, wybrana reguła może oddać decyzję procesowi nadzorującemu w czasie rzeczywistym — mechanizm, którego używają np. runtime'y kontenerów do selektywnego zezwalania na syscalle normalnie wymagające capability, jakich sandboksowany proces mieć nie powinien.
let fd: int = seccomp::install_notify(rules, seccomp::ACTION_KILL_PROCESS, seccomp::ARCH_X86_64)
let n: seccomp::Notif = seccomp::notif_receive(fd) ;; blokuje do następnego zdarzenia
;; n.nr, n.pid, n.args[0..6] — zdecyduj...
if seccomp::notif_id_valid(fd, n.id) is
seccomp::notif_respond(fd, n.id, 0, 0, 0) ;; zezwól, syscall zwraca 0
end
| Funkcja | Opis |
|---|---|
install_notify(rules, default_action, arch) -> int |
zwraca fd powiadomień (lub wartość ujemną) |
notif_receive(fd) -> Notif |
czeka na kolejne zdarzenie |
notif_respond(fd, id, val, error, flags) -> bool |
odpowiedz na zdarzenie |
notif_id_valid(fd, id) -> bool |
sprawdź, czy id wciąż aktualne |
close_notify(fd) -> bool |
zamknij fd powiadomień |
Oznacz regułę do przekazania nadzorcy przez ACTION_USER_NOTIF zamiast
zwykłej akcji (Rule { nr: ..., action: seccomp::ACTION_USER_NOTIF }).
| Funkcja | Opis |
|---|---|
export_hex(prog) -> string |
zserializuj zbudowany program do stringa (do cache'owania) |
import_hex(s) -> [int] |
odwrotność export_hex |
export_pfc(prog) -> string |
czytelny dla człowieka disasm (do debugowania) |
Bez operacji na plikach — persystencja to jedna linijka
use "std -> fs" from "fs" + fs::write(...) u wywołującego, nie
narzucona zależność.
Stałe (EPERM, ENOENT, EACCES, EINVAL, ENOSYS, ...) plus pełne
errno_number(name) -> int / errno_name(num) -> string (>130 wpisów,
standardowa numeracja Linuksa, niezależna od architektury).
ACTION_ALLOW, ACTION_KILL_PROCESS, ACTION_KILL_THREAD/ACTION_KILL,
ACTION_TRAP, ACTION_LOG, ACTION_TRACE, ACTION_USER_NOTIF, oraz
action_errno(err) / action_trace(msg) do budowania wariantów z
payloadem.
ARCH_X86_64 (domyślna), ARCH_AARCH64, ARCH_I386, ARCH_ARM.
number(name) / number_x86_64(name) — x86_64, >320 wpisów.
number_aarch64(name) — aarch64 (tabela generyczna, inna numeracja niż
x86_64 — to nie jest literówka, to dwa różne ABI). number_for_arch(name, arch) — dyspozytor. Czego zabraknie, można nadal filtrować przez
allow_nr/deny_errno_nr po surowym numerze.
examples/basic_sandbox.h#— statyczna allow-lista +KILL_PROCESS.examples/network_sandbox.h#— allow-lista + gniazda wychodzące, jawnyEPERMnabind/listen/accept*, trybLOGdo nauki wymagań programu przed przejściem naKILL_PROCESS.examples/arg_comparator_sandbox.h#—openatdozwolony tylko do odczytu (komparator na fladzeO_ACCMODE).examples/notify_sandbox.h#—mount()oddane do decyzji nadzorcy przezSECCOMP_RET_USER_NOTIF.
Rozważane, ale niepotrzebne: nie ma tu żadnej statycznej biblioteki Rust
do zlinkowania --whole-archive, bo cała logika (enkoder BPF, tabele
syscalli, komparatory, buildery reguł) jest czystym H#. Jedyna prawdziwa
granica FFI (ffi.h#) to cztery zwykłe funkcje libc — stąd
extern static [c], dokładnie tak jak malloc/free w testach
kompilatora H# (tests/compiler/unsafe_arena_kinds_test.h#).
- Tylko Linux (seccomp-bpf to mechanizm jądra Linuksa).
install*/notify*wymagają backendu LLVM (patrz wyżej).- Komparatory poza
eqoperują na dolnych 32 bitach argumentu (patrzcomparators.h#) — w praktyce wystarcza to niemal zawsze, ale nie jest to pełna precyzja 64-bit jak w prawdziwymlibseccomp. - Brak wsparcia dla starych, zmultipleksowanych syscalli (
socketcall,ipcna i386) — dotyczy głównie starszych architektur 32-bitowych. - Tabela
syscalls_aarch64.h#była zestawiona ręcznie z publicznej numeracjiasm-generic/unistd.h— w przeciwieństwie do dekady stabilnej tabeli x86_64, warto zweryfikować ją względem nagłówków konkretnego jądra przed użyciem produkcyjnym.
MIT — zobacz LICENSE.