Skip to content

Commit 23d33bb

Browse files
committed
ext/libxml: reject changing the external entity loader while it is running
The loader callback ran with a borrowed $this (non-closures are not ref-held by zend_call_function), so re-registering the loader from within its own callback freed the executing callback and its bound object, causing a use-after-free. Guard it with an in-callback flag and throw instead, as already done in ext/curl, ext/sqlite3 and ext/xml.
1 parent 840625f commit 23d33bb

3 files changed

Lines changed: 67 additions & 0 deletions

File tree

‎ext/libxml/libxml.c‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,7 +777,10 @@ static xmlParserInputPtr php_libxml_external_entity_loader(const char *URL,
777777

778778
#undef ADD_NULL_OR_STRING_KEY
779779

780+
bool old_in_callback = LIBXML(entity_loader_in_callback);
781+
LIBXML(entity_loader_in_callback) = true;
780782
zend_call_known_fcc(&LIBXML(entity_loader_callback), &retval, 3, params, /* named_params */ NULL);
783+
LIBXML(entity_loader_in_callback) = old_in_callback;
781784

782785
if (Z_ISUNDEF(retval)) {
783786
php_libxml_ctx_error(context,
@@ -1030,6 +1033,7 @@ static PHP_RINIT_FUNCTION(libxml)
10301033
* do not affect the current request.
10311034
*/
10321035
LIBXML(entity_loader_disabled) = false;
1036+
LIBXML(entity_loader_in_callback) = false;
10331037

10341038
return SUCCESS;
10351039
}
@@ -1254,6 +1258,14 @@ PHP_FUNCTION(libxml_set_external_entity_loader)
12541258
Z_PARAM_FUNC_NO_TRAMPOLINE_FREE_OR_NULL(fci, fcc)
12551259
ZEND_PARSE_PARAMETERS_END();
12561260

1261+
/* Changing the loader while it is running would free the executing
1262+
* callback (and its bound object), resulting in a use-after-free. */
1263+
if (LIBXML(entity_loader_in_callback)) {
1264+
zend_release_fcall_info_cache(&fcc);
1265+
zend_throw_error(NULL, "Cannot change the external entity loader while it is being executed");
1266+
RETURN_THROWS();
1267+
}
1268+
12571269
/* Unset old callback if it's defined */
12581270
if (ZEND_FCC_INITIALIZED(LIBXML(entity_loader_callback))) {
12591271
zend_fcc_dtor(&LIBXML(entity_loader_callback));

‎ext/libxml/php_libxml.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ ZEND_BEGIN_MODULE_GLOBALS(libxml)
4545
zend_llist *error_list;
4646
zend_fcall_info_cache entity_loader_callback;
4747
bool entity_loader_disabled;
48+
/* Set while the user entity loader callback is running, so that the
49+
* callback cannot free itself by re-registering the loader (use-after-free). */
50+
bool entity_loader_in_callback;
4851
ZEND_END_MODULE_GLOBALS(libxml)
4952

5053
typedef struct _libxml_doc_props {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
--TEST--
2+
libxml_set_external_entity_loader(): re-registering the loader from within its own callback is rejected (no use-after-free)
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
class Loader {
8+
public function load($public, $system, $context) {
9+
/* Re-enter the setter while this callback (and its bound $this) is still
10+
* on the stack. The bound object is only kept alive by the global loader
11+
* fcc, so freeing it here used to trigger a use-after-free. It must throw
12+
* instead. */
13+
try {
14+
libxml_set_external_entity_loader(null);
15+
} catch (\Error $e) {
16+
echo $e->getMessage(), "\n";
17+
}
18+
/* $this must still be valid after the rejected re-registration. */
19+
var_dump($this instanceof Loader);
20+
21+
$f = fopen("php://temp", "r+");
22+
fwrite($f, "<!ELEMENT foo (#PCDATA)>");
23+
rewind($f);
24+
return $f;
25+
}
26+
}
27+
28+
$xml = <<<XML
29+
<!DOCTYPE foo PUBLIC "-//FOO/BAR" "http://example.com/foobar">
30+
<foo>bar</foo>
31+
XML;
32+
33+
/* Pass a fresh object as a non-closure callable: after this call returns, the
34+
* only reference to it is the one held by the global entity loader. */
35+
libxml_set_external_entity_loader([new Loader, 'load']);
36+
37+
$dd = new DOMDocument;
38+
$dd->loadXML($xml);
39+
var_dump($dd->validate());
40+
41+
/* Changing the loader is allowed again once the callback is no longer running. */
42+
libxml_set_external_entity_loader(null);
43+
var_dump(libxml_get_external_entity_loader());
44+
45+
echo "Done.\n";
46+
?>
47+
--EXPECT--
48+
Cannot change the external entity loader while it is being executed
49+
bool(true)
50+
bool(true)
51+
NULL
52+
Done.

0 commit comments

Comments
 (0)