57021 bc insight lack of panic recovery in housekeeping goroutine creates potential for denial of service
Submitted on Oct 22nd 2025 at 18:01:35 UTC by @rionnaldi for Attackathon | VeChain Hayabusa Upgrade
Report ID: #57021
Report Type: Blockchain/DLT
Report severity: Insight
Target: https://github.com/vechain/thor/compare/v2.3.2...release/hayabusa
Impacts:
Temporary freezing of network transactions by delaying one block by 500% or more of the average block time of the preceding 24 hours beyond standard difficulty adjustments
Description
Brief/Intro
The lack of a defer recover() statement at the beginning of the housekeeping goroutine is a critical fragility. Long-running goroutines responsible for core node functions, especially those processing potentially malicious external input, must be resilient to panics. The wash function is a prime candidate for panics because it involves many complex interactions with the state and untrusted transaction data.
Vulnerability Details
The housekeeping function runs as a long-lived goroutine, performing the critical task of periodically cleaning ("washing") the transaction pool. This wash operation is complex, involving the creation of new state objects and the validation of transactions against the state by calling txObj.Executable(). The Executable method in turn calls resolved.BuyGas(), which interacts with multiple native smart contracts.
The housekeeping goroutine does not implement a recover mechanism. If any part of the code within the for loop panics (e.g., due to a nil pointer dereference, an out-of-bounds slice access, or a bug in a native contract triggered by a malformed transaction), the panic will be unhandled. This will cause the housekeeping goroutine to crash and exit permanently, leaving the node without its transaction pool cleaning mechanism.
Vulnerable Code (in txpool/tx_pool.go):
func (p *TxPool) housekeeping() {
logger.Debug("enter housekeeping")
defer logger.Debug("leave housekeeping")
ticker := time.NewTicker(time.Second * 1)
defer ticker.Stop()
headSummary := p.repo.BestBlockSummary()
for {
// NO RECOVER MECHANISM HERE
select {
case <-p.ctx.Done():
return
case <-ticker.C:
// ... complex logic that calls wash() ...
// A panic inside wash() will crash this goroutine.
executables, removedLegacy, removedDynamicFee, err := p.wash(headSummary)
}
}
}Impact Details
Permanent Denial of Service on Transaction Processing: As demonstrated in the PoC, once the
housekeepinggoroutine crashes, the node permanently loses its ability to clean the transaction pool. Expired transactions will accumulate indefinitely. The pool will fill with these "zombie" transactions, eventually reaching its capacity (options.Limit). At this point, the node will reject all new incoming transactions, effectively taking it offline in terms of transaction processing and propagation. The only remediation is to restart the node.Resource Exhaustion: The accumulation of unswept transactions in memory will lead to a gradual increase in the node's memory footprint, which can eventually lead to an Out-Of-Memory (OOM) crash, causing the entire node process to terminate.
Network Degradation: If an attacker can successfully execute this attack against multiple nodes, particularly Authority Masternodes, it could significantly impair the transaction processing capability of the entire network, leading to increased confirmation times and reduced throughput.
References
https://github.com/vechain/thor/blob/release/hayabusa/txpool/tx_pool.go
Link to Proof of Concept
https://gist.github.com/rionnaldi/fb50d3b13e3aad311cf37d1f63c89451
Proof of Concept
Attack overview
State Poisoning: The attacker first submits a transaction that writes a malformed RLP value (specifically
0x01, which is invalid as a vp-encoded string) to a storage slot associated with a contract. The PoC simulates this by directly modifying the state database to place this value in theusercredit slot of theprototypecontract.Triggering the Panic: The attacker submits a second transaction to the target node's
txpool. This transaction is crafted to trigger a read of the poisoned storage slot during validation (e.g., by calling a function on the contract that checks the user's credit).Goroutine Crash: When the
housekeepinggoroutine'swashfunction processes this second transaction, it callstxObj.Executable(). This leads to theprototypecontract attempting to decode the malformed RLP data from storage. The RLP parser panics due to a "slice bounds out of range" error.Permanent DoS: The panic is unhandled, crashing the
housekeepinggoroutine. The node's transaction pool is no longer cleaned of expired, invalid, or successfully processed transactions.
Observed behavior (Expected vs Actual)
Expected: Housekeeping continues running and cleans expired transactions after processing the malicious transaction.
Actual: Housekeeping crashes due to an unhandled panic, leaving expired transactions in the pool and causing progressive resource depletion and eventual denial of service until node restart.
Was this helpful?