Releases: orieg/php-judy
Release list
v2.7.1
Release 2.7.1
- SECURITY FIX: Guard against destructor re-entrancy and heap Use-After-Free (UAF) in
Judy::STRING_TO_ENTRYduring ArrayAccess overwrites,Judy::set(), andJudy::pruneExpired()sweeps (#198). - Commits new slot payload prior to invoking
zval_ptr_dtor(), and unlinks keys from the JudySL trie before freeing entry structs. - Bumped
PHP_JUDY_VERSIONandpackage.xmlto2.7.1.
v2.7.0
PHP Judy v2.7.0
- Native Cache & TTL Storage Engine: Added
Judy::STRING_TO_ENTRY(type 11). Stores values with TTL timestamps (uint32) and 16-bit metadata flags (uint16_t) directly packed into native C struct entries (judy_cache_entry_t) in the JudySL radix trie without secondary PHP array wrappers or lookup indexes. - Native Cache Entry Methods:
set(),get(),pruneExpired(),getEntry(),getExpiry(), andgetFlags().pruneExpired(?int $now = null)performs a single-pass trie sweep directly in C with 0 PHP heap allocations (12.9 ms per 100k items). - Bulk & Iteration: Full integration with
ArrayAccess,Iterator,keys(),values(),toArray(),slice(),getAll(), and serialization. - Expanse Integration: Added pure-Rust Expanse backend integration support (
--with-expanse) for modern 64-bit microarchitectures and Windows MSVC support.
v2.6.0
php-judy 2.6.0 bundles a patched libJudy by default and fixes a memory-safety defect present in every previously shipped release.
Upgrade note
If you use any MIXED type (INT_TO_MIXED, STRING_TO_MIXED, STRING_TO_MIXED_HASH, STRING_TO_MIXED_ADAPTIVE), upgrade. A use-after-free during teardown (#162) predates the vendoring work and is present in all earlier releases; its usual symptom is a zend_mm_heap corrupted abort.
Changes
- 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.
Building
./configure now needs no system libJudy and downloads nothing at build time. --with-judy=DIR still links a system library, is CI-tested on every push, and remains supported indefinitely. The bundled build is 64-bit only; a 32-bit target is refused with a clear message rather than mis-built.
The bundled library is Judy 1.0.5 (LGPL-2.1-or-later), modified. Every patch is recorded in libjudy/PATCHES.md with a per-file LGPL §2(b) change notice; see THIRD-PARTY.md.
Measurements
BENCHMARK.md carries the figures, including where php-judy loses — PHP arrays win most per-element scalar operations, and INT_TO_MIXED uses more memory than a PHP array. Full investigation record: research/libjudy-modernization/FINDINGS.md. Tracker: #142.
v2.5.2
php-judy 2.5.2 — one extension change and three documentation fixes. No API or behaviour change; nothing to migrate.
Changes
mergeWith() no longer re-descends from the root for a key its own cursor is already standing on. The slot-to-zval conversion is now 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 by construction: the three new .phpt files (slot reuse, re-entrancy, MIXED refcount) pass against the pre-change build as well.
No performance number is claimed. The redundant descend is gone, but no benchmark accompanies the change, and none should be inferred from it pending a run on an idle host. baselines/latest.json and BENCHMARK.md's figures are untouched.
Docs
API.mdnow documents thattoArray()coerces integer-looking string keys on string-keyed types —"42"returns asint 42, while"07"and" 42"stay strings — and that feeding such a key back as an offset throws. This warning reached the stub and AGENTS.md in 2.5.1 but neverAPI.md, which is the referencecomposer.jsonadvertises.- BENCHMARK.md records why
php-code-coverageand Infection were examined and rejected as Judy fits. research/write-probe-cost/probebench.cnow honourskeylen, which unblocks the ADAPTIVE/SSO probe that had never been runnable (#118). Not part of the shipped package.
Upgrading
Nothing required. MIGRATION_2.5.0.md remains the guide for the 2.5.x line — read it if you are coming from 2.4.x, or from 2.5.0 (§6 covers the embedded-NUL rejection added in 2.5.1).
Note on benchmarks
BENCHMARK.md's figures were measured on 2.4.2 and verified unchanged on 2.5.0 (0 regressions, run-wide median −0.04%). Two changes since then touch the write and merge paths — 2.5.1's NUL guard and this release's merge refactor — and neither has been benchmarked. The next baseline refresh is the first measurement of either.
v2.5.1
php-judy 2.5.1 — string keys containing an embedded NUL byte are now rejected instead of silently truncated.
Upgrading from 2.5.0: read §6 of MIGRATION_2.5.0.md. If you build keys from pack(), serialize(), or raw hash digests, they can contain a NUL and were already being corrupted.
Upgrading from 2.4.x: read the whole of MIGRATION_2.5.0.md — 2.5.0's negative-key and size() changes apply to you as well.
What changed
STRING_TO_INT and STRING_TO_MIXED truncated a string key at its first NUL byte, so two distinct keys collided and one value was destroyed with no signal:
$j = new Judy(Judy::STRING_TO_INT);
$j["ab\x00cd"] = 1;
$j["ab"] = 2;
$j->count(); // 1 <-- two keys written, one survived
$j["ab\x00cd"]; // 2 <-- reads back the value written under "ab"The four *_HASH / *_ADAPTIVE types already rejected such keys on write — but only on write. Every ordered and range operation still truncated, because all six types seek through the same JudySL key index:
$h->first("ab\x00cd"); // "ab" <-- wrong
$h->slice("ab\x00cd", "zz")->keys(); // ["ab","zz"] <-- includes "ab"All six types now throw on every path that takes a string key: offsetSet/Get/Exists/Unset, increment(), fromArray(), putAll(), getAll(), slice(), deleteRange(), first()/last()/searchNext()/prev(), and the bounds of keys()/values()/toArray()/size().
JudySL keys are NUL-terminated C strings by construction — which is exactly why JudyHS can hold arbitrary bytes and JudySL cannot — so rejecting is the only correct behaviour once truncation is off the table.
High-byte keys are unaffected. Keys containing 0x80–0xFF were always binary-safe and remain so: they store, round-trip, and sort in unsigned byte order, and the 0xFF prefix-successor carry arithmetic in examples/symbol-table-prefix.php is unchanged. Only 0x00 is rejected.
Why this is a patch release
It turns previously-passing calls into exceptions on the four hash/adaptive types. That is defensible at patch level only because those calls were already returning wrong answers — matching a different key after truncating at the NUL. Nothing correct is broken; existing silent data loss is being surfaced.
Also in this release
tests/boundary_binary_keys_001.phpt— binary-key coverage across all six string-keyed types, both the high-byte half (which must keep working) and the NUL half- Docs:
AGENTS.mdpitfalls,README.md, and the symbol-table example now qualify the binary-safety claim
Closes #117.
Note on benchmarks
BENCHMARK.md's figures were measured on 2.4.2 and verified unchanged on 2.5.0 (0 regressions, run-wide median −0.04%). The NUL guard adds a memchr to the write path that has not been benchmarked; the next baseline refresh will measure it.
v2.5.0
php-judy 2.5.0 — ranged reads and counts, opt-in iteration mirror, debugger support, and a negative-key BC break.
Upgrading: read MIGRATION_2.5.0.md. Two changes have no runtime signal — negative integer offsets on the write path (§1) and size() on string-keyed arrays (§3) — and one breaks named-argument callers of size() (§4).
Changes
- 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)
Notes
- Benchmark figures in BENCHMARK.md were measured on 2.4.2 and have not been re-run on 2.5.0; the ranged reads and the
optimizeIterationmirror are not represented there. A refreshed baseline follows separately.
v2.4.2
Hardening release folding in the security & correctness sprint.
Security
- Fix use-after-free write on
*_TO_MIXEDoverwrite/unset when a stored value's destructor re-enters and mutates the same array. - Fix type confusion (undefined behavior) in
getAll()/next()/rewind()for adaptive types.
Leaks
- Add
get_gcso reference cycles through MIXED values are collectable (previously leaked until request shutdown). - Fix
__unserialize()reinit leak and clone/slice OOM-path zval leaks.
Correctness
- Adaptive counter no longer double-counts when value 0 is re-stored.
$j[] =after clone/fromArray/putAll no longer overwrites index 0.first()/last()/searchNext()/prev()now work on adaptive types.fromArray()/putAll()reject non-integer keys on integer-keyed types.equals()on INT_TO_PACKED no longer risks an infinite loop.- Allocation failure (JERR) is reported as failure, not success.
- Bulk operations stop on the first thrown key;
forEach/filter/mapcallbacks may re-enter safely.
Feature
- Set operations (
intersect/diff/xor) now supported forSTRING_TO_INT_ADAPTIVE.
Build / packaging
- Extension compiles warning-free; CI fails on any new compiler warning.
- Minimum PHP raised to 8.1 (8.0 is no longer tested).
- Governance docs added (CONTRIBUTING, SECURITY, issue templates).
Full changelog in package.xml.
v2.4.1
v2.4.0
What's New
New Features
- Fast ZPP (Zend Parameter Parsing) macros across all methods for improved performance
- Native
zend_object_handlers(read/write/has/unset_dimension) bypass ArrayAccess overhead - Native C iterators via
get_iteratorfor zero-overheadforeachloops - BITSET set operations —
union(),intersect(),diff(),xor()methods - INT_TO_INT set operations — union/intersect/diff/xor with left-wins value semantics
slice($start, $end)— efficient range extraction on all array typesJsonSerializableinterface —json_encode()works natively on Judy arrays__serialize()/__unserialize()— native PHP serialize/unserialize support- Batch operations —
fromArray(),toArray(),putAll(),getAll(),increment() INT_TO_PACKEDtype (6) — GC-free opaque value storage viaphp_var_serializeSTRING_TO_MIXED_HASHtype (7) — JudyHS-backed O(1) string-to-mixed mapSTRING_TO_INT_HASHtype (8) — JudyHS-backed O(1) string-to-int mapSTRING_TO_MIXED_ADAPTIVEtype (9) — SSO short-string optimization via JudyLSTRING_TO_INT_ADAPTIVEtype (10) — SSO for string-to-int.stub.phparginfo generation replaces hand-written C arginfokeys(),values()— native C extraction of keys and values (2–3× faster)sumValues(),averageValues()— C-level aggregation for integer-valued typespopulationCount(start, end)— range counting via Judy internal population cachedeleteRange(start, end)— bulk deletion in a single C passequals(Judy $other)— short-circuit identity comparison- C-level
forEach(),filter(),map()— bypass PHP Iterator protocol - String set operations — union/intersect/diff/xor for STRING_TO_INT and STRING_TO_MIXED types
mergeWith(Judy $other)— in-place merge for accumulation patterns (avoidsunion()allocation)
Bug Fixes
- JudySL hang on Windows x64 —
cJU_MASKATSTATE 0xffLtruncation at States 5–8 (issue #46) - Double-traversal elimination in adaptive types — removes redundant JLG/JHSG before JLI/JHSI
hs_arrayNULL-check injudy_free_array_internalprevents memory leak in adaptive types
Performance
- JLG+JLI elimination — 50% fewer tree traversals in write paths for STRING types
- Branch prediction hints (
JUDY_LIKELY/JUDY_UNLIKELY) on hot lookup and validation paths - LTO and loop unrolling enabled in compiler flags
- Memory layout repacked for cache-line alignment and reduced padding
- Stack safety — 64KB stack buffers replaced with heap-allocated
key_scratchbuffer (Fiber-safe) foreachiterator overhaul — valid flag, heap key buffer,zend_stringreuse- Tagged-union packed format for INT_TO_PACKED — eliminates serialize for scalars
- O(1)
count()for all types — counter on every insert/delete replaces O(n) scans - Type-specialized bulk-insert loops in
fromArray()/putAll()/__unserialize()
Improvements
- 174 tests covering all features
- PHPDoc annotations on all methods (IDE completions, generated API.md)
- Automated
API.mdgeneration fromJudy.stub.php+ metadata — CI enforces freshness
Resources
Installation
Via PECL:
pecl install judyVia PIE:
pie install orieg/judyv2.3.0
What's New
- NEW FEATURE: Windows PECL DLL builds now published with each release (PHP 8.1-8.5, x86/x64, TS/NTS)
- NEW FEATURE: PHP 8.5 support added to CI matrix
Bug Fixes
- FIX: PHP 8.1+ compile fix - ZVAL_DUP replaced with ZVAL_COPY throughout
- FIX: free_obj memory leak resolved (Judy array, zvals, and iterator state now properly freed)
- FIX: BITSET clone bug fixed (was iterating new array instead of source)
- FIX: STRING_TO_INT counter no longer overcounts on value overwrite
- FIX: E_ERROR replaced with exceptions in constructor and write_dimension
- FIX: foreach-by-ref now throws a proper error instead of E_ERROR; fixed emalloc leak
- FIX: long->zend_long throughout; JLF() iterator bug fixed; redundant JLG lookups removed
- FIX: Removed -march=native and -flto from config.m4 for portability
Improvements
- IMPROVEMENT: CI workflow consolidated; Windows benchmarks added to CI reporting
- IMPROVEMENT: Version consistency check added to CI (PHP_JUDY_VERSION vs package.xml)