java-topology/defects/php/patch/php-0003-interface-dedup-hashset.md

3.7 KiB
Raw Blame History

php-0003: zend_do_implement_interfaces — O(I²) interface dedup linear scan

Summary

Field Value
ID php-0003
Severity MEDIUM
CWE CWE-407 (Algorithmic Complexity)
File Zend/zend_inheritance.c
Function zend_do_implement_interfaces
Lines 22592290
Speedup ~250x op-count at I=500 interfaces

Defect

When PHP links a class that implements I interfaces (including inherited ones), zend_do_implement_interfaces builds a deduplicated interfaces[] array. For each new interface, it scans the already-accumulated array linearly:

// zend_inheritance.c:2259
for (i = 0; i < ce->num_interfaces; i++) {
    zend_class_entry *iface = interfaces[num_parent_interfaces + i];
    // ...
    for (uint32_t j = 0; j < num_interfaces; j++) {  // O(num_interfaces)
        if (interfaces[j] == iface) {                 // pointer equality
            // skip duplicate
            ...
            break;
        }
    }
    if (iface) {
        interfaces[num_interfaces] = iface;
        num_interfaces++;
    }
}

The outer loop runs ce->num_interfaces times. The inner loop scans up to num_interfaces (parent + accumulated so far) each time. Total: O(P×I + I²) where P = parent interface count, I = new interface count.

In deeply nested interface hierarchies (Symfony, Laravel, Drupal), a class can implement dozens of transitive interfaces. E.g., a class implementing 5 interfaces where each interface extends 10 others = 50+ total interfaces.

Root Cause

The interfaces[] array is a flat C array with no corresponding hash set. Duplicate detection uses pointer equality but relies on a linear scan.

Fix

Use a temporary HashTable (or a pointer-keyed hash set) to track which interface pointers have already been added:

--- a/Zend/zend_inheritance.c
+++ b/Zend/zend_inheritance.c
@@ -2251,7 +2251,9 @@ static void zend_do_implement_interfaces(...)
 {
 	uint32_t num_parent_interfaces = ce->parent ? ce->parent->num_interfaces : 0;
 	uint32_t num_interfaces = num_parent_interfaces;
+	HashTable iface_set;
+	zend_hash_init(&iface_set, num_parent_interfaces + ce->num_interfaces, NULL, NULL, 0);
+	/* pre-populate with parent interfaces */
+	for (uint32_t k = 0; k < num_parent_interfaces; k++) {
+		zend_hash_index_add_empty_element(&iface_set, (zend_ulong)(uintptr_t)interfaces[k]);
+	}

 	for (i = 0; i < ce->num_interfaces; i++) {
 		zend_class_entry *iface = interfaces[num_parent_interfaces + i];
-		/* O(num_interfaces) linear scan for duplicate */
-		for (uint32_t j = 0; j < num_interfaces; j++) {
-			if (interfaces[j] == iface) {
-				/* duplicate found */
-				...
-				iface = NULL;
-				break;
-			}
-		}
+		/* O(1) hash lookup for duplicate */
+		if (zend_hash_index_find(&iface_set, (zend_ulong)(uintptr_t)iface) != NULL) {
+			/* duplicate: inherit constants but don't add to interfaces[] */
+			ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&iface->constants_table, key, c) {
+				do_inherit_constant_check(ce, c, key);
+			} ZEND_HASH_FOREACH_END();
+			continue;
+		}
 		if (iface) {
 			interfaces[num_interfaces] = iface;
+			zend_hash_index_add_empty_element(&iface_set, (zend_ulong)(uintptr_t)iface);
 			num_interfaces++;
 		}
 	}
+	zend_hash_destroy(&iface_set);

Measurement

Simulated with PhpInterfaceDedupAlgorithm.java:

I slow ops fast ops ratio
50 1,275 50 25×
200 20,100 200 100×
500 125,250 500 250×

References

  • Zend/zend_inheritance.c lines 22512312 (zend_do_implement_interfaces)
  • Zend/zend_inheritance.c lines 22682285 (inner dedup loop)
  • Related: php-0004 (zend_do_inherit_interfaces same pattern)