Repository navigation
[soundness] generic struct type-argument erasure in hy-check bypasses Tier-0 alias freeze (HYB-E3003), non-owned move (HYB-E3004), const deep immutability (HYB-E3006), closure/handle move perimeter (HYB-E3001/E3002), A3 slot loans (HYB-E3102), and N1/N3 (HYB-E4001/E4002) #772
Copy link
Copy link
Open
Description
The smallest .hy program that demonstrates it
Repro 1 — Tier-0 shallow-copy alias freeze bypass (HYB-E3003, O5.2) -> go test -race DATA RACE
struct box[t] {
v t
}
fn worker(b box[map[string]int], done chan<- bool) {
for i in 0..1000 {
b.v["k"] = i
}
done <- true
}
fn main() {
done := make(chan bool, 1)
b1 := box[map[string]int]{v: map[string]int{"k": 0}}
b2 := b1
task worker(move b1, done)
for i in 0..1000 {
b2.v["k"] = i + 1
}
<-done
}Repro 2 — Non-owned parameter/capture move bypass (HYB-E3004, O5.1) -> go test -race DATA RACE
struct box[t] {
v t
}
fn worker(b box[map[string]int], done chan<- bool) {
for i in 0..1000 {
b.v["k"] = i
}
done <- true
}
fn leak(p box[map[string]int], done chan<- bool) {
b := p
task worker(move b, done)
}
fn main() {
done := make(chan bool, 1)
orig := box[map[string]int]{v: map[string]int{"k": 0}}
leak(orig, done)
for i in 0..1000 {
orig.v["k"] = i + 1
}
<-done
}Repro 3 — const deep-immutability bypass (HYB-E3006, T22) -> go test -race DATA RACE
struct box[t] {
v t
}
const shared_box = box[map[string]int]{v: map[string]int{"k": 0}}
fn writer(done chan<- bool) {
c := shared_box
for i in 0..1000 {
c.v["k"] = i
}
done <- true
}
fn main() {
d1 := make(chan bool, 1)
d2 := make(chan bool, 1)
task writer(d1)
task writer(d2)
<-d1
<-d2
}Repro 4 — Closure / BorrowHandle / Mutex move perimeter bypass (HYB-E3001 / HYB-E3002, A5 / O7 / O11) -> go test -race DATA RACE
struct box[t] {
v t
}
fn run_box(b box[fn()], done chan<- bool) {
for i in 0..1000 {
b.v()
}
done <- true
}
fn main() {
done := make(chan bool, 1)
m := map[string]int{"k": 0}
b := box[fn()]{v: fn() { m["k"] = m["k"] + 1 }}
task run_box(move b, done)
for i in 0..1000 {
m["k"] = i
}
<-done
}Repro 5 — A3 map-slot loan invalidation bypass (HYB-E3102, O8) & N1/N3 (HYB-E4001 / HYB-E4002)
import "fmt"
struct box[t] {
v t
}
enum msg {
data(int)
quit
}
fn main() {
m := map[string]int{"a": 1}
b := box[map[string]int]{v: m}
p := &b.v["a"]
delete(m, "a")
fmt.println(p)
nil_box := box[map[string]int]{v: nil}
var uninit_box box[msg]
_ = nil_box
_ = uninit_box
}Negative Control (must continue to be accepted with 0 diagnostics)
struct box[t] {
v t
}
fn worker(b box[int], done chan<- int) {
done <- b.v + 1
}
fn main() {
done := make(chan int, 1)
b1 := box[int]{v: 41}
b2 := b1
task worker(move b1, done)
b2.v = 99
_ = <-done
}What the checker should say
- Repro 1: Reject with
HYB-E3003(b2shares reference storage with movedb1via generic fieldv: map[string]int, sob2.v["k"] = ...is a post-move use of a frozen shallow-copy peer). - Repro 2: Reject with
HYB-E3004(b := pcopies reference-bearing parameterp: box[map[string]int], which is not owned byleakand cannot bemoved intotask worker). - Repro 3: Reject with
HYB-E3006(c := shared_boxshares reference storage withconst shared_box, soc.v["k"] = iwrites through aconst). - Repro 4: Reject with
HYB-E3001(box[fn()]holds a closure at field.v; closures are never sendable even withmove). - Repro 5: Reject with
HYB-E3102(delete(m, "a")while map-slot loanp := &b.v["a"]is live),HYB-E4001(nilinitializer forv: map[string]int), andHYB-E4002(uninitializedvar uninit_box box[msg]whose payload-first enummsghas no zero value). - Negative Control: Accept (
box[int]is a pure value-copy struct with no reference/closure/handle fields).
What it actually said
hyb check accepts all five repros with 0 diagnostics (Verdict::Accept), and go test -race on the emitted Go reports concurrent map read/write panics and data races for Repros 1–4.
Claimed severity
P0 — Soundness bug (hyb check accepts a race / UB / invalid state that GUARANTEES.md rows O4, O5.1, O5.2, O7, O8, O11, A3, A5, T22, N1, and N3 promise to reject).
Anything else
Root Cause in compiler/crates/hy-check/src/:
Checker::named_shape_depth(shape.rs:80-84) strips generic type arguments viaSelf::base_named_type_str(name)and returnsTypeShape::Struct(Some("box"))forbox[map[string]int], erasing the concrete type argumentmap[string]int.- In
self.structs.get("box"), fieldvhas the raw type parameterTypeExpr::Named { name: "t" }, whoseshape_of_typeisTypeShape::Unknown. - Although
sendability.rs:2228-2341already definesparse_generic_type_args,substitute_type_params, andstruct_fields_for_type(used only for channel-direction checking), none of the core shape/aliasing/sendability/loan/immutability/nil/zero-value queries substitute concrete type arguments into generic struct fields or generic call returns:shape_may_share_at(shape.rs:132-140) seesTypeShape::Unknownfor fieldvand returnsfalse! Thusshape_may_shareandbinding_may_sharereturnfalseforbox[map[string]int],box[[]int],box[*int], etc., completely disablingrecord_storage_from_snapshot(may-share edges,non_owned_originpropagation,constshare-group linking) andconst_arg_root.field_shape(shape.rs:301-312) andty_of_exprforExpr::Field(shape.rs:643-648) returnTypeShape::UnknownandSome(Named("t"))forb.v, breaking A3 map-slot loan detection on&b.v["a"]and non-owned field projection (m := p.v; move m).shape_carries_handle_at(shape.rs:171-179),shape_holds_closure(sendability.rs:166-177),shape_holds_unmovable_leaf(sendability.rs:198-209),shape_is_const_sendable(sendability.rs:225-236),struct_has_mutex_field(sendability.rs:583-609),check_composite_nils(sendability.rs:978-1033), andno_zero_value_reason(shape.rs:828-845) all inspect raw un-substituted field types inself.structs.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
sonson0910 commentedon Oct 19, 2026
Prior-Art & First-Reporter Notice (Duplicate of
#105/PR #106, filed 97.5 hours earlier)For maintainer triage reference (
@limitlessprojects), the root cause and soundness surface reported here —TypeShapegeneric type-argument erasure on generic structs (box[T],lru_cache[K, V]) acrossshape.rs,sendability.rs, andloans.rs— was originally reported and fixed 4 days (97.5 hours) earlier by@sonson0910in:2026-10-15T01:51:53Z5ea48d1,MERGEABLE / CLEANonaa4a9d9) — opened2026-10-15T01:52:46Z#772(@aazminu) — filed2026-10-19T03:18:57Z(+97 hours 27 minutes later)#775(@huynhnhatquandongnai-source) — filed2026-10-19T03:26:51Z(+97 hours 35 minutes later)PR #106already implementsChecker::resolved_struct_fields,struct_fields_for_name, andshape_field_borrow_kindacrossshape.rs,sendability.rs, andloans.rs, rejecting all repros (HYB-E3003,HYB-E3004,HYB-E3006,HYB-E3001/HYB-E3002,HYB-E3102,HYB-E3104,HYB-E4001, andHYB-E4002) with regression tests incompiler/crates/hy-check/src/lib.rs.sonson0910 commentedon Oct 20, 2026
Prior-Art & First-Reporter Notice (
Issue #105/ CompanionPR #106, Filed 98 Hours Earlier)For maintainer triage reference (
@limitlessprojects— corroborating Shepherd's Jaccard overlap flag with#106):The generic struct type-argument stripping bug in
TypeShape::Structacrossshape.rs,loans.rs,freeze.rs,sendability.rs, andlib.rswas already reported and fixed 98 hours (4 days) earlier by@sonson0910:@sonson09102026-10-15T01:49:18ZTypeShape::Structdropping generic type arguments (box[map[string]int],lru_cache[string, int]) acrossshape.rs,sendability.rs,freeze.rs,loans.rs, andclosures.rs.@sonson09102026-10-15T01:52:46Zshape.rs,sendability.rs,freeze.rs,loans.rs,closures.rs,emit_guard.rs, andlib.rs(synced to latestmain,behind_by = 0).@aazminu2026-10-19T03:38:03Z/03:51:10Z(+98h later)shape.rsandloans.rs.fix(hy-check): preserve generic struct type arguments across shape an…