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
| 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 |
| 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) |