Skip to content

Commit 8c93604

Browse files
committed
fix(bytecode): skip DUPN/SWAPN/EXCHANGE immediates in analyze_legacy
Treat these opcodes like PUSH when building the jump table so JUMPDEST bytes in the immediate are not marked valid, and simplify padding now that incomplete immediates are covered by iterator overflow.
1 parent a094a93 commit 8c93604

1 file changed

Lines changed: 34 additions & 10 deletions

File tree

crates/bytecode/src/legacy/analysis.rs

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)