@@ -13,11 +13,9 @@ pub(crate) fn analyze_legacy(bytecode: Bytes) -> (JumpTable, Bytes) {
1313 let start = range. start ;
1414 let mut iterator = start;
1515 let end = range. end ;
16- let mut prev_byte: u8 = 0 ;
1716 let mut last_byte: u8 = 0 ;
1817
1918 while iterator < end {
20- prev_byte = last_byte;
2119 last_byte = unsafe { * iterator } ;
2220 if last_byte == opcode:: JUMPDEST {
2321 // SAFETY: Jumps are max length of the code
@@ -30,6 +28,10 @@ pub(crate) fn analyze_legacy(bytecode: Bytes) -> (JumpTable, Bytes) {
3028 // bytecode allocation; `wrapping_add` keeps that offset
3129 // computation defined (the `< end` guard prevents any OOB read).
3230 iterator = iterator. wrapping_add ( push_offset as usize + 2 ) ;
31+ } else if is_dupn_swapn_exchange ( last_byte) {
32+ // Same as PUSH: skip the 1-byte immediate. Trailing opcodes may
33+ // advance past `end`; `wrapping_add` keeps that defined.
34+ iterator = iterator. wrapping_add ( 2 ) ;
3335 } else {
3436 // SAFETY: Iterator access range is checked in the while loop
3537 iterator = unsafe { iterator. add ( 1 ) } ;
@@ -38,17 +40,14 @@ pub(crate) fn analyze_legacy(bytecode: Bytes) -> (JumpTable, Bytes) {
3840 }
3941
4042 // Calculate padding needed:
41- // push_overflow: bytes needed for incomplete PUSH immediate data
43+ // push_overflow covers incomplete PUSH / DUPN / SWAPN / EXCHANGE immediates
44+ // that caused the iterator to advance past the end of the bytecode.
4245 let push_overflow = ( iterator as usize ) - ( end as usize ) ;
4346 let mut padding = push_overflow;
4447
45- if last_byte == opcode:: STOP {
46- // DUPN/SWAPN/EXCHANGE have 1-byte immediates that aren't handled by the loop above,
47- // so we need extra padding to ensure safe execution.
48- padding += is_dupn_swapn_exchange ( prev_byte) as usize ;
49- } else {
50- // Add final STOP instruction and immediate for DUPN/SWAPN/EXCHANGE
51- padding += 1 + is_dupn_swapn_exchange ( last_byte) as usize ;
48+ if last_byte != opcode:: STOP {
49+ // Append a final STOP so execution always has a terminating opcode.
50+ padding += 1 ;
5251 }
5352
5453 let bytecode = if padding > 0 {
@@ -212,4 +211,29 @@ mod tests {
212211 }
213212 }
214213 }
214+
215+ #[ test]
216+ fn test_jumpdest_in_dupn_swapn_exchange_immediate_is_not_valid ( ) {
217+ // Regression: DUPN/SWAPN/EXCHANGE have a 1-byte immediate that must be
218+ // skipped during analysis (same as PUSH data). Otherwise a JUMPDEST byte
219+ // in the immediate is incorrectly marked as a valid jump target.
220+ for op in [ opcode:: DUPN , opcode:: SWAPN , opcode:: EXCHANGE ] {
221+ let bytecode = vec ! [ op, opcode:: JUMPDEST , opcode:: STOP ] ;
222+ let ( jump_table, padded_bytecode) = analyze_legacy ( bytecode. clone ( ) . into ( ) ) ;
223+ assert_eq ! ( padded_bytecode. len( ) , bytecode. len( ) ) ;
224+ assert ! ( !jump_table. is_valid( 1 ) , "immediate of {op:#04x} must not be JUMPDEST" ) ;
225+ }
226+ }
227+
228+ #[ test]
229+ fn test_truncated_dupn_swapn_exchange_are_padded_like_push ( ) {
230+ // Truncated opcode+immediate must pad the missing immediate and a STOP.
231+ for op in [ opcode:: DUPN , opcode:: SWAPN , opcode:: EXCHANGE ] {
232+ let bytecode = vec ! [ op] ;
233+ let ( _, padded_bytecode) = analyze_legacy ( bytecode. clone ( ) . into ( ) ) ;
234+ assert_eq ! ( padded_bytecode. len( ) , bytecode. len( ) + 2 ) ;
235+ assert_eq ! ( & padded_bytecode[ ..] , & [ op, opcode:: STOP , opcode:: STOP ] ) ;
236+ }
237+ }
238+
215239}
0 commit comments