1use vyre::ir::DataType as IrDataType;
10
11use crate::value::Value;
12use vyre::ir::DataType;
13
14use std::cell::Cell;
15use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
16
17#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
29pub struct OobReport {
30 pub oob_loads: u64,
32 pub oob_stores: u64,
34 pub oob_atomics: u64,
36}
37
38impl OobReport {
39 #[must_use]
42 pub fn total(&self) -> u64 {
43 self.oob_loads
44 .saturating_add(self.oob_stores)
45 .saturating_add(self.oob_atomics)
46 }
47}
48
49thread_local! {
50 static OOB_COUNTS: Cell<OobReport> = const { Cell::new(OobReport {
53 oob_loads: 0,
54 oob_stores: 0,
55 oob_atomics: 0,
56 }) };
57}
58
59fn record_oob_load() {
60 OOB_COUNTS.with(|c| {
61 let mut r = c.get();
62 r.oob_loads = r.oob_loads.saturating_add(1);
63 c.set(r);
64 });
65}
66
67fn record_oob_store() {
68 OOB_COUNTS.with(|c| {
69 let mut r = c.get();
70 r.oob_stores = r.oob_stores.saturating_add(1);
71 c.set(r);
72 });
73}
74
75fn record_oob_atomic() {
76 OOB_COUNTS.with(|c| {
77 let mut r = c.get();
78 r.oob_atomics = r.oob_atomics.saturating_add(1);
79 c.set(r);
80 });
81}
82
83pub fn reset_oob_report() {
85 OOB_COUNTS.with(|c| c.set(OobReport::default()));
86}
87
88#[must_use]
90pub fn oob_report() -> OobReport {
91 OOB_COUNTS.with(Cell::get)
92}
93
94#[derive(Debug, Clone)]
100pub struct Buffer {
101 pub(crate) bytes: Arc<RwLock<Vec<u8>>>,
102 pub(crate) element: IrDataType,
103}
104
105impl Buffer {
106 #[must_use]
108 pub fn new(bytes: Vec<u8>, element: DataType) -> Self {
109 Self {
110 bytes: Arc::new(RwLock::new(bytes)),
111 element,
112 }
113 }
114
115 fn read_bytes(&self) -> RwLockReadGuard<'_, Vec<u8>> {
122 self.bytes
123 .read()
124 .unwrap_or_else(|_| panic!("reference Buffer byte lock was poisoned"))
125 }
126
127 fn write_bytes(&self) -> RwLockWriteGuard<'_, Vec<u8>> {
130 self.bytes
131 .write()
132 .unwrap_or_else(|_| panic!("reference Buffer byte lock was poisoned"))
133 }
134
135 pub(crate) fn len(&self) -> u32 {
136 let bytes_guard = self.read_bytes();
137 let count = if let Some(bits) = self.element.bit_width() {
138 bytes_guard
139 .len()
140 .checked_mul(8)
141 .map(|total_bits| total_bits / bits)
142 .unwrap_or(usize::MAX)
143 } else if let Some(stride) = self.element.size_bytes() {
144 if stride == 0 {
145 bytes_guard.len()
146 } else {
147 bytes_guard.len() / stride
148 }
149 } else {
150 bytes_guard.len()
151 };
152 match u32::try_from(count) {
153 Ok(value) => value,
154 Err(_) => {
155 debug_assert!(
156 false,
157 "Buffer::len overflowed u32::MAX for byte_len={}; element={:?}. \
158 Fix: split or downsize the buffer so per-element indexing remains representable.",
159 bytes_guard.len(),
160 self.element
161 );
162 u32::MAX
163 }
164 }
165 }
166
167 pub(crate) fn byte_len(&self) -> usize {
168 self.read_bytes().len()
169 }
170
171 pub(crate) fn element(&self) -> &IrDataType {
172 &self.element
173 }
174
175 pub(crate) fn zero_fill(&self) {
176 self.write_bytes().fill(0);
177 }
178
179 pub(crate) fn into_bytes(self) -> Vec<u8> {
180 std::sync::Arc::try_unwrap(self.bytes)
183 .map(|rw| {
184 rw.into_inner()
185 .unwrap_or_else(|_| panic!("reference Buffer byte lock was poisoned"))
186 })
187 .unwrap_or_else(|shared| {
188 shared
189 .read()
190 .unwrap_or_else(|_| panic!("reference Buffer byte lock was poisoned"))
191 .clone()
192 })
193 }
194
195 #[must_use]
197 pub fn to_value(self) -> crate::value::Value {
198 crate::value::Value::from(self.into_bytes())
199 }
200}
201
202pub(crate) fn load(buffer: &Buffer, index: u32) -> Value {
203 let bytes_guard = buffer.read_bytes();
204 let stride = buffer.element.min_bytes();
205 let ty = ir_to_conform_type(buffer.element.clone());
206 if matches!(buffer.element, IrDataType::Bytes) {
207 let offset = index as usize;
208 if offset > bytes_guard.len() {
209 record_oob_load();
210 return Value::from(Vec::new());
211 }
212 return Value::from(&bytes_guard[offset..]);
213 }
214 let Some(offset) = byte_offset(index, stride) else {
215 record_oob_load();
216 return Value::try_zero_for(ty).unwrap_or_else(|| Value::from(Vec::new()));
217 };
218 if stride == 0 || offset + stride > bytes_guard.len() {
219 record_oob_load();
220 return Value::try_zero_for(ty).unwrap_or_else(|| Value::from(Vec::new()));
221 }
222 read_element(ty.clone(), &bytes_guard[offset..offset + stride])
223 .unwrap_or_else(|_| Value::try_zero_for(ty).unwrap_or_else(|| Value::from(Vec::new())))
224}
225
226pub(crate) fn store(buffer: &mut Buffer, index: u32, value: &Value) {
227 let mut bytes_guard = buffer.write_bytes();
228 let stride = buffer.element.min_bytes();
229 if matches!(buffer.element, IrDataType::Bytes) {
230 let offset = index as usize;
231 if offset >= bytes_guard.len() {
232 record_oob_store();
233 return;
234 }
235 let bytes = value.to_bytes();
236 let available = bytes_guard.len() - offset;
237 let write_len = bytes.len().min(available);
238 bytes_guard[offset..offset + write_len].copy_from_slice(&bytes[..write_len]);
239 return;
240 }
241 let Some(offset) = byte_offset(index, stride) else {
242 record_oob_store();
243 return;
244 };
245 if stride == 0 || offset + stride > bytes_guard.len() {
246 record_oob_store();
247 return;
248 }
249 write_element(
250 buffer.element.clone(),
251 &mut bytes_guard[offset..offset + stride],
252 value,
253 );
254}
255
256pub(crate) fn atomic_load(buffer: &Buffer, index: u32) -> Option<u32> {
257 let bytes_guard = buffer.read_bytes();
258 let stride = buffer.element.min_bytes().max(4);
259 let Some(offset) = byte_offset(index, stride) else {
260 record_oob_atomic();
261 return None;
262 };
263 if offset + 4 > bytes_guard.len() {
264 record_oob_atomic();
265 None
266 } else {
267 Some(read_u32(&bytes_guard[offset..offset + 4]))
268 }
269}
270
271pub(crate) fn atomic_store(buffer: &mut Buffer, index: u32, value: u32) {
272 let mut bytes_guard = buffer.write_bytes();
273 let stride = buffer.element.min_bytes().max(4);
274 let Some(offset) = byte_offset(index, stride) else {
275 record_oob_atomic();
276 return;
277 };
278 if offset + 4 <= bytes_guard.len() {
279 write_u32(&mut bytes_guard[offset..offset + 4], value);
280 } else {
281 record_oob_atomic();
282 }
283}
284
285fn byte_offset(index: u32, stride: usize) -> Option<usize> {
286 (index as usize).checked_mul(stride)
287}
288
289fn write_element(element: IrDataType, target: &mut [u8], value: &Value) {
290 match element {
291 IrDataType::U32 => {
292 value.write_bytes_width_into(target);
293 }
294 IrDataType::I32 => {
295 value.write_bytes_width_into(target);
296 }
297 IrDataType::Bool => {
298 value.write_bytes_width_into(target);
299 }
300 IrDataType::U64 => {
301 value.write_bytes_width_into(target);
302 }
303 IrDataType::F32 => {
304 let v = match value {
309 Value::Float(v) => *v as f32,
310 Value::U32(v) => f32::from_bits(*v),
311 _ => 0.0,
312 };
313 let v = crate::execution::typed_ops::canonical_f32(v);
314 target.copy_from_slice(&v.to_le_bytes());
315 }
316 IrDataType::Bytes | IrDataType::Vec2U32 | IrDataType::Vec4U32 => {
317 value.write_bytes_width_into(target);
318 }
319 _ => {
320 value.write_bytes_width_into(target);
321 }
322 }
323}
324
325fn read_element(ty: DataType, bytes: &[u8]) -> Result<Value, String> {
326 Value::from_element_bytes(ty, bytes)
327}
328
329fn read_u32(bytes: &[u8]) -> u32 {
330 u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
331}
332
333fn write_u32(bytes: &mut [u8], value: u32) {
334 bytes.copy_from_slice(&value.to_le_bytes());
335}
336
337fn ir_to_conform_type(ty: IrDataType) -> DataType {
338 match ty {
339 IrDataType::U32 => DataType::U32,
340 IrDataType::I32 => DataType::I32,
341 IrDataType::U64 => DataType::U64,
342 IrDataType::F32 => DataType::F32,
343 IrDataType::F64 => DataType::F64,
344 IrDataType::Vec2U32 => DataType::Vec2U32,
345 IrDataType::Vec4U32 => DataType::Vec4U32,
346 IrDataType::Bool => DataType::U32,
347 IrDataType::Bytes => DataType::Bytes,
348 other => other,
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 fn f32_bits(value: Value) -> u32 {
357 match value {
358 Value::Float(value) => (value as f32).to_bits(),
359 other => {
360 let bytes = other.to_bytes();
361 u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
362 }
363 }
364 }
365
366 #[test]
367 fn f32_load_canonicalizes_subnormal_and_nan_payloads() {
368 let positive_subnormal = Buffer::new(1u32.to_le_bytes().to_vec(), DataType::F32);
369 assert_eq!(f32_bits(load(&positive_subnormal, 0)), 0x0000_0000);
370
371 let negative_subnormal = Buffer::new(0x8000_0001u32.to_le_bytes().to_vec(), DataType::F32);
372 assert_eq!(f32_bits(load(&negative_subnormal, 0)), 0x8000_0000);
373
374 let payload_nan = Buffer::new(0x7fa0_0001u32.to_le_bytes().to_vec(), DataType::F32);
375 assert_eq!(f32_bits(load(&payload_nan, 0)), 0x7fc0_0000);
376 }
377
378 #[test]
379 fn f32_store_canonicalizes_subnormal_and_nan_payloads() {
380 let mut subnormal = Buffer::new(vec![0; 4], DataType::F32);
381 store(
382 &mut subnormal,
383 0,
384 &Value::Float(f64::from(f32::from_bits(0x8000_0001))),
385 );
386 assert_eq!(f32_bits(subnormal.to_value()), 0x8000_0000);
387
388 let mut payload_nan = Buffer::new(vec![0; 4], DataType::F32);
389 store(&mut payload_nan, 0, &Value::U32(0x7fa0_0001));
390 assert_eq!(f32_bits(payload_nan.to_value()), 0x7fc0_0000);
391 }
392
393 #[test]
394 fn oob_accesses_are_counted_and_in_bounds_are_not() {
395 reset_oob_report();
399 let buf = Buffer::new(vec![0u8; 8], DataType::U32); let _ = load(&buf, 0);
401 let _ = load(&buf, 1);
402 assert_eq!(oob_report().total(), 0, "in-bounds loads must not count");
403
404 let _ = load(&buf, 2); let _ = load(&buf, 99); let after_loads = oob_report();
407 assert_eq!(after_loads.oob_loads, 2, "two OOB loads counted");
408 assert_eq!(after_loads.oob_stores, 0);
409
410 let mut wbuf = Buffer::new(vec![0u8; 8], DataType::U32);
411 store(&mut wbuf, 1, &Value::U32(7)); store(&mut wbuf, 5, &Value::U32(9)); let after_store = oob_report();
414 assert_eq!(
415 after_store.oob_stores, 1,
416 "one OOB store counted, in-bounds not"
417 );
418
419 let mut abuf = Buffer::new(vec![0u8; 8], DataType::U32);
420 atomic_store(&mut abuf, 7, 3); assert_eq!(oob_report().oob_atomics, 1, "OOB atomic store counted");
422
423 reset_oob_report();
424 assert_eq!(oob_report().total(), 0, "reset clears the tally");
425 }
426
427 #[test]
428 fn poisoned_reference_buffer_lock_is_not_silently_recovered() {
429 let buffer = Buffer::new(vec![0u8; 8], DataType::U32);
434 let poisoner = buffer.clone();
435 let _ = std::thread::spawn(move || {
436 let _guard = poisoner.write_bytes();
437 panic!("poison reference buffer lock mid-store");
438 })
439 .join();
440
441 let panic = std::panic::catch_unwind(|| {
442 let _ = buffer.len();
443 })
444 .expect_err("poisoned reference Buffer lock must panic instead of recovering");
445 let message = panic
446 .downcast_ref::<String>()
447 .map(String::as_str)
448 .or_else(|| panic.downcast_ref::<&'static str>().copied())
449 .unwrap_or("<non-string panic>");
450 assert!(
451 message.contains("reference Buffer byte lock was poisoned"),
452 "panic must name the poisoned reference buffer lock, got: {message}"
453 );
454 }
455}