rstsr (fix): unsafe-soundness audit follow-up — pointer provenance, broadcast-write rejection, uninitialized-allocation contract - #105
Merged
Conversation
…review Follow-up of the T1 unsafe-soundness audit (base acfa93e). BUG 1-4 were already fixed by RESTGroup#103/RESTGroup#104; this commit ports the remaining comment-only content and applies two API-level unsafe downgrades agreed with the maintainer. Comment-only (from the audit's safety-comments.patch + fixes.patch): - SAFETY annotations across 61 files (rstsr-common, rstsr-core, rstsr-native-impl, rstsr-dtype-traits): layout machinery, storage, operators, device kernels, allocation. Iterator files follow RESTGroup#103's view-only redesign comments where the audit text was superseded. Unsafe downgrades (pure integer arithmetic, cannot cause UB; worst case is a meaningless value or a panic): - `DimShapeAPI::unravel_index_f`/`unravel_index_c` (rstsr-common/src/layout/shape.rs): `unsafe fn` -> `fn`; `# Safety` docs replaced by unchecked-behavior notes and `# Panics` sections. - `Layout::index_uncheck` (rstsr-common/src/layout/layoutbase.rs): same treatment; doc now states the offset may be negative/meaningless. - Upstream call sites updated: ~30 now-redundant `unsafe` blocks and their stale justifications removed (layout iterators, naive matmul kernels, argmin/argmax reductions, op_tri, faer symmetrize, rstsr-sci-traits distance metrics). `set_offset` call sites keep their `unsafe` blocks. Docs: - `DataForceMutAPI::force_mut` and `TensorAny::force_mut` `# Safety` sections reworded: state the actual caller obligation (unique access while the returned mutable view is alive) instead of a generic "highly unsafe" note. Verification: `cargo check --workspace` 0 warnings; rstsr-core 116+302+2+184 (+184 doc) tests, rstsr-common 36 (+4 doc), rstsr-native-impl 4, rstsr-sci-traits 2 all pass. Pre-existing on the base commit, unrelated: rstsr-sci-traits doctest E0659; rstsr-native-impl `rayon` feature E0425. Most or all contents generated by AI (model glm-5.3-flash, via Claude Code agent), from the T1 audit patch and subsequent review. Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: glm-5.3-flash <service@zhipuai.cn>
…reakage Two pre-existing failures on the base commit (present before the soundness-audit work): - rstsr-sci-traits doctests aborted with E0659 (`rstsr_sci_traits` ambiguous): rustdoc's doctest compilation passes the crate to itself via `--extern`, and the glob-imported self-alias (`pub(crate) use crate as rstsr_sci_traits` in prelude_dev) conflicted with that outer name in files using `rstsr_sci_traits::` paths. The alias exists so that files meant to be symlinked across crates (e.g. auto_impl_rayon.rs, cf. the rstsr-openblas device crates) can use absolute crate-qualified paths unchanged. It is now an explicit `extern crate self as rstsr_sci_traits;` in lib.rs, which shadows the injected extern instead of conflicting; the portable files and their paths are untouched. - rstsr-native-impl --features rayon failed with 84 errors: the feature did not enable `rstsr-common/rayon` (par-iteration dispatch helpers and IntoParallelIterator for IterLayout), and prelude_dev did not re-export `rayon::ThreadPool`. Both now mirror rstsr-core's setup. Most or all contents generated by AI (model glm-5.3-flash, via Claude Code agent). Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: glm-5.3-flash <service@zhipuai.cn>
…nsafe-audit 4.1) Replace the write-through `c.as_ptr() as *mut` pattern (shared reborrow of the `&mut` slice, read-only provenance - UB under Stacked Borrows, Miri-flagged) at all 36 sites in rstsr-native-impl/src/cpu_rayon, plus the same pattern in rstsr-sci-traits/src/distance/native_impl.rs (cdist_rayon / cdist_weighted_rayon, 4 sites, beyond the audit's original scope). - `AtomicPtr::new(x.as_mut_ptr())` hoisted before the parallel region; `load(Ordering::Relaxed)` at the old derivation point. Relaxed suffices: publication to tasks happens via rayon spawn, and the pointer is never reassigned through the atomic. - Inner `into_par_iter` closures capture `&AtomicPtr` and load inside (raw pointers are !Send/!Sync even behind shared references). - Per-site SAFETY comments rewritten from the NOTE-as_ptr form to the established op_tri.rs AtomicPtr form. Import consolidation: `AtomicPtr`/`Ordering` and `rayon::prelude::*` join rstsr-native-impl's feature-gated prelude_dev, removing per-file boilerplate from all cpu_rayon kernels. A-B benchmark (rstsr-improve-trajectory 2026-09-15-atomicptr-hoist-ab, 6 interleaved rounds, before = aa24643): geometric mean 0.968 - no regression on any affected kernel; cdist family -24%, in-place blocked-2D -5%, naive matmul -2%, others within noise. Tests: rstsr-core rayon 116+302+2+184, rstsr-sci-traits, and workspace `cargo check --all-targets` all green. Most or all contents generated by AI (model glm-5.3-flash). Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: glm-5.3-flash <service@zhipuai.cn>
…check_strides zero-alloc rewrite - Capability model: a TensorMut with a stride-0 (broadcasted) layout stays constructible and readable; every write path now rejects `layout.is_broadcasted()`, the same predicate `assign`/`fill` already enforced. Gated: the add_assign family (tensor and scalar impls), mapi_f/mapi_fnmut_f, the matmul output driver, iter_mut/indexed_iter_mut constructors, and axes_iter_mut/indexed_axes_iter_mut (guard on the iterated axes only; yielded items keep non-iterated broadcast axes as inert views, caught by the item-level gates on write). `index_mut` needs no gate (single-element `&mut` access is borrow-checked). - Unary in-place ops on owned broadcasted tensors fall back to producing a fresh output instead of erroring, matching the binary-op reuse policy. - check_strides: insertion sort into an 8-pair stack buffer (heap fallback for higher dimensionality), no allocation in the common case; cumulative span arithmetic is checked and raises InvalidLayout on overflow instead of wrapping in release mode. Semantics unchanged (shape-1 axes are skipped before the stride loop). - Regression tests: broadcast write rejection in each touched file; layout tests for zero-stride handling, >8-axis heap fallback, and span overflow. Informal timing of Layout::new (3-D): 33.5 -> 8.0 ns/call. Most or all contents generated by AI (model glm-5.3-flash). Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: glm-5.3-flash <service@zhipuai.cn>
…-audit 4.3) The process-wide default of `TensorIterOrder` was a `static mut` read by the safe `get_default()` on effectively every tensor operation (creation, binary arithmetic, axes iteration). Concurrent `change_default()` would have been a data race (UB) reachable through safe readers. - replace the `static mut` with `AtomicU8` + `Relaxed` ordering (an independent policy byte that orders no other memory); - `change_default` is now a safe fn; - drop the `impl_changeable_default!` macro (single instantiation) in favor of direct impls; - `TensorIterOrder` gains `#[repr(u8)]`; discriminant round-trip via a guarded match whose fallback arm only guards memory corruption; - round-trip regression test (restores the process-global default). `core::sync::atomic` keeps `#![cfg_attr(not(test), no_std)]` intact. Note: `unsafe fn` -> `fn` on the public trait is breaking for downstream implementors of `ChangeableDefault`; there are no in-tree or known ones. Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: glm-5.3-flash <service@zhipuai.cn>
… allocation re-home The owned-Mat conversion used mem::forget + Vec::from_raw_parts to re-home faer's allocation into an owning Vec. Verified against the faer 0.22.6 source, the dealloc layout mismatched on every numeric conversion, not just hypothetically: faer over-aligns power-of-two-sized, drop-free element types to max(align, 64) and pads row capacity to the alignment multiple, while the Vec deallocates with align_of::<T> and the logical length (e.g. a 5x1 Mat<f64>: alloc 64 B / align 64 vs dealloc 40 B / align 8). Mismatched dealloc layout is UB by the GlobalAlloc contract, benign only under allocators that ignore the layout (glibc). into_rstsr now copies the logical elements column-wise via Mat::col_as_slice (safe code; new T: Clone bound) into a fresh contiguous column-major buffer (stride [1, nrows], offset 0) and lets faer drop its own allocation. Downstream eigh/cholesky/pinv call sites are unaffected; their following into_contig becomes a no-op view. Docs: the impl docstring states the copy behavior, the reason, and the zero-copy alternative mat.as_ref().into_rstsr() (non-owning TensorView); the MatRef/ColRef/MatMut impls (ManuallyDrop, never freed, already sound) got one-line ownership docs; the IntoRSTSR trait doc notes that ownership semantics are impl-specific. IntoRSTSR is now exported via prelude rstsr_traits: the facade rstsr::prelude re-exports only rstsr_traits/structs/funcs/macros (not prelude_dev), so facade users previously could not call into_rstsr. Tests: test_mat_owned_into_rstsr (content, contiguous layout, padded 5x1 capacity case, zero-copy route) and a doctest. cargo test -p rstsr-core (lib 123, doc 185) and cargo check --workspace green. Most or all contents generated by AI (model glm-5.3-flash). Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: glm-5.3-flash <service@zhipuai.cn>
The duplicate-axes check `for i in 0..axes_check.len() - 1` underflowed `0usize - 1` for an empty axes list, panicking (obscurely in release) instead of iterating. Replaced with an adjacent-pairs `windows(2)` scan at all four constructor sites (axes_iter / axes_iter_mut / indexed_* variants). Semantics come from the existing machinery, no new code path: an empty selection gives a 0-d `layout_axes` (IterLayout yields exactly one element) and the full `layout_inner`, so `axes_iter(())` / `axes_iter(vec![])` now yield a single whole-tensor view — the product over an empty axes set is 1, matching NumPy `ndindex()` convention. `()` is a reachable spelling via `From<()> for AxesIndex`. Regression tests: test_axes_iter_empty_axes (both spellings, indexed pair, duplicate axes still rejected) and test_axes_iter_mut_empty_axes; verified under row_major (default+rayon) and col_major+rayon feature sets. Pre-existing col_major `*_broadcast_err` lib-test failures are unrelated (fail on clean tree too). Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: glm-5.3-flash <service@zhipuai.cn>
Make the uninitialized_vec family's acceptance precise and migrate the last unsafe internal users onto sound paths: - rstsr-common: new `alloc_vec_contract.md`, rustdoc-included on all three allocation functions via `#[doc = include_str!]`. States the hazard taxonomy by element type, the write-vs-assignment rule (assignment drops the old slot value; `write` does not), the three blessed instantiations (uninit_impl+assume_init_impl / statically-POD FFI buffers / unsafe `empty`), and rules for new code. - rstsr-core: `DeviceMatMulAPI::matmul_uninit` — write-only `c = alpha * (a @ b)` into MaybeUninit storage, never reads `c`; new required trait method, implemented by all seven in-repo devices (BLAS crates and faer reinterpret the storage to the POD dtype after TypeId dispatch and pass beta = 0 under the BLAS non-read convention; other dtypes run dedicated write-only naive kernels in rstsr-native-impl). The allocating wrapper `op_refa_refb_matmul` now runs uninit_impl -> matmul_uninit -> assume_init_impl instead of `empty` + beta = 0. - All naive matmul kernels gained `beta.is_zero()` guards: the beta-scaling path now honors the BLAS non-read convention (previously a non-finite value in `c` could propagate through `0 * c` on the naive fallbacks). CPU-serial kernels and the DeviceCpuSerial impl bind `TC: Clone + Zero`. - diag/concatenate migrated from `empty` + `assign` to `uninit_impl` + `assign_uninit` + `assume_init_impl` with no trait-bound changes: `assign` writes with drop-of-old semantics, so over `empty` storage it dropped uninitialized memory as `T` on every call for allocatable types. `concatenate` sizes storage via `bounds_index()`. - rstsr-sci-traits: distance kernels allocate `Vec<MaybeUninit<M::Out>>` with write-only fills and one localized re-view (was plain-`T` uninitialized_vec with unconstrained `MetricDistAPI::Out`). - matmul_from/matmul_with_output docs note the beta = 0 non-read convention. Tests: `rstsr-core/tests/allocatable_dtype.rs` (BigInt through the safe API surface with exact values, plus drop-counting guards asserting created == dropped for element-wise ops, matmul, diag, concatenate) and in-crate cdist tests in rstsr-sci-traits (exact euclidean values, both orders; non-POD Out guard, serial and rayon). All green: cargo check --workspace; rstsr-core 125+10+302+2+185 (also with --features faer); rstsr-common 40+4; rstsr-native-impl serial and rayon features; rstsr-sci-traits 5; rstsr-core doctests 185. Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: glm-5.3-flash <service@zhipuai.cn>
…e-provenance task pointers in batched matmul - Output-write entry points that lacked the broadcast-layout gate now reject stride-0 output layouts, completing the capability model (holding a broadcast TensorMut stays legal; every write path rejects it): the *_with_output family (op_mutc_refa_refb), the op_with_func drivers (op_mutc_refa_refb_func / op_muta_refb_func / op_muta_func), and vecdot_from_f. A broadcast output under rayon previously handed overlapping offsets to different threads (data race); the serial path silently aliased writes. - Batched broadcast gemm, parallel-outer branch (device_faer and the five BLAS device crates): the per-task slice handle now derives from a hoisted AtomicPtr of c.as_mut_ptr() (unique provenance) instead of c.as_ptr() cast to *mut (a shared reborrow; writing through it is provenance UB, and each task held an overlapping full-length &mut). The incorrect SAFETY comment in device_faer is corrected. - syrk lower-triangle write-back (five BLAS device crates): write pointers now derive from an AtomicPtr hoist of as_mut_ptr() instead of as_ptr().add(..) as *mut. - Tests: broadcast rejection next to each gate; test_matmul_rule7_broadcast_parallel_outer (DeviceFaer, 64 batches, crosses the parallel-outer threshold; matmul and matmul_from with beta != 0). Most or all contents generated by AI (model glm-5.3-flash). Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: glm-5.3-flash <service@zhipuai.cn>
- rustfmt (`cargo fmt --all -- --check`): reflow lines and SAFETY comments in the rayon reduction kernels (rstsr-native-impl), the AtomicPtr hoist blocks of the five BLAS device crates' batched matmul and the openblas build script, rstsr-sci-traits distance kernels and tests, rstsr-common (rearrangement, tensordot_to_einsum), and the allocatable-dtype test suite; also the comment rewraps in the rstsr-core test modules touched by this branch. - clippy (`--all-targets --all-features -- -D warnings`): drop an unused test-module re-import of `crate::prelude_dev::*` (the parent module already imports it, so `use super::*` covers it) and remove an unnecessary `unsafe` block in the op_with_func broadcast-rejection test (`MaybeUninit::write` is a safe method); the closure keeps a block body since `write` returns `&mut T`. Most or all contents generated by AI (model glm-5.3-flash). Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: glm-5.3-flash <service@zhipuai.cn>
…(col_major CI) The broadcast-write rejection tests added in 1bc2c39 read back the untouched tensor through `view().iter()`, which follows the device default order. The test layout `Layout::new([2, 3], [0, 1], 0)` is f-contiguous, so under the col_major feature set the read-back visits elements first-axis-fastest and the row-major expectation vectors mismatched — 7 lib-test failures in the unittests-col-major CI job. The rejection gates themselves were already correct under both feature sets (the `is_err()` assertions pass). Following the established test convention, pin the device to row-major (`device.set_default_order(RowMajor)`) at the start of each of these tests; read-backs stay plain `.iter()` and assert the same element sequence regardless of the crate's default-order feature. Verified: rstsr-core lib tests green under col_major (118 passed) and default (129 passed) feature sets. Most or all contents generated by AI (model glm-5.3-flash). Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: glm-5.3-flash <service@zhipuai.cn>
Re-run `cargo fmt --all` with the updated stable rustfmt; comment rewrap preferences changed in the 1.100+ formatter, so a few comment blocks formatted with an older nightly toolchain (which additionally honors the nightly-only wrap_comments option) no longer matched the CI check. Most or all contents generated by AI (model glm-5.3-flash). Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: glm-5.3-flash <service@zhipuai.cn>
ajz34
force-pushed
the
260915-unsafe-soundness-3
branch
from
September 17, 2026 13:27
ed235ae to
6ca47ac
Compare
Re-run `cargo fmt --all` with rustfmt 1.100 (rustc 1.100.0-nightly, 2026-09-16), matching the CI runner's post-1.100 formatter: comment wrapping (rustfmt.toml wrap_comments/comment_width/overflow_delimited_expr) now takes effect on stable-derived toolchains, so blocks formatted by older rustfmt (1.9.0 stable / pre-1.100 nightly) diverge again — e.g. the openblas build script comment rewrap, overflow_delimited_expr-style match/array literals in rstsr-blas-traits util and rstsr-common layout iterator tests, and comment reflows in native-impl kernels. Most or all contents generated by AI (model glm-5.3-flash). Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: glm-5.3-flash <service@zhipuai.cn>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Behavior change
(in-place assign family,
mapifamily, the matmul*_with_outputdrivers,op_with_funcdrivers,vecdot_from_f, and theiter_mut/axes_iter_mutconstructors). Constructing and reading a broadcast
TensorMutstays legal.Previously a broadcast output under rayon handed overlapping offsets to different
threads (a data race); the serial path silently aliased writes. Unary in-place ops
on owned broadcasted tensors fall back to producing a fresh output, matching the
binary-op reuse policy.
Enhancement
uninitialized_vecfamily in rstsr-common now has a written contract(
alloc_vec_contract.md, rustdoc-included on all three allocation functions):hazard taxonomy by element type, the write-vs-assignment rule, and the blessed
instantiations for new code.
Layout::check_stridesno longer allocates in the common case (8-pair stackbuffer, heap fallback for higher dimensionality) and raises
InvalidLayoutonspan overflow instead of wrapping in release mode. Semantics unchanged;
Layout::newon a 3-D layout informally measured 33.5 -> 8.0 ns/call.API breaking changes (user should not feel that)
DeviceMatMulAPIgains the required methodmatmul_uninit: write-onlyc = alpha * (a @ b)intoMaybeUninitstorage, never readsc; implemented byall seven in-repo devices. The allocating wrapper now runs
uninit_impl->matmul_uninit->assume_init_implinstead ofempty+beta = 0.DimShapeAPI::unravel_index_f/unravel_index_candLayout::index_uncheckareno longer
unsafe(pure integer arithmetic; worst case a meaningless value or apanic).
ChangeableDefault::change_defaultis now a safe fn (defaults backed byAtomicU8); breaking only for external implementors of the trait — none known.Matinto_rstsrgained aT: Clonebound (it now copies; see BugFix).
Bug Fix
c.as_ptr() as *mutpattern is replaced by a hoistedAtomicPtr::new(as_mut_ptr())at all 36 sites of the rstsr-native-impl rayon kernels and in the cdist rayon
kernels of rstsr-sci-traits; the batched-broadcast gemm parallel-outer branch
(device_faer + the five BLAS device crates) and the syrk write-back now derive
per-task pointers from
as_mut_ptr()(unique provenance) — previously each taskheld an overlapping full-length
&mut.Matinto_rstsr:mem::forgetVec::from_raw_partsmismatched the dealloc layout on every numeric conversion(faer over-aligns drop-free element types to at least 64 B and pads row capacity).
It now copies column-wise; the zero-copy route is
mat.as_ref().into_rstsr()(a
TensorView).TensorIterOrderdefault: it was astatic mutread by the safeget_default()on effectively every tensoroperation.
axes_iter/axes_iter_mut(and indexed variants) panicking on an empty axeslist (
0usize - 1underflow);axes_iter(())now yields a single whole-tensorview, matching the NumPy
ndindex()convention.beta = 0non-read convention: anon-finite value in
ccould propagate through0 * c.diag/concatenatedropping uninitialized memory asTfor allocatableelement types (
assignoveremptystorage); both now useuninit_impl+assign_uninit+assume_init_impl. The rstsr-sci-traits distance kernelslikewise fill
Vec<MaybeUninit<Out>>write-only.rayonfeature (now enablesrstsr-common/rayon); both already failed on thebase commit.
Context
acfa93e(working notes andbenchmark records: https://github.com/ajz34/rstsr-improve-trajectory). The bugs
found by the audit were fixed in rstsr-core (fix): view-only iteration API (iter* on views, iter_mut* on mut views) #103/rstsr (fix): three bugs from unsafe-soundness check #104; this branch completes the remaining
work: comment-only SAFETY annotations (61 files across rstsr-common, rstsr-core,
rstsr-native-impl, rstsr-dtype-traits), removal of unnecessary
unsafeblocks,and the audit's design-level flags (parallel-kernel pointer provenance, writes
through broadcast layouts,
static mutdefaults, the faer allocation re-home),plus the uninitialized-allocation contract.
rstsr-core/tests/allocatable_dtype.rs: BigInt through the safeAPI surface with exact values, plus drop-counting guards (created == dropped) for
elementwise ops, matmul, diag, concatenate; exact-value cdist tests in
rstsr-sci-traits (serial and rayon).
— no affected kernel regressed, cdist family -24%): recorded at
https://github.com/ajz34/rstsr-improve-trajectory.
IntoRSTSRis now exported via the preluderstsr_traits, so facade users cancall
into_rstsr(previously prelude_dev only).cargo check --workspaceclean; rstsr-core 125+10+302+2+185 (incl.doctests, also with
--features faer); rstsr-common, rstsr-native-impl (serial +rayon), rstsr-sci-traits all green.
🤖 Generated with Claude Code