diff --git a/pyre/extra_tests/parity_tests/pickletools_optimize_bridge_abort.py b/pyre/extra_tests/parity_tests/pickletools_optimize_bridge_abort.py new file mode 100644 index 00000000000..a4308bcb7c3 --- /dev/null +++ b/pyre/extra_tests/parity_tests/pickletools_optimize_bridge_abort.py @@ -0,0 +1,36 @@ +"""A bridge walk that adopts a blackhole terminal must keep the frame's result. + +``pickletools.optimize`` ends in a single ``return out.getvalue()``. Under the +JIT, the guard-failure bridge walk over its opcode loop can stop inside the +reconstructed callee, adopt the blackhole chain it has already driven to a +return, and then abort the trace. The adopted terminal carries that frame's +``DoneWithThisFrame`` value, which is the function's return value; dropping it +leaves the caller with neither a result nor a replay, so ``optimize`` returns +``None`` and ``pickle.loads`` raises ``EOFError: Ran out of input``. + +Two rounds over the varying protocol/width matrix are required: the trace is +compiled during the first round and the guard fails in the second, and a fixed +input never reaches the failing shape. +""" + +import pickle +import pickletools +import sys + +for _round in range(2): + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + n = sys.maxsize + while n: + for expected in (-n, n): + data = pickletools.optimize(pickle.dumps(expected, proto)) + assert data is not None, ( + f"optimize() returned None (proto={proto} n={expected})" + ) + got = pickle.loads(data) + assert got == expected, ( + f"round-trip gave {got!r}, want {expected!r} " + f"(proto={proto} n={expected})" + ) + n = n >> 1 + +print("OK") diff --git a/pyre/pyre-jit-trace/src/trace.rs b/pyre/pyre-jit-trace/src/trace.rs index 6f64fca9ecf..ae0bf016633 100644 --- a/pyre/pyre-jit-trace/src/trace.rs +++ b/pyre/pyre-jit-trace/src/trace.rs @@ -2048,6 +2048,17 @@ fn drive_bridge_carrier_walk( // A declined adopt leaves everything to the rollback below, which is the // pre-existing behaviour. let live_root_addr = sym.live_vable_frame_addr(); + // Clear a stash the sub-walk left BEFORE adopting, not after. An adopted + // terminal stores the frame's `DoneWithThisFrame*` result here + // (`try_adopt_blackhole`), and that result IS this drain's answer: the + // guard's caller takes it as the bridge resolution + // (`fbw_finish_concrete_take` in `call_jit.rs`). Clearing after the adopt + // dropped it, and the drop is silent — the adopt also commits the walk-end + // state, so the caller neither finds a concrete nor replays through the + // blackhole, and the frame's return value reaches Python as `None`. + // Clearing first keeps the same protection against a stale stash while + // leaving whatever the adopt installs intact. + crate::jitcode_dispatch::fbw_finish_payload_reset(); let adopted = crate::jitcode_dispatch::fbw_executed_effect_count() != effects_at_entry && try_adopt_blackhole(ctx, cf_addr, live_root_addr, WalkEndCommitLeg::CarrierAbort); if crate::jitcode_dispatch::fbw_debug_abort_enabled() {