Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 33 additions & 15 deletions package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,22 @@
<email>nicolas@brousse.info</email>
<active>yes</active>
</lead>
<date>2026-08-22</date>
<date>2026-08-23</date>
<time>00:00:00</time>
<version>
<release>2.7.0</release>
<api>2.7.0</api>
<release>2.7.1</release>
<api>2.7.1</api>
</version>
<stability>
<release>stable</release>
<api>stable</api>
</stability>
<license uri="http://www.php.net/license">PHP</license>
<notes>
- 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.
</notes>
<contents>
<dir name="/">
Expand Down Expand Up @@ -319,6 +311,7 @@
<file name="tests/regression_unset_hash_adaptive_untouched_001.phpt" role="test" />
<file name="tests/regression_unset_string_to_int_hash_001.phpt" role="test" />
<file name="tests/regression_unset_string_to_int_hash_002.phpt" role="test" />
<file name="tests/regression_string_to_entry_reentrancy_001.phpt" role="test" />
<file md5sum="f4c1dde3511c574eef8fbf6a99f9d27b" name="config.m4" role="src" />
<file md5sum="14c238115fee50dfe70b13bc8da311d3" name="config.w32" role="src" />
<file name="Judy.stub.php" role="src" />
Expand Down Expand Up @@ -445,6 +438,31 @@
<configureoption default="bundled" name="with-judy" prompt="libJudy to build against: &quot;bundled&quot; compiles the bundled copy (default); give the install prefix DIR of a system libJudy to link against it instead" />
</extsrcrelease>
<changelog>
<release>
<date>2026-08-22</date>
<version>
<release>2.7.0</release>
<api>2.7.0</api>
</version>
<stability>
<release>stable</release>
<api>stable</api>
</stability>
<notes>
- 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.
</notes>
</release>
<release>
<date>2026-08-19</date>
<version>
Expand Down
18 changes: 11 additions & 7 deletions php_judy.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion php_judy.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
50 changes: 50 additions & 0 deletions tests/regression_string_to_entry_reentrancy_001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
--TEST--
Regression: Judy STRING_TO_ENTRY destructor reentrancy and UAF safety
--SKIPIF--
<?php if (!extension_loaded("judy")) print "skip"; ?>
--FILE--
<?php
class EvilObject {
public function __construct(public Judy $judy, public string $key) {}
public function __destruct() {
// Reentrantly unset or access key during destruction
unset($this->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
Loading