Skip to content

Commit 1c22fe3

Browse files
committed
Recover from panic in HandleTransaction to prevent peer crash
When a transaction times out during a range query, the timeout path closes LevelDB iterators while the handler goroutine is still using them, causing a nil pointer dereference that crashes the peer. Add a deferred recover() in HandleTransaction to catch such panics and return an error response instead of crashing. This is safe because the transaction has already timed out and the resources are being freed. Also adds a regression test that verifies the panic is recovered and an error response is sent. Fixes #5048 Signed-off-by: Jaskirat-s7 <jaskiratsingh7812@gmail.com>
1 parent da3c2a8 commit 1c22fe3

2 files changed

Lines changed: 56 additions & 0 deletions

File tree

core/chaincode/handler.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,25 @@ type handleFunc func(*pb.ChaincodeMessage, *TransactionContext) (*pb.ChaincodeMe
245245
// returned by the delegate are sent to the chat stream. Any errors returned by the
246246
// delegate are packaged as chaincode error messages.
247247
func (h *Handler) HandleTransaction(msg *pb.ChaincodeMessage, delegate handleFunc) {
248+
// Recover from panics that can occur when the transaction context is cleaned up
249+
// (e.g., iterators closed) due to execution timeout while this goroutine is still
250+
// actively using those resources. This prevents peer crashes from nil pointer
251+
// dereferences in the underlying LevelDB iterator.
252+
// See https://github.com/hyperledger/fabric/issues/5048
253+
defer func() {
254+
if r := recover(); r != nil {
255+
chaincodeLogger.Errorf("[%s] Recovered from panic handling %s: %v", shorttxid(msg.Txid), msg.Type, r)
256+
resp := &pb.ChaincodeMessage{
257+
Type: pb.ChaincodeMessage_ERROR,
258+
Payload: []byte(fmt.Sprintf("%s failed: transaction ID: %s: recovered from panic: %v", msg.Type, msg.Txid, r)),
259+
Txid: msg.Txid,
260+
ChannelId: msg.ChannelId,
261+
}
262+
h.ActiveTransactions.Remove(msg.ChannelId, msg.Txid)
263+
h.serialSendAsync(resp)
264+
}
265+
}()
266+
248267
chaincodeLogger.Debugf("[%s] handling %s from chaincode", shorttxid(msg.Txid), msg.Type.String())
249268
if !h.registerTxid(msg) {
250269
return

core/chaincode/handler_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,43 @@ var _ = Describe("Handler", func() {
495495
}))
496496
})
497497
})
498+
499+
// Regression test for https://github.com/hyperledger/fabric/issues/5048
500+
// When a transaction times out during a range query, the timeout path closes
501+
// the LevelDB iterator while the handler goroutine is still using it, causing
502+
// a nil pointer dereference panic. The recover() in HandleTransaction prevents
503+
// this from crashing the peer.
504+
Context("when the delegate panics", func() {
505+
It("recovers and sends an error response", func() {
506+
panickingDelegate := func(msg *pb.ChaincodeMessage, txContext *chaincode.TransactionContext) (*pb.ChaincodeMessage, error) {
507+
panic("simulated nil pointer dereference from closed iterator")
508+
}
509+
510+
Expect(func() {
511+
handler.HandleTransaction(incomingMessage, panickingDelegate)
512+
}).NotTo(Panic())
513+
514+
Eventually(fakeChatStream.SendCallCount).Should(Equal(1))
515+
msg := fakeChatStream.SendArgsForCall(0)
516+
Expect(msg.Type).To(Equal(pb.ChaincodeMessage_ERROR))
517+
Expect(msg.Txid).To(Equal("tx-id"))
518+
Expect(msg.ChannelId).To(Equal("channel-id"))
519+
Expect(string(msg.Payload)).To(ContainSubstring("recovered from panic"))
520+
})
521+
522+
It("deregisters the transaction ID", func() {
523+
panickingDelegate := func(msg *pb.ChaincodeMessage, txContext *chaincode.TransactionContext) (*pb.ChaincodeMessage, error) {
524+
panic("simulated panic")
525+
}
526+
527+
handler.HandleTransaction(incomingMessage, panickingDelegate)
528+
529+
Expect(fakeTransactionRegistry.RemoveCallCount()).To(Equal(1))
530+
channelID, transactionID := fakeTransactionRegistry.RemoveArgsForCall(0)
531+
Expect(channelID).To(Equal("channel-id"))
532+
Expect(transactionID).To(Equal("tx-id"))
533+
})
534+
})
498535
})
499536

500537
Describe("HandlePutState", func() {

0 commit comments

Comments
 (0)