====== 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.
→ [[lyx_-_programmiersprache:sprache:module-und-importe|Module und Importe]] · [[lyx_-_programmiersprache:sprache:ffi|FFI]] · [[lyx_-_programmiersprache:guides:do-178c|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** ([[https://github.com/SEOLizer/LyX-Compiler/issues/1873|#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 [[https://github.com/SEOLizer/LyX-Compiler/issues/1865|#1865]].
Erhoben mit 1.1.14A:
^ Fehlt ^ Folge ^ Issue ^
| ''nanosleep'' | ''sleep()'' aus ''std.os'' stirbt trotz ''system.time'' | [[https://github.com/SEOLizer/LyX-Compiler/issues/1866|#1866]] |
| Terminal-''ioctl'' | ''ioctl'' gibt es nur über ''hardware.*'' — ein TUI muss ''hardware.gpio'' deklarieren | [[https://github.com/SEOLizer/LyX-Compiler/issues/1867|#1867]] |
| ''poll''/''select''/''epoll''/''timerfd'' | keine Ereignisschleife, kein Server mit mehreren Verbindungen | [[https://github.com/SEOLizer/LyX-Compiler/issues/1868|#1868]] |
| ''pipe''/''dup''/''fcntl'' | ''std.pipe'' unbenutzbar; ein Kindprozess kann nichts zurückgeben | [[https://github.com/SEOLizer/LyX-Compiler/issues/1869|#1869]] |
| ''setuid''/''setsid''/''unshare''/''chroot'' | Rechte **abgeben** ist nicht deklarierbar; ''std.sysadmin'' unbenutzbar | [[https://github.com/SEOLizer/LyX-Compiler/issues/1870|#1870]] |
| System-V-IPC, POSIX-Queues | ''std.ipc_sysv'' unbenutzbar | [[https://github.com/SEOLizer/LyX-Compiler/issues/1871|#1871]] |
| ''chdir'', ''statfs'', xattr, inotify, ''sendfile''/''readv''/''writev'', ''mremap''/''madvise'', ''ptrace''/''perf''/''bpf'' | einzelne Units ganz oder teilweise unbenutzbar | [[https://github.com/SEOLizer/LyX-Compiler/issues/1872|#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'' | [[https://github.com/SEOLizer/LyX-Compiler/issues/1875|#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.
> ([[https://github.com/SEOLizer/LyX-Compiler/issues/1198|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 ([[https://github.com/SEOLizer/LyX-Compiler/issues/1826|#1826]]) |
| ''0x20'' | ''LBF_CAP_KI_GRAPH_WRITE'' | ''ki.graph'' — dasselbe ([[https://github.com/SEOLizer/LyX-Compiler/issues/1826|#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 [[https://github.com/SEOLizer/LyX-Compiler/issues/1755|#1755]], [[https://github.com/SEOLizer/LyX-Compiler/issues/1759|#1759]] und [[https://github.com/SEOLizer/LyX-Compiler/issues/1797|#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" ([[https://github.com/SEOLizer/LyX-Compiler/issues/1815|#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 ([[https://github.com/SEOLizer/LyX-Compiler/issues/1823|#1823]]). Deklarierbar ist nur ''process.*'', etwa ''process.signal'' ([[https://github.com/SEOLizer/LyX-Compiler/issues/1198|#1198]]).
>
> **Drei Namen werden zwar angenommen, aber nicht sauber geführt** ([[https://github.com/SEOLizer/LyX-Compiler/issues/1826|#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.
> ([[https://github.com/SEOLizer/LyX-Compiler/issues/1231|Issue #1231]], [[https://github.com/SEOLizer/LyX-Compiler/issues/1340|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.
> ([[https://github.com/SEOLizer/LyX-Compiler/issues/1108|Issue #1108]])
----
**Weiterführende Seiten:**
* [[lyx_-_programmiersprache:sprache:module-und-importe|Module und Importe — grant/restrict im Kontext]]
* [[lyx_-_programmiersprache:sprache:ffi|FFI — externe Funktionen einbinden]]
* [[lyx_-_programmiersprache:sprache:attributes-pragmas|Attribute und Pragmas]]
Letzte Aktualisierung: 2026-09-05 ([[https://github.com/SEOLizer/LyX-Compiler/issues/1826|#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).