Skip to content

lib: reject mutations of the container being sorted - #475

Merged
jow- merged 1 commit into
ucode-lang:masterfrom
darekhta:fix/sort-callback-mutation
Oct 9, 2026
Merged

jow- merged 1 commit into
ucode-lang:masterfrom
darekhta:fix/sort-callback-mutation

Conversation

@darekhta

@darekhta darekhta commented Oct 8, 2026 •

Copy link
Copy Markdown

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.

@jow-

jow- commented Oct 8, 2026 •

Copy link
Copy Markdown
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.

Comment thread types.c Outdated
Comment thread types.c Outdated
Comment thread types.c Outdated
Comment thread types.c Outdated
Comment thread types.c Outdated
Comment thread types.c Outdated
Comment thread types.c Outdated
Comment thread types.c Outdated
Comment thread types.c Outdated
Comment thread types.c Outdated
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
darekhta force-pushed the fix/sort-callback-mutation branch from 4dcff62 to 4711d3c Compare October 9, 2026 07:29
@darekhta

darekhta commented Oct 9, 2026

Copy link
Copy Markdown
Author

Thanks for the review! Replaced all direct ext_flag accesses with ucv_is_constant() / ucv_set_constant() and force-pushed the updated commit.

@jow-
jow- merged commit cc29153 into ucode-lang:master Oct 9, 2026
2 checks passed
@jow-

jow- commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

Merged, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants