Skip to content

Add stack-limit guards to var_export(), print_r() and unserialize() - #25014

Open
Volkopat wants to merge 2 commits into
php:masterfrom
Volkopat:fix/stack-limit-var-export-print-r
Open

Volkopat wants to merge 2 commits into
php:masterfrom
Volkopat:fix/stack-limit-var-export-print-r

Conversation

@Volkopat

Copy link
Copy Markdown

Fixes #24605.

ZEND_CHECK_STACK_LIMIT guards the recursion in serialize(), var_dump() and debug_zval_dump(),
but not in var_export(), print_r() or unserialize(). On a structure that fits the stack those
functions recurse until it is exhausted and the process dies with SIGSEGV - no error, no message.

Reproduced first

Each function run in its own process, stock php:8.5-cli, --ulimit stack=524288:524288, 3,000 levels:

call result before the fix
serialize() throws Error (already guarded)
var_export() exit 139, SIGSEGV
print_r() exit 139, SIGSEGV
unserialize() exit 139, SIGSEGV

Change

The same guard the already-fixed paths use, at each recursive entry point:

  • ext/standard/var.c - php_var_export_ex() (return FAILURE;)
  • Zend/zend.c - zend_print_zval_r_to_buf(), the recursion behind print_r(), which lives in Zend
    rather than ext/standard (return;)
  • ext/standard/var_unserializer.re - php_var_unserialize_internal() (return 0;)

Each site returns the type its function already uses, so no signatures change. 3 files, +18 lines.

#ifdef ZEND_CHECK_STACK_LIMIT
	if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) {
		zend_call_stack_size_error();
		return FAILURE;
	}
#endif

After

Built from current master with the patch applied, same structure, same 512 KB stack:

--- serialize (already guarded) ---
  serialize threw: Maximum call stack size of 475136 bytes (zend.max_allowed_stack_size - zend.reserved_stack_size) reached. Infinite recursion?
--- var_export ---
  var_export threw: Maximum call stack size of 475136 bytes ... reached. Infinite recursion?
--- print_r ---
  print_r threw: Maximum call stack size of 475136 bytes ... reached. Infinite recursion?
--- unserialize ---
  unserialize threw: Maximum call stack size of 475136 bytes ... reached. Infinite recursion?

All four now raise the same catchable Error, and the surrounding behaviour is unchanged for
structures that fit.

Scope I did not test

unserialize()'s own max_depth option (default 4096) usually rejects deep input first, so the new
guard only matters when the stack is small relative to that limit - which is the case tested here.
I did not test a very large max_depth against a large stack.

These three recursion paths were missing ZEND_CHECK_STACK_LIMIT, so deeply nested input exhausted the stack and killed the process instead of raising the catchable Error that serialize() already produces. Guard each recursive entry point the same way the already-fixed paths do.
…ards

Covers all three entry points with zend.max_allowed_stack_size lowered, asserting each raises the catchable Error instead of exhausting the stack.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

var_export(), print_r() and unserialize() segfault on deep nesting — the stack-limit guard from #22871 is incomplete

1 participant