deb.sury.org icon indicating copy to clipboard operation
deb.sury.org copied to clipboard

PHP segfaults on strtotime() when files for system timezone not present (e.g. PHP-FPM chroot)

Open GreenReaper opened this issue 2 years ago • 6 comments

Frequently asked questions

Describe the bug PHP 8.1.5 crashes on strtotime() with a SIGSEGV if the file associated with the selected timezone or the fallback (UTC) is not readable at the expected location, e.g. if it is not exposed to a chroot.

Program received signal SIGSEGV, Segmentation fault.
0x00005555556995e3 in timelib_fetch_timezone_offset (tz=tz@entry=0x0, ts=ts@entry=1650857288, transition_time=transition_time@entry=0x7fffffffba80)
    at ./ext/date/lib/parse_tz.c:1333
1333    ./ext/date/lib/parse_tz.c: No such file or directory.
(gdb) bt
#0  0x00005555556995e3 in timelib_fetch_timezone_offset (tz=tz@entry=0x0, ts=ts@entry=1650857288, transition_time=transition_time@entry=0x7fffffffba80)
    at ./ext/date/lib/parse_tz.c:1333
#1  0x0000555555699764 in timelib_get_time_zone_info (ts=ts@entry=1650857288, tz=tz@entry=0x0) at ./ext/date/lib/parse_tz.c:1423
#2  0x000055555569c32f in timelib_unixtime2local (tm=tm@entry=0x7fffb0fe1b00, ts=1650857288) at ./ext/date/lib/unixtime2tm.c:141
#3  0x000055555567319b in zif_strtotime (execute_data=0x7ffff50154d0, return_value=0x7ffff5015410) at ./ext/date/php_date.c:1059
#4  0x0000555555887edc in ZEND_DO_ICALL_SPEC_RETVAL_USED_HANDLER () at ./Zend/zend_vm_execute.h:1297
#5  execute_ex (ex=0x0) at ./Zend/zend_vm_execute.h:55404
#6  0x000055555588dcad in zend_execute (op_array=0x7ffff5088000, return_value=0x0) at ./Zend/zend_vm_execute.h:59771
#7  0x00005555558201ed in zend_execute_scripts (type=type@entry=8, retval=retval@entry=0x0, file_count=file_count@entry=3) at ./Zend/zend.c:1792
#8  0x00005555557bbd01 in php_execute_script (primary_file=primary_file@entry=0x7fffffffe190) at ./main/main.c:2538
#9  0x000055555566aa5d in main (argc=<optimized out>, argv=<optimized out>) at ./sapi/fpm/fpm/fpm_main.c:1914

Initially I reported this as derickr/timelib#126 but the developer suggested it might be due to distro patches, and indeed Debian patches the code to use the system timezone database (and localtime) for reasons described here and in more depth here.

I'm no gdb expert, but I think the above means that timelib_tzinfo *tz = tm->tz_info; is null in timelib_unixtime2local(). The functions do not appear to be protected against null pointer dereference, and while the upstream developer was receptive to doing so: https://github.com/php/php-src/commit/87f341b1c2841062128290e56991924c6f73a5bd and https://github.com/php/php-src/commit/b461c4673bc15372556d40ab5e8ac9a0d02e7704 - it doesn't make sense to call it without a valid pointer.

Perhaps the issue is in the patched-in function seek_to_tz_position(), in that it returns 0 if it can't map the file:

+		orig = map_tzfile(timezone, maplen);
+		if (orig == NULL) {
+			return 0;
+		}

...rather than fall back to, say return inmem_seek_to_tz_position(tzf, timezone, &timezonedb_builtin);

However, this might not be enough by itself considering that the patched timelib_parse_tzfile() uses memmap.

To Reproduce

Accessing a php file containing <?php echo strtotime("now") . "\n"; ?> via php-fpm in chroot triggers the issue, when either:

  • The chroot does not contain a map or copy of /usr/share/zoneinfo/ in the expected location, or
  • It does, but the zone file is not present, e.g. mv /usr/share/zoneinfo/Europe/Berlin /usr/share/zoneinfo/Europe/Berlin2
  • If date.timezone is not valid it falls back to UTC, then mv /usr/share/zoneinfo/UTC /usr/share/zoneinfo/UTC2 breaks it.

The latter two break in the PHP CLI, i.e. it's not specifically a PHP-FPM issue, but a timezone issue most likely seen in chroot.

This is dynamic; you can start php8.1-fpm, load the page, it works, move the file and reload, it crashes, move it back, it works.

The nginx PHP processing block for this setup is:

       location ~ ".php$" {
                include /etc/nginx/fastcgi_params;
                fastcgi_split_path_info ^(.+\.php)(/.+)$;
                fastcgi_pass unix:/run/php/php8.1-fpm-sitename.sock;
                fastcgi_param SCRIPT_FILENAME $fastcgi_script_name;

                # Buffer response up to 128kb in 4kb chunks
                fastcgi_buffers 32 4k;
        }

date.timezone is set to Europe/Berlin in /etc/php/8.1/fpm/php.ini The pool definition in /etc/php/8.1/fpm/pool.d includes chroot = /var/www/sitename

The backtrace was captured by attaching gdb to the FPM child processes, after running the main process with: gdb --args /usr/sbin/php-fpm8.1 --nodaemonize --fpm-config /etc/php/8.1/fpm/php-fpm.conf

If php8.1-xdebug is installed, the backtrace changes, but the closest part of the stack remains the same.

Expected behavior

PHP should fall back to a safe default and not crash, but give a warning about the inability to access the system timezone database, especially if this results in a timezone that is not the user-selected timezone.

Even better if it knows it's in a chroot, in which case it might suggest something like what I did to fix it (using a bind mount):

  • mkdir -p /var/www/sitename/usr/share/zoneinfo
  • edit /etc/fstab:
/usr/share/zoneinfo     /var/www/sitename/usr/share/zoneinfo        none    bind,ro,noatime 0       0
  • mount /var/www/sitename/usr/share/zoneinfo

Part of the solution may involve changing the pool config template. It encourages use of chroot (hence I tried it), while noting that:

; Note: chrooting is a great security feature and should be used whenever
;       possible. However, all PHP paths will be relative to the chroot
;       (error_log, sessions.save_path, ...).

The system timezone database isn't mentioned here since upstream doesn't use it. Since Drupal does, it could mention it, and maybe offer a hint as to how to expose it.

Distribution (please complete the following information):

  • OS: Debian Bullseye [with some backports packages]
  • Architecture: amd64
  • Repository: packages.sury.org/php/ bullseye main

Package(s) (please complete the following information): php8.1-fpm: 8.1.5-1+0~20220422.16+debian11~1.gbpfcaf04 Using PHP 8.1.5 (fpm-fcgi) (built: Apr 22 2022 04:56:05)

Additional context

I'm trying to move a legacy Drupal 6 installation from a server running PHP 5.6 on FreeBSD to a Drupal Bullseye server running PHP 8.1 (or another supported version, if necessary). After a few rounds of code fixes for various deprecations, I ran into the above.

GreenReaper avatar Apr 26 '22 15:04 GreenReaper

@remicollet This seems like a genuine issue? Do you want me to patch the patch and send it back, or do you want to fix this yourself?

oerdnj avatar Jun 25 '22 08:06 oerdnj

@oerdnj path welcome, will review it (more eyes can be useful)

remicollet avatar Jun 26 '22 05:06 remicollet

I cannot reproduce from CLI

Using

<?php
define('TESTZ', 'Europe/Oslo');
define('ZONE', '/usr/share/zoneinfo/' . TESTZ);

if (file_exists(ZONE.'old')) {
	rename(ZONE.'old', ZONE);
	printf("DEBUG: %s restored\n", ZONE);
} else {
	rename(ZONE, ZONE.'old');
	printf("DEBUG: %s moved\n", ZONE);
}
// $tz = DateTimeZone::listIdentifiers();
//print_r($tz);
//printf("DEBUG: %d zones, version %s, %s Oslo\n", count($tz), timezone_version_get(), in_array(TESTZ, $tz) ? 'with' : 'Without');


try {
	printf("DEBUG: strtotime=%s\n", strtotime("now"));
} catch (Error $e) {
	printf("DEBUG: exception = %sd\n", $e->getMessage());
}
try {
	if ($z = new DateTimezone(TESTZ)) {
		if ($d = new DateTime('now', $z)) {
			printf("DEBUG: %s\n", $d->format('r'));
		} else {
			echo "Cannot create DateTime\n";
		}
	} else {
		echo "Cannot create DateTimezone\n";
	}
} catch (Exception $e) {
	printf("DEBUG: exception = %sd\n", $e->getMessage());
}

I got

DEBUG: 425 zones, version 2022.1, with Oslo
DEBUG: /usr/share/zoneinfo/Europe/Oslo moved
DEBUG: exception = Timezone database is corrupt. Please file a bug report as this should never happen
DEBUG: exception = DateTimeZone::__construct(): Unknown or bad timezone (Europe/Oslo)

P.S. tested with 8.1.8RC1 and latest version r22 of the patch

remicollet avatar Jun 27 '22 06:06 remicollet

It probably won't show up the same way because the commits I mentioned above trying to at least not crash were merged into 8.1.6 and 8.0.20. This appears to have been done without a changelog entry, perhaps on the view that it is not a bug in PHP, but rather in some distributions, as the commit message suggests. They may have the original effect of crashing on 7.x and 5.x as I did not immediately see a patch for those platforms.

On all platforms, there is no documentary advice that chroots will fail to have timezone information without separate installation or mapping of the timezone database, which I guess the timezone database is corrupt message is hinting at - normally it'd be built-in, so not subject to breaking separately.

GreenReaper avatar Jun 27 '22 15:06 GreenReaper

At least a CLI reproducer will be welcome, as I wasn't able to reproduce the segfault in any version (5.6 to 8.1)

remicollet avatar Jun 28 '22 05:06 remicollet

From a quick look at the code, I think only 8.0.0 to 8.0.19 and 8.1.0 to 8.1.6 could be affected.

remicollet avatar Jun 28 '22 05:06 remicollet