Skip to content

🔒 [security fix] Gracefully handle poisoned locks and enforce safe unwrap policy#13

Draft
tcrypt25519 wants to merge 2 commits intomasterfrom
fix-unsafe-lock-poisoning-13036901566451122350
Draft

🔒 [security fix] Gracefully handle poisoned locks and enforce safe unwrap policy#13
tcrypt25519 wants to merge 2 commits intomasterfrom
fix-unsafe-lock-poisoning-13036901566451122350

Conversation

@tcrypt25519
Copy link
Copy Markdown
Owner

🎯 What: This security fix addresses unsafe lock acquisitions in the mempooloracle crate. It replaces panics on poisoned locks with structured error handling and graceful shutdowns.
⚠️ Risk: A single thread panic while holding a lock could previously cause other threads to panic upon lock acquisition, leading to a full process crash or Denial of Service (DoS) of the oracle's P2P transport and tracking logic.
🛡️ Solution:

  1. Graceful Error Handling: Lock acquisitions now return a TrackerError::LockPoisoned instead of panicking.
  2. API Refactoring: The MempoolHandle public API was updated to return Result, allowing callers to handle synchronization issues.
  3. Preventive Guardrails: Added #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))] to the crate root to ensure no new unsafe unwraps are introduced in the future.
  4. Task Resilience: Background worker loops now exit cleanly if they encounter a poisoned lock, preventing further corruption.

PR created automatically by Jules for task 13036901566451122350 started by @tcrypt25519

- Replace unsafe `.expect()` and `.unwrap()` on locks with error handling.
- Update `MempoolHandle` API to return `Result<..., TrackerError>` for all lock-acquiring methods.
- Handle lock poisoning in background tasks by exiting loops safely.
- Add `TrackerError::LockPoisoned` variant for structured error handling.
- Enforce `clippy::unwrap_used` and `clippy::expect_used` denials outside of tests.
@google-labs-jules
Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@semanticdiff-com
Copy link
Copy Markdown

semanticdiff-com Bot commented Apr 20, 2026

Review changes with  SemanticDiff

Changed Files
File Status
  crates/mempooloracle/examples/cli_tool.rs  100% smaller
  crates/mempooloracle/src/transport/p2p.rs  79% smaller
  crates/mempooloracle/src/lib.rs  71% smaller
  crates/bulkmail/src/adapter/solana.rs  67% smaller

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces stricter linting rules to deny unwrap and expect in non-test code and refactors the MempoolHandle API to return Result types instead of Option to handle lock poisoning. However, these changes introduce a potential deadlock in estimated_blocks_to_confirm due to nested read locks on self.inner when calling self.gas_ahead. Additionally, the breaking changes to the public API have not been reflected in the integration tests, which will cause compilation failures.

I am having trouble creating individual review comments. Click here to see my feedback.

crates/mempooloracle/src/lib.rs (508)

high

The refactoring of the MempoolHandle public API to return Result instead of Option is a breaking change that has not been reflected in the integration tests. Specifically, crates/mempooloracle/tests/mempool_oracle.rs still uses the old API (e.g., lines 62-63, 78-79), which will cause the test suite to fail compilation. Please update the tests to handle the new Result return types.

crates/mempooloracle/src/lib.rs (520-536)

high

This implementation introduces a potential deadlock due to nested read locks on self.inner. estimated_blocks_to_confirm acquires a read lock at line 520 and then calls self.gas_ahead(id) at line 525, which attempts to acquire the same read lock again. On some platforms (like Linux with certain pthread configurations), a reader can block if a writer is waiting, leading to a deadlock if the same thread already holds a read lock.

To fix this, you should avoid calling public methods that acquire locks from within other methods that already hold the lock. Inlining the gas_ahead logic or refactoring it into a private method that takes a reference to the inner state is recommended.

    pub fn estimated_blocks_to_confirm(&self, id: &TxId) -> Result<Option<u64>, TrackerError> {
        let inner = self.inner.read().map_err(|_| TrackerError::LockPoisoned)?;
        let tx = match inner.find_tx_by_id(id) {
            Some(tx) => tx,
            None => return Ok(None),
        };

        if inner.classify_transaction(&tx) != TxClassification::Marketable {
            return Ok(None);
        }

        let effective_priority_fee = inner.effective_priority_fee(&tx);
        let key_for_tx = (std::cmp::Reverse(effective_priority_fee), tx.sender, tx.nonce);

        let mut gas_ahead: u64 = 0;
        for (key, _) in inner.priority_queue.range(..key_for_tx) {
            let (_, addr, nonce) = key;
            if let Some(tx_ahead) = inner.find_tx_by_addr_and_nonce(*addr, *nonce) {
                gas_ahead += tx_ahead.gas_limit;
            }
        }

        let usable_capacity =
            (inner.last_block_gas_limit as f64 * (1.0 - inner.private_flow_ratio)) as u64;
        if usable_capacity == 0 {
            return Ok(Some(u64::MAX));
        }

        Ok(Some(gas_ahead.saturating_add(tx.gas_limit) / usable_capacity))
    }

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