Capabilities (LCBS)

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


1. Deklaration

@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


2. Verfügbare Capabilities

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.

Was sich damit NICHT anfordern lässt

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 @capabilities und verliert damit den gesamten Filter; das ist der Grund, warum die Lücke praktisch wiegt.
 
process.signal ist 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 mit expected ], got . ab, weil signal ein reserviertes Wort ist.
(Issue #1198)

2b. Was daraus im LBF-Manifest wird (nur ''--target=lyxos'')

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:

  • Die vier Gerätezugriffe teilen sich ein Bit. Der Kernel kennt genau eine Geräteklasse; eine feinere Trennung wäre eine Zusage, die sich nicht durchsetzen ließe. Aufteilen geht später, ohne bestehende Bedeutungen zu verschieben.
  • 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_caps im Lyx-OS-Kernel bildet 0x100 auf PLEDGE_DEVICE ab; 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-TLV 0x41, mit hardware.i2c zusätzlich Bit 0x100.

Ebenfalls offen: für proc.spawn gibt es kein Capability-Wort — der Name wird mit unbekannte Capability abgewiesen, das Bit 0x08 lässt sich damit nicht gezielt setzen (#1823). Deklarierbar ist nur process.*, etwa process.signal (#1198).

Drei Namen werden zwar angenommen, aber nicht sauber geführt (#1826, nachgemessen mit lyxc 1.2.2B): ki.embed und ki.graph übersetzen, erscheinen im Sicherheits-Audit aber unter „Explizite Capabilities: keine“; audio.play erscheint dort als + unknown. Zum Vergleich führt fs.read sich korrekt als + fs.read. Wer sich auf diese drei verlässt, bekommt eine Zusage, die im Audit nicht nachweisbar ist.

Immer aktiv

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)

3. grant und restrict an Imports

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

Seit 1.1.12E: die std-Units sagen selbst, was sie brauchen (#1340)

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: wer std.fs einbindet, 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: grant beschränkt die importierte Unit zur Laufzeit nicht, und der erzeugte seccomp-Filter ist in allen Fällen gleich. Wirksam ist allein die Modulebene — fehlt fs.read im @capabilities der 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)

4. @uses_caller_cap

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.


5. FFI ist Fail-Closed

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.


6. Sicherheits-Score

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).


7. Wirkung zur Laufzeit

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.


8. Einschränkungen im aktuellen Stand

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:

  • Die pauschale “/„-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.
  • Existiert ein genannter Pfad nicht, wird keine Regel angelegt und der Zugriff bleibt verboten (fail-closed).
 
Nicht durchgesetzt werden host: und port: an Netzwerk-Capabilities sowie path: an fs.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 mit Capability-Argument wird NICHT durchgesetzt hin.
(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).