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 — derzeit nicht deklarierbar, siehe unten |
Das sind alle 25 gültigen Namen. Jeder andere wird mit unbekannte Capability abgewiesen.
process.signallässt sich nicht schreiben. Der Parser bricht mitexpected ], got .ab, weilsignalein reserviertes Wort ist. Die Capability existiert im Compiler, ist über die Deklaration aber nicht erreichbar.
(Issue #1198)
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
Die Eindämmung wird nicht durchgesetzt. Die Angaben werden geprüft, beschränken die importierte Unit aber nicht: Selbstimport std.fs grant [];— der Unit wird nichts gewährt — liest anschließend anstandslos Dateien. Der erzeugte seccomp-Filter ist in allen Fällen identisch (71 Regeln),grant/restrictfließen also nicht ein.
Wirksam ist allein die Modulebene: Fehltfs.readim@capabilitiesder Datei, bricht derselbe Zugriff mit Exit 159 ab. Das Zero-Privilege-Modell steht — es endet an der Unit-Grenze.
Zu beachten beim Sicherheits-Score: Er vergibt +10 für „alle Imports haben explizites grant„. Dieser Posten beschreibt bis auf Weiteres eine Absicht, keine Eindämmung.
(Issue #1231)
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 einzelnesfs.writeohnepath: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 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:
