From 2bbb34dc13c44db056ae0493b7a56f451058b030 Mon Sep 17 00:00:00 2001 From: Yan Date: Sun, 9 Aug 2026 18:03:11 +0000 Subject: [PATCH] Restore the SLEIGH builder's walker when a nested build throws SleighBuilder::delaySlot points the builder at a ParserWalker on its own stack frame and puts the previous one back only on the normal exit path. When the delay-slot instruction has no p-code section, PcodeBuilder::build throws UnimplError, the walker is destroyed with the frame, and the builder is left holding that address. Sleigh::oneInstruction catches the exception and describes it through exactly that pointer, reading a ParserContext out of stack the handler has already reused, so Context.translate segfaults on any delay-slot branch followed by an instruction with no semantics. Eight bytes of SPARC are enough: Context("sparc:BE:32:default").translate(bytes.fromhex("63748596a7b8c9da")) SleighBuilder::appendCrossBuild saves and restores the walker the same unguarded way around a build() that can throw. Restore both through a scope object so an exception leaving either method cannot outlive the walker it installed. The reported instruction is now the branch rather than its delay slot, which matches the instruction length UnimplError already carries. Co-Authored-By: Claude Opus 5 --- pypcode/sleigh/sleigh.cc | 32 ++++++++++++++++++++++++++------ tests/test_pypcode.py | 24 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/pypcode/sleigh/sleigh.cc b/pypcode/sleigh/sleigh.cc index 304a73b..8cc5b1b 100644 --- a/pypcode/sleigh/sleigh.cc +++ b/pypcode/sleigh/sleigh.cc @@ -365,13 +365,37 @@ void SleighBuilder::appendBuild(OpTpl *bld,int4 secnum) walker->popOperand(); } +/// \brief Restore a SleighBuilder's walker and unique offset when the enclosing scope exits +/// +/// A delay slot or a CROSSBUILD is built through a ParserWalker that lives on the stack frame of +/// the builder method, so the builder must point back at its original walker before that frame +/// goes away, including when the nested build throws. Otherwise Sleigh::oneInstruction reports +/// the failure through a walker that no longer exists. +class WalkerScope { + ParserWalker *&curwalker; ///< The builder's current walker + uintb &curuniqueoffset; ///< The builder's current unique offset + ParserWalker *oldwalker; ///< The walker in use when this scope was entered + uintb olduniqueoffset; ///< The unique offset in use when this scope was entered +public: + /// Record the walker and unique offset to put back + WalkerScope(ParserWalker *&w,uintb &off) : curwalker(w), curuniqueoffset(off) { + oldwalker = w; + olduniqueoffset = off; + } + /// Put back the recorded walker and unique offset + ~WalkerScope(void) { + curwalker = oldwalker; + curuniqueoffset = olduniqueoffset; + } +}; + void SleighBuilder::delaySlot(OpTpl *op) { // Append pcode for an entire instruction (delay slot) // in the middle of the current instruction ParserWalker *tmp = walker; - uintb olduniqueoffset = uniqueoffset; + WalkerScope scope(walker,uniqueoffset); // Restore original context on every exit Address baseaddr = tmp->getAddr(); int4 fallOffset = tmp->getLength(); @@ -392,8 +416,6 @@ void SleighBuilder::delaySlot(OpTpl *op) fallOffset += len; bytecount += len; } while(bytecount < delaySlotByteCnt); - walker = tmp; // Restore original context - uniqueoffset = olduniqueoffset; } void SleighBuilder::setLabel(OpTpl *op) @@ -416,7 +438,7 @@ void SleighBuilder::appendCrossBuild(OpTpl *bld,int4 secnum) uintb addr = spc->wrapOffset( vn->getOffset().fix(*walker) ); ParserWalker *tmp = walker; - uintb olduniqueoffset = uniqueoffset; + WalkerScope scope(walker,uniqueoffset); // Restore original context on every exit Address newaddr(spc,addr); setUniqueOffset(newaddr); @@ -434,8 +456,6 @@ void SleighBuilder::appendCrossBuild(OpTpl *bld,int4 secnum) buildEmpty(ct,secnum); else build(construct,secnum); - walker = tmp; - uniqueoffset = olduniqueoffset; } /// \param min is the minimum number of allocations before a reuse is expected diff --git a/tests/test_pypcode.py b/tests/test_pypcode.py index 4f258a8..84ad41e 100755 --- a/tests/test_pypcode.py +++ b/tests/test_pypcode.py @@ -247,6 +247,30 @@ def test_partial_unimpl_failure(self): tx = ctx.translate(b"\xd0\x00\xa8\x00") # and r0, r0; unimpl assert len(get_imarks(tx)) == 1 + def test_delay_slot_unimpl_failure(self): + # The delay slot is built through a walker owned by SleighBuilder::delaySlot, and the + # error is reported through the builder's walker once that frame is gone. + for lang, insns in [ + ("Toy:BE:32:default", b"\xf5\x00\xa8\x00"), # callds 0x0; unimpl + ("sparc:BE:32:default", b"\x63\x74\x85\x96\xa7\xb8\xc9\xda"), # call; unimpl + ]: + with self.subTest(lang=lang): + ctx = Context(lang) + with self.assertRaises(UnimplError) as ctxmgr: + ctx.translate(insns) + # The branch is reported, not the instruction in its delay slot. + assert "0x00000000" in str(ctxmgr.exception) + + def test_partial_delay_slot_unimpl_failure(self): + for lang, insns in [ + ("Toy:BE:32:default", b"\xd0\x00\xf5\x00\xa8\x00"), # and r0, r0; callds 0x0; unimpl + ("sparc:BE:32:default", b"\x01\x00\x00\x00\x63\x74\x85\x96\xa7\xb8\xc9\xda"), # nop; call; unimpl + ]: + with self.subTest(lang=lang): + ctx = Context(lang) + tx = ctx.translate(insns) + assert len(get_imarks(tx)) == 1 + def test_not_cached(self): ctx = Context("x86:LE:64:default") tx = ctx.translate(b"\xeb\xfe", 5)