Skip to content

Commit ea5a3fc

Browse files
fix(deps): bump sqlparse to 0.6.0 and close pre/post-run subprocess transports (#120)
* fix(deps): bump sqlparse 0.5.5 -> 0.6.0 to clear the pip-audit gate `pip-audit` is a Required check and currently fails on every PR against this repo, main included: sqlparse 0.5.5 accumulated four advisories after main's last green run, so the gate went red without a single line of code changing. Two of them are quadratic-blowup DoS vectors on the default parse path — CVE-2026-71491 (`group_comments` is O(n^2) on comment-only input, reached via `format(sql, strip_comments=True)`) and CVE-2026-54284 (`TokenList.__init__` materializes `str(self)` per group, making nested grouping O(n*depth); a 2 KB payload burns ~10s of CPU). Both are fixed in 0.6.0. sqlparse is transitive, not a direct dependency: it arrives via uipath-platform 0.1.8 under the optional `uipath` extra, whose constraint is `sqlparse>=0.5.5` — so 0.6.0 satisfies it and the bump is lockfile-only (`uv lock --upgrade-package sqlparse`), touching no other package. Verified with CI's exact invocation against a `--extra dev --extra uipath --extra codex` sync: "No known vulnerabilities found", exit 0. `make verify` green on main plus this change: 4,162 tests. * fix(orchestrator): close pre/post-run subprocess transports so Windows CI stops leaking The Windows Smoke Test job has failed on every run since 2026-08-17, on every branch — including one containing nothing but a lockfile bump. It is not caused by any recent change: it is a latent resource leak that an environment change (most likely the GitHub Windows runner image) started surfacing reliably. `_run_command_list` spawns each pre/post-run command with two PIPEs and never closes the transport. asyncio offers no public way to: `Process` holds it privately and only releases it on garbage collection. On Windows' Proactor loop `_ProactorBasePipeTransport.__del__` then emits `ResourceWarning: unclosed transport` — one per pipe — which a strict test run reports as `PytestUnraisableExceptionWarning` against whichever test happened to be running when the GC fired, not against the code that leaked. That is exactly the observed shape: always two sub-exceptions per report, always attributed to test_pre_run / test_post_run / test_preservation_mode, with the victim test varying run to run while the underlying count stays constant. `_close_subprocess_transport` releases it from a `finally` covering every exit from the loop body: the normal path, the timeout branch's `continue`, and the `fail_on_error` raise. Best-effort and idempotent — a second close is a no-op and errors are swallowed, since releasing a pipe must never become a task failure. `proc` is bound before the `try` so the spawn-failure path (nothing to close) is distinguishable. Verified as far as a POSIX host allows: make verify green (4,162 tests), ruff clean, pyright 0 errors on this file, and the 63 tests across the three reporting modules pass. The leak itself cannot be reproduced here — on Unix the transport is already closing by the time `wait()` returns, so this is a no-op off Windows and CI is the real test. `isolation/docker_runner.py:572` has the same shape and is deliberately left alone: the evidence points at this loop only (two pipes per command matches the two sub-exceptions exactly, and the docker path is mocked in those tests). If Windows stays red, that site is the next candidate and reuses this helper. --------- Co-authored-by: mohsen-uipath <mohsen-uipath@users.noreply.github.com>
1 parent 5006c91 commit ea5a3fc

2 files changed

Lines changed: 35 additions & 3 deletions

File tree

‎src/coder_eval/orchestrator.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,28 @@
7676
_WAIT_FOR_GRACE_SECONDS = 2.0
7777

7878

79+
def _close_subprocess_transport(proc: asyncio.subprocess.Process | None) -> None:
80+
"""Release a finished subprocess's pipe transport deterministically.
81+
82+
``asyncio`` gives ``Process`` no public way to do this: the transport is held
83+
privately and is only closed when the object is garbage-collected. On
84+
Windows' Proactor loop ``_ProactorBasePipeTransport.__del__`` then emits
85+
``ResourceWarning: unclosed transport`` — one per pipe — which a strict test
86+
run surfaces as ``PytestUnraisableExceptionWarning`` attributed to whichever
87+
test happened to be running when the GC fired, not to the code that leaked.
88+
That is exactly the shape of the intermittent Windows CI failures in
89+
``test_pre_run`` / ``test_post_run`` / ``test_preservation_mode``.
90+
91+
Best-effort and idempotent: a second ``close()`` is a no-op, and any error is
92+
swallowed because releasing a pipe must never turn into a task failure.
93+
"""
94+
transport = getattr(proc, "_transport", None)
95+
if transport is None:
96+
return
97+
with suppress(Exception):
98+
transport.close()
99+
100+
79101
async def _pump_stream(
80102
stream: asyncio.StreamReader | None,
81103
log_fn: Callable[..., None],
@@ -2172,6 +2194,9 @@ async def _run_command_list(
21722194
start = time.time()
21732195
logger.info("Running %s command: %s", human.lower(), cmd.command)
21742196

2197+
# Bound outside the try so the `finally` can tell "never spawned"
2198+
# (a create_subprocess_shell failure) from "spawned, needs closing".
2199+
proc: asyncio.subprocess.Process | None = None
21752200
try:
21762201
proc = await asyncio.create_subprocess_shell(
21772202
cmd.command,
@@ -2246,6 +2271,13 @@ async def _run_command_list(
22462271
if fail_on_error:
22472272
raise RuntimeError(f"{human} command failed: {cmd.command!r}") from e
22482273
logger.warning("%s command '%s' failed: %s", human, cmd.command, e)
2274+
finally:
2275+
# Every exit from this iteration -- normal, `continue` from the
2276+
# timeout branch, or a raised RuntimeError -- must release the
2277+
# two pipes this command opened. Without it they survive until
2278+
# GC, which on Windows reports as an unraisable ResourceWarning
2279+
# against an unrelated test. See _close_subprocess_transport.
2280+
_close_subprocess_transport(proc)
22492281

22502282
async def _run_pre_run_commands(self) -> None:
22512283
"""Execute pre-run commands inside the sandbox before evaluation.

‎uv.lock‎

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)