Judy

PHP Judy implements sparse dynamic arrays (aka Judy Arrays)


PHP Judy implements sparse dynamic arrays (aka Judy Arrays). This extension is based
on the Judy C library. A Judy array consumes memory only when it is populated,
yet can grow to take advantage of all available memory if desired. Judy's key benefits
are scalability, high performance, and memory efficiency.

License: PHP

Latest releases

Version Release Download
2.6.0 2026-08-19 Judy-2.6.0.tgz
2.5.2 2026-08-18 Judy-2.5.2.tgz
2.5.1 2026-08-18 Judy-2.5.1.tgz
2.5.0 2026-08-17 Judy-2.5.0.tgz
2.4.2 2026-08-14 Judy-2.4.2.tgz
Show all releases

Changelog

Version Message
2.6.0 - FIX: use-after-free during teardown of the MIXED types (INT_TO_MIXED,
STRING_TO_MIXED, STRING_TO_MIXED_HASH, STRING_TO_MIXED_ADAPTIVE), reachable
through both Judy::free() and ordinary object destruction (#162). Freeing a
slot calls zval_ptr_dtor(); where the value is a shared collectable one that
fills the GC root buffer, gc_collect_cycles() then runs synchronously inside
the free loop and re-enters judy_object_get_gc() on the half-destroyed
object. The usual symptom is a "zend_mm_heap corrupted" abort. This defect
PREDATES the bundled libJudy and is present in every previously shipped
release: anyone using a MIXED type should upgrade.
- FIX: libJudy compiled with aggressive loop optimization silently loses
Judy::BITSET keys — jp_1Index is 8 bytes where the code writes up to 15
(#131). Fixed in the bundled tree (patch P1) and guarded by a differential
fuzzer that re-proves itself on every CI run by planting the defect and
failing if the fuzzer does not catch it.
- FIX: further upstream libJudy 1.0.5 defects, fixed in the bundled tree
(#127): the SEARCH_LINEAR/COPYINDEX pair, which was a no-op masking silent
data loss; an off-by-one in InsArray; an out-of-scope read in Cascade; and
hygiene fixes (patches P2-P7). Every patch carries an entry in
libjudy/PATCHES.md and a per-file LGPL section 2(b) change notice.
- BUILD: the bundled, patched libJudy is now the DEFAULT build. ./configure
needs no system library and downloads nothing at build time, and the Windows
build no longer regex-patches library sources in CI. --with-judy=DIR still
links a system libJudy, is CI-tested on every push, and stays supported
indefinitely.
- BUILD: a 32-bit target is now refused with a clear message instead of being
mis-built, and the release matrix is constrained to x64 (#159, #160).
- PERF: integer-keyed paths gain hardware popcount (#149) and a byte-order fix
on the JudyL descend (#150); the string layer loses redundant work (#154).
Figures are in BENCHMARK.md and are deliberately not restated here. Note
that the delivered speedup is NOT one number: with linkage held constant
against an unpatched build of the same tree, about 96.5% of the gain on
integer paths is attributable to these patches but only about 40% of the
gain on string paths, the remainder being static-versus-shared linkage.
2.5.2 - PERF: mergeWith() no longer re-descends from the root for a key its own
cursor is already standing on. The slot-to-zval conversion is shared with the
descending read path so the two cannot drift, and the cursor's slot is reused
only where it genuinely holds the value: directly on the integer-keyed and
trie types, and on *_HASH / *_ADAPTIVE only when the payload is mirrored
(an optimizeIteration instance of STRING_TO_INT_HASH, or long-keyed
STRING_TO_INT_ADAPTIVE). Behaviour-preserving: the three new .phpt files pass
against the pre-change build too. **No performance number is claimed** — the
redundant descend is gone, but no benchmark accompanies the change and none
should be inferred pending a run on an idle host. baselines/latest.json and
BENCHMARK.md figures are untouched.
- DOC: API.md now documents that toArray() coerces integer-looking string keys
on string-keyed types ("42" comes back as int 42, while "07" and " 42" stay
strings) and that feeding such a key back as an offset throws. The warning
reached the stub and AGENTS.md in 2.5.1 but not API.md, which is the reference
composer.json advertises.
- DOC: BENCHMARK.md records why php-code-coverage and Infection were examined
and rejected as Judy fits.
2.5.1 - FIX: string keys containing an embedded NUL byte are now rejected with an
exception on all six string-keyed types, instead of being silently truncated
at the NUL. Previously STRING_TO_INT and STRING_TO_MIXED truncated the key, so
"ab\0cd" and "ab" collided and one value was destroyed with no signal; the
four *_HASH/*_ADAPTIVE types already rejected such keys on write but still
truncated them on every ordered and range operation (first/last/searchNext/
prev/slice/deleteRange and the bounds of keys/values/toArray/size), because
all six seek through the same JudySL key index. JudySL keys are NUL-terminated
by construction, so rejecting is the only correct behaviour. High-byte keys
(0x80-0xFF) are unaffected and remain binary-safe, including 0xFF prefix-carry
arithmetic. See MIGRATION_2.5.0.md.
2.5.0 - BC BREAK: a negative integer offset now stores that key instead of appending.
Integer keys are unsigned machine words, so $j[-1] = $v addresses the maximum
index and reads back as -1. Previously every key in [PHP_INT_MIN, -1] was
discarded and the value appended at the next free index, so isset($j[-1]) was
false immediately after the write. See MIGRATION_2.5.0.md.
- BC BREAK: $j[] = $v now throws when the maximum index is occupied, instead of
wrapping onto index 0 and overwriting it.
- FIX: $j[] = $v no longer loses a value after a negative-offset write left the
append watermark stale
- FIX: map()/filter() preserve negative keys instead of relocating them
- SECURITY: fix use-after-free write on *_TO_MIXED overwrite/unset when a stored
value's destructor re-enters and mutates the same array (write-before-dtor /
delete-before-free)
- SECURITY: fix type confusion (UB) in getAll()/next()/rewind() for adaptive
types, which queried a JudyHS operation against the JudyL (SSO) store
- FIX: add get_gc handler so reference cycles through MIXED values are collectable
(previously leaked until request shutdown)
- FIX: STRING_TO_*_ADAPTIVE counter no longer double-counts when the value 0 is
re-stored (size/count/equals/averageValues were affected)
- FIX: $j[] = append after clone/fromArray/putAll no longer overwrites index 0
- FIX: first()/last()/searchNext()/prev() now work on adaptive types
- FIX: fromArray()/putAll() reject non-integer keys on integer-keyed types instead
of inserting at the string's hash
- FIX: __unserialize() on a populated object frees prior contents (no leak)
- FIX: forEach()/filter()/map() callbacks may re-enter without corrupting iteration
- FIX: equals() on INT_TO_PACKED no longer risks an infinite loop
- FIX: allocation failure (JERR) during write/unset is reported as failure, not success
- FIX: bulk operations stop on the first thrown key instead of continuing with a
pending exception; clone/slice no longer leak zvals or diverge on OOM paths
- FEATURE: keys(), values() and toArray() take an inclusive [$start, $end] key
range, where null leaves that side unbounded. All key types; string-keyed types
require string bounds and compare them lexicographically. A bounded read is one
traversal writing straight into the PHP array — prefer it to slice($lo,
$hi)->keys(), which copies a whole sub-array first.
- FEATURE: size($start, $end) counts that same range, including on the six
string-keyed types, without materialising anything. Previously it accepted
string bounds, ignored them, and returned the whole-array count. Its parameters
were renamed $index_start/$index_end -> $start/$end to match the other range
methods, which breaks named-argument callers only; its defaults moved from
(0, -1) to (null, null). populationCount() is unchanged and stays
integer-keyed-only — it answers from libJudy's O(1) population cache, which the
string-keyed stores lack. See MIGRATION_2.5.0.md.
- FEATURE: new Judy($type, optimizeIteration: true) mirrors payloads into the key
index for 24-47% faster ordered reads, at a write-path and memory cost. Opt-in,
per-instance, off by default, and honoured only by STRING_TO_INT_HASH and
STRING_TO_INT_ADAPTIVE; isIterationOptimized() reports what took effect.
- FEATURE: Judy instances are now legible to debuggers — var_dump()/print_r()
show type, count, memory usage, first/last key and a bounded element preview
(judy.debug_preview_size). Ships lldb/gdb pretty-printers for the extension's
own structs under scripts/.
- FEATURE: set operations (intersect/diff/xor) now supported for STRING_TO_INT_ADAPTIVE
- BUILD: extension compiles warning-free; CI now fails on any new compiler warning
- BUILD: minimum PHP raised to 8.1 (PHP 8.0 is no longer tested in CI)
2.4.2 - SECURITY: fix use-after-free write on *_TO_MIXED overwrite/unset when a stored
value's destructor re-enters and mutates the same array (write-before-dtor /
delete-before-free)
- SECURITY: fix type confusion (UB) in getAll()/next()/rewind() for adaptive
types, which queried a JudyHS operation against the JudyL (SSO) store
- FIX: add get_gc handler so reference cycles through MIXED values are collectable
(previously leaked until request shutdown)
- FIX: STRING_TO_*_ADAPTIVE counter no longer double-counts when the value 0 is
re-stored (size/count/equals/averageValues were affected)
- FIX: $j[] = append after clone/fromArray/putAll no longer overwrites index 0
- FIX: first()/last()/searchNext()/prev() now work on adaptive types
- FIX: fromArray()/putAll() reject non-integer keys on integer-keyed types instead
of inserting at the string's hash
- FIX: __unserialize() on a populated object frees prior contents (no leak)
- FIX: forEach()/filter()/map() callbacks may re-enter without corrupting iteration
- FIX: equals() on INT_TO_PACKED no longer risks an infinite loop
- FIX: allocation failure (JERR) during write/unset is reported as failure, not success
- FIX: bulk operations stop on the first thrown key instead of continuing with a
pending exception; clone/slice no longer leak zvals or diverge on OOM paths
- FEATURE: set operations (intersect/diff/xor) now supported for STRING_TO_INT_ADAPTIVE
- BUILD: extension compiles warning-free; CI now fails on any new compiler warning
- BUILD: minimum PHP raised to 8.1 (PHP 8.0 is no longer tested in CI)
Show complete changelog