- H1: CommandLineTool.execute_streaming now purely delegates to injected executor; removes 150-line duplicate Popen loop that bypassed 1MB cap, cancel() and timeout protections - H2: thread the blocking readline into a daemon reader thread; main loop polls queue with 0.1s ticks so timeout/1MB checks fire even for silent commands (verified: 3s timeout on 15s sleep, was 15s+ before) - H3: stdin=DEVNULL on both Popen sites; stdin-waiting commands now hit EOF instead of deadlocking the tool thread forever - M1: heredoc delimiter uniquification in write_file guidance (content line equal to AIBOX_EOF no longer truncates into shell injection) - M2: 3600s total deadline on registry streaming forward loop Verification: PoC regression 5/5 PASS (timeout/1MB/cancel/delegation), full unit suite 264 passed. Audit report updated with Batch E results.
99 lines
3.5 KiB
Python
99 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""审计修复 PoC 回归验证(H1/H2/H3 + 1MB + cancel)。
|
|
|
|
验证 2026-08-17 审计五项修复的实际运行效果:
|
|
PoC-1 (H2): 无输出命令 + timeout=3s → 应 ~3s 返回超时(修复前阻塞 15s+)
|
|
PoC-2 (H3): 等待 stdin 的命令 + timeout=3s → 应超时返回而非永久挂起
|
|
PoC-3 : 输出 >1MB → 应截断且总输出 ≈1MB
|
|
PoC-4 : cancel() 应在宽限时间内终止长命令
|
|
PoC-5 (H1) : CommandLineTool(工具层,非 executor)跑 >1MB 输出 →
|
|
截断生效即证明已走 executor 委托路径
|
|
"""
|
|
|
|
import queue
|
|
import sys
|
|
import threading
|
|
import time
|
|
import types
|
|
from pathlib import Path
|
|
|
|
AIBOX_DIR = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(AIBOX_DIR))
|
|
sys.modules.setdefault("websockets", types.ModuleType("websockets"))
|
|
|
|
from subsystems.executors import SubprocessCommandExecutor
|
|
from tools.command_tool import CommandLineTool
|
|
|
|
results = []
|
|
|
|
|
|
def check(name, ok, detail):
|
|
results.append((name, ok, detail))
|
|
print(f"[{'PASS' if ok else 'FAIL'}] {name}: {detail}")
|
|
|
|
|
|
# ---------- PoC-1 (H2): 无输出命令超时 ----------
|
|
ex = SubprocessCommandExecutor()
|
|
t0 = time.time()
|
|
r = ex.execute("powershell -NoProfile -Command Start-Sleep 15", timeout=3)
|
|
elapsed = time.time() - t0
|
|
check("PoC-1 无输出命令超时(H2)", elapsed < 6 and "超时" in r,
|
|
f"{elapsed:.1f}s 返回(修复前阻塞15s+),结果含超时: {'超时' in r}")
|
|
|
|
# ---------- PoC-2 (H3): 等待 stdin 的命令 ----------
|
|
# stdin=DEVNULL 下命令立即收到 EOF:要么快速退出,要么按 timeout 返回;
|
|
# 判定标准 = 不永久挂起(修复前 readline 无 EOF 永久阻塞)。
|
|
t0 = time.time()
|
|
r = ex.execute("powershell -NoProfile -Command Read-Host", timeout=3)
|
|
elapsed = time.time() - t0
|
|
check("PoC-2 stdin等待命令不挂起(H3)", elapsed < 6,
|
|
f"{elapsed:.1f}s 返回(修复前永久挂起)")
|
|
|
|
# ---------- PoC-3: 1MB 输出截断 ----------
|
|
t0 = time.time()
|
|
r = ex.execute(
|
|
'powershell -NoProfile -Command "$s=\'x\'*2048; 1..1024 | ForEach-Object { $s }"',
|
|
timeout=60)
|
|
elapsed = time.time() - t0
|
|
size_mb = len(r.encode("utf-8", errors="replace")) / 1024 / 1024
|
|
check("PoC-3 1MB输出截断", "截断" in r and size_mb < 1.15,
|
|
f"{elapsed:.1f}s,结果 {size_mb:.2f}MB,含截断标记: {'截断' in r}")
|
|
|
|
# ---------- PoC-4: cancel() 中断 ----------
|
|
out_q = queue.Queue()
|
|
done = threading.Event()
|
|
|
|
|
|
def _run():
|
|
ex.execute_streaming(
|
|
"powershell -NoProfile -Command Start-Sleep 60",
|
|
out_q, timeout=120)
|
|
done.set()
|
|
|
|
|
|
t0 = time.time()
|
|
th = threading.Thread(target=_run, daemon=True)
|
|
th.start()
|
|
time.sleep(2)
|
|
ex.cancel()
|
|
done.wait(timeout=10)
|
|
elapsed = time.time() - t0
|
|
check("PoC-4 cancel中断", done.is_set() and elapsed < 8,
|
|
f"cancel 后 {elapsed:.1f}s 结束(修复前杀不到进程需等满120s)")
|
|
|
|
# ---------- PoC-5 (H1): 工具层委托 executor(截断保护贯通) ----------
|
|
tool = CommandLineTool(executor=ex)
|
|
t0 = time.time()
|
|
r = tool.execute(
|
|
command='powershell -NoProfile -Command "$s=\'y\'*2048; 1..1024 | ForEach-Object { $s }"',
|
|
timeout=60)
|
|
elapsed = time.time() - t0
|
|
size_mb = len(r.encode("utf-8", errors="replace")) / 1024 / 1024
|
|
check("PoC-5 工具层1MB贯通(H1)", "截断" in r and size_mb < 1.15,
|
|
f"{elapsed:.1f}s,工具层结果 {size_mb:.2f}MB(修复前无限累积)")
|
|
|
|
# ---------- 汇总 ----------
|
|
fails = [n for n, ok, _ in results if not ok]
|
|
print("\n==== 汇总: %d/%d PASS ====" % (len(results) - len(fails), len(results)))
|
|
sys.exit(1 if fails else 0)
|