Skip to content

Security verification — 2.2.18

Date: 2026-09-28. Baseline: 2.2.17 / aa1746e. Platform: Linux, Python 3.13.14. A dedicated test environment points worker subprocesses at the 2.2.18 source; packaged checks install the wheel into a fresh environment outside the repository.

Verification Result
Ordinary Python tests, including golden numerical fixtures 2,285 cases covered and passing, split runs and changed-test reruns; 22 extended/packaging cases excluded from this selection
Built wheel in a fresh environment 3 passed: import/assets, CPU experiment outside the source tree, local GUI service with authentication/training/export/shutdown
Frontend tests 282 passed in 64 files; typecheck, lint and production build passed
API schema/types and Python critical-error lint passed; generated OpenAPI/types match source
Actual Chromium login private file redirect → one-time bootstrap → authenticated Home; consumed-link replay and missing-CSRF write refused; isolated test context closed
Same synthetic probes on 2.2.17 and 2.2.18 six confirmed weaknesses reproduced before the patch and refused/redacted/metered after it; see the review
VM lockfile, pip-audit/OSV 43 packages, 0 known vulnerabilities
GPU additional-dependency lockfile, pip-audit/OSV 26 packages, 0 known vulnerabilities
Frontend npm audit, including development dependencies 0 known vulnerabilities

The ordinary selection combines the main 2,280-case batch (the old CSRF expectation was updated and its test passed on rerun), two documentation command tests, the added plot-coordinate regression, and two figure-helper isolation regressions. A separate final 60-case import/figure/security run and the final packaging run verify late changes. Counts are not added across overlapping runs.

The table records the initial local patch verification. Native pywebview, image OS packages and the broader operational work remain outside that verification; see the 2.2.18 review.

Additional release staging checks

  • GitHub's Linux, macOS and Windows runners passed the default installation and package/security boundary checks. The Windows AST scan now reads source files as UTF-8 explicitly.
  • Python 3.10 uses -I for directly executed figure helpers; the guard accepts isolated mode as well as 3.11+ safe-path mode. Both keep the bundle directory off Python's initial import path.
  • The offline VM's Python 3.14 runtime matches all 43 locked packages. A separately installed candidate completed CSV upload, Virtual PA simulation, CPU training and result retrieval as the unprivileged worker.
  • The candidate GPU image passed a cold-cache DeltaGRU CUDA forward/backward probe under the production container options, and a waveform import/Virtual PA probe.
  • A separate API in the VM, a temporary private bridge and the candidate GPU agent/image completed PA training, DPD training and DPD inference on CUDA. Checkpoint hashes matched. Foreign-origin requests, anonymous exports, cross-workspace reads and traversal versions were refused; repeated CSV exports matched. These checks used synthetic data, and the temporary services, bridge credentials and workspaces were removed afterward.
  • Chromium verified the output-waveform legends at 1366 and 390 pixels. Bare line breaks remain available after label escaping; attributes and links remain escaped. The layout regression waits for responsive resizing to complete.
  • The production reset timer was active, with the next reset at 11:59 UTC on 2026-09-28. Its configured 11:59/23:59 UTC schedule remains the independent twice-daily cleanup mechanism.

The hosted rollout and final publication results are recorded with the 2.2.18 release.


Hardening report (S13)

What was checked before calling the Studio a release candidate, with the evidence for each claim. Numbers live in docs/releases/performance-report.md; this file covers security, platform, migration/recovery, accessibility and the candidate build. Items that need a person or a machine that does not exist here are listed as pending, not as done.

Date: 2026-09-06. Machine: the S00 baseline machine (docs/baseline/baseline-report.md).

Security checks

Negative tests (all through the real ASGI application)

tests/integration/test_hardening.py (16 tests) plus the earlier S04/S07/S11 tests. One row per threat in docs/architecture/threat-model.md:

Check Result
Malicious Origin / cross-site write / DNS rebinding refused (403 cross_origin_write, 400 host_not_allowed), CSRF header required for every write, no CORS
Path traversal and symlink escape on artifact download refused (404 artifact_missing); traversal ids never reach the file system
Import roots: ../, symlink to a file outside, symlink to a directory outside not listed, not readable (409 "escapes the import root")
Experiment packages: traversal member, symlink member, member larger than recorded (64 MB zero bomb in a 1 MB zip), 10 001 members, damaged hash, unsupported version refused before any write (unsafe_path, unsafe_member, hash_mismatch, too_many_members, unsupported_version); the workspace is unchanged
Chunked request body without Content-Length over the 2 MB cap 413 payload_too_large
Uploads over the cap (dataset file, package) 413, partial file removed
XSS: HTML in run names, notes; no HTML sinks in the frontend; diagnostic pages returned as JSON data and escaped by React; dangerouslySetInnerHTML/innerHTML/eval absent from frontend/src; CSP script-src 'self' on every response; the real-server journey fails on any browser console error
Malicious pickle in a checkpoint (__reduce__ → open(marker, "w")) refused by the single restricted loader (load_checkpoint, weights_only=True), marker never created, evaluation raises a workspace error; legacy state_dict checkpoints still load; no torch.load without weights_only=True, no allow_pickle=True, no pickle.load anywhere in opendpd/, steps/, modules/, utils/
Accelerator held by another process / out of memory classified as device_busy_or_out_of_memory with a hint; never retried or moved silently
Security headers CSP, nosniff, X-Frame-Options: DENY, Referrer-Policy: same-origin, Cache-Control: no-store on API responses

Dependency and code scanning (2026-09-06)

Scan Result Action
pip-audit (development venv, Python 3.13) pillow 12.2.0 (14 PYSEC advisories, fixed in 12.3.0) and setuptools 78.1.0 (PYSEC-2025-49, PYSEC-2026-3447) both upgraded in the venv; re-run: no known vulnerabilities; torch/torchaudio/torchcodec/torchvision are CUDA nightlies not on PyPI and were skipped by the tool
npm audit (frontend, runtime and dev) 0 vulnerabilities —
flake8 critical selection (E9,F63,F7,F82,F401,F841) 0 —
Manual review of the local boundary (opendpd/server/security.py) body cap now holds without Content-Length; every response gets the security headers; no CORS —

Neither tool pins the project's dependencies: pyproject.toml declares lower bounds, so a user installing today gets the fixed versions. The release checklist (S14) repeats both scans on the release runner.

Accepted residual risks

Listed in the threat model: same-user local processes, plain HTTP on loopback, in-memory sessions, inline styles in the CSP, Plotly's HTML subset for server-generated trace names, absolute paths in provenance records (private, full packages only).

Platform evidence

Item Status Evidence
Packaged GUI: install, train smoke, export, exit (Linux, fresh venv) verified tests/packaging/test_wheel_install.py::test_wheel_ships_gui_and_one_command_serves_it drives the packaged service over HTTP: bootstrap → built-in dataset → 2-epoch run → result → share package download → healthy → SIGTERM exits clean and releases the lock
Chromium journeys (1366×768, 1920×1080) verified frontend/e2e/journey.spec.ts J1–J3 + gallery
Firefox journeys verified locally and in CI same spec, firefox-1366 project
WebKit journeys CI only ci.yml installs Chromium, Firefox and WebKit with --with-deps; this Arch host lacks the Ubuntu-built WebKit's libraries (libicu74, libflite1) so WebKit could not be launched here
Real service driven by a real browser verified (Chromium, Firefox) frontend/e2e/live.spec.ts: bootstrap URL → register example → train → logs → result charts → share export download, with console errors (including CSP violations) failing the test; weekly.yml repeats it on ubuntu with all three engines
Playwright projects 4 chromium-1366, chromium-1920, firefox-1366, webkit-1366 (visual baselines Chromium-only)
macOS: real opendpd gui, default browser launch, Ctrl+C cleanup pending (human) no Mac here; weekly CI covers the headless service on macOS only
Windows: opendpd gui, process-group cleanup, paths with spaces/Unicode pending (human) no Windows machine or runner
Safari (real) pending (human) WebKit Linux port is not Safari

Migration and recovery

tests/integration/test_workspace_recovery.py (3 tests) and the S03 runtime tests:

Scenario Result
Workspace moved to another path (with spaces, ω, 工作区, parentheses) runs, results and checkpoints found; run.json, config.resolved.json, artifacts.json, result.json carry no absolute path; re-evaluation reproduces the stored metrics within the frozen tolerance; the service indexes the moved runs, serves artifact downloads and exports a package
Backup copy restored elsewhere while the live copy is damaged the copy opens, exports a full package, which imports into a fresh workspace and re-evaluates
Port already in use opendpd gui --port N exits non-zero with "port N is already in use; pick another with --port or omit it to auto-select"; no lock file left
Server restart with a live worker S03: interrupted runs are marked interrupted with a reason (test_restart_recovery_marks_interrupted)
Killed worker S03: failed/worker_died
Unwritable run directory S03: explicit failure before start
Malicious import see the package rows above

Provenance files keep the absolute command that was executed (a record, not an input); they travel only in full packages.

Accessibility

  • Automated: frontend/e2e/a11y.spec.ts runs axe-core (WCAG 2.1 A/AA rule tags) on Home, Datasets, Experiments, New experiment, Results, a result page, Settings, the component gallery and every tab of a live run detail. Serious/critical violations fail the test. Findings fixed in S13: primary colour 3.36:1 → #3a6f99 (5.4:1 on white), warning tone 4.35:1 → #9d5200 (5.4:1), unselected toggle buttons 4.49:1 → action.active #5a5c5e (6.3:1), chart containers role="img" with focusable children → role="figure", malformed evidence definition lists.
  • Keyboard: the same spec completes register example → New experiment → Start run → Logs tab with Tab/Enter/Arrow keys only (ARIA tabs pattern).
  • Colour is never the only encoding: status chips carry a label and an icon (StatusChip), evidence badges carry text, spectrum traces differ by dash pattern, AM-AM/AM-PM traces by marker symbol, training curves by both (seriesDash/seriesSymbol, unit-tested).
  • Pending (human): a manual screen-reader pass; the automated audit covers rules, not the experience.

Performance

The numbers are in docs/releases/performance-report.md, generated by scripts/perf_report.py against a real opendpd gui server, a real worker and a real browser (frontend/e2e/live.spec.ts); the JSON behind the report records machine, data hashes and software versions. Two of the first measurements failed a frozen target, and the plan forbids relaxing a target or skipping a measurement, so both were investigated on the same machine:

Finding (first full run, commit 68953a1) Cause What changed
GUI overhead on training: pairs 3.8 %, 15.4 %, 12.8 % (median 12.8 %, target ≤ 5 %) Bookkeeping, not training. The supervisor stamped started_at when it spawned the worker and finished_at when it noticed the exit at a later poll tick, while opendpd run stamps both inside the executor. The gap was a constant ≈ 0.8 s per run: with an explicit budget of 8 threads on both legs the difference stayed at ≈ 0.7 s (11 %), and the worker's own status events showed the training itself within 0.2 % of the CLI. The supervisor now adopts the executor's started_at/finished_at from the worker's record (docs/architecture/runtime.md, "Timestamps"; test_run_completes_through_worker_with_events), so the two paths report the same clock. Start-up and exit detection are reported next to the training wall as lifecycle time, not hidden.
Event latency max 2.408 s in one of 247 events (target ≤ 2 s; p95 0.311 s); 2.939 s in the second full run A measurement artifact, not the service: the per-event diagnostics of the second run show the two slowest "events" are the store's own status events written at spawn (sequence 1 and 2) and the next three are the first epoch's events, all emitted while the harness was still busy with the API-latency phase and before it started polling. Their "latency" was the harness's own delay; every slow poll itself took 3 ms. Measured with the poll loop already open the maximum was 0.354 s (--phases events alone). The harness now opens the poll loop first and counts only events emitted after that moment, and records the five slowest events with type, sequence number and the duration of the poll that returned them; the final report (max 0.359 s, p95 0.314 s over 245 events) was produced this way.
Stress import extra RSS 18.9 MB in the first run, then 1 788.9 MB (target ≤ 1 GB) in the second Both numbers were wrong the same way: ru_maxrss counts the file-backed pages of the memory-mapped 1.6 GB source and of the two memory-mapped outputs, which are the OS file cache the plan excludes; the first run only looked good because its baseline process happened to be equally inflated. The import itself streams (chunked CSV and .npy writing). The harness samples RssAnon of the import process every 20 ms and reports the peak anonymous memory (2.9 MB above the baseline) as the target figure, with the file-backed peak (2.3 GB) and the total RSS stated next to it. A failed import can no longer show as a pass, and the stress import runs in its own scratch workspace (the second run had failed on an existing dataset id and reported 0 MB).
execution.num_threads accepted by the schema but applied nowhere A declared knob that did nothing. apply_thread_budget in the shared executor applies it on every path (test_thread_budget_is_applied_by_the_executor); the harness measures overhead with torch's default budget (the target figure); --overhead-threads default,8 adds the same pairs with an explicit equal budget as a diagnostic (included in this stage's report: median −0.5 %, so the budget was never the cause).

Regenerate with python scripts/perf_report.py --workspace <scratch>/ws --minutes 30 --stress on an idle machine; --phases re-runs a subset and merges it into the JSON.

Release candidate

Built with python -m build from the S13 tree (checksums are recorded in the progress table entry of the closing commit, docs/releases/studio-progress.md); twine check passed for both files. The wheel carries the built frontend (opendpd/studio/static/build-info.json), installs without Node.js and serves the GUI with one command (packaging tests above). Version 2.2.0.dev0 is a candidate label: tagging a release is a maintainer decision (S14).

Open items carried to S14

  • Human platform checks: macOS and Windows desktops, real Safari, default browser launch.
  • External trial and onboarding measurements (S14).
  • GPU-tier performance and the full benchmark matrix (L4, human-approved).

2.2.5 math-rendering boundary

LaTeX uses bundled KaTeX and local fonts. MathFormula.tsx is the sole reviewed HTML sink: KaTeX escapes text, limits expansion and size, and trusts only known coefficient classes for interaction. URLs, resource inclusion and arbitrary HTML/style commands remain disabled. MathFormula.test.tsx covers malicious TeX/HTML and catalog interaction; the repository scanner continues to reject HTML sinks elsewhere. Existing CSP and tenant boundaries remain in force.