Skip to content

Releases: orieg/php-judy

v2.7.1

Choose a tag to compare

@orieg orieg released this 23 Aug 06:08
c14718a

Release 2.7.1

  • SECURITY FIX: Guard against destructor re-entrancy and heap Use-After-Free (UAF) in Judy::STRING_TO_ENTRY during ArrayAccess overwrites, Judy::set(), and Judy::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_VERSION and package.xml to 2.7.1.

v2.7.0

Choose a tag to compare

@orieg orieg released this 22 Aug 22:12
77831bf

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(), and getFlags(). 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

Choose a tag to compare

@orieg orieg released this 19 Aug 18:28
c850210

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

Choose a tag to compare

@orieg orieg released this 18 Aug 04:49
a6083a2

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.md now documents that toArray() coerces integer-looking string keys on string-keyed types — "42" returns as int 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 never API.md, which is the reference composer.json advertises.
  • BENCHMARK.md records why php-code-coverage and Infection were examined and rejected as Judy fits.
  • research/write-probe-cost/probebench.c now honours keylen, 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

Choose a tag to compare

@orieg orieg released this 18 Aug 03:47
84b20ec

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 0x800xFF 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.md pitfalls, 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

Choose a tag to compare

@orieg orieg released this 17 Aug 23:43
968e982

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 optimizeIteration mirror are not represented there. A refreshed baseline follows separately.

v2.4.2

Choose a tag to compare

@orieg orieg released this 14 Aug 16:55
dde54bf

Hardening release folding in the security & correctness sprint.

Security

  • Fix use-after-free write on *_TO_MIXED overwrite/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_gc so 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/map callbacks may re-enter safely.

Feature

  • Set operations (intersect/diff/xor) now supported for STRING_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

Choose a tag to compare

@orieg orieg released this 14 Aug 05:02
f562727
  • FIX: PHP 8.6 build — XtOffsetOf() alias removed in PHP 8.6.0alpha2; use standard C offsetof() instead (thanks Remi Collet, #65)

v2.4.0

Choose a tag to compare

@orieg orieg released this 03 Mar 07:38
d258bee

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_iterator for zero-overhead foreach loops
  • BITSET set operationsunion(), 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 types
  • JsonSerializable interface — json_encode() works natively on Judy arrays
  • __serialize()/__unserialize() — native PHP serialize/unserialize support
  • Batch operationsfromArray(), toArray(), putAll(), getAll(), increment()
  • INT_TO_PACKED type (6) — GC-free opaque value storage via php_var_serialize
  • STRING_TO_MIXED_HASH type (7) — JudyHS-backed O(1) string-to-mixed map
  • STRING_TO_INT_HASH type (8) — JudyHS-backed O(1) string-to-int map
  • STRING_TO_MIXED_ADAPTIVE type (9) — SSO short-string optimization via JudyL
  • STRING_TO_INT_ADAPTIVE type (10) — SSO for string-to-int
  • .stub.php arginfo generation replaces hand-written C arginfo
  • keys(), values() — native C extraction of keys and values (2–3× faster)
  • sumValues(), averageValues() — C-level aggregation for integer-valued types
  • populationCount(start, end) — range counting via Judy internal population cache
  • deleteRange(start, end) — bulk deletion in a single C pass
  • equals(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 (avoids union() allocation)

Bug Fixes

  • JudySL hang on Windows x64cJU_MASKATSTATE 0xffL truncation at States 5–8 (issue #46)
  • Double-traversal elimination in adaptive types — removes redundant JLG/JHSG before JLI/JHSI
  • hs_array NULL-check in judy_free_array_internal prevents 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_scratch buffer (Fiber-safe)
  • foreach iterator overhaul — valid flag, heap key buffer, zend_string reuse
  • 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.md generation from Judy.stub.php + metadata — CI enforces freshness

Resources

Installation

Via PECL:

pecl install judy

Via PIE:

pie install orieg/judy

v2.3.0

Choose a tag to compare

@orieg orieg released this 27 Feb 21:28
6065ed4

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)