Skip to content

fix: don't rollback/unlock a candidate this session never locked - #10

Merged
fastrevmd-lab merged 1 commit into
masterfrom
fix/mec-997-rollback-after-failed-lock
Sep 30, 2026
Merged

fastrevmd-lab merged 1 commit into
masterfrom
fix/mec-997-rollback-after-failed-lock

Conversation

@fastrevmd-lab

Copy link
Copy Markdown
Member

Summary

Config/lock state was set up in connect() on both transports such that a failed lock() still left rollback()/close() willing to act:

  • PyEZTransport: self._cfg was assigned before lock(). rollback() and close() only checked self._cfg is not None, so a LockError from Config.lock() still resulted in rollback and unlock being sent for a candidate this session never locked.
  • RustezTransport: already tracked self._locked, but only consulted it in close(). rollback() still only checked self._cfg is not None, so a failed lock() (including the new --dry-run path added in srxsync: lock candidate config at connect() in rustez, matching PyEZ #9, since dry-run now locks) still sent rollback 0.

On a real device, Junos refuses <lock> when another session already has uncommitted changes in the shared candidate. If we ignore that failure and roll back anyway, we can discard that operator's uncommitted edits on a production firewall — a fail-closed violation.

Fix:

  • PyEZTransport: add self._locked, set only after Config.lock() succeeds; gate rollback() and the unlock() call in close() on it.
  • RustezTransport: gate rollback() on the existing self._locked flag (it already gated close()).
  • Orchestrator._fetch_source: move t.connect() inside the try/finally so a failed lock on the source device still calls t.close() instead of leaking the NETCONF session until GC/process exit.

Test plan

  • Added test_rollback_and_close_are_noops_after_failed_lock to tests/unit/test_transport_pyez.py and tests/unit/test_transport_rustez.py: fake Config.lock() raises during connect(), then assert rollback/unlock are never called. Verified each fails against pre-fix code (git stash the transport file, rerun) and passes with the fix.
  • Added test_fetch_source_closes_session_when_lock_fails to tests/unit/test_orchestrator.py: source connect() raises, assert close() still ran. Verified it fails against pre-fix orchestrator.py and passes with the fix.
  • ruff check . — all checks passed
  • mypy srxsync — no issues found in 19 source files
  • pytest tests/unit — 76 passed

Did not run the rust extra CI job locally (requires a cp312 rustez wheel not available in this environment); RustezTransport is exercised via the fake-module fixture already used by test_transport_rustez.py, which does not need the real wheel.

Confirming the device-side blast radius (whether an unlocked <rollback> succeeds against a candidate with another session's uncommitted changes) requires a write against vsrx-ci and is flagged in the source issue for a human with lab access — not done here.

🤖 Generated with Claude Code

Config was assigned in connect() before lock() ran on both transports.
If lock() raised (e.g. another session already has uncommitted changes
in the shared candidate), rollback() and close() still checked only
self._cfg is not None and went ahead with `rollback 0` / `unlock` for a
lock we never held -- risking another operator's uncommitted edits on
a real device. rustez already tracked self._locked but only consulted
it in close(), not rollback().

Add self._locked to PyEZTransport, set it only after lock() succeeds,
and gate rollback()/close() on it in both transports. Also move
orchestrator._fetch_source's connect() inside its try/finally so a
failed lock on the source device still closes the NETCONF session
instead of leaking it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@fastrevmd-lab
fastrevmd-lab merged commit 7f85e0c into master Sep 30, 2026
7 checks passed
@fastrevmd-lab
fastrevmd-lab deleted the fix/mec-997-rollback-after-failed-lock branch September 30, 2026 06:32
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