Unintended Reentrant Invocation of Non-reentrant Code Via Nested Calls

Description

Unintended Reentrant Invocation of Non-reentrant Code Via Nested Calls occurs when a product invokes code assumed to be reentrant, but nested function calls inadvertently trigger a second invocation of non-reentrant code, modifying program state unexpectedly. In complex products, a single function call can lead to numerous code paths through deeply nested calls. Attackers manipulating inputs—particularly in systems executing untrusted scripts like web browsers—can achieve unexpected control flows. The weakness emerges when code paths alter program state that the original caller expects to remain unchanged.

Risk

Reentrancy vulnerabilities have severe security implications. Use-after-free conditions may be triggered. Memory corruption may occur. Unexpected state changes may cause crashes. Security checks may be bypassed. Data integrity may be compromised. Object lifetimes may be violated. Arbitrary code execution may result. Smart contract funds may be stolen.

Solution

Execute untrusted event handlers asynchronously rather than synchronously, ensuring calls into non-reentrant code are strictly serialized. Pay special attention to type coercion points. Ensure code is reentrant by avoiding non-local data modifications, preventing self-modification, and avoiding calls to other non-reentrant code. Use reentrancy guards and mutex locks for critical sections.

Common Consequences

ImpactDetails
IntegrityScope: Integrity

Unexpected State - Exploitation can leave the application in an unexpected state with variables reassigned.
IntegrityScope: Integrity

Memory Corruption - Reentrancy can cause use-after-free and other memory corruption issues.
ConfidentialityScope: Confidentiality

Execute Unauthorized Code - Memory corruption from reentrancy may lead to arbitrary code execution.

Example Code

Vulnerable Code

// Vulnerable: Widget class with reentrancy issue

class Image {
public:
    void click() {
        // Execute script associated with this image
        // VULNERABLE: Script execution can cause reentrancy
        scriptEngine->executeScript(this->onClick_script);
    }

    ~Image() {
        // Destructor
    }
};

class Widget {
private:
    Image* backgroundImage;
    int state;

public:
    Widget() {
        backgroundImage = new Image();
        state = 0;
    }

    void click() {
        state = 1;  // Set state before nested call

        // VULNERABLE: This can execute arbitrary script
        // Script might call changeBackgroundImage()
        backgroundImage->click();

        // VULNERABLE: backgroundImage may have been deleted!
        // If script called changeBackgroundImage(), we now have
        // a dangling pointer
        state = 2;  // This line assumes backgroundImage still valid
    }

    void changeBackgroundImage(Image* newImage) {
        // VULNERABLE: Called from within click() via script
        delete backgroundImage;  // Deletes object being used!
        backgroundImage = newImage;
    }
};

// Attack scenario:
// 1. Widget::click() is called
// 2. backgroundImage->click() executes script
// 3. Script calls widget->changeBackgroundImage(evilImage)
// 4. Original backgroundImage is deleted
// 5. click() returns to Widget::click()
// 6. Widget::click() accesses deleted backgroundImage -> UAF!
// Vulnerable: Request class with reentrancy in type coercion

class Request {
private:
    std::string uri;
    std::string credentials;
    bool sent;

public:
    void setup(const std::string& newUri, const std::string& newCreds) {
        uri = newUri;
        credentials = newCreds;
        sent = false;
    }

    void send() {
        if (sent) return;

        // VULNERABLE: String coercion can execute script
        // toString() on untrusted object
        std::string uriStr = scriptEngine->coerceToString(uri);

        // Script might have called setup() with different credentials!
        // Now we have inconsistent state:
        // uriStr from old uri, but credentials from new setup()

        sendRequest(uriStr, credentials);
        sent = true;
    }
};

// Attack:
// 1. request->setup("http://good.com", "goodCreds")
// 2. request->send() calls coerceToString()
// 3. coerceToString executes script that calls:
//    request->setup("http://evil.com", "evilCreds")
// 4. send() continues with:
//    - uri string from "http://good.com"
//    - credentials from "evilCreds"
// Result: Credentials sent to wrong server!
// Vulnerable: Smart contract reentrancy

contract VulnerableBank {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount, "Insufficient balance");

        // VULNERABLE: External call before state update
        // Attacker's fallback function can call withdraw() again
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");

        // VULNERABLE: Balance updated AFTER external call
        // Attacker has already re-entered and withdrawn again!
        balances[msg.sender] -= amount;
    }
}

// Attack contract:
contract Attacker {
    VulnerableBank public bank;

    constructor(address _bank) {
        bank = VulnerableBank(_bank);
    }

    function attack() public payable {
        bank.deposit{value: 1 ether}();
        bank.withdraw(1 ether);
    }

    // Fallback function called during withdraw
    receive() external payable {
        if (address(bank).balance >= 1 ether) {
            // ATTACK: Re-enter withdraw before balance update
            bank.withdraw(1 ether);
        }
    }
}

Fixed Code

// Fixed: Widget class with reentrancy protection

class Image {
public:
    void click() {
        // Safe: Schedule script for deferred execution
        scriptEngine->scheduleScript(this->onClick_script);
    }
};

class Widget {
private:
    Image* backgroundImage;
    int state;
    bool processingClick;  // FIXED: Reentrancy guard

    // FIXED: Use reference counting for safe object lifetime
    std::shared_ptr<Image> backgroundImagePtr;

public:
    Widget() {
        backgroundImagePtr = std::make_shared<Image>();
        state = 0;
        processingClick = false;
    }

    void click() {
        // FIXED: Reentrancy guard
        if (processingClick) {
            return;  // Prevent reentrant calls
        }
        processingClick = true;

        state = 1;

        // FIXED: Keep local reference to prevent deletion
        std::shared_ptr<Image> localImage = backgroundImagePtr;

        // Even if script changes backgroundImagePtr,
        // localImage keeps the object alive
        localImage->click();

        // FIXED: Object still valid due to shared_ptr
        state = 2;

        processingClick = false;
    }

    void changeBackgroundImage(std::shared_ptr<Image> newImage) {
        // FIXED: Old image destroyed only when all references gone
        backgroundImagePtr = newImage;
    }
};

// Alternative: Async event handling
class AsyncWidget {
private:
    Image* backgroundImage;
    std::queue<std::function<void()>> pendingEvents;
    bool processingEvents;

public:
    void click() {
        // FIXED: Queue event for async processing
        pendingEvents.push([this]() {
            backgroundImage->click();
        });

        // Process events only at top level
        if (!processingEvents) {
            processEventQueue();
        }
    }

    void processEventQueue() {
        processingEvents = true;

        while (!pendingEvents.empty()) {
            auto event = pendingEvents.front();
            pendingEvents.pop();
            event();
        }

        processingEvents = false;
    }
};
// Fixed: Request class with reentrancy protection

class Request {
private:
    std::string uri;
    std::string credentials;
    bool sent;
    bool inProgress;  // FIXED: Reentrancy guard
    std::mutex requestMutex;  // FIXED: Thread-safe guard

public:
    void setup(const std::string& newUri, const std::string& newCreds) {
        std::lock_guard<std::mutex> lock(requestMutex);

        // FIXED: Prevent setup during send
        if (inProgress) {
            throw std::runtime_error("Cannot modify request in progress");
        }

        uri = newUri;
        credentials = newCreds;
        sent = false;
    }

    void send() {
        std::lock_guard<std::mutex> lock(requestMutex);

        if (sent) return;

        // FIXED: Mark as in-progress before any external calls
        inProgress = true;

        // FIXED: Copy values before coercion to prevent TOCTOU
        std::string localUri = uri;
        std::string localCreds = credentials;

        // Now coercion can't affect our local copies
        std::string uriStr = scriptEngine->coerceToString(localUri);

        // FIXED: Use local copies, immune to reentrancy
        sendRequest(uriStr, localCreds);

        sent = true;
        inProgress = false;
    }
};

// Alternative: Capture state atomically
class SafeRequest {
private:
    struct RequestState {
        std::string uri;
        std::string credentials;
    };

    std::shared_ptr<RequestState> state;
    std::atomic<bool> sent;

public:
    void setup(const std::string& newUri, const std::string& newCreds) {
        // FIXED: Atomically replace entire state
        auto newState = std::make_shared<RequestState>();
        newState->uri = newUri;
        newState->credentials = newCreds;

        std::atomic_store(&state, newState);
        sent = false;
    }

    void send() {
        if (sent.exchange(true)) return;

        // FIXED: Get immutable snapshot of state
        auto snapshot = std::atomic_load(&state);

        // Safe: snapshot is immutable, can't be affected by reentrancy
        std::string uriStr = scriptEngine->coerceToString(snapshot->uri);
        sendRequest(uriStr, snapshot->credentials);
    }
};
// Fixed: Smart contract with reentrancy protection

contract SecureBank {
    mapping(address => uint256) public balances;
    mapping(address => bool) private locked;  // FIXED: Reentrancy guard

    // FIXED: Modifier to prevent reentrancy
    modifier noReentrant() {
        require(!locked[msg.sender], "Reentrant call");
        locked[msg.sender] = true;
        _;
        locked[msg.sender] = false;
    }

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    // FIXED: Checks-Effects-Interactions pattern
    function withdraw(uint256 amount) public noReentrant {
        // Checks
        require(balances[msg.sender] >= amount, "Insufficient balance");

        // FIXED: Effects BEFORE interactions
        balances[msg.sender] -= amount;

        // Interactions (external call) LAST
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

// Alternative using OpenZeppelin's ReentrancyGuard
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecureBankV2 is ReentrancyGuard {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    // FIXED: Use nonReentrant modifier
    function withdraw(uint256 amount) public nonReentrant {
        require(balances[msg.sender] >= amount, "Insufficient balance");

        // Effects before interactions
        balances[msg.sender] -= amount;

        // Safe external call
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

CVE Examples

  • Web browser vulnerabilities where script execution during DOM operations caused use-after-free through reentrancy
  • Smart contract hacks including the DAO attack which exploited reentrancy to drain millions in cryptocurrency

  • CWE-662: Improper Synchronization (parent)
  • CWE-663: Use of a Non-reentrant Function in a Concurrent Context (peer)
  • CWE-416: Use After Free (can precede)
  • CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition (related)

References

  1. MITRE Corporation. "CWE-1265: Unintended Reentrant Invocation of Non-reentrant Code Via Nested Calls." https://cwe.mitre.org/data/definitions/1265.html
  2. The DAO Hack Analysis - Reentrancy Attack
  3. OpenZeppelin. "ReentrancyGuard"