Skip to content

fix: return errors instead of panicking on engine and user input - #758

Merged
ptondereau merged 1 commit into
extphprs:masterfrom
ptondereau:fix/panics-non-breaking
Aug 1, 2026
Merged

fix: return errors instead of panicking on engine and user input#758
ptondereau merged 1 commit into
extphprs:masterfrom
ptondereau:fix/panics-non-breaking

Conversation

@ptondereau

Copy link
Copy Markdown
Member

Description

First slice of the "panics where a Result belongs" item from the v0.16 backlog, limited to the changes that are not breaking, so they can ship on the current release train.

A panic in an extern "C" frame is not a Rust panic. try_catch calls resume_unwind on a caught panic and the generated handlers are extern "C", not C-unwind, so the process aborts. Each fix below removes a panic reachable from engine-provided or user input.

  • php_error passed the caller's message as php_error_docref's format argument, which is declared PHP_ATTRIBUTE_FORMAT(printf, 3, 4) in every supported version. A message containing %s or %n read garbage varargs. It is now passed as a %s argument, and the try_into on the error-type bits no longer panics. PHP 8.5 added php_error_docref_unchecked for exactly this reason.
  • http_server_vars built the _SERVER auto-global name with ZendStr::new("_SERVER", false).as_mut_ptr(). The ZBox dropped at the end of the statement, so zend_is_auto_global read freed Zend memory. The same shape existed in the #[cfg(not(php81))] arm of http_request_vars, which is still live for a --no-default-features build against PHP 8.0 because min_api_version() only floors at 8.1 under the enum feature.
  • ini_values() panicked on any directive whose value is not valid UTF-8. zend_ini_entry.value is a zend_string holding arbitrary bytes from php.ini, so this was reachable on a normal install. Values are now decoded lossily, an unregistered directive table yields an empty map instead of a null deref, and a non-IS_PTR entry is skipped rather than trusted.
  • throw_property_access_error interpolated a property name into a CString. zend_string is length prefixed, so $obj->{"a\0b"} is legal PHP and aborted inside the extern "C" read_property frame. The message now falls back to a static one.
  • The generic class constructor used .throw().expect(...), turning a failed throw into an abort. Now let _ = e.throw();, matching the idiom already used in src/zend/handlers.rs.
  • FromZendObject for String had two panic!s marked // TODO: become an error, so a PHP class whose __toString() throws aborted the worker. It now returns Error::Exception, Error::ZvalConversion, or the new Error::NotStringable. It also passed (*obj.ce).__tostring unchecked into zend_call_known_function, whose non-null assertion is ZEND_ASSERT and therefore compiled out of a release build: a class without __toString() segfaulted rather than failing. Both the class entry and the handler are now checked.
  • From<Error> for PhpException stringified through err.to_string(), discarding the class and stack of a caught PHP exception even though with_object existed. It now reattaches the exception object. Worth noting the old path often did not merely degrade: the Debug dump contained NUL bytes, so CString::new rejected it and PHP reported Exception: String given contains NUL-bytes instead of anything about __toString.

Also adds .#debug and .#zts-debug dev shells. Zend assertions such as the handler check above are compiled out of a release PHP, so bugs that segfault in production only assert on a ZEND_DEBUG build, and the flake previously offered no way to get one.

Error is #[non_exhaustive], so the new NotStringable variant is additive.

Verified on all four dev shells: NTS and ZTS, each non-debug and ZEND_DEBUG. Both new tests were confirmed to fail without their fix rather than trusting a green run.

One gap: the use-after-free fix in the #[cfg(not(php81))] arm is not compiler-checked locally, since it needs --no-default-features against PHP 8.0.

Checklist

@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 30659294744

Coverage increased (+0.4%) to 66.959%

Details

  • Coverage increased (+0.4%) from the base build.
  • Patch coverage: 28 uncovered changes across 5 files (45 of 73 lines covered, 61.64%).
  • 5 coverage regressions across 3 files.

Uncovered Changes

File Changed Covered %
src/error.rs 12 0 0.0%
src/zend/globals.rs 28 20 71.43%
src/builders/class.rs 5 0 0.0%
src/types/object.rs 27 25 92.59%
src/zend/handlers.rs 1 0 0.0%

Coverage Regressions

5 previously-covered lines in 3 files lost coverage.

File Lines Losing Coverage Coverage
src/builders/class.rs 2 77.15%
src/zend/handlers.rs 2 0.0%
src/error.rs 1 0.0%

Coverage Stats

Coverage Status
Relevant Lines: 13356
Covered Lines: 8943
Line Coverage: 66.96%
Coverage Strength: 43.41 hits per line

💛 - Coveralls

@github-actions

Copy link
Copy Markdown

🐰 Bencher Report

Projectext-php-rs
Branchfix/panics-non-breaking
TestbedPHP 8.4.24 (cli) (built: Jul 29 2026 09:44:18) (NTS)

⚠️ WARNING: Truncated view!

The full continuous benchmarking report exceeds the maximum length allowed on this platform.

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

🐰 View full continuous benchmarking report in Bencher

@ptondereau
ptondereau marked this pull request as ready for review July 31, 2026 21:03
@ptondereau
ptondereau merged commit fcbe219 into extphprs:master Aug 1, 2026
66 checks passed
@ptondereau
ptondereau deleted the fix/panics-non-breaking branch August 1, 2026 18:20
@Xenira Xenira mentioned this pull request Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants