Loading
Commits on Source 15
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
___lock/___unlock emulated a C mutex using an atomic lock-word fast path plus a throwaway hand-off object stored in a map. When an unlocker took locksMu before a contending locker had registered its hand-off, ___unlock synthesized one and immediately discarded it; the waiter then blocked on a fresh, never-unlocked mutex forever, and the lock word was left non-zero, so every subsequent ___lock on that address also blocked. The process wedged permanently at zero CPU. The window is reachable through the exported API alone: localtime, localtime_r and mktime all take the single process-global timezone lock (_lock4) via __secs_to_zone on every call, so goroutines calling them concurrently can hit it. Reported against modernc.org/quickjs evaluating JS Date local-time getters. Replace the scheme with a per-address sync.Mutex, created lazily and reference-counted for cleanup. The opaque C lock word is no longer touched (nothing reads it outside these two functions); routing all mutual exclusion through the per-address mutex makes the hand-off race-free, so a release delivered before the waiter blocks is no longer lost. Add TestIssue51: a mutual-exclusion stress on ___lock/___unlock plus a concurrent localtime_r stress; both clean under -race. Fixes: #51 Co-Authored-By:
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
-
cznic authored
-
cznic authored
-
cznic authored
The failure path re-read *ptr in a separate step instead of reporting the value observed during the failed comparison. C11 7.17.7.4 puts that update inside the atomic read-modify-write ("Atomically, compares ... and if false, updates the value in expected with the value pointed to by object"), and the hardware does exactly that: cmpxchg loads the actual value in the same instruction. If *ptr transiently reverted to the expected value inside the window between the failed CAS and the separate reload, the helper returned failure while writing the expected value back into *expected. Callers spinning on while (atomic_compare_exchange_strong(&lock, &zero, 1) != 0) ; then left the loop without having acquired anything, because that idiom exits precisely when the reported old value equals the expected one. Restore the invariant that a failure reports a witness != old by retrying while the observed value is still old. A strong CAS that failed against old means *p != old at that instant, so a witness equal to old is never a valid failure report; and if the value did revert, a cmpxchg issued at that moment would have succeeded, making the retry more faithful than the bogus report. Affects the lock-free Int32/Uint32/Int64/Uint64 helpers. The Int8/Int16 and __c11_atomic_compare_exchange_strong* variants read the witness under the same mutex that guards the compare and were already correct. Found via modernc.org/libquickjs CI builders wedging at 100% CPU for 19+ hours in test262's Atomics agent tests, which use exactly that spin idiom. Hang rates on a 2-CPU linux/386 box, 200 runs of a single test: 20/200 (BigInt64Array) and 16/200 (Int32Array) before, 0/200 after. TestIssue52/failure_witness fails on the old code (order 10^3 violations per 2000000 attempts on amd64, 19028/2000000 on 386) and passes on the new. Only the contended case can observe this: TestIssue52/uncontended_semantics passes even on the broken code, which is why it went unnoticed. Closes #52 Co-Authored-By:Claude Opus 4.8 (1M context) <noreply@anthropic.com>
-
cznic authored
-
cznic authored
getdirentries(2) reports records of deleted files with d_fileno set to zero and readdir(3) must not return them. On FFS such a record survives an unlink whenever it is the first one in a directory block, so any directory larger than a single 512-byte block reported ghost entries for files that were already gone. That made Tcl's glob (via libtcl8.6) see deleted files, causing the modernc.org/libsqlite3 openbsd/amd64 test failures delete_db-1.3.1, delete_db-1.4.1, multiplex-2.4.5, multiplex-5.3.prep and quota-5.3.prep. The failures only showed up in full-suite runs, once the shared working directory had grown past one block. Also stop iterating on a malformed d_reclen rather than spinning on it, matching the checks in OpenBSD's _readdir_unlocked(). Co-Authored-By:Claude Opus 4.8 (1M context) <noreply@anthropic.com>
-
cznic authored
VaList takes anything outside int/uint/int32/.../float64/uintptr through a default case that memcpy's reflect.TypeOf(v).Size() bytes of the value. For a Go string that copies the 16 byte {ptr, len} header into the va_list. C reads the first word as a char* and, Go strings not being NUL terminated, vsnprintf runs past the end of the string, appending whatever sits next to it on the Go heap. The header is also wider than the 8 bytes a caller sizes a va_list slot at, so the write itself overruns the allocation. That is how modernc.org/quickjs leaked Go runtime internals into a script-visible Error.message, see quickjs#14. C has no string type, so ccgo never emits one here; every occurrence is hand written and always a bug. Panic instead of corrupting silently, in both the ccgo/v3 (etc.go) and ccgo/v4 (rtl.go) copies. The default case stays as it is, that is what carries a C struct or array passed by value. Callers must pass CString(s). Note that a modernc.org/quickjs older than v0.22.0 trips this panic on any host function error. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
cznic authored
852ba3a8 moved musl lock state out of the C lock word *p and into a process-global map keyed on the word's address. musl's freeaddrinfo deliberately frees the buffer holding its lock word while still holding that lock and never unlocks: LOCK(b->lock); if (!(b->ref -= cnt)) free(b); else UNLOCK(b->lock); That is correct in C, because the word is inside the block and dies with it. With the state in a global map the entry outlived the free, still locked, so when the allocator handed the same address to a later getaddrinfo the next freeaddrinfo blocked forever in ___lock at zero CPU, with no error. Deterministic on linux/386 and linux/arm, where the allocator recycles the address; latent on linux/amd64, which escapes on allocator behaviour rather than by design. Reported as libtcl9.0's 386 and arm builders no longer completing test runs. Put all lock state back in *p, as musl does: 0 free, 1 held, 2 held with a waiter possibly parked, driven by a_cas/swap. Freeing the memory then discards the state exactly as C expects, and a recycled calloc'd block starts out unlocked. Blocking uses a parking lot keyed by address in place of musl's futex: lockWait re-checks *p under lockParkMu and ___unlock stores to *p before lockWake takes that mutex, so a release landing before the waiter parks is seen as a value change rather than lost, which is what made #51 reachable. Parking lot entries exist only while a goroutine is really parked, so an abandoned lock leaves nothing behind and the hazard is gone generically, not just for freeaddrinfo. The global mutex is off the fast path, and an unbalanced ___unlock is now inert instead of a nil dereference. Add TestMuslLockLifetime: a recycled lock word, parking lot reclamation over the uncontended, contended and abandoned paths, an unbalanced unlock, and a bounded getaddrinfo/freeaddrinfo loop. TestIssue51 keeps its lock word in a package-level Go var; the race detector ignores atomics outside the Go arena and data segments, so a word in libc-allocated memory would carry no happens-before edge to the Go state a critical section guards. The real static musl lock words are package-level [1]int32, so this also matches them. Verified on linux/386 and linux/arm, where the reproducer completes and the unfixed ___lock hangs on the second iteration: go test -race, TestLibc (473 files, 0 build and 0 exec failures), build_all_targets across all targets and tags, and modernc.org/libtcl9.0 both for httpProxy.test and for its full suite (170 test files, 68391 assertions, 0 failed on each). Retract v1.74.2 and v1.74.3, which carry the defect. Co-Authored-By:
Claude Opus 5 (1M context) <noreply@anthropic.com>