Help us improve
Share bugs, ideas, or general feedback.
Audits PHP deserialization surfaces (unserialize, session decoders, WDDX, phar metadata, custom handlers) for UAFs via back-references, type confusion, partial-object __destruct, heap overflows. Targets PHP 5.4-5.6 sources with advisory corpus verification.
npx claudepluginhub califio/skills --plugin php-unserialize-auditHow this skill is triggered — by the user, by Claude, or both
Slash command
/php-unserialize-audit:php-unserialize-auditThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Purpose: find exploitable bugs in PHP's deserialization surface and related object-lifetime code that is reachable through deserialization. For general PHP engine memory-safety bugs outside the deserialization flow (e.g. the `extract()` callback-reentrancy UAF, `array_walk` callback mutations, generic hash-slot UAFs), use the companion `/php-audit` skill.
Analyzes PHP code for insecure deserialization vulnerabilities including unserialize with user input, missing allowed_classes, object injection, gadget chains, and Phar metadata triggers.
Identifies and exploits insecure deserialization vulnerabilities in Java, PHP, Python, and .NET apps during authorized penetration tests. Uses ysoserial, PHPGGC, and Burp Suite to detect serialized data and achieve RCE.
Identifies and exploits insecure deserialization vulnerabilities in Java, PHP, Python, and .NET apps during authorized penetration tests. Uses ysoserial, PHPGGC, and Burp Suite to detect serialized data and achieve RCE.
Share bugs, ideas, or general feedback.
Purpose: find exploitable bugs in PHP's deserialization surface and related object-lifetime code that is reachable through deserialization. For general PHP engine memory-safety bugs outside the deserialization flow (e.g. the extract() callback-reentrancy UAF, array_walk callback mutations, generic hash-slot UAFs), use the companion /php-audit skill.
The advisory corpus at work/phpcodz/research/pch-0{10,11,14,15,19,20,21,22,25,26,27,28,29,30,31,32,33,34}.md is the ground truth for what "exploitable" looks like here — re-read one or two before starting if you are unfamiliar.
/php-unserialize-audit <target> [focus]
Target can be:
spl, date, gmp, soap, session, phar, standard, sqlite3, pdo, intl, wddxext/spl/spl_array.c, ext/standard/var_unserializer.reSplObjectStorage, SoapFault, Exception::getTraceAsStringunserialize, session-deserializer, magic-methods, custom-unserializers, allExamples:
/php-unserialize-audit spl/php-unserialize-audit unserialize "UAF via R: back-references"/php-unserialize-audit ext/date/php_date.c/php-unserialize-audit session "multi-entry decode UAF"/php-unserialize-audit magic-methods "type confusion in __toString"/php-unserialize-audit all "double-free"PHP sources live at /home/x/kernel/php_old/work/src/php-<version>/. The primary target versions are PHP 5.4.x–5.6.x (matches the advisory corpus), but the same bug classes appear in PHP 7.x with different zval layout. Check which version the user intends; if unspecified, default to php-5.5.14 (the version most PoCs target). Extract the tarball if no extracted tree exists:
cd /home/x/kernel/php_old/work/src && tar xzf php-5.5.14.tar.gz
Every class below has at least one confirmed instance in the advisory corpus. Use this as the hunting checklist. For each finding, map it to a U-class so the synthesis pass can deduplicate.
ce->unserialize handler frees zval still tracked in var_hashAn extension implements ce->unserialize (e.g. spl_array_object_unserialize, spl_object_storage_unserialize, spl_dllist_unserialize, gmp_unserialize). Inside:
ALLOC_INIT_ZVAL(pvar) creates a fresh zval.php_var_unserialize(&pvar, &p, max, &var_hash) parses one value — during that call, pvar gets appended to var_hash->data[] via var_push_dtor_no_addref so future R:n / r:n back-references can resolve to it.zval_ptr_dtor(&pvar), zval_dtor(&pvar), or FREE_ZVAL(pvar) drops refcount to zero and frees the zval value.R:n in the serialized stream (or from a later sibling in the outer array) still resolves to that freed slot — attacker controls the type/value byte on the freed chunk.Confirmed instances: pch-027 (ArrayObject pflags, pmembers), pch-028 (SplObjectStorage pcount, pmembers), pch-029 (SplDoublyLinkedList flags), pch-032 (GMP zv_ptr).
Fix pattern to distinguish good code from bad: correct handlers use var_push_dtor_no_addref themselves before dropping ref, or never call zval_ptr_dtor on zvals that went through php_var_unserialize.
__wakeup, __toString, __destruct) freeing a property mid-deserializationobject_common2 invokes __wakeup after process_nested_data populates properties. A user-defined __wakeup that calls unset($this->foo) or $this->foo = 1 drops the refcount of the old property zval — but that old zval was registered in var_hash, so an outer R:n still resolves to it.
Confirmed: pch-021 [CVE-2015-2787] (generic __wakeup UAF), pch-020 [CVE-2015-0273] (DateTime), pch-033 (SplObjectStorage + __wakeup), pch-034 (SplDoublyLinkedList + __wakeup), pch-019 (DateTimeZone __wakeup type confusion).
convert_to_long / convert_to_string / convert_to_double on a var_hash-tracked zvalImplicit destruction: converting an array/object zval in-place frees the old value. If the caller got the zval from php_var_unserialize and didn't re-register it, R: references to it UAF.
Confirmed: pch-022 (DateInterval's PHP_DATE_INTERVAL_READ_PROPERTY macro).
process_nested_data failure path frees partially-registered zvalsInside process_nested_data, if an inner php_var_unserialize fails, the function calls zval_dtor(data); FREE_ZVAL(data); and returns 0 — but data may already be in var_hash from a nested call that succeeded partway.
Confirmed: pch-030 case (ii).
Serializable::unserialize reentrancy freeing already-registered stateA PHP class implementing Serializable can inside its unserialize() method call unserialize() recursively, free local state, or reassign $this->data = 1. The outer deserializer's var_hash still holds references to the freed zvals.
Confirmed: pch-030 cases (i) and (iii).
zval_ptr_dtorSession decoders (PS_SERIALIZER_DECODE_FUNC(php), php_binary, php_serialize) loop over multiple key=value pairs, call php_var_unserialize per value, then zval_ptr_dtor(¤t) — the next iteration can reference the freed slot via R:n.
Confirmed: pch-031.
Z_TYPE_P check before Z_STRVAL_P / Z_LVAL_P / Z_ARRVAL_P / Z_OBJ_HT_PMagic methods and formatters read properties via zend_read_property then dereference without type check. Attacker supplies a serialized object whose property is a long instead of a string: Z_STRVAL_P treats the long as a pointer → arbitrary read, or vice-versa leaks pointers as integer values.
Confirmed: pch-025 (SoapFault __toString), pch-023 (SoapClient), pch-026 (Exception getTraceAsString — fake HashTable via fake Bucket).
memcpy / ereallocint len parameter used in erealloc(ptr, *len + l + 1) followed by memcpy(..., l) with l = vallen from a fake string-type zval. Negative vallen passes signed check, becomes huge unsigned in memcpy → heap overflow.
Confirmed: pch-026 (TRACE_APPEND_STRL).
__destruct on malformed serialized streamunserialize() parses O:len:"classname":elements:{ successfully (instantiates the class), then parse fails mid-property. Return value is destroyed, refcount hits 0, __destruct() runs on a class the attacker picked. Lets you invoke __destruct at an arbitrary point in a script (before $shutdown_functions setup, etc.).
Confirmed: pch-011.
safeUnserialize-style prefilters accept input that unserialize_str handles differently than the filter expects — e.g. length-prefix mismatches, whitespace/terminator handling. Allows re-introducing O: / C: into input that was supposed to be object-free.
Confirmed: pch-010 (IPB safeUnserialize bypass).
Classname parsing differences between engines cause unserialize() to accept payloads that var_export later round-trips into executable code.
Confirmed: pch-015, pch-016. (Only relevant if HHVM is in scope — usually skip.)
Same __wakeup/__destruct primitives as unserialize(), reached via a different decoder. WDDX is historically under-audited.
Confirmed: pch-014.
zval_ptr_dtor on two paths of an error goto chain. Grep for goto outexcept / goto error inside custom unserializers and diff which zvals have been ALLOC_INIT_ZVAL-ed vs freed on each path.zval_ptr_dtor on a zval whose refcount was never incremented after a transfer.Z_TYPE: ALLOC_ZVAL(x) without INIT_ZVAL / INIT_PZVAL → stack garbage becomes type tag.count * sizeof: in zend_hash_copy size computations, allocation sizing based on attacker-controlled count.phar_parse_* unserializes phar metadata on file_exists / stat / fopen — reachable via phar:// stream wrapper from any file op on attacker-controlled input.Grep gives candidates; the agent must verify by reading the full function and the shared context. Never report a raw grep match as a finding.
For U1, U3, U4, U6 (custom unserializer / convert_to / loop UAFs):
ripgrep -n 'php_var_unserialize' <target_files>
For each hit, check within the same function for:
zval_ptr_dtor / zval_dtor / FREE_ZVAL on the unserialized zvalconvert_to_long / convert_to_string / convert_to_double / convert_to_boolean on itwhile, for) with unserialize inside and dtor between iterationsFor U2, U9 (magic-method UAFs, partial __destruct):
ripgrep -n 'call_user_function_ex|zend_call_method|BG\(serialize_lock\)|__wakeup|__destruct|__toString' <target_files>
Trace the caller — is it inside object_common2 / unserialize flow? Can the callee reassign $this properties whose zvals were registered in var_hash?
For U5 (Serializable reentrancy):
ripgrep -n 'ce->unserialize\s*==\s*NULL|ce->unserialize(rval' Zend/ ext/standard/var_unserializer.re
Check paths where a user-defined unserialize() can re-enter php_var_unserialize on the same var_hash.
For U7 (type confusion):
ripgrep -nB5 'Z_STRVAL_P|Z_LVAL_P|Z_ARRVAL_P|Z_OBJ_HT_P|Z_OBJPROP_P' <target_files>
For each hit, verify the preceding 5–10 lines contain a matching Z_TYPE_P(...) == IS_STRING / IS_LONG / IS_ARRAY check on the same zval, OR that the zval was produced by a function that guarantees type.
For U8 (signed-length memcpy/erealloc):
ripgrep -n 'erealloc.*\+.*len|memcpy.*len' <target_files>
Check the declared type of len and whether it can be negative or attacker-controlled.
For U10 (serialize/unserialize inconsistency):
Read unserialize_str in var_unserializer.re side-by-side with any safe_unserialize-style prefilter in the target. Compare length-prefix handling, embedded quotes, terminator semantics.
Double-free:
ripgrep -n 'zval_ptr_dtor|FREE_ZVAL' <target_files> | look for same variable name twice in one function across different goto paths
Do NOT start from a hardcoded file map. The attack surface is defined by code patterns, not filenames. Run the discovery queries below across the entire source tree and union the results — every hit is a potential target.
ce->unserializerg -n --no-heading 'ce->unserialize\s*=|->unserialize\s*=\s*\w+_unserialize|INIT_CLASS_ENTRY.*unserialize' ext/ Zend/
rg -n --no-heading 'zend_class_unserialize_t|ZEND_FN\(.*unserialize' ext/ Zend/
Every matched function is an unserializer handler. Record: function name, containing class, file:line, and the file it lives in.
__wakeup, __toString, __destruct, __set_state, __sleep)rg -n --no-heading '"__wakeup"|"__toString"|"__destruct"|"__set_state"|"__sleep"|"__serialize"|"__unserialize"' ext/ Zend/
rg -n --no-heading 'PHP_ME\([^,]+,\s*__(wakeup|toString|destruct|set_state|sleep|serialize|unserialize)' ext/ Zend/
rg -n --no-heading 'ZEND_METHOD\([^,]+,\s*__(wakeup|toString|destruct|set_state|sleep|serialize|unserialize)' ext/ Zend/
These are the magic-method implementations. For each, record the class and method, and flag the file as a target for U2/U7 analysis. Every class here that is serializable is a potential U2 vector — the exploit is a user-defined subclass overriding the magic method, or the built-in magic method itself mutating state during unserialize.
rg -n --no-heading 'PS_SERIALIZER_DECODE_FUNC|PS_SERIALIZER_ENCODE_FUNC|ps_serializer\b' ext/
rg -n --no-heading 'php_wddx_deserialize|wddx_stack_destroy' ext/wddx/ 2>/dev/null
rg -n --no-heading 'phar_parse_metadata|phar_parse_.*file' ext/phar/ 2>/dev/null
php_var_unserialize outside the core deserializerrg -n --no-heading 'php_var_unserialize' ext/ Zend/ sapi/
Every caller outside ext/standard/var_unserializer.* is a potential U1/U3/U6 site — it's someone wiring the deserializer into a new context where they may not respect var_hash rules.
zend_read_property → Z_*_P without type guardrg -n --no-heading -B2 -A6 'zend_read_property' ext/ Zend/
Every hit where the returned zval is dereferenced via Z_STRVAL_P, Z_LVAL_P, Z_ARRVAL_P, Z_OBJ_HT_P, or Z_OBJPROP_P without a Z_TYPE_P(...) == check in the intervening lines is a U7 candidate.
Z_ARRVAL_P / HASH_OF on attacker-reachable zvalrg -n --no-heading 'Z_ARRVAL_P|HASH_OF|zend_hash_apply_with_arguments' ext/ Zend/
Cross-reference against D5 — these are the pch-026 (Exception getTraceAsString) shape.
rg -n --no-heading -B1 -A3 'erealloc\b' ext/ Zend/ | rg -B1 -A3 'memcpy'
rg -n --no-heading 'int\s+\*?\w*len\b|int\s+vallen' ext/ Zend/
For each erealloc(ptr, a + len + b) + memcpy(dst, src, len) pair, check the declared type of len. int or long = U8 candidate.
rg -nU --multiline --no-heading 'zval_ptr_dtor\([^)]*\)[\s\S]{0,500}zval_ptr_dtor\([^)]*\)' ext/ Zend/
Then within each function, look for same-variable pairs across distinct goto paths.
rg -n --no-heading 'php_stream_wrapper_register|phar_url_stat|phar_wrapper' ext/
phar:// is the big one — file_exists, fopen, stat on a phar:// URL invokes phar metadata unserialize. Any function in an extension that does a file-type operation on user-controlled paths is a reachability vector.
rg -n --no-heading 'zend_ce_serializable|implements\s+Serializable|zend_class_implements.*serializable' ext/ Zend/
After running D1–D10, produce a deduplicated target list: {file_path, reason, U-classes to check}. This list is what Step 2 gathers context for and Step 3 partitions across agents.
The advisory corpus confirms bugs in these files. Your D1–D10 discovery must find all of them; if it doesn't, your queries are broken. Use the seed list as a cross-check after discovery — every file below should appear in the union of D1–D10 results, and anything in D1–D10 that isn't here is a potentially under-audited target.
Core deserializer:
ext/standard/var_unserializer.re + var_unserializer.c (generated) — the R: / r: / C: / O: state machine, process_nested_data, object_common1, object_common2, finish_nested_data, var_push_dtor*, var_destroyext/standard/var.c — serialize side (needed for U10 inconsistency checks against unserialize_str)Zend/zend_variables.c — zval_ptr_dtor / zval_dtor / _zval_ptr_dtor_wrapper definitionsZend/zend_hash.c — HashTable / Bucket semantics, zend_hash_copy, zend_hash_apply_with_argumentsZend/zend_execute_API.c — call_user_function_ex, magic method dispatchZend/zend_API.c — convert_to_long / convert_to_string / convert_to_double / convert_to_boolean (U3 semantics)Zend/zend_operators.c — same conversion fallbacksClasses with custom ce->unserialize (primary U1/U3/U4/U6 targets):
ext/spl/spl_array.c — ArrayObject, ArrayIterator (pch-027)ext/spl/spl_observer.c — SplObjectStorage, MultipleIterator (pch-028, pch-033)ext/spl/spl_dllist.c — SplDoublyLinkedList, SplStack, SplQueue (pch-029, pch-034)ext/spl/spl_heap.c — SplHeap, SplPriorityQueue, SplMinHeap, SplMaxHeapext/spl/spl_fixedarray.c — SplFixedArrayext/date/php_date.c — DateTime, DateTimeImmutable, DateTimeZone, DateInterval, DatePeriod (pch-019, pch-020, pch-022)ext/gmp/gmp.c — GMP (5.6+) (pch-032)ext/phar/phar.c + ext/phar/phar_object.c — phar metadata unserialize (reachable via phar:// stream wrapper)ext/intl/*/*.c — Collator, DateFormatter, MessageFormatter, NumberFormatter, ResourceBundle, several have custom (de)serializersext/standard/basic_functions.c — any serialization callbacksMagic-method / type-punning risk (primary U2/U7 targets):
Zend/zend_exceptions.c — getTraceAsString, __toString on Exception (pch-026)ext/soap/soap.c — SoapFault __toString, SoapClient __call (pch-023, pch-025)ext/spl/spl_exceptions.cext/date/php_date.c — DateTime __wakeup, DateTimeZone __wakeup (pch-019, pch-020)Session deserializers (U6 targets):
ext/session/session.c — PS_SERIALIZER_DECODE_FUNC for php, php_binary, php_serialize (pch-031)ext/session/mod_*.c — file / user / memcache session handlersphp_session_decodeAlternate decoders:
ext/wddx/wddx.c — WDDX packet deserializer (pch-014)ext/phar/phar.c — phar metadata path (invokes php_var_unserialize on file metadata)ext/standard/url.c — parse_str edge cases (rarely, but historically problematic)Serializable-interface implementers (U5 targets):
zend_class_implements(ce, 1, zend_ce_serializable) — discover via D10, not hardcodedPartial-object __destruct (U9 targets):
__destruct — the reachability is via a malformed O: payload that aborts mid-parse after instantiation. Discover via D2, pair with U9 verification.Use this list to validate discovery coverage. The actual agent work assignment comes from the D1–D10 union.
Map the user's <target> argument to a concrete file set:
.c and .re in ext/<name>/.h if related)ZEND_METHOD(<ClassName> / PHP_METHOD(<ClassName> and <classname>_object_handlersunserialize, session-deserializer, etc.) → use the Target file map aboveall → the full union, partitioned across agents by extension directoryIf the argument is ambiguous, print what you found and ask. Never silently narrow.
Sub-agents only read their assigned files. The semantic facts that determine whether a bug exists — zval layout, var_hash registration rules, magic-method invocation points, convert_to_* side effects — live in different files. Produce a SHARED_CONTEXT block yourself and paste it verbatim into every agent prompt in Step 3.
Read Zend/zend.h and Zend/zend_types.h. Extract:
struct _zval_struct definition (PHP 5.x: pointer-based; PHP 7.x: inline with zend_value union) — include every field with exact types and offsetsstruct _zval_gc_infostruct bucketstruct _hashtable — list head, number used, refcount, destructor pointerZ_REFCOUNT_P, Z_ADDREF_P, Z_DELREF_P, Z_SET_REFCOUNT_PIS_NULL, IS_LONG, IS_DOUBLE, IS_BOOL, IS_ARRAY, IS_OBJECT, IS_STRING, IS_RESOURCERead ext/standard/var_unserializer.re top-to-bottom and ext/standard/php_var.h. Extract:
php_unserialize_data_t / var_entries structurevar_push_dtor is called vs var_push_dtor_no_addref vs var_pushR: and r: case blocks — exactly which array they index and whether they addrefvar_destroy — what it frees and in what orderprocess_nested_data, object_common1, object_common2 definitionsKey invariants to state plainly in the context block:
php_var_unserialize successfully is appended to var_hash->data[] via var_push_dtor_no_addref with its current refcount, so R:n can resolve to it. Calling zval_ptr_dtor on such a zval without first guarding via var_push_dtor creates a UAF oracle."R:n resolves to the zval itself (reference), r:n returns a copy of its value."convert_to_long/string/double/boolean on an array or object zval calls zval_dtor on the old value before writing the new primitive."Grep for __wakeup / __toString / __destruct invocation sites:
ripgrep -n '"__wakeup"|"__toString"|"__destruct"|zend_user_unserialize' Zend/ ext/standard/
For each: which function calls it, under what state (is var_hash still active? has the object been added to var_hash yet?), and whether callbacks can mutate $this properties that are in var_hash.
For each file in the target set, extract:
ce->unserialize = X assignments and the function X signaturePS_SERIALIZER_DECODE_FUNC(...) bodies (function name, parameters, return convention)var_hash (safer) or the caller's (UAF-prone)For each custom unserializer in the target, grep for callers:
ripgrep -n '<unserializer_function_name>' ext/ Zend/
Usually invoked from var_unserializer.re's C: (custom) case — confirm the arguments and the caller's expectations about var_hash state.
Record which PHP version(s) the target codebase is. If multiple are extracted, confirm with the user. Map each finding to the affected-version matrix from the advisory corpus.
SHARED_CONTEXT block=== SHARED CONTEXT (read by all audit agents) ===
--- PHP version ---
[e.g. 5.5.14 from /home/x/kernel/php_old/work/src/php-5.5.14]
--- zval / HashTable layout ---
[paste struct definitions verbatim, with refcount semantics]
--- var_hash registration rules ---
[state the three invariants about var_push_dtor_no_addref, R:/r: resolution, convert_to_* side effects]
--- Magic method dispatch ---
__wakeup: called from [file:line] after [state]. Can mutate: [yes/no] properties currently in var_hash.
__toString: ...
__destruct: ...
--- Custom unserializer vtable (target files) ---
<class>::<unserializer_function> (file:line): signature, uses own var_hash [yes/no], caller [file:line]
--- convert_to_* semantics ---
convert_to_long(zv): if Z_TYPE(zv) in {IS_ARRAY, IS_OBJECT}, calls zval_dtor(zv) before overwriting.
[... same for string/double/bool ...]
--- Known good-vs-bad patterns ---
GOOD: handler calls var_push_dtor_no_addref(&var_hash, &pvar) before zval_ptr_dtor(&pvar).
BAD: handler calls zval_ptr_dtor(&pvar) directly (enables R:n UAF).
=== END SHARED CONTEXT ===
Keep it factual and terse. No analysis yet.
Launch one Agent per file (or small related-file group — e.g. spl_dllist.c + spl_dllist.h in one agent). All in a single message, all model="sonnet". Every file in the target must be covered; do not cap or skip.
Each agent prompt MUST contain these sections in order:
Section A — Context
"You are auditing PHP for exploitable memory-safety bugs in the unserialize surface: UAF, double-free, type confusion, heap overflow via signed-length, partial-object __destruct, parse inconsistency. The advisory corpus at work/phpcodz/research/pch-0{10,11,14,15,19,20,21,22,25,26,27,28,29,30,31,32,33,34}.md is the ground truth for bug shape."
Section B — Shared context (paste VERBATIM)
Paste the SHARED_CONTEXT block. Do not summarize.
Section C — File assignment
"Read the ENTIRE file at . Use the shared context to resolve any struct, macro, or function defined elsewhere. Do not skim."
Section D — Bug classes to hunt
Paste the U1–U12 taxonomy above verbatim, plus the adjacent classes (double-free, refcount underflow, uninitialized Z_TYPE, phar metadata).
Section E — Verification requirements per class
For EACH candidate finding, the agent must explicitly state:
php_var_unserialize), the exact line it is freed, and the R:/r: offset that resolves to the freed slot. If the agent can't construct a serialized string that reaches the UAF, downgrade to LOW confidence.__wakeup/__toString/__destruct is dispatched, and the exact PHP code the attacker would put in the magic method to free that property.php_var_unserialize on the same var_hash.Z_*_P access and the absence of a type check in the preceding basic block (not just the preceding line — walk back through the CFG).zval_ptr_dtor / FREE_ZVAL on the same zval across a goto chain."Do NOT report speculative patterns. If you can't name the line where the bad thing happens, don't file it."
Section F — Noise filter
"Do NOT report: missing NULL checks on malloc (PHP uses emalloc which longjmps on OOM), code style, deprecated API usage, pure bug-class pattern matches without a reachable trigger."
Section G — Output format
For each finding: (a) file:line (b) one-line summary (c) U-class (U1–U12 or "other: ") (d) trigger payload sketch (serialized string) (e) reachability: which PHP-facing function reaches the vuln (unserialize, session_decode, phar:// stream open, __wakeup of user class, etc.) (f) primitive: UAF-read / UAF-write / type-confusion-read / heap-overflow / __destruct-at-arbitrary-point / RCE (g) confidence HIGH/MEDIUM/LOW with justification tied to Section E.
After all sonnet agents complete, collect every finding rated HIGH or MEDIUM in U1–U9 or double-free. Launch ONE model="opus" agent (or parallelize per-finding if >6 findings) with:
SHARED_CONTEXT blockOpus output per finding: CONFIRMED (with PoC sketch) / REJECTED (with reason) / NEEDS-MORE-DATA (with specific question).
Do not use opus for the initial sweep — only verification.
Present results as a table:
| File:Line | Bug | U-class | Trigger | Primitive | Confidence |
Group by primitive severity (RCE > UAF-write > UAF-read > type-confusion-read > heap-overflow > __destruct-arb > crash). For each CONFIRMED finding, include the opus PoC sketch inline.
Cross-reference against the phpcodz corpus: if a finding matches an existing advisory (same file, same function, same class), note the advisory number — this is a rediscovery, valuable for confirming the audit methodology but not a new bug unless the version is post-patch.
emalloc without checking return value" — emalloc never returns NULL; it longjmps.max_input_vars and memory_limit; not a finding without a specific bypass.Z_STRVAL_P without explicit type check" — valid if the preceding code path guarantees IS_STRING (e.g. string-only deserializer branch). Require CFG reasoning, not a single-line grep match.zval_ptr_dtor after php_var_unserialize" — valid if var_push_dtor_no_addref was called, or if the zval was re-initialized (ALLOC_INIT_ZVAL then a new php_var_unserialize into a different slot). Require tracing the specific zval, not the variable name.If the user asks for a quick scan without the full context pass (e.g. "just grep for the convert_to_long pattern in ext/date"), skip Steps 2–4 and run the Detection heuristics directly, but prefix every output with "UNVERIFIED CANDIDATE — context pass was skipped." Do not rate confidence above LOW in one-shot mode.