fix(core): close HIGH ASVS findings V7.1.1 and correctness bugs BUG-1..6

- V7.1.1: /swarm/check no longer returns raw exception text; logs detail
  server-side, returns generic 'probe failed'.
- BUG-1: register EditAction -> SSHDriver so edit ticks no longer crash.
- BUG-2: topology reconcile matches generator-named deckies by
  expected-name membership instead of a hyphen heuristic.
- BUG-3: intel provider lookups acquire the per-provider semaphore so
  declared concurrency bounds are enforced.
- BUG-4: RuleIndex.install evicts a rule from kinds it no longer applies to.
- BUG-5: UnixSocketBus.connect() is lock-guarded with a double-check so
  concurrent first-connects open exactly one socket and reader task.
- BUG-6/V5.1.3: multi-token JSON-field search binds each token to a
  distinct parameter instead of collapsing to the last value.

Regression tests added for every fix, verified red-before/green-after.
V4.1.1c/V12.1.1 (updater master-CN gate) and V12.5.1 (tarball include-list)
confirmed already fixed in prior commits and left untouched.
This commit is contained in:
2026-06-09 23:12:49 -04:00
parent 8d18c59201
commit 6a8af315fb
16 changed files with 737 additions and 24 deletions

View File

@@ -7,6 +7,7 @@ the tmp filesystem — no Docker, no external broker.
from __future__ import annotations
import asyncio
import contextlib
import pathlib
import stat
@@ -130,3 +131,52 @@ class TestEndToEnd:
server = BusServer(sock, group=None)
with pytest.raises(FileNotFoundError):
await server.start()
class TestConcurrentConnect:
"""BUG-5: connect() must hold the bus lock so concurrent first-connects
can't each open a socket and spawn a reader (orphaning the loser's FD +
reader_loop task)."""
async def test_concurrent_connect_opens_one_socket_and_reader(
self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
) -> None:
sock = tmp_path / "bus.sock"
server = BusServer(sock, group=None)
await server.start()
serve_task = asyncio.create_task(server.serve_forever())
bus = UnixSocketBus(sock, client_name="race-client")
# Wrap the real transport opener with a counter, and yield control
# mid-open so two racing connect() calls actually interleave. Without
# the lock both would pass the `self._writer is None` guard and each
# open a socket; the lock + re-check collapse that to one open.
real_open = asyncio.open_unix_connection
calls = 0
async def _counting_open(*args, **kwargs):
nonlocal calls
calls += 1
await asyncio.sleep(0) # force the scheduler to interleave callers
return await real_open(*args, **kwargs)
monkeypatch.setattr(asyncio, "open_unix_connection", _counting_open)
try:
await asyncio.gather(bus.connect(), bus.connect(), bus.connect())
# Exactly one socket opened and exactly one live reader task.
assert calls == 1
assert bus._writer is not None
assert bus._reader_task is not None
assert not bus._reader_task.done()
finally:
await bus.close()
serve_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await serve_task
await server.close()
# close() reaped the single reader task — no orphans left behind.
assert bus._reader_task is None