Inhaltsverzeichnis

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.signal lässt sich nicht schreiben. Der Parser bricht mit expected ], got . ab, weil signal ein 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: Selbst import 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/restrict fließen also nicht ein.

Wirksam ist allein die Modulebene: Fehlt fs.read im @capabilities der 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:

 
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: