Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Member access within null pointer in ext/spl/spl_observer.c #14639

Closed
YuanchengJiang opened this issue Jun 23, 2024 · 6 comments
Closed

Member access within null pointer in ext/spl/spl_observer.c #14639

YuanchengJiang opened this issue Jun 23, 2024 · 6 comments

Comments

@YuanchengJiang
Copy link

YuanchengJiang commented Jun 23, 2024

Description

The following code:

<?php
function varToString($var) {
try {
} catch (Exception $e) {
}
$vars = [$var1, $var2, $var3];
foreach ($vars as $i => $v1) {
foreach ($vars as $j => $v2) {
if ($i < $j) {
try {
} catch (Exception $e) {
$result["serialize_{$i}"] = "Error: " . $e->getMessage();
$result["serialize_{$j}"] = "Error: " . $e->getMessage();
}
try {
$result["unserialize_{$i}"] = unserialize(serialize($v1));
$result["unserialize_{$j}"] = unserialize(serialize($v2));
} catch (Exception $e) {
$result["unserialize_{$i}"] = "Error: " . $e->getMessage();
$result["unserialize_{$j}"] = "Error: " . $e->getMessage();
$result["base64_encode_{$i}"] = base64_encode(varToString($v1));
$result["base64_encode_{$j}"] = base64_encode(varToString($v2));
} catch (Exception $e) {
$result["base64_encode_{$i}"] = "Error: " . $e->getMessage();
$result["base64_encode_{$j}"] = "Error: " . $e->getMessage();
}
try {
$result["base64_decode_{$i}"] = base64_decode(base64_encode(varToString($v1)));
$result["base64_decode_{$j}"] = base64_decode(base64_encode(varToString($v2)));
} catch (Exception $e) {
$result["base64_decode_{$i}"] = "Error: " . $e->getMessage();
$result["base64_decode_{$j}"] = "Error: " . $e->getMessage();
$result["md5_{$i}"] = md5(varToString($v1));
$result["md5_{$j}"] = md5(varToString($v2));
} catch (Exception $e) {
$result["md5_{$i}"] = "Error: " . $e->getMessage();
$result["md5_{$j}"] = "Error: " . $e->getMessage();
}
try {
$result["sha1_{$i}"] = sha1(varToString($v1));
$result["sha1_{$j}"] = sha1(varToString($v2));
} catch (Exception $e) {
$result["sha1_{$i}"] = "Error: " . $e->getMessage();
$result["sha1_{$j}"] = "Error: " . $e->getMessage();
$result["url_encode_{$i}"] = urlencode(varToString($v1));
$result["url_encode_{$j}"] = urlencode(varToString($v2));
} catch (Exception $e) {
$result["url_encode_{$i}"] = "Error: " . $e->getMessage();
$result["url_encode_{$j}"] = "Error: " . $e->getMessage();
}
try {
$result["url_decode_{$i}"] = urldecode(urlencode(varToString($v1)));
$result["url_decode_{$j}"] = urldecode(urlencode(varToString($v2)));
} catch (Exception $e) {
$result["url_decode_{$i}"] = "Error: " . $e->getMessage();
$result["url_decode_{$j}"] = "Error: " . $e->getMessage();
$result["html_encode_{$i}"] = htmlspecialchars(varToString($v1));
$result["html_encode_{$j}"] = htmlspecialchars(varToString($v2));
} catch (Exception $e) {
$result["html_encode_{$i}"] = "Error: " . $e->getMessage();
$result["html_encode_{$j}"] = "Error: " . $e->getMessage();
}
try {
$result["html_decode_{$i}"] = htmlspecialchars_decode(htmlspecialchars(varToString($v1)));
$result["html_decode_{$j}"] = htmlspecialchars_decode(htmlspecialchars(varToString($v2)));
} catch (Exception $e) {
$result["html_decode_{$i}"] = "Error: " . $e->getMessage();
$result["html_decode_{$j}"] = "Error: " . $e->getMessage();
$result["strlen_{$i}"] = strlen(varToString($v1));
$result["strlen_{$j}"] = strlen(varToString($v2));
} catch (Exception $e) {
$result["strlen_{$i}"] = "Error: " . $e->getMessage();
$result["strlen_{$j}"] = "Error: " . $e->getMessage();
}
try {
$result["trim_{$i}"] = trim(varToString($v1));
$result["trim_{$j}"] = trim(varToString($v2));
} catch (Exception $e) {
$result["trim_{$i}"] = "Error: " . $e->getMessage();
$result["trim_{$j}"] = "Error: " . $e->getMessage();
$result["strtoupper_{$i}"] = strtoupper(varToString($v1));
$result["strtoupper_{$j}"] = strtoupper(varToString($v2));
} catch (Exception $e) {
$result["strtoupper_{$i}"] = "Error: " . $e->getMessage();
$result["strtoupper_{$j}"] = "Error: " . $e->getMessage();
}
try {
$result["strtolower_{$i}"] = strtolower(varToString($v1));
$result["strtolower_{$j}"] = strtolower(varToString($v2));
} catch (Exception $e) {
$result["strtolower_{$i}"] = "Error: " . $e->getMessage();
$result["strtolower_{$j}"] = "Error: " . $e->getMessage();
$result["strrev_{$i}"] = strrev(varToString($v1));
} catch (Exception $e) {
$result["abs_{$i}"] = "Error: " . $e->getMessage();
$result["abs_{$j}"] = "Error: " . $e->getMessage();
$result["rand_{$i}_{$j}"] = rand(min($i, $j), max($i, $j));
} catch (Exception $e) {
$result["rand_{$i}_{$j}"] = "Error: " . $e->getMessage();
}
try {
if (is_callable($v2)) {
$result["function_call_{$j}"] = $v2();
}
} catch (Exception $e) {
$result["function_call_{$j}"] = "Error: " . $e->getMessage();
$safe_v1 = var_export(varToString($v1), true);
$safe_v2 = var_export(varToString($v2), true);
$code = '$result["eval_' . $i . '_' . $j . '"] = ' . $safe_v1 . ' . " " . ' . $safe_v2 . ';';
eval($code);
} catch (Exception $e) {
$result["eval_error_{$i}_{$j}"] = "Error: " . $e->getMessage();
}
}
}
}
return $result;
}
$b = new SplObjectStorage();
for ($i = 10000; $i > 0; $i--) {
$object = new StdClass();
$a[] = $object;
$b->attach($object);
}
$fiber = new Fiber(function (): void {
while (true) {
}
});

Resulted in this output:

/php-src/ext/spl/spl_observer.c:121:26: runtime error: member access within null pointer of type 'spl_SplObjectStorageElement' (aka 'struct _spl_SplObjectStorageElement')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /php-src/ext/spl/spl_observer.c:121:26 in

To reproduce:

php -d "memory_limit=2M" ./test.php

PHP Version

PHP 8.4.0-dev

Operating System

ubuntu 22.04

@Girgias
Copy link
Member

Girgias commented Jun 23, 2024

Can you try and reduce the reproducible?

@YuanchengJiang
Copy link
Author

@Girgias I have reduced the case by delta debugging (might not be the minimal one). It still requires lots of junk code to be reproduced, which also confuses me.

@Girgias
Copy link
Member

Girgias commented Jun 23, 2024

Okay, I will have a look at this at one point, but I am busy with some other stuff. Does this only reproduce on master or also on PHP 8.2?

@YuanchengJiang
Copy link
Author

YuanchengJiang commented Jun 23, 2024

Not sure. I do not have 8.2 in my hands now. It reproduced in PHP 8.4.0-dev.

Hope valgrind output helps:

==788749== Process terminating with default action of signal 11 (SIGSEGV)
==788749==  Access not within mapped region at address 0x0
==788749==    at 0x625058: spl_object_storage_dtor (spl_observer.c:122)
==788749==    by 0x83A3E6: zend_hash_destroy (zend_hash.c:1741)
==788749==    by 0x624E9C: spl_SplObjectStorage_free_storage (spl_observer.c:82)
==788749==    by 0x91FCF1: zend_objects_store_free_object_storage (zend_objects_API.c:122)
==788749==    by 0x80136F: zend_shutdown_executor_values (zend_execute_API.c:401)
==788749==    by 0x801404: shutdown_executor (zend_execute_API.c:418)
==788749==    by 0x81CB14: zend_deactivate (zend.c:1308)
==788749==    by 0x76B9D9: php_request_shutdown (main.c:1886)
==788749==    by 0x9AA0E3: do_cli (php_cli.c:1136)
==788749==    by 0x9AA5F0: main (php_cli.c:1340)

@nielsdos
Copy link
Member

nielsdos commented Jul 6, 2024

I can't reproduce this yet, but because it's a NULL deref and there's a memory limit involved, I'm gonna take an educated guess and say this is a classic "publish before initialize" bug. Probably we're allocating some memory and already putting an object somewhere, allocating some more during initialization which fails. Then it bails out and we're hitting a semi-initialized object.

@nielsdos
Copy link
Member

nielsdos commented Jul 6, 2024

With a bit of fiddling, reduced to

<?php
$b = new SplObjectStorage();
for ($i = 10000; $i > 0; $i--) {
    $object = new StdClass();
    $object->a = str_repeat("a", 2);
    $b->attach($object);
}

So yeah, indeed what I thought: spl_object_storage_attach_handle creates an entry already, but only fills it in at the end with spl_object_storage_create_element which allocates memory. In this case the allocation fails and we're left with a NULL slot. Doing the allocation first isn't an option because we want to check whether the slot is occupied before allocating memory...

nielsdos added a commit to nielsdos/php-src that referenced this issue Jul 6, 2024
…erver.c

`spl_object_storage_attach_handle` creates an entry already, but only
fills it in at the end with `spl_object_storage_create_element` which
allocates memory. In this case the allocation fails and we're left with
a NULL slot. Doing the allocation first isn't an option because we want
to check whether the slot is occupied before allocating memory.
The simplest solution is to set the entry to NULL and check for a NULL
pointer upon destruction.
nielsdos added a commit that referenced this issue Jul 6, 2024
* PHP-8.2:
  Fix GH-14639: Member access within null pointer in ext/spl/spl_observer.c
nielsdos added a commit that referenced this issue Jul 6, 2024
* PHP-8.3:
  Fix GH-14639: Member access within null pointer in ext/spl/spl_observer.c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants