Skip to main content

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}