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