From 84a1d7ec65cf73ccd9a0884295a8a038686f7ea2 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 15:35:45 -0400 Subject: [PATCH 1/2] Require a value-producing head before matching return tails --- src/decompiler.rs | 18 +++++ tests/fixtures/padded_value_returns.py | 19 +++++ tests/fixtures/sunk_value_return_control.py | 10 +++ tests/integration.rs | 43 +++++++++++ tests/pyc/padded_value_returns_1.3.11.pyc | Bin 0 -> 665 bytes tests/pyc/padded_value_returns_1.3.12.pyc | Bin 0 -> 629 bytes tests/pyc/padded_value_returns_1.3.13.pyc | Bin 0 -> 633 bytes tests/pyc/padded_value_returns_1.3.14.pyc | Bin 0 -> 641 bytes tests/pyc/padded_value_returns_3.3.11.pyc | Bin 0 -> 681 bytes tests/pyc/padded_value_returns_3.3.12.pyc | Bin 0 -> 645 bytes tests/pyc/padded_value_returns_3.3.13.pyc | Bin 0 -> 649 bytes tests/pyc/padded_value_returns_3.3.14.pyc | Bin 0 -> 657 bytes tests/pyc/padded_value_returns_9.3.11.pyc | Bin 0 -> 729 bytes tests/pyc/padded_value_returns_9.3.12.pyc | Bin 0 -> 701 bytes tests/pyc/padded_value_returns_9.3.13.pyc | Bin 0 -> 705 bytes tests/pyc/padded_value_returns_9.3.14.pyc | Bin 0 -> 713 bytes tests/pyc/sunk_value_return_control.3.11.pyc | Bin 0 -> 460 bytes tests/pyc/sunk_value_return_control.3.12.pyc | Bin 0 -> 444 bytes tests/pyc/sunk_value_return_control.3.13.pyc | Bin 0 -> 438 bytes tests/pyc/sunk_value_return_control.3.14.pyc | Bin 0 -> 441 bytes tools/compile_padded_value_returns.py | 74 +++++++++++++++++++ 21 files changed, 164 insertions(+) create mode 100644 tests/fixtures/padded_value_returns.py create mode 100644 tests/fixtures/sunk_value_return_control.py create mode 100644 tests/pyc/padded_value_returns_1.3.11.pyc create mode 100644 tests/pyc/padded_value_returns_1.3.12.pyc create mode 100644 tests/pyc/padded_value_returns_1.3.13.pyc create mode 100644 tests/pyc/padded_value_returns_1.3.14.pyc create mode 100644 tests/pyc/padded_value_returns_3.3.11.pyc create mode 100644 tests/pyc/padded_value_returns_3.3.12.pyc create mode 100644 tests/pyc/padded_value_returns_3.3.13.pyc create mode 100644 tests/pyc/padded_value_returns_3.3.14.pyc create mode 100644 tests/pyc/padded_value_returns_9.3.11.pyc create mode 100644 tests/pyc/padded_value_returns_9.3.12.pyc create mode 100644 tests/pyc/padded_value_returns_9.3.13.pyc create mode 100644 tests/pyc/padded_value_returns_9.3.14.pyc create mode 100644 tests/pyc/sunk_value_return_control.3.11.pyc create mode 100644 tests/pyc/sunk_value_return_control.3.12.pyc create mode 100644 tests/pyc/sunk_value_return_control.3.13.pyc create mode 100644 tests/pyc/sunk_value_return_control.3.14.pyc create mode 100644 tools/compile_padded_value_returns.py diff --git a/src/decompiler.rs b/src/decompiler.rs index 33a3465d..cdc438ae 100644 --- a/src/decompiler.rs +++ b/src/decompiler.rs @@ -43919,6 +43919,24 @@ impl<'a> Ctx<'a> { .iter() .map(|x| (canon(x.op), x.arg)) .collect(); + // Masked runtime exits can leave padding between POP_EXCEPT and + // a return whose value was computed earlier. Padding is not a + // value-building tail: matching it against the mainline would + // swallow the actual response/call and resume with an empty stack. + // Look through padding, then require a real independent value head. + let head = tail.iter().find(|(op, _)| !matches!( + op, Op::NOP | Op::NOT_TAKEN | Op::CACHE | Op::EXTENDED_ARG | Op::RESUME + )); + let value_head = head.map_or(false, |(op, arg)| { + matches!(op, Op::LOAD_FAST | Op::LOAD_FAST_LOAD_FAST | Op::LOAD_NAME + | Op::LOAD_DEREF | Op::LOAD_CLASSDEREF | Op::LOAD_GLOBAL + | Op::LOAD_CONST | Op::LOAD_SMALL_INT | Op::LOAD_COMMON_CONSTANT) + || (*arg == 0 && matches!(op, + Op::BUILD_TUPLE | Op::BUILD_LIST | Op::BUILD_SET | Op::BUILD_MAP)) + }); + if !value_head { + return None; + } // require a NON-DEGENERATE tail: at least one value op before // the RETURN. A lone RETURN_VALUE (the clause's value was // loaded+SWAPed UNDER the POP_EXCEPT, _collections_abc 3.12 diff --git a/tests/fixtures/padded_value_returns.py b/tests/fixtures/padded_value_returns.py new file mode 100644 index 00000000..fb1d66fb --- /dev/null +++ b/tests/fixtures/padded_value_returns.py @@ -0,0 +1,19 @@ +# Generate .pyc files with the matching CPython 3.11–3.14 interpreter: +# python tools/compile_padded_value_returns.py +# Each RETURN_VALUE receives 1, 3 or 9 NOPs, with jumps and exception +# boundaries relocated. Padding never constructs a new return value. + +def synchronous(read, build): + try: + item = read() + except ValueError: + return build("missing") + return build(item) + + +async def asynchronous(read, build): + try: + item = await read() + except ValueError: + return build("missing") + return build(item) diff --git a/tests/fixtures/sunk_value_return_control.py b/tests/fixtures/sunk_value_return_control.py new file mode 100644 index 00000000..3b28c204 --- /dev/null +++ b/tests/fixtures/sunk_value_return_control.py @@ -0,0 +1,10 @@ +# Versioned .pyc files are consumed by tests/integration.rs. + +def select(value): + try: + entry = value.entry + except AttributeError: + kind = "missing" + else: + kind = "present" + return kind, value diff --git a/tests/integration.rs b/tests/integration.rs index f4af5082..7fbcb952 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -169,3 +169,46 @@ fn py314_hello() { "py3.14 hello", ); } + +#[test] +fn real_value_build_still_matches_the_sunk_tail() { + for version in ["3.11", "3.12", "3.13", "3.14"] { + let (src, clean) = decompile_fixture(&format!("sunk_value_return_control.{version}.pyc")); + assert!(clean, "Python {version}: output marked incomplete:\n{src}"); + assert_contains(&src, &[ + r#"def select(value): + try: + entry = value.entry + except AttributeError: + kind = 'missing' + else: + kind = 'present' + return kind, value"#, + ], version); + } +} + +#[test] +fn padding_does_not_match_a_mainline_value_build() { + for version in ["3.11", "3.12", "3.13", "3.14"] { + for padding in [1, 3, 9] { + let (src, clean) = decompile_fixture( + &format!("padded_value_returns_{padding}.{version}.pyc")); + assert!(clean, "Python {version}, {padding} NOPs: incomplete:\n{src}"); + assert_contains(&src, &[ + r#"def synchronous(read, build): + try: + item = read() + except ValueError: + return build('missing') + return build(item)"#, + r#"async def asynchronous(read, build): + try: + item = await read() + except ValueError: + return build('missing') + return build(item)"#, + ], version); + } + } +} diff --git a/tests/pyc/padded_value_returns_1.3.11.pyc b/tests/pyc/padded_value_returns_1.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2ef2c6bf7e63539b4cff08cc58f6c6af67bb8e80 GIT binary patch literal 665 zcmcIhO-n*S6umQ}`CS%Bn-)SL2&I1@+O-PY2!d_IoL9Zj_hjZhKgu>K(WYhi3o=Cd zC#_q&cJ&8@w{sEQdFHFAZ3FkrnLB6Z&OP&xNJJR`Jh!%=-fv!C-jMr)d@^?p$q+aU z;1D=LB%Br@%WBlu0tAq&tK2hB9RUhG7$Kiv%*Q1)Ha5XpEOFifu|kxRa5BPrlSNIa z(K1A!2yN)FGPH@(^yp8G+8@(`6Kd#E>c+{KIMGtIZfpBOm?F!ZfY7Sk2_D&cojU>D z(i&1dF`Z7&GL|MSnLpM$mMt`yZ&o?ic%`L|SA>R1L*|-YCnmc_?NpeCX-g5q!%G-0 z2FN(VRWt#&sSLX(ncsHwmGm6p00vOWbLl0N&&DdyGu0PhxiQUCw| literal 0 HcmV?d00001 diff --git a/tests/pyc/padded_value_returns_1.3.12.pyc b/tests/pyc/padded_value_returns_1.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d55d035af45cb113f4e45dfbb17d2141625b4d6c GIT binary patch literal 629 zcmb7BJxc>Y5S_WbGv8`#Y$OB}HH{X5*od8#V511ygfn~ULB80%GYKk460Af)K^qZl zgeW5D&+s2yD_D!yNGfMzB8oOH?AzJhH?#BJKE+~T1^`#Fmz(!}6P_vl8rY5eK?=@+ zLlf43BO%tb6w7H}OaPr?RmflNa6_QMF5Gqrdm8OkRLvxl z$@aKoNk>j^*j2|BmYgn|oLfA*tMLC*jYri~Xhzsu{VBQ=FmCMOB%n89W^0Y^BZAL|J?4Q22G6L1+%vwN1o2TZd^ uS{GYd#~h*3Tb~6wFSLWRg%Wr3)|`mbmHKFZfZ&xO;#<_fVSkXI^Wq1a^ma7> literal 0 HcmV?d00001 diff --git a/tests/pyc/padded_value_returns_1.3.13.pyc b/tests/pyc/padded_value_returns_1.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7ed911c6de4e66a2c9cafdf4432827c94e5540ec GIT binary patch literal 633 zcmb7BJx{_=6uq~v$hRVki7^Gy7+7SHu%MfR3k$);!BktY2^4c5C9v46n(01b&QHhHI~@11+!J*Po38500dX%*fcUS99sng2XEVL^(?0BW!Z z1T|#DrdAM#+K&=IXRaCryCbaub;vXCLIk}g=N^AoC1WwOaNp-scQJG zl3E6ovH_bS0}UK%RX^Nzq;%Zo5dGM$+4Agdq7D^=uaj+Be(1n+T9&UnUArySIN5C0 zU+dbkD~k>1r0bC_i^ryA+17qfmG6_yo330x?)!1scMp%K<90kraW0$YLW6L^9-k0#`Bl~7r3Nf>IPg#%XotZ+@?3gqbTDOZir`o sSBKgt9#ZAQaf9&|l=JLm=X3B7_S;ieYr)7Zy}*bN~PV literal 0 HcmV?d00001 diff --git a/tests/pyc/padded_value_returns_1.3.14.pyc b/tests/pyc/padded_value_returns_1.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f6ccbc43a82f9cb5eb698044a9c42fb425a502b2 GIT binary patch literal 641 zcmaJY6r6W^N%TyjXknqo#DEwC(-;USc48wILBv7`F?Z@ozSzB!*afv#&{nLC zjR@NNJFc~{6yp!@Z4$*oJ=nR~z1g>KW^X3K)ln z0x?p-NFyW4#l)|R5*snQ(G?Liqe+9y@W56V)p>+j( z>3m}fTI*s-Ou?l#1(B+&lv~?pYHgJ&zOyQ2U1mgJN~dH8pp99(**&-E`O;C@M#;7vyLhCx7p23CHMP(<4u;fmt$ZNswYsn55N{6i=;$t0l;I>Y zip#N#xTRmbpK7VcFvBid-1SfYYwYQeyJ=EOtcNW8mPZ8Zv41k5zcR@Q&c)b}`T}H# zAtPi&^8pQm3|~1&izh*}^wZ@qFU7S`8&q=)CzPi%D}i&GG%w5qt_JgTO$pqL<-~)? z(<9}?6TdgwMmHm=OSF3hdM6AlSL?Q4aaN@MbM%#WwB}6^(R(yR;~ygo-4j0nH^y!Z literal 0 HcmV?d00001 diff --git a/tests/pyc/padded_value_returns_3.3.11.pyc b/tests/pyc/padded_value_returns_3.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d8162a77b8647e829a48fcddfff44b264d58f7d GIT binary patch literal 681 zcmcIhyG{Z@6uq+p@-|>X>Xw>`mU99%LrTNEoG>R`C|7O$1hI}8?)XqI3&($M8&%=> z3`?&`b=6cVHQ%*#Y01M2qixx!%fm)l2wjw#YI_NFN}BSZ(QzWOV^*%PZq{vy5!$>) z;ZlH%67<3`xKF0}n5BQ)(pSwei{?jH@eWRZs!!e+EP(g@b zX$gjD;Y7>d8(ycB3RJy%4G-c;ct~dXBg^vKMDr{`C7CQl-YN6oQLQfQs(y?K`lTM% Z-6xoF#@HY5S_WbGv8kz*vWw+2EmkpAa+(l8bMG5IkTr8Y?u3mR6cqINcu+M@lku%`X zg+<^ulFHAhOPzScuOK5tb$dq#+c$TMFTw#9v8#dE6M*B#fcNmZ0- znr~ySEnQhyby}_`Y+2Z_IJf!orV3vcHXYXG zE7(&CWjfJHwFB#H8XOFja+GSgLbzTn#3-|E)&qN!bDtMjq;lJ|B5gl#>sF+?ExRFP zS%zUm^DSGn#IoZaw*qd9<)&#_wzYR8!}qvNhn`q!oB{l#4(792`-)B<0M*; zgGq29heXUNbC6_ZW#9!+Ny}2*Z9)rTP=ExaNz2tx@|b|UlK!k2qwg`80|U1D2I3G{ z5r8R}0qM=bm)BF`J-75bu*TxxUs;O>c?G)C^U4tMEv(_>`xv3O F;|F~tZ#)11 literal 0 HcmV?d00001 diff --git a/tests/pyc/padded_value_returns_9.3.11.pyc b/tests/pyc/padded_value_returns_9.3.11.pyc new file mode 100644 index 0000000000000000000000000000000000000000..adb6012757c7654420d502f3e4ea24fb79978c66 GIT binary patch literal 729 zcmc(dPfG$p7{=e3(fl74h*u#AgwmU*=+-IlA_#U7bGBO0Rc3b8$Sx@nLB|mK0LCI6 z`xc!$Y`6LX!rnYY@2qKs-karp_LV=NYC0Px!0dHK9~fBQhc806czn`nl> zVE{+K2`b^V2pLwRi!6W-z1qsX_0l1r(1Q{BG1hEcQX{9ZoNy5np&l}Ir zgc>bFXz3|J2fC~b9in=A^l#6qyiE&EprK2t8>cpL!li26(hh`ZiVSmnLaTDee{AV> z?)Y?5Ye@AAsZ?sZZEDh#xf8u>T0)b#R+V#&SK6w*A~akyWVUtgMC7?qI}=T#X-N^m z<~1xA3uF}KDjb8mM3y~~>@u#tiawwm0zts5ko6J!1uMajUHkq6pHMu6A0@*?K>47O z5Xu~t2;;UHvw}@MR6<4US;*UE8r*9%xmDNp#Rh&- bm%-gdnJ_}gBY5S`h*GvAtkB3K2KKpJD0Mg$8h!A20Yku!VhLB80%GX@nzgO#Wt7_bvW zh+?a?wg2E+!CJgVtelN8sA&ZE*vFf>nYXi#eM}^x8~`p7&)09e7CaH}F6^)%Nu&t^ z8n6Tc39-(yyuiAZ00wa_6u9l$2r$@&n@+(FqX@UzO-C^TzrT4H1>O0%#y}3DRzQJY zJq5Pd(-nbvsKF**gc`QkoF6JVN;&1V7W&ZztLWJ?QdVS^`-ZeF;hQU-Qxv}8xOPeD zVbkgKd&aeut8%N(rt3*t<*JqtwpiZK?aR`pWLb?@>wZkt%Y}7WDOWrtW7OP7JJ9p0 zIKf_M1kO`=euw#9WA=6F3BfEN8Tr$keKK~7ra9wHeqfsL7qjwkIP_n`Zg~x*c0rm1 zSuR7Ghc!e9z6_F43jDbB{rdfs(p&1As|kWVobJO$Gy_LCW*i$CI89OF5;Jg>n&$jI zM$rR9q7R%CpKGzUiBik7K84yRy@w}D72y@_DVd^Q`oRPP1TP#BU*jeY2T6kV$sZ*1 Bc*Ot! literal 0 HcmV?d00001 diff --git a/tests/pyc/padded_value_returns_9.3.13.pyc b/tests/pyc/padded_value_returns_9.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7812335a6fa7074016568ee4b729b8bf006606df GIT binary patch literal 705 zcmbtSJx>Bb5S_U_JOuo}XiO{sk&s-YHbe?Ep|G%_A=p@ulRNMdILIzXq(y5@G&I)6 z1pk1xbe7iF`Ui~n2b@J9L>d!!voCMw-ptOtoWHabVgQh<>+jFQx2F%{(}iQ^uM+7) z5w?Is5ec!T=&Yhll>jPnS;+50~oRs(2YtOF!K zW)*RgC6xGFUJO!Dg+msHDh`x_r!{OLY^OFrFLY|uUF(?hCfAv#a?3Eiz`1MJO;5Gk zRzt{k6N!YEZd;-)_Nw+}+vS$nYZ<0#ndOd5FLR56j@WH=y|Cyyl?&cuY!&;WTV0PY`D=EDr*rBodG{&qwCuHpfb<@E=Uc>898g4M9-l}Q1k5b?f)9|#G zSJ`WrZbODdhbkjp7$_qfxhymIDfIJFFxP0BZrwWKQTil*8h?f0lOf_)B!HX#D#7UG EZ-+p0qyPW_ literal 0 HcmV?d00001 diff --git a/tests/pyc/padded_value_returns_9.3.14.pyc b/tests/pyc/padded_value_returns_9.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..64d17f012cdbbe19ec5f2f849620d16f45a6abb9 GIT binary patch literal 713 zcma)4Jxc>Y5PfrdXW}J(BZ5(kMgl=l3oS$uEW}1Ef>;R>bElr#69-$c6c*8^KK_PJ8S{qIlQs+54YwM^RGoM*63wYLly(@ zktIgT8YyH%-uTi2CUae?QEjRd*uW@eT7rtPPNP^iW?D59`1z%f#AZtkw87~IhA;x1 zm&IpZU=07tOdtgh^I{kt)hUb=J*B+TE>n9WR|wo?Da$e=LQ}dqC$zQ$ui%8H=etFv z`%Na3VcK_eNDQfSP7FqNOOE*lC>7xOrx#ao)p$ zD@xd?hYC%Z_wqin9L%*>{OLPUo>bm$NW z{R7dz;9t>E_S&(_f&`1F-dfh7f%)=%Kj!m>_mWB_6#z(oG$)gzV)7mR?;$^KW}q2^ z3HNXTCOL-^tuJd)i;6db%uZ0okbogvqx(K1Vv-Sc)^tbzJ@U$_~~OQxpn1AcFK8&mlR*SxwKuzH5}lp zEz2=Q^q)eZFyEC-NKtNBed!{wEL^8mTXfer!<192cM#gK_qqc=CYa6Yk|*(T1Q2H^ zw-8WG%S|)crxi~N;*iOCs;7pZIv8645H0s7)78zW=Ba^#`7jKz-N-yFW$Ws_bYZlum2qYwk>0~2-|H32m7s+*N AP5=M^ literal 0 HcmV?d00001 diff --git a/tests/pyc/sunk_value_return_control.3.12.pyc b/tests/pyc/sunk_value_return_control.3.12.pyc new file mode 100644 index 0000000000000000000000000000000000000000..58db421e78e9e63bfd11358b6ef7445be05c7bc7 GIT binary patch literal 444 zcmZ8dK}!Nb6n-7)VxbF*?&d^6^s$p4BgS(rpJ0UPe& z3c3OMgfgcmB#p;qrFOLGmF~ThbRMT=( zGdnS!jIkCZz|4)+sgZfij`u&*jh}BFQrllZ(3IJFP=gqx%%v9SIGu?U=G>*$;n^rC sFQlN9EwZ@e6+%>i>j<;NLAzsnEq2J$*hZf%qD2CQ5E91XWRpiRzhSdxtN;K2 literal 0 HcmV?d00001 diff --git a/tests/pyc/sunk_value_return_control.3.13.pyc b/tests/pyc/sunk_value_return_control.3.13.pyc new file mode 100644 index 0000000000000000000000000000000000000000..68224696b4b55b851933f1f99abab0b88b7d65fd GIT binary patch literal 438 zcmZ9IyGsK>5XNWkk;~&^0^%bn%n4}@!8CThu&_;G5Cn@5lTA2txs%;JNjkBLrAV;X zO8h(QEyK1}b_Zu6R?bFa;lK{x%s2dYc9VQQs{_FB<^1_Qeu&>t{{{I~NfyNf8c+rX zJ>qKZxuXUQJPMK2F->3%j^PN^{|HEf1lozSrBz`aPPMsjLCXSgprHj?1%CW4?CKTh z5?8NF>w-~E10k(}$GI2uTuqu-vyogl5Q2GUk)Vf+h0N8ZdD-$K%1neyqwfVBt~PTV zM{Wqp1+R6zp@C2LxhI@=EX>+IQ3~;F!gQ);QN@aAl6K?G%YUO z=WcUjGfqRXG&ZKW!g%}DSpNLTfL%_2;7DQh#)wm37iUhK!QL0Qk#TJ=!kt~!A4yfO oyJU936C~&W%V3h}ok7@%e7eW-SgLR4YKd4Ugd{18tT2W71~oBbdH?_b literal 0 HcmV?d00001 diff --git a/tests/pyc/sunk_value_return_control.3.14.pyc b/tests/pyc/sunk_value_return_control.3.14.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f8c9e4dfb4f8cb62004a630efb9059997bf98a6 GIT binary patch literal 441 zcmZ8dyGjF55S_cPyfzOIjZuROL177)!ZPBcuoX$+1B9T-RhA^X%e{BSbipc?V&M0H~N@4Gt%RAnhbMTN| tQ=iD1dSH|IgJ+l^11yCpOm6kOcF?8UEQ_VQGnY$5Dj{TQBuJ4-n;+e^VfFw3 literal 0 HcmV?d00001 diff --git a/tools/compile_padded_value_returns.py b/tools/compile_padded_value_returns.py new file mode 100644 index 00000000..d3441ad4 --- /dev/null +++ b/tools/compile_padded_value_returns.py @@ -0,0 +1,74 @@ +"""Regenerate the padded value-return fixtures with CPython 3.11–3.14. + +Run once per interpreter from any directory. This prepares bytecode inputs +for tests/integration.rs; it is not a separate test runner. Only RETURN_VALUE +is padded, leaving versions that emit RETURN_CONST as compatibility controls. +""" +import importlib.util +import marshal +from pathlib import Path +import sys +import types + +def pad_return_values(code, count=3): + """Add NOP padding while retaining jumps and exact handler boundaries.""" + import dis + instructions = list(dis.get_instructions(code)) + # This compact fixture needs no extended operands. NOPs + # reproduce the padded return layout, preserving the return's value. + assert not any(item.opname == 'EXTENDED_ARG' for item in instructions) + starts, sizes, cursor = {}, {}, 0 + for index, item in enumerate(instructions): + starts[item.offset] = cursor + end = instructions[index + 1].offset if index + 1 < len(instructions) else len(code.co_code) + sizes[item.offset] = end - item.offset + cursor += sizes[item.offset] + (2 * count if item.opname == 'RETURN_VALUE' else 0) + starts[len(code.co_code)] = cursor + output = bytearray() + for item in instructions: + if item.opname == 'RETURN_VALUE': + output.extend(bytes((dis.opmap['NOP'], 0)) * count) + actual = len(output) + raw = bytearray(code.co_code[item.offset:item.offset + sizes[item.offset]]) + if item.opcode in getattr(dis, "hasjump", dis.hasjrel + dis.hasjabs): + delta = (starts[item.argval] - (actual + sizes[item.offset])) // 2 + operand = -delta if 'BACKWARD' in item.opname else delta + assert 0 <= operand <= 255 + raw[1] = operand + output.extend(raw) + def varint(value, marker=False): + digits = [value & 63] + while value >> 6: + value >>= 6 + digits.append(value & 63) + digits.reverse() + for index in range(len(digits) - 1): + digits[index] |= 64 + if marker: + digits[0] |= 128 + return bytes(digits) + table = b''.join(varint(starts[row.start] // 2, True) + + varint((starts[row.end] - starts[row.start]) // 2) + + varint(starts[row.target] // 2) + + varint((row.depth << 1) | row.lasti) + for row in dis.Bytecode(code).exception_entries) + padded = code.replace(co_code=bytes(output), co_exceptiontable=table, co_linetable=b'') + return padded + + +if __name__ == "__main__": + if not (3, 11) <= sys.version_info[:2] <= (3, 14): + raise SystemExit("Use CPython 3.11, 3.12, 3.13 or 3.14") + root = Path(__file__).resolve().parents[1] + source_path = Path("tests/fixtures/padded_value_returns.py") + source = (root / source_path).read_bytes() + module = compile(source, source_path.as_posix(), "exec", dont_inherit=True) + version = f"{sys.version_info.major}.{sys.version_info.minor}" + header = (importlib.util.MAGIC_NUMBER + (3).to_bytes(4, "little") + + importlib.util.source_hash(source)) + for count in (1, 3, 9): + padded = module.replace(co_consts=tuple( + pad_return_values(value, count) if isinstance(value, types.CodeType) else value + for value in module.co_consts)) + path = root / f"tests/pyc/padded_value_returns_{count}.{version}.pyc" + path.write_bytes(header + marshal.dumps(padded)) From 7d4d6492012facbd0a1bcb7dff0955a3a34cd459 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 18:33:37 -0400 Subject: [PATCH 2/2] Add harness coverage for the reconstruction fix --- .../corpus/3.11.14/padded_value_returns_3.py | 19 ++++++++++++++++++ .../corpus/3.11.14/padded_value_returns_3.pyc | Bin 0 -> 681 bytes tests/corpus/3.12.3/padded_value_returns_3.py | 19 ++++++++++++++++++ .../corpus/3.12.3/padded_value_returns_3.pyc | Bin 0 -> 645 bytes tests/corpus/3.13.7/padded_value_returns_3.py | 19 ++++++++++++++++++ .../corpus/3.13.7/padded_value_returns_3.pyc | Bin 0 -> 649 bytes tests/corpus/3.14.2/padded_value_returns_3.py | 19 ++++++++++++++++++ .../corpus/3.14.2/padded_value_returns_3.pyc | Bin 0 -> 657 bytes tools/compile_padded_value_returns.py | 7 +++++++ 9 files changed, 83 insertions(+) create mode 100644 tests/corpus/3.11.14/padded_value_returns_3.py create mode 100644 tests/corpus/3.11.14/padded_value_returns_3.pyc create mode 100644 tests/corpus/3.12.3/padded_value_returns_3.py create mode 100644 tests/corpus/3.12.3/padded_value_returns_3.pyc create mode 100644 tests/corpus/3.13.7/padded_value_returns_3.py create mode 100644 tests/corpus/3.13.7/padded_value_returns_3.pyc create mode 100644 tests/corpus/3.14.2/padded_value_returns_3.py create mode 100644 tests/corpus/3.14.2/padded_value_returns_3.pyc diff --git a/tests/corpus/3.11.14/padded_value_returns_3.py b/tests/corpus/3.11.14/padded_value_returns_3.py new file mode 100644 index 00000000..fb1d66fb --- /dev/null +++ b/tests/corpus/3.11.14/padded_value_returns_3.py @@ -0,0 +1,19 @@ +# Generate .pyc files with the matching CPython 3.11–3.14 interpreter: +# python tools/compile_padded_value_returns.py +# Each RETURN_VALUE receives 1, 3 or 9 NOPs, with jumps and exception +# boundaries relocated. Padding never constructs a new return value. + +def synchronous(read, build): + try: + item = read() + except ValueError: + return build("missing") + return build(item) + + +async def asynchronous(read, build): + try: + item = await read() + except ValueError: + return build("missing") + return build(item) diff --git a/tests/corpus/3.11.14/padded_value_returns_3.pyc b/tests/corpus/3.11.14/padded_value_returns_3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d8162a77b8647e829a48fcddfff44b264d58f7d GIT binary patch literal 681 zcmcIhyG{Z@6uq+p@-|>X>Xw>`mU99%LrTNEoG>R`C|7O$1hI}8?)XqI3&($M8&%=> z3`?&`b=6cVHQ%*#Y01M2qixx!%fm)l2wjw#YI_NFN}BSZ(QzWOV^*%PZq{vy5!$>) z;ZlH%67<3`xKF0}n5BQ)(pSwei{?jH@eWRZs!!e+EP(g@b zX$gjD;Y7>d8(ycB3RJy%4G-c;ct~dXBg^vKMDr{`C7CQl-YN6oQLQfQs(y?K`lTM% Z-6xoF#@HY5S_WbGv8kz*vWw+2EmkpAa+(l8bMG5IkTr8Y?u3mR6cqINcu+M@lku%`X zg+<^ulFHAhOPzScuOK5tb$dq#+c$TMFTw#9v8#dE6M*B#fcNmZ0- znr~ySEnQhyby}_`Y+2Z_IJf!orV3vcHXYXG zE7(&CWjfJHwFB#H8XOFja+GSgLbzTn#3-|E)&qN!bDtMjq;lJ|B5gl#>sF+?ExRFP zS%zUm^DSGn#IoZaw*qd9<)&#_wzYR8!}qvNhn`q!oB{l#4(792`-)B<0M*; zgGq29heXUNbC6_ZW#9!+Ny}2*Z9)rTP=ExaNz2tx@|b|UlK!k2qwg`80|U1D2I3G{ z5r8R}0qM=bm)BF`J-75bu*TxxUs;O>c?G)C^U4tMEv(_>`xv3O F;|F~tZ#)11 literal 0 HcmV?d00001 diff --git a/tools/compile_padded_value_returns.py b/tools/compile_padded_value_returns.py index d3441ad4..52e1f539 100644 --- a/tools/compile_padded_value_returns.py +++ b/tools/compile_padded_value_returns.py @@ -72,3 +72,10 @@ def varint(value, marker=False): for value in module.co_consts)) path = root / f"tests/pyc/padded_value_returns_{count}.{version}.pyc" path.write_bytes(header + marshal.dumps(padded)) + if count == 3: + # Also exercise a representative padded input in verify_corpus.py. + # Keep the directory named after the interpreter producing the pyc. + corpus = root / "tests/corpus" / ".".join(map(str, sys.version_info[:3])) + corpus.mkdir(parents=True, exist_ok=True) + (corpus / "padded_value_returns_3.py").write_bytes(source) + (corpus / "padded_value_returns_3.pyc").write_bytes(header + marshal.dumps(padded))