From 8950af05e02b880a4319bc993e843de482ca78f6 Mon Sep 17 00:00:00 2001 From: Pascal Michel Date: Fri, 9 Oct 2026 15:20:49 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01JU1LsUp4FdmPq8mvC368G3 --- .vault-tools/plan-kopie/LIESMICH.md | 13 +- ...mutanten-kopie-loopback-nachbesserung-1.md | 693 ++++++++++ .../2026-10-09-mutanten-kopie-loopback.md | 1229 +++++++++++++++++ .../plan-kopie/mutanten_lauf_ENTWURF.md | 4 +- .../plan-kopie/mutanten_lauf_kopie.sh | 61 +- .../plan-kopie/mutanten_lauf_kopie_treiber.py | 718 ++++++++++ 6 files changed, 2713 insertions(+), 5 deletions(-) create mode 100644 .vault-tools/plan-kopie/docs/plans/2026-10-09-mutanten-kopie-loopback-nachbesserung-1.md create mode 100644 .vault-tools/plan-kopie/docs/plans/2026-10-09-mutanten-kopie-loopback.md create mode 100644 .vault-tools/plan-kopie/mutanten_lauf_kopie_treiber.py diff --git a/.vault-tools/plan-kopie/LIESMICH.md b/.vault-tools/plan-kopie/LIESMICH.md index bd3ce48..bf08488 100644 --- a/.vault-tools/plan-kopie/LIESMICH.md +++ b/.vault-tools/plan-kopie/LIESMICH.md @@ -5,7 +5,7 @@ title: "Planwerkzeuge: Plan auf eine Kopie anwenden und jede Welle dagegen vergl summary: "Skripte für die Standardschleife bei Plänen mit ausgeschriebenem Code — schlanke Kopie bauen, Plan auf die Kopie anwenden, je Welle Menge und Bytes vergleichen, Aus-wird-Blöcke prüfen, Codeblöcke auf unsichtbare Zeichen absuchen, Escape-Folgen per Codepunkt setzen. Entstanden 2026-09-17 in der Trading Plattform (Teil 3b)." tags: [vault-tools, codeworkflow] created: 2026-09-17 -updated: 2026-10-06 +updated: 2026-10-09 --- # Planwerkzeuge für die Kopie mit angewendetem Plan @@ -24,7 +24,7 @@ Die Regel dazu steht in [[AI/Regelwerk/codeworkflow|codeworkflow.md]] („Die Ko 7. **Rot-Nachweis je Task (seit 2026-09-18):** `rot_nachweis.py [--fixturen ] [--node-modules ] [--conftest-als-test] [--venv ] [--frontend ] [--projektname ]`. **Für ein anderes Projekt als die Trading Plattform braucht es die drei letzten Schalter (seit 2026-09-24):** Ihre Vorgaben sind die der Trading Plattform (`~/venvs/trading`, `dashboard`, `trading-plattform`). Jedes andere Projekt nennt seine Werte, für den Nexhorizons Hub `--venv '~/venvs/hub' --frontend frontend --projektname 'active - Nexhorizons Hub'`, das Venv in einfachen Anführungszeichen, sonst macht die Git-Bash aus `~` einen Windows-Pfad. Das Skript druckt die benutzten Werte in der ersten Zeile. Die Vorgabe `trading-plattform` passt nur auf Pläne mit Arbeitsbaum-Pfaden; ältere Trading-Pläne mit absoluten Pfaden unter `active - Trading Plattform` brauchen `--projektname 'active - Trading Plattform'`, sonst endet der Lauf mit einem `ValueError`. Weiter fest verdrahtet bleiben die Distribution `Debian`, die Ordner `tests/` und `tests/fixtures/`, die Endungen `.test.ts`/`.test.tsx` und `npx.cmd vitest`. **Vorbedingung:** Das Projekt ist im Venv nicht editierbar installiert (`pip install -e`); sonst löst der Finder von setuptools ein fehlendes Untermodul gegen das echte Projekt auf, und ein Test ohne Umsetzung läuft grün. `tests/conftest.py` gilt dabei als Produktivseite und wird zurückgesetzt; nur wenn die neue Fixture eines Tasks dort liegt, lässt `--conftest-als-test` sie stehen (Beleg unten, 2026-09-22). `plan_materialisiere.py` wendet eine Taskdatei immer ganz an; „erst der Test, dann die Umsetzung" lässt sich damit nicht beobachten. Das Skript stellt den Zustand dazwischen auf Wegwerfkopien her — Basis plus Voraussetzungstasks plus der Task, danach die produktiven Zieldateien zurück auf den Stand vor dem Task — und fährt die Testdateien des Tasks (pytest in WSL, vitest unter Windows über eine Junction, die es vor dem Löschen löst). Es meldet je Task `ROT` mit der ersten Fehlerzeile oder `GRUEN`; ein Test, der ohne Umsetzung grün ist, misst sie nicht. Die Basis-Kopie zieht man **vor** der ersten Welle. `spec.json` nennt je Task die Voraussetzungstasks. Erster Einsatz am 2026-09-18 an vier Plänen der Trading Plattform: 35 Tasks, alle rot an der gemeinten Stelle, keiner grün ohne Umsetzung. Die Executer brauchen den Tanz mit dem Beiseitelegen der Produktivdatei dann nicht mehr; ihr Auftrag sagt das ausdrücklich. 8. **Mutanten fahren (seit 2026-09-27):** `mutanten_lauf.py --wurzel --mutanten /mutanten.toml [--nur ID,…] [--nur-muster] [--bericht ] [--zeitlimit ]`, in WSL mit einem Python ab 3.11. Die Mutanten stehen nicht mehr als Prosa im Plan, sondern in `mutanten.toml` im Planordner. Das Format, die Status und die Abdichtung stehen in `mutanten_lauf_ENTWURF.md`. Das Skript verweigert eine Wurzel mit `.git` und jeden Pfad aus `ohne`. Seit 2026-10-06 sucht es die pytest-Konfiguration wie pytest selbst, aber nie oberhalb der Wurzel, und gibt sie mit `-c` vor; eine fremde `pyproject.toml` im Windows-Temp stört damit nicht mehr, und ein Bedienfehler von pytest steht mit dem Ende der pytest-Ausgabe im Befund statt als `FEHLT` je Test (Entwurf, Abschnitt 3, „Konfiguration“ und „Abbruch von pytest“). Ob die Suche pytest selbst entspricht, prüft `mutanten_lauf_probe_konfig.py ` mit dem Python eines Venvs, in dem pytest liegt (26 Aufbauten, Exit 0 ohne Abweichung). Es fährt zuerst die Grundlinie, dann je Mutant nur die genannten Tests, setzt die Datei zurück und prüft das per SHA-256. Je Test gibt es die Fehlermeldung mit aus; ob ein Test an der gemeinten Stelle fällt, liest man dort. `--nur-muster` prüft nur, ob jedes Muster genau einmal dasteht, ohne pytest. Die Kopie legt man in WSL unter `/tmp` an (`cp -a`, danach `__pycache__` löschen). Auf `/mnt/d` läuft es auch, nur langsamer. **Aus der Git-Bash ein Skript nie mit `wsl … bash /mnt/d/…` starten:** Die Git-Bash macht aus dem Pfad einen Windows-Pfad. Aus der PowerShell starten oder `MSYS_NO_PATHCONV=1` davorsetzen. Ein SIGTERM auf `mutanten_lauf.py` selbst (etwa `timeout -k` oder ein Hilfsagent, der abgebrochen wird) löst dort einen eigenen Handler aus: Der laufende Mutant wird per SHA-Probe zurückgesetzt, die Temp-Ordner werden geräumt, und die Ausgabe trägt `ABBRUCH: SIGTERM` (Treiber-Fall 35, `mutanten_lauf_treiber.py`). - **Die Plankritik fährt die Mutanten selbst (seit 2026-09-27, nachgebessert 2026-09-28):** Liegt `mutanten.toml` im Planordner, startet `plankritik.js` in der Vorbereitung `mutanten_lauf_kopie.sh`. Das Skript kopiert die Kopie mit angewendetem Plan nach `/tmp` in WSL, fährt dort den Läufer in einem eigenen Netz-Namensraum (`unshare --map-current-user --net`, gemessen in WSL Debian als Benutzer `claude`: UID innen bleibt 1000, nur Schnittstelle `lo`, 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. Der Arg `mutanten` wählt `voll` (Vorgabe), `muster` oder `aus`; die Rückgabe trägt dazu `mutanten_status` (`aus`, `ohne_kopie`, `keine_toml`, `ohne_ergebnis` oder `exit_`) und `mutanten_modus` (`voll`, `muster`, `aus`). Der Wrapper gibt vor dem Lauf eine Zeile `modus: voll` oder `modus: muster (…)` aus; bei `modus: muster` weiß die Linse, dass kein Mutant gefahren ist. Der ganze Lauf hat 540 s (`BUDGET_S`, `timeout -k 15`), weil das Bash-Werkzeug eines Hilfsagenten nach zehn Minuten endet; eine überschrittene Grenze meldet die Zeile `ZEITGRENZE`. Je Mutant gilt zusätzlich ein Zeitlimit `ZEITLIMIT_S` (Vorgabe 120 s, muss kleiner sein als `BUDGET_S`): B1 mit 74 Mutanten brauchte 168 s gesamt, rund 2 s je Mutant. Bei Exit 2 oder 3 des Läufers sagt die Linse, der Lauf sei vor oder während der Mutanten gescheitert. Pfade übergibt man mit Vorwärtsschrägstrichen: `wsl -d Debian -- …` reicht die Befehlszeile an die Linux-Shell, und die verschluckt Backslashes. + **Die Plankritik fährt die Mutanten selbst (seit 2026-09-27, nachgebessert 2026-09-28):** Liegt `mutanten.toml` im Planordner, startet `plankritik.js` in der Vorbereitung `mutanten_lauf_kopie.sh`. Das Skript kopiert die Kopie mit angewendetem Plan nach `/tmp` in WSL, fährt dort den Läufer in einem eigenen Netz-Namensraum (`unshare --map-current-user --net`, gemessen in WSL Debian als Benutzer `claude`: UID innen bleibt 1000, nur Schnittstelle `lo`, 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. Der Arg `mutanten` wählt `voll` (Vorgabe), `muster` oder `aus`; die Rückgabe trägt dazu `mutanten_status` (`aus`, `ohne_kopie`, `keine_toml`, `ohne_ergebnis` oder `exit_`) und `mutanten_modus` (`voll`, `muster`, `aus`). Der Wrapper gibt vor dem Lauf eine Zeile `modus: voll` oder `modus: muster (…)` aus; bei `modus: muster` weiß die Linse, dass kein Mutant gefahren ist. Der ganze Lauf hat 540 s (`BUDGET_S`, `timeout -k 15`), weil das Bash-Werkzeug eines Hilfsagenten nach zehn Minuten endet; eine überschrittene Grenze meldet die Zeile `ZEITGRENZE`. Je Mutant gilt zusätzlich ein Zeitlimit `ZEITLIMIT_S` (Vorgabe 120 s, muss kleiner sein als `BUDGET_S`): B1 mit 74 Mutanten brauchte 168 s gesamt, rund 2 s je Mutant. Bei Exit 2 oder 3 des Läufers sagt die Linse, der Lauf sei vor oder während der Mutanten gescheitert. Pfade übergibt man mit Vorwärtsschrägstrichen: `wsl -d Debian -- …` reicht die Befehlszeile an die Linux-Shell, und die verschluckt Backslashes. ## Was belegt ist @@ -49,6 +49,15 @@ Nachbesserung vom 2026-10-06 (Plankritik 2 von C1 im Projekt After Install, Plan - Ein `pruefer` fand in der ersten Fassung den Symlink-Fall und fehlende Fälle; die Nachbesserung behob beides. Ein zweiter `pruefer` fand danach keinen Verhaltensfehler mehr, nur eine Aufräumliste (Kopfzeile je Interpreter ohne eigenen Fall, Kurzfassung bei Rückgabewert 3, `;` und Seitenvorschub in `tox.ini`, Selbstprüfung der Probe, lange Fassung im Bericht, zwei Lücken benennen); sie ist in Nachbesserung 2 erledigt, Treiber 46 von 46. - 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 die Fälle W1 bis W13: 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). Seit Nachbesserung 1 auch: `ip` scheitert in der Probe (W10), `ip` meldet Erfolg und `lo` bleibt aus (W11), `ip` scheitert erst im Hauptaufruf nach der Modus-Zeile (W12), `setpriv` wirft keine Fähigkeit ab (W13). W10 bis W13 ersetzen dazu einzelne Werkzeuge durch kleine `sh`-Skripte. Neuer Stand ohne Fehler. +- Gegen das Skript vor der Änderung rot: W1, W2, W4, W5, W9. Gegen kaputte Varianten genau an ihrem Fall, gefahren mit W1 bis W9: 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. Gefahren mit W1 bis W13 (Nachbesserung 1, `docs/plans/2026-10-09-mutanten-kopie-loopback-nachbesserung-1.md`): ohne `exit 2` nach der Probe W10, W11 und W13; Probe ohne `setpriv` W1 bis W9 und W12; Hauptaufruf ohne den Zweig nach `||` W12; Probe ohne Fähigkeitsprüfung W13. +- 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. +- Die Fähigkeiten prüft das Skript nur an der Probe und nur `CapEff` und `CapAmb`, nicht am Läufer selbst. Alle vier Mengen am Läufer prüft Treiber-Fall W3. +- Die Vorprüfung steht vor der Suche nach `mutanten.toml`. Fehlt auf einem Rechner `ip`, `setpriv` oder `python3` oder kommt `lo` nicht hoch, endet das Skript deshalb auch für einen Planordner ohne `mutanten.toml` mit `MUTANTEN_LAUF_FEHLER` und Exit 2 statt mit `KEINE_MUTANTEN_TOML`. `plankritik.js` meldet dann `ohne_ergebnis` statt `keine_toml`, und die Linse erfährt nicht, dass die `mutanten.toml` fehlt. Das bleibt so. +- Lauf mit echten Daten: die 73 Mutanten des Pakets RB3 (Kopie `rb3d-kopie`) durch das neue Skript, alle `OK`, T1 und T12 im Namensraum. +- Ein `pruefer` fand keinen blockierenden Befund. Seine Lücken (kein Fall für den Fehlerzweig der Probe und den Zweig nach `||` im Hauptaufruf, Kopfkommentar zu weit gefasst, der Satz zur Reihenfolge oben) behebt Nachbesserung 1. Rohdaten: `AI/secondbrain/trading-plattform-memory/2026-10-09-mkopie-roh/`. + Seit 2026-09-18 nimmt `welle_vergleich.py` unter `## Zieldateien` auch Pfade relativ zum Projekt (ohne Laufwerk und ohne Wurzel), weil Pläne für einen Git-Worktree keine absoluten Pfade nennen; ein `..` darin ist ein Befund wie ein fremder absoluter Pfad. Jede Zeile braucht weiter die Klammer hinter dem Pfad (`(neu)`, `(geändert)`, `(gelöscht)`). `plan_materialisiere.py` erkennt eine Zieldatei ohne Schrägstrich nur mit den Endungen py, ts, tsx, toml, md, css und json — für `compose.yaml` in der Projektwurzel schreibt die Überschrift `./compose.yaml`. Der Treiber fährt dazu drei weitere Fälle (relative Grundlinie, relative Abweichung, `..` nach außen), zusammen 17. Seit 2026-09-24 hat `rot_nachweis.py` die Schalter `--venv`, `--frontend` und `--projektname`; vorher waren `~/venvs/trading`, `dashboard` und `trading-plattform/` fest verdrahtet. Ein Hub-Lauf im Trading-Venv wäre für die Tasks mit `argon2-cffi` und `pyotp` rot aus einem fehlenden Paket gewesen, nicht aus der fehlenden Umsetzung. Fehlt das Venv in WSL, steht beim Task jetzt `VORAUSSETZUNG FEHLT` statt `ROT`, und die Schlusszeile zählt diese Läufe getrennt. Belegt am selben Tag: Der Vorgabe-Befehl ist byte-gleich mit der alten Fassung, falsche Schalterwerte enden mit Exit 3, und der erste echte Lauf über 38 Tasks des Hub meldete keinen grün ohne Umsetzung. Im selben Lauf zeigte sich eine zweite Lücke: Beim Zurücksetzen blieben Ordner stehen, die erst der Task angelegt hatte. Ein leeres `hub/auth/` liest Python als Namespace-Paket, und die Fehlerzeile nannte `hub.auth.passwort` statt `hub.auth`. Jetzt gehen solche Ordner mit, sobald sie leer sind; derselbe Lauf für die Tasks 7, 9 und 11 zeigt danach genau die Zeile aus dem Plan. Die unabhängige Prüfung desselben Tages fand eine dritte Lücke: Ein vergessenes `--venv` fiel still auf das Trading-Venv zurück, und Task 7 stand als `ROT … No module named 'argon2'` da. Seitdem wird eine erste Fehlerzeile, die ein Modul außerhalb des Projekts und außerhalb der Zieldateien vermisst, als `VORAUSSETZUNG FEHLT` gemeldet. Das trifft auch den Ende-zu-Ende-Task des Hub, dessen Tests `playwright` brauchen. Mit den Vorgaben lieferte das Skript für die sieben Tasks des Trading-Pakets K vor und nach allen Änderungen dieselben Zeilen. Zwei weitere Lücken desselben Tages, beide ein ROT, das nichts misst: **Ein Arbeitsordner mit Windows-Kurznamen** (`C:/Users/PASCAL~1/…`) ließ vitest dort starten, die Dateien aber über den langen Namen auflösen, und jede Oberflächen-Testdatei endete als `Cannot find module '/src/…'`. Das Skript löst den Arbeitsordner jetzt auf den langen Namen auf; danach zeigte Trading-Task 395 wieder `9 failed | 52 passed` statt `no tests`, und die Hub-Tasks 24, 25, 29 und 37 die Zeile aus dem Plan (`Error: Failed to resolve import "./kontrast" …`). Die erste `Error:`-Zeile steht seitdem mit in der Ergebniszeile, weil die `FAIL`-Zeile allein den Grund nicht zeigt. **Ein Test, der über Vite eine Datei aus `node_modules` lädt** (`?raw`), scheitert hinter der Junction mit `Denied ID`, weil Vite nichts außerhalb seines Wurzelordners ausliefert; das Skript meldet das als `VORAUSSETZUNG FEHLT`. Für solche Tasks läuft der Nachweis in einer Kopie mit echtem `node_modules` (242 MB, gut 20 Sekunden). diff --git a/.vault-tools/plan-kopie/docs/plans/2026-10-09-mutanten-kopie-loopback-nachbesserung-1.md b/.vault-tools/plan-kopie/docs/plans/2026-10-09-mutanten-kopie-loopback-nachbesserung-1.md new file mode 100644 index 0000000..63f9782 --- /dev/null +++ b/.vault-tools/plan-kopie/docs/plans/2026-10-09-mutanten-kopie-loopback-nachbesserung-1.md @@ -0,0 +1,693 @@ +--- +type: plan +title: "Mutantenlauf auf der Kopie: Nachbesserung 1 (Fälle für die Fehlerzweige der lo-Prüfung)" +status: active +summary: "Delta zum Plan vom 2026-10-09: Treiber-Fälle W10 bis W13 (Probe scheitert an ip, lo bleibt aus, ip scheitert erst im Hauptaufruf, setpriv wirft nichts ab), Varianten v6 bis v9 als Regressionsnachweis, Kopfkommentar des Skripts berichtigt, Satz zur Reihenfolge vor KEINE_MUTANTEN_TOML in LIESMICH und Entwurf, Datensatz rb3d-kopie im Plan." +tags: [plan, vault-tools, pruefwerkzeuge] +created: 2026-10-09 +updated: 2026-10-09 +modell: haiku +modell_begruendung: "Jeder Block steht wörtlich da, Zieldateien, Verbote und Haltepunkte sind genannt; kein Codeblock trägt typografische Anführungszeichen, Striche oder Auslassungspunkte." +--- + +Übersicht: [LIESMICH der Planwerkzeuge](../../LIESMICH.md) · Vorgänger: [Plan lo im Netz-Namensraum](2026-10-09-mutanten-kopie-loopback.md) + +# Mutantenlauf auf der Kopie: Nachbesserung 1 + +## Ziel + +Der Treiber `mutanten_lauf_kopie_treiber.py` sieht danach auch die Fehlerzweige der lo-Prüfung: eine gescheiterte Probe (W10, W11), die Fähigkeitsprüfung der Probe (W13) und den Zweig nach `||` im Hauptaufruf (W12). Jeder dieser Fälle ist an einer kaputten Variante rot gesehen. Kopfkommentar, LIESMICH und Entwurf sagen genau, was die Vorprüfung prüft und dass sie vor `KEINE_MUTANTEN_TOML` steht. Der Plan nennt für den Lauf mit echten Daten den richtigen Datensatz. + +## Nicht im Umfang + +- Das Verhalten von `mutanten_lauf_kopie.sh` bleibt gleich. Im Skript ändert sich nur der Kopfkommentar; Schritt 2 prüft das. +- Die Reihenfolge der Vorprüfung vor `KEINE_MUTANTEN_TOML` bleibt (Befund 3), sie wird nur beschrieben. +- Kein neuer Lauf gegen das Skript aus HEAD, kein neuer Lauf der Varianten v1 bis v5, kein neuer Lauf mit echten Daten (siehe Getroffene Annahmen). +- `mutanten_lauf.py`, `mutanten_lauf_treiber.py`, `.claude/workflows/*` und alles unter `D:/Claude/.arbeitsbaeume/` bleiben unberührt. + +## Warum so + +Der `pruefer` fand am 2026-10-09 keinen blockierenden Befund. Treiber W1 bis W9 grün, die 73 Mutanten von RB3 durch das neue Skript alle `OK`, T1 und T12 im Namensraum grün. Rohdaten: `D:/Claude/AI/secondbrain/trading-plattform-memory/2026-10-09-mkopie-roh/pruefer/` (`angriffe.py`, `angriffe.out`, `varianten_pruefer.py`). + +| Befund | Hier | +|---|---| +| 1 (wichtig): kein Fall für `exit 2` nach der Probe und keiner für den Zweig nach `\|\|` im Hauptaufruf; v6 lief ganz grün | W10 (A1), W11 (A3), W12 (A4), dazu W13 für die Fähigkeitsprüfung der Probe ohne Variante; Varianten v6 und v7 des Prüfers, dazu v8 und v9 | +| 2 (klein): Kopfkommentar Z. 42 bis 43 zu weit | Kopfkommentar trennt Probe und Hauptaufruf, nennt CapEff und CapAmb und nur die Probe | +| 3 (klein): Vorprüfung vor `KEINE_MUTANTEN_TOML` | bleibt; Satz in LIESMICH, Entwurf und Kopfkommentar | +| 4 (klein): Datensatz `rb3d-kopie` statt `rb3b-kopie` | drei Ersetzungen im Plan, Abschnitt „Lauf mit echten Daten“ | + +### Bauweise der neuen Fälle + +Aus `angriffe.py` des Prüfers übernommen: Der `PATH` des Skripts ist ein Ordner mit Verweisen auf alle Werkzeuge aus `WERKZEUGE`. Einzelne Werkzeuge sind dort durch kleine Skripte mit `#!/bin/sh` ersetzt. W4, W5 und W9 lassen ein Werkzeug weg, W10 bis W13 ersetzen eines. + +| Fall | Ersatz | Erwartung | Gemessen oder gerechnet | +|---|---|---|---| +| W10 | `ip`: Meldung auf stderr, Exit 1 | Exit 2, `MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht nutzbar`, Text der Attrappe in stdout, keine Modus-Zeile, keine `EXIT`-Zeile, keine Kopie | gemessen als A1 | +| W11 | `ip`: Exit 0 ohne Wirkung | dasselbe, mit `Errno 101` in stdout | gemessen als A3 | +| W12 | `ip`: zählt seine Aufrufe in einer Datei, ab dem zweiten Exit 1, sonst `exec` des echten `ip` | Exit 2, `modus: voll`, `MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht hochgefahren`, `EXIT mutanten_lauf: 2`, kein `MW7`, Text der Attrappe in stderr, genau zwei Aufrufe, keine Kopie | gemessen als A4, ohne die Zählung | +| W13 | `setpriv`: überspringt alles bis `--` und startet den Rest per `exec`, ohne etwas abzuwerfen | wie W10, mit `Faehigkeiten nicht abgeworfen` in stdout | gerechnet | + +W13 rechnet so: `unshare --keep-caps` hebt die Fähigkeiten in die ambiente Menge. Sonst käme `ip` nach dem `exec` der `sh` nicht an `CAP_NET_ADMIN`. Ohne `setpriv` behält `python3` sie deshalb, wie bei Variante v1 des Plans (W3 rot, gemessen). + +### Varianten + +| Variante | Änderung | rot erwartet | gemessen oder gerechnet | +|---|---|---|---| +| `v6_probe_ohne_exit` | Zeile `exit 2` nach der Meldung der Probe gestrichen (v6 des Prüfers) | W10, W11, W13 | W1 bis W9 grün gemessen (Prüfer), W10 rot gemessen (A6), W11 und W13 gerechnet | +| `v7_probe_ohne_setpriv` | Probe ohne `setpriv` (v7 des Prüfers) | W1 bis W9, W12 | gerechnet | +| `v8_haupt_ohne_oder` | Hauptaufruf ohne den Zweig nach `\|\|` | W12 | gerechnet | +| `v9_probe_ohne_faehigkeiten` | Probe ohne Fähigkeitsprüfung (`if False:`) | W13 | gerechnet | + +Damit ist jeder neue Fall einmal rot: W10 und W11 an v6, W12 an v7 und v8, W13 an v6 und v9. + +## Zieldateien + +Pfade relativ zu `D:/Claude/.vault-tools/plan-kopie/`: + +- `mutanten_lauf_kopie_treiber.py` (geändert) +- `mutanten_lauf_kopie.sh` (geändert) +- `LIESMICH.md` (geändert) +- `mutanten_lauf_ENTWURF.md` (geändert) +- `docs/plans/2026-10-09-mutanten-kopie-loopback.md` (geändert) + +## Verbote + +- Nur die fünf Zieldateien ändern. Kein `git add`, kein `git commit`. +- Hilfsdateien nur im Rot-Ordner `C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot-n1` (Git-Bash: `/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot-n1`, WSL: `/mnt/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot-n1`). +- Jeden Block byte-genau übernehmen: keine Umformulierung, keine Ersetzung von Zeichen, keine zusätzlichen Leerzeilen. Aus-Block als `old_string`, wird-Block als `new_string` im Edit-Werkzeug; die Variantendatei mit dem Write-Werkzeug. +- Kein `sed`, kein Suchen-und-Ersetzen über ganze Dateien. +- WSL-Aufrufe nur aus der PowerShell. +- Kein Netz außer `127.0.0.1`. +- Den Treiber nicht fahren, während eine Plankritik läuft. +- Steht ein Aus-Block nicht genau einmal in seiner Datei oder weicht eine Pflicht-Ausgabe ab: 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 Ausgangsstand + +In der Git-Bash: + +```bash +# Rot-Ordner neu anlegen, Skript im jetzigen Stand als Vergleichsdatei hineinlegen +R=/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot-n1 +rm -rf "$R" && mkdir -p "$R" +cp /d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie.sh "$R/skript_vor_n1.sh" +# Ausgangsstand: Plan umgesetzt, nicht committet +git -C /d/Claude status --porcelain -- .vault-tools +``` + +Erwartet: genau diese sechs Zeilen, in beliebiger Reihenfolge. Sonst anhalten und melden. + +```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-nachbesserung-1.md +?? .vault-tools/plan-kopie/docs/plans/2026-10-09-mutanten-kopie-loopback.md +?? .vault-tools/plan-kopie/mutanten_lauf_kopie_treiber.py +``` + +## Schritt 1 — Tests zuerst: Treiber + +### `mutanten_lauf_kopie_treiber.py`, Stelle 1 von 7 + +Aus: + +```python +#!/usr/bin/env python3 +"""Treiber fuer mutanten_lauf_kopie.sh: Faelle W1 bis W9 (lo im Netz-Namensraum). +``` + +wird: + +```python +#!/usr/bin/env python3 +"""Treiber fuer mutanten_lauf_kopie.sh: Faelle W1 bis W13 (lo im Netz-Namensraum). +``` + +### `mutanten_lauf_kopie_treiber.py`, Stelle 2 von 7 + +Aus: + +```python +Nicht im Treiber: ob plankritik.js das Skript so aufruft wie hier +(MSYS_NO_PATHCONV, cygpath -m -l). +""" +``` + +wird: + +```python +Faelle W10 bis W13 (Nachbesserung 1, Plan +docs/plans/2026-10-09-mutanten-kopie-loopback-nachbesserung-1.md) ersetzen im +Attrappen-Ordner einzelne Werkzeuge durch kleine sh-Skripte, nach den +Angriffen A1, A3 und A4 des Pruefers vom 2026-10-09: ip scheitert in der +Probe (W10), ip meldet Erfolg und lo bleibt aus (W11), ip scheitert erst im +Hauptaufruf (W12), setpriv wirft keine Faehigkeit ab (W13). + +Nicht im Treiber: ob plankritik.js das Skript so aufruft wie hier +(MSYS_NO_PATHCONV, cygpath -m -l); ob die Probe bei Exit 0 mit falscher +Ausgabe scheitert. +""" +``` + +### `mutanten_lauf_kopie_treiber.py`, Stelle 3 von 7 + +Aus: + +```python +import os +import shutil +``` + +wird: + +```python +import os +import shlex +import shutil +``` + +### `mutanten_lauf_kopie_treiber.py`, Stelle 4 von 7 + +Aus: + +```python +# Was mutanten_lauf_kopie.sh ueber PATH aufruft (Stand 2026-10-09); daraus +# bauen W4, W5 und W9 ihre Attrappen-Ordner. +``` + +wird: + +```python +# Was mutanten_lauf_kopie.sh ueber PATH aufruft (Stand 2026-10-09); daraus +# bauen W4, W5, W9 und W10 bis W13 ihre Attrappen-Ordner. +``` + +### `mutanten_lauf_kopie_treiber.py`, Stelle 5 von 7 + +Aus: + +```python +def fall_w9(skript): + fahre_ohne_werkzeug("W9", skript, "python3") + + +def prozesse_mit_marke(marke): +``` + +wird: + +```python +def fall_w9(skript): + fahre_ohne_werkzeug("W9", skript, "python3") + + +def baue_attrappen_mit_ersatz(ordner, ersatz): + # Wie baue_attrappen, aber jedes Werkzeug aus ersatz ist ein kleines + # sh-Skript statt eines Verweises auf das echte Werkzeug (Bauweise aus den + # Angriffen des Pruefers vom 2026-10-09). + ordner.mkdir() + for name in WERKZEUGE: + pfad = ordner / name + if name in ersatz: + pfad.write_text("#!/bin/sh\n" + ersatz[name] + "\n", encoding="utf-8") + pfad.chmod(0o755) + else: + os.symlink(shutil.which(name), pfad) + + +def fahre_probe_scheitert(name, skript, ersatz, merkmal): + # Die Vorpruefung muss scheitern: Exit 2 mit der Meldung der Probe und dem + # Merkmal des Angriffs, ohne Modus-Zeile, ohne EXIT-Zeile, ohne Kopie. + fall = fall_einfach() + try: + ordner = fall.basis / "attrappen" + baue_attrappen_mit_ersatz(ordner, ersatz) + vorher = plankritik_ordner() + ergebnis = starte_skript(skript, fall, pfad=str(ordner)) + neu = sorted(str(p) for p in plankritik_ordner() - vorher) + ok = ( + ergebnis.returncode == 2 + and "MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht nutzbar" in ergebnis.stdout + and merkmal in ergebnis.stdout + and "modus:" not in ergebnis.stdout + and "EXIT mutanten_lauf" not in ergebnis.stdout + and not neu + ) + melde(name, ok, "neue Kopien={0}, {1}".format(neu, kurz(ergebnis))) + fall.pruefe_kopie(name) + finally: + fall.aufraeumen() + + +def fall_w10(skript): + # ip scheitert schon in der Probe (Angriff A1 des Pruefers). + fahre_probe_scheitert("W10", skript, {"ip": "echo ip-attrappe scheitert >&2; exit 1"}, "ip-attrappe scheitert") + + +def fall_w11(skript): + # ip meldet Erfolg, lo bleibt aus (A3): Die Verbindung der Probe scheitert + # mit Errno 101. + fahre_probe_scheitert("W11", skript, {"ip": "exit 0"}, "Errno 101") + + +def fall_w12(skript): + # ip klappt in der Probe und scheitert erst im Hauptaufruf (A4); eine + # Zaehlerdatei haelt fest, der wievielte Aufruf es ist. Erwartet: + # Modus-Zeile, Meldung des Hauptaufrufs, EXIT-Zeile mit 2, kein Laeufer, + # genau zwei Aufrufe von ip, keine Kopie unter /tmp. + fall = fall_einfach() + try: + zaehler = fall.basis / "ip_zaehler" + z = shlex.quote(str(zaehler)) + ip_echt = shlex.quote(shutil.which("ip")) + ersatz = {"ip": ( + "n=0\n" + "if [ -f " + z + " ]; then read n < " + z + "; fi\n" + "n=$((n + 1))\n" + "echo \"$n\" > " + z + "\n" + "if [ \"$n\" -ge 2 ]; then echo \"ip-attrappe zweiter Aufruf\" >&2; exit 1; fi\n" + "exec " + ip_echt + " \"$@\"" + )} + ordner = fall.basis / "attrappen" + baue_attrappen_mit_ersatz(ordner, ersatz) + vorher = plankritik_ordner() + ergebnis = starte_skript(skript, fall, pfad=str(ordner)) + neu = sorted(str(p) for p in plankritik_ordner() - vorher) + try: + aufrufe = zaehler.read_text(encoding="ascii").strip() + except OSError: + aufrufe = "keine" + ok = ( + ergebnis.returncode == 2 + and "modus: voll" in ergebnis.stdout + and "MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht hochgefahren" in ergebnis.stdout + and "EXIT mutanten_lauf: 2" in ergebnis.stdout + and "nicht nutzbar" not in ergebnis.stdout + and "MW7" not in ergebnis.stdout + and "ip-attrappe zweiter Aufruf" in ergebnis.stderr + and aufrufe == "2" + and not neu + ) + melde("W12", ok, "ip-aufrufe={0}, neue Kopien={1}, {2}".format(aufrufe, neu, kurz(ergebnis))) + fall.pruefe_kopie("W12") + finally: + fall.aufraeumen() + + +SETPRIV_OHNE_WIRKUNG = ( + "while [ \"$#\" -gt 0 ] && [ \"$1\" != \"--\" ]; do shift; done\n" + "shift\n" + "exec \"$@\"" +) + + +def fall_w13(skript): + # setpriv wirft keine Faehigkeit ab und startet nur den Befehl hinter --: + # Die Faehigkeitspruefung der Probe muss anschlagen. + fahre_probe_scheitert("W13", skript, {"setpriv": SETPRIV_OHNE_WIRKUNG}, "Faehigkeiten nicht abgeworfen") + + +def prozesse_mit_marke(marke): +``` + +### `mutanten_lauf_kopie_treiber.py`, Stelle 6 von 7 + +Aus: + +```python +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), +] +``` + +wird: + +```python +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), + ("W10", fall_w10), + ("W11", fall_w11), + ("W12", fall_w12), + ("W13", fall_w13), +] +``` + +### `mutanten_lauf_kopie_treiber.py`, Stelle 7 von 7 + +Aus: + +```python + zerleger = argparse.ArgumentParser(description="Treiber fuer mutanten_lauf_kopie.sh (Faelle W1 bis W9)") +``` + +wird: + +```python + zerleger = argparse.ArgumentParser(description="Treiber fuer mutanten_lauf_kopie.sh (Faelle W1 bis W13)") +``` + +Dann in der Git-Bash prüfen: + +```bash +# Treiber kompilieren, nur ASCII, keine CR, vier neue Faelle; __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 +grep -c '^def fall_w1[0-3](skript):$' mutanten_lauf_kopie_treiber.py +``` + +Erwartet: `kompiliert`, dann `0`, `0`, `4`. + +### Kein Rot vor der Umsetzung + +Diese Nachbesserung ändert das Verhalten des Skripts nicht. Die neuen Fälle sind deshalb schon gegen den jetzigen Stand grün; W10 bis W12 hat der Prüfer als A1, A3 und A4 gemessen. Ihr Rot zeigen die Varianten in Schritt 4. + +## Schritt 2 — Umsetzung: Kopfkommentar des Skripts + +### `./mutanten_lauf_kopie.sh`, Stelle 1 von 1 + +Aus: + +```bash +# - 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 +``` + +wird: + +```bash +# - Vorpruefung vor dem Kopieren: Fehlt ip, setpriv oder python3, kommt lo in einer Probe im +# eigenen Namensraum nicht hoch, gelingt dort keine Verbindung ueber 127.0.0.1 oder hat die +# Probe nach setpriv noch CapEff oder CapAmb ungleich 0, endet das Skript mit +# MUTANTEN_LAUF_FEHLER und Exit 2, ohne EXIT-Zeile. Die Faehigkeiten prueft das Skript nur an +# dieser Probe und nur CapEff und CapAmb, nicht am Laeufer selbst; alle vier Mengen am Laeufer +# prueft Treiber-Fall W3. Die Vorpruefung steht vor der Suche nach mutanten.toml: Scheitert +# sie, endet das Skript auch fuer einen Planordner ohne mutanten.toml mit Exit 2 statt mit +# KEINE_MUTANTEN_TOML. +# - Hauptaufruf: Kommt lo dort nicht hoch, endet das Skript nach der Modus-Zeile mit +# MUTANTEN_LAUF_FEHLER, Exit 2 und EXIT-Zeile, ohne den Laeufer zu starten. Ein Lauf ohne +# Netzsperre oder ohne lo ist nie die Ausweichloesung. +# - Treiber: mutanten_lauf_kopie_treiber.py, Faelle W1 bis W13 (W10 bis W13 seit Nachbesserung 1, +# docs/plans/2026-10-09-mutanten-kopie-loopback-nachbesserung-1.md). +set -u +``` + +Dann in der Git-Bash: + +```bash +# nur ASCII, keine CR, und ausser Kommentarzeilen nichts geaendert +cd /d/Claude/.vault-tools/plan-kopie +R=/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot-n1 +LC_ALL=C grep -cP '[^\x00-\x7F]' mutanten_lauf_kopie.sh +tr -cd '\r' < mutanten_lauf_kopie.sh | wc -c +diff <(grep -v '^#' "$R/skript_vor_n1.sh") <(grep -v '^#' mutanten_lauf_kopie.sh) && echo nur-kommentar +``` + +Erwartet: `0`, `0`, `nur-kommentar`. + +In der PowerShell: + +```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: nur `exit=0`. + +## Schritt 3 — Verifikation: Treiber gegen das Skript + +```powershell +# Treiber in WSL gegen das Skript nach Nachbesserung 1 +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-n1\ausgabe-neu.txt" +"exit=$LASTEXITCODE" +Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot-n1\ausgabe-neu.txt" -Pattern "^W\d.*FEHLER:|^gerechnete|^TREIBER_FEHLER|^rundreise" +Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot-n1\ausgabe-neu.txt" -Pattern "^W1[0-3] ok" +``` + +Erwartet: + +- `exit=0`. +- Aus dem ersten `Select-String` genau zwei Treffer: einer mit `rundreise: /tmp/mkopie_treiber_rundreise_`, einer endet auf `gerechnete Fehler: 0`. +- Aus dem zweiten genau vier Treffer, sie enden auf `W10 ok`, `W11 ok`, `W12 ok`, `W13 ok`. + +Sonst anhalten und die ganze Datei `ausgabe-neu.txt` melden. Gerechnet, nicht gemessen ist nur W13; steht dort `W13 FEHLER: neue Kopien=[], exit=0`, hat die Probe ohne `setpriv` keine Fähigkeiten gesehen. Das ist ein Befund für den Koordinator, kein Grund, den Treiber anzupassen. + +## Schritt 4 — Regressionsnachweis: vier Varianten + +### Varianten bauen + +Leg mit dem Write-Werkzeug die Datei `C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot-n1\varianten_n1.py` an. + +Inhalt: + +```python +# Baut vier kaputte Fassungen von mutanten_lauf_kopie.sh fuer Nachbesserung 1 (v6 bis v9). +# Aufruf: python varianten_n1.py +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) + + +varianten = {} +# v6: Probe meldet, das Skript laeuft weiter; Zeile exit 2 nach der Meldung gestrichen (erwartet rot: W10, W11, W13) +zeilen = text.split("\n") +treffer = [i for i, z in enumerate(zeilen) + if z.lstrip().startswith("echo \"MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht nutzbar")] +if len(treffer) != 1 or zeilen[treffer[0] + 1].strip() != "exit 2": + raise SystemExit("Meldung der Probe mit exit 2 nicht eindeutig gefunden") +k = treffer[0] +varianten["v6_probe_ohne_exit"] = "\n".join(zeilen[:k + 1] + zeilen[k + 2:]) +# v7: Probe ohne setpriv (erwartet rot: W1 bis W9, W12) +varianten["v7_probe_ohne_setpriv"] = ersetze( + text, "exec setpriv --inh-caps=-all --ambient-caps=-all -- python3 -c ", "exec python3 -c ", 1) +# v8: Hauptaufruf ohne den Zweig nach || (erwartet rot: W12) +varianten["v8_haupt_ohne_oder"] = ersetze( + text, " || { echo \"MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht hochgefahren\"; exit 2; }", "", 1) +# v9: Probe ohne Faehigkeitspruefung (erwartet rot: W13) +varianten["v9_probe_ohne_faehigkeiten"] = ersetze( + text, "if int(felder[\"CapEff\"], 16) != 0 or int(felder[\"CapAmb\"], 16) != 0:", "if False:", 1) + +for name, inhalt in varianten.items(): + if inhalt == text: + raise SystemExit(name + " unveraendert") + (ziel / (name + ".sh")).write_bytes(inhalt.encode("utf-8")) + print(name) +``` + +In der Git-Bash: + +```bash +# Varianten aus dem Skript nach Nachbesserung 1 bauen +R=/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot-n1 +python "$R/varianten_n1.py" /d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie.sh "$R" +``` + +Erwartet genau diese vier Zeilen: + +```text +v6_probe_ohne_exit +v7_probe_ohne_setpriv +v8_haupt_ohne_oder +v9_probe_ohne_faehigkeiten +``` + +Endet das Skript mit einer Meldung statt der vier Zeilen: anhalten und melden. + +### Treiber gegen jede Variante + +```powershell +# Treiber in WSL gegen jede kaputte Variante +foreach ($v in "v6_probe_ohne_exit","v7_probe_ohne_setpriv","v8_haupt_ohne_oder","v9_probe_ohne_faehigkeiten") { + 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-n1/$v.sh" *> "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot-n1\ausgabe-$v.txt" + "$v exit=$LASTEXITCODE" + Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot-n1\ausgabe-$v.txt" -Pattern "^W\d.*FEHLER:|^gerechnete|^TREIBER_FEHLER" +} +``` + +Erwartet je Variante `exit=1`, und die `FEHLER`-Zeilen betreffen genau diese Fälle. Zeilen wie `W12 (kopie) FEHLER:` oder `W12 (aufraeumen) FEHLER:` zählen zu ihrem Fall: + +| Variante | rote Fälle | gerechnet: Zeilen der neuen Fälle | was es belegt | +|---|---|---|---| +| `v6_probe_ohne_exit` | W10, W11, W13 | `W10 FEHLER: neue Kopien=[], exit=2`, `W11 FEHLER: neue Kopien=[], exit=0`, `W13 FEHLER: neue Kopien=[], exit=0` | W10, W11 und W13 sehen ein Skript, das nach gescheiterter Probe weiterläuft | +| `v7_probe_ohne_setpriv` | W1 bis W9, W12 | `W12 FEHLER: ip-aufrufe=1, neue Kopien=[], exit=2` | die Fähigkeitsprüfung der Probe schlägt an; mit Fähigkeiten läuft kein Mutant | +| `v8_haupt_ohne_oder` | W12 | `W12 FEHLER: ip-aufrufe=2, neue Kopien=[], exit=0` | W12 sieht den fehlenden Zweig nach `\|\|` | +| `v9_probe_ohne_faehigkeiten` | W13 | `W13 FEHLER: neue Kopien=[], exit=0` | W13 sieht eine Probe ohne Fähigkeitsprüfung | + +- Pflicht ist die Menge der roten Fälle je Variante. Weicht sie ab: anhalten und die ganze Ausgabedatei der Variante melden. Nicht weiter zu Schritt 5. +- Gerechnet, nicht gemessen: die Zeilen in der dritten Spalte und die Zahl hinter `gerechnete Fehler:` (3, 10, 1, 1). Weicht nur ein solcher Wert ab, meldest du ihn und machst weiter. + +## Schritt 5 — Doku und Plan + +### `LIESMICH.md`, Stelle 1 von 1 + +Aus: + +```markdown +- `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. +``` + +wird: + +```markdown +- `mutanten_lauf_kopie_treiber.py` ruft das Skript wie die Plankritik mit Windows-Pfaden auf und fährt die Fälle W1 bis W13: 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). Seit Nachbesserung 1 auch: `ip` scheitert in der Probe (W10), `ip` meldet Erfolg und `lo` bleibt aus (W11), `ip` scheitert erst im Hauptaufruf nach der Modus-Zeile (W12), `setpriv` wirft keine Fähigkeit ab (W13). W10 bis W13 ersetzen dazu einzelne Werkzeuge durch kleine `sh`-Skripte. Neuer Stand ohne Fehler. +- Gegen das Skript vor der Änderung rot: W1, W2, W4, W5, W9. Gegen kaputte Varianten genau an ihrem Fall, gefahren mit W1 bis W9: 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. Gefahren mit W1 bis W13 (Nachbesserung 1, `docs/plans/2026-10-09-mutanten-kopie-loopback-nachbesserung-1.md`): ohne `exit 2` nach der Probe W10, W11 und W13; Probe ohne `setpriv` W1 bis W9 und W12; Hauptaufruf ohne den Zweig nach `||` W12; Probe ohne Fähigkeitsprüfung W13. +- 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. +- Die Fähigkeiten prüft das Skript nur an der Probe und nur `CapEff` und `CapAmb`, nicht am Läufer selbst. Alle vier Mengen am Läufer prüft Treiber-Fall W3. +- Die Vorprüfung steht vor der Suche nach `mutanten.toml`. Fehlt auf einem Rechner `ip`, `setpriv` oder `python3` oder kommt `lo` nicht hoch, endet das Skript deshalb auch für einen Planordner ohne `mutanten.toml` mit `MUTANTEN_LAUF_FEHLER` und Exit 2 statt mit `KEINE_MUTANTEN_TOML`. `plankritik.js` meldet dann `ohne_ergebnis` statt `keine_toml`, und die Linse erfährt nicht, dass die `mutanten.toml` fehlt. Das bleibt so. +- Lauf mit echten Daten: die 73 Mutanten des Pakets RB3 (Kopie `rb3d-kopie`) durch das neue Skript, alle `OK`, T1 und T12 im Namensraum. +- Ein `pruefer` fand keinen blockierenden Befund. Seine Lücken (kein Fall für den Fehlerzweig der Probe und den Zweig nach `||` im Hauptaufruf, Kopfkommentar zu weit gefasst, der Satz zur Reihenfolge oben) behebt Nachbesserung 1. Rohdaten: `AI/secondbrain/trading-plattform-memory/2026-10-09-mkopie-roh/`. +``` + +### `mutanten_lauf_ENTWURF.md`, Stelle 1 von 1 + +Aus: + +```markdown +Treiber: `mutanten_lauf_kopie_treiber.py` (W1 bis W9), Plan `docs/plans/2026-10-09-mutanten-kopie-loopback.md`. +``` + +wird: + +```markdown +Treiber: `mutanten_lauf_kopie_treiber.py` (W1 bis W13), Plan `docs/plans/2026-10-09-mutanten-kopie-loopback.md` und seine Nachbesserung 1. Die Fähigkeiten prüft der Wrapper nur an der Probe (`CapEff`, `CapAmb`), nicht am Läufer. Weil die Vorprüfung vor der Suche nach `mutanten.toml` steht, endet er ohne `ip`, `setpriv`, `python3` oder nutzbares `lo` auch ohne `mutanten.toml` mit Exit 2 statt mit `KEINE_MUTANTEN_TOML`. +``` + +### `docs/plans/2026-10-09-mutanten-kopie-loopback.md`, Stelle 1 von 3 + +Aus: + +```markdown +- **Datensatz:** Paket RB3 der Trading Plattform, Kopie `D:/Claude/.arbeitsbaeume/rb3b-kopie`, Planordner `D:/Claude/.arbeitsbaeume/trading-plattform-rb3/docs/plans/2026-10-08-paket-rb3-totmann-ping` (dieselben wie in `rb3_mutanten.sh`). Fehlt die Kopie, vorher neu bauen. +``` + +wird: + +```markdown +- **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. +``` + +### `docs/plans/2026-10-09-mutanten-kopie-loopback.md`, Stelle 2 von 3 + +Aus: + +```powershell +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/rb3b-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" +``` + +wird: + +```powershell +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" +``` + +### `docs/plans/2026-10-09-mutanten-kopie-loopback.md`, Stelle 3 von 3 + +Aus: + +```markdown +Die übrigen Mutanten haben denselben Status wie in Teil A von `rb3_mutanten.sh`. +``` + +wird: + +```markdown +Die übrigen Mutanten haben denselben Status wie in Teil A von `rb3d_mutanten.sh`. +``` + +## Schritt 6 — Abschlussprüfung + +In der Git-Bash: + +```bash +# keine CR in den fuenf Zieldateien, nichts ausserhalb der Zieldateien geaendert +cd /d/Claude/.vault-tools/plan-kopie +for f in mutanten_lauf_kopie_treiber.py mutanten_lauf_kopie.sh LIESMICH.md mutanten_lauf_ENTWURF.md docs/plans/2026-10-09-mutanten-kopie-loopback.md; do printf '%s %s\n' "$f" "$(tr -cd '\r' < "$f" | wc -c)"; done +ls -d __pycache__ 2>/dev/null || echo kein-pycache +git -C /d/Claude status --porcelain -- .vault-tools +``` + +Erwartet: je Datei die Zahl `0`, dann `kein-pycache`, dann dieselben sechs Zeilen wie in Schritt 0. + +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 (Treiber 7, Skript 1, LIESMICH 1, Entwurf 1, Plan 3); +- die Ausgaben aus Schritt 0, 1, 2, 3 (alle `Select-String`-Treffer im Wortlaut), 4 (Variantenzeilen und alle `Select-String`-Treffer im Wortlaut) und 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 fünf Zieldateien sind byte-gleich mit der Kopie, auf die diese Nachbesserung per `plan_materialisiere.py` angewendet wurde (`cmp`); sonst ist unter `.vault-tools/` nichts geändert. +2. Treiber in WSL gegen das Skript nach Nachbesserung 1, selbst gefahren: Exit 0, `gerechnete Fehler: 0`, `W10 ok` bis `W13 ok` stehen da. +3. Die vier Varianten selbst gesehen: rot genau an ihren Fällen laut Tabelle in Schritt 4. Die Ausgabedateien aus dem Rot-Ordner liegen als Rohdaten neben `pruefer/` unter `AI/secondbrain/trading-plattform-memory/2026-10-09-mkopie-roh/`. +4. Schritt 2 hat `nur-kommentar` gezeigt: Das Verhalten des Skripts ist unverändert, der Lauf mit echten Daten vom 2026-10-09 gilt weiter. + +## Lauf mit echten Daten + +Entfällt. Das Skript ändert sich nur im Kommentar, Schritt 2 belegt das mit `grep -v '^#'`. Es gilt der Lauf des Prüfers vom 2026-10-09: 73 Mutanten von RB3 aus `rb3d-kopie`, alle `OK`, T1 und T12 im Namensraum. + +## Risiken + +1. **W13 auf einem Rechner ohne ambiente Fähigkeiten.** Hebt `unshare --keep-caps` die Fähigkeiten nicht in die ambiente Menge, sieht die Probe ohne `setpriv` keine, und W13 ist schon gegen das neue Skript rot. Merkmal: `W13 FEHLER: neue Kopien=[], exit=0` in Schritt 3. Dann wäre auch v7 grün statt rot; Befund für den Koordinator. +2. **Zählerdatei in W12.** Die Attrappe schreibt im Namensraum in den Fallordner des Treibers. Gelingt das nicht, zählt `ip` nicht weiter. Merkmal: `ip-aufrufe=` ungleich `2` auf dem neuen Skript. +3. **Gleichzeitige Plankritik.** Legt eine Plankritik während des Treiberlaufs `/tmp/plankritik-mut-*` an, meldet der Treiber `(aufraeumen)` für den gerade laufenden Fall. +4. **Laufzeit.** Fünf Treiberläufe; v7 wartet in W6 bis zum Ende der Schleife (rund sechs Sekunden). Zusammen gerechnet unter einer Viertelstunde, gemessen nicht. + +## Getroffene Annahmen + +- W13 ließ sich ohne Variante bauen: eine `setpriv`-Attrappe, die nichts abwirft. v7 des Prüfers bleibt trotzdem im Nachweis, wie beauftragt. Weil v7 W13 nicht rot macht (die Probe scheitert dort ebenfalls), kommt v9 dazu: Probe ohne Fähigkeitsprüfung, rot nur an W13. +- Für den Zweig nach `||` im Hauptaufruf kommt Variante v8 dazu. Der Auftrag nannte nur v6 und v7; v6 lässt W12 grün, v7 macht W12 nur über die Probe rot. +- W10 bis W13 fahren keinen Kontrolllauf wie W4, W5 und W9. Jeder prüft stattdessen ein Merkmal, das nur seine Attrappe oder die Probe erzeugt (Text der Attrappe, `Errno 101`, Zählerstand, `Faehigkeiten nicht abgeworfen`). Dass ein Ordner nur aus Verweisen ein grünes Skript ergibt, zeigt der Kontrolllauf in W4. +- „Keine Reste“ in W12 heißt: kein neuer Ordner `/tmp/plankritik-mut-*` und die übergebene Kopie byte-gleich. Prozessreste prüft W6. +- Das echte `ip` holt W12 über `shutil.which("ip")` des Treibers statt fest über `/usr/bin/ip` wie der Prüfer. +- Kein neuer Lauf gegen das Skript aus HEAD und gegen v1 bis v5: Sie belegen W1 bis W9, und an diesen Fällen ändert sich nichts. LIESMICH sagt deshalb, mit welchen Fällen welche Varianten gefahren sind. +- Im Plan ändert sich neben dem Datensatz auch der Verweis auf das Skript mit Teil A (`rb3d_mutanten.sh` statt `rb3_mutanten.sh`), weil `rb3_mutanten.sh` `rb3b-kopie` benutzt. Die Verweise auf `rb3_mutanten.sh` im Anlass des Plans bleiben, sie beschreiben die Geschichte. +- Der Satz zu Befund 3 steht in LIESMICH, wie beauftragt, und zusätzlich knapp im Kopfkommentar und im Entwurf, weil beide die Vorprüfung beschreiben. +- `modell: haiku`: Alles steht wörtlich da, kein Codeblock trägt typografische Zeichen. + +## Offene Punkte + +- **Gerechnet, nicht gemessen:** W13 grün gegen das neue Skript; die roten Mengen von v7, v8 und v9; bei v6 W11 und W13 rot; alle Zeilen hinter `FEHLER:` und die Zahlen hinter `gerechnete Fehler:` in Schritt 4. Gemessen sind W10 bis W12 als A1, A3 und A4, v6 grün an W1 bis W9 und rot an W10 (A6). +- **Ausgabevergleich der Probe ohne Fall.** Endet die Probe mit 0, aber mit anderer Ausgabe als `lo-probe ok`, greift der zweite Teil der Bedingung. Dafür gibt es weder Fall noch Variante; im Docstring des Treibers als Lücke benannt. +- **`plankritik.js` verliert die Meldung** weiter (offener Punkt des Plans). Mit Befund 3 trifft das jetzt auch den Hinweis auf eine fehlende `mutanten.toml`. +- **Weitere Prüfrunde:** Die Nachbesserung ändert nur Treiber und Text. Ob ein `pruefer` sie noch einmal liest oder Schritt 3 und 4 genügen, entscheidest du. diff --git a/.vault-tools/plan-kopie/docs/plans/2026-10-09-mutanten-kopie-loopback.md b/.vault-tools/plan-kopie/docs/plans/2026-10-09-mutanten-kopie-loopback.md new file mode 100644 index 0000000..9101902 --- /dev/null +++ b/.vault-tools/plan-kopie/docs/plans/2026-10-09-mutanten-kopie-loopback.md @@ -0,0 +1,1229 @@ +--- +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: `. +- **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 -- '` | +| 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: +setpriv: +python3: +unshare: +timeout: +sh: +wslpath: +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 ] +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 +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.`. 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//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. diff --git a/.vault-tools/plan-kopie/mutanten_lauf_ENTWURF.md b/.vault-tools/plan-kopie/mutanten_lauf_ENTWURF.md index c4b26f9..049ac8b 100644 --- a/.vault-tools/plan-kopie/mutanten_lauf_ENTWURF.md +++ b/.vault-tools/plan-kopie/mutanten_lauf_ENTWURF.md @@ -5,7 +5,7 @@ title: "Entwurf: mutanten_lauf.py — allgemeiner Mutantenläufer aus der Planta summary: "Entscheidungsliste, Dateiformat mutanten.toml, Ablauf, Status und Exit-Codes, Abdichtung und Angriffsliste für den allgemeinen Mutantenläufer. Punkt 3 der Arbeitsweise vom 2026-09-26." tags: [vault-tools, codeworkflow, pruefwerkzeuge] created: 2026-09-27 -updated: 2026-10-06 +updated: 2026-10-09 --- # Entwurf: `mutanten_lauf.py` @@ -163,7 +163,7 @@ Jeder pytest-Aufruf bekommt eine Kopie der Umgebung mit diesen Änderungen: - `http_proxy`, `https_proxy`, `all_proxy` und dieselben in Großschrift zeigen auf `http://127.0.0.1:9`; `no_proxy` und `NO_PROXY` werden entfernt. Bibliotheken, die Proxy-Variablen lesen, laufen damit gegen einen geschlossenen Port. - `PYTHONDONTWRITEBYTECODE=1`, `COLUMNS=300`, `MUTANTEN_LAUF_ERGEBNIS=`, `PYTHONPATH` mit dem Plugin-Ordner vorn. -**Netzsperre (seit 2026-09-28, Nachbesserung N1):** Der Wrapper `mutanten_lauf_kopie.sh` startet `mutanten_lauf.py` selbst unter `unshare --map-current-user --net`, nicht erst den einzelnen pytest-Aufruf. Gemessen in WSL Debian als Benutzer `claude` (util-linux 2.41.5): Die UID bleibt innen wie außen 1000, es gibt nur die Schnittstelle `lo`, `connect` auf eine Adresse außerhalb von localhost endet mit `OSError` (errno 101) oder `gaierror`. Nicht `-r` (macht UID 0, könnte Rechte-Tests der Mutanten selbst verfälschen). Fehlt `unshare` ohne Netz, bricht der Wrapper vorher mit `MUTANTEN_LAUF_FEHLER: unshare ohne Netz nicht moeglich` ab, statt einen ungesperrten Lauf durchzulassen. +**Netzsperre (seit 2026-09-28, Nachbesserung N1):** Der Wrapper `mutanten_lauf_kopie.sh` startet `mutanten_lauf.py` selbst unter `unshare --map-current-user --net`, nicht erst den einzelnen pytest-Aufruf. Gemessen in WSL Debian als Benutzer `claude` (util-linux 2.41.5): Die UID bleibt innen wie außen 1000, es gibt nur die Schnittstelle `lo`, `connect` auf eine Adresse außerhalb von localhost endet mit `OSError` (errno 101) oder `gaierror`. Nicht `-r` (macht UID 0, könnte Rechte-Tests der Mutanten selbst verfälschen). 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 W13), Plan `docs/plans/2026-10-09-mutanten-kopie-loopback.md` und seine Nachbesserung 1. Die Fähigkeiten prüft der Wrapper nur an der Probe (`CapEff`, `CapAmb`), nicht am Läufer. Weil die Vorprüfung vor der Suche nach `mutanten.toml` steht, endet er ohne `ip`, `setpriv`, `python3` oder nutzbares `lo` auch ohne `mutanten.toml` mit Exit 2 statt mit `KEINE_MUTANTEN_TOML`. **Was das nicht abdichtet:** einen Unterprozess mit absolutem Pfad (so kam der Vorfall vom 2026-09-26 zustande), einen Socket ohne Proxy, einen Client mit `trust_env=False` oder mit eigenem Transport, Dateien mit absolutem Pfad außerhalb von `HOME`. Die eigentliche Sperre bleibt die Netzsperre der Testdateien selbst und eine Kopie ohne Geheimnisdateien (`ohne`). diff --git a/.vault-tools/plan-kopie/mutanten_lauf_kopie.sh b/.vault-tools/plan-kopie/mutanten_lauf_kopie.sh index 34f38f3..23b4ee2 100644 --- a/.vault-tools/plan-kopie/mutanten_lauf_kopie.sh +++ b/.vault-tools/plan-kopie/mutanten_lauf_kopie.sh @@ -27,6 +27,31 @@ # mutanten_lauf.py nach dem SIGTERM nicht rechtzeitig endet; sein eigener SIGTERM-Handler setzt # die Zieldatei vorher zurueck und raeumt seine Temp-Ordner auf. # - 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 in einer Probe im +# eigenen Namensraum nicht hoch, gelingt dort keine Verbindung ueber 127.0.0.1 oder hat die +# Probe nach setpriv noch CapEff oder CapAmb ungleich 0, endet das Skript mit +# MUTANTEN_LAUF_FEHLER und Exit 2, ohne EXIT-Zeile. Die Faehigkeiten prueft das Skript nur an +# dieser Probe und nur CapEff und CapAmb, nicht am Laeufer selbst; alle vier Mengen am Laeufer +# prueft Treiber-Fall W3. Die Vorpruefung steht vor der Suche nach mutanten.toml: Scheitert +# sie, endet das Skript auch fuer einen Planordner ohne mutanten.toml mit Exit 2 statt mit +# KEINE_MUTANTEN_TOML. +# - Hauptaufruf: Kommt lo dort nicht hoch, endet das Skript nach der Modus-Zeile mit +# MUTANTEN_LAUF_FEHLER, Exit 2 und EXIT-Zeile, ohne den Laeufer zu starten. Ein Lauf ohne +# Netzsperre oder ohne lo ist nie die Ausweichloesung. +# - Treiber: mutanten_lauf_kopie_treiber.py, Faelle W1 bis W13 (W10 bis W13 seit Nachbesserung 1, +# docs/plans/2026-10-09-mutanten-kopie-loopback-nachbesserung-1.md). set -u KOPIE_WIN="${1:-}" @@ -55,6 +80,37 @@ if ! unshare --map-current-user --net true 2>/dev/null; then 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 + KOPIE=$(wslpath -u "$KOPIE_WIN") PLANORDNER=$(wslpath -u "$PLANORDNER_WIN") @@ -88,7 +144,10 @@ else echo "modus: voll" fi -timeout -k 15 "$BUDGET_S" unshare --map-current-user --net \ +# 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 \ --wurzel "$TMPKOPIE" --mutanten "$MUTANTEN_TOML" --zeitlimit "$ZEITLIMIT_S" $MUSTER_SCHALTER EXITCODE=$? diff --git a/.vault-tools/plan-kopie/mutanten_lauf_kopie_treiber.py b/.vault-tools/plan-kopie/mutanten_lauf_kopie_treiber.py new file mode 100644 index 0000000..9e71dfd --- /dev/null +++ b/.vault-tools/plan-kopie/mutanten_lauf_kopie_treiber.py @@ -0,0 +1,718 @@ +#!/usr/bin/env python3 +"""Treiber fuer mutanten_lauf_kopie.sh: Faelle W1 bis W13 (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 ] +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. + +Faelle W10 bis W13 (Nachbesserung 1, Plan +docs/plans/2026-10-09-mutanten-kopie-loopback-nachbesserung-1.md) ersetzen im +Attrappen-Ordner einzelne Werkzeuge durch kleine sh-Skripte, nach den +Angriffen A1, A3 und A4 des Pruefers vom 2026-10-09: ip scheitert in der +Probe (W10), ip meldet Erfolg und lo bleibt aus (W11), ip scheitert erst im +Hauptaufruf (W12), setpriv wirft keine Faehigkeit ab (W13). + +Nicht im Treiber: ob plankritik.js das Skript so aufruft wie hier +(MSYS_NO_PATHCONV, cygpath -m -l); ob die Probe bei Exit 0 mit falscher +Ausgabe scheitert. +""" + +import argparse +import hashlib +import os +import shlex +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, W9 und W10 bis W13 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 baue_attrappen_mit_ersatz(ordner, ersatz): + # Wie baue_attrappen, aber jedes Werkzeug aus ersatz ist ein kleines + # sh-Skript statt eines Verweises auf das echte Werkzeug (Bauweise aus den + # Angriffen des Pruefers vom 2026-10-09). + ordner.mkdir() + for name in WERKZEUGE: + pfad = ordner / name + if name in ersatz: + pfad.write_text("#!/bin/sh\n" + ersatz[name] + "\n", encoding="utf-8") + pfad.chmod(0o755) + else: + os.symlink(shutil.which(name), pfad) + + +def fahre_probe_scheitert(name, skript, ersatz, merkmal): + # Die Vorpruefung muss scheitern: Exit 2 mit der Meldung der Probe und dem + # Merkmal des Angriffs, ohne Modus-Zeile, ohne EXIT-Zeile, ohne Kopie. + fall = fall_einfach() + try: + ordner = fall.basis / "attrappen" + baue_attrappen_mit_ersatz(ordner, ersatz) + vorher = plankritik_ordner() + ergebnis = starte_skript(skript, fall, pfad=str(ordner)) + neu = sorted(str(p) for p in plankritik_ordner() - vorher) + ok = ( + ergebnis.returncode == 2 + and "MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht nutzbar" in ergebnis.stdout + and merkmal in ergebnis.stdout + and "modus:" not in ergebnis.stdout + and "EXIT mutanten_lauf" not in ergebnis.stdout + and not neu + ) + melde(name, ok, "neue Kopien={0}, {1}".format(neu, kurz(ergebnis))) + fall.pruefe_kopie(name) + finally: + fall.aufraeumen() + + +def fall_w10(skript): + # ip scheitert schon in der Probe (Angriff A1 des Pruefers). + fahre_probe_scheitert("W10", skript, {"ip": "echo ip-attrappe scheitert >&2; exit 1"}, "ip-attrappe scheitert") + + +def fall_w11(skript): + # ip meldet Erfolg, lo bleibt aus (A3): Die Verbindung der Probe scheitert + # mit Errno 101. + fahre_probe_scheitert("W11", skript, {"ip": "exit 0"}, "Errno 101") + + +def fall_w12(skript): + # ip klappt in der Probe und scheitert erst im Hauptaufruf (A4); eine + # Zaehlerdatei haelt fest, der wievielte Aufruf es ist. Erwartet: + # Modus-Zeile, Meldung des Hauptaufrufs, EXIT-Zeile mit 2, kein Laeufer, + # genau zwei Aufrufe von ip, keine Kopie unter /tmp. + fall = fall_einfach() + try: + zaehler = fall.basis / "ip_zaehler" + z = shlex.quote(str(zaehler)) + ip_echt = shlex.quote(shutil.which("ip")) + ersatz = {"ip": ( + "n=0\n" + "if [ -f " + z + " ]; then read n < " + z + "; fi\n" + "n=$((n + 1))\n" + "echo \"$n\" > " + z + "\n" + "if [ \"$n\" -ge 2 ]; then echo \"ip-attrappe zweiter Aufruf\" >&2; exit 1; fi\n" + "exec " + ip_echt + " \"$@\"" + )} + ordner = fall.basis / "attrappen" + baue_attrappen_mit_ersatz(ordner, ersatz) + vorher = plankritik_ordner() + ergebnis = starte_skript(skript, fall, pfad=str(ordner)) + neu = sorted(str(p) for p in plankritik_ordner() - vorher) + try: + aufrufe = zaehler.read_text(encoding="ascii").strip() + except OSError: + aufrufe = "keine" + ok = ( + ergebnis.returncode == 2 + and "modus: voll" in ergebnis.stdout + and "MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht hochgefahren" in ergebnis.stdout + and "EXIT mutanten_lauf: 2" in ergebnis.stdout + and "nicht nutzbar" not in ergebnis.stdout + and "MW7" not in ergebnis.stdout + and "ip-attrappe zweiter Aufruf" in ergebnis.stderr + and aufrufe == "2" + and not neu + ) + melde("W12", ok, "ip-aufrufe={0}, neue Kopien={1}, {2}".format(aufrufe, neu, kurz(ergebnis))) + fall.pruefe_kopie("W12") + finally: + fall.aufraeumen() + + +SETPRIV_OHNE_WIRKUNG = ( + "while [ \"$#\" -gt 0 ] && [ \"$1\" != \"--\" ]; do shift; done\n" + "shift\n" + "exec \"$@\"" +) + + +def fall_w13(skript): + # setpriv wirft keine Faehigkeit ab und startet nur den Befehl hinter --: + # Die Faehigkeitspruefung der Probe muss anschlagen. + fahre_probe_scheitert("W13", skript, {"setpriv": SETPRIV_OHNE_WIRKUNG}, "Faehigkeiten nicht abgeworfen") + + +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), + ("W10", fall_w10), + ("W11", fall_w11), + ("W12", fall_w12), + ("W13", fall_w13), +] + + +def main(): + zerleger = argparse.ArgumentParser(description="Treiber fuer mutanten_lauf_kopie.sh (Faelle W1 bis W13)") + 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())