Skip to content

Fixes #16#19

Open
rlerdorf wants to merge 1 commit into
masterfrom
fix/issue-16-segfault-on-reload
Open

Fixes #16#19
rlerdorf wants to merge 1 commit into
masterfrom
fix/issue-16-segfault-on-reload

Conversation

@rlerdorf

@rlerdorf rlerdorf commented Apr 5, 2026

Copy link
Copy Markdown
Member

The timezonedb extension calls php_date_set_tzdb(&timezonedb_external) during MINIT, pointing the date extension's global tzdb pointer at data inside the timezonedb shared object. When Apache restarts, the timezonedb SO gets unloaded but the date extension still holds the now-stale pointer. On re-initialization, the date extension starts before timezonedb (due to the dependency order) and tries to validate the "UTC" timezone against the invalid pointer, causing a segfault in seek_to_tz_position.

The fix registers the existing PHP_MSHUTDOWN_FUNCTION in the module entry and resets the tzdb pointer back to PHP's builtin timezone database before the SO is unloaded.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses an Apache restart segfault caused by the date extension retaining a tzdb pointer that references memory inside the timezonedb shared object after it has been unloaded.

Changes:

  • Registers the module’s PHP_MSHUTDOWN_FUNCTION in the zend_module_entry.
  • Resets the date extension tzdb pointer back to PHP’s builtin timezone database during module shutdown.
  • Exposes the PHP_MSHUTDOWN_FUNCTION(timezonedb) declaration in the public header.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
timezonedb.c Hooks MSHUTDOWN and restores tzdb pointer to builtin DB before unload to prevent stale-pointer crashes.
php_timezonedb.h Adds the PHP_MSHUTDOWN_FUNCTION(timezonedb) prototype to match the module entry wiring.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@derickr derickr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to break the Windows builds?

@rlerdorf

rlerdorf commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

This seems to break the Windows builds?

It is because timelib_builtin_db() is declared in ext/date/lib/timelib.h with no export macro. On Linux everything is "exported" so it works there. But there is a bigger problem.

ext/date keeps the external timezone database in a plain C global (php_date.c):

timelib_tzdb *php_date_global_timezone_db;
int php_date_global_timezone_db_enabled;

An extension such as pecl/timezonedb sets it via php_date_set_tzdb(), pointing it at static data inside the extension's shared object. There is no API to unset it, and ext/date never clears it on shutdown.

The bug

PHP_MINIT_FUNCTION(date) clears the global, but only after registering INI entries:

PHP_MINIT_FUNCTION(date)
{
        REGISTER_INI_ENTRIES();          /* may validate date.timezone against the tzdb */
        date_register_classes();
        register_php_date_symbols(module_number);

        php_date_global_timezone_db = NULL;      /* reset happens too late */
        php_date_global_timezone_db_enabled = 0;
        ...
}

REGISTER_INI_ENTRIES() invokes the date.timezone modify handler, which validates the timezone name against DATE_TIMEZONEDB a macro that prefers php_date_global_timezone_db when it is non-NULL.

Crash scenario (mod_php / embedded SAPIs)

  1. Apache loads libphp, which dlopens timezonedb.so; its MINIT points php_date_global_timezone_db into the extension's data.
  2. On graceful restart, PHP shuts down and timezonedb.so is dlclosed, but libphp (and its C globals) can remain resident. The global now dangles into unmapped memory.
  3. On re-init, date's MINIT runs before timezonedb's (dependency order). REGISTER_INI_ENTRIES() validates the timezone (e.g. "UTC") through the dangling pointer and segfaults in seek_to_tz_position().

Whether step 3 crashes depends on where the SO would be remapped (ASLR), so reproduction is intermittent. Reported against pecl/timezonedb as issue #16 (observed on PHP 8.2.18).

Fix

Clear the globals before anything can read them:

  • Move php_date_global_timezone_db = NULL; (and _enabled = 0) to the top of date's MINIT, before REGISTER_INI_ENTRIES(), and/or
  • Reset both globals in date's MSHUTDOWN.

Either placement removes every dereference of the stale pointer. The reset has sat after REGISTER_INI_ENTRIES() since the external-tzdb feature was introduced (2005, afccca77dd8).

Related limitation

php_date_set_tzdb() only accepts a database newer than the builtin:

if (php_version_compare(tzdb->version, builtin->version) > 0) { ... }

so an extension cannot restore the builtin db itself (same version compares equal), and timelib_builtin_db() is not PHPAPI-exported, so extensions cannot even link against it on Windows. Extensions therefore have no way to undo a php_date_set_tzdb() call; the fix must live in ext/date.

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.

3 participants