Lyx bindet jedes Programm an ein Capability-Modell: Der Compiler erzeugt aus den deklarierten Berechtigungen einen seccomp-Filter, der zur Laufzeit jeden nicht angeforderten Systemaufruf hart abbricht. Das Modell ist Zero-Privilege mit Default-Deny — was nicht ausdrücklich gewährt wurde, ist verboten.
Ohne Deklaration läuft ein Programm ungehärtet: der Compiler meldet Capability-Modell: NONE und erzeugt keinen seccomp-Filter.
→ Module und Importe · FFI · DO-178C
@capabilities steht vor allen anderen Deklarationen der Datei:
@capabilities([system.exit, system.memory.heap])
import std.io;
fn main(): int64 {
PrintLn("Hallo");
return 0;
}
Mehrere Berechtigungen werden mit Komma getrennt. Alternativ erlaubt @capabilities(dynamic) die Auflösung zur Laufzeit.
Ein unbekannter Name ist ein Compile-Fehler:
@capabilities([zzz.erfunden])
→ sema error: unbekannte Capability
| Bereich | Capability | Zweck |
|---|---|---|
| System | system.exit | Prozess beenden |
system.env | Umgebungsvariablen lesen | |
system.time | Systemzeit | |
system.rand | Zufallszahlen | |
system.memory.heap | Heap-Allokation (brk, mmap) |
|
system.memory.stack | Stack-Wachstum | |
system.unsafe.format_string | Format-String-FFI (printf-Klasse) |
|
| Dateisystem | fs.read / fs.write | Lesen / Schreiben |
fs.create / fs.delete | Anlegen / Löschen | |
fs.meta | Metadaten lesen (stat) |
|
fs.perm | Metadaten schreiben (chmod, chown) |
|
fs.exec | Ausführbar öffnen | |
| Netzwerk | network.tcp.connect | Ausgehende TCP-Verbindung |
network.tcp.bind | TCP-Port belegen (Server) | |
network.udp.connect / network.udp.bind | UDP ausgehend / gebunden | |
network.unix | Unix-Domain-Sockets | |
network.raw | Raw-Sockets | |
| Prozesse | process.exec | Programm starten |
process.fork | Prozess duplizieren | |
process.exit | Prozess beenden | |
process.sched | Scheduling beeinflussen | |
process.signal | Signale senden | |
| Konfiguration | system.config | Persistente Konfiguration schreiben (seit 1.1.11A) |
| Geräte | hardware.block | Blockgeräte (seit 1.1.8B) |
hardware.i2c / hardware.usb | I²C- / USB-Zugriff (seit 1.1.8D) | |
hardware.gpio / hardware.spi | GPIO- / SPI-Zugriff (seit 1.1.8D) |
| KI (nur lyxos-Manifest) | ki.embed / ki.graph | Einbettungen / Wissensgraph — auf Linux ohne Syscall-Wirkung (seit 1.1.11E) |
| Ton | audio.mic | Mikrofon; eine Wiedergabe-Zusage gibt es nicht (#1873) |
Das sind alle 36 gültigen Namen der Registry (src/security/capabilities.lyx), nachgemessen mit lyxc 1.1.14A. Jeder andere wird mit unbekannte Capability abgewiesen.
Der seccomp-Filter ist fail-closed: was keine Capability nennt, wird beim Aufruf mit SIGSYS beendet (Exit 159). Hier zählen zwei verschiedene Dinge, das ist die häufigste Verwechslung:
| Größe | Zahl |
|---|---|
| Capability-Namen in der Registry | 36 |
| Syscall-Nummern, die der Filter freigibt | 82 |
| Syscall-Nummern, die die Standardbibliothek benutzt | 154 |
| davon erlaubt und benutzt | 52 |
erlaubt, von std nicht benutzt (FFI-/libc-Pfade) | 30 |
| benutzt, von keiner Capability gedeckt | 101 |
Die Lücke ist also nicht 154 − 82, sondern die Differenz der Mengen: 101 Syscalls, für die es keine Schreibweise gibt. Ein Name deckt dabei viele Nummern ab — fs.read allein steht für open, read, lseek, stat und Verwandte. Zum Schließen braucht es deshalb keine 101 neuen Namen, sondern etwa zehn (io.wait, io.fd, system.tty, process.privileges, system.namespace, ipc.sysv, ipc.mqueue, fs.xattr, fs.watch, debug.trace, kernel.bpf) plus ein paar Erweiterungen bestehender Namen (system.time um nanosleep, memory.mmap um mremap/madvise, process.sched um sched_yield …). Der vollständige Zuordnungsvorschlag steht als Kommentar an #1865.
Erhoben mit 1.1.14A:
| Fehlt | Folge | Issue |
|---|---|---|
nanosleep | sleep() aus std.os stirbt trotz system.time | #1866 |
Terminal-ioctl | ioctl gibt es nur über hardware.* — ein TUI muss hardware.gpio deklarieren | #1867 |
poll/select/epoll/timerfd | keine Ereignisschleife, kein Server mit mehreren Verbindungen | #1868 |
pipe/dup/fcntl | std.pipe unbenutzbar; ein Kindprozess kann nichts zurückgeben | #1869 |
setuid/setsid/unshare/chroot | Rechte abgeben ist nicht deklarierbar; std.sysadmin unbenutzbar | #1870 |
| System-V-IPC, POSIX-Queues | std.ipc_sysv unbenutzbar | #1871 |
chdir, statfs, xattr, inotify, sendfile/readv/writev, mremap/madvise, ptrace/perf/bpf | einzelne Units ganz oder teilweise unbenutzbar | #1872 |
Verdrahtung von hardware.block und system.config | der Name ist gültig, erreicht den Filter aber nicht — hardware.block gibt auf Linux nichts frei, Blockgeräte-ioctl hängt an hardware.gpio | #1875 |
Das ist die Kehrseite von fail-closed, nicht ein Loch in der Härtung. Der Filter lässt nur durch, was benannt ist — es fehlen die Namen. Wer eine betroffene Unit heute braucht, baut ohne@capabilitiesund verliert damit den gesamten Filter; das ist der Grund, warum die Lücke praktisch wiegt.
process.signalist seit lyxc 1.0.17K deklarierbar (#1198).@capabilities([process.signal])übersetzt und läuft; Schlüsselwörter sind als Capability-Pfadsegment zugelassen. Bis 1.0.17F brach der Parser hier mitexpected ], got .ab, weilsignalein reserviertes Wort ist.
(Issue #1198)
Auf –target=lyxos schreibt der Compiler die Berechtigungen als Bitmaske in die CAPS-TLV des LBF-Manifests. Der Kernel liest sie beim Laden und leitet daraus seine PLEDGE-Klassen ab. Auf allen anderen Zielen entsteht stattdessen ein seccomp-Filter — die Namen sind dieselben, der Mechanismus nicht.
| Bit | Bedeutung | Gesetzt von |
|---|---|---|
0x1 | LBF_CAP_FS_READ | fs.read |
0x2 | LBF_CAP_FS_WRITE | fs.write |
0x4 | Netzwerk | network.* |
0x8 | Prozesse | process.* |
0x10 | LBF_CAP_KI_EMBED | ki.embed — wird angenommen, taucht im Audit aber nicht auf (#1826) |
0x20 | LBF_CAP_KI_GRAPH_WRITE | ki.graph — dasselbe (#1826) |
0x40 | LBF_CAP_BLOCK | hardware.block |
0x80 | Audio bzw. PLEDGE_CONFIG | system.config |
0x100 | LBF_CAP_DEVICE | hardware.i2c, hardware.usb, hardware.gpio, hardware.spi |
Die Bitwerte stammen aus den Umsetzungsberichten zu #1755, #1759 und #1797 (die maßgebliche Belegung steht in src/std/lyxos/lbf_layout.lyx); die Zuordnung Name → Bit gibt der Compiler selbst aus, siehe unten. Zwei Dinge sind daran bemerkenswert:
hardware.block ist bewusst getrennt — PLEDGE_BLOCK ist im Kernel eine andere Klasse als der Gerätezugriff.Ein Name, der kein Bit setzt, wird beim Übersetzen genannt statt stillschweigend verworfen:
warning: --target=lyxos: @capabilities(quatsch.blah) setzt kein Bit in der CAPS-TLV.
Das LBF kommt beim Ladeprogramm an wie eines ohne Manifest (nur Standardausgabe).
Abgebildet sind: fs.read, fs.write, network.*, process.*, ki.embed, ki.graph,
audio.*, system.config, hardware.block, hardware.i2c/usb/gpio/spi.
Die Gerätebits kommen inzwischen an — das Issue dazu ist aber noch offen.lbf_map_capsim Lyx-OS-Kernel bildet0x100aufPLEDGE_DEVICEab; im Quelltext steht ausdrücklich, dass die Zusage vorher „still unter den Tisch fiel„ (#1815 — im Kernel umgesetzt, auf GitHub noch nicht geschlossen). Gegengeprüft:@capabilities([fs.read, hardware.block])erzeugt die CAPS-TLV0x41, mithardware.i2czusätzlich Bit0x100.
Ebenfalls offen: fürproc.spawngibt es kein Capability-Wort — der Name wird mitunbekannte Capabilityabgewiesen, das Bit0x08lässt sich damit nicht gezielt setzen (#1823). Deklarierbar ist nurprocess.*, etwaprocess.signal(#1198).
Drei Namen werden zwar angenommen, aber nicht sauber geführt (#1826, nachgemessen mit lyxc 1.2.2B):ki.embedundki.graphübersetzen, erscheinen im Sicherheits-Audit aber unter „Explizite Capabilities: keine“;audio.playerscheint dort als+ unknown. Zum Vergleich führtfs.readsich korrekt als+ fs.read. Wer sich auf diese drei verlässt, bekommt eine Zusage, die im Audit nicht nachweisbar ist.
Drei Capabilities sind implizit gesetzt, weil ohne sie kein Programm startet:
| Capability | Syscalls |
|---|---|
system.exit | exit_group |
system.memory.heap | brk, mmap(MAP_ANON) |
system.rand | getrandom (Initialisierung der Stack-Canaries) |
Jeder import kann angeben, welche Berechtigungen die eingebundene Unit erhalten soll. grant zählt auf, was weitergegeben wird, restrict nimmt zurück:
@capabilities([system.exit, system.memory.heap, fs.read])
import std.io grant [system.exit];
import std.fs grant [fs.read];
Beides lässt sich am selben Import kombinieren, und mehrere Imports stehen wahlweise in einer Deklaration:
import std.io grant [system.exit], std.string grant [system.exit];
Eine Unit kann nur bekommen, was das aufrufende Modul selbst besitzt. Diese Prüfung greift zur Übersetzungszeit:
sema error: grant enthält Capability die nicht in Parent-Capabilities ist
Jede std-Unit trägt inzwischen ein eigenes @capabilities. Damit ist grant keine bloße Absichtserklärung mehr, sondern wird gegen die Deklaration des Moduls geprüft. Drei Fälle, mit 1.1.14A gemessen:
| Schreibweise | Ergebnis |
|---|---|
import std.fs grant []; | sema error (line 2): grant fuehrt nicht, was das Modul deklariert: fs.create — der Bau bricht ab |
import std.fs; (ohne grant) | zwei Warnungen, Bau läuft: „Import ohne explizites grant — mit grant wird die Deklaration des Moduls geprueft (#1340)„ und „importiertes Modul deklariert Capability, die das Programm nicht fuehrt — zur Laufzeit greift seccomp“ |
grant vollständig, aber das Programm führt die Rechte nicht | sema error: grant enthält Capability die nicht in Parent-Capabilities ist |
Ein Bau gelingt also erst, wenn grant genau das aufzählt, was die Unit deklariert, und das Programm dieselben Rechte in seinem eigenen @capabilities führt:
@capabilities([system.exit, system.memory.heap,
fs.create, fs.delete, fs.meta, fs.perm, fs.read, fs.write, system.time])
import std.fs grant [fs.create, fs.delete, fs.meta, fs.perm, fs.read, fs.write, system.time];
Das ist unbequem und soll es sein: werstd.fseinbindet, führt danach sichtbar sieben Rechte — statt sie unbemerkt mitzunehmen. Wer weniger will, bindet eine kleinere Unit ein.
Was weiterhin nicht durchgesetzt wird. Geprüft wird die Deklaration, nicht die Ausführung:grantbeschränkt die importierte Unit zur Laufzeit nicht, und der erzeugte seccomp-Filter ist in allen Fällen gleich. Wirksam ist allein die Modulebene — fehltfs.readim@capabilitiesder Datei, bricht der Zugriff mit Exit 159 ab. Das Zero-Privilege-Modell steht; es endet an der Unit-Grenze.
Zum Sicherheits-Score: Er vergibt +10 für „alle Imports haben explizites grant„. Seit #1340 ist dieser Posten wenigstens statisch gedeckt — eine Eindämmung zur Laufzeit beschreibt er nach wie vor nicht.
(Issue #1231, Issue #1340)
Eine Funktion, die eine Berechtigung des Aufrufers benötigt, deklariert das ausdrücklich. Geprüft wird gegen die aufrufende Funktion — diese muss die Berechtigung selbst per @capabilities tragen. Die Angabe am Dateikopf genügt dafür nicht:
@capabilities([system.exit, system.memory.heap, fs.read])
import std.io;
@uses_caller_cap([fs.read])
fn LiesKonfiguration(): int64 {
return 0;
}
@capabilities([fs.read])
fn main(): int64 {
PrintLn(IntToStr(LiesKonfiguration()));
return 0;
}
Fehlt das @capabilities an main, wird der Aufruf abgelehnt:
sema error: Caller hat nicht: fs.read (benötigt von @uses_caller_cap)
Das gilt auch für implizite Capabilities: @uses_caller_cap([system.exit]) verlangt ebenfalls ein ausdrückliches @capabilities([system.exit]) an der aufrufenden Funktion.
extern fn ohne passende Capability wird abgewiesen — nicht gewarnt, sondern abgelehnt:
sema error: extern fn: unbekanntes FFI-Symbol erfordert @capabilities([...])
(FFI-Sandbox Fail-Closed)
Ein FFI-Aufruf ist damit immer eine bewusste, sichtbare Entscheidung.
Jeder Build gibt eine Bewertung aus. Sie zeigt unmittelbar, was die Deklaration bewirkt:
# ohne @capabilities
Capability-Modell: NONE (kein @capabilities -- keine LCBS-Haertung)
Sicherheits-Score: 33/45
+ 0: seccomp fehlt
+8: Grant-Modell -- 1 Import(e) ohne grant (-2 pro fehlendem grant)
# mit @capabilities und vollständigen grants
+ seccomp (SECCOMP_RET_KILL_PROCESS, 53 Regeln)
Sicherheits-Score: 45/45
+10: Grant-Modell -- alle Imports haben explizites grant
+ 5: seccomp aktiv
Weitere Posten: W^X (getrennte PT_LOAD-Segmente), RELRO, und der Nachweis, dass keine Klasse-3-Extern-Funktion ohne system.unsafe.format_string verwendet wird. PIE ist derzeit nicht implementiert (+0).
Der erzeugte seccomp-Filter arbeitet mit SECCOMP_RET_KILL_PROCESS. Ein nicht gewährter Systemaufruf beendet den Prozess sofort:
@capabilities([system.exit, system.memory.heap])
import std.fs;
fn main(): int64 {
if (FileExists("/etc/hostname"c)) { return 1; }
return 0;
}
$ ./programm
Ungültiger Betriebssystemaufruf (Speicherabzug geschrieben)
$ echo $?
159
Mit fs.read in der Liste läuft dasselbe Programm durch.
Eine Capability kann Argumente tragen, die ihren Geltungsbereich einschränken:
@capabilities([system.exit, system.memory.heap, fs.read(path: "/tmp")])
Der Argumentname wird geprüft — ein Tippfehler wie pfad: wird mit unbekanntes Capability-Argument abgewiesen.
path: an einer fs-Capability wird seit lyxc 1.0.15E durchgesetzt. Der Compiler legt je genanntem Pfad eine eigene Landlock-Regel an, mit genau den Zugriffsarten der Capability, zu der das Argument gehört. Ein Lesen außerhalb schlägt zur Laufzeit fehl:
@capabilities([system.exit, system.memory.heap, fs.read(path: "/tmp")])
// ReadFile("/etc/hostname"c, …) -> -1; mit fs.read(path: "/etc") -> gelesene Bytes
Zwei Regeln dazu:
“/„-Regel entfällt genau dann, wenn alle fs-Capabilities einen Pfad tragen. Trägt auch nur eine keinen, bleibt sie stehen — ein einzelnes fs.write ohne path: hebt die Beschränkung der übrigen also faktisch auf.Nicht durchgesetzt werdenhost:undport:an Netzwerk-Capabilities sowiepath:anfs.perm. Dort wird nur der Argumentname geprüft; der Geltungsbereich dokumentiert die Absicht, erzwingt sie aber nicht — die Capability wirkt für die Zugriffsentscheidung als reines Ja/Nein. Der Compiler weist auf jede solche Stelle mitCapability-Argument wird NICHT durchgesetzthin.
(Issue #1108)
Weiterführende Seiten:
Letzte Aktualisierung: 2026-09-05 (#1826, nachgemessen mit lyxc 1.2.2B) — ki.embed/ki.graph sind inzwischen deklarierbar, werden im Audit aber nicht geführt; audio.play erscheint dort als unknown. Tabelle und Kasten entsprechend gezogen.
Vorherige letzte Aktualisierung: 2026-08-27 — CAPS-TLV am Erzeugnis nachgemessen (0x41 für fs.read, hardware.block); Kernel-Abbildung von 0x100 auf PLEDGE_DEVICE bestätigt. Namensliste gegen lyxc 1.1.11B neu erhoben (31 statt 25 Namen: system.config aus #1797, hardware.* aus #1755/#1759); Abschnitt 2b zur CAPS-TLV des LBF-Manifests ergänzt.
Letzte Aktualisierung: 2026-08-30 — Registry gegen src/security/capabilities.lyx abgeglichen (36 Namen statt 31, ki.embed/ki.graph/audio.mic ergänzt) und Abschnitt „Was sich damit NICHT anfordern lässt“ aufgenommen (#1865–#1873, aus dieser Prüfung gemeldet).
Zuvor 2026-08-30 — grant wird seit 1.1.12E gegen die @capabilities der importierten std-Unit geprüft (#1340); die drei Fälle gegen 1.1.14A gemessen.
Vorherige Aktualisierung: 2026-08-13 — process.signal ist seit lyxc 1.0.17K deklarierbar (#1198).
Codebeispiele geprüft: gegen lyxc 1.2.5C übersetzt (Prüflauf 2026-09-08 über die gesamte Doku: 574 Vollprogramme, 0 echte Fehler; zusätzlich 5159 Aufrufe gegen die pub fn-Signaturen in aurum/std gehalten, 0 Abweichungen).