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