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 let test = sys::Rf_protect(sys::Rf_allocVector(SEXPTYPE::REALSXP, 1));
109 let sexp_addr = test.0 as usize;
110 let data_addr = sys::DATAPTR_RO(test) as usize;
111 SEXPREC_DATA_OFFSET.store(data_addr - sexp_addr, Ordering::Relaxed);
112 sys::Rf_unprotect(1);
113 }
114}
115
116/// Try to recover the source R SEXP from a data pointer.
117///
118/// Given a pointer that may point into an R vector's data area, this
119/// subtracts the known SEXPREC header size to get a candidate SEXP, then
120/// verifies it:
121/// 1. The SEXP type tag (bits 0-4 of sxpinfo) matches `expected_type`
122/// 2. `ALTREP(candidate)` is false (only non-ALTREP vectors have fixed-offset data)
123/// 3. `XLENGTH(candidate)` matches `expected_len` (safe for non-ALTREP)
124///
125/// Returns `None` if:
126/// - The offset hasn't been initialized yet
127/// - The pointer doesn't come from an R vector
128/// - The candidate SEXP has the wrong type or length
129/// - The candidate is an ALTREP vector (data not at fixed offset from SEXP)
130///
131/// # Why this is outside Rust's memory model (see #63)
132///
133/// This is a conservative-GC-style probe, analogous to Boehm GC scanning
134/// the heap without allocation provenance. We compute a speculative pointer
135/// via `wrapping_byte_sub` (well-defined pointer arithmetic) and read the
136/// first 4 bytes (sxpinfo bits) to check whether the address looks like the
137/// start of a SEXPREC. For pointers that did not come from an R SEXP, that
138/// read has no valid allocation provenance under Rust's Stacked / Tree
139/// Borrows model — it's defined behavior at the hardware level (the heap
140/// is contiguous mapped memory), but Miri correctly flags it as UB.
141///
142/// We guard the read with a 4096-byte address floor (below which the
143/// candidate would cross into unmapped memory), the ALTREP bit check
144/// (prevents calling dispatch fns on garbage), and the length check
145/// (filters random garbage with high probability). Callers that cannot
146/// tolerate a false positive must not rely on this path alone.
147///
148/// To keep Miri green, the whole recovery is a no-op under `#[cfg(miri)]`:
149/// we always return `None`, and callers fall back to the copy path. This
150/// is not a correctness change — the copy path is always a valid alternative.
151///
152/// # Safety
153///
154/// Must be called on R's main thread. The data pointer must be valid
155/// (i.e., it must point to readable memory for at least `expected_len`
156/// elements, which is guaranteed if it came from an Arrow buffer).
157pub unsafe fn try_recover_r_sexp(
158 data_ptr: *const u8,
159 expected_type: SEXPTYPE,
160 expected_len: usize,
161) -> Option<SEXP> {
162 // Speculative sxpinfo read has no provenance for Rust-allocated buffers
163 // (see `#63`). Under Miri, skip the probe entirely so the copy path is
164 // exercised and the analyzer stays clean. No functional regression.
165 #[cfg(miri)]
166 {
167 let _ = (data_ptr, expected_type, expected_len);
168 return None;
169 }
170
171 #[cfg_attr(miri, allow(unreachable_code))]
172 let offset = SEXPREC_DATA_OFFSET.load(Ordering::Relaxed);
173 if offset == 0 {
174 return None;
175 }
176
177 // Zero-length vectors can't be recovered (R uses sentinel pointer 0x1,
178 // and empty Arrow buffers use dangling pointers).
179 if expected_len == 0 {
180 return None;
181 }
182
183 let data_addr = data_ptr as usize;
184
185 // Reject pointers that would wraparound or are in invalid ranges.
186 // R's sentinel for empty vectors is 0x1; wrapping_byte_sub on small
187 // addresses produces huge values (top of address space) → segfault.
188 // The 4096 threshold also guards against speculative reads near page
189 // boundaries — for non-R pointers (e.g. Rust-allocated Arrow buffers),
190 // subtracting the offset could land before the start of mapped memory.
191 if data_addr < offset.saturating_add(4096) {
192 return None;
193 }
194
195 // Compute candidate SEXP by subtracting header size.
196 // wrapping_byte_sub is defined behavior for all pointer arithmetic.
197 let candidate_ptr = (data_ptr as *mut SEXPREC).wrapping_byte_sub(offset);
198
199 let candidate = SEXP(candidate_ptr);
200
201 // Quick check: type tag (bits 0-4 of sxpinfo, which is the first field).
202 // For Rust-allocated buffers this reads arbitrary heap memory, but
203 // wrapping_sub ensures the pointer arithmetic itself is defined.
204 // The read is a plain u32 load from mapped heap.
205 // No public R API reads TYPEOF without side effects on invalid pointers,
206 // so a raw sxpinfo read is unavoidable here.
207 let sxpinfo_bits = unsafe { *(candidate.0 as *const u32) };
208 let type_bits = sxpinfo_bits & 0x1f;
209 if type_bits != expected_type as u32 {
210 return None;
211 }
212
213 // Reject ALTREP via R's public API (SexpExt::is_altrep → ALTREP(x)).
214 // ALTREP vectors store data via indirection — can't recover them.
215 // This also gates the xlength() call below: for non-ALTREP, XLENGTH is
216 // just STDVEC_LENGTH (a struct field read, no dispatch, no error).
217 // For ALTREP, XLENGTH dispatches through the class vtable which would
218 // crash on a garbage pointer.
219 if candidate.is_altrep() {
220 return None;
221 }
222
223 // For non-ALTREP, xlength() → Rf_xlength → STDVEC_LENGTH — a direct
224 // struct field read with no dispatch and no "long vectors not supported"
225 // error. (LENGTH() wraps XLENGTH with an INT_MAX check that can
226 // R_BadLongVector; XLENGTH itself never errors for non-ALTREP vectors.)
227 if candidate.len() != expected_len {
228 return None;
229 }
230
231 // No DATAPTR_RO round-trip check needed: for non-ALTREP vectors,
232 // STDVEC_DATAPTR(x) == (char*)x + SEXPREC_DATA_OFFSET, and we
233 // constructed candidate = data_ptr - offset, so the round-trip
234 // is tautologically true. The type + ALTREP + length checks above
235 // are the actual discriminators.
236
237 Some(candidate)
238}