[BUG][Agent] run_ps() ohne Timeout — ein hängender PowerShell-Aufruf blockiert den gesamten VM-Scan dauerhaft #214

Open
opened 2026-07-20 22:55:22 +00:00 by chinux · 1 comment
Owner

Befund

Windows-VM-Guest-Agent/src/collect.rs:32:

pub async fn run_ps(script: &str) -> String {
    match tokio::process::Command::new("powershell.exe")
        .args(["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script])
        .output()
        .await

Kein Timeout. run_ps_blocking() (Zeile 50) ebenso. Damit kann jeder PowerShell-gestuetzte Collector unbegrenzt haengen: collect_updates, collect_docker, collect_disk, collect_network, collect_ports, collect_cpu, kernel_version, hostname_string.

Timeouts existieren im Agent bisher nur beim HTTP-Probing (collect.rs:663, 700/900 ms) und beim WS-Connect (main.rs:124, 15 s). Der gesamte Scan-Pfad ist ungeschuetzt.

Auswirkung

collect_scan() sammelt sieben Collectors via tokio::join!. join! wartet auf alle — ein einziger haengender PowerShell-Prozess blockiert den kompletten Scan dauerhaft. Der Agent bleibt WS-seitig verbunden (Heartbeat laeuft in einer eigenen Task), liefert aber nie wieder ein vm_scan. Im Dashboard sieht das aus wie "Agent verbunden, Daten veraltet" — eine Fehlersignatur, die schwer zuzuordnen ist, weil sie nicht wie ein Absturz aussieht.

Realistische Ausloeser: WMI-Hang (klassisch nach Windows-Updates), winget wartet auf einen Quellen-Refresh, Get-CimInstance auf einem ueberlasteten RDS-Host, Netzlaufwerk-Timeout in Get-Disk.

Fix

Haerte die PowerShell-Aufrufe im Windows-VM-Guest-Agent gegen Haenger ab.

--- 1. TIMEOUT ---
collect.rs: run_ps() bekommt einen Timeout-Wrapper.

    pub async fn run_ps_timeout(script: &str, secs: u64) -> Option<String>

Implementierung mit tokio::time::timeout um .output(). Bei Ablauf: Child
KILLEN (nicht nur den Future droppen — .output() konsumiert das Child, also
auf .spawn() + wait_with_output() umbauen, damit ein Handle zum Killen
bleibt), WARN loggen, None zurueckgeben.

run_ps() bleibt als duenner Wrapper bestehen:
    run_ps(s) = run_ps_timeout(s, DEFAULT_PS_TIMEOUT).await.unwrap_or_default()
So sind alle bestehenden Aufrufer sofort geschuetzt, ohne sie einzeln
anzufassen.

--- 2. TIMEOUT-BUDGETS ---
Nicht ein Wert fuer alles. Als Konstanten oben in der Datei:

    DEFAULT_PS_TIMEOUT      = 15 s   // hostname, kernel, os, cpu, ram
    PS_TIMEOUT_INVENTORY    = 30 s   // disk, network, ports, services
    PS_TIMEOUT_UPDATES      = 180 s  // winget/Windows Update sind legitim langsam

collect_updates() explizit auf PS_TIMEOUT_UPDATES setzen — mit dem Default
wuerde ein normaler Update-Check faelschlich abgebrochen.

--- 3. GESAMTBUDGET FUER collect_scan ---
Zusaetzlich das ganze tokio::join! in ein timeout() legen (z.B. 240 s).
Laeuft es ab, wird ein TEILWEISER Scan gesendet: die fertigen Felder
gefuellt, die haengenden weggelassen. Ein Teilscan ist besser als gar keiner
— und "Feld fehlt" ist im Backend bereits ein behandelter Zustand.

Im Payload ergaenzen:
    "scan_partial": true,
    "scan_failed_collectors": ["updates", "ports"]
Damit ist im Backend und in der Doku unterscheidbar, ob ein Feld leer ist,
weil es nichts zu melden gibt, oder weil der Collector nicht durchkam. Genau
diese Unterscheidung fehlt heute an mehreren Stellen (vgl. #212).

--- 4. run_ps_blocking ---
Gleiche Behandlung mit std::process + wait_timeout-Pattern (eigener Thread
oder busy-wait mit try_wait in kurzem Intervall, keine neue Dependency).
Aufrufer pruefen und ggf. auf die async-Variante umstellen, falls es keinen
Grund fuer blocking gibt.

--- 5. LOGGING ---
Bei jedem Timeout eine WARN-Zeile mit Collector-Name und Budget. Die Zeile
muss im normalen Agent-Log landen, damit der Fall ueber die Log-Ansicht
diagnostizierbar ist statt nur als "Daten veraltet" aufzufallen.

--- 6. LINUX-AGENT PRUEFEN ---
VM-Guest-Agent (Linux) auf dasselbe Muster pruefen — Kommando-Aufrufe ohne
Timeout dort ebenfalls absichern. Falls bereits geschuetzt: im Issue
vermerken, kein Code noetig.

--- 7. TEST ---
Ein Skript, das garantiert haengt ("Start-Sleep -Seconds 600"), gegen
run_ps_timeout mit 2 s: kehrt nach ~2 s mit None zurueck, und der
powershell.exe-Prozess ist danach nachweislich beendet (kein Zombie).

Definition of Done

  • Kein PowerShell-Aufruf im Agent ohne Timeout
  • Timeout killt den Child-Prozess, kein Zombie
  • Update-Check hat ein eigenes, grosszuegiges Budget
  • Teilscan statt Totalausfall, mit scan_partial im Payload
  • Timeouts erscheinen als WARN im Agent-Log
  • Linux-Agent geprueft
## Befund `Windows-VM-Guest-Agent/src/collect.rs:32`: ```rust pub async fn run_ps(script: &str) -> String { match tokio::process::Command::new("powershell.exe") .args(["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script]) .output() .await ``` **Kein Timeout.** `run_ps_blocking()` (Zeile 50) ebenso. Damit kann jeder PowerShell-gestuetzte Collector unbegrenzt haengen: `collect_updates`, `collect_docker`, `collect_disk`, `collect_network`, `collect_ports`, `collect_cpu`, `kernel_version`, `hostname_string`. Timeouts existieren im Agent bisher nur beim HTTP-Probing (`collect.rs:663`, 700/900 ms) und beim WS-Connect (`main.rs:124`, 15 s). Der gesamte Scan-Pfad ist ungeschuetzt. ## Auswirkung `collect_scan()` sammelt sieben Collectors via `tokio::join!`. `join!` wartet auf **alle** — ein einziger haengender PowerShell-Prozess blockiert den kompletten Scan dauerhaft. Der Agent bleibt WS-seitig verbunden (Heartbeat laeuft in einer eigenen Task), liefert aber nie wieder ein `vm_scan`. Im Dashboard sieht das aus wie "Agent verbunden, Daten veraltet" — eine Fehlersignatur, die schwer zuzuordnen ist, weil sie nicht wie ein Absturz aussieht. Realistische Ausloeser: WMI-Hang (klassisch nach Windows-Updates), `winget` wartet auf einen Quellen-Refresh, `Get-CimInstance` auf einem ueberlasteten RDS-Host, Netzlaufwerk-Timeout in `Get-Disk`. ## Fix ``` Haerte die PowerShell-Aufrufe im Windows-VM-Guest-Agent gegen Haenger ab. --- 1. TIMEOUT --- collect.rs: run_ps() bekommt einen Timeout-Wrapper. pub async fn run_ps_timeout(script: &str, secs: u64) -> Option<String> Implementierung mit tokio::time::timeout um .output(). Bei Ablauf: Child KILLEN (nicht nur den Future droppen — .output() konsumiert das Child, also auf .spawn() + wait_with_output() umbauen, damit ein Handle zum Killen bleibt), WARN loggen, None zurueckgeben. run_ps() bleibt als duenner Wrapper bestehen: run_ps(s) = run_ps_timeout(s, DEFAULT_PS_TIMEOUT).await.unwrap_or_default() So sind alle bestehenden Aufrufer sofort geschuetzt, ohne sie einzeln anzufassen. --- 2. TIMEOUT-BUDGETS --- Nicht ein Wert fuer alles. Als Konstanten oben in der Datei: DEFAULT_PS_TIMEOUT = 15 s // hostname, kernel, os, cpu, ram PS_TIMEOUT_INVENTORY = 30 s // disk, network, ports, services PS_TIMEOUT_UPDATES = 180 s // winget/Windows Update sind legitim langsam collect_updates() explizit auf PS_TIMEOUT_UPDATES setzen — mit dem Default wuerde ein normaler Update-Check faelschlich abgebrochen. --- 3. GESAMTBUDGET FUER collect_scan --- Zusaetzlich das ganze tokio::join! in ein timeout() legen (z.B. 240 s). Laeuft es ab, wird ein TEILWEISER Scan gesendet: die fertigen Felder gefuellt, die haengenden weggelassen. Ein Teilscan ist besser als gar keiner — und "Feld fehlt" ist im Backend bereits ein behandelter Zustand. Im Payload ergaenzen: "scan_partial": true, "scan_failed_collectors": ["updates", "ports"] Damit ist im Backend und in der Doku unterscheidbar, ob ein Feld leer ist, weil es nichts zu melden gibt, oder weil der Collector nicht durchkam. Genau diese Unterscheidung fehlt heute an mehreren Stellen (vgl. #212). --- 4. run_ps_blocking --- Gleiche Behandlung mit std::process + wait_timeout-Pattern (eigener Thread oder busy-wait mit try_wait in kurzem Intervall, keine neue Dependency). Aufrufer pruefen und ggf. auf die async-Variante umstellen, falls es keinen Grund fuer blocking gibt. --- 5. LOGGING --- Bei jedem Timeout eine WARN-Zeile mit Collector-Name und Budget. Die Zeile muss im normalen Agent-Log landen, damit der Fall ueber die Log-Ansicht diagnostizierbar ist statt nur als "Daten veraltet" aufzufallen. --- 6. LINUX-AGENT PRUEFEN --- VM-Guest-Agent (Linux) auf dasselbe Muster pruefen — Kommando-Aufrufe ohne Timeout dort ebenfalls absichern. Falls bereits geschuetzt: im Issue vermerken, kein Code noetig. --- 7. TEST --- Ein Skript, das garantiert haengt ("Start-Sleep -Seconds 600"), gegen run_ps_timeout mit 2 s: kehrt nach ~2 s mit None zurueck, und der powershell.exe-Prozess ist danach nachweislich beendet (kein Zombie). ``` ## Definition of Done - [ ] Kein PowerShell-Aufruf im Agent ohne Timeout - [ ] Timeout killt den Child-Prozess, kein Zombie - [ ] Update-Check hat ein eigenes, grosszuegiges Budget - [ ] Teilscan statt Totalausfall, mit `scan_partial` im Payload - [ ] Timeouts erscheinen als WARN im Agent-Log - [ ] Linux-Agent geprueft
Author
Owner

Blockiert #215 — die neuen Collectors dürfen nicht ohne Timeout-Budget dazukommen.

Blockiert #215 — die neuen Collectors dürfen nicht ohne Timeout-Budget dazukommen.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
chinux/theProx#214
No description provided.