Repository navigation
fix(policies): roll back every failed load and detach it from shared inputs - #803
Conversation
…m shared inputs PolicyManager::load rolled back only on a std::runtime_error, so any other exception left the policy's handlers and inputs registered and every retry failed with "already exists". It now rolls back on any exception: a std::runtime_error is wrapped as before, anything else is rethrown as it was, so the API still answers a std::invalid_argument with 422. A load that failed before its policy had started also left the policy on every input it reached. On a reused input the policy count stayed one too high, so removing that input's last real policy never tore it down. The rollback now detaches the policy from every input it reached, and InputStream::remove_policy is a no-op for a policy that is not attached, so a removal is never signalled twice. The mock input gains except_on_start_logic, which throws a std::logic_error, for the test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ned inputs Each rollback step now runs on its own and logs a failure as a warning, so one step that throws neither skips the rest nor replaces the exception that started the rollback, which would turn a 422 into a 500. The policy is detached only from the inputs it was attached to: an input the failing iteration reached but never joined is no longer touched after its lock is released, where a concurrent removal could already have freed it. Resources policies are stopped before they are removed. The concurrency test stops after 15 s on a slow build, its watchdog waits 180 s so it only fires on a hang, it keeps its log output to warnings, and the tests now check that a failed load leaves no input or handler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
AI Code Review — risk tier: Advisory only. A human owns the merge decision. Summary — Net positive. This head fixes the previously-flagged resources-policy dangling-pointer issue by stopping/detaching the still-owned policy before rethrowing on Resolved since last review
Previously raised — still open
🤖 AI Code Review · run · prompts: Previous review · 2026-10-28 · 019e442AI Code Review — risk tier: Advisory only. A human owns the merge decision. Summary — Net positive: this closes the rollback gaps described in the PR (non- Findings [High] [security] Resources policy is attached to its input before registration succeeds, leaving a dangling pointer if Resolved since last review
Previously raised — still open
🤖 AI Code Review · run · prompts: Previous review · 2026-10-28 · 5a1e1a2AI Code Review — risk tier: Advisory only. A human owns the merge decision. Summary — Net positive. This head fixes the rollback gaps for non- Resolved since last review
Previously raised — still open
🤖 AI Code Review · run · prompts: Previous review · 2026-10-28 · 72cc961AI Code Review — risk tier: Advisory only. A human owns the merge decision. Summary — Net positive: this closes two real rollback gaps (non- Findings [Medium] [security] Rollback may dereference a freed Pre-existing / out-of-scope
🤖 AI Code Review · run · prompts: |
LCOV of commit
|
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 72cc961f2a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Once a resources policy is registered, DELETE /api/v1/policies can remove it while the load is still running, so the rollback must not keep a raw pointer to it. Resources policies are now attached to their input before they are registered and are not touched afterwards; the rollback keeps their names and looks each one up under the policy map lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Declined. AI review finding "Rollback may dereference a freed
The pointer that could go stale was the resources policies'. Codex flagged it, and 5a1e1a2 tracks them by name. |
|
@codex review |
|
/ai-review |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5a1e1a28d0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Once stop() has detached a started policy, nothing keeps its inputs alive, and a concurrent removal of an input's last policy can free it before the rollback's detach loop reaches it. The rollback now keeps the names of the inputs the policy joined and detaches each under the input map lock, skipping one that is already gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
|
/ai-review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 019e44237d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
If a reused input's last other policy is removed while a failing load still counts on it, remove_policy leaves the input and its resources policy running, and nothing rechecks them once the rollback detaches the policy. The rollback now detaches each attached input under the policy map lock, as remove_policy does, and for a reused input runs the same teardown, now shared as _remove_input_if_unused. A resources policy whose registration fails is stopped at once, which detaches it from its input, instead of staying attached until the rollback removes that input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Fixed in eb1b6f8: AI review finding "Resources policy is attached to its input before registration succeeds, leaving a dangling pointer if module_add throws". When In practice the pointer could not have been dereferenced, because that input is always one this load created and the rollback removes it. The fix costs nothing and closes the gap. A new test, "Bad Config: a resources policy that cannot be registered is rolled back", covers that failure. It registers a user policy under the name the resources policy needs, then checks that the failed load leaves no policy, input or handler behind, and that the policy that owns the name keeps running. |
|
@codex review |
|
/ai-review |
|
🔁 AI Code Review updated for |
|
Codex Review: Didn't find any major issues. Keep it up! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Follows #801.
Summary
Two failure paths in
PolicyManager::loadthat predate #801, plus a test showing that #801 removed a lock-order deadlock.Every failed load is rolled back.
loadcaught onlystd::runtime_error. Any other exception, such as thestd::invalid_argumentPcapPlusPlus threw for an interface name before #802, skipped the rollback: the policy's handlers and inputs stayed registered, and every retry failed with "already exists". Now both tries share one rollback, with these outcomes:std::runtime_erroris wrapped inpolicy [x] failed to start: …as before;POST /api/v1/policiesstill answers astd::invalid_argumentwith 422.A failed load no longer stays attached to a shared input.
policies_count()stayed one too high, so removing that input's last real policy never tore down the input or its-resourcespolicy.InputStream::remove_policyis now a no-op for a policy that isn't attached. Without that, a detach afterstop()would signal a secondRemovePolicy, and the resources handler would count the removal twice, because it counts those signals as deltas.The rollback is best-effort. Each step runs on its own and logs a failure as a warning. A step that throws therefore neither skips the rest nor replaces the exception that started the rollback, which would turn a 422 into a 500. Resources policies are now stopped before they're unregistered.
The rollback survives a concurrent DELETE.
loadandremove_policyaren't serialised, so the API can remove things while a load is failing. The rollback keeps no pointer that a deletion could free:remove_policydoes. An input that is already gone is skipped.remove_policyskipped. That teardown is now one helper,_remove_input_if_unused, shared by both paths.Lock order between
loadandremove_policy. This branch doesn't change any locking; the fix is in #801. Before #801's last commit, the two took the input and policy maps in opposite orders:loadheld the input map (module_get_locked) whilecreate_resources_policyregistered the resources policy in the policy map;remove_policyholds the policy map and then removes the input from the input map.A POST and a DELETE could therefore deadlock. Since #801's 5f9d5e5,
loadregisters the resources policy only after it has released the input map lock. The stress test below shows the difference.Tests
Bad Config: a failed load leaves no reference on a shared input:second_viewselects two taps. One is the inputfirst_viewalready runs; the other has a missingpcap_file, so the load fails beforesecond_viewstarts. Checks:policies_count()is unchanged;second_view's handlers are gone, and only one input is left;first_viewtears that input down.Before the fix:
3 == 2, and the input stayed.Bad Config: an exception that is not a runtime_error is rolled back too: the mock input's newexcept_on_start_logicthrows astd::logic_error. Two loads must both throw it, unwrapped, and leave no policy, handler or input behind. Before the fix, the second load failed withmodule name 'default_view-anycast-default_net' already exists.Bad Config: a resources policy that cannot be registered is rolled back: a user policy takes the name a load's resources policy needs, so the load fails after it has started. Nothing the load made may remain, and the policy that owns the name must keep running.Loading and removing policies concurrently does not deadlock:Suite: all 25 ctest targets pass on macOS (Release).
Still open (older than this change)
AddPolicycarries the handler count as it stood for that tap, whileRemovePolicycarries the full count. So a shared input'sinput_resourceshandler_countis undercounted after a multi-tap policy goes, whether by a normal removal or a failed load.DELETE /api/v1/taps/{name}takes the tap map, then the policy map, then the tap map again, so two concurrent tap DELETEs can deadlock. It's the only lock cycle the review found, and it doesn't involveload,remove_policyor the readers.loadandremove_policyaren't serialised against each other; only loads share_load_mutex. That is safe with the lock order above, but worth keeping in mind for future changes to either.🤖 Generated with Claude Code