Skip to content

Commit 5262320

Browse files
committed
fix: tighten stack validation semantics
1 parent 48b580b commit 5262320

4 files changed

Lines changed: 468 additions & 157 deletions

File tree

src/generator/stack_ops.rs

Lines changed: 207 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,21 @@ use crate::protocol::Version;
3737
use crate::stack::{InstanceObject, StackObject, StackObjectRef};
3838
use std::collections::{HashMap, HashSet};
3939

40+
fn decode_signed_le_i64(int_bytes: &[u8]) -> i64 {
41+
let size = int_bytes.len().min(8);
42+
let mut value: i64 = 0;
43+
44+
for (index, byte) in int_bytes.iter().take(size).enumerate() {
45+
value |= (*byte as i64) << (index * 8);
46+
}
47+
48+
if size > 0 && size < 8 && int_bytes[size - 1] & 0x80 != 0 {
49+
value |= !0i64 << (size * 8);
50+
}
51+
52+
value
53+
}
54+
4055
impl Generator {
4156
/// clean up the stack to prepare for the STOP opcode.
4257
///
@@ -47,7 +62,8 @@ impl Generator {
4762
/// - if stack is empty: push a None value
4863
/// - if stack has one MARK: pop it and push None
4964
/// - if stack has multiple items with MARK: combine items above MARK into tuple
50-
/// - if stack has multiple items without MARK: combine top items into tuples
65+
/// - if stack has multiple items without MARK: use protocol-appropriate
66+
/// cleanup to reduce the stack to one item
5167
///
5268
/// this method is called at the end of generation to ensure the pickle is
5369
/// valid before emitting the final STOP opcode.
@@ -62,15 +78,17 @@ impl Generator {
6278
self.emit_opcode(Tuple);
6379
}
6480

65-
// at this point, stack has no MARKs, just regular items
66-
// keep combining until we have exactly 1 item
67-
// use TUPLE2/TUPLE3 which don't require MARKs
81+
// at this point, stack has no MARKs, just regular items.
82+
// protocol 2+ can use TUPLE2/TUPLE3, but protocol 0/1 must not emit
83+
// those shortcut opcodes during cleanup.
6884
let mut safety_counter = 0;
6985
while self.state.stack.len() > 1 && safety_counter < 10000 {
7086
safety_counter += 1;
7187

7288
let stack_len = self.state.stack.len();
73-
if stack_len >= 3 {
89+
if self.state.version < Version::V2 {
90+
self.emit_opcode(Pop);
91+
} else if stack_len >= 3 {
7492
self.emit_opcode(Tuple3);
7593
} else if stack_len == 2 {
7694
self.emit_opcode(Tuple2);
@@ -237,8 +255,8 @@ impl Generator {
237255
self.push(StackObject::Dict(HashMap::new()));
238256
}
239257
Dict => {
240-
// allow interior mutability in hash keys - our Hash/Eq implementations
241-
// are value-based and we never mutate objects used as dict keys
258+
// allow interior mutability in hash keys because StackObjectRef
259+
// hashes and compares by Rc identity, not the borrowed value
242260
#[allow(clippy::mutable_key_type)]
243261
let mut accumulated = HashMap::new();
244262

@@ -324,8 +342,8 @@ impl Generator {
324342
}
325343
}
326344
FrozenSet => {
327-
// allow interior mutability in hash keys - our Hash/Eq implementations
328-
// are value-based and we never mutate objects used as set members
345+
// allow interior mutability in hash keys because StackObjectRef
346+
// hashes and compares by Rc identity, not the borrowed value
329347
#[allow(clippy::mutable_key_type)]
330348
let mut accumulated = HashSet::new();
331349

@@ -353,10 +371,9 @@ impl Generator {
353371
};
354372
// in protocol 0-1, INT opcode with 00/01 represents booleans
355373
// protocol 2+ has dedicated NEWTRUE/NEWFALSE opcodes
356-
if matches!(self.state.version, Version::V0 | Version::V1)
357-
&& (value == 0 || value == 1)
358-
{
359-
self.push(StackObject::Bool(value == 1));
374+
let is_bool_literal = matches!(arg_bytes, Some(b"00\n" | b"01\n"));
375+
if matches!(self.state.version, Version::V0 | Version::V1) && is_bool_literal {
376+
self.push(StackObject::Bool(value != 0));
360377
} else {
361378
self.push(StackObject::Int(value));
362379
}
@@ -416,14 +433,7 @@ impl Generator {
416433
let size = arg_bytes[0] as usize;
417434
if arg_bytes.len() > size {
418435
let int_bytes = &arg_bytes[1..1 + size];
419-
// interpret as little-endian integer without static size
420-
// limit to i64 size (8 bytes) to prevent overflow
421-
let mut value: i64 = 0;
422-
for (i, &b) in int_bytes.iter().enumerate().take(8) {
423-
// safe: i < 8, so i*8 < 64, shift is always valid
424-
value |= (b as i64) << (i * 8);
425-
}
426-
self.push(StackObject::Int(value));
436+
self.push(StackObject::Int(decode_signed_le_i64(int_bytes)));
427437
}
428438
}
429439
}
@@ -438,14 +448,7 @@ impl Generator {
438448
]) as usize;
439449
if arg_bytes.len() >= 4 + size {
440450
let int_bytes = &arg_bytes[4..4 + size];
441-
// interpret as little-endian integer without static size
442-
// limit to i64 size (8 bytes) to prevent overflow
443-
let mut value: i64 = 0;
444-
for (i, &b) in int_bytes.iter().enumerate().take(8) {
445-
// safe: i < 8, so i*8 < 64, shift is always valid
446-
value |= (b as i64) << (i * 8);
447-
}
448-
self.push(StackObject::Int(value));
451+
self.push(StackObject::Int(decode_signed_le_i64(int_bytes)));
449452
}
450453
}
451454
}
@@ -578,12 +581,16 @@ impl Generator {
578581
return;
579582
}
580583

581-
// pops state and instance, updates instance's args
582-
if let (Some(state), Some(instance_ref)) = (self.pop(), self.pop()) {
583-
if let StackObject::Instance(ref mut inst) = *instance_ref.borrow_mut() {
584+
// BUILD pops the state but mutates the existing instance in place.
585+
if let Some(state) = self.pop() {
586+
let Some(instance_ref) = self.peek().cloned() else {
587+
return;
588+
};
589+
590+
let mut instance = instance_ref.borrow_mut();
591+
if let StackObject::Instance(ref mut inst) = *instance {
584592
inst.args = state;
585593
}
586-
self.push(instance_ref.borrow().clone());
587594
}
588595
}
589596
Inst => {
@@ -709,8 +716,7 @@ impl Generator {
709716
if let Ok(index_str) = std::str::from_utf8(arg_bytes) {
710717
if let Ok(index) = index_str.trim().parse() {
711718
if let Some(obj) = self.get(index) {
712-
let cloned = obj.borrow().clone();
713-
self.push(cloned);
719+
self.push_ref(obj);
714720
}
715721
}
716722
}
@@ -720,8 +726,7 @@ impl Generator {
720726
if let Some(arg_bytes) = arg_bytes {
721727
let index = arg_bytes[0] as usize;
722728
if let Some(obj) = self.get(index) {
723-
let cloned = obj.borrow().clone();
724-
self.push(cloned);
729+
self.push_ref(obj);
725730
}
726731
}
727732
}
@@ -734,8 +739,7 @@ impl Generator {
734739
arg_bytes[3],
735740
]) as usize;
736741
if let Some(obj) = self.get(index) {
737-
let cloned = obj.borrow().clone();
738-
self.push(cloned);
742+
self.push_ref(obj);
739743
}
740744
}
741745
}
@@ -745,9 +749,8 @@ impl Generator {
745749
if let Ok(index_str) = std::str::from_utf8(arg_bytes) {
746750
if let Ok(index) = index_str.trim().parse() {
747751
if let Some(top) = self.peek() {
748-
let obj = top.borrow().clone();
749-
if !matches!(obj, StackObject::Mark) {
750-
self.put(index, obj);
752+
if !matches!(*top.borrow(), StackObject::Mark) {
753+
self.put(index, top.clone());
751754
}
752755
}
753756
}
@@ -759,9 +762,8 @@ impl Generator {
759762
if let Some(arg_bytes) = arg_bytes {
760763
let index = arg_bytes[0] as usize;
761764
if let Some(top) = self.peek() {
762-
let obj = top.borrow().clone();
763-
if !matches!(obj, StackObject::Mark) {
764-
self.put(index, obj);
765+
if !matches!(*top.borrow(), StackObject::Mark) {
766+
self.put(index, top.clone());
765767
}
766768
}
767769
}
@@ -776,17 +778,15 @@ impl Generator {
776778
arg_bytes[3],
777779
]) as usize;
778780
if let Some(top) = self.peek() {
779-
let obj = top.borrow().clone();
780-
if !matches!(obj, StackObject::Mark) {
781-
self.put(index, obj);
781+
if !matches!(*top.borrow(), StackObject::Mark) {
782+
self.put(index, top.clone());
782783
}
783784
}
784785
}
785786
}
786787
Memoize => {
787-
if let Some(top) = self.pop() {
788-
self.put(self.state.memo.len(), top.borrow().clone());
789-
self.push(top.borrow().clone())
788+
if let Some(top) = self.peek().cloned() {
789+
self.put(self.state.memo.len(), top);
790790
}
791791
}
792792
Ext1 | Ext2 | Ext4 => {
@@ -804,7 +804,17 @@ impl Generator {
804804
// NEXT_BUFFER pushes a buffer object to the stack
805805
self.push(StackObject::Bytes(Vec::new())); // Use empty bytes as placeholder
806806
}
807-
Proto | ReadOnlyBuffer | Stop | Frame => {
807+
ReadOnlyBuffer => {
808+
if let Some(buffer) = self.pop() {
809+
let readonly_buffer = match &*buffer.borrow() {
810+
StackObject::Bytes(bytes) => StackObject::Bytes(bytes.clone()),
811+
StackObject::ByteArray(bytes) => StackObject::Bytes(bytes.clone()),
812+
other => other.clone(),
813+
};
814+
self.push(readonly_buffer);
815+
}
816+
}
817+
Proto | Stop | Frame => {
808818
// these opcodes don't manipulate the stack, but we're being
809819
// explicit about it so that we know we've covered all opcodes
810820
}
@@ -817,3 +827,148 @@ impl Generator {
817827
// pos, format!("{:?}", opcode), before, after, delta);
818828
}
819829
}
830+
831+
#[cfg(test)]
832+
mod tests {
833+
use super::*;
834+
use crate::opcodes::OpcodeKind;
835+
use crate::{Generator, Version};
836+
use std::rc::Rc;
837+
838+
#[test]
839+
fn cleanup_for_stop_uses_pop_for_protocol_0() {
840+
let mut generator = Generator::new(Version::V0);
841+
generator.push(StackObject::Int(1));
842+
generator.push(StackObject::Int(2));
843+
generator.push(StackObject::Int(3));
844+
845+
generator.cleanup_for_stop();
846+
847+
assert_eq!(generator.state.stack.len(), 1);
848+
assert!(generator.output.contains(&OpcodeKind::Pop.as_u8()));
849+
assert!(!generator.output.contains(&OpcodeKind::Tuple2.as_u8()));
850+
assert!(!generator.output.contains(&OpcodeKind::Tuple3.as_u8()));
851+
}
852+
853+
#[test]
854+
fn cleanup_for_stop_keeps_tuple_shortcuts_for_protocol_2() {
855+
let mut generator = Generator::new(Version::V2);
856+
generator.push(StackObject::Int(1));
857+
generator.push(StackObject::Int(2));
858+
generator.push(StackObject::Int(3));
859+
860+
generator.cleanup_for_stop();
861+
862+
assert_eq!(generator.state.stack.len(), 1);
863+
assert!(generator.output.contains(&OpcodeKind::Tuple3.as_u8()));
864+
}
865+
866+
#[test]
867+
fn int_opcode_only_treats_00_and_01_as_bools() {
868+
let mut generator = Generator::new(Version::V0);
869+
generator.process_stack_ops(OpcodeKind::Int, Some(b"1\n"));
870+
assert!(matches!(
871+
*generator.peek().unwrap().borrow(),
872+
StackObject::Int(1)
873+
));
874+
875+
generator.reset();
876+
generator.process_stack_ops(OpcodeKind::Int, Some(b"01\n"));
877+
assert!(matches!(
878+
*generator.peek().unwrap().borrow(),
879+
StackObject::Bool(true)
880+
));
881+
882+
generator.reset();
883+
generator.process_stack_ops(OpcodeKind::Int, Some(b"00\n"));
884+
assert!(matches!(
885+
*generator.peek().unwrap().borrow(),
886+
StackObject::Bool(false)
887+
));
888+
}
889+
890+
#[test]
891+
fn long_opcodes_sign_extend_short_negative_values() {
892+
let mut generator = Generator::new(Version::V4);
893+
generator.process_stack_ops(OpcodeKind::Long1, Some(&[1, 0xff]));
894+
assert!(matches!(
895+
*generator.peek().unwrap().borrow(),
896+
StackObject::Int(-1)
897+
));
898+
899+
generator.reset();
900+
generator.process_stack_ops(OpcodeKind::Long4, Some(&[1, 0, 0, 0, 0x80]));
901+
assert!(matches!(
902+
*generator.peek().unwrap().borrow(),
903+
StackObject::Int(-128)
904+
));
905+
}
906+
907+
#[test]
908+
fn get_put_and_memoize_preserve_aliasing() {
909+
let mut generator = Generator::new(Version::V4);
910+
let shared = StackObjectRef::new(StackObject::List(Vec::new()));
911+
generator.push_ref(shared.clone());
912+
913+
generator.process_stack_ops(OpcodeKind::BinPut, Some(&[0]));
914+
generator.process_stack_ops(OpcodeKind::BinGet, Some(&[0]));
915+
916+
let top = generator.peek().unwrap().clone();
917+
let below = generator.peek_at(1).unwrap().clone();
918+
let memoized = generator.get(0).unwrap();
919+
assert!(Rc::ptr_eq(&top.0, &shared.0));
920+
assert!(Rc::ptr_eq(&below.0, &shared.0));
921+
assert!(Rc::ptr_eq(&memoized.0, &shared.0));
922+
923+
generator.reset();
924+
generator.push_ref(shared.clone());
925+
generator.process_stack_ops(OpcodeKind::Memoize, None);
926+
let memoized = generator.get(0).unwrap();
927+
let top = generator.peek().unwrap().clone();
928+
assert_eq!(generator.state.stack.len(), 1);
929+
assert!(Rc::ptr_eq(&memoized.0, &shared.0));
930+
assert!(Rc::ptr_eq(&top.0, &shared.0));
931+
}
932+
933+
#[test]
934+
fn build_mutates_existing_instance_in_place() {
935+
let mut generator = Generator::new(Version::V4);
936+
let callable = StackObjectRef::new(StackObject::Global {
937+
module: "builtins".to_string(),
938+
name: "object".to_string(),
939+
});
940+
let initial_args = StackObjectRef::new(StackObject::Tuple(Vec::new()));
941+
let instance = StackObjectRef::new(StackObject::Instance(InstanceObject {
942+
callable,
943+
args: initial_args,
944+
}));
945+
let state = StackObjectRef::new(StackObject::Dict(HashMap::new()));
946+
947+
generator.push_ref(instance.clone());
948+
generator.push_ref(state.clone());
949+
generator.process_stack_ops(OpcodeKind::Build, None);
950+
951+
assert_eq!(generator.state.stack.len(), 1);
952+
let top = generator.peek().unwrap().clone();
953+
assert!(Rc::ptr_eq(&top.0, &instance.0));
954+
955+
let borrowed = top.borrow();
956+
let StackObject::Instance(instance) = &*borrowed else {
957+
panic!("expected instance on stack after BUILD");
958+
};
959+
assert!(Rc::ptr_eq(&instance.args.0, &state.0));
960+
}
961+
962+
#[test]
963+
fn readonly_buffer_turns_bytearray_into_bytes() {
964+
let mut generator = Generator::new(Version::V5);
965+
generator.push(StackObject::ByteArray(vec![1, 2, 3]));
966+
967+
generator.process_stack_ops(OpcodeKind::ReadOnlyBuffer, None);
968+
969+
assert!(matches!(
970+
&*generator.peek().unwrap().borrow(),
971+
StackObject::Bytes(bytes) if bytes == &vec![1, 2, 3]
972+
));
973+
}
974+
}

0 commit comments

Comments
 (0)