Skip to content

Commit 769bcd2

Browse files
committed
avoid reading entry data as num_entries when offset > 0
1 parent 4712fef commit 769bcd2

File tree

2 files changed

+120
-35
lines changed

2 files changed

+120
-35
lines changed

sdk/pinocchio/src/sysvars/slot_hashes/raw.rs

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,35 @@ use super::*;
1111
/// Validates that a buffer is properly sized for `SlotHashes` data.
1212
///
1313
/// Checks that the buffer length is `8 + (N × 40)` for some `N ≤ 512`.
14-
/// Unlike the `SlotHashes` constructor, this function does not require
15-
/// the buffer to be exactly 20,488 bytes.
14+
///
15+
/// This returns the maximum number of entries that can fit in the buffer.
1616
#[inline(always)]
17-
pub(crate) fn validate_buffer_size(buffer_len: usize) -> Result<(), ProgramError> {
18-
// Must have space for 8-byte header
19-
if buffer_len < NUM_ENTRIES_SIZE {
20-
return Err(ProgramError::AccountDataTooSmall);
21-
}
17+
pub(crate) fn validate_buffer_size(
18+
buffer_len: usize,
19+
offset: usize,
20+
) -> Result<usize, ProgramError> {
21+
let data_len = if offset == 0 {
22+
if buffer_len == MAX_SIZE {
23+
return Ok(MAX_ENTRIES);
24+
}
25+
// Must have space for 8-byte header
26+
if buffer_len < NUM_ENTRIES_SIZE {
27+
return Err(ProgramError::AccountDataTooSmall);
28+
}
29+
buffer_len - NUM_ENTRIES_SIZE
30+
} else {
31+
buffer_len
32+
};
2233

23-
// Calculate how many entries can fit
24-
let data_len = buffer_len - NUM_ENTRIES_SIZE;
2534
if data_len % ENTRY_SIZE != 0 {
2635
return Err(ProgramError::InvalidArgument);
2736
}
28-
29-
let max_entries = data_len / ENTRY_SIZE;
30-
if max_entries > MAX_ENTRIES {
37+
let max_entries_in_buffer = data_len / ENTRY_SIZE;
38+
if max_entries_in_buffer > MAX_ENTRIES {
3139
return Err(ProgramError::InvalidArgument);
3240
}
3341

34-
Ok(())
42+
Ok(max_entries_in_buffer)
3543
}
3644

3745
/// Validates offset parameters for fetching `SlotHashes` data.
@@ -55,26 +63,26 @@ pub fn validate_fetch_offset(offset: usize, buffer_len: usize) -> Result<(), Pro
5563

5664
/// Copies `SlotHashes` sysvar bytes into `buffer`, performing validation.
5765
///
58-
/// Returns the number of entries present in the sysvar.
66+
/// Returns the number of entries *that can fit in the buffer*. If the buffer
67+
/// can fit more entries than are present in the sysvar from `offset` to the end,
68+
/// this returned value will mismatch the actual entries filled into the buffer.
69+
/// When `offset > 0`, it is recommended not to provide buffers larger than
70+
/// `MAX_SIZE - NUM_ENTRIES_SIZE - (offset * ENTRY_SIZE)`.
5971
#[inline(always)]
6072
pub fn fetch_into(buffer: &mut [u8], offset: usize) -> Result<usize, ProgramError> {
61-
if buffer.len() != MAX_SIZE {
62-
validate_buffer_size(buffer.len())?;
63-
}
73+
let num_entries = validate_buffer_size(buffer.len(), offset)?;
6474

6575
validate_fetch_offset(offset, buffer.len())?;
6676

6777
// SAFETY: `buffer.len()` and `offset` are both validated above.
6878
unsafe { fetch_into_unchecked(buffer, offset) }?;
6979

70-
let num_entries = read_entry_count_from_bytes(buffer).unwrap_or(0);
71-
72-
let required_len = NUM_ENTRIES_SIZE + num_entries * ENTRY_SIZE;
73-
if buffer.len() < required_len {
74-
return Err(ProgramError::InvalidArgument);
80+
if offset == 0 {
81+
// If header is preset, read entries from it
82+
Ok(read_entry_count_from_bytes(buffer).unwrap_or(0))
83+
} else {
84+
Ok(num_entries)
7585
}
76-
77-
Ok(num_entries)
7886
}
7987

8088
/// Copies `SlotHashes` sysvar bytes into `buffer` **without** validation.

sdk/pinocchio/src/sysvars/slot_hashes/test_raw.rs

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,74 @@ extern crate std;
66

77
#[test]
88
fn test_validate_buffer_size() {
9+
// ===== Tests with offset = 0 (buffer includes header) =====
10+
11+
// Too small to fit header
912
let small_len = 4;
10-
assert!(raw::validate_buffer_size(small_len).is_err());
13+
assert!(raw::validate_buffer_size(small_len, 0).is_err());
1114

15+
// Misaligned: header + partial entry
1216
let misaligned_len = NUM_ENTRIES_SIZE + 39;
13-
assert!(raw::validate_buffer_size(misaligned_len).is_err());
17+
assert!(raw::validate_buffer_size(misaligned_len, 0).is_err());
1418

19+
// Too large: more than MAX_ENTRIES
1520
let oversized_len = NUM_ENTRIES_SIZE + (MAX_ENTRIES + 1) * ENTRY_SIZE;
16-
assert!(raw::validate_buffer_size(oversized_len).is_err());
21+
assert!(raw::validate_buffer_size(oversized_len, 0).is_err());
1722

23+
// Valid cases with offset = 0
1824
let valid_empty_len = NUM_ENTRIES_SIZE;
19-
assert!(raw::validate_buffer_size(valid_empty_len).is_ok());
25+
assert_eq!(raw::validate_buffer_size(valid_empty_len, 0).unwrap(), 0);
2026

2127
let valid_one_len = NUM_ENTRIES_SIZE + ENTRY_SIZE;
22-
assert!(raw::validate_buffer_size(valid_one_len).is_ok());
28+
assert_eq!(raw::validate_buffer_size(valid_one_len, 0).unwrap(), 1);
2329

2430
let valid_max_len = NUM_ENTRIES_SIZE + MAX_ENTRIES * ENTRY_SIZE;
25-
assert!(raw::validate_buffer_size(valid_max_len).is_ok());
31+
assert_eq!(
32+
raw::validate_buffer_size(valid_max_len, 0).unwrap(),
33+
MAX_ENTRIES
34+
);
35+
36+
// Edge case: exactly at the boundary (MAX_SIZE)
37+
assert_eq!(raw::validate_buffer_size(MAX_SIZE, 0).unwrap(), MAX_ENTRIES);
38+
39+
// ===== Tests with offset != 0 (buffer doesn't include header) =====
40+
41+
// Valid cases with non-zero offset - buffer contains only entry data
42+
43+
// Buffer for exactly 1 entry
44+
assert_eq!(raw::validate_buffer_size(ENTRY_SIZE, 8).unwrap(), 1);
45+
46+
// Buffer for exactly 2 entries
47+
assert_eq!(raw::validate_buffer_size(2 * ENTRY_SIZE, 8).unwrap(), 2);
48+
49+
// Buffer for maximum entries (without header space)
50+
assert_eq!(
51+
raw::validate_buffer_size(MAX_ENTRIES * ENTRY_SIZE, 8).unwrap(),
52+
MAX_ENTRIES
53+
);
54+
55+
// Buffer for 10 entries
56+
assert_eq!(raw::validate_buffer_size(10 * ENTRY_SIZE, 48).unwrap(), 10);
2657

27-
// Edge case: exactly at the boundary
28-
let boundary_len = NUM_ENTRIES_SIZE + MAX_ENTRIES * ENTRY_SIZE;
29-
assert!(raw::validate_buffer_size(boundary_len).is_ok());
58+
// Error cases with non-zero offset
59+
60+
// Misaligned buffer - not a multiple of ENTRY_SIZE
61+
assert!(raw::validate_buffer_size(ENTRY_SIZE + 1, 8).is_err());
62+
assert!(raw::validate_buffer_size(ENTRY_SIZE - 1, 8).is_err());
63+
assert!(raw::validate_buffer_size(39, 8).is_err()); // 39 is not divisible by 40
64+
65+
// Too many entries - more than MAX_ENTRIES
66+
assert!(raw::validate_buffer_size((MAX_ENTRIES + 1) * ENTRY_SIZE, 8).is_err());
67+
assert!(raw::validate_buffer_size((MAX_ENTRIES + 10) * ENTRY_SIZE, 48).is_err());
68+
69+
// Empty buffer with offset (valid - 0 entries)
70+
assert_eq!(raw::validate_buffer_size(0, 8).unwrap(), 0);
71+
72+
// ===== Additional edge cases =====
73+
74+
// Large offset values (should still work for buffer size validation)
75+
assert_eq!(raw::validate_buffer_size(5 * ENTRY_SIZE, 1000).unwrap(), 5);
76+
assert!(raw::validate_buffer_size(5 * ENTRY_SIZE + 1, 2000).is_err()); // misaligned
3077
}
3178

3279
#[test]
@@ -90,9 +137,10 @@ fn test_fetch_into_host_stub() {
90137
let n3 = raw::fetch_into(&mut one_entry, 0).expect("fetch_into(one_entry, 0)");
91138
assert_eq!(n3, 0);
92139

93-
// 4. Header-skipped fetch should fail because header is missing.
140+
// 4. Header-skipped fetch should succeed and return the number of entries that fit.
94141
let mut skip_header = std::vec![0u8; ENTRY_SIZE];
95-
assert!(raw::fetch_into(&mut skip_header, 8).is_err());
142+
let entries_count = raw::fetch_into(&mut skip_header, 8).expect("fetch_into(skip_header, 8)");
143+
assert_eq!(entries_count, 1); // Buffer can fit exactly 1 entry
96144

97145
// 5. Mis-aligned buffer size should error.
98146
let mut misaligned = std::vec![0u8; NUM_ENTRIES_SIZE + 39];
@@ -106,3 +154,32 @@ fn test_fetch_into_host_stub() {
106154
let mut small = std::vec![0u8; 200];
107155
assert!(raw::fetch_into(&mut small, MAX_SIZE - 199).is_err());
108156
}
157+
158+
/// Test that `fetch_into` with offset correctly avoids interpreting slot
159+
/// data as entry count.
160+
#[cfg(test)]
161+
#[test]
162+
fn test_fetch_into_offset_avoids_incorrect_entry_count() {
163+
// The core issue: when fetch_into is called with offset != 0, the first
164+
// 8 bytes of the buffer contains header data, NOT entry data.
165+
let mut buffer = std::vec![0u8; 3 * ENTRY_SIZE];
166+
167+
// Call fetch_into with offset 8 (skipping the 8-byte header)
168+
let result = raw::fetch_into(&mut buffer, 8);
169+
170+
assert!(
171+
result.is_ok(),
172+
"fetch_into should succeed with offset that skips header"
173+
);
174+
175+
let entries_that_fit = result.unwrap();
176+
assert_eq!(
177+
entries_that_fit, 3,
178+
"Should return number of entries that fit in buffer, not some slot number"
179+
);
180+
181+
// Buffer for exactly 1 entry starting from offset 48 (2nd entry)
182+
let mut second_entry_buffer = std::vec![0u8; ENTRY_SIZE];
183+
let second_result = raw::fetch_into(&mut second_entry_buffer, 48).unwrap();
184+
assert_eq!(second_result, 1);
185+
}

0 commit comments

Comments
 (0)