Repository navigation
lib: reject mutations of the container being sorted - #475
Merged
Merged
Conversation
Collaborator
|
Good find, this actually solves an issue reported independently already for objects. The proposed solution there was to snapshot the object's keylist before iteration which I wasn't entirely happy about since I feel a heap allocated copy of an object's key list on every trivial iteration does not fit the design goals of ucode well - so the proposed constifying of the iterated container feels more elegant. |
jow-
reviewed
Oct 8, 2026
A sort() comparator, or a __tostring__ metamethod invoked by the default
comparator, runs script code while qsort() operates on the array entry
storage. Growing the array from there reallocates the entries behind
qsort()'s back and shrinking it leaves qsort() working on stale slots,
both leading to use-after-free and crashes. Sorting objects is affected
the same way since the hash table entry pointers are kept across the
comparator calls, which adding or deleting keys invalidates.
Mark the container immutable for the duration of the sort by reusing the
constant flag: all script-level mutation paths already raise a type
error for constant values, so push(), pop(), shift(), unshift(),
splice(), index assignments, delete and nested sort() calls on the
container being sorted now fail with "array value is immutable" instead
of corrupting memory. The flag is cleared again once qsort() returns,
also when the comparator raised an exception.
Extend the constant check in types.c to the array mutators which did
not honour it yet, so that the lock is complete at the C level as well.
As a consequence, ucv_array_set(), ucv_array_pop(), ucv_array_shift(),
ucv_array_unshift() and ucv_array_delete() now refuse constant arrays
like ucv_array_push() and ucv_object_add() already did, and the
ucv_array_sort*() and ucv_object_sort*() functions become no-ops for
constant containers.
Also check for a pending exception after each string conversion in the
default comparator, since a failing __tostring__ metamethod previously
led to comparing a NULL string, and skip the default comparator as well
once an exception is pending.
Fixes `ucode -p 'a=[3,2,1]; sort(a, (x, y) => { for (let i = 0; i < 1000; i++) push(a, i); return x - y; })'`
crashing.
Signed-off-by: darekhta <dmitri.arekhta@me.com>
darekhta
force-pushed
the
fix/sort-callback-mutation
branch
from
October 9, 2026 07:29
4dcff62 to
4711d3c
Compare
Author
|
Thanks for the review! Replaced all direct |
Collaborator
|
Merged, thanks! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A sort() comparator, or a tostring metamethod invoked by the default comparator, runs script code while qsort() operates on the array entry storage. Growing the array from there reallocates the entries behind qsort()'s back and shrinking it leaves qsort() working on stale slots, both leading to use-after-free and crashes. Sorting objects is affected the same way since the hash table entry pointers are kept across the comparator calls, which adding or deleting keys invalidates.
Mark the container immutable for the duration of the sort by reusing the constant flag: all script-level mutation paths already raise a type error for constant values, so push(), pop(), shift(), unshift(), splice(), index assignments, delete and nested sort() calls on the container being sorted now fail with "array value is immutable" instead of corrupting memory. The flag is cleared again once qsort() returns, also when the comparator raised an exception.
Extend the constant check in types.c to the array mutators which did not honour it yet, so that the lock is complete at the C level as well. As a consequence, ucv_array_set(), ucv_array_pop(), ucv_array_shift(), ucv_array_unshift() and ucv_array_delete() now refuse constant arrays like ucv_array_push() and ucv_object_add() already did, and the ucv_array_sort*() and ucv_object_sort*() functions become no-ops for constant containers.
Also check for a pending exception after each string conversion in the default comparator, since a failing tostring metamethod previously led to comparing a NULL string, and skip the default comparator as well once an exception is pending.
Fixes
ucode -p 'a=[3,2,1]; sort(a, (x, y) => { for (let i = 0; i < 1000; i++) push(a, i); return x - y; })'crashing.