diff --git a/NEWS b/NEWS index 42534ef4c376..6fcc446e5ee0 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,10 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.4.27 +- CLI + . Fix GH-22567 (Windows ZTS CLI SAPI should refresh its TSRMLS cache during + request activation). (matyhtf) + - DOM: . Fixed use-after-free when re-constructing a DOMXPath whose php:function registrations are freed while still reachable from the cycle collector. @@ -13,6 +17,8 @@ PHP NEWS . Fixed Dom\HTMLDocument giving attributes the namespace of their element when a fragment is parsed with an xlink, xml or xmlns context element. (Ilia Alshanetsky) + . Fixed bug GH-23729 (DOMXPath::__construct() use-after-free during an + evaluation). (David Carlier) - Intl: . Fixed cloning IntlDateFormatter and MessageFormatter losing PHP-side state @@ -28,10 +34,20 @@ PHP NEWS . Fixed bug GH-23106 (mb_strpos() reads past the end of a haystack ending in a truncated UTF-8 sequence). (Lazizbek Ergashev) +- Opcache: + . Fixed OSS-Fuzz #546798343 (Heap-buffer-overflow in optimizer with + FCCs and inlining). (ndossche) + . Fixed bug GH-23693 (Tracing JIT produces wrong results for a guard on a + loop-invariant addition). (Ilia Alshanetsky) + - PDO: . Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid column index. (Ilia Alshanetsky) +- Readline: + . Fixed a heap over-read in the interactive shell prompt when cli.prompt is + set to an empty string. (Ilia Alshanetsky) + - Sockets: . Fixed socket_select() silently truncating sets larger than FD_SETSIZE on Windows. (David Carlier) @@ -40,9 +56,22 @@ PHP NEWS . Fixed a crash when SQLite3::close() is called from a userland callback. (Ilia Alshanetsky) +- Standard: + . Fixed three Windows-only proc_open() defects: an uninitialized + PROCESS_INFORMATION, an indeterminate comspec pointer after a failed + lookup, and an unchecked CreateFileA() failure. (Ilia Alshanetsky) + . Fixed out-of-bounds read when parsing a malformed MX answer in + dns_get_mx(). (Ilia Alshanetsky) + +- XSL: + . Fixed bug GH-23730 (use-after-free when XSLTProcessor::importStylesheet() + is called during a transformation). (David Carlier) + - Zip: . Fixed ZipArchive::extractTo() ignoring files given in a non-list array. (David Carlier) + . Fixed bug GH-23747 (ZipArchive::close() use-after-free from a progress or + cancel callback). (David Carlier) 24 Sep 2026, PHP 8.4.26 diff --git a/Zend/Optimizer/optimize_func_calls.c b/Zend/Optimizer/optimize_func_calls.c index ce6c43afaedb..5449535c560a 100644 --- a/Zend/Optimizer/optimize_func_calls.c +++ b/Zend/Optimizer/optimize_func_calls.c @@ -63,6 +63,7 @@ static void zend_delete_call_instructions(zend_op_array *op_array, zend_op *opli case ZEND_DO_ICALL: case ZEND_DO_UCALL: case ZEND_DO_FCALL_BY_NAME: + case ZEND_CALLABLE_CONVERT: call++; break; case ZEND_SEND_VAL: diff --git a/ext/dom/php_dom.h b/ext/dom/php_dom.h index 13f49879bb38..d399d745084b 100644 --- a/ext/dom/php_dom.h +++ b/ext/dom/php_dom.h @@ -66,6 +66,7 @@ extern zend_module_entry dom_module_entry; typedef struct dom_xpath_object { php_dom_xpath_callbacks xpath_callbacks; + uint32_t evaluation_depth; bool register_node_ns; dom_object dom; } dom_xpath_object; diff --git a/ext/dom/tests/gh23729.phpt b/ext/dom/tests/gh23729.phpt new file mode 100644 index 000000000000..93ad67fde005 --- /dev/null +++ b/ext/dom/tests/gh23729.phpt @@ -0,0 +1,73 @@ +--TEST-- +GH-23729 (Use-after-free when DOMXPath is reconstructed during an evaluation) +--CREDITS-- +djarfluka +--EXTENSIONS-- +dom +--FILE-- +__construct($GLOBALS['other']); + } catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } + return 'r'; +} + +function nested() { + echo 'nested: ', $GLOBALS['xpath']->evaluate('string(/root/b)'), PHP_EOL; + return 'n'; +} + +function test(string $class, object $doc, object $other) { + $xpath = new $class($doc); + $xpath->registerNamespace('php', 'http://php.net/xpath'); + $xpath->registerPhpFunctions(); + + $GLOBALS['xpath'] = $xpath; + $GLOBALS['other'] = $other; + + var_dump($xpath->evaluate('string(php:function("reconstruct"))')); + /* The evaluation the callback tried to destroy must still be usable. */ + var_dump($xpath->evaluate('string(/root/a)')); + /* A nested evaluation must not lift the guard of the outer one. */ + var_dump($xpath->evaluate('concat(php:function("nested"), php:function("reconstruct"))')); + var_dump($xpath->query('//b[php:function("reconstruct")]')->length); + + /* Reconstructing outside of an evaluation is still allowed. */ + $xpath->__construct($other); + var_dump($xpath->document->documentElement->nodeName); +} + +$doc = new DOMDocument(); +$doc->loadXML('12'); +$other = new DOMDocument(); +$other->loadXML(''); +test(DOMXPath::class, $doc, $other); + +$doc = Dom\XMLDocument::createFromString('12'); +$other = Dom\XMLDocument::createFromString(''); +test(Dom\XPath::class, $doc, $other); + +?> +--EXPECT-- +Error: Cannot call DOMXPath::__construct() while an XPath evaluation is in progress +string(1) "r" +string(1) "1" +nested: 2 +Error: Cannot call DOMXPath::__construct() while an XPath evaluation is in progress +string(2) "nr" +Error: Cannot call DOMXPath::__construct() while an XPath evaluation is in progress +int(1) +string(5) "other" +Error: Cannot call Dom\XPath::__construct() while an XPath evaluation is in progress +string(1) "r" +string(1) "1" +nested: 2 +Error: Cannot call Dom\XPath::__construct() while an XPath evaluation is in progress +string(2) "nr" +Error: Cannot call Dom\XPath::__construct() while an XPath evaluation is in progress +int(1) +string(5) "other" diff --git a/ext/dom/xpath.c b/ext/dom/xpath.c index 4f1b3b52714e..d4367c9aaa60 100644 --- a/ext/dom/xpath.c +++ b/ext/dom/xpath.c @@ -126,6 +126,13 @@ static void dom_xpath_construct(INTERNAL_FUNCTION_PARAMETERS, zend_class_entry * RETURN_THROWS(); } + dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS); + if (UNEXPECTED(intern->evaluation_depth > 0)) { + zend_throw_error(NULL, "Cannot call %s::__construct() while an XPath evaluation is in progress", + ZSTR_VAL(Z_OBJCE_P(ZEND_THIS)->name)); + RETURN_THROWS(); + } + DOM_GET_OBJ(docp, doc, xmlDocPtr, docobj); xmlXPathContextPtr ctx = xmlXPathNewContext(docp); @@ -134,7 +141,6 @@ static void dom_xpath_construct(INTERNAL_FUNCTION_PARAMETERS, zend_class_entry * RETURN_THROWS(); } - dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS); xmlXPathContextPtr oldctx = intern->dom.ptr; if (oldctx != NULL) { php_libxml_decrement_doc_ref((php_libxml_node_object *) &intern->dom); @@ -301,7 +307,9 @@ static void php_xpath_eval(INTERNAL_FUNCTION_PARAMETERS, int type, bool modern) ctxp->nsNr = in_scope_ns.count; } + intern->evaluation_depth++; xmlXPathObjectPtr xpathobjp = xmlXPathEvalExpression(BAD_CAST expr, ctxp); + intern->evaluation_depth--; ctxp->node = NULL; if (register_node_ns && nodep != NULL) { diff --git a/ext/opcache/jit/ir/ir_x86.dasc b/ext/opcache/jit/ir/ir_x86.dasc index ca42001a8816..f5efb66698da 100644 --- a/ext/opcache/jit/ir/ir_x86.dasc +++ b/ext/opcache/jit/ir/ir_x86.dasc @@ -1923,6 +1923,21 @@ static bool ir_match_has_mem_deps(ir_ctx *ctx, ir_ref ref, ir_ref root) return 0; } +/* A naive check if anything that emits code, and so clobbers the flags, is + * scheduled between the flags setting instruction and the fusion root */ +static bool ir_match_has_flags_deps(ir_ctx *ctx, ir_ref ref, ir_ref root) +{ + ir_ref pos = ctx->prev_ref[root]; + + while (pos > ref) { + if (ctx->ir_base[pos].op != IR_SNAPSHOT) { + return 1; + } + pos = ctx->prev_ref[pos]; + } + return pos != ref; +} + static void ir_match_fuse_load(ir_ctx *ctx, ir_ref ref, ir_ref root) { if (ir_in_same_block(ctx, ref) && @@ -3089,7 +3104,9 @@ store_int: if (IR_IS_CONST_REF(op2_insn->op2) && !IR_IS_SYM_CONST(ctx->ir_base[op2_insn->op2].op) && ctx->ir_base[op2_insn->op2].val.i64 == 0) { - if (op2_insn->op1 == insn->op2 - 1) { /* previous instruction */ + if (op2_insn->op1 == insn->op2 - 1 /* previous instruction */ + && ir_in_same_block(ctx, op2_insn->op1) + && !ir_match_has_flags_deps(ctx, insn->op2, ref)) { ir_insn *op1_insn = &ctx->ir_base[op2_insn->op1]; if ((op1_insn->op == IR_OR || op1_insn->op == IR_AND || op1_insn->op == IR_XOR) || diff --git a/ext/opcache/tests/jit/gh23693.phpt b/ext/opcache/tests/jit/gh23693.phpt new file mode 100644 index 000000000000..1540f0c1368f --- /dev/null +++ b/ext/opcache/tests/jit/gh23693.phpt @@ -0,0 +1,41 @@ +--TEST-- +GH-23693: Tracing JIT reads stale flags for a guard on a hoisted addition +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit_buffer_size=64M +opcache.jit=tracing +opcache.jit_hot_func=1 +--EXTENSIONS-- +opcache +--FILE-- +> 2; + $s = 0; + for ($dy = -1; $dy <= 1; ++$dy) { + for ($i = 0; $i < $n; ++$i) { + $ny = $y + $dy; + if ($ny < 0) { + continue; + } + if ($ny >= 4) { + continue; + } + $s += ($ny << 2) | ($i & 3); + } + } + return $s; +} +for ($k = 0; $k < 30; ++$k) { + f(16, 200); +} +var_dump(f(16, 200)); +var_dump(f(0, 200)); +?> +--EXPECT-- +int(2700) +int(1400) diff --git a/ext/opcache/tests/opt/oss_fuzz_546798343.phpt b/ext/opcache/tests/opt/oss_fuzz_546798343.phpt new file mode 100644 index 000000000000..f7f058a11652 --- /dev/null +++ b/ext/opcache/tests/opt/oss_fuzz_546798343.phpt @@ -0,0 +1,19 @@ +--TEST-- +OSS-Fuzz #546798343 (Heap-buffer-overflow in zend_delete_call_instructions with callable conversion) +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +--FILE-- + +--EXPECT-- +Done diff --git a/ext/readline/readline_cli.c b/ext/readline/readline_cli.c index ff5caee9eb7d..8319b3b43641 100644 --- a/ext/readline/readline_cli.c +++ b/ext/readline/readline_cli.c @@ -130,7 +130,7 @@ static zend_string *cli_get_prompt(char *block, char prompt) /* {{{ */ char *prompt_spec = CLIR_G(prompt) ? CLIR_G(prompt) : DEFAULT_PROMPT; bool unicode_warned = false; - do { + while (*prompt_spec) { if (*prompt_spec == '\\') { switch (prompt_spec[1]) { case '\\': @@ -198,9 +198,9 @@ static zend_string *cli_get_prompt(char *block, char prompt) /* {{{ */ smart_str_appendc(&retval, '?'); } } - } while (++prompt_spec && *prompt_spec); - smart_str_0(&retval); - return retval.s; + ++prompt_spec; + } + return smart_str_extract(&retval); } /* }}} */ diff --git a/ext/standard/dns.c b/ext/standard/dns.c index 64301ed04161..61319565da7b 100644 --- a/ext/standard/dns.c +++ b/ext/standard/dns.c @@ -1120,6 +1120,10 @@ PHP_FUNCTION(dns_get_mx) RETURN_FALSE; } cp += i; + if (cp + 10 > end) { + php_dns_free_handle(handle); + RETURN_FALSE; + } GETSHORT(type, cp); cp += INT16SZ + INT32SZ; GETSHORT(i, cp); @@ -1127,6 +1131,10 @@ PHP_FUNCTION(dns_get_mx) cp += i; continue; } + if (cp + 2 > end) { + php_dns_free_handle(handle); + RETURN_FALSE; + } GETSHORT(weight, cp); if ((i = dn_expand(answer.qb2, end, cp, buf, sizeof(buf)-1)) < 0) { php_dns_free_handle(handle); diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c index d2d51de5a856..bd4cf7a0a0ee 100644 --- a/ext/standard/proc_open.c +++ b/ext/standard/proc_open.c @@ -695,7 +695,7 @@ static void init_startup_info(STARTUPINFOW *si, descriptorspec_item *descriptors static void init_process_info(PROCESS_INFORMATION *pi) { - memset(&pi, 0, sizeof(pi)); + memset(pi, 0, sizeof(*pi)); } /* on success, returns length of *comspec, which then needs to be efree'd by caller */ @@ -746,7 +746,7 @@ static size_t find_comspec_nt(wchar_t **comspec) static zend_result convert_command_to_use_shell(wchar_t **cmdw, size_t cmdw_len) { - wchar_t *comspec; + wchar_t *comspec = NULL; size_t len = find_comspec_nt(&comspec); if (len == 0) { php_error_docref(NULL, E_WARNING, "Command conversion failed"); @@ -829,7 +829,7 @@ static zend_result set_proc_descriptor_to_blackhole(descriptorspec_item *desc) #ifdef PHP_WIN32 desc->childend = CreateFileA("nul", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); - if (desc->childend == NULL) { + if (desc->childend == INVALID_HANDLE_VALUE) { php_error_docref(NULL, E_WARNING, "Failed to open nul"); return FAILURE; } diff --git a/ext/xsl/php_xsl.h b/ext/xsl/php_xsl.h index 36bd9cc72844..ac4ac5fc2fba 100644 --- a/ext/xsl/php_xsl.h +++ b/ext/xsl/php_xsl.h @@ -55,6 +55,7 @@ extern zend_module_entry xsl_module_entry; typedef struct xsl_object { void *ptr; HashTable *parameter; + uint32_t transform_depth; bool hasKeys; php_libxml_ref_obj *sheet_ref_obj; zend_long securityPrefs; diff --git a/ext/xsl/tests/gh23730.phpt b/ext/xsl/tests/gh23730.phpt new file mode 100644 index 000000000000..893f6277734b --- /dev/null +++ b/ext/xsl/tests/gh23730.phpt @@ -0,0 +1,81 @@ +--TEST-- +GH-23730 (Use-after-free when a stylesheet is imported during a transformation) +--EXTENSIONS-- +dom +xsl +--CREDITS-- +djarfluka +--FILE-- +importStylesheet($GLOBALS['other']); + echo $from, ': no error', PHP_EOL; + } catch (Error $e) { + echo $from, ': ', $e::class, ': ', $e->getMessage(), PHP_EOL; + } +} + +function callback($nodes) { + import_other('callback'); + return $nodes[0]; +} + +$xml = new DOMDocument(); +$xml->registerNodeClass(DOMElement::class, MyElement::class); +$xml->loadXML('a'); + +$xsl = new DOMDocument(); +$xsl->loadXML(<< + + + +XML); + +$other = new DOMDocument(); +$other->loadXML('REPLACED'); + +$proc = new XSLTProcessor(); +$proc->registerPHPFunctions(); +$proc->importStylesheet($xsl); + +$GLOBALS['proc'] = $proc; +$GLOBALS['other'] = $other; + +var_dump($proc->transformToXml($xml)); +var_dump($proc->transformToDoc($xml)->textContent); + +$uri = tempnam(sys_get_temp_dir(), 'gh23730'); +var_dump($proc->transformToUri($xml, $uri) > 0); +@unlink($uri); + +/* Importing outside of a transformation is still allowed. */ +var_dump($proc->importStylesheet($other)); +var_dump($proc->transformToXml($xml)); + +?> +--EXPECT-- +callback: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress +destructor: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress +string(24) " +a +" +callback: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress +destructor: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress +string(1) "a" +callback: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress +destructor: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress +bool(true) +bool(true) +string(31) " +REPLACED +" diff --git a/ext/xsl/xsltprocessor.c b/ext/xsl/xsltprocessor.c index cf5a941d95ca..fa6b2d41e601 100644 --- a/ext/xsl/xsltprocessor.c +++ b/ext/xsl/xsltprocessor.c @@ -175,6 +175,12 @@ PHP_METHOD(XSLTProcessor, importStylesheet) RETURN_THROWS(); } + xsl_object *intern = Z_XSL_P(id); + if (UNEXPECTED(intern->transform_depth > 0)) { + zend_throw_error(NULL, "Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress"); + RETURN_THROWS(); + } + nodep = php_libxml_import_node(docp); if (nodep == NULL) { zend_argument_type_error(1, "must be a valid XML node"); @@ -251,8 +257,6 @@ PHP_METHOD(XSLTProcessor, importStylesheet) RETURN_FALSE; } - xsl_object *intern = Z_XSL_P(id); - /* Detach object */ clone_lxml_obj->document->ptr = NULL; /* The namespace mappings need to be kept alive. @@ -333,6 +337,8 @@ static xmlDocPtr php_xsl_apply_stylesheet(zval *id, xsl_object *intern, xsltStyl return NULL; } + intern->transform_depth++; + if (intern->profiling) { if (php_check_open_basedir(ZSTR_VAL(intern->profiling))) { f = NULL; @@ -438,6 +444,8 @@ static xmlDocPtr php_xsl_apply_stylesheet(zval *id, xsl_object *intern, xsltStyl efree(intern->doc); intern->doc = NULL; + intern->transform_depth--; + return newdocp; } diff --git a/ext/zip/php_zip.c b/ext/zip/php_zip.c index 682736d4fe76..42a5a527cfc7 100644 --- a/ext/zip/php_zip.c +++ b/ext/zip/php_zip.c @@ -1582,8 +1582,16 @@ PHP_METHOD(ZipArchive, open) if (ze_obj->archive) { /* we already have an opened zip, free it */ + if (ze_obj->archive->close) { + efree(resolved_path); + zend_throw_error(NULL, "Already being closed"); + RETURN_THROWS(); + } intern = ze_obj->archive->za; - if (zip_close(intern) != 0) { + ze_obj->archive->close = true; + err = zip_close(intern); + ze_obj->archive->close = false; + if (err != 0) { php_error_docref(NULL, E_WARNING, "Empty string as source"); efree(resolved_path); RETURN_FALSE; @@ -1668,7 +1676,14 @@ PHP_METHOD(ZipArchive, close) ze_obj = Z_ZIP_P(self); + if (ze_obj->archive->close) { + zend_throw_error(NULL, "Already being closed"); + RETURN_THROWS(); + } + + ze_obj->archive->close = true; err = zip_close(intern); + ze_obj->archive->close = false; if (err) { php_error_docref(NULL, E_WARNING, "%s", zip_strerror(intern)); /* Save error for property reader */ diff --git a/ext/zip/php_zip.h b/ext/zip/php_zip.h index 8385674a1cf0..e5930c87aada 100644 --- a/ext/zip/php_zip.h +++ b/ext/zip/php_zip.h @@ -73,6 +73,7 @@ typedef struct _php_zip_archive { /* libzip reads buffers until the archive is closed, can outlive the object. */ char **buffers; int buffers_cnt; + bool close; #ifdef HAVE_PROGRESS_CALLBACK zval progress_callback; #endif diff --git a/ext/zip/tests/gh23747.phpt b/ext/zip/tests/gh23747.phpt new file mode 100644 index 000000000000..24e977410e17 --- /dev/null +++ b/ext/zip/tests/gh23747.phpt @@ -0,0 +1,102 @@ +--TEST-- +GH-23747 (ZipArchive::close() from inside a progress or cancel callback causes segv) +--CREDITS-- +djarfluka +--EXTENSIONS-- +zip +--SKIPIF-- + +--FILE-- +open($filename, ZipArchive::CREATE | ZipArchive::OVERWRITE); + for ($i = 0; $i < 64; $i++) { + $zip->addFromString("f$i.txt", str_repeat('x', 2000)); + } +} + +$filename = __DIR__ . '/gh23747.zip'; + +$zip = new ZipArchive(); +populate($zip, $filename); +$zip->registerProgressCallback(0.0, function ($rate) use ($zip) { + static $done = false; + if (!$done) { + $done = true; + try { + $zip->close(); + } catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } + } +}); +var_dump($zip->close()); + +$zip = new ZipArchive(); +populate($zip, $filename); +$zip->registerCancelCallback(function () use ($zip) { + static $done = false; + if (!$done) { + $done = true; + try { + $zip->close(); + } catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } + } + return 0; +}); +var_dump($zip->close()); + +$zip = new ZipArchive(); +populate($zip, $filename); +$zip->registerProgressCallback(0.0, function ($rate) use ($zip) { + static $done = false; + if (!$done) { + $done = true; + try { + $zip->close(); + } catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } + } +}); +var_dump($zip->open($filename)); +var_dump($zip->count()); + +$zip = new ZipArchive(); +populate($zip, $filename); +$zip->registerProgressCallback(0.0, function ($rate) use ($zip, $filename) { + static $done = false; + if (!$done) { + $done = true; + try { + $zip->open($filename); + } catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } + } +}); +var_dump($zip->close()); +?> +--CLEAN-- + +--EXPECT-- +Error: Already being closed +bool(true) +Error: Already being closed +bool(true) +Error: Already being closed +bool(true) +int(64) +Error: Already being closed +bool(true) diff --git a/sapi/cli/php_cli.c b/sapi/cli/php_cli.c index 4b19cf0604c6..6894f57c12c0 100644 --- a/sapi/cli/php_cli.c +++ b/sapi/cli/php_cli.c @@ -356,6 +356,15 @@ static void sapi_cli_log_message(const char *message, int syslog_type_int) /* {{ } /* }}} */ +static int sapi_cli_activate(void) /* {{{ */ +{ +#if defined(PHP_WIN32) && defined(ZTS) + ZEND_TSRMLS_CACHE_UPDATE(); +#endif + return SUCCESS; +} +/* }}} */ + static int sapi_cli_deactivate(void) /* {{{ */ { fflush(stdout); @@ -420,7 +429,7 @@ static sapi_module_struct cli_sapi_module = { php_cli_startup, /* startup */ php_module_shutdown_wrapper, /* shutdown */ - NULL, /* activate */ + sapi_cli_activate, /* activate */ sapi_cli_deactivate, /* deactivate */ sapi_cli_ub_write, /* unbuffered write */