Skip to content

Add: reject kind4 device pointers dispatched or freed on the wrong worker - #1430

Merged
ChaoWao merged 1 commit into
hw-native-sys:mainfrom
ChaoWao:feat/g2-device-addr-guard
Jul 22, 2026
Merged

Add: reject kind4 device pointers dispatched or freed on the wrong worker#1430
ChaoWao merged 1 commit into
hw-native-sys:mainfrom
ChaoWao:feat/g2-device-addr-guard

Conversation

@ChaoWao

@ChaoWao ChaoWao commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Scope

Device-address guard ② (worker-memory-model.md §9). A child (kind4) device
pointer is a bare int today — no owner, no generation (tensor.h
child_memory is one bit). Once it enters a TaskArgs it can be freed,
copied, or run on a worker that never allocated it; a raw device VA is not
globally unique, so worker B could silently touch worker A's (or illegal)
device memory. Guard ① (#1397) only stopped host-addr rewrite — nothing checked
the owning worker.

This tracks live child pointers by their exact (worker_id, ptr) provenance
and validates every kind4 consumer — the device memory ops and dispatch
(L2 Worker.run is not a consumer). Python-only; no wire / C++ / ABI change.

Delivered

  • Typed provenance table on the Worker: (worker_id, ptr) -> {malloc_owned, domain_allocation_ids}. Each op is atomic under one private lock. Ordering is
    safety-first: malloc records after the native alloc; free and domain
    release revoke before the native free.
  • malloc / free / copy_to / copy_from: free validates an exact live malloc
    base, revokes provenance, then does the native free (a commit barrier, L2
    and L3 identical). A native-free failure is a terminal leak — provenance is
    not restored and an explicit retry is rejected; an async unwind after a
    successful free can never leave a freed address live. copy_* require the
    device-side pointer live on that worker. Covers both Worker.malloc and the
    direct orch.malloc/copy/free path (single Orchestrator choke).
  • submit_next_level / submit_next_level_group: a child_memory argument must
    resolve to exactly one eligible target worker (resolved eligibility, not raw
    -1) and be live there; 0 or >= 2 candidates is rejected. The resolved
    owner is passed to C++ as the effective affinity so the child TensorKey
    is keyed by its owner, not -1. The group path materialises a full per-member
    affinity only when workers is empty/None; a non-empty list is length-checked
    (never padded), and a group that resolves two members to the same worker is
    rejected (a group must dispatch to distinct workers).
  • CommDomain window / buffer pointers enter provenance on allocate_domain
    and are revoked in _release_domain_now before the backend free (commit
    barrier): the deferred window stays dispatchable, but once release begins the
    pointers are undispatchable and a partial backend-release failure leaves them
    dropped rather than "live forever".

Boundary (explicit)

  • Catches stale-before-reuse; does not prevent strict ABA — needs P1
    generation handles.
  • Raw-C++ deep-bypass (worker._orch._o.malloc) out of scope (needs C++).
  • Remote path (RemoteBufferHandle) already owner+generation guarded — exempt.

Testing

  • 40 device-free UT (test_child_addr_guard.py): provenance table,
    target resolution, orchestrator memory ops incl. free commit-barrier +
    native-error terminal-leak
    , single/group dispatch + resolved affinity +
    group length/duplicate-worker validation, CommDomain dispatch and
    release-before-free commit barrier, L2 path. Fix-specific tests verified
    failing against pre-change source.
  • Broad UT sweep (test_worker/ + task/callable): 498 passed / 2 skipped
    — no regression. ruff + pyright clean.
  • Static non-regression on every kind4/domain example (all pin worker=).
  • Simulation / onboard via CI (st-sim-*, st-onboard-*).

@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Jul 22, 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

Run ID: 3234b090-642b-4645-956d-0169e366ff7a

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

The change adds exact child-pointer provenance tracking, validates worker ownership for memory operations and task dispatch, records and revokes domain allocations, and adds comprehensive unit coverage for L2, L3, grouped submission, and stale-pointer behavior.

Changes

Child pointer provenance

Layer / File(s) Summary
Worker provenance lifecycle
python/simpler/worker.py
Workers track malloc and CommDomain pointer ownership, validate L2 memory operations, revoke released domain pointers, and clear provenance during teardown.
Orchestrator dispatch and memory guards
python/simpler/orchestrator.py
Worker-bound orchestrators resolve eligible targets, guard individual and grouped submissions, and validate malloc, free, and copy operations.
Provenance and dispatch tests
tests/ut/py/test_worker/test_child_addr_guard.py
Tests cover pointer lifecycle, target resolution, orchestrator operations, submission guards, domain revocation, and L2 behavior.

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

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Orchestrator
  participant Worker
  participant CppOrchestrator
  Caller->>Orchestrator: submit child-managed TaskArgs
  Orchestrator->>Worker: resolve candidates and check provenance
  Worker-->>Orchestrator: approve or reject target
  Orchestrator->>CppOrchestrator: submit validated task
Loading

Poem

A rabbit guards each pointer’s trail,
Through worker paths where checks prevail.
Domains bloom, then fade away,
Stale paws are turned from harm’s array.
“Hop!” says the hare—the guards now stay.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 summarizes the main change: rejecting kind4 device pointers on the wrong worker.
Description check ✅ Passed The description is directly related to the provenance guard and memory/dispatch validation 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.

🧹 Nitpick comments (1)
python/simpler/orchestrator.py (1)

196-199: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: avoid rebinding the worker parameter. Line 197 reassigns the int parameter worker to self._worker. It's correct today because cpp_worker_id = int(worker) captures the int first, but the name now carries two meanings and the same pattern recurs in submit_next_level_group; a later edit that reads worker expecting the pin id below this point would break silently. Consider a distinct local (e.g. worker_obj).

♻️ Suggested rename
         cpp_worker_id = int(worker)
-        worker = self._worker
-        captured_refs = worker._capture_remote_sidecar_refs(remote_sidecar) if worker is not None else []
-        child_ptrs = worker._child_ptrs_in_args(c_args) if worker is not None else []
+        worker_obj = self._worker
+        captured_refs = worker_obj._capture_remote_sidecar_refs(remote_sidecar) if worker_obj is not None else []
+        child_ptrs = worker_obj._child_ptrs_in_args(c_args) if worker_obj is not None else []
🤖 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 `@python/simpler/orchestrator.py` around lines 196 - 199, In the affected
orchestration flow, avoid rebinding the worker parameter after computing
cpp_worker_id. Store self._worker in a distinct local such as worker_obj and use
that local for _capture_remote_sidecar_refs and _child_ptrs_in_args; apply the
same naming correction in submit_next_level_group while preserving the existing
None fallback behavior.
🤖 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.

Nitpick comments:
In `@python/simpler/orchestrator.py`:
- Around line 196-199: In the affected orchestration flow, avoid rebinding the
worker parameter after computing cpp_worker_id. Store self._worker in a distinct
local such as worker_obj and use that local for _capture_remote_sidecar_refs and
_child_ptrs_in_args; apply the same naming correction in submit_next_level_group
while preserving the existing None fallback behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 694eaeb1-a272-4a98-817f-e458089d5622

📥 Commits

Reviewing files that changed from the base of the PR and between 8199971 and e254c29.

📒 Files selected for processing (3)
  • python/simpler/orchestrator.py
  • python/simpler/worker.py
  • tests/ut/py/test_worker/test_child_addr_guard.py

@ChaoWao
ChaoWao force-pushed the feat/g2-device-addr-guard branch from e254c29 to c5d61e2 Compare July 22, 2026 02:27
@ChaoWao

ChaoWao commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks — addressed all four findings (force-pushed).

1 [high] worker=-1 resolved owner not passed to C++ → wrong TensorKey. Fixed. After the child_memory arg narrows to a unique target, that worker is now passed as the effective affinity to _o.submit_next_level (cpp_worker_id = next(iter(candidates))), and the group path materialises a full per-member affinity vector, pinning each child member to its resolved owner ([-1, W, …]). So the child TensorKey is keyed (ptr, W), not (ptr, -1) — the missed-dep and same-VA-collision cases are closed. New tests assert the affinity actually handed to C++ (call_args.args[5]), not just that submit was called.

2 [high] CommDomain release had no provenance commit barrier. Fixed. The revoke moved to the start of _release_domain_now, before the backend free, under the provenance lock. Through the deferred-release window (logical release → post-drain physical teardown) the pointers stay live for in-flight tasks; once physical release begins they are undispatchable. A partial/failed backend release now leaves them dropped (a recoverable leak) instead of stranded-live (use-after-free). New tests cover both the drop-before-dispatch ordering and the backend-failure-still-dropped path against the real _release_domain_now.

3 [medium] transaction-failure tests. Added: native malloc raises → nothing recorded (orch + L2); native free raises → provenance kept for retry (contract now explicit + tested); CommDomain partial-release failure; and the deterministic revoke-before-free interleaving.

4 [low] doc scope. PR body now says "every kind4 consumer (L2 Worker.run is not a consumer)". examples/workers/l3/child_memory/README.md now distinguishes "no automatic lifecycle (no auto-malloc/free)" from "still ownership-tracked: (worker_id, ptr) provenance validated on every copy/dispatch".

Verification: 36 device-free UT (all 4 fix-specific tests verified failing against the pre-fix source), 494/2-skip broad sweep, ruff + pyright clean, no wire/C++/ABI change.

@ChaoWao
ChaoWao force-pushed the feat/g2-device-addr-guard branch from c5d61e2 to d8e7f6a Compare July 22, 2026 03:05
@ChaoWao

ChaoWao commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

Round 2 addressed (force-pushed).

1 [high] free() not transactional → UAF on async unwind. Fixed with a safety-first commit barrier: free now validates the exact malloc base, revokes provenance, then calls the native free — under the lock, both L2 (Worker.free) and L3 (Orchestrator.free). An async unwind (KeyboardInterrupt after the binding returns) can no longer leave a freed address live. A native-free failure is now a terminal leak, never a re-authorized maybe-freed address; the earlier "keep for retry" contract is gone. New tests: free revokes before the native call (spy asserts already-revoked at native time, L2 + L3), and native-error → revoked + retry rejected.

2 [medium] group swallowed illegal workers length. Fixed: the full per-member affinity is materialised only when workers is empty/None. A non-empty list is length-checked (len(workers) == len(args_list)) and copied — never padded — so 2 args + workers=[0] now raises instead of becoming [0, -1] and bypassing the C++ length check. New test covers the rejection.

Should-fix (both done):

  • The positive group affinity test is now a single-member group (schedulable) — no more {1}-eligibility-for-two-distinct-workers case.
  • Stale "revoke after backend free" comment on allocate_domain, the _child_prov_drop_domain docstring, and .docs/tasks/g2-design.md §2 all now say "revoke before the backend free (commit barrier)".

Verification: 39 device-free UT — I reverted both round-2 hunks and confirmed the 4 fix-specific tests fail with the right assertions, then restored. 497/2-skip broad sweep, ruff + pyright clean, no wire/C++/ABI change.

@ChaoWao
ChaoWao force-pushed the feat/g2-device-addr-guard branch from d8e7f6a to 4936744 Compare July 22, 2026 03:36
@ChaoWao

ChaoWao commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

Round 3 addressed (force-pushed 49367442).

1 [blocker] group could pin two members to the same worker. Fixed. After the per-member affinity write-back, a group with two members resolving to the same owner (a duplicate non-negative affinity — e.g. two child args on the same chip) is now rejected ("a group must dispatch to distinct workers") rather than emitting [W, W] and serializing on one WorkerThread. New regression: two child members owned by the same worker → reject.

2 [blocker] free contract vs docs mismatch. Fixed everywhere to the implemented safety-first semantics (validate → revoke → native free; native-free failure = terminal leak, no retry):

  • PR body (also corrected counts to 40 UT / 498 broad).
  • worker.py _child_alloc_prov init comment, the provenance-helpers block comment, and _child_prov_clear_malloc docstring.
  • tests/…/test_child_addr_guard.py section header.
  • .docs/tasks/g2-design.md §2 (free ordering + linearization).
  • (task-device-addr-guard2.md is the original task prompt / spec — left as historical; happy to annotate if you'd prefer.)

Non-blocking: _next_level_target_ids() L4 fallback now returns the stable _next_level_worker_ids instead of range(len(_next_level_workers)).

Verification: 40 device-free UT — the group-duplicate fix verified failing (DID NOT RAISE) against reverted source; 498/2-skip broad sweep; ruff + pyright clean.

CI: agreed on your read — last round's st-onboard-a5 (bidirectional-ring allreduce) passed on c5d61e20 and my diff doesn't touch comm, so it's a hardware flake; ut-a5 died at runner "Set up job" (SSL/action-download infra). I'm watching this run and will rerun those if they recur, and only conclude "flake" once a rerun is green.

@ChaoWao
ChaoWao force-pushed the feat/g2-device-addr-guard branch from 4936744 to 8e95959 Compare July 22, 2026 04:40
@ChaoWao

ChaoWao commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

Round 4 addressed (force-pushed 8e959593).

1 [high] empty entry could re-authorize a freed pointer. Fixed, fail-closed:

  • New entries set their role before insertion; dropping the last role deletes the key directly — a role-less entry now never exists.
  • _ChildProvEntry.is_live() (malloc_owned or domain_allocation_ids); _child_prov_require_live and the dispatch guard both test is_live(), not key presence. So even if an entry were momentarily left empty by an interrupted revoke, no copy_*/dispatch passes.
  • Regression: an injected role-less entry is rejected by require_live / require_malloc_base / dispatch; and dropping the last role deletes outright.

2 [medium] provenance analysis outside the remote-ref rollback window. Fixed: the (fallible) kind4 analysis now runs before _capture_remote_sidecar_refs — capture is the last step before the try — in both the single and group paths. An analysis failure can no longer strand captured refs (deferring a remote free forever). Regression asserts capture is not called when the analysis raises.

Should-fix / track:

  • Deterministic contention tests added: free holds _child_prov_lock across the native free (mutual exclusion), and a domain-release-vs-dispatch check confirms a dispatch landing during the native release is rejected (already revoked).
  • distinct-workers scope narrowed in comment + design doc: the check rejects duplicate pinned owners (the hazard the affinity write-back creates); full injective feasibility (a wildcard member colliding with a pinned one) is the scheduler's pre-existing capacity concern, explicitly not claimed here.
  • Doc numbers synced to 45 UT / 503 broad; _next_level_target_ids() L4 fallback → stable _next_level_worker_ids; and task-device-addr-guard2.md §1 free-order updated to the implemented revoke-before-native / terminal-leak / fail-closed semantics.

Verification: 45 device-free UT — both blocker fixes verified failing against reverted source (empty-entry DID-NOT-RAISE; capture called-1-time); 503/2-skip broad sweep; ruff + pyright clean; no wire/C++/ABI change.

…rker

A child (kind4) device pointer is a bare int today — no owner, no generation
(tensor.h child_memory is one bit). Once such a pointer entered a TaskArgs it
could be freed, copied, or run on a worker that never allocated it: a raw
device VA is not globally unique, so worker B silently touched worker A's (or
illegal) device memory. Guard ① (hw-native-sys#1397) only stopped host-addr rewrite from
corrupting a device VA; nothing checked the pointer's owning worker.

Track live child pointers by their exact (worker_id, ptr) provenance and
validate every kind4 consumer (device ops and dispatch; L2 Worker.run is not a
consumer):

- Provenance table on the Worker: a typed (worker_id, ptr) -> entry map
  (malloc_owned + domain_allocation_ids), so a malloc base and an aliasing
  CommDomain buffer are distinct roles. Each op is atomic under one private
  lock; ordering is safety-first (record after alloc, revoke before free). Async
  interruption is fail-closed: a role is fully set before its entry is inserted,
  the last role is deleted directly (no role-less entry ever exists), and every
  live check tests the roles, not key presence, so an entry momentarily left
  empty never re-authorizes a freed pointer. Cleared on close().
- malloc: recorded after the backend malloc succeeds. free: a safety-first
  commit barrier — validate an exact live malloc base, then revoke provenance
  BEFORE the native free, so an async unwind (e.g. a KeyboardInterrupt after the
  binding returns) can never leave a freed address live; a native-free failure
  becomes a terminal leak, never a re-authorized maybe-freed address. copy_*
  require the device-side pointer live on that worker. Covers both Worker.malloc
  (L2 + L3) and the direct orch.malloc/copy/free paths (single Orchestrator
  choke), L2 and L3 unified.
- submit_next_level / submit_next_level_group: a child_memory argument must
  resolve to exactly one eligible target worker (judged on the resolved
  eligibility, not a raw worker=-1) and be a live allocation there; 0 or >= 2
  candidates is ambiguous and rejected. The resolved owner is passed to C++ as
  the effective affinity so the child TensorKey is keyed by its owner, not the
  raw -1 (otherwise the same buffer submitted once as -1 and once as W gets two
  keys and its dep is missed). The group path materialises a full per-member
  affinity only when workers is empty/None; a non-empty workers list is length-
  checked against args (never silently padded, which would bypass the C++
  length check). A group must dispatch to distinct workers, so two members that
  resolve to the same owner (a duplicate non-negative affinity) are rejected
  rather than silently serialized on one WorkerThread.
- CommDomain window / buffer pointers enter provenance on allocate_domain and
  are revoked in _release_domain_now BEFORE the backend free (a commit barrier):
  the deferred window stays dispatchable, but once physical release begins the
  pointers are undispatchable, so a concurrent dispatch cannot validate a
  being-freed pointer and a partial backend-release failure leaves them dropped
  rather than "live forever".

Boundary: catches stale-before-reuse, NOT strict ABA (a re-malloc of the same
VA becomes live again) — that needs P1 generation handles. Python-only, no
wire / C++ / ABI change; the raw-C++ deep-bypass (worker._orch._o.malloc) is
out of scope.

The remote-slot-ref capture runs after (not before) the kind4 provenance
analysis, so an analysis failure cannot strand captured refs outside the
rollback try and defer a remote free forever.

Adds 45 device-free UT (provenance table incl. empty-entry fail-closed, target
resolution, orchestrator memory ops incl. free commit-barrier + native-error +
lock-held-across-native, single/group dispatch + resolved affinity + group
length/duplicate-worker validation, capture-after-analysis ordering, CommDomain
dispatch and release-before-free commit barrier, L2 path).
@ChaoWao
ChaoWao merged commit c8c7b73 into hw-native-sys:main Jul 22, 2026
16 checks passed
@ChaoWao
ChaoWao deleted the feat/g2-device-addr-guard branch July 22, 2026 05:04
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.

1 participant