Skip to content

Add: the Buffer/Tensor wire ABI and owner-side create_buffer - #1599

Merged
ChaoWao merged 1 commit into
hw-native-sys:mainfrom
YunjiQin:p1-b-abi-foundation
Aug 5, 2026
Merged

Add: the Buffer/Tensor wire ABI and owner-side create_buffer#1599
ChaoWao merged 1 commit into
hw-native-sys:mainfrom
YunjiQin:p1-b-abi-foundation

Conversation

@YunjiQin

@YunjiQin YunjiQin commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

The buffer ABI: how L3+ tasks name their data

At L3 and above a task argument is a raw pointer plus a child_memory bool, so the receiver has to
guess what it was handed. This lands the typed, self-describing replacement — a canonical identity, a
backend descriptor, and a strided view — which together let a consumer resolve a buffer exactly
across the L3→L2 and L4→L3 boundaries with no side table and no address rewriting.

User guide: docs/buffer-abi.md, including the
"Why Tensor and ChipTensor are two types" section with the two rejected unifications.

Nothing dispatches a Tensor yetTaskArgs still carries the device POD — so this is
behavior-neutral. It lands on its own because the byte layout is frozen once it ships, and it should
be read in its own context rather than inside the cutover that consumes it.

What lands here

src/common/task_interface/buffer.h 3 wire types + validate_buffer_descriptor / validate_tensor + tensor_extent_bytes / tensors_overlap, every offset and size pinned by static_assert
python/bindings/task_interface.cpp those 3 types and their 3 enums bound directly
python/simpler/buffer.py Buffer (owns a POSIX shm), the wrap_* constructors, ImportRegistry (map-once by canonical identity)
python/simpler/worker.py Worker.create_buffer, released through the retryable cleanup journal

Three types. Buffer is an owned backing with a lifecycle, which stays with the Worker that created
it. Tensor is the argument a user builds and submits: the buffer descriptor embedded whole, plus a
view, and no address — at submit time none exists, since a POSIX_SHM backing maps to a different VA
in every process and a DEVICE_MALLOC one is valid only on its owner chip. ChipTensor is the POD the
L2 runtime ABI reads, which must carry an address because the kernel dereferences it.

Naming — the model from #1676 / #1681

Worker domain:  Tensor      — address-free, cross-edge logical view
Chip domain:    ChipTensor  — materialized GM-address-bearing POD

Rebased onto #1681, which moved the device POD off the global Tensor name. That is what frees the
plain name for the L3+ type, so Buffer and Tensor are global in C++ and spelled identically in
Python — no namespace, no per-language alias, no second public meaning for Tensor.

TENSOR_STRIDE_BYTES and TENSOR_CHILD_MEMORY_OFFSET, which describe the device POD, become
CHIP_TENSOR_STRIDE_BYTES / CHIP_TENSOR_CHILD_MEMORY_OFFSET so they cannot be read as belonging to
the wire type exported beside them.

Bound, not mirrored — and no bytes cross the binding

CanonicalIdentity, BufferDescriptor and Tensor are the C++ structs bound directly rather than a
Python re-encoding of the same bytes: one layout definition instead of two, one validator instead of
a full one and a weaker one. Buffer and ImportRegistry stay Python — they own a SharedMemory
and a process-local mapping cache, neither of which is ABI.

The types expose their fields and not their encoding: there is no pack() / unpack(). Python
builds a Tensor and receives one already decoded, and the sole path from wire bytes to a Tensor
runs inside C++. Withholding the encoding is what keeps validate_tensor a gate rather than a
habit — a second way in is a second thing to remember to validate on.

Measured on the receive path, decoding a 4-tensor blob into objects: 37 µs → 0.44 µs.

No second transport

A Tensor rides the TaskArgs mailbox blob that write_blob / read_blob already implement in
task_args.h. The cutover swaps that blob's element from ChipTensor to Tensor and moves
TaskArgsView with it, leaving ChipTensor only in ChipStorageTaskArgs. A second blob codec
beside the existing one would be the same structure twice, so this PR ships the types and their gate
and leaves the wire where it is.

Cutover note. TaskArgsView::tensors(i) does not validate today, which is correct while the
element is a ChipTensor — an address plus a shape has no wire invariant a decoder could test. The
flip must add validate_tensor(t) there, because Tensor does have invariants and this PR
deliberately leaves no other way in.

Tensor is foundation-only until the wire flip

TaskArgs.add_tensor still takes a ChipTensor, so Tensor lives in simpler.buffer and is not
re-exported from simpler.task_interface. A public type whose own submit call rejects it is worse
than no public type; it joins that module in the cutover that makes TaskArgs carry it.

test_wire_tensor_stays_off_the_public_submit_surface pins the two facts to each other — whichever
moves first fails until the other follows.

A latent defect, found and fixed

ImportRegistry keyed on identity.pack(), and pack() emitted _pad, which defeats the
padding-insensitive equality and hashing the model rests on: two decodes of one backing landed in two
registry entries, so map-once silently did not hold. Caught with a failing repro first. The registry
now keys on the identity itself; with pack() gone the mistake is no longer expressible at all,
which is a stronger guarantee than the regression test it replaces.

Today the padding is zero end to end, so this is latent — it becomes real the moment the cutover puts
descriptors on a live mailbox.

What is deliberately NOT here

  • The wire flip. TaskArgs still carries the device POD, so buffer.tensor(...) has no consumer
    and the submit-time checks the doc describes are not reachable.
  • The other allocators (alloc_shared_tensor, alloc_child_tensor), device-memory ownership moving
    onto the Worker, comm-domain VMM_WINDOW buffers, and the example/scene-test migration.
  • ChipTaskArgs<ChipTensor> naming at the chip boundary (Support: define layered runtime naming #1676 point 4). Worth flagging for whoever
    picks it up: ChipTaskArgs is already taken by the device-side orchestration arg in
    src/{arch}/runtime/*/pto_types.h, while the host-side type is ChipStorageTaskArgs.

Verification

  • Build green — all four arch×runtime C++ trees plus the nanobind extension
  • pyut — 1131 passed, 6 skipped
  • cpput — 81/81 (no-hardware subset); test_buffer is 13 cases, including field-by-field
    decode rejection, a fixed-seed 4096-iteration arbitrary-bytes pass, and padding-insensitive
    identity keying
  • pre-commit green (clang-format / clang-tidy / cpplint / markdownlint / ruff / pyright)
  • a2a3sim and a5sim full scene suites rc=0 on the immediately preceding revision; re-running
    on this one (the delta since is binding surface, tests and docs only — no runtime path)
  • Hardware (onboard) — not run

The malformed-bytes cases live in tests/ut/cpp/types/test_buffer.cpp rather than Python, because
with no unpack() those states cannot be built from Python at all — which is the point.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 87a45cff-7e2d-42ec-a831-f675a462a7e7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces a typed BufferHandle and tensor-view ABI with canonical identities, backend descriptors, validation, blob serialization, Python materialization, worker-owned shared-memory allocation, bindings, documentation, and C++/Python tests.

Changes

BufferHandle ABI

Layer / File(s) Summary
ABI contracts and documentation
src/common/task_interface/*, docs/*, mkdocs.yml, tests/ut/cpp/*
Defines fixed-layout identities, descriptors, tensor views, backend tags, validation rules, blob codecs, shared datatype constants, documentation, and ABI-focused tests.
Python packing and materialization
python/simpler/buffer_handle.py, python/bindings/task_interface.cpp, tests/ut/py/test_buffer_handle.py
Adds Python wire-format types, handle constructors, shared-memory and pointer-backed materialization, blob helpers, bindings, and decode/runtime validation tests.
Worker allocation and teardown
python/simpler/worker.py
Adds Worker.create_buffer, per-worker identity tracking, owned-handle registration, and shared-memory cleanup during teardown.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Poem

A rabbit hops through shared memory bright,
Naming each buffer with identity right.
Strides curl softly, blobs cross the way,
Workers clean handles at close of day.
“Nibble the ABI—hooray!”

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the Buffer/Tensor wire ABI and the owner-side create_buffer API, which are the primary changes.
Description check ✅ Passed The description covers the same Buffer/Tensor ABI and create_buffer objectives, although several file names and implementation details do not match the summarized changes.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
tests/ut/cpp/types/test_buffer_handle.cpp (1)

71-82: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

EnumValuesAreFrozen doesn't pin BackendKind::FORK_COW.

Every other BackendKind value (0-4) is checked here but FORK_COW = 5 is missing, even though the test's stated purpose is to "pin the sizes, enum values, and the blob codec from the outside". Trivial to add for parity with the rest of this test.

✅ Proposed addition
     EXPECT_EQ(static_cast<uint8_t>(BackendKind::DEVICE_MALLOC), 4);
+    EXPECT_EQ(static_cast<uint8_t>(BackendKind::FORK_COW), 5);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/ut/cpp/types/test_buffer_handle.cpp` around lines 71 - 82, Add an
assertion in BufferHandleAbi.EnumValuesAreFrozen that verifies
BackendKind::FORK_COW converts to uint8_t value 5, preserving the existing
checks for BackendKind values 0–4.
src/common/task_interface/buffer_handle.h (1)

313-328: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

BufferRefBlobView::ref(i) has no bounds check on i.

ref(i) memcpy's sizeof(BufferRef) bytes at ref_bytes + i * sizeof(BufferRef) with no check that i < ref_count (or i >= 0). Current callers (bufferref_blob_descriptors/refs/scalars in task_interface.cpp) all loop i < view.ref_count, so this isn't exploitable today, but the function is documented as the single shared extraction point for three receive boundaries, so a future caller passing an unchecked index would read out of the validated region.

Optional: add an i < ref_count assert/throw inside ref() itself for defense-in-depth.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/common/task_interface/buffer_handle.h` around lines 313 - 328, Add bounds
validation to BufferRefBlobView::ref before computing the byte offset or calling
memcpy, rejecting indices below zero or greater than or equal to ref_count with
the established assertion or exception mechanism. Preserve the existing
BufferRef validation and extraction behavior for valid indices.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/buffer-handle-abi.md`:
- Around line 174-186: Add a VMM_WINDOW row to the Backends table, documenting
that it materializes as the device VA carved by allocate_domain and is used for
communication-domain window buffers. Keep it alongside DEVICE_MALLOC and
preserve the table’s wire-ABI completeness statement.

In `@python/simpler/buffer_handle.py`:
- Around line 239-242: Update the layout comment immediately above
_BUFFER_REF_TAIL to use the actual wire sizes: BufferRef 144 B and
BufferHandleDescriptor 88 B, while preserving the existing field description and
size assertion.

In `@src/common/task_interface/buffer_handle.h`:
- Around line 80-96: Update validate_buffer_ref to reject any BufferRef whose
backend_kind is FORK_COW unless access is exactly READ, preserving the existing
address_space/backend_kind validation for all other combinations. Add or extend
C++ validation tests to cover FORK_COW with WRITE and READWRITE access as
rejected, while retaining acceptance for FORK_COW with READ.

---

Nitpick comments:
In `@src/common/task_interface/buffer_handle.h`:
- Around line 313-328: Add bounds validation to BufferRefBlobView::ref before
computing the byte offset or calling memcpy, rejecting indices below zero or
greater than or equal to ref_count with the established assertion or exception
mechanism. Preserve the existing BufferRef validation and extraction behavior
for valid indices.

In `@tests/ut/cpp/types/test_buffer_handle.cpp`:
- Around line 71-82: Add an assertion in BufferHandleAbi.EnumValuesAreFrozen
that verifies BackendKind::FORK_COW converts to uint8_t value 5, preserving the
existing checks for BackendKind values 0–4.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d442b3d1-f5cd-43eb-b613-298f4efd8dd7

📥 Commits

Reviewing files that changed from the base of the PR and between 6bd6c3b and cb27557.

📒 Files selected for processing (12)
  • docs/README.md
  • docs/buffer-handle-abi.md
  • mkdocs.yml
  • python/bindings/task_interface.cpp
  • python/simpler/buffer_handle.py
  • python/simpler/worker.py
  • src/common/task_interface/buffer_handle.h
  • src/common/task_interface/data_type.h
  • src/common/task_interface/tensor.h
  • tests/ut/cpp/CMakeLists.txt
  • tests/ut/cpp/types/test_buffer_handle.cpp
  • tests/ut/py/test_buffer_handle.py
💤 Files with no reviewable changes (1)
  • src/common/task_interface/tensor.h

Comment thread docs/buffer-handle-abi.md Outdated
Comment thread python/simpler/buffer_handle.py Outdated
Comment thread src/common/task_interface/buffer_handle.h Outdated
@YunjiQin
YunjiQin force-pushed the p1-b-abi-foundation branch from b2e2bc9 to 57a89fd Compare July 30, 2026 12:39
@YunjiQin YunjiQin changed the title P1-B PR-1: the BufferHandle/Tensor wire ABI, its codec, and create_buffer Add: the BufferHandle/Tensor wire ABI, its codec, and create_buffer Jul 30, 2026
@YunjiQin

Copy link
Copy Markdown
Contributor Author

Went through the CodeRabbit findings — all five held up against the current code, none was a false positive. Fixed in 9fe7bb9.

The one that mattered: validate_buffer_ref is documented as the single gate every receive boundary runs, but the FORK_COW⇒READ rule only existed in the Python mirror's descriptor constructor. A FORK_COW descriptor arriving over the wire with a write grant passed C++ decode — and the failure is silent, not loud: the consumer's first write splits the page into a private copy and the owner observes nothing. Both mirrors reject it now, with a test covering WRITE and READWRITE plus the FORK_SHM case that stays legal.

  • BufferRefBlobView::ref(i) bounds check — agreed and added. Not reachable today (every caller loops to ref_count), but it is the documented extraction point for three consumers and nothing tells a fourth that the bound is the caller's job.
  • FORK_COW = 5 not pinned — correct, it was the one enumerator the frozen-value test skipped.
  • Stale BufferRef layout comment — correct: it still said 272 B / 216 B from before the ABI was slimmed. Actual is 144 / 88, which the assertion on the next line already enforced.
  • VMM_WINDOW missing from the backend table — correct, the table listed five of the six wire values.

On the docstring-coverage warning (32% vs 80%): added docstrings where they state a contract that is not evident from the signature — the two wire enums, BufferHandle.to_descriptor / close, and ImportRegistry.resolve / unregister / close (notably: close on the owner unlinks, close on the registry does not, which is the kind of thing worth writing down). I deliberately did not document the mechanical dunders (__len__, __post_init__, __getitem__) to reach the threshold — the repo's own gate has no docstring check, and one-line restatements of what the signature already says are what the project's comment rule steers away from.

Verified after the fixes: pyut 913 passed, cpput 67/67, a2a3sim scene tests 43 passed.

@YunjiQin
YunjiQin force-pushed the p1-b-abi-foundation branch from 47a6e42 to 202c8c7 Compare August 3, 2026 12:32
@YunjiQin YunjiQin changed the title Add: the BufferHandle/Tensor wire ABI, its codec, and create_buffer Add: the Buffer/Tensor wire ABI, its codec, and create_buffer Aug 3, 2026
@YunjiQin
YunjiQin force-pushed the p1-b-abi-foundation branch 2 times, most recently from 29abadc to 4b74067 Compare August 3, 2026 13:05
@ChaoWao

ChaoWao commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Naming direction from the repository-wide audit is now captured in #1676. The relevant rule is: software identifiers use the owning entity (Worker / Chip / Core), while numeric levels remain architecture prose only.

For this PR, the canonical model should be:

Worker domain:  Tensor      — address-free, cross-edge logical view
Chip domain:    ChipTensor  — materialized GM-address-bearing TMR/HBG tensor

The current head does not yet satisfy that model:

  1. C++ global ::Tensor is the Chip-owned TMR/HBG POD, but only its Python binding is renamed to ChipTensor. The C++ type should also become ChipTensor (a temporary using Tensor = ChipTensor compatibility alias is fine for external kernel sources). Do not introduce L2Tensor or LocalTensor.
  2. Python simpler.buffer.Tensor is a separate dataclass mirror of C++ simpler::Tensor. Public Python and C++ Tensor must be the same canonical type and semantics; prefer binding simpler::Tensor directly rather than maintaining a second public implementation.
  3. simpler.task_interface.Tensor is documented as the object users submit, but TaskArgs.add_tensor currently rejects it and accepts only ChipTensor | RemoteTensorRef. Either complete the TaskArgs<Tensor> wire cutover here, or keep the new type explicitly internal/foundation-only until that cutover. Do not expose a public Tensor whose advertised operation rejects it.
  4. The intended later argument naming is TaskArgs<Tensor> at the Worker layer and ChipTaskArgs<ChipTensor> at the Chip boundary (the current ChipStorageTaskArgs). Existing L0TaskArgs / L2TaskArgs naming can migrate separately to CoreTaskArgs / Chip-owned names; it need not expand this PR.

The important merge condition for #1599 is not a repository-wide rename. It is that the new ABI does not establish a second public meaning for Tensor, or a Python/C++ name mismatch that every later cutover must preserve.

@YunjiQin
YunjiQin force-pushed the p1-b-abi-foundation branch 3 times, most recently from a427a61 to 9aef900 Compare August 5, 2026 09:45
@YunjiQin YunjiQin changed the title Add: the Buffer/Tensor wire ABI, its codec, and create_buffer Add: the Buffer/Tensor wire ABI and owner-side create_buffer Aug 5, 2026
At L3 and above a task argument is a raw pointer plus a `child_memory` bool, so
the receiver has to guess what it was handed. This adds the typed,
self-describing replacement: a canonical identity, a backend descriptor, and a
strided view, which together let a consumer resolve a buffer exactly across the
L3→L2 and L4→L3 boundaries with no side table and no address rewriting.

Nothing dispatches a `Tensor` yet — `TaskArgs` still carries the device POD — so
this is behavior-neutral. It lands on its own because the byte layout is frozen
once it ships, and it should be read in its own context rather than inside the
cutover that consumes it.

Because nothing dispatches it, `Tensor` stays in `simpler.buffer` and is not
re-exported from `simpler.task_interface`. `TaskArgs.add_tensor` still takes a
`ChipTensor`, and a public type whose own submit call rejects it is worse than no
public type at all; it joins that module in the cutover that makes `TaskArgs`
carry it. A test pins the two facts to each other, so whichever moves first fails
until the other follows.

Three types. `Buffer` is an owned backing with a lifecycle, which stays with the
Worker that created it. `Tensor` is the argument a user builds and submits: the
buffer descriptor embedded whole, plus a view, and no address — at submit time
none exists, since a POSIX_SHM backing maps to a different VA in every process
and a DEVICE_MALLOC one is valid only on its owner chip. `ChipTensor` is the POD
the L2 runtime ABI reads, which must carry an address because the kernel
dereferences it.

There is no new transport. A `Tensor` rides the TaskArgs mailbox blob that
`write_blob` / `read_blob` already implement; the cutover swaps that blob's
element from `ChipTensor` to `Tensor` and moves `TaskArgsView` with it, leaving
`ChipTensor` only in `ChipStorageTaskArgs`. A second blob codec beside the
existing one would be the same structure twice, so this commit ships the types
and their gate and leaves the wire where it is.

The names follow codestyle rule 13: an L3+ domain object takes the unprefixed
name, the chip context takes the `Chip` prefix, and a public type is spelled the
same in C++ and Python. `Buffer` and `Tensor` are therefore global in both
languages — no namespace and no per-language alias — which hw-native-sys#1681 made possible by
moving the device POD off the global `Tensor` name. `TENSOR_STRIDE_BYTES` and
`TENSOR_CHILD_MEMORY_OFFSET`, which describe that POD, become
`CHIP_TENSOR_STRIDE_BYTES` / `CHIP_TENSOR_CHILD_MEMORY_OFFSET` so they cannot be
read as belonging to the wire type exported beside them.

Rule 13 also asks that one public type be one type, so `CanonicalIdentity`,
`BufferDescriptor` and `Tensor` are the C++ structs bound directly rather than a
Python re-encoding of the same bytes. One layout definition instead of two, one
validator instead of a full one and a weaker one, and no `struct.unpack` between
a receiver and its arguments. `Buffer` and `ImportRegistry` stay Python — they
own a `SharedMemory` and a process-local mapping cache, neither of which is ABI.

No bytes cross that binding in either direction: the types expose their fields and
not their encoding. Python builds a `Tensor` and receives one already decoded, and
the sole path from wire bytes to a `Tensor` is the mailbox blob's, inside C++.
Withholding the encoding is what keeps `validate_tensor` a gate — a second way in
is a second thing to remember to validate on. It also makes a class of mistake
unwritable rather than merely fixed: keying an import registry on packed bytes
splits one backing in two the moment their padding differs, and with no packed
bytes to reach for, that key cannot be written. The malformed-bytes cases are
therefore exercised where they can be built, in the C++ tests.

`CanonicalIdentity` is fixed-length with no length field, so hashing and
comparison cannot read past it whatever bytes arrive — a structural property
rather than one a validator has to enforce. Padding is excluded from equality,
from hashing, and from `pack()`, so two decodes of one backing can never key
differently; `ImportRegistry` keys on the identity itself for the same reason,
and map-once holds however the wire padding arrived. Generation 0 is reserved
for uninitialized and rejected.

`validate_tensor` is the single gate every trust boundary runs. It bounds
`body_len` and `ndims`, checks the address_space x backend matrix, requires
strides > 0 and a known dtype, rejects a view extending past its backing, and
requires FORK_COW to grant READ only: a copy-on-write page splits on the
consumer's first write into a private copy the owner never sees, so a write
grant there would be silently unobservable rather than an error. Its descriptor
half is `validate_buffer_descriptor`, split out because a descriptor also
arrives on its own, with no view attached.

`AddressSpace` and `MAX_TENSOR_DIMS` sit where their readers are: the rank bound
is shared by both tensor types and lives in data_type.h, while the address space
is a `BufferDescriptor` field alone and lives with it in buffer.h.

`Worker.create_buffer` allocates an owner-side `Buffer` over POSIX shm, and every
one it hands out is registered so teardown can unlink the backing. Release is a
retryable cleanup-journal entry alongside the host buffers: a `Buffer` whose
close fails keeps its registry entry, so a later `close()` re-drives it instead
of leaking the shm silently.

Also bounds-checks `get_element_size`, which indexed its table with a raw `u8`
that now arrives from the wire.
@ChaoWao
ChaoWao merged commit b535fa2 into hw-native-sys:main Aug 5, 2026
14 of 18 checks passed
ChaoWao added a commit that referenced this pull request Aug 5, 2026
…heck (#1703)

#1599 froze the byte layout and shipped `validate_tensor` as the receive-side
gate every later boundary is meant to run. Three things in it do not hold up,
and a frozen ABI is the wrong place to leave them. No field, offset, or enum
value changes: every static_assert in buffer.h is untouched.

`tensor_extent_bytes` summed `(shapes[i]-1)*strides[i]` into a uint64. Both
fields are u32, so a single product already reaches ~2^64 and the multiply by
the element size overflows on top of it: the extent wrapped to a small value and
`validate_tensor` accepted the view as in-bounds. One dimension is enough —
shapes=(2147483649,), strides=(2147483648,), FLOAT32 validated against a 4-byte
backing while addressing ~16 EiB — and the path is reachable from Python through
`buffer.tensor(...)`. The arithmetic now saturates, and an extent that lands on
the saturation sentinel is refused by name rather than compared against
`nbytes`, so a descriptor claiming an absurd `nbytes` cannot buy the view back.

`tensors_overlap` inherited the same wrap, where the consequence is worse than a
rejected argument: two fully overlapping views compare as disjoint, which at the
dependency layer is a missed edge rather than an error. Its end offsets saturate
too, and its test sets a nonzero `byte_offset` so the end-offset addition is
what overflows — with an origin of zero the case passes against the unfixed
code and guards nothing.

`BufferDescriptor::operator==` bounded a memcmp by an unvalidated `body_len`. It
is clamped to DESC_MAX_BYTES, so the one length field in the header cannot bound
a read past the array it indexes.

The arbitrary-bytes pass asserted magic / generation / body_len / ndims on its
survivors but not the footprint invariant, which is why 4096 random blobs never
caught the overflow; it asserts it now.

The L3+ child check in `create_buffer` counted chip and sub children only, so an
L4 whose children are local L3 Workers was refused. A next-level child is a
forked process that maps a POSIX_SHM backing by name exactly as a chip or sub
child does, so it can consume the buffer; `_next_level_shms` counts now, and the
error message names all three shapes. `create_buffer` had no test: the new ones
cover the gate in each child shape, the childless L3+ refusal, the L2 leaf that
needs no child at all, buffer id uniqueness and the single nonce within one
incarnation, and that a failing close is reported while its registry entry stays
for the cleanup journal to retry.

`wrap_fork_inherited` derived `backend_kind` from `access`, tagging FORK_SHM
whenever the grant was not READ. FORK_SHM and FORK_COW are opposite kernel write
semantics, not two spellings of one grant, and inferring either from the other
makes a read-only MAP_SHARED backing inexpressible: it is tagged FORK_COW, and
FORK_COW's READ-only rule then locks it there. The caller holds the mmap and is
the only party that knows which it is, so it states the tag; the default pair
stays the safe one, FORK_COW with READ. The function had no caller, so nothing
depends on the old signature.

Four pieces of text described machinery that does not exist.
`ImportRegistry.materialize` pointed a caller holding raw bytes at
`BufferDescriptor.unpack`, and the test module said it pins a `pack`/`unpack`
round trip; neither exists, and withholding the encoding is exactly what keeps
construction the only way in. `validate_tensor`'s comment claimed the validator
stands behind materialization as well — it does not, since
`ImportRegistry.materialize` takes an already-decoded descriptor and adds no
endpoint check, and a header that freezes an ABI should not overstate its own
gate. `docs/buffer-abi.md` named the wire type `simpler.task_interface.Tensor`
while the same page's status note, the module and a test all say it stays in
`simpler.buffer` until the cutover. Its `h.shm.buf` sample now records that it
is transitional: byte access belongs on the view, since a device backing has no
`shm` and code written against one forks by backend.

The endpoint x address_space matrix stays open — `materialize` still resolves a
DEVICE backing for a host endpoint. That is a behavioural gate with its own test
surface, not a comment fix.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants