While running Boa’s Test262, the test occasionally failed with exit code 134.
The logs show the following errors:
malloc(): unsorted double linked list corrupted
The issue is the “normal Test262 release run is aborted due to an allocator consistency error” (issue #638).
At first, I assumed some recent change was corrupting memory somewhere. However, because Test262 runs a large number of tests in parallel, the logs alone don’t reveal which specific test is causing the problem.
First, compare the before and after the change
The first thing I did was compare the state before and after the change.
I built release versions of Boa from the revision before the change and the revision where the issue was observed, then ran Test262 under the same conditions.
Ubuntu x86_64
release build
2 MiB worker stack
4 workers
Under these conditions, both the pre‑change and post‑change runs were able to execute all 50,595 cases to completion.
47,606 passed
2,056 ignored
933 failed
0 panic
The abnormal termination of the allocator could not be reproduced either.
Since the SHA256 hashes of the executables also matched, at least from this execution result we can’t say the issue was caused by some other change.
However, in the problematic CI run, the exit code 134 occurred twice in a row. Just because a rerun succeeded doesn’t mean the problem has been resolved.
Narrow down to a small case
Looking at the logs, just before the crash the following suite was being executed:
GeneratorFunction/prototype- an asynchronous private method of a class
Both are related to Promises and async processing.
So, instead of repeatedly running the entire Test262, I created a small test that isolates the condition under which the garbage collector runs during Promise resolution.
The problem arose from code that, while a Promise was being resolved, extracted resolve and reject and then called the then getter. User code can execute during the getter’s execution, which also triggers garbage collection.
The reference to the Promise had disappeared
In the Promise handling, resolve and reject are taken from the internal shared capture.
At that point, the location that kept the Promise as a trackable reference was also lost.
Usually this doesn’t cause an immediate problem. Because the operation is short and garbage collection doesn’t run, the memory remains intact and the code proceeds.
However, if a then getter or a host hook is called afterward, user code executes. If garbage collection runs during that interval, the Promise may be deemed an unnecessary object and be reclaimed.
In that state, when later code tries to use the Promise, it will end up referencing freed memory.
What ultimately appeared was an error indicating that the allocator’s internal data structure had been corrupted.
malloc(): unsorted double linked list corrupted
Since the error originated from malloc, it initially looked like something was directly corrupting the heap. In reality, the root cause was a mismatch between garbage‑collection reclamation and the retention of references.
Reproducing with a regression test
By adding only the regression test to the revision before the fix and running it, we were able to reproduce the SIGSEGV on ARM64 Linux.
This test includes the following conditions:
thengetter returns a callablethengetter returns a non‑callable valuethengetter throws an exception- Re‑entering during Promise resolution to call
resolveorreject
Since simply resolving a Promise does not cause a problem, it was necessary to combine the getter call with points where garbage collection could occur.
Keep the Promise as a root during processing
In Boa PR #86 (PR #86), we modified the code to retain the Promise as a root while it is being processed.
Even after extracting resolve and reject from the shared capture, we ensure that the Promise remains trackable until the then getter, host hook, and job creation have completed.
Conceptually, the difference looks like this:
Before fix:
Extract resolve/reject from the Promise
-> The Promise is no longer retained
-> Garbage collection occurs during getter execution
After fix:
Extract resolve/reject from the Promise
-> Keep it root‑referenced while processing
-> Getter and job creation finish
-> Release the root reference
This change guarantees that the objects needed during processing stay alive regardless of when garbage collection runs.
Verification after the fix
After the fix, the regression test passed, as well as the engine tests, the build, strict Clippy checks, and format verification.
In addition, we compared Test262 before and after the change. Both the release build and the ASAN build after the fix completed all 50,595 cases with no panics.
The ASAN build after the fix also did not produce any memory errors.
ASAN: 47,605 passed / 2,056 ignored / 934 failed / 0 panic
release:47,606 passed / 2,056 ignored / 933 failed / 0 panic
There is a one‑case discrepancy between the failure counts of ASAN and the release build, but that is due to an existing conformance failure. This does not mean that all Test262 failures have been eliminated by this fix.
We have verified not only the overall execution results but also that the regression test that failed before the fix now passes, and that no memory errors appear in ASAN.
Summary
The issue manifested as an occasional allocator error that occurred during the parallel execution of Test262.
A full comparison before and after the change did not reproduce the problem, but by narrowing the focus from the logs to the Promise resolution phase and using a small test that includes the then getter—where garbage collection runs—we were able to reproduce the SIGSEGV.
The root cause was that after extracting resolve and reject from the shared capture, the Promise was not retained until the subsequent processing completed.
The fix itself involves keeping the Promise as a root while it is being processed.
Even though the error looked like a memory corruption, it was actually a boundary issue with garbage collection concerning how long the necessary objects should be kept as roots.
Problems of this kind are hard to detect by simply rerunning normal tests many times, so it was necessary to shrink the reproduction scenario and create a test that fails on the pre‑fix revision.