From 1e925f5f22d0bdb2cd425c82421c7764bcc43980 Mon Sep 17 00:00:00 2001 From: InauguralPhysicist Date: Fri, 24 Jul 2026 21:24:07 -0500 Subject: [PATCH] Fix MT use-after-free: module-env observer-slot growth vs unlocked reads (#694) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit observer_slot_update_e grew env->obs with a bare realloc + non-atomic pointer store while holding no lock, and the report of / when is / predicate opcodes read a chain-resolved env's obs array outside any lock. A worker reading a MODULE binding's observer slot concurrently with the main thread first-observing a NEW module binding therefore walked a freed array. This is exactly the use-after-free class #607 fixed for names/values/ assign_counts — obs was simply never brought into that scheme. It survived because module obs grows only on a binding's FIRST observation, and only on the main thread, so the window is narrow enough never to have bitten. obs now follows the same discipline: - growth on a shared module env copies into a fresh block under g_module_env_lock and RETIRES the old one (freed at park/destroy) rather than freeing it, so a reader can never touch freed memory; - the new block is published with a release store, and only THEN the wider cap, also release; - readers go through a new env_obs_slot() accessor that loads cap first and pointer second, both acquire. That load/store ordering is load-bearing and commented as such: reading pointer-before-cap would admit new-cap-with-old-block, i.e. an out-of-bounds read of the retired array. Single-threaded runs and thread-local frame/loop envs keep the plain realloc and plain loads behind one predicted-false g_vm_multithreaded branch, so nothing off the MT path pays for this. Scope: same-slot value races between two threads writing one binding stay out of scope, exactly as #607 declared — retirement keeps both blocks alive, so that is slot-value semantics, not array memory safety. All 17 obs access sites converted (an initial survey found only 9 because the grep filtered out lines containing obs_cap). Validation: tests/test_obs_mt_race.eigs 5 TSan data races -> 0 (rc 66 -> 0) tests/test_tsan.sh 11 passed, 0 failed; seeded race still detected, so the gate is not vacuous release suite 3183/3183 ASan + detect_leaks=1 3187/3187, leak tally 0, [87] leak-clean Closes #694 Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 21 ++ src/eigenscript.c | 76 ++++- src/eigenscript.h | 27 ++ src/vm.c | 73 +++-- tests/test_obs_mt_race.eigs | 633 ++++++++++++++++++++++++++++++++++++ tests/test_tsan.sh | 2 +- 6 files changed, 783 insertions(+), 49 deletions(-) create mode 100644 tests/test_obs_mt_race.eigs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9014176f..c54b6177 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -148,6 +148,27 @@ All notable changes to EigenScript are documented here. by pointing the instrument at itself. ### Fixed +- **MT use-after-free: module-env observer-slot growth raced unlocked worker + reads (#694).** `observer_slot_update_e` grew `env->obs` with a bare + `realloc` + non-atomic pointer store while holding no lock, and the + `report of` / `when is` / predicate opcodes read a chain-resolved env's + `obs` array outside any lock. A worker reading a MODULE binding's observer + slot concurrently with the main thread first-observing a NEW module binding + therefore walked a freed array — the exact use-after-free class #607 fixed + for `names`/`values`/`assign_counts`, which `obs` was never brought into. + The window is narrow (module obs grows only on a binding's *first* + observation, only on the main thread), which is why it never bit in + practice. `obs` now follows the same #607 discipline: growth on a shared + module env copies into a fresh block under `g_module_env_lock`, RETIRES the + old one (freed at park/destroy) instead of freeing it, and publishes the + pointer with a release store — then the cap, also release. Readers go + through the new `env_obs_slot()` accessor, which loads cap *then* pointer + with acquires; the reverse order would admit new-cap-with-old-block, an + out-of-bounds read of the retired array. Single-threaded and thread-local + frame envs keep the plain realloc and plain loads behind one + predicted-false branch, so there is no cost off the MT path. New TSan + regression `test_obs_mt_race.eigs` (in the `test_tsan.sh` race-free slice) + reports 5 data races without the fix and 0 with it. - **`report_value` no longer calls a monotone converging sequence `diverging` (#674).** The value channel's raw-step divergence test (#422) judged a window's steps "non-vanishing" when the recent half's mean diff --git a/src/eigenscript.c b/src/eigenscript.c index 8ec8fea6..1296273c 100644 --- a/src/eigenscript.c +++ b/src/eigenscript.c @@ -536,18 +536,22 @@ void observer_slot_record_value(ObserverSlot *s, double v) { s->v_used = 1; } +/* #694: grow e->obs to cover `idx`. Defined with the #607 module-env MT + * helpers below (it needs the lock + retire machinery). Returns 0 on OOM. */ +static int observer_obs_grow(Env *e, int idx); + /* Core: fold a precomputed entropy into binding slot `idx` of env `e`. */ static void observer_slot_update_e(Env *e, int idx, double new_entropy) { if (!e || idx < 0) return; - if (idx >= e->obs_cap) { - int ncap = idx + 8; - ObserverSlot *no = realloc(e->obs, (size_t)ncap * sizeof(ObserverSlot)); - if (!no) return; - memset(no + e->obs_cap, 0, (size_t)(ncap - e->obs_cap) * sizeof(ObserverSlot)); - e->obs = no; - e->obs_cap = ncap; - } - ObserverSlot *s = &e->obs[idx]; + if (idx >= e->obs_cap && !observer_obs_grow(e, idx)) return; + /* Acquire-load the (possibly just-republished) block rather than reading + * e->obs plainly: a worker assigning a module binding can reach here while + * the main thread grows. Which block a racing writer lands in is #607's + * declared out-of-scope case (same-slot value semantics, not array memory + * safety) — retirement keeps either block alive — but the POINTER word + * itself must still be synchronized. */ + ObserverSlot *s = env_obs_slot(e, idx); + if (!s) return; s->prev_dH = s->dH; if (!s->used || s->obs_age == 0) { s->dH = 0; /* first observation of this binding */ @@ -565,8 +569,10 @@ void observer_slot_update(Env *e, int idx, Value *newval) { observer_slot_update_e(e, idx, compute_entropy(newval)); /* #294 also fold the raw value into the value-signal channel (numbers only: * the relative-delta step is only defined for a scalar trajectory). */ - if (newval && newval->type == VAL_NUM && e && idx >= 0 && idx < e->obs_cap) - observer_slot_record_value(&e->obs[idx], newval->data.num); + if (newval && newval->type == VAL_NUM) { + ObserverSlot *s = env_obs_slot(e, idx); + if (s) observer_slot_record_value(s, newval->data.num); + } } /* #262 Phase-3 D: update a binding's observer slot from a raw immediate @@ -574,8 +580,8 @@ void observer_slot_update(Env *e, int idx, Value *newval) { * tracked Value. Same trajectory math as observer_slot_update. */ void observer_slot_update_num(Env *e, int idx, double num) { observer_slot_update_e(e, idx, entropy_of_num(num)); - if (e && idx >= 0 && idx < e->obs_cap) /* #294 value-signal channel */ - observer_slot_record_value(&e->obs[idx], num); + ObserverSlot *vs = env_obs_slot(e, idx); /* #294 value-signal channel */ + if (vs) observer_slot_record_value(vs, num); } void observer_slot_reset(Env *e) { @@ -1697,6 +1703,50 @@ static void env_grow_retire(Env *env, int new_cap) { env->capacity = new_cap; } +/* #694: grow the observer-slot array to cover binding `idx`. + * + * Single-threaded (and for every thread-local frame/loop env) this stays the + * plain realloc it always was. On a SHARED module env under MT it mirrors the + * #607 discipline exactly, because the hazard is identical: worker threads + * read a chain-resolved module env's obs array with no lock held (report of / + * when is on a module binding), while the main thread first-observes a new + * module binding and grows it. A realloc there frees the block a worker is + * still walking — the same use-after-free class #607 fixed for + * names/values/assign_counts, and the reason this one survived is that module + * obs grows only on the FIRST observation of a binding, so the window is + * narrow enough never to have been hit in practice. + * + * So: copy into a fresh block, RETIRE the old one (freed at park/destroy, + * bounded by the same geometric-waste argument), publish the pointer with a + * release store, and only then publish the wider cap — see env_obs_slot for + * why that store order is load-bearing. */ +static int observer_obs_grow(Env *e, int idx) { + int ncap = idx + 8; + size_t nsz = (size_t)ncap * sizeof(ObserverSlot); + size_t osz = (size_t)e->obs_cap * sizeof(ObserverSlot); + if (!env_mt_shared(e)) { + ObserverSlot *no = realloc(e->obs, nsz); + if (!no) return 0; + memset((char *)no + osz, 0, nsz - osz); + e->obs = no; + e->obs_cap = ncap; + return 1; + } + pthread_mutex_lock(&g_module_env_lock); + if (idx >= e->obs_cap) { /* re-check: another grow may have won */ + osz = (size_t)e->obs_cap * sizeof(ObserverSlot); + ObserverSlot *no = malloc(nsz); + if (!no) { pthread_mutex_unlock(&g_module_env_lock); return 0; } + if (e->obs) memcpy(no, e->obs, osz); + memset((char *)no + osz, 0, nsz - osz); + env_retire_block(e, e->obs); + __atomic_store_n(&e->obs, no, __ATOMIC_RELEASE); + __atomic_store_n(&e->obs_cap, ncap, __ATOMIC_RELEASE); + } + pthread_mutex_unlock(&g_module_env_lock); + return 1; +} + /* Hash rebuild for a shared env under MT: same sizing as * env_hash_rebuild, but the old bucket arrays are retired, not freed — * belt-and-suspenders (probes are lock-serialized, but a freed bucket diff --git a/src/eigenscript.h b/src/eigenscript.h index 58f9d093..769cde0f 100644 --- a/src/eigenscript.h +++ b/src/eigenscript.h @@ -947,6 +947,33 @@ static inline int *env_assign_counts_ptr(Env *e) { return e->assign_counts; } +/* #694: bounds-checked observer-slot access for an env the MAIN thread may + * grow concurrently. Same class as env_values_ptr, but obs needs the CAP and + * the POINTER to be read consistently, so the two are ordered against each + * other: the writer (observer_obs_grow) publishes the new block with a + * release store and only THEN the new cap, also release. A reader that loads + * cap first (acquire) and the pointer second (acquire) therefore sees either + * - the old cap, with an old-or-new block — both hold >= old_cap slots, + * since the new block is a superset copy and the old one is retired, or + * - the new cap, which by the release/acquire chain guarantees the new + * block is already visible. + * Reading them in the other order would admit new-cap-with-old-block, i.e. an + * out-of-bounds read of the retired array. Returns NULL when idx is out of + * range, so callers replace the `idx < e->obs_cap && e->obs[idx].used` idiom + * with a null check. Single-threaded: plain loads behind one predicted-false + * branch. */ +static inline struct ObserverSlot *env_obs_slot(Env *e, int idx) { + if (!e || idx < 0) return NULL; + if (__builtin_expect(g_vm_multithreaded, 0)) { + int cap = __atomic_load_n(&e->obs_cap, __ATOMIC_ACQUIRE); + if (idx >= cap) return NULL; + struct ObserverSlot *o = __atomic_load_n(&e->obs, __ATOMIC_ACQUIRE); + return o ? &o[idx] : NULL; + } + if (idx >= e->obs_cap || !e->obs) return NULL; + return &e->obs[idx]; +} + Env* env_new(Env *parent); void env_set(Env *env, const char *name, Value *val); Value* env_get(Env *env, const char *name); diff --git a/src/vm.c b/src/vm.c index 3573fa50..cc4c4dde 100644 --- a/src/vm.c +++ b/src/vm.c @@ -31,11 +31,8 @@ Value* builtin_observe(Value *arg); * disagree on loop classification (the same lockstep invariant the opcode * encoding enforces). Returns 1 if (*dH, *ent) were filled. */ static inline int obs_stall_trajectory(double *dH, double *ent) { - if (g_last_obs_slot_env && - g_last_obs_slot_idx >= 0 && - g_last_obs_slot_idx < g_last_obs_slot_env->obs_cap && - g_last_obs_slot_env->obs[g_last_obs_slot_idx].used) { - const ObserverSlot *s = &g_last_obs_slot_env->obs[g_last_obs_slot_idx]; + const ObserverSlot *s = env_obs_slot(g_last_obs_slot_env, g_last_obs_slot_idx); + if (s && s->used) { *dH = s->dH; *ent = s->entropy; return 1; } @@ -163,7 +160,7 @@ static void obs_dump_scope(const char *scope, Env *e, int shared, * (OBSERVE_ASSIGN_LOCAL / OBSERVE_NAME_POST), and recycled call envs * reset both counters (vm_park_call_env), preserving per-call * semantics: max() of the two is the true assignment count. */ - const ObserverSlot *os = (e->obs && i < e->obs_cap) ? &e->obs[i] : NULL; + const ObserverSlot *os = env_obs_slot(e, i); long when = e->assign_counts ? e->assign_counts[i] : -1; if (os && os->used && os->obs_age > when) when = os->obs_age; char whenbuf[16]; @@ -1260,8 +1257,9 @@ void jit_helper_report_slot(int slot) { CallFrame *frame = &g_vm.frames[g_vm.frame_count - 1]; Env *e = frame->fn_env; Value *result; - if (e && slot < e->obs_cap && e->obs[slot].used) { - result = make_str(observer_slot_report(&e->obs[slot])); + const ObserverSlot *os_r = env_obs_slot(e, (int)slot); + if (os_r && os_r->used) { + result = make_str(observer_slot_report(os_r)); } else { result = make_str("equilibrium"); /* unobserved binding — no trajectory */ } @@ -1291,8 +1289,8 @@ void jit_helper_observe_name_post(EigsChunk *chunk, int name_idx) { /* #262 Phase-3 D2: slot-source the temporal snapshot — mirror of the * interpreter CASE(OBSERVE_NAME_POST). */ if (__builtin_expect(g_trace_obs_hist, 0)) { - const ObserverSlot *os = &oe->obs[oidx]; - trace_record_obs(name, os->entropy, os->dH, os->last_entropy); + const ObserverSlot *os = env_obs_slot(oe, oidx); + if (os) trace_record_obs(name, os->entropy, os->dH, os->last_entropy); } } } @@ -4374,8 +4372,9 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume) { uint16_t slot = read_u16(ip); ip += 2; Env *e = frame->fn_env; Value *result; - if (e && (int)slot < e->obs_cap && e->obs[slot].used) { - result = make_str(observer_slot_report(&e->obs[slot])); + const ObserverSlot *os_l = env_obs_slot(e, (int)slot); + if (os_l && os_l->used) { + result = make_str(observer_slot_report(os_l)); } else { result = make_str("equilibrium"); /* unobserved binding */ } @@ -4399,8 +4398,9 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume) { DISPATCH(); } Value *result; - if (oidx >= 0 && oidx < oe->obs_cap && oe->obs[oidx].used) { - result = make_str(observer_slot_report(&oe->obs[oidx])); + const ObserverSlot *os_n = env_obs_slot(oe, oidx); + if (os_n && os_n->used) { + result = make_str(observer_slot_report(os_n)); } else { result = make_str("equilibrium"); /* unobserved binding */ } @@ -4415,8 +4415,9 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume) { uint16_t slot = read_u16(ip); ip += 2; Env *e = frame->fn_env; Value *result; - if (e && (int)slot < e->obs_cap) - result = make_str(observer_slot_report_value(&e->obs[slot])); + const ObserverSlot *vs_l = env_obs_slot(e, (int)slot); + if (vs_l) + result = make_str(observer_slot_report_value(vs_l)); else result = make_str("equilibrium"); vm_push(result); @@ -4438,8 +4439,9 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume) { DISPATCH(); } Value *result; - if (oidx >= 0 && oidx < oe->obs_cap) - result = make_str(observer_slot_report_value(&oe->obs[oidx])); + const ObserverSlot *vs_n = env_obs_slot(oe, oidx); + if (vs_n) + result = make_str(observer_slot_report_value(vs_n)); else result = make_str("equilibrium"); vm_push(result); @@ -4453,7 +4455,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume) { * (classifies "equilibrium", mirroring report_value's fallback). */ uint16_t slot = read_u16(ip); ip += 2; Env *e = frame->fn_env; - const ObserverSlot *s = (e && (int)slot < e->obs_cap) ? &e->obs[slot] : NULL; + const ObserverSlot *s = env_obs_slot(e, (int)slot); vm_push(observer_slot_trajectory(s)); DISPATCH(); } @@ -4472,7 +4474,7 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume) { vm_push_slot(slot_null()); DISPATCH(); } - const ObserverSlot *s = (oidx >= 0 && oidx < oe->obs_cap) ? &oe->obs[oidx] : NULL; + const ObserverSlot *s = env_obs_slot(oe, oidx); vm_push(observer_slot_trajectory(s)); DISPATCH(); } @@ -4482,8 +4484,8 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume) { * from the local's slot trajectory; no-observation tuple if unpopulated. */ uint16_t slot = read_u16(ip); ip += 2; Env *e = frame->fn_env; - if (e && (int)slot < e->obs_cap && e->obs[slot].used) { - const ObserverSlot *s = &e->obs[slot]; + const ObserverSlot *s = env_obs_slot(e, (int)slot); + if (s && s->used) { Value *list = make_list(4); list_append_owned(list, make_str(observer_slot_report(s))); list_append_owned(list, make_num(s->entropy)); @@ -4505,8 +4507,8 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume) { if (h == 0) { h = env_hash_name(name); if (chunk->const_hashes) chunk->const_hashes[name_idx] = h; } int oidx = -1, odepth = 0; Env *oe = env_resolve_chain(frame->env, name, h, &oidx, &odepth); - if (oe && oidx >= 0 && oidx < oe->obs_cap && oe->obs[oidx].used) { - const ObserverSlot *s = &oe->obs[oidx]; + const ObserverSlot *s = env_obs_slot(oe, oidx); + if (s && s->used) { Value *list = make_list(4); list_append_owned(list, make_str(observer_slot_report(s))); list_append_owned(list, make_num(s->entropy)); @@ -4546,8 +4548,8 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume) { * fresh; its history entry was created by the SET's * trace_assign). */ if (__builtin_expect(g_trace_obs_hist, 0)) { - const ObserverSlot *os = &oe->obs[oidx]; - trace_record_obs(name, os->entropy, os->dH, os->last_entropy); + const ObserverSlot *os = env_obs_slot(oe, oidx); + if (os) trace_record_obs(name, os->entropy, os->dH, os->last_entropy); } } } @@ -4623,8 +4625,8 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume) { * yet, so the answer matches the value path. */ int oidx = -1, odepth = 0; Env *oe = env_resolve_chain(frame->env, name, h, &oidx, &odepth); - if (oe && oidx >= 0 && oidx < oe->obs_cap && oe->obs[oidx].used) { - const ObserverSlot *s = &oe->obs[oidx]; + const ObserverSlot *s = env_obs_slot(oe, oidx); + if (s && s->used) { if (kind == 3) result = make_num(s->entropy); else if (kind == 4) result = make_num(s->dH); else result = make_num(observer_settledness(s->dH)); @@ -4835,9 +4837,8 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume) { * the predicate is false. */ uint16_t kind = read_u16(ip); ip += 2; int result = 0; - if (g_last_obs_slot_idx >= 0 && g_last_obs_slot_env && - g_last_obs_slot_idx < g_last_obs_slot_env->obs_cap) { - const ObserverSlot *s = &g_last_obs_slot_env->obs[g_last_obs_slot_idx]; + const ObserverSlot *s = env_obs_slot(g_last_obs_slot_env, g_last_obs_slot_idx); + if (s) { switch (kind) { case 0: result = observer_slot_converged(s); break; case 1: result = observer_slot_stable(s); break; @@ -4859,8 +4860,9 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume) { uint16_t slot = read_u16(ip); ip += 2; Env *e = frame->fn_env; int result = 0; - if (e && (int)slot < e->obs_cap && e->obs[slot].used) - result = vm_slot_predicate(&e->obs[slot], kind); + const ObserverSlot *ps_l = env_obs_slot(e, (int)slot); + if (ps_l && ps_l->used) + result = vm_slot_predicate(ps_l, kind); vm_push(make_num(result ? 1.0 : 0.0)); DISPATCH(); } @@ -4881,8 +4883,9 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume) { DISPATCH(); } int result = 0; - if (oidx >= 0 && oidx < oe->obs_cap && oe->obs[oidx].used) - result = vm_slot_predicate(&oe->obs[oidx], kind); + const ObserverSlot *ps_n = env_obs_slot(oe, oidx); + if (ps_n && ps_n->used) + result = vm_slot_predicate(ps_n, kind); vm_push(make_num(result ? 1.0 : 0.0)); DISPATCH(); } diff --git a/tests/test_obs_mt_race.eigs b/tests/test_obs_mt_race.eigs new file mode 100644 index 00000000..8fa797a2 --- /dev/null +++ b/tests/test_obs_mt_race.eigs @@ -0,0 +1,633 @@ +# #694: MT race regression — module-env observer-slot array growth vs +# unlocked worker reads. Workers spin on `report of` a MODULE binding (which +# chain-resolves into the module env and reads its obs array with no lock), +# while the main thread first-observes NEW module bindings, growing that same +# obs array. Pre-fix the grow was a bare realloc + non-atomic pointer store, +# so the workers walked a freed block: TSan-visible use-after-free. +# Run under: make tsan && setarch -R src/eigenscript tests/test_obs_mt_race.eigs + +shared is 1 + +define reader() as: + local seen is 0 + for i in range of 4000: + r is report of shared + seen is seen + 1 + return seen + +h1 is spawn of reader +h2 is spawn of reader +h3 is spawn of reader + +# Each of these is a FIRST observation of a new module binding, so the obs +# array grows (idx+8 headroom => a realloc every 8 bindings) while the three +# workers above are mid-flight reading it. +b0 is 0 +b1 is 1 +b2 is 2 +b3 is 3 +b4 is 4 +b5 is 5 +b6 is 6 +b7 is 7 +b8 is 8 +b9 is 9 +b10 is 10 +b11 is 11 +b12 is 12 +b13 is 13 +b14 is 14 +b15 is 15 +b16 is 16 +b17 is 17 +b18 is 18 +b19 is 19 +b20 is 20 +b21 is 21 +b22 is 22 +b23 is 23 +b24 is 24 +b25 is 25 +b26 is 26 +b27 is 27 +b28 is 28 +b29 is 29 +b30 is 30 +b31 is 31 +b32 is 32 +b33 is 33 +b34 is 34 +b35 is 35 +b36 is 36 +b37 is 37 +b38 is 38 +b39 is 39 +b40 is 40 +b41 is 41 +b42 is 42 +b43 is 43 +b44 is 44 +b45 is 45 +b46 is 46 +b47 is 47 +b48 is 48 +b49 is 49 +b50 is 50 +b51 is 51 +b52 is 52 +b53 is 53 +b54 is 54 +b55 is 55 +b56 is 56 +b57 is 57 +b58 is 58 +b59 is 59 +b60 is 60 +b61 is 61 +b62 is 62 +b63 is 63 +b64 is 64 +b65 is 65 +b66 is 66 +b67 is 67 +b68 is 68 +b69 is 69 +b70 is 70 +b71 is 71 +b72 is 72 +b73 is 73 +b74 is 74 +b75 is 75 +b76 is 76 +b77 is 77 +b78 is 78 +b79 is 79 +b80 is 80 +b81 is 81 +b82 is 82 +b83 is 83 +b84 is 84 +b85 is 85 +b86 is 86 +b87 is 87 +b88 is 88 +b89 is 89 +b90 is 90 +b91 is 91 +b92 is 92 +b93 is 93 +b94 is 94 +b95 is 95 +b96 is 96 +b97 is 97 +b98 is 98 +b99 is 99 +b100 is 100 +b101 is 101 +b102 is 102 +b103 is 103 +b104 is 104 +b105 is 105 +b106 is 106 +b107 is 107 +b108 is 108 +b109 is 109 +b110 is 110 +b111 is 111 +b112 is 112 +b113 is 113 +b114 is 114 +b115 is 115 +b116 is 116 +b117 is 117 +b118 is 118 +b119 is 119 +b120 is 120 +b121 is 121 +b122 is 122 +b123 is 123 +b124 is 124 +b125 is 125 +b126 is 126 +b127 is 127 +b128 is 128 +b129 is 129 +b130 is 130 +b131 is 131 +b132 is 132 +b133 is 133 +b134 is 134 +b135 is 135 +b136 is 136 +b137 is 137 +b138 is 138 +b139 is 139 +b140 is 140 +b141 is 141 +b142 is 142 +b143 is 143 +b144 is 144 +b145 is 145 +b146 is 146 +b147 is 147 +b148 is 148 +b149 is 149 +b150 is 150 +b151 is 151 +b152 is 152 +b153 is 153 +b154 is 154 +b155 is 155 +b156 is 156 +b157 is 157 +b158 is 158 +b159 is 159 +b160 is 160 +b161 is 161 +b162 is 162 +b163 is 163 +b164 is 164 +b165 is 165 +b166 is 166 +b167 is 167 +b168 is 168 +b169 is 169 +b170 is 170 +b171 is 171 +b172 is 172 +b173 is 173 +b174 is 174 +b175 is 175 +b176 is 176 +b177 is 177 +b178 is 178 +b179 is 179 +b180 is 180 +b181 is 181 +b182 is 182 +b183 is 183 +b184 is 184 +b185 is 185 +b186 is 186 +b187 is 187 +b188 is 188 +b189 is 189 +b190 is 190 +b191 is 191 +b192 is 192 +b193 is 193 +b194 is 194 +b195 is 195 +b196 is 196 +b197 is 197 +b198 is 198 +b199 is 199 +b200 is 200 +b201 is 201 +b202 is 202 +b203 is 203 +b204 is 204 +b205 is 205 +b206 is 206 +b207 is 207 +b208 is 208 +b209 is 209 +b210 is 210 +b211 is 211 +b212 is 212 +b213 is 213 +b214 is 214 +b215 is 215 +b216 is 216 +b217 is 217 +b218 is 218 +b219 is 219 +b220 is 220 +b221 is 221 +b222 is 222 +b223 is 223 +b224 is 224 +b225 is 225 +b226 is 226 +b227 is 227 +b228 is 228 +b229 is 229 +b230 is 230 +b231 is 231 +b232 is 232 +b233 is 233 +b234 is 234 +b235 is 235 +b236 is 236 +b237 is 237 +b238 is 238 +b239 is 239 +b240 is 240 +b241 is 241 +b242 is 242 +b243 is 243 +b244 is 244 +b245 is 245 +b246 is 246 +b247 is 247 +b248 is 248 +b249 is 249 +b250 is 250 +b251 is 251 +b252 is 252 +b253 is 253 +b254 is 254 +b255 is 255 +b256 is 256 +b257 is 257 +b258 is 258 +b259 is 259 +b260 is 260 +b261 is 261 +b262 is 262 +b263 is 263 +b264 is 264 +b265 is 265 +b266 is 266 +b267 is 267 +b268 is 268 +b269 is 269 +b270 is 270 +b271 is 271 +b272 is 272 +b273 is 273 +b274 is 274 +b275 is 275 +b276 is 276 +b277 is 277 +b278 is 278 +b279 is 279 +b280 is 280 +b281 is 281 +b282 is 282 +b283 is 283 +b284 is 284 +b285 is 285 +b286 is 286 +b287 is 287 +b288 is 288 +b289 is 289 +b290 is 290 +b291 is 291 +b292 is 292 +b293 is 293 +b294 is 294 +b295 is 295 +b296 is 296 +b297 is 297 +b298 is 298 +b299 is 299 +b300 is 300 +b301 is 301 +b302 is 302 +b303 is 303 +b304 is 304 +b305 is 305 +b306 is 306 +b307 is 307 +b308 is 308 +b309 is 309 +b310 is 310 +b311 is 311 +b312 is 312 +b313 is 313 +b314 is 314 +b315 is 315 +b316 is 316 +b317 is 317 +b318 is 318 +b319 is 319 +b320 is 320 +b321 is 321 +b322 is 322 +b323 is 323 +b324 is 324 +b325 is 325 +b326 is 326 +b327 is 327 +b328 is 328 +b329 is 329 +b330 is 330 +b331 is 331 +b332 is 332 +b333 is 333 +b334 is 334 +b335 is 335 +b336 is 336 +b337 is 337 +b338 is 338 +b339 is 339 +b340 is 340 +b341 is 341 +b342 is 342 +b343 is 343 +b344 is 344 +b345 is 345 +b346 is 346 +b347 is 347 +b348 is 348 +b349 is 349 +b350 is 350 +b351 is 351 +b352 is 352 +b353 is 353 +b354 is 354 +b355 is 355 +b356 is 356 +b357 is 357 +b358 is 358 +b359 is 359 +b360 is 360 +b361 is 361 +b362 is 362 +b363 is 363 +b364 is 364 +b365 is 365 +b366 is 366 +b367 is 367 +b368 is 368 +b369 is 369 +b370 is 370 +b371 is 371 +b372 is 372 +b373 is 373 +b374 is 374 +b375 is 375 +b376 is 376 +b377 is 377 +b378 is 378 +b379 is 379 +b380 is 380 +b381 is 381 +b382 is 382 +b383 is 383 +b384 is 384 +b385 is 385 +b386 is 386 +b387 is 387 +b388 is 388 +b389 is 389 +b390 is 390 +b391 is 391 +b392 is 392 +b393 is 393 +b394 is 394 +b395 is 395 +b396 is 396 +b397 is 397 +b398 is 398 +b399 is 399 +b400 is 400 +b401 is 401 +b402 is 402 +b403 is 403 +b404 is 404 +b405 is 405 +b406 is 406 +b407 is 407 +b408 is 408 +b409 is 409 +b410 is 410 +b411 is 411 +b412 is 412 +b413 is 413 +b414 is 414 +b415 is 415 +b416 is 416 +b417 is 417 +b418 is 418 +b419 is 419 +b420 is 420 +b421 is 421 +b422 is 422 +b423 is 423 +b424 is 424 +b425 is 425 +b426 is 426 +b427 is 427 +b428 is 428 +b429 is 429 +b430 is 430 +b431 is 431 +b432 is 432 +b433 is 433 +b434 is 434 +b435 is 435 +b436 is 436 +b437 is 437 +b438 is 438 +b439 is 439 +b440 is 440 +b441 is 441 +b442 is 442 +b443 is 443 +b444 is 444 +b445 is 445 +b446 is 446 +b447 is 447 +b448 is 448 +b449 is 449 +b450 is 450 +b451 is 451 +b452 is 452 +b453 is 453 +b454 is 454 +b455 is 455 +b456 is 456 +b457 is 457 +b458 is 458 +b459 is 459 +b460 is 460 +b461 is 461 +b462 is 462 +b463 is 463 +b464 is 464 +b465 is 465 +b466 is 466 +b467 is 467 +b468 is 468 +b469 is 469 +b470 is 470 +b471 is 471 +b472 is 472 +b473 is 473 +b474 is 474 +b475 is 475 +b476 is 476 +b477 is 477 +b478 is 478 +b479 is 479 +b480 is 480 +b481 is 481 +b482 is 482 +b483 is 483 +b484 is 484 +b485 is 485 +b486 is 486 +b487 is 487 +b488 is 488 +b489 is 489 +b490 is 490 +b491 is 491 +b492 is 492 +b493 is 493 +b494 is 494 +b495 is 495 +b496 is 496 +b497 is 497 +b498 is 498 +b499 is 499 +b500 is 500 +b501 is 501 +b502 is 502 +b503 is 503 +b504 is 504 +b505 is 505 +b506 is 506 +b507 is 507 +b508 is 508 +b509 is 509 +b510 is 510 +b511 is 511 +b512 is 512 +b513 is 513 +b514 is 514 +b515 is 515 +b516 is 516 +b517 is 517 +b518 is 518 +b519 is 519 +b520 is 520 +b521 is 521 +b522 is 522 +b523 is 523 +b524 is 524 +b525 is 525 +b526 is 526 +b527 is 527 +b528 is 528 +b529 is 529 +b530 is 530 +b531 is 531 +b532 is 532 +b533 is 533 +b534 is 534 +b535 is 535 +b536 is 536 +b537 is 537 +b538 is 538 +b539 is 539 +b540 is 540 +b541 is 541 +b542 is 542 +b543 is 543 +b544 is 544 +b545 is 545 +b546 is 546 +b547 is 547 +b548 is 548 +b549 is 549 +b550 is 550 +b551 is 551 +b552 is 552 +b553 is 553 +b554 is 554 +b555 is 555 +b556 is 556 +b557 is 557 +b558 is 558 +b559 is 559 +b560 is 560 +b561 is 561 +b562 is 562 +b563 is 563 +b564 is 564 +b565 is 565 +b566 is 566 +b567 is 567 +b568 is 568 +b569 is 569 +b570 is 570 +b571 is 571 +b572 is 572 +b573 is 573 +b574 is 574 +b575 is 575 +b576 is 576 +b577 is 577 +b578 is 578 +b579 is 579 +b580 is 580 +b581 is 581 +b582 is 582 +b583 is 583 +b584 is 584 +b585 is 585 +b586 is 586 +b587 is 587 +b588 is 588 +b589 is 589 +b590 is 590 +b591 is 591 +b592 is 592 +b593 is 593 +b594 is 594 +b595 is 595 +b596 is 596 +b597 is 597 +b598 is 598 +b599 is 599 + +t1 is thread_join of h1 +t2 is thread_join of h2 +t3 is thread_join of h3 + +if t1 == 4000 and t2 == 4000 and t3 == 4000 and b599 == 599: + print of "PASS: observer-slot MT growth race (#694)" +else: + print of "FAIL: observer-slot MT growth race (#694)" diff --git a/tests/test_tsan.sh b/tests/test_tsan.sh index fd4e3ddd..fa6bd2b8 100755 --- a/tests/test_tsan.sh +++ b/tests/test_tsan.sh @@ -33,7 +33,7 @@ tsan_warnings() { echo "=== concurrency slice must be race-free ===" SLICE="test_concurrent test_spawn_parallel test_chan_dict_xthread test_spawn_gc \ test_channel_nb test_spawn_channel_exit test_spawn_args \ - test_spawn_arena_return test_spawn_jit" + test_spawn_arena_return test_spawn_jit test_obs_mt_race" for t in $SLICE; do f="$TESTS_DIR/$t.eigs" [ -f "$f" ] || continue