miniextendr_api/r_memory.rs
1//! Utilities for recovering R SEXPs from raw data pointers.
2//!
3//! R stores vector data at a fixed offset after the SEXPREC header. Given a
4//! pointer into that data region, we can subtract the header size to recover
5//! the SEXP — then verify it by reading raw memory fields (type tag, ALTREP
6//! bit, and vecsxp.length) without calling any R functions.
7//!
8//! This is used by:
9//! - Arrow integration: zero-copy IntoR when the buffer is genuinely R-backed
10//! (the value buffer must be unsliced and exactly sized — see #867)
11//!
12//! It is deliberately *not* used for `Cow<[T]>` IntoR: a bare `&[T]` carries no
13//! provenance metadata to prove it points at the start of an R vector, so a
14//! borrowed sub-slice would probe off into unrelated memory (the #880 hazard).
15//! That path always copies instead.
16//!
17//! # Initialization
18//!
19//! [`init_sexprec_data_offset`] must be called during package init (before any
20//! recovery attempts). It measures the offset on a real R vector, so it works
21//! across R versions and platforms.
22//!
23//! # R's VECTOR_SEXPREC layout
24//!
25//! ```text
26//! // From R's Defn.h:
27//! typedef struct VECTOR_SEXPREC {
28//! SEXPREC_HEADER; // sxpinfo(8) + attrib(8) + gengc_next(8) + gengc_prev(8)
29//! struct vecsxp_struct { // length(8) + truelength(8)
30//! R_xlen_t length;
31//! R_xlen_t truelength;
32//! } vecsxp;
33//! } VECTOR_SEXPREC;
34//!
35//! typedef union { VECTOR_SEXPREC s; double align; } SEXPREC_ALIGN;
36//! #define STDVEC_DATAPTR(x) ((void *)(((SEXPREC_ALIGN *)(x)) + 1))
37//! ```
38//!
39//! On 64-bit: `sizeof(VECTOR_SEXPREC)` = 48 bytes, `sizeof(SEXPREC_ALIGN)` = 48.
40//! Data starts at `sexp + 48`. All vector types (REALSXP, INTSXP, RAWSXP,
41//! STRSXP, VECSXP) use the same `VECTOR_SEXPREC` header.
42//!
43//! # Why not `#[repr(C)]` mirror struct?
44//!
45//! A Rust `#[repr(C)]` struct mirroring `VECTOR_SEXPREC` would give a
46//! compile-time `size_of` instead of runtime measurement. However:
47//! - R's layout can vary by version and compile options (32-bit, padding)
48//! - The runtime measurement is one allocation at init — negligible
49//! - A `repr(C)` mirror struct doesn't help with the real safety issue:
50//! reading from a speculative pointer. `addr_of!` computes field addresses
51//! without dereferencing, but we still need to `read()` the type tag — and
52//! that read is from potentially invalid memory for non-R pointers.
53//!
54//! The verification (type tag + ALTREP check + XLENGTH) prevents false
55//! positives. Only the type tag requires a raw sxpinfo read; ALTREP and
56//! XLENGTH use R's public C API.
57//!
58//! # Safety of speculative reads
59//!
60//! The candidate pointer is computed from pointer arithmetic on the input
61//! data_ptr. For Rust-owned buffers (not R-backed), this points into
62//! arbitrary heap memory. We must be careful about which R functions we
63//! call on it:
64//!
65//! - **`ALTREP(x)`** — safe: just reads `x->sxpinfo.alt` (a single bit).
66//! - **`XLENGTH(x)`** on non-ALTREP — safe: reads `STDVEC_LENGTH` (struct
67//! field, no dispatch, no error).
68//! - **`LENGTH(x)`** — UNSAFE: wraps XLENGTH with `> INT_MAX` check that
69//! calls `R_BadLongVector()` (throws R error on garbage with large length).
70//! - **`DATAPTR_RO(x)`** — UNSAFE on ALTREP: dispatches through class vtable
71//! (bogus function pointers on garbage). On non-ALTREP: `STDVEC_DATAPTR`
72//! which also checks for long vectors.
73//!
74//! The verification sequence is:
75//! 1. Raw sxpinfo type tag (bits 0-4) — no public TYPEOF that's safe on garbage
76//! 2. `ALTREP(candidate)` — gates step 3 (rejects ALTREP before XLENGTH dispatch)
77//! 3. `XLENGTH(candidate)` — safe for non-ALTREP (STDVEC_LENGTH, no errors)
78
79use std::sync::atomic::{AtomicUsize, Ordering};
80
81use crate::sys::{self};
82use crate::{SEXP, SEXPREC, SEXPTYPE, SexpExt};
83
84/// Offset in bytes from SEXP address to data pointer for standard (non-ALTREP) vectors.
85///
86/// `DATAPTR_RO(sexp) == (sexp as *const u8).add(SEXPREC_DATA_OFFSET)`
87///
88/// Zero means not yet initialized.
89static SEXPREC_DATA_OFFSET: AtomicUsize = AtomicUsize::new(0);
90
91/// Get the computed SEXPREC data offset.
92///
93/// Returns 0 if not yet initialized.
94#[inline]
95pub fn sexprec_data_offset() -> usize {
96 SEXPREC_DATA_OFFSET.load(Ordering::Relaxed)
97}
98
99/// Compute and store the SEXPREC data offset by measuring a real R vector.
100///
101/// Must be called from R's main thread during package init.
102///
103/// # Safety
104///
105/// Must be called on R's main thread with R initialized.
106pub unsafe fn init_sexprec_data_offset() {
107 unsafe {
108 // keep raw: bootstrap init probe. Runs before the crate's protect
109 // infrastructure is guaranteed set up; a one-shot balanced protect of a
110 // throwaway REALSXP is the clearest expression here.
111 let test = sys::Rf_protect(sys::Rf_allocVector(SEXPTYPE::REALSXP, 1));
112 let sexp_addr = test.0 as usize;
113 let data_addr = sys::DATAPTR_RO(test) as usize;
114 SEXPREC_DATA_OFFSET.store(data_addr - sexp_addr, Ordering::Relaxed);
115 sys::Rf_unprotect(1);
116 }
117}
118
119/// Try to recover the source R SEXP from a data pointer.
120///
121/// Given a pointer that may point into an R vector's data area, this
122/// subtracts the known SEXPREC header size to get a candidate SEXP, then
123/// verifies it:
124/// 1. The SEXP type tag (bits 0-4 of sxpinfo) matches `expected_type`
125/// 2. `ALTREP(candidate)` is false (only non-ALTREP vectors have fixed-offset data)
126/// 3. `XLENGTH(candidate)` matches `expected_len` (safe for non-ALTREP)
127///
128/// Returns `None` if:
129/// - The offset hasn't been initialized yet
130/// - The pointer doesn't come from an R vector
131/// - The candidate SEXP has the wrong type or length
132/// - The candidate is an ALTREP vector (data not at fixed offset from SEXP)
133///
134/// # Why this is outside Rust's memory model (see #63)
135///
136/// This is a conservative-GC-style probe, analogous to Boehm GC scanning
137/// the heap without allocation provenance. We compute a speculative pointer
138/// via `wrapping_byte_sub` (well-defined pointer arithmetic) and read the
139/// first 4 bytes (sxpinfo bits) to check whether the address looks like the
140/// start of a SEXPREC. For pointers that did not come from an R SEXP, that
141/// read has no valid allocation provenance under Rust's Stacked / Tree
142/// Borrows model — it's defined behavior at the hardware level (the heap
143/// is contiguous mapped memory), but Miri correctly flags it as UB.
144///
145/// We guard the read with a 4096-byte address floor (below which the
146/// candidate would cross into unmapped memory), the ALTREP bit check
147/// (prevents calling dispatch fns on garbage), and the length check
148/// (filters random garbage with high probability). Callers that cannot
149/// tolerate a false positive must not rely on this path alone.
150///
151/// To keep Miri green, the whole recovery is a no-op under `#[cfg(miri)]`:
152/// we always return `None`, and callers fall back to the copy path. This
153/// is not a correctness change — the copy path is always a valid alternative.
154///
155/// # Safety
156///
157/// Must be called on R's main thread. The data pointer must be valid
158/// (i.e., it must point to readable memory for at least `expected_len`
159/// elements, which is guaranteed if it came from an Arrow buffer).
160pub unsafe fn try_recover_r_sexp(
161 data_ptr: *const u8,
162 expected_type: SEXPTYPE,
163 expected_len: usize,
164) -> Option<SEXP> {
165 // Speculative sxpinfo read has no provenance for Rust-allocated buffers
166 // (see `#63`). Under Miri, skip the probe entirely so the copy path is
167 // exercised and the analyzer stays clean. No functional regression.
168 #[cfg(miri)]
169 {
170 let _ = (data_ptr, expected_type, expected_len);
171 return None;
172 }
173
174 #[cfg_attr(miri, allow(unreachable_code))]
175 let offset = SEXPREC_DATA_OFFSET.load(Ordering::Relaxed);
176 if offset == 0 {
177 return None;
178 }
179
180 // Zero-length vectors can't be recovered (R uses sentinel pointer 0x1,
181 // and empty Arrow buffers use dangling pointers).
182 if expected_len == 0 {
183 return None;
184 }
185
186 let data_addr = data_ptr as usize;
187
188 // Reject pointers that would wraparound or are in invalid ranges.
189 // R's sentinel for empty vectors is 0x1; wrapping_byte_sub on small
190 // addresses produces huge values (top of address space) → segfault.
191 // The 4096 threshold also guards against speculative reads near page
192 // boundaries — for non-R pointers (e.g. Rust-allocated Arrow buffers),
193 // subtracting the offset could land before the start of mapped memory.
194 if data_addr < offset.saturating_add(4096) {
195 return None;
196 }
197
198 // Compute candidate SEXP by subtracting header size.
199 // wrapping_byte_sub is defined behavior for all pointer arithmetic.
200 let candidate_ptr = (data_ptr as *mut SEXPREC).wrapping_byte_sub(offset);
201
202 let candidate = SEXP(candidate_ptr);
203
204 // Quick check: type tag (bits 0-4 of sxpinfo, which is the first field).
205 // For Rust-allocated buffers this reads arbitrary heap memory, but
206 // wrapping_sub ensures the pointer arithmetic itself is defined.
207 // The read is a plain u32 load from mapped heap.
208 // No public R API reads TYPEOF without side effects on invalid pointers,
209 // so a raw sxpinfo read is unavoidable here.
210 let sxpinfo_bits = unsafe { *(candidate.0 as *const u32) };
211 let type_bits = sxpinfo_bits & 0x1f;
212 if type_bits != expected_type as u32 {
213 return None;
214 }
215
216 // Reject ALTREP via R's public API (SexpExt::is_altrep → ALTREP(x)).
217 // ALTREP vectors store data via indirection — can't recover them.
218 // This also gates the xlength() call below: for non-ALTREP, XLENGTH is
219 // just STDVEC_LENGTH (a struct field read, no dispatch, no error).
220 // For ALTREP, XLENGTH dispatches through the class vtable which would
221 // crash on a garbage pointer.
222 if candidate.is_altrep() {
223 return None;
224 }
225
226 // For non-ALTREP, xlength() → Rf_xlength → STDVEC_LENGTH — a direct
227 // struct field read with no dispatch and no "long vectors not supported"
228 // error. (LENGTH() wraps XLENGTH with an INT_MAX check that can
229 // R_BadLongVector; XLENGTH itself never errors for non-ALTREP vectors.)
230 if candidate.len() != expected_len {
231 return None;
232 }
233
234 // No DATAPTR_RO round-trip check needed: for non-ALTREP vectors,
235 // STDVEC_DATAPTR(x) == (char*)x + SEXPREC_DATA_OFFSET, and we
236 // constructed candidate = data_ptr - offset, so the round-trip
237 // is tautologically true. The type + ALTREP + length checks above
238 // are the actual discriminators.
239
240 Some(candidate)
241}
242
243// Miri-only regression for the `#[cfg(miri)]` gate in `try_recover_r_sexp` (#202).
244//
245// The whole module is gated `all(test, miri)`: the exercise is inherently a Miri
246// exercise, and running the speculative `sxpinfo` read under a normal `cargo test`
247// would read arbitrary heap bytes adjacent to a Rust allocation (unsound; the very
248// reason the gate exists). Compiled out otherwise, so it neither runs the unsound
249// read nor leaves an unused `use super::*` under the standard toolchain.
250#[cfg(all(test, miri))]
251mod miri_tests {
252 use super::*;
253
254 /// Round-trips `try_recover_r_sexp` on a Rust-allocated buffer under Miri.
255 ///
256 /// A non-zero offset is forced so the function reaches the speculative
257 /// `sxpinfo` read rather than bailing at the `offset == 0` guard. With the
258 /// `#[cfg(miri)]` no-op branch in place the call returns `None` immediately
259 /// and Miri stays clean. If that gate is removed, Miri flags the
260 /// `wrapping_byte_sub` read (a load outside the buffer's provenance) as UB
261 /// and this test fails — which is exactly what keeps the gate honest.
262 #[test]
263 fn recover_from_rust_buffer_returns_none() {
264 // Force the speculative-read path (skip the `offset == 0` early return).
265 // Any plausible SEXPREC header size works; 56 B matches a 64-bit header.
266 SEXPREC_DATA_OFFSET.store(56, Ordering::Relaxed);
267
268 // A Rust-owned buffer; a heap address is far above the 4096-byte floor.
269 let buf = vec![0u8; 1024];
270 let recovered = unsafe { try_recover_r_sexp(buf.as_ptr(), SEXPTYPE::INTSXP, buf.len()) };
271 assert!(
272 recovered.is_none(),
273 "try_recover_r_sexp must return None for a non-R (Rust) buffer under Miri"
274 );
275
276 // Don't leak the bogus offset into any other test in this crate.
277 SEXPREC_DATA_OFFSET.store(0, Ordering::Relaxed);
278 }
279}