Files
claude-regelwerk/.vault-tools/plan-kopie/docs/plans/2026-10-09-mutanten-kopie-loopback.md
Pascal MichelandClaude Opus 5.5 8950af05e0 Mutantenlauf auf der Kopie: lo im Netz-Namensraum, fail-closed, Treiber W1 bis W13
mutanten_lauf_kopie.sh faehrt lo im eigenen Netz-Namensraum hoch (unshare --keep-caps, ip, setpriv,
exec in python3) und prueft das vorher fail-closed (ip, setpriv, python3, lo-Probe mit
Faehigkeitspruefung). Anlass: Im Paket RB3 der Trading Plattform scheiterten Tests mit Server auf
127.0.0.1 im Namensraum an Errno 101 (lo aus).
Neuer Treiber mutanten_lauf_kopie_treiber.py mit W1 bis W13; Regressionsnachweis gegen HEAD (W1, W2,
W4, W5, W9 rot) und neun kaputte Varianten v1 bis v9, je an ihren Faellen rot. Lauf mit echten Daten:
alle 73 RB3-Mutanten durch das neue Skript 73/73 OK, T1 und T12 jetzt im Namensraum. Ein pruefer
las Skript und Treiber (kein blockierender Befund; der wichtige zu fehlenden Faellen fuer die
Fehlerzweige ist mit Nachbesserung 1 behoben, W10 bis W13).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JU1LsUp4FdmPq8mvC368G3
2026-10-09 15:20:49 +02:00

1230 lines
56 KiB
Markdown

---
type: plan
title: "Mutantenlauf auf der Kopie: lo im Netz-Namensraum, fail-closed"
status: active
summary: "Ein Task: mutanten_lauf_kopie.sh fährt lo im eigenen Netz-Namensraum hoch (unshare mit --keep-caps, ip, setpriv, exec in python3) und prüft das vorher fail-closed; dazu ein eigener Treiber mit den Fällen W1 bis W9, Regressionsnachweis gegen git show HEAD und fünf kaputte Varianten, Nachtrag in LIESMICH und Entwurf."
tags: [plan, vault-tools, pruefwerkzeuge]
created: 2026-10-09
updated: 2026-10-09
modell: sonnet
effort: low
modell_begruendung: "Jeder Block steht wörtlich da, Zieldateien, Verbote und Haltepunkte sind genannt; LIESMICH- und Entwurf-Block tragen typografische Anführungszeichen und Auslassungspunkte, die Haiku still ersetzt, deshalb sonnet mit effort low."
---
Übersicht: [LIESMICH der Planwerkzeuge](../../LIESMICH.md) · Anlass: Paket RB3 der Trading Plattform (`D:/Claude/.arbeitsbaeume/rb-proben/rb3_mutanten.sh`, Kopfkommentar)
# Mutantenlauf auf der Kopie: lo im Netz-Namensraum, fail-closed
## Ziel
`mutanten_lauf_kopie.sh` fährt den Mutantenläufer weiter in einem eigenen Netz-Namensraum, aber mit hochgefahrenem `lo`. Ein Test, der einen Server auf `127.0.0.1` startet, läuft dort grün, statt mit `OSError: [Errno 101] Network is unreachable` die Grundlinie rot zu machen. Die Netzsperre nach außen bleibt, der Läufer behält keine Fähigkeiten, und fehlt ein Baustein (`ip`, `setpriv`, `python3`, nutzbares `lo`), endet das Skript vor dem Kopieren mit `MUTANTEN_LAUF_FEHLER` und Exit 2. Die Plankritik bekommt damit für Projekte mit solchen Tests einen Mutantenlauf statt einer roten Grundlinie.
## Nicht im Umfang
- `mutanten_lauf.py` und sein Treiber `mutanten_lauf_treiber.py` bleiben unverändert.
- `.claude/workflows/plankritik.js` bleibt unverändert, auch die Auswertung ohne `EXIT`-Zeile (siehe Offene Punkte).
- `rb3_mutanten.sh` und die anderen Dateien unter `D:/Claude/.arbeitsbaeume/rb-proben/` bleiben unverändert.
- Begrenzungsmenge (`CapBnd`) und `no_new_privs` werden nicht angefasst.
- Kein Lauf gegen ein echtes Projekt durch den Executer; den Lauf mit echten Daten fährt der Koordinator.
## Warum so
### Anlass
- Das Skript fährt `mutanten_lauf.py` seit 2026-09-28 unter `unshare --map-current-user --net`.
- Im frischen Netz-Namensraum ist `lo` aus. Jeder Test mit einem Server auf `127.0.0.1` scheitert dort mit `Errno 101`, auch ohne Mutant.
- Gemessen am 2026-10-09 im Paket RB3: Die Mutanten T1 und T12 musste der Koordinator außerhalb des Namensraums fahren (`rb3_mutanten.sh`, Kopfkommentar).
- `plankritik.js` ruft das Skript für jede `mutanten.toml` auf. Ein Projekt mit solchen Tests bekam dort eine rote Grundlinie statt eines Mutantenlaufs.
### Was die Probe des Koordinators misst
`D:/Claude/.arbeitsbaeume/rb-proben/lo_probe.py`, gelesen am 2026-10-09:
- Sie legt einen Server-Socket auf `127.0.0.1` an, den Port wählt der Kernel (`bind` mit Port 0), und ruft `listen(1)`.
- Ein zweiter Socket verbindet sich mit Zeitlimit 3 s dorthin. Ausgabe `connect ok` oder `connect scheitert: <Fehler>`.
- **Misst:** `lo` ist oben, `127.0.0.1` ist im Namensraum adressierbar, der TCP-Verbindungsaufbau gelingt. Den Aufbau schließt der Kernel über die Warteschlange von `listen` ab, ohne `accept`.
- **Misst nicht:** Datenaustausch (kein `accept`, kein `send`; das tut W1). uid und Fähigkeiten (hat der Koordinator getrennt gemessen; W3 prüft sie). Ob `127.0.0.1` innen vom `lo` des Rechners getrennt ist (W8).
- **Falle:** Die Probe endet auch bei `connect scheitert` mit Exit 0. Die Vorprüfung im Skript vergleicht deshalb Exit-Code **und** Ausgabetext.
### Entscheidungsliste
| Frage | Herkunft | Antwort |
|---|---|---|
| Wie kommt `lo` im Namensraum hoch? | Quelle: Messung des Koordinators am 2026-10-09 (WSL Debian, Benutzer `claude`, util-linux aus Debian 13) | `unshare --map-current-user --net --keep-caps sh -c 'ip link set lo up && exec setpriv --inh-caps=-all --ambient-caps=-all -- <befehl>'` |
| Braucht es `--keep-caps`? | Annahme (abgeleitet: ohne vererbte Fähigkeiten verliert `sh` beim `exec` die Fähigkeiten aus dem Benutzer-Namensraum und `ip` scheitert) → **vom Executer zu messen**: Vorprobe 2 und Variante v4 | `--keep-caps` steht in Probe und Hauptaufruf |
| Welche Rechte hat der Läufer? | Quelle: Messung (uid 1000 wie außen, `CapEff` und `CapAmb` 0). `CapPrm` und `CapInh` 0: Annahme nach der Umrechnung bei `execve` in capabilities(7), nicht nachgelesen → Projektvorgabe, W3 prüft alle vier | W3 |
| Erreicht das SIGTERM von `timeout` den Läufer? | Projektvorgabe des Koordinators (exec-Kette). Dass `unshare` ohne `--fork` und `setpriv` per `exec` weitergeben und `timeout` ohne `--foreground` auch seine Prozessgruppe trifft, ist aus dem Gedächtnis, nicht nachgelesen → W6 misst den Elternprozess des Läufers (`timeout`) und die Zeile `ABBRUCH: SIGTERM`, v2 zeigt W6 rot | `sh` geht per `exec setpriv`, `setpriv` per `exec` in `python3` über |
| Was ist ein Fehlschlag, was folgt? | Projektvorgabe des Koordinators | Werkzeug fehlt, oder die Probe endet ungleich 0, oder ihre Ausgabe ist nicht genau `lo-probe ok` → `MUTANTEN_LAUF_FEHLER: …`, Exit 2. Nie ein Lauf ohne Netzsperre oder ohne `lo` |
| Wo steht die Vorprüfung? | Projektvorgabe (Planer) | direkt nach der bestehenden `unshare`-Prüfung, vor `wslpath`, `mktemp` und der Modus-Zeile; keine `EXIT`-Zeile, wie bei fehlendem `unshare` |
| Wird `python3` mitgeprüft? | Projektvorgabe (Planer), aus der Angriffsliste „fehlender Interpreter“ | ja, Fall W9 |
| Wie wandelt das Skript Pfade? | Quelle: `mutanten_lauf_kopie.sh`, Zeilen 58 und 59: `wslpath -u` auf beide Argumente | Der Treiber gibt ihm `wslpath -w` seiner `/tmp`-Ordner |
| Kommt `wslpath -u` von der Ausgabe von `wslpath -w` zurück? | Annahme → **vom Executer zu messen**: Vorprobe 1; der Treiber prüft die Rundreise selbst vor dem ersten Fall und endet sonst mit `TREIBER_FEHLER`, Exit 2 | Haltepunkt, falls nicht |
| Welche Befehle braucht das Skript über `PATH`? | Quelle: Skripttext nach der Änderung | `unshare wslpath mktemp cp chmod find rm timeout sh true ip setpriv python3`; W4, W5, W9 fahren erst einen Kontrolllauf mit allen als Attrappen, dann einen ohne den einen |
| Ist `127.0.0.1` im Namensraum das `lo` des Rechners? | Annahme (eigenes Loopback je Netz-Namensraum, aus dem Gedächtnis) → W8 misst mit einem Dienst des Treibers, v5 zeigt W8 rot | getrennt |
| Darf ein Test ins Netz? | Projektvorgabe des Koordinators | Nur `127.0.0.1`. `192.0.2.1` (TEST-NET-1) nur im Namensraum; der Test prüft vorher, dass es keine Route außer über `lo` gibt, und scheitert ohne Namensraum dort, vor jedem `connect` |
| Zeiten für W6 | Projektvorgabe (Planer), gerechnet | `BUDGET_S=10`, `ZEITLIMIT_S=6`, zwei hängende Mutanten: Die Grenze trifft den ersten oder den zweiten, die Prüfungen hängen nicht davon ab |
### Angriffsliste
| Klasse | Angriff | Fall | Erwartung mit dem neuen Skript |
|---|---|---|---|
| fehlt | `ip` nicht im `PATH` | W4 | `MUTANTEN_LAUF_FEHLER: ip fehlt`, Exit 2, keine Modus-Zeile, keine `EXIT`-Zeile, keine Kopie unter `/tmp` |
| fehlt | `setpriv` nicht im `PATH` | W5 | ebenso mit `setpriv` |
| fehlt (Interpreter) | `python3` nicht im `PATH` | W9 | ebenso mit `python3` |
| fehlt | `--keep-caps` fehlt, `lo` kommt nicht hoch | Variante v4 | Vorprüfung meldet `nicht nutzbar`, alle Fälle rot |
| fehlt | Vorprüfung fehlt | Variante v3 | W4, W5, W9 rot |
| zusätzlich | Fähigkeiten bleiben beim Läufer | Variante v1 | W3 rot |
| zusätzlich | Zwischenschale bleibt stehen | Variante v2 | W6 rot (Elternprozess `sh` statt `timeout`) |
| Verweis ins Leere | Verbindung nach außen | W2 | `ENETUNREACH`, kein Zeitlimit-Fehler |
| Verweis ins Leere | Dienst des Rechners auf `127.0.0.1` | W8 | unerreichbar, beim Dienst kommt keine Verbindung an |
| Umgebung | kein Netz-Namensraum | Variante v5 | W2 und W8 rot, ohne Verbindung nach außen |
| Umgebung | uid innen anders als außen | W3 | uid wie außen |
| Umgebung | `PATH` ohne `/usr/sbin` und `/sbin` | W4, W5, W9 | Attrappen-Ordner; der Kontrolllauf mit allen Attrappen ist grün |
| Umgebung | Windows-Pfade (UNC aus `wslpath -w`) | alle | Rundreise vor dem ersten Fall |
| Wertebereich | `BUDGET_S` klein | W6 | `ZEITGRENZE`, Exit 124, `ABBRUCH: SIGTERM`, kein Prozess des Falls, keine Temp-Reste |
| Reihenfolge | Die Grenze trifft den ersten oder den zweiten hängenden Mutanten | W6 | Prüfungen unabhängig davon, welcher |
| Reihenfolge | Server-Test vorn oder hinten | — | nicht anwendbar: Der Namensraum gilt für den ganzen Läuferprozess, nicht je Test |
| leer, null, falscher Typ, doppelt | — | — | nicht anwendbar: Das Skript liest keine neue Datenstruktur; Argumente und `mutanten.toml` prüfen Skript und Läufer wie bisher (Läufer-Treiber, Fälle 19 bis 27) |
| Rückwärtsverträglichkeit | Grundlinie ohne Server, Modus `voll` und `muster` | W7 | wie bisher |
Nach jedem Fall prüft der Treiber zusätzlich, dass die übergebene Kopie byte-gleich ist und kein Ordner `/tmp/plankritik-mut-*` liegen blieb.
## Zieldateien
Pfade relativ zu `D:/Claude/.vault-tools/plan-kopie/`:
- `mutanten_lauf_kopie.sh` (geändert)
- `mutanten_lauf_kopie_treiber.py` (neu)
- `LIESMICH.md` (geändert)
- `mutanten_lauf_ENTWURF.md` (geändert)
## Verbote
- Nur die vier Zieldateien anlegen oder ändern. `mutanten_lauf.py`, `mutanten_lauf_treiber.py`, `.claude/workflows/*` und alles unter `D:/Claude/.arbeitsbaeume/` bleiben unberührt.
- Hilfsdateien nur im Rot-Ordner `C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot` (Git-Bash: `/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot`, WSL: `/mnt/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot`).
- Kein `git add`, kein `git commit`.
- Jeden Block byte-genau übernehmen: keine Umformulierung, keine Ersetzung typografischer Zeichen, keine zusätzlichen Leerzeilen. Aus-Block als `old_string`, wird-Block als `new_string` im Edit-Werkzeug; die neue Datei mit dem Write-Werkzeug.
- Kein `sed`, kein Suchen-und-Ersetzen über ganze Dateien.
- WSL-Aufrufe nur aus der PowerShell. Aus der Git-Bash macht die Pfadumwandlung aus `/mnt/...` einen Windows-Pfad.
- Kein Netz außer `127.0.0.1`. Den Test aus W2 nie von Hand außerhalb des Treibers fahren.
- Den Treiber nicht fahren, während eine Plankritik läuft; er zählt neue Ordner `/tmp/plankritik-mut-*`.
- Steht ein Aus-Block nicht genau einmal in seiner Datei, weicht eine Ausgabe von der Erwartung ab oder widerspricht etwas im Plan einem anderen Teil: anhalten und melden. Den Treiber nie anpassen, damit ein Fall grün wird.
- `python -m py_compile` legt `__pycache__` an; den Ordner danach löschen.
## Schritt 0 — Rot-Ordner und Vorproben
In der Git-Bash den Rot-Ordner anlegen und das Skript aus dem letzten Commit hineinlegen. `git show` schreibt dort die Bytes unverändert, die PowerShell würde Zeilenenden umschreiben:
```bash
# Rot-Ordner neu anlegen, Skript aus HEAD als Wegwerfdatei hineinlegen
R=/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot
rm -rf "$R" && mkdir -p "$R"
git -C /d/Claude show HEAD:.vault-tools/plan-kopie/mutanten_lauf_kopie.sh > "$R/mutanten_lauf_kopie_alt.sh"
# vor der Umsetzung muss die Wegwerfdatei gleich der Arbeitsdatei sein und ohne CR
cmp "$R/mutanten_lauf_kopie_alt.sh" /d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie.sh && echo gleich
tr -cd '\r' < "$R/mutanten_lauf_kopie_alt.sh" | wc -c
```
Erwartet: `gleich` und `0`.
Leg mit dem Write-Werkzeug die Datei `C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot\vorprobe.sh` an.
Inhalt:
```bash
#!/usr/bin/env bash
# Vorproben zum Plan 2026-10-09-mutanten-kopie-loopback; legt nur einen leeren Temp-Ordner an und loescht ihn
# 1: Rundreise eines /tmp-Ordners ueber wslpath -w und wslpath -u
d=$(mktemp -d /tmp/mkopie-vorprobe-XXXXXX)
w=$(wslpath -w "$d")
z=$(wslpath -u "$w")
if [ "$z" = "$d" ]; then echo "rundreise: gleich"; else echo "rundreise: anders: $d -> $w -> $z"; fi
rmdir "$d"
# 2: ohne --keep-caps darf ip lo nicht hochfahren
unshare --map-current-user --net sh -c 'ip link set lo up; echo "ohne keep-caps: rc=$?"'
# 3: Startkette wie vom Koordinator gemessen, mit seiner Probe
unshare --map-current-user --net --keep-caps sh -c 'ip link set lo up && exec setpriv --inh-caps=-all --ambient-caps=-all -- python3 "$1"' sh /mnt/d/Claude/.arbeitsbaeume/rb-proben/lo_probe.py
# 4: Fundorte der Werkzeuge
for n in ip setpriv python3 unshare timeout sh wslpath; do echo "$n: $(command -v "$n")"; done
```
Dann in der Git-Bash auf CR prüfen und in der PowerShell fahren:
```bash
# Vorprobe darf kein CR tragen
tr -cd '\r' < /c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot/vorprobe.sh | wc -c
```
```powershell
# Vorproben in WSL
wsl -d Debian -- bash /mnt/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot/vorprobe.sh
"exit=$LASTEXITCODE"
```
Erwartet:
```text
rundreise: gleich
RTNETLINK answers: Operation not permitted
ohne keep-caps: rc=2
connect ok
ip: <pfad>
setpriv: <pfad>
python3: <pfad>
unshare: <pfad>
timeout: <pfad>
sh: <pfad>
wslpath: <pfad>
exit=0
```
- `rundreise: gleich` und `connect ok` sind Pflicht. Weicht eine der beiden ab: anhalten und melden.
- Zeilen 2 und 3 sind gerechnet, nicht gemessen. Pflicht ist nur `rc=` ungleich 0; den Wortlaut beider Zeilen meldest du.
- Jede Fundort-Zeile muss einen Pfad tragen; die Pfade meldest du.
## Schritt 1 — Tests zuerst: Treiber
### `mutanten_lauf_kopie_treiber.py`, ganze Datei
Die ganze Datei ist neu:
```python
#!/usr/bin/env python3
"""Treiber fuer mutanten_lauf_kopie.sh: Faelle W1 bis W9 (lo im Netz-Namensraum).
Zweck: Baut je Fall unter /tmp ein kleines Projekt (kopie/) und einen
Planordner (plan/) mit mutanten.toml, ruft mutanten_lauf_kopie.sh mit beiden
Ordnern als Windows-Pfaden auf, wie plankritik.js es tut, und vergleicht
Exit-Code und Ausgabezeilen mit der Erwartung aus dem Plan
docs/plans/2026-10-09-mutanten-kopie-loopback.md. Jeder Fall raeumt seine
Ordner selbst auf. Nach jedem Fall prueft der Treiber, dass die uebergebene
Kopie byte-gleich ist und kein Ordner /tmp/plankritik-mut-* liegen blieb.
Aufruf (in WSL): python mutanten_lauf_kopie_treiber.py [--skript <pfad>]
Ohne --skript faehrt er mutanten_lauf_kopie.sh neben sich, mit --skript eine
andere Fassung (Regressionsnachweis: Stand aus git show HEAD, kaputte
Varianten). W7 ist die Grundlinie und laeuft zuerst; ist sie rot, messen die
anderen Faelle wenig.
Pfade: Das Skript wandelt seine Argumente mit wslpath -u um. Der Treiber gibt
ihm deshalb wslpath -w seiner /tmp-Ordner und prueft vor dem ersten Fall,
dass die Rundreise denselben Pfad ergibt.
Netz: Die Tests verbinden sich nur mit 127.0.0.1 und, nur im Netz-Namensraum
des Skripts, mit 192.0.2.1 (TEST-NET-1). Der Test dazu prueft zuerst, dass es
keine Route ausser ueber lo gibt; ohne Namensraum scheitert er dort, bevor er
eine Verbindung versucht.
Nicht im Treiber: ob plankritik.js das Skript so aufruft wie hier
(MSYS_NO_PATHCONV, cygpath -m -l).
"""
import argparse
import hashlib
import os
import shutil
import socket
import subprocess
import sys
import tempfile
import time
import uuid
from pathlib import Path
HIER = Path(__file__).resolve().parent
SKRIPT_STANDARD = HIER / "mutanten_lauf_kopie.sh"
INTERPRETER = "/home/claude/venvs/trading/bin/python"
BASH = shutil.which("bash")
# Was mutanten_lauf_kopie.sh ueber PATH aufruft (Stand 2026-10-09); daraus
# bauen W4, W5 und W9 ihre Attrappen-Ordner.
WERKZEUGE = (
"unshare", "wslpath", "mktemp", "cp", "chmod", "find", "rm", "timeout",
"sh", "true", "ip", "setpriv", "python3",
)
FEHLERLISTE = []
def melde(fallname, ok, zusatz=""):
if ok:
print("{0} ok".format(fallname))
else:
print("{0} FEHLER: {1}".format(fallname, zusatz))
FEHLERLISTE.append(fallname)
def kurz(ergebnis):
return "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr)
def sha_von(pfad):
return hashlib.sha256(pfad.read_bytes()).hexdigest()
def plankritik_ordner():
return set(Path("/tmp").glob("plankritik-mut-*"))
def windows_pfad(pfad):
ergebnis = subprocess.run(["wslpath", "-w", str(pfad)], capture_output=True, text=True, timeout=30)
return ergebnis.stdout.strip()
def linux_pfad(pfad):
ergebnis = subprocess.run(["wslpath", "-u", pfad], capture_output=True, text=True, timeout=30)
return ergebnis.stdout.strip()
# --- Projektbausteine -------------------------------------------------------
GRUNDDATEIEN = {
"pytest.ini": "[pytest]\n",
"paket/__init__.py": "",
"tests/__init__.py": "",
}
MODUL = (
"def f1(x):\n"
" if x > 0:\n"
" return True\n"
" return False\n"
)
TEST_MODUL = (
"from paket.modul import f1\n"
"\n"
"\n"
"def test_f1_positiv():\n"
" assert f1(5) is True\n"
)
DIENST = (
"import socket\n"
"\n"
"\n"
"def antwort_ueber_lo(nachricht):\n"
" server = socket.socket()\n"
" server.bind((\"127.0.0.1\", 0))\n"
" server.listen(1)\n"
" client = socket.create_connection(server.getsockname(), timeout=5)\n"
" verbindung, _ = server.accept()\n"
" client.sendall(nachricht)\n"
" daten = verbindung.recv(1024)\n"
" for sock in (client, verbindung, server):\n"
" sock.close()\n"
" return daten\n"
)
TEST_DIENST = (
"from paket.dienst import antwort_ueber_lo\n"
"\n"
"\n"
"def test_echo_ueber_lo():\n"
" assert antwort_ueber_lo(b\"hallo\") == b\"hallo\"\n"
)
NETZ = (
"AUSSEN = (\"192.0.2.1\", 9)\n"
"\n"
"\n"
"def aussen_adresse():\n"
" return AUSSEN\n"
)
TEST_NETZ = (
"import errno\n"
"import socket\n"
"\n"
"import pytest\n"
"\n"
"from paket.netz import aussen_adresse\n"
"\n"
"\n"
"def test_keine_route_nach_aussen():\n"
" with open(\"/proc/net/route\", encoding=\"ascii\") as datei:\n"
" zeilen = datei.read().splitlines()[1:]\n"
" schnittstellen = [zeile.split()[0] for zeile in zeilen if zeile.strip()]\n"
" assert all(name == \"lo\" for name in schnittstellen), \"Routen ausser lo: {0}\".format(schnittstellen)\n"
" sock = socket.socket()\n"
" sock.settimeout(5)\n"
" try:\n"
" sock.connect(aussen_adresse())\n"
" except OSError as exc:\n"
" assert exc.errno == errno.ENETUNREACH, \"erwartet ENETUNREACH: {0!r}\".format(exc)\n"
" else:\n"
" pytest.fail(\"Verbindung nach aussen gelungen\")\n"
" finally:\n"
" sock.close()\n"
)
RECHTE = (
"def status_feld(name):\n"
" with open(\"/proc/self/status\", encoding=\"utf-8\", errors=\"replace\") as datei:\n"
" for zeile in datei:\n"
" schluessel, _, wert = zeile.partition(\":\")\n"
" if schluessel == name:\n"
" return wert.strip()\n"
" raise KeyError(name)\n"
)
HAENGEN = (
"def schritt_a(n):\n"
" return n + 1\n"
"\n"
"\n"
"def schritt_b(n):\n"
" return n + 2\n"
)
TEST_HAENGEN = (
"from paket.haengen import schritt_a, schritt_b\n"
"\n"
"\n"
"def test_schritt_a():\n"
" assert schritt_a(1) == 2\n"
"\n"
"\n"
"def test_schritt_b():\n"
" assert schritt_b(1) == 3\n"
)
SCHLEIFE = " while True:\n pass\n"
PRUEFZEILE_AUSSEN = " assert info.value.errno in (errno.ECONNREFUSED, errno.ENETUNREACH), repr(info.value)\n"
def text_test_rechte(uid):
return (
"from paket.rechte import status_feld\n"
"\n"
"UID_AUSSEN = \"" + str(uid) + "\"\n"
"\n"
"\n"
"def test_keine_faehigkeiten_gleiche_uid():\n"
" for feld in (\"CapInh\", \"CapPrm\", \"CapEff\", \"CapAmb\"):\n"
" wert = status_feld(feld)\n"
" assert int(wert, 16) == 0, \"{0} ist {1}\".format(feld, wert)\n"
" assert status_feld(\"Uid\").split()[0] == UID_AUSSEN\n"
)
def text_test_aussen(port):
return (
"import errno\n"
"import socket\n"
"\n"
"import pytest\n"
"\n"
"PORT_AUSSEN = " + str(port) + "\n"
"\n"
"\n"
"def test_dienst_des_rechners_unerreichbar():\n"
" sock = socket.socket()\n"
" sock.settimeout(5)\n"
" try:\n"
" with pytest.raises(OSError) as info:\n"
" sock.connect((\"127.0.0.1\", PORT_AUSSEN))\n"
" finally:\n"
" sock.close()\n"
+ PRUEFZEILE_AUSSEN
)
def kopf():
return "format = 1\npython = \"{0}\"\n\n".format(INTERPRETER)
def mutant(id_, datei, aus, wird, rot_bei):
return (
"[[mutant]]\n"
"id = \"" + id_ + "\"\n"
"datei = \"" + datei + "\"\n"
"aus = '''\n" + aus + "'''\n"
"wird = '''\n" + wird + "'''\n"
"rot_bei = [\"" + rot_bei + "\"]\n"
"\n"
)
class Fall:
"""Ein Fall unter /tmp: kopie/ mit den Projektdateien, plan/ mit mutanten.toml."""
def __init__(self, dateien, mutanten):
self.basis = Path(tempfile.mkdtemp(prefix="mkopie_treiber_"))
self.kopie = self.basis / "kopie"
self.plan = self.basis / "plan"
self.plan.mkdir()
alle = dict(GRUNDDATEIEN)
alle.update(dateien)
self.snap = {}
for relpfad, inhalt in alle.items():
pfad = self.kopie / relpfad
pfad.parent.mkdir(parents=True, exist_ok=True)
pfad.write_text(inhalt, encoding="utf-8")
self.snap[pfad] = sha_von(pfad)
(self.plan / "mutanten.toml").write_text(kopf() + mutanten, encoding="utf-8")
def pruefe_kopie(self, fallname):
for pfad, sha in self.snap.items():
if not pfad.exists() or sha_von(pfad) != sha:
melde(fallname + " (kopie)", False, "uebergebene Kopie veraendert: {0}".format(pfad))
return
def aufraeumen(self):
shutil.rmtree(self.basis, ignore_errors=True)
def starte_skript(skript, fall, modus="voll", pfad=None):
env = dict(os.environ)
if pfad is not None:
env["PATH"] = pfad
cmd = [BASH, str(skript), windows_pfad(fall.kopie), windows_pfad(fall.plan), modus]
return subprocess.run(cmd, capture_output=True, text=True, env=env, timeout=180)
# --- Faelle ----------------------------------------------------------------
def fahre_einfach(name, skript, dateien, mutanten, mutant_id):
fall = Fall(dateien, mutanten)
try:
ergebnis = starte_skript(skript, fall)
ok = (
ergebnis.returncode == 0
and "{0} OK".format(mutant_id) in ergebnis.stdout
and "EXIT mutanten_lauf: 0" in ergebnis.stdout
)
melde(name, ok, kurz(ergebnis))
fall.pruefe_kopie(name)
finally:
fall.aufraeumen()
def fall_einfach():
return Fall(
{"paket/modul.py": MODUL, "tests/test_modul.py": TEST_MODUL},
mutant("MW7", "paket/modul.py", " return True\n", " return False\n",
"tests/test_modul.py::test_f1_positiv"),
)
def fall_w7(skript):
# Grundlinie wie bisher: kein Server-Test, Modus voll und muster.
fall = fall_einfach()
try:
voll = starte_skript(skript, fall)
muster = starte_skript(skript, fall, modus="muster")
ok = (
voll.returncode == 0
and "modus: voll" in voll.stdout
and "MW7 OK" in voll.stdout
and "EXIT mutanten_lauf: 0" in voll.stdout
and muster.returncode == 0
and "modus: muster" in muster.stdout
and "EXIT mutanten_lauf: 0" in muster.stdout
)
melde("W7", ok, "voll " + kurz(voll) + "\nmuster " + kurz(muster))
fall.pruefe_kopie("W7")
finally:
fall.aufraeumen()
def fall_w1(skript):
# Ein Test startet einen Server auf 127.0.0.1 und tauscht Daten aus.
fahre_einfach(
"W1", skript,
{"paket/dienst.py": DIENST, "tests/test_dienst.py": TEST_DIENST},
mutant("MW1", "paket/dienst.py", " return daten\n", " return daten[::-1]\n",
"tests/test_dienst.py::test_echo_ueber_lo"),
"MW1",
)
def fall_w2(skript):
# Verbindung nach aussen scheitert mit ENETUNREACH; der Mutant zielt auf
# 127.0.0.1:9 und faellt nur, wenn lo oben ist (ECONNREFUSED).
fahre_einfach(
"W2", skript,
{"paket/netz.py": NETZ, "tests/test_netz.py": TEST_NETZ},
mutant("MW2", "paket/netz.py", "AUSSEN = (\"192.0.2.1\", 9)\n", "AUSSEN = (\"127.0.0.1\", 9)\n",
"tests/test_netz.py::test_keine_route_nach_aussen"),
"MW2",
)
def fall_w3(skript):
# CapInh, CapPrm, CapEff, CapAmb gleich 0 und dieselbe uid wie aussen.
fahre_einfach(
"W3", skript,
{"paket/rechte.py": RECHTE, "tests/test_rechte.py": text_test_rechte(os.getuid())},
mutant("MW3", "paket/rechte.py", " return wert.strip()\n", " return \"1\"\n",
"tests/test_rechte.py::test_keine_faehigkeiten_gleiche_uid"),
"MW3",
)
def baue_attrappen(ordner, ohne):
ordner.mkdir()
for name in WERKZEUGE:
if name != ohne:
os.symlink(shutil.which(name), ordner / name)
def fahre_ohne_werkzeug(name, skript, werkzeug):
# Erst ein Kontrolllauf mit allen Attrappen (muss gruen sein), dann einer
# ohne das eine Werkzeug (muss vor dem Kopieren mit Exit 2 enden).
fall = fall_einfach()
try:
alle = fall.basis / "attrappen_alle"
ohne = fall.basis / "attrappen_ohne"
baue_attrappen(alle, None)
baue_attrappen(ohne, werkzeug)
kontrolle = starte_skript(skript, fall, pfad=str(alle))
vorher = plankritik_ordner()
ergebnis = starte_skript(skript, fall, pfad=str(ohne))
neu = sorted(str(p) for p in plankritik_ordner() - vorher)
ok = (
kontrolle.returncode == 0
and "MW7 OK" in kontrolle.stdout
and ergebnis.returncode == 2
and "MUTANTEN_LAUF_FEHLER: {0} fehlt".format(werkzeug) in ergebnis.stdout
and "modus:" not in ergebnis.stdout
and "EXIT mutanten_lauf" not in ergebnis.stdout
and not neu
)
melde(name, ok, "kontrolle exit={0}, ohne {1} exit={2}, neue Kopien={3}\nkontrolle {4}\nohne {1} {5}".format(
kontrolle.returncode, werkzeug, ergebnis.returncode, neu, kurz(kontrolle), kurz(ergebnis)))
fall.pruefe_kopie(name)
finally:
fall.aufraeumen()
def fall_w4(skript):
fahre_ohne_werkzeug("W4", skript, "ip")
def fall_w5(skript):
fahre_ohne_werkzeug("W5", skript, "setpriv")
def fall_w9(skript):
fahre_ohne_werkzeug("W9", skript, "python3")
def prozesse_mit_marke(marke):
"""pid -> argv aller Prozesse, deren Umgebung die Marke traegt."""
gefunden = {}
for envdatei in Path("/proc").glob("[0-9]*/environ"):
try:
if marke.encode("ascii") not in envdatei.read_bytes():
continue
argv = (envdatei.parent / "cmdline").read_bytes().split(b"\0")
except OSError:
continue
gefunden[envdatei.parent.name] = [teil.decode("utf-8", "replace") for teil in argv if teil]
return gefunden
def elternname(pid):
try:
stat = Path("/proc/{0}/stat".format(pid)).read_text()
ppid = stat.rsplit(")", 1)[1].split()[1]
return Path("/proc/{0}/comm".format(ppid)).read_text().strip()
except (OSError, IndexError):
return None
def fall_w6(skript):
# Zeitgrenze: zwei haengende Mutanten, BUDGET_S 10, ZEITLIMIT_S 6. Waehrend
# des Laufs muss timeout der Elternprozess des Laeufers sein (keine
# Zwischenschale); danach lebt kein Prozess des Falls mehr.
fall = Fall(
{"paket/haengen.py": HAENGEN, "tests/test_haengen.py": TEST_HAENGEN},
mutant("MW6a", "paket/haengen.py", " return n + 1\n", SCHLEIFE, "tests/test_haengen.py::test_schritt_a")
+ mutant("MW6b", "paket/haengen.py", " return n + 2\n", SCHLEIFE, "tests/test_haengen.py::test_schritt_b"),
)
try:
wert = uuid.uuid4().hex
marke = "KOPIE_TREIBER_MARKE=" + wert
eigenes_tmpdir = fall.basis / "tmpdir"
eigenes_tmpdir.mkdir()
env = dict(os.environ)
env.update({
"BUDGET_S": "10",
"ZEITLIMIT_S": "6",
"TMPDIR": str(eigenes_tmpdir),
"KOPIE_TREIBER_MARKE": wert,
})
cmd = [BASH, str(skript), windows_pfad(fall.kopie), windows_pfad(fall.plan), "voll"]
proc = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
eltern = None
pytest_gesehen = False
for _ in range(12):
time.sleep(0.5)
for pid, argv in prozesse_mit_marke(marke).items():
if (argv and Path(argv[0]).name.startswith("python")
and any(teil.endswith("mutanten_lauf.py") for teil in argv)):
eltern = elternname(pid)
if "pytest" in argv:
pytest_gesehen = True
if eltern is not None and pytest_gesehen:
break
try:
stdout, stderr = proc.communicate(timeout=90)
except subprocess.TimeoutExpired:
proc.kill()
stdout, stderr = proc.communicate()
stderr += "\nTREIBER: Skript nach 90 s nicht beendet"
reste = prozesse_mit_marke(marke)
tmp_reste = sorted(p.name for p in eigenes_tmpdir.iterdir())
ok = (
proc.returncode == 124
and "ZEITGRENZE" in stdout
and "EXIT mutanten_lauf: 124" in stdout
and "ABBRUCH: SIGTERM" in stdout
and eltern == "timeout"
and pytest_gesehen
and not reste
and not tmp_reste
)
melde("W6", ok, "exit={0}, eltern des laeufers={1}, pytest gesehen={2}, reste={3}, tmp={4}\n{5}{6}".format(
proc.returncode, eltern, pytest_gesehen, reste, tmp_reste, stdout, stderr))
fall.pruefe_kopie("W6")
finally:
fall.aufraeumen()
def fall_w8(skript):
# Ein Dienst des Treibers auf 127.0.0.1 darf aus dem Namensraum nicht
# erreichbar sein, und bei ihm darf keine Verbindung ankommen.
dienst = socket.socket()
dienst.bind(("127.0.0.1", 0))
dienst.listen(5)
dienst.setblocking(False)
fall = None
try:
fall = Fall(
{"tests/test_aussen.py": text_test_aussen(dienst.getsockname()[1])},
mutant("MW8", "tests/test_aussen.py", PRUEFZEILE_AUSSEN, " assert False\n",
"tests/test_aussen.py::test_dienst_des_rechners_unerreichbar"),
)
ergebnis = starte_skript(skript, fall)
try:
verbindung, _ = dienst.accept()
verbindung.close()
angekommen = True
except BlockingIOError:
angekommen = False
ok = (
ergebnis.returncode == 0
and "MW8 OK" in ergebnis.stdout
and "EXIT mutanten_lauf: 0" in ergebnis.stdout
and not angekommen
)
melde("W8", ok, "beim Dienst des Rechners angekommen={0}, {1}".format(angekommen, kurz(ergebnis)))
fall.pruefe_kopie("W8")
finally:
dienst.close()
if fall is not None:
fall.aufraeumen()
FAELLE = [
("W7", fall_w7),
("W1", fall_w1),
("W2", fall_w2),
("W3", fall_w3),
("W4", fall_w4),
("W5", fall_w5),
("W6", fall_w6),
("W8", fall_w8),
("W9", fall_w9),
]
def main():
zerleger = argparse.ArgumentParser(description="Treiber fuer mutanten_lauf_kopie.sh (Faelle W1 bis W9)")
zerleger.add_argument("--skript", default=str(SKRIPT_STANDARD), help="zu pruefende Fassung des Skripts")
argumente = zerleger.parse_args()
skript = Path(argumente.skript)
print("skript: {0}".format(skript))
fehlend = [name for name in WERKZEUGE if shutil.which(name) is None]
if BASH is None:
fehlend.append("bash")
if fehlend:
print("TREIBER_FEHLER: Werkzeuge fehlen: {0}".format(", ".join(fehlend)))
return 2
if not skript.is_file():
print("TREIBER_FEHLER: Skript fehlt: {0}".format(skript))
return 2
if not Path(INTERPRETER).is_file():
print("TREIBER_FEHLER: Interpreter fehlt: {0}".format(INTERPRETER))
return 2
probe = Path(tempfile.mkdtemp(prefix="mkopie_treiber_rundreise_"))
try:
windows = windows_pfad(probe)
zurueck = linux_pfad(windows) if windows else ""
finally:
probe.rmdir()
if not windows or zurueck != str(probe):
print("TREIBER_FEHLER: wslpath-Rundreise: {0} -> {1} -> {2}".format(probe, windows, zurueck))
return 2
print("rundreise: {0} -> {1}".format(probe, windows))
for name, funktion in FAELLE:
vorher = plankritik_ordner()
try:
funktion(skript)
except Exception as exc:
melde(name, False, "Ausnahme: {0!r}".format(exc))
liegen_geblieben = sorted(str(p) for p in plankritik_ordner() - vorher)
if liegen_geblieben:
melde(name + " (aufraeumen)", False, "Kopien unter /tmp blieben liegen: {0}".format(liegen_geblieben))
print("gerechnete Fehler: {0}".format(len(FEHLERLISTE)))
return 0 if not FEHLERLISTE else 1
if __name__ == "__main__":
sys.exit(main())
```
Dann in der Git-Bash prüfen:
```bash
# Treiber kompilieren, nur ASCII, keine CR; __pycache__ danach weg
cd /d/Claude/.vault-tools/plan-kopie
python -m py_compile mutanten_lauf_kopie_treiber.py && echo kompiliert
rm -rf __pycache__
LC_ALL=C grep -cP '[^\x00-\x7F]' mutanten_lauf_kopie_treiber.py
tr -cd '\r' < mutanten_lauf_kopie_treiber.py | wc -c
```
Erwartet: `kompiliert`, dann `0`, dann `0`.
## Schritt 2 — Erwarteter Fehlschlag: Treiber gegen das Skript aus HEAD
```powershell
# Treiber in WSL gegen die Wegwerfdatei aus git show HEAD
wsl -d Debian -- timeout 900 /home/claude/venvs/trading/bin/python /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie_treiber.py --skript /mnt/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot/mutanten_lauf_kopie_alt.sh *> "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot\ausgabe-alt.txt"
"exit=$LASTEXITCODE"
Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot\ausgabe-alt.txt" -Pattern "^W\d.*FEHLER:|^gerechnete|^TREIBER_FEHLER"
```
Erwartet: `exit=1`, und die Treffer von `Select-String` enden auf diese Zeilenanfänge, in dieser Reihenfolge:
```text
W1 FEHLER: exit=2
W2 FEHLER: exit=1
W4 FEHLER: kontrolle exit=0, ohne ip exit=0, neue Kopien=[]
W5 FEHLER: kontrolle exit=0, ohne setpriv exit=0, neue Kopien=[]
W9 FEHLER: kontrolle exit=0, ohne python3 exit=127, neue Kopien=[]
gerechnete Fehler: 5
```
- **Pflicht ist W1 rot** (Grundlinie scheitert an `Errno 101`, der Läufer endet mit 2). Fehlt W1 unter den roten Fällen: anhalten und melden.
- Gerechnet, nicht gemessen: W2 rot über `MW2 LUECKE` (bei `lo` aus scheitert auch `127.0.0.1:9` mit `ENETUNREACH`); W4, W5, W9 rot, weil das alte Skript nicht prüft; der Wert `exit=127` bei W9 (`unshare` findet `python3` nicht). W3, W6, W7 und W8 grün.
- Weicht die **Menge** der roten Fälle ab: anhalten und die Ausgabe melden. Weicht nur ein gerechneter Wert hinter `FEHLER:` ab, meldest du ihn und machst weiter.
## Schritt 3 — Umsetzung: Skript
Wende alle Blöcke dieses Abschnitts in der Reihenfolge an.
### `./mutanten_lauf_kopie.sh`, Stelle 1 von 3
Aus:
```bash
# - chmod 700 auf die Kopie der Kopie direkt nach dem Kopieren.
set -u
```
wird:
```bash
# - chmod 700 auf die Kopie der Kopie direkt nach dem Kopieren.
#
# Nachbesserung 2026-10-09 (Paket RB3 der Trading Plattform, Plan
# docs/plans/2026-10-09-mutanten-kopie-loopback.md):
# - lo im Netz-Namensraum: Im frischen Namensraum war lo aus; jeder Test mit einem Server auf
# 127.0.0.1 scheiterte mit Errno 101, auch ohne Mutant. Jetzt startet unshare zusaetzlich mit
# --keep-caps, eine sh faehrt lo mit ip hoch, und setpriv wirft danach alle vererbbaren und
# ambienten Faehigkeiten ab und geht per exec in python3 ueber. Gemessen vom Koordinator am
# 2026-10-09 in WSL Debian als Benutzer claude: uid 1000 wie aussen, CapEff und CapAmb 0,
# Verbindung ueber 127.0.0.1 gelingt. 127.0.0.1 ist das lo des Namensraums, nicht das des
# Rechners; ein Dienst des Rechners auf 127.0.0.1 bleibt unerreichbar (Treiber-Fall W8).
# - exec-Kette: unshare, sh, setpriv und python3 sind derselbe Prozess, das SIGTERM von timeout
# trifft den Laeufer selbst; keine Zwischenschale bleibt stehen (Treiber-Fall W6).
# - Vorpruefung vor dem Kopieren: Fehlt ip, setpriv oder python3, kommt lo nicht hoch oder
# bleiben Faehigkeiten, endet das Skript mit MUTANTEN_LAUF_FEHLER und Exit 2. Ein Lauf ohne
# Netzsperre oder ohne lo ist nie die Ausweichloesung.
# - Treiber: mutanten_lauf_kopie_treiber.py, Faelle W1 bis W9.
set -u
```
### `./mutanten_lauf_kopie.sh`, Stelle 2 von 3
Aus:
```bash
echo "MUTANTEN_LAUF_FEHLER: unshare ohne Netz nicht moeglich"
exit 2
fi
```
wird:
```bash
echo "MUTANTEN_LAUF_FEHLER: unshare ohne Netz nicht moeglich"
exit 2
fi
# Vorpruefung lo (seit 2026-10-09): Werkzeuge da, lo kommt im eigenen Namensraum hoch, eine
# Verbindung ueber 127.0.0.1 gelingt, und nach setpriv sind CapEff und CapAmb 0. Scheitert
# etwas davon, endet das Skript hier, vor dem Kopieren.
for WERKZEUG in ip setpriv python3; do
if ! command -v "$WERKZEUG" >/dev/null 2>&1; then
echo "MUTANTEN_LAUF_FEHLER: $WERKZEUG fehlt (gebraucht fuer lo im Netz-Namensraum)"
exit 2
fi
done
LO_PROBE='import socket
server = socket.socket()
server.bind(("127.0.0.1", 0))
server.listen(1)
client = socket.create_connection(server.getsockname(), timeout=3)
client.close()
server.close()
felder = {}
for zeile in open("/proc/self/status", encoding="utf-8", errors="replace"):
schluessel, _, wert = zeile.partition(":")
felder[schluessel] = wert.strip()
if int(felder["CapEff"], 16) != 0 or int(felder["CapAmb"], 16) != 0:
raise SystemExit("Faehigkeiten nicht abgeworfen: CapEff " + felder["CapEff"] + ", CapAmb " + felder["CapAmb"])
print("lo-probe ok")'
LO_AUSGABE=$(timeout 30 unshare --map-current-user --net --keep-caps \
sh -c 'ip link set lo up && exec setpriv --inh-caps=-all --ambient-caps=-all -- python3 -c "$1"' sh "$LO_PROBE" 2>&1)
LO_EXIT=$?
if [ "$LO_EXIT" -ne 0 ] || [ "$LO_AUSGABE" != "lo-probe ok" ]; then
echo "MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht nutzbar (Exit $LO_EXIT): $LO_AUSGABE"
exit 2
fi
```
### `./mutanten_lauf_kopie.sh`, Stelle 3 von 3
Aus:
```bash
timeout -k 15 "$BUDGET_S" unshare --map-current-user --net \
python3 -u /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf.py \
```
wird:
```bash
# exec-Kette ohne Zwischenschale (seit 2026-10-09): unshare, sh, setpriv und python3 sind derselbe
# Prozess, das SIGTERM von timeout trifft den Laeufer selbst (Treiber-Fall W6).
timeout -k 15 "$BUDGET_S" unshare --map-current-user --net --keep-caps \
sh -c 'ip link set lo up || { echo "MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht hochgefahren"; exit 2; }; exec setpriv --inh-caps=-all --ambient-caps=-all -- "$@"' sh \
python3 -u /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf.py \
```
Danach in der Git-Bash und der PowerShell prüfen:
```bash
# nur ASCII, keine CR im Skript
cd /d/Claude/.vault-tools/plan-kopie
LC_ALL=C grep -cP '[^\x00-\x7F]' mutanten_lauf_kopie.sh
tr -cd '\r' < mutanten_lauf_kopie.sh | wc -c
```
```powershell
# Syntax des Skripts in WSL
wsl -d Debian -- bash -n /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie.sh
"exit=$LASTEXITCODE"
```
Erwartet: `0`, `0`, dann nur `exit=0`.
## Schritt 4 — Verifikation: Treiber gegen das neue Skript, dann gegen fünf Varianten
### Neues Skript
```powershell
# Treiber in WSL gegen das neue Skript
wsl -d Debian -- timeout 900 /home/claude/venvs/trading/bin/python /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie_treiber.py *> "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot\ausgabe-neu.txt"
"exit=$LASTEXITCODE"
Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot\ausgabe-neu.txt" -Pattern "^W\d.*FEHLER:|^gerechnete|^TREIBER_FEHLER|^rundreise"
```
Erwartet: `exit=0` und genau zwei Treffer, einer beginnt mit `rundreise: /tmp/mkopie_treiber_rundreise_`, der andere endet auf `gerechnete Fehler: 0`. Sonst anhalten und die ganze Datei `ausgabe-neu.txt` melden.
### Varianten bauen
Leg mit dem Write-Werkzeug die Datei `C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot\varianten.py` an.
Inhalt:
```python
# Baut fuenf kaputte Fassungen des neuen mutanten_lauf_kopie.sh fuer den Regressionsnachweis.
# Aufruf: python varianten.py <neues skript> <zielordner>
import sys
from pathlib import Path
quelle = Path(sys.argv[1])
ziel = Path(sys.argv[2])
text = quelle.read_bytes().decode("utf-8")
if "\r" in text:
raise SystemExit("Skript hat CR")
def ersetze(text, aus, wird, anzahl):
gefunden = text.count(aus)
if gefunden != anzahl:
raise SystemExit("{0!r}: {1} Treffer statt {2}".format(aus, gefunden, anzahl))
return text.replace(aus, wird)
HAUPT_EXEC = "exec setpriv --inh-caps=-all --ambient-caps=-all -- \"$@\"'"
START = "# Vorpruefung lo (seit 2026-10-09)"
ENDE = "nicht nutzbar (Exit $LO_EXIT): $LO_AUSGABE\"\n exit 2\nfi\n"
varianten = {}
# v1: python3 ohne setpriv, Faehigkeiten bleiben (erwartet rot: W3)
varianten["v1_ohne_setpriv"] = ersetze(text, HAUPT_EXEC, "exec \"$@\"'", 1)
# v2: setpriv ohne exec, sh bleibt als Zwischenschale stehen (erwartet rot: W6)
varianten["v2_zwischenschale"] = ersetze(
text, HAUPT_EXEC, "setpriv --inh-caps=-all --ambient-caps=-all -- \"$@\"; RC=$?; exit \"$RC\"'", 1)
# v3: ohne Vorpruefung (erwartet rot: W4, W5, W9)
if text.count(START) != 1 or text.count(ENDE) != 1:
raise SystemExit("Vorpruefung nicht eindeutig gefunden")
anfang = text.index(START)
ende = text.index(ENDE) + len(ENDE)
if ende <= anfang:
raise SystemExit("Ende der Vorpruefung vor ihrem Anfang")
varianten["v3_ohne_vorpruefung"] = text[:anfang] + text[ende:]
# v4: ohne --keep-caps in Probe und Hauptaufruf (erwartet rot: alle)
varianten["v4_ohne_keep_caps"] = ersetze(text, "--net --keep-caps", "--net", 2)
# v5: Hauptaufruf ohne unshare und ohne sh, also ohne Netz-Namensraum (erwartet rot: W2, W8)
zeilen = text.split("\n")
treffer = [i for i, z in enumerate(zeilen) if z.startswith("timeout -k 15 \"$BUDGET_S\" unshare ")]
if len(treffer) != 1 or not zeilen[treffer[0] + 1].lstrip().startswith("sh -c "):
raise SystemExit("Hauptaufruf nicht eindeutig gefunden")
i = treffer[0]
varianten["v5_ohne_namensraum"] = "\n".join(zeilen[:i] + ["timeout -k 15 \"$BUDGET_S\" " + chr(92)] + zeilen[i + 2:])
for name, inhalt in varianten.items():
(ziel / (name + ".sh")).write_bytes(inhalt.encode("utf-8"))
print(name)
```
```bash
# Varianten aus dem neuen Skript bauen
R=/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot
python "$R/varianten.py" /d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie.sh "$R"
```
Erwartet genau diese fünf Zeilen:
```text
v1_ohne_setpriv
v2_zwischenschale
v3_ohne_vorpruefung
v4_ohne_keep_caps
v5_ohne_namensraum
```
Endet das Skript mit einer Meldung statt der fünf Zeilen: anhalten und melden.
### Treiber gegen jede Variante
```powershell
# Treiber in WSL gegen jede kaputte Variante
foreach ($v in "v1_ohne_setpriv","v2_zwischenschale","v3_ohne_vorpruefung","v4_ohne_keep_caps","v5_ohne_namensraum") {
wsl -d Debian -- timeout 900 /home/claude/venvs/trading/bin/python /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie_treiber.py --skript "/mnt/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot/$v.sh" *> "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot\ausgabe-$v.txt"
"$v exit=$LASTEXITCODE"
Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot\ausgabe-$v.txt" -Pattern "^W\d.*FEHLER:|^gerechnete|^TREIBER_FEHLER"
}
Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot\ausgabe-v4_ohne_keep_caps.txt" -Pattern "nicht nutzbar" | Select-Object -First 1
```
Erwartet je Variante `exit=1`, und die `FEHLER`-Zeilen betreffen genau diese Fälle. Zeilen wie `W6 (aufraeumen) FEHLER:` oder `W6 (kopie) FEHLER:` zählen zu ihrem Fall:
| Variante | rote Fälle | was es belegt |
|---|---|---|
| `v1_ohne_setpriv` | W3 | W3 sieht Fähigkeiten, die beim Läufer bleiben |
| `v2_zwischenschale` | W6 | W6 sieht eine Schale zwischen `timeout` und Läufer |
| `v3_ohne_vorpruefung` | W4, W5, W9 | die Vorprüfung ist der Grund für Exit 2 ohne Kopie |
| `v4_ohne_keep_caps` | W1 bis W9, alle | ohne `--keep-caps` kommt `lo` nicht hoch, und das Skript läuft nicht ungesperrt weiter |
| `v5_ohne_namensraum` | W2, W8 | W2 und W8 sehen eine fehlende Netzsperre, ohne Verbindung nach außen |
Die letzte Zeile des Blocks zeigt eine Zeile `MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht nutzbar (Exit 2): RTNETLINK answers: Operation not permitted`. Gerechnet ist der Wortlaut nach `nicht nutzbar`; Pflicht ist nur die Zeile selbst. Den Wortlaut meldest du, er ist die Messung zur Frage „Braucht es `--keep-caps`?“.
Weicht die Menge der roten Fälle einer Variante ab: anhalten und die ganze Ausgabedatei der Variante melden. Nicht weiter zu Schritt 5.
## Schritt 5 — Doku
### `LIESMICH.md`, Stelle 1 von 3
Aus:
```markdown
created: 2026-09-17
updated: 2026-10-06
---
```
wird:
```markdown
created: 2026-09-17
updated: 2026-10-09
---
```
### `LIESMICH.md`, Stelle 2 von 3
Aus:
```markdown
jede Verbindung nach außen scheitert) und gibt die Ausgabe an die Linse `arithmetik`. Fehlt `unshare` ohne Netz, bricht das Skript vorher mit `MUTANTEN_LAUF_FEHLER` ab, statt ungesperrt zu laufen.
```
wird:
```markdown
jede Verbindung nach außen scheitert; seit 2026-10-09 mit `lo` oben, siehe Nachtrag unten) und gibt die Ausgabe an die Linse `arithmetik`. Fehlt `unshare` ohne Netz, fehlen `ip`, `setpriv` oder `python3` oder kommt `lo` im Namensraum nicht hoch, bricht das Skript vorher mit `MUTANTEN_LAUF_FEHLER` ab, statt ungesperrt oder ohne `lo` zu laufen.
```
### `LIESMICH.md`, Stelle 3 von 3
Aus:
```markdown
- Rohdaten: `AI/secondbrain/linux-haertung-after-install-memory/2026-10-06-c1-abschluss/roh/` (`02-*`, `03-gegenprobe/`, `07-*`, `08-gegenprobe2/` mit dem nachgebesserten Läufer, `09-*` für Nachbesserung 2).
```
wird:
```markdown
- Rohdaten: `AI/secondbrain/linux-haertung-after-install-memory/2026-10-06-c1-abschluss/roh/` (`02-*`, `03-gegenprobe/`, `07-*`, `08-gegenprobe2/` mit dem nachgebesserten Läufer, `09-*` für Nachbesserung 2).
Nachbesserung vom 2026-10-09 (Paket RB3 der Trading Plattform, Plan `docs/plans/2026-10-09-mutanten-kopie-loopback.md`): Im frischen Netz-Namensraum war `lo` aus. Jeder Test mit einem Server auf `127.0.0.1` scheiterte dort mit „Network is unreachable“ (`Errno 101`), auch ohne Mutant; die Mutanten T1 und T12 des Pakets liefen deshalb von Hand außerhalb des Namensraums. `mutanten_lauf_kopie.sh` startet den Läufer jetzt mit `unshare --map-current-user --net --keep-caps`, fährt `lo` mit `ip` hoch, wirft mit `setpriv` alle vererbbaren und ambienten Fähigkeiten ab und geht per `exec` in `python3` über, ohne Zwischenschale. Eine Vorprüfung vor dem Kopieren verlangt `ip`, `setpriv` und `python3`, ein nutzbares `lo` mit einer Verbindung über `127.0.0.1` und `CapEff` und `CapAmb` gleich 0; sonst endet das Skript mit `MUTANTEN_LAUF_FEHLER` und Exit 2. `127.0.0.1` ist im Namensraum dessen eigenes `lo`, ein Dienst des Rechners bleibt unerreichbar. Belegt am selben Tag:
- `mutanten_lauf_kopie_treiber.py` ruft das Skript wie die Plankritik mit Windows-Pfaden auf und fährt neun Fälle: Server auf `127.0.0.1` (W1), keine Route nach außen und `ENETUNREACH` (W2), uid wie außen und keine Fähigkeiten (W3), `ip`, `setpriv` oder `python3` fehlen (W4, W5, W9), Zeitgrenze mit `timeout` als Elternprozess des Läufers und ohne Prozessreste (W6), Grundlinie ohne Server in beiden Modi (W7), Dienst des Rechners unerreichbar (W8). Neuer Stand ohne Fehler.
- Gegen das Skript vor der Änderung rot: W1, W2, W4, W5, W9. Gegen kaputte Varianten genau an ihrem Fall: ohne `setpriv` W3, `setpriv` ohne `exec` W6, ohne Vorprüfung W4, W5 und W9, ohne `--keep-caps` alle (die Vorprüfung meldet `Operation not permitted`), ohne Namensraum W2 und W8.
- Nicht abgeworfen und nicht geprüft: die Begrenzungsmenge (`CapBnd`) und `no_new_privs`. `plankritik.js` führt eine Vorprüfung mit `MUTANTEN_LAUF_FEHLER` wie bisher bei fehlendem `unshare` als `ohne_ergebnis`, ohne den Text der Meldung.
```
### `mutanten_lauf_ENTWURF.md`, Stelle 1 von 2
Aus:
```markdown
created: 2026-09-27
updated: 2026-10-06
---
```
wird:
```markdown
created: 2026-09-27
updated: 2026-10-09
---
```
### `mutanten_lauf_ENTWURF.md`, Stelle 2 von 2
Aus:
```markdown
Fehlt `unshare` ohne Netz, bricht der Wrapper vorher mit `MUTANTEN_LAUF_FEHLER: unshare ohne Netz nicht moeglich` ab, statt einen ungesperrten Lauf durchzulassen.
```
wird:
```markdown
Fehlt `unshare` ohne Netz, bricht der Wrapper vorher mit `MUTANTEN_LAUF_FEHLER: unshare ohne Netz nicht moeglich` ab, statt einen ungesperrten Lauf durchzulassen. **Seit 2026-10-09 (Paket RB3 der Trading Plattform):** `lo` ist im Namensraum oben. Der Wrapper startet `unshare … --keep-caps`, fährt `lo` mit `ip` hoch und geht über `setpriv` (alle vererbbaren und ambienten Fähigkeiten abgeworfen) per `exec` in den Läufer über. Eine Vorprüfung verlangt `ip`, `setpriv`, `python3`, ein nutzbares `lo` und `CapEff` und `CapAmb` gleich 0, sonst `MUTANTEN_LAUF_FEHLER` und Exit 2. Eine Verbindung auf `127.0.0.1` erreicht damit Dienste im Namensraum, nicht die des Rechners; nach außen bleibt es bei errno 101. Treiber: `mutanten_lauf_kopie_treiber.py` (W1 bis W9), Plan `docs/plans/2026-10-09-mutanten-kopie-loopback.md`.
```
## Schritt 6 — Abschlussprüfung
```bash
# keine CR in den vier Zieldateien, nichts ausserhalb der Zieldateien geaendert
cd /d/Claude/.vault-tools/plan-kopie
for f in mutanten_lauf_kopie.sh mutanten_lauf_kopie_treiber.py LIESMICH.md mutanten_lauf_ENTWURF.md; do printf '%s %s\n' "$f" "$(tr -cd '\r' < "$f" | wc -c)"; done
git -C /d/Claude status --porcelain -- .vault-tools
```
Erwartet: je Datei die Zahl `0`. `git status` zeigt genau diese fünf Zeilen, in beliebiger Reihenfolge:
```text
M .vault-tools/plan-kopie/LIESMICH.md
M .vault-tools/plan-kopie/mutanten_lauf_ENTWURF.md
M .vault-tools/plan-kopie/mutanten_lauf_kopie.sh
?? .vault-tools/plan-kopie/docs/plans/2026-10-09-mutanten-kopie-loopback.md
?? .vault-tools/plan-kopie/mutanten_lauf_kopie_treiber.py
```
Den Rot-Ordner lässt du stehen; der Koordinator legt die Ausgabedateien ab und löscht ihn.
## Bericht
Melde:
- je Zieldatei die Zahl der angewendeten Aus/wird-Paare und ob die neue Datei geschrieben ist;
- die Ausgaben aus Schritt 0 (Vorproben im Wortlaut), Schritt 2, Schritt 3 und Schritt 4 (alle `Select-String`-Treffer im Wortlaut), Schritt 6;
- jede Abweichung vom Plan, jeden gerechneten Wert, der anders ausfiel, und jede getroffene Annahme. „Keine Abweichungen“ nur, wenn es keine gab.
## Fertig heißt (Koordinator)
1. Die vier Zieldateien sind byte-gleich mit der Kopie, auf die dieser Plan per `plan_materialisiere.py` angewendet wurde (`cmp`), und sonst ist unter `.vault-tools/` nichts geändert.
2. Treiber in WSL gegen das neue Skript: Exit 0, `gerechnete Fehler: 0`, selbst gefahren.
3. Rot-Nachweis selbst gesehen: Skript aus HEAD rot mindestens bei W1, gemeldete Menge W1, W2, W4, W5, W9; die fünf Varianten genau an ihren Fällen laut Tabelle in Schritt 4. Die Ausgabedateien aus dem Rot-Ordner liegen als Rohdaten abgelegt.
4. Der Lauf mit echten Daten (unten) ist gefahren, T1 und T12 stehen auf `OK`.
5. Ein `pruefer` hat Skript und Treiber gegen diesen Plan gelesen; kein offener blockierender oder wichtiger Befund.
## Lauf mit echten Daten
- **Datensatz:** Paket RB3 der Trading Plattform, Kopie `D:/Claude/.arbeitsbaeume/rb3d-kopie`, Planordner `D:/Claude/.arbeitsbaeume/trading-plattform-rb3/docs/plans/2026-10-08-paket-rb3-totmann-ping` (dieselben wie in `rb3d_mutanten.sh`). Die Mutanten der `mutanten.toml` passen nur zum Stand in `rb3d-kopie`, nicht zu `rb3b-kopie` aus `rb3_mutanten.sh` (berichtigt in Nachbesserung 1). Fehlt die Kopie, vorher neu bauen.
- **Befehl** (PowerShell, im Hintergrund mit Zeitlimit des Werkzeugs bis 3600000; `BUDGET_S` hoch, weil RB3 mit `--zeitlimit 300` lief):
```powershell
# RB3 durch das neue Skript, Aufrufform wie in plankritik.js
wsl -d Debian -- env BUDGET_S=3000 ZEITLIMIT_S=300 timeout 3300 bash /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie.sh "D:/Claude/.arbeitsbaeume/rb3d-kopie" "D:/Claude/.arbeitsbaeume/trading-plattform-rb3/docs/plans/2026-10-08-paket-rb3-totmann-ping" voll *> "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot\rb3.txt"
"exit=$LASTEXITCODE"
```
- **Auffallen müsste:** T1 und T12 stehen auf `OK`, ohne `Errno 101` in der Grundlinie. Die übrigen Mutanten haben denselben Status wie in Teil A von `rb3d_mutanten.sh`. Keine Zeile `MUTANTEN_LAUF_FEHLER`, keine Zeile `ZEITGRENZE`. Steht ein Mutant anders als in Teil A, hat die neue Startkette das Verhalten eines Tests verändert.
- **Gegenprobe Rückwärtsverträglichkeit:** eine der sechs `mutanten.toml` aus der Gegenprobe vom 2026-10-06 (LIESMICH, `03-gegenprobe/`) durch altes und neues Skript; gleicher Exit-Code und gleicher Status je Mutant. Welche, entscheidet der Koordinator.
## Risiken
1. **Rundreise der Pfade.** Gibt `wslpath -u` die Ausgabe von `wslpath -w` nicht zurück, endet der Treiber vor dem ersten Fall mit `TREIBER_FEHLER: wslpath-Rundreise`. Merkmal schon in Vorprobe 1. Dann muss der Treiber seine Ordner unter dem Windows-Temp anlegen; das ist eine neue Planfassung.
2. **Attrappen-Ordner.** `wslpath` hängt über einen Verweis an `/init`, `python3` an `/usr/bin/python3.<n>`. Folgt einer davon dem Verweis nicht, scheitert der Kontrolllauf in W4, W5 und W9. Merkmal: `kontrolle exit=` ungleich 0.
3. **Prozesse im Benutzer-Namensraum unlesbar.** Liest der Treiber `/proc/<pid>/environ` der Prozesse im Namensraum nicht, findet W6 weder Läufer noch pytest. Merkmal: `eltern des laeufers=None, pytest gesehen=False`; dann wäre auch `reste=` ohne Aussage, deshalb verlangt W6 beides.
4. **Gleichzeitige Plankritik.** Legt eine Plankritik während des Treiberlaufs `/tmp/plankritik-mut-*` an, meldet der Treiber `(aufraeumen)` für den gerade laufenden Fall. Der Treiber löscht fremde Ordner nicht.
5. **Langsamer Rechner in W6.** Braucht die Grundlinie länger als etwa vier Sekunden, trifft die Grenze schon den ersten hängenden Mutanten. Die Prüfungen hängen nicht davon ab, welcher es ist.
6. **Fail-closed trifft bisher laufende Projekte.** Fehlt auf einem Rechner `iproute2` oder `setpriv`, bekam die Plankritik bisher einen Mutantenlauf, jetzt `mutanten_status: ohne_ergebnis`. Merkmal: Log der Plankritik „Hilfsagent ohne verwertbares Ergebnis“. Das ist gewollt.
7. **Begrenzungsmenge und `no_new_privs`.** Nicht angefasst. Ein Programm mit Datei-Fähigkeiten bekäme im Benutzer-Namensraum wieder welche; die Netzsperre bleibt davon unberührt, geprüft ist das nicht.
## Getroffene Annahmen
- Der Treiber legt seine Ordner unter `/tmp` an und übergibt `wslpath -w` dieser Ordner (UNC-Pfad); das Skript wandelt mit `wslpath -u` zurück. Die Rundreise ist ungemessen und deshalb Vorprobe 1 und Startprüfung des Treibers.
- `python3` steht zusätzlich zu `ip` und `setpriv` in der Werkzeugprüfung (W9), aus der Angriffsliste.
- Über die Mindestliste hinaus gibt es W8 (Dienst des Rechners unerreichbar) und W9 (`python3` fehlt).
- W2 prüft „keine Netzsperre“ über die Routingtabelle (`/proc/net/route`) statt über eine Verbindung. So ist W2 ohne Namensraum rot, ohne dass ein Paket hinausgeht; Variante v5 zeigt das.
- W3 prüft neben `CapEff` und `CapAmb` auch `CapPrm` und `CapInh` (gerechnet 0).
- Die Vorprüfung gibt wie die bestehende `unshare`-Prüfung keine `EXIT`-Zeile aus.
- Der Regressionsnachweis umfasst neben dem Lauf gegen HEAD fünf kaputte Varianten, damit jeder neue Fall einmal rot gesehen ist.
- Der Entwurf `mutanten_lauf_ENTWURF.md` beschreibt die Netzsperre des Wrappers und wird mitgezogen, obwohl der Auftrag nur LIESMICH und Kopfkommentar nennt.
- Ein Plan mit einem Task, als Einzeldatei wie die Pläne daneben.
## Offene Punkte
- **`plankritik.js` verliert die Meldung.** Endet das Skript mit `MUTANTEN_LAUF_FEHLER` ohne `EXIT`-Zeile, setzt `plankritik.js` `mutanten_status` auf `ohne_ergebnis` und gibt die Rohausgabe nicht weiter; die Linse sieht den Grund nicht. Das gilt schon heute bei fehlendem `unshare`. Entscheidung beim Koordinator: im Skript eine `EXIT`-Zeile ausgeben oder `plankritik.js` anpassen, in einem eigenen Plan.
- **Gerechnete Werte.** Exit-Codes hinter `FEHLER:` im Lauf gegen HEAD, Wortlaut von `ip` ohne `--keep-caps`, `CapPrm` und `CapInh` gleich 0, Elternprozess `timeout`: alles gerechnet; der Executer misst es in den Schritten 0, 2 und 4.
- **`rb3_mutanten.sh`** fährt T1 und T12 weiter außerhalb des Namensraums. Ob es nach dem Lauf mit echten Daten weg kann, entscheidet der Koordinator.