Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -291,15 +291,27 @@ jobs:
- name: Test with embed feature
run: cargo test --workspace --release --features closure,embed,anyhow,smartstring,observer,indexmap --no-fail-fast

- name: Build with static TSRMLS cache
- name: Assert dynamic get_module exports
if: matrix.phpts == 'ts'
run: |
cargo build --release --example hello_world
nm -D --defined-only target/release/examples/libhello_world.so | grep -q ' T get_module$'
nm -D --defined-only target/release/examples/libhello_world.so | grep -q ' T hello_world_get_module$'

- name: Build in static extension mode
if: matrix.phpts == 'ts'
# Compile-only coverage of the ZEND_ENABLE_STATIC_TSRMLS_CACHE branches
# in wrapper.c. Test executables cannot link in this mode against a
# in wrapper.c and of the EXT_PHP_RS_STATIC_EXT symbol gating in
# #[php_module]. Test executables cannot link in this mode against a
# prebuilt libphp: `_tsrm_ls_cache` only resolves when the extension is
# statically linked into the PHP binary itself.
env:
EXT_PHP_RS_STATIC_TSRMLS_CACHE: "1"
run: cargo build --workspace --release --features closure,embed,anyhow,smartstring,observer,indexmap
EXT_PHP_RS_STATIC_EXT: "1"
run: |
cargo build --workspace --release --features closure,embed,anyhow,smartstring,observer,indexmap
cargo build --release --example hello_world
nm -D --defined-only target/release/examples/libhello_world.so | grep -q ' T hello_world_get_module$'
! nm -D --defined-only target/release/examples/libhello_world.so | grep -q ' T get_module$'

build-musl:
name: musl / ${{ matrix.php }} / ${{ matrix.phpts[1] }}
Expand Down
4 changes: 2 additions & 2 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ fn main() -> Result<()> {
"PHP_CONFIG",
"PATH",
"EXT_PHP_RS_ALLOWED_BINDINGS",
"EXT_PHP_RS_STATIC_TSRMLS_CACHE",
"EXT_PHP_RS_STATIC_EXT",
] {
println!("cargo:rerun-if-env-changed={env_var}");
}
Expand Down Expand Up @@ -261,7 +261,7 @@ fn main() -> Result<()> {
#[cfg(feature = "observer")]
defines.push(("EXT_PHP_RS_OBSERVER", "1"));

if env::var("EXT_PHP_RS_STATIC_TSRMLS_CACHE").is_ok_and(|v| v == "1") {
if env::var("EXT_PHP_RS_STATIC_EXT").is_ok_and(|v| v == "1") {
defines.push(("ZEND_ENABLE_STATIC_TSRMLS_CACHE", "1"));
}

Expand Down
11 changes: 6 additions & 5 deletions crates/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,20 +146,21 @@ Generates the C glue required to statically link the extension into php-src.

Writes a `config.m4`, a `php_<name>.h` header and a `<name>_glue.c` shim into an output directory.
Copy that directory to `php-src/ext/<name>/` together with the prebuilt `lib<name>.a` (built with
`crate-type = ["staticlib"]`, and `EXT_PHP_RS_STATIC_TSRMLS_CACHE=1` for ZTS builds), then run
`crate-type = ["staticlib"]` and `EXT_PHP_RS_STATIC_EXT=1`), then run
`./buildconf --force && ./configure --enable-<name>`.

Only one ext-php-rs extension can be linked into a single PHP binary: the `get_module` and
`ext_php_rs_*` symbols are fixed names, and two Rust static libraries collide on the Rust standard
library symbols. The shim relies on `__attribute__((constructor))`, so gcc or clang is required.
Only one ext-php-rs extension can be linked into a single PHP binary: the `ext_php_rs_*` symbols
are fixed names, and two Rust static libraries collide on the Rust standard library symbols. The
shim relies on `__attribute__((constructor))`, so gcc or clang is required.

USAGE:
cargo-php static-glue [OPTIONS]

OPTIONS:
--ext-name <EXT_NAME>
Name used for the php-src extension. Defaults to the library target name with dashes
replaced by underscores. Must be a valid C identifier
replaced by underscores. Must be a valid C identifier. Only affects file and configure
naming; the Rust symbols always come from the library target name

--force
Overwrite existing files in the output directory
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
source: crates/cli/src/static_glue.rs
expression: "render_config_m4(\"my_ext\")"
expression: "render_config_m4(\"my_ext\", \"my_lib\")"
---
dnl Generated by `cargo php static-glue`. Do not edit.
PHP_ARG_ENABLE([my_ext],
Expand All @@ -17,13 +17,13 @@ if test "$PHP_MY_EXT" != "no"; then
MY_EXT_RUST_LIB_DIR="$abs_srcdir/ext/my_ext"
fi

AC_MSG_CHECKING([for libmy_ext.a])
if test ! -f "$MY_EXT_RUST_LIB_DIR/libmy_ext.a"; then
AC_MSG_ERROR([libmy_ext.a not found in $MY_EXT_RUST_LIB_DIR. Build it first (cargo build --release, with EXT_PHP_RS_STATIC_TSRMLS_CACHE=1 for ZTS) and copy it there, or set MY_EXT_RUST_LIB_DIR.])
AC_MSG_CHECKING([for libmy_lib.a])
if test ! -f "$MY_EXT_RUST_LIB_DIR/libmy_lib.a"; then
AC_MSG_ERROR([libmy_lib.a not found in $MY_EXT_RUST_LIB_DIR. Build it first (EXT_PHP_RS_STATIC_EXT=1 cargo build --release) and copy it there, or set MY_EXT_RUST_LIB_DIR.])
fi
AC_MSG_RESULT([$MY_EXT_RUST_LIB_DIR/libmy_ext.a])
AC_MSG_RESULT([$MY_EXT_RUST_LIB_DIR/libmy_lib.a])

PHP_ADD_LIBRARY_WITH_PATH([my_ext], [$MY_EXT_RUST_LIB_DIR])
PHP_ADD_LIBRARY_WITH_PATH([my_lib], [$MY_EXT_RUST_LIB_DIR])
EXTRA_LIBS="$EXTRA_LIBS -lpthread -ldl -lm"

PHP_NEW_EXTENSION([my_ext], [my_ext_glue.c], [no])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
---
source: crates/cli/src/static_glue.rs
expression: "render_glue_c(\"my_ext\")"
expression: "render_glue_c(\"my_ext\", \"my_ext\")"
---
/* Generated by `cargo php static-glue`. Do not edit. */
#include "php_my_ext.h"

/* Exported by the Rust staticlib (#[php_module]). */
extern zend_module_entry *get_module(void);
/* Exported by the Rust staticlib (#[php_module]). Crate-prefixed to
avoid colliding with other extensions' get_module. */
extern zend_module_entry *my_ext_get_module(void);

zend_module_entry my_ext_module_entry;

__attribute__((constructor))
static void my_ext_fill_module_entry(void) {
my_ext_module_entry = *get_module();
my_ext_module_entry = *my_ext_get_module();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
source: crates/cli/src/static_glue.rs
expression: "render_glue_c(\"my_ext\", \"my_lib\")"
---
/* Generated by `cargo php static-glue`. Do not edit. */
#include "php_my_ext.h"

/* Exported by the Rust staticlib (#[php_module]). Crate-prefixed to
avoid colliding with other extensions' get_module. */
extern zend_module_entry *my_lib_get_module(void);

zend_module_entry my_ext_module_entry;

__attribute__((constructor))
static void my_ext_fill_module_entry(void) {
my_ext_module_entry = *my_lib_get_module();
}
62 changes: 36 additions & 26 deletions crates/cli/src/static_glue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ use std::{
/// Writes a `config.m4`, a `php_<name>.h` header and a `<name>_glue.c` shim
/// into an output directory. Copy that directory to `php-src/ext/<name>/`
/// together with the prebuilt `lib<name>.a` (built with `crate-type =
/// ["staticlib"]`, and `EXT_PHP_RS_STATIC_TSRMLS_CACHE=1` for ZTS builds),
/// then run `./buildconf --force && ./configure --enable-<name>`.
/// ["staticlib"]` and `EXT_PHP_RS_STATIC_EXT=1`), then run
/// `./buildconf --force && ./configure --enable-<name>`.
///
/// Only one ext-php-rs extension can be linked into a single PHP binary:
/// the `get_module` and `ext_php_rs_*` symbols are fixed names, and two Rust
/// static libraries collide on the Rust standard library symbols. The shim
/// relies on `__attribute__((constructor))`, so gcc or clang is required.
/// the `ext_php_rs_*` symbols are fixed names, and two Rust static libraries
/// collide on the Rust standard library symbols. The shim relies on
/// `__attribute__((constructor))`, so gcc or clang is required.
#[derive(Parser)]
pub struct StaticGlue {
/// Path to the Cargo manifest of the extension. Defaults to the manifest
Expand All @@ -30,6 +30,8 @@ pub struct StaticGlue {
out: Option<PathBuf>,
/// Name used for the php-src extension. Defaults to the library target
/// name with dashes replaced by underscores. Must be a valid C identifier.
/// Only affects file and configure naming; the Rust symbols always come
/// from the library target name.
#[arg(long)]
ext_name: Option<String>,
/// Overwrite existing files in the output directory.
Expand All @@ -39,20 +41,21 @@ pub struct StaticGlue {

impl StaticGlue {
pub fn handle(self) -> AResult<()> {
let ext_name = match self.ext_name {
Some(name) => name,
None => find_staticlib_target(self.manifest.as_deref())?.replace('-', "_"),
};
let lib_name = find_staticlib_target(self.manifest.as_deref())?.replace('-', "_");
Comment thread
ptondereau marked this conversation as resolved.
let ext_name = self.ext_name.unwrap_or_else(|| lib_name.clone());
validate_ext_name(&ext_name)?;

let out_dir = self.out.unwrap_or_else(|| PathBuf::from(&ext_name));
fs::create_dir_all(&out_dir)
.with_context(|| format!("Failed to create output directory {}", out_dir.display()))?;

let files = [
("config.m4", render_config_m4(&ext_name)),
("config.m4", render_config_m4(&ext_name, &lib_name)),
(&format!("php_{ext_name}.h"), render_header(&ext_name)),
(&format!("{ext_name}_glue.c"), render_glue_c(&ext_name)),
(
&format!("{ext_name}_glue.c"),
render_glue_c(&ext_name, &lib_name),
),
];

for (name, content) in &files {
Expand All @@ -71,15 +74,16 @@ impl StaticGlue {
println!(
"\nNext steps:\n\
1. Build the static library, using the php-config of a PHP build matching the one you will link into:\n\
\x20 PHP_CONFIG=/path/to/php-config EXT_PHP_RS_STATIC_TSRMLS_CACHE=1 cargo build --release\n\
\x20 (EXT_PHP_RS_STATIC_TSRMLS_CACHE=1 is required for ZTS targets, harmless otherwise)\n\
\x20 PHP_CONFIG=/path/to/php-config EXT_PHP_RS_STATIC_EXT=1 cargo build --release\n\
\x20 (EXT_PHP_RS_STATIC_EXT=1 hides the unprefixed get_module export and enables the static TSRMLS cache needed on ZTS)\n\
2. Copy the glue into php-src:\n\
\x20 cp -r {out} /path/to/php-src/ext/{name}\n\
\x20 cp target/release/lib{name}.a /path/to/php-src/ext/{name}/\n\
\x20 cp target/release/lib{lib}.a /path/to/php-src/ext/{name}/\n\
3. Rebuild the configure script and enable the extension:\n\
\x20 cd /path/to/php-src && ./buildconf --force && ./configure --enable-{name} <other flags>",
out = out_dir.display(),
name = ext_name,
lib = lib_name,
);

Ok(())
Expand Down Expand Up @@ -142,24 +146,25 @@ fn render_header(name: &str) -> String {
)
}

fn render_glue_c(name: &str) -> String {
fn render_glue_c(name: &str, symbol: &str) -> String {
format!(
"/* Generated by `cargo php static-glue`. Do not edit. */\n\
#include \"php_{name}.h\"\n\
\n\
/* Exported by the Rust staticlib (#[php_module]). */\n\
extern zend_module_entry *get_module(void);\n\
/* Exported by the Rust staticlib (#[php_module]). Crate-prefixed to\n\
\x20\x20\x20avoid colliding with other extensions' get_module. */\n\
extern zend_module_entry *{symbol}_get_module(void);\n\
\n\
zend_module_entry {name}_module_entry;\n\
\n\
__attribute__((constructor))\n\
static void {name}_fill_module_entry(void) {{\n\
\t{name}_module_entry = *get_module();\n\
\t{name}_module_entry = *{symbol}_get_module();\n\
}}\n"
)
}

fn render_config_m4(name: &str) -> String {
fn render_config_m4(name: &str, lib_name: &str) -> String {
let upper = name.to_uppercase();
format!(
"dnl Generated by `cargo php static-glue`. Do not edit.\n\
Expand All @@ -177,13 +182,13 @@ fn render_config_m4(name: &str) -> String {
\x20 {upper}_RUST_LIB_DIR=\"$abs_srcdir/ext/{name}\"\n\
\x20 fi\n\
\n\
\x20 AC_MSG_CHECKING([for lib{name}.a])\n\
\x20 if test ! -f \"${upper}_RUST_LIB_DIR/lib{name}.a\"; then\n\
\x20 AC_MSG_ERROR([lib{name}.a not found in ${upper}_RUST_LIB_DIR. Build it first (cargo build --release, with EXT_PHP_RS_STATIC_TSRMLS_CACHE=1 for ZTS) and copy it there, or set {upper}_RUST_LIB_DIR.])\n\
\x20 AC_MSG_CHECKING([for lib{lib_name}.a])\n\
\x20 if test ! -f \"${upper}_RUST_LIB_DIR/lib{lib_name}.a\"; then\n\
\x20 AC_MSG_ERROR([lib{lib_name}.a not found in ${upper}_RUST_LIB_DIR. Build it first (EXT_PHP_RS_STATIC_EXT=1 cargo build --release) and copy it there, or set {upper}_RUST_LIB_DIR.])\n\
\x20 fi\n\
\x20 AC_MSG_RESULT([${upper}_RUST_LIB_DIR/lib{name}.a])\n\
\x20 AC_MSG_RESULT([${upper}_RUST_LIB_DIR/lib{lib_name}.a])\n\
\n\
\x20 PHP_ADD_LIBRARY_WITH_PATH([{name}], [${upper}_RUST_LIB_DIR])\n\
\x20 PHP_ADD_LIBRARY_WITH_PATH([{lib_name}], [${upper}_RUST_LIB_DIR])\n\
\x20 EXTRA_LIBS=\"$EXTRA_LIBS -lpthread -ldl -lm\"\n\
\n\
\x20 PHP_NEW_EXTENSION([{name}], [{name}_glue.c], [no])\n\
Expand All @@ -202,12 +207,17 @@ mod tests {

#[test]
fn glue_copies_get_module_into_static_entry_before_main() {
insta::assert_snapshot!(render_glue_c("my_ext"));
insta::assert_snapshot!(render_glue_c("my_ext", "my_ext"));
}

#[test]
fn glue_symbol_prefix_comes_from_lib_target_not_ext_name() {
insta::assert_snapshot!(render_glue_c("my_ext", "my_lib"));
}

#[test]
fn config_m4_links_prebuilt_staticlib_and_registers_static_extension() {
insta::assert_snapshot!(render_config_m4("my_ext"));
insta::assert_snapshot!(render_config_m4("my_ext", "my_lib"));
}

#[test]
Expand Down
48 changes: 27 additions & 21 deletions crates/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,35 +80,33 @@ extern crate proc_macro;
///
/// The generated read-property handler writes a fresh `zend_string` with
/// refcount=1 into the `rv` slot via `set_zval`. PHP's `Exception::getMessage`
/// (and siblings such as `getFile`) read this with `zval_get_string + RETURN_STR`,
/// which addrefs to 2 and transfers the pointer to `return_value` without
/// changing the refcount. The stack `rv` then goes out of scope without
/// (and siblings such as `getFile`) read this with `zval_get_string +
/// RETURN_STR`, which addrefs to 2 and transfers the pointer to `return_value`
/// without changing the refcount. The stack `rv` then goes out of scope without
/// `zval_ptr_dtor`, orphaning one refcount per call.
///
/// This affects any `#[php_class]` extending `\Exception` with a `#[php(prop)]`
/// field whose type allocates a `zend_string` (e.g. `String`, `Vec<u8>` when
/// converted to a binary string, or any `IntoZval` impl producing `IS_STRING_EX`)
/// — most commonly when the field shadows the parent `\Exception::$message`.
/// Direct property access (`$obj->field`) via the `FETCH_OBJ_R` opcode is
/// **not** affected because the bytecode handler properly consumes the rv ref.
/// converted to a binary string, or any `IntoZval` impl producing
/// `IS_STRING_EX`) — most commonly when the field shadows the parent
/// `\Exception::$message`. Direct property access (`$obj->field`) via the
/// `FETCH_OBJ_R` opcode is **not** affected because the bytecode handler
/// properly consumes the rv ref.
///
/// **Workarounds, in order of preference:**
///
/// 1. **Do not shadow the inherited property name.** Rename the field
/// (e.g. `payload` instead of `message`) and expose it through a
/// `#[php_method]` getter. The method-return path is not affected by
/// this leak.
/// Note: `\Exception::getMessage` is `final` in PHP, so overriding it
/// directly via `#[php_method] fn get_message(...)` is rejected at
/// class registration.
/// 1. **Do not shadow the inherited property name.** Rename the field (e.g.
/// `payload` instead of `message`) and expose it through a `#[php_method]`
/// getter. The method-return path is not affected by this leak. Note:
/// `\Exception::getMessage` is `final` in PHP, so overriding it directly via
/// `#[php_method] fn get_message(...)` is rejected at class registration.
///
/// 2. **Write the value into the parent's real property slot via
/// `zend_update_property_stringl`** (raw FFI). PHP's `getMessage`
/// then reads from real storage through `zend_std_read_property`,
/// bypassing the leaky `rv` path entirely. This requires dropping
/// `#[php(prop)]` from the shadow field and populating the parent
/// slot at construction time. See `biscuit-php` (`src/errors.rs`)
/// for a worked example.
/// `zend_update_property_stringl`** (raw FFI). PHP's `getMessage` then reads
/// from real storage through `zend_std_read_property`, bypassing the leaky
/// `rv` path entirely. This requires dropping `#[php(prop)]` from the shadow
/// field and populating the parent slot at construction time. See
/// `biscuit-php` (`src/errors.rs`) for a worked example.
///
/// Tracked by the
/// `prop_string_field_does_not_leak_on_repeated_get_message` regression test.
Expand Down Expand Up @@ -1686,7 +1684,15 @@ fn php_const_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2 {
/// get_module()` so that PHP can get this information.
///
/// The function is renamed to `get_module` if you have used another name. The
/// function is passed an instance of `ModuleBuilder` which allows you to
/// macro also exports a crate-prefixed `<crate_name>_get_module` alias, used
/// when [statically linking the extension into
/// php-src](../advanced/static_linking.md). Building with
/// `EXT_PHP_RS_STATIC_EXT=1` removes the unmangled `get_module` export (the
/// function itself stays callable from Rust) so a statically linked
/// extension cannot collide with another extension exporting `get_module`.
/// Dynamic builds (`extension=` / `dl()`) must not set the variable.
///
/// The function is passed an instance of `ModuleBuilder` which allows you to
/// register the following (if required):
///
/// - Functions, classes, and constants
Expand Down
Loading
Loading