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
| Impact | Details |
|---|---|
| Integrity | Scope: Integrity Unexpected State - Exploitation can leave the application in an unexpected state with variables reassigned. |
| Integrity | Scope: Integrity Memory Corruption - Reentrancy can cause use-after-free and other memory corruption issues. |
| Confidentiality | Scope: 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
Related CWEs
- 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
- MITRE Corporation. "CWE-1265: Unintended Reentrant Invocation of Non-reentrant Code Via Nested Calls." https://cwe.mitre.org/data/definitions/1265.html
- The DAO Hack Analysis - Reentrancy Attack
- OpenZeppelin. "ReentrancyGuard"