diff --git a/package.xml b/package.xml index dc6498e..9555682 100644 --- a/package.xml +++ b/package.xml @@ -16,11 +16,11 @@ nicolas@brousse.info yes - 2026-08-22 + 2026-08-23 - 2.7.0 - 2.7.0 + 2.7.1 + 2.7.1 stable @@ -28,18 +28,10 @@ PHP -- FEATURE: Native cache and TTL storage engine -- 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 PHP array wrappers or secondary lookup indexes (#188). -- FEATURE: 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). -- FEATURE: Full bulk and iteration support on STRING_TO_ENTRY -- ArrayAccess, - Iterator, keys(), values(), toArray(), slice(), getAll(), and serialization. -- BUILD: Pure-Rust Expanse backend integration support (--with-expanse) for - modern 64-bit microarchitectures, zero-leak memory management, and cross-platform - MSVC Windows support without source patching. +- 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. @@ -319,6 +311,7 @@ + @@ -445,6 +438,31 @@ + + 2026-08-22 + + 2.7.0 + 2.7.0 + + + stable + stable + + +- FEATURE: Native cache and TTL storage engine -- 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 PHP array wrappers or secondary lookup indexes (#188). +- FEATURE: 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). +- FEATURE: Full bulk and iteration support on STRING_TO_ENTRY -- ArrayAccess, + Iterator, keys(), values(), toArray(), slice(), getAll(), and serialization. +- BUILD: Pure-Rust Expanse backend integration support (--with-expanse) for + modern 64-bit microarchitectures, zero-leak memory management, and cross-platform + MSVC Windows support without source patching. + + 2026-08-19 diff --git a/php_judy.c b/php_judy.c index d33e45d..d5714ff 100644 --- a/php_judy.c +++ b/php_judy.c @@ -1153,10 +1153,12 @@ int judy_object_write_dimension_helper(zval *object, zval *offset, zval *value) } judy_cache_entry_t *entry = (judy_cache_entry_t *)(uintptr_t)(*slot); if (entry != NULL) { - zval_ptr_dtor(&entry->value); + zval old_val; + ZVAL_COPY_VALUE(&old_val, &entry->value); entry->expires_at = 0; entry->flags = 0; ZVAL_COPY(&entry->value, value); + zval_ptr_dtor(&old_val); } else { entry = (judy_cache_entry_t *)emalloc(sizeof(judy_cache_entry_t)); entry->expires_at = 0; @@ -5984,11 +5986,13 @@ PHP_METHOD(Judy, set) entry = (judy_cache_entry_t *)(uintptr_t)(*slot); if (entry != NULL) { - /* Overwrite existing entry */ - zval_ptr_dtor(&entry->value); + /* Overwrite existing entry: update new state before destroying old value */ + zval old_val; + ZVAL_COPY_VALUE(&old_val, &entry->value); entry->expires_at = expires_at; entry->flags = (uint16_t)flags; ZVAL_COPY(&entry->value, value); + zval_ptr_dtor(&old_val); } else { /* Allocate new entry */ entry = (judy_cache_entry_t *)emalloc(sizeof(judy_cache_entry_t)); @@ -6107,15 +6111,15 @@ PHP_METHOD(Judy, pruneExpired) memcpy(key_to_del, kindex, klen); key_to_del[klen] = '\0'; - zval_ptr_dtor(&entry->value); - efree(entry); - - /* Delete the key from JudySL */ + /* Delete the key from JudySL before running destructor */ JSLD(Rc_int, intern->array, key_to_del); intern->counter--; judy_string_bytes_sub(intern, (Word_t)klen); pruned_count++; + zval_ptr_dtor(&entry->value); + efree(entry); + /* Find the next key strictly greater than key_to_del */ JSLN(PValue, intern->array, key_to_del); if (PValue != NULL && PValue != PJERR) { diff --git a/php_judy.h b/php_judy.h index e6f731b..412fb6f 100644 --- a/php_judy.h +++ b/php_judy.h @@ -19,7 +19,7 @@ #ifndef PHP_JUDY_H #define PHP_JUDY_H -#define PHP_JUDY_VERSION "2.7.0" +#define PHP_JUDY_VERSION "2.7.1" #define PHP_JUDY_EXTNAME "judy" /* Windows x64 (LLP64): Force 64-bit Word_t to match libjudy ABI. diff --git a/tests/regression_string_to_entry_reentrancy_001.phpt b/tests/regression_string_to_entry_reentrancy_001.phpt new file mode 100644 index 0000000..f1a5886 --- /dev/null +++ b/tests/regression_string_to_entry_reentrancy_001.phpt @@ -0,0 +1,50 @@ +--TEST-- +Regression: Judy STRING_TO_ENTRY destructor reentrancy and UAF safety +--SKIPIF-- + +--FILE-- +judy[$this->key]); + } +} + +class PruneEvilObject { + public function __construct(public Judy $judy) {} + public function __destruct() { + // Reentrantly access/prune the cache + $this->judy->pruneExpired(); + } +} + +// 1. ArrayAccess overwrite reentrancy +$j = new Judy(Judy::STRING_TO_ENTRY); +$j["k"] = new EvilObject($j, "k"); +$j["k"] = 42; +var_dump(isset($j["k"])); + +// 2. Judy::set() overwrite reentrancy +$j2 = new Judy(Judy::STRING_TO_ENTRY); +$j2->set("token", new EvilObject($j2, "token"), 300); +$j2->set("token", "new_token_payload", 300); +var_dump(isset($j2["token"])); + +// 3. Judy::pruneExpired() reentrancy +$j3 = new Judy(Judy::STRING_TO_ENTRY); +$j3->set("expired_item", new PruneEvilObject($j3), 1); +sleep(2); +$evicted = $j3->pruneExpired(); +var_dump($evicted >= 1); +var_dump(count($j3)); + +echo "OK\n"; +?> +--EXPECT-- +bool(false) +bool(false) +bool(true) +int(0) +OK