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