Skip to content

fix: remediate 2026-10-03 code review (F1-F8) - #32

Merged
libre-7 merged 2 commits into
mainfrom
fix/audit-2026-10-03
Oct 4, 2026
Merged

libre-7 merged 2 commits into
mainfrom
fix/audit-2026-10-03

Conversation

@libre-7

@libre-7 libre-7 commented Oct 4, 2026

Copy link
Copy Markdown
Owner

Summary

Remediation of the 2026-10-03 code review of main @ 82c8882 (v1.3.0). Eight findings: two High, three Medium, three Low.

No image behaviour change for a correctly-configured deployment. The point of F1/F2/F3 is that three failure modes stop being silent and start being loud.

Fixes the review of f3117dd (2026-09-29) landed are all still intact — this builds on PRs #28–#31 rather than reverting them.

The two High findings

F1 — install-websockets.sh installed websockets where the gateway cannot import it.

The script used bare pip / python3 via docker exec. The gateway runs /app/venv/bin/python3, in a venv created with include-system-site-packages = false:

$ cat /app/venv/pyvenv.cfg
include-system-site-packages = false
'/usr/local/lib/python3.12/site-packages' in sys.path   ->  False

So the dependency landed in the system interpreter (or ~/.local) — neither is on the venv's sys.path — and the script then "verified" it with the same wrong interpreter, printed success, and left the platform unable to load. The documented happy path (README tells users to re-run this after every rebuild) could not work.

The same defect made the adapter lookup fail: import plugins... under the wrong interpreter raises, then find /app/venv -path '*/simplex/adapter.py' returns nothing on a current Hermes layout (the plugin lives in /app/hermes-agent-src). The script printed ⚠ skipping DM send verification and exited 0 — so the verification PR #28 added as its headline fix never ran.

Now: resolves the venv explicitly (HERMES_VENV, default /app/venv) for install and verification, pins websockets==17.0.1 to match the image, widens the find, and warns loudly if the venv is missing instead of silently falling back.

F2 — both readiness gates matched any port containing the number.

ss -tln | grep -q :5225     # also matches 15225, 52250, 52251…

Under network_mode: host — which this project requires, so the containers share loopback — ss lists the entire host's listening sockets. An unrelated host service on a port containing 5225 would satisfy the startup gate and report the WebSocket API ready while simplex-chat was still starting or already dead. The socat gate had the same exposure.

Replaced with a shared port_listening() helper that anchors on the exact port. Both code paths anchor deliberately: the sport = :PORT filter is also re-verified with awk rather than trusted via grep -q ., because an ss that doesn't understand the filter prints the whole socket table and grep -q . would match any line. (That second bug was in my first attempt at this fix — the mutation test caught it.)

Medium and Low

# Fix
F3 /_address_settings 1 … used a literal 1. The daemon's contract is /_address_settings <userId> <json(settings)> (verified in bots/api/COMMANDS.md @ v7.0.2), so the id now comes from the /user activeUser response, with userContactLinkCreated / usersList fallbacks. Success was detected by searching any event for the substring userContactLinkUpdated; it now correlates the corrId the daemon echoes (Server.hs wraps every response as {corrId, resp}). Skips with a warning when no id is determinable, rather than applying settings to a guessed profile.
F4 README.md:196 still described the removed sed -i adapter patching, fourteen lines below the note saying it was removed.
F5 base-refresh.yml called skopeo, which isn't on ubuntu-latest and had no install step — and with no set -e the empty result was silent. Now uses preinstalled docker buildx imagetools inspect, set -euo pipefail, and fails loudly on an empty digest.
F6 Unraid template floated :latest while compose pinned a digest, so Unraid's Update button could silently jump versions. Pinned to v1.3.0.
F7 README.md:238 said "v1.2.0" directly above a # v1.3.0 digest.
F8 Added SECURITY.md — private reporting path plus an explicit threat-model statement (unauthenticated API, loopback default, socat opt-in removes that protection).

Tests

No Docker or network needed — bash tests/run-all.sh:

  • test-port-gate.sh — creates real TCP listeners and asserts port_listening() matches an exact port: decoy-port case, the awk fallback for old iproute2, empty socket table, and fail-closed when ss is absent. The function body is extracted from entrypoint.sh at runtime so the test can't drift from what ships.
  • test-installer-interpreter.sh — stubs docker and pip, asserts the installer uses only absolute venv paths, pins the version, finds an adapter outside the venv, and fails on an unrecognised adapter. Skips (exit 77) where no Hermes install exists.
  • test-setup-userid.py — drives the real setup block from entrypoint.sh against a fake WebSocket daemon, including a wrong-corrId decoy that the old substring match would have accepted as success.
  • check-docs.py — pin/version agreement across README, compose, and the template; documented env vars exist in code; installer executes no sed -i.

Wired into the lint CI job so a regression fails the build rather than surfacing at runtime.

Verification

Gate Result
bash tests/run-all.sh all gates passed
shellcheck entrypoint.sh install-websockets.sh tests/*.sh clean, 0 findings
py_compile (healthcheck + tests) OK
YAML / XML parse OK
Mutation testing each behavioural suite re-run against a deliberately re-introduced version of its bug → failed as expected, then green again on restore

Not verified: no container was built or booted (no Docker CLI in the review environment). Socat bridge startup, first-run setup against live SMP servers, and real-silicon ARM64 remain covered only by the existing CI smoke matrix — which will exercise the reworked port_listening() on both amd64 and emulated arm64.

Upstream CLI flags used by the entrypoint were re-verified against simplex-chat v7.0.2 source (-p chat-server-port, -y yes-migrate, -r mark-read, -x SOCKS5 :9050, --create-bot-*), as was the -d file-prefix DB behaviour.

Two High, three Medium, three Low findings from the v1.3.0 audit. No image
behaviour change for correct deployments; the point of F1/F2/F3 is that
failure modes stop being silent.

Fixed:
- install-websockets.sh: install into and verify with the gateway's
  interpreter (/app/venv, include-system-site-packages=false), not bare
  pip/python3. The old pair installed websockets where the venv cannot
  import it and then "verified" it with the same wrong interpreter, so it
  reported success over a broken gateway. Install is now pinned to the
  image's websockets version; a missing venv warns instead of silently
  falling back.
- install-websockets.sh: locate the adapter with the gateway interpreter
  and widen the find fallback. `find /app/venv -path '*/simplex/adapter.py'`
  returns nothing on a current Hermes layout, so the v1.3.0 DM-send
  verification silently skipped and exited 0.
- entrypoint.sh: add port_listening() and use it for both readiness gates.
  `ss -tln | grep -q :5225` also matched 15225/52250/52251, and under host
  networking `ss` sees the whole host — an unrelated service could satisfy
  the startup gate. Both branches now anchor on the exact port: the
  `sport = :PORT` filter is re-verified with awk rather than `grep -q .`,
  since an ss that ignores the filter prints the whole socket table.
- entrypoint.sh: /_address_settings now uses the userId read from the
  /user activeUser response (contract: bots/api/COMMANDS.md) instead of a
  literal 1, and confirms success by correlating corrId instead of
  matching an event substring. Skips with a warning when no id is known.
- base-refresh.yml: skopeo is not on ubuntu-latest and had no install step,
  so the digest could come back empty with no set -e to catch it. Use the
  preinstalled buildx, set -euo pipefail, and fail loudly on empty.
- README:196 described the removed sed -i adapter patching, contradicting
  line 210. Corrected, plus a warning on why the installer targets the venv.
- Unraid template floated :latest while compose pinned a digest; pinned to
  v1.3.0 and README install steps updated to match.
- CHANGELOG: pin prose said v1.2.0 above a v1.3.0 digest.

Added:
- SECURITY.md with a private reporting path and an explicit statement of
  the threat model (unauthenticated API, loopback default, socat opt-in).
- tests/ regression suites, run by tests/run-all.sh and wired into the
  lint CI job: exact-port gate against real TCP listeners, installer
  interpreter/venv behaviour with docker+pip stubbed, and the setup
  handshake against a fake WebSocket daemon including a wrong-corrId
  decoy. check-docs.py enforces pin and env-var consistency.

Verified: shellcheck clean; 4 suites green. Each behavioural suite was
mutation-tested against a re-introduced version of its bug and failed as
expected. Not verified: no container was built or booted (no Docker CLI),
so socat startup, live SMP first-run setup, and real-silicon arm64 rest on
the existing CI smoke matrix.

Refs 2026-10-03 review (F1-F8)
The setup-handshake suite imported the real `websockets` at module scope and
again inside run_case, so it failed on any runner without the package (caught
by CI on PR #32: ModuleNotFoundError). The code under test only calls
websockets.connect(), which the suite replaces with a fake daemon anyway, so
inject a stub module into sys.modules for the duration of exec instead.

Verified: 18/18 pass both with the real package present and with it made
unimportable via a sys.meta_path blocker.
@libre-7
libre-7 merged commit f1d802b into main Oct 4, 2026
4 checks passed
@libre-7
libre-7 deleted the fix/audit-2026-10-03 branch October 4, 2026 14:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant