Skip to content
Open
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
1 change: 1 addition & 0 deletions allowed_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ bind! {
zend_resource,
zend_string,
zend_string_init_interned,
zend_throw_error,
zend_throw_exception_ex,
zend_throw_exception_object,
zend_type,
Expand Down
3 changes: 2 additions & 1 deletion crates/macros/src/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -972,7 +972,8 @@ fn expr_to_php_stub(expr: &Expr) -> String {
}
}

/// Returns true if the given type is nullable in PHP (i.e., it's an `Option<T>`).
/// Returns true if the given type is nullable in PHP (i.e., it's an
/// `Option<T>`).
///
/// Note: Having a default value does NOT make a type nullable. A parameter with
/// a default value is optional (can be omitted), but passing `null` explicitly
Expand Down
7 changes: 7 additions & 0 deletions docsrs_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,13 @@ unsafe extern "C" {
pub static mut zend_interrupt_function:
::std::option::Option<unsafe extern "C" fn(execute_data: *mut zend_execute_data)>;
}
unsafe extern "C" {
pub fn zend_throw_error(
exception_ce: *mut zend_class_entry,
format: *const ::std::os::raw::c_char,
...
);
}
unsafe extern "C" {
pub static mut zend_standard_class_def: *mut zend_class_entry;
}
Expand Down
3 changes: 2 additions & 1 deletion src/builders/ini.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,8 @@ impl AsRef<str> for IniBuilder {
}
}

// Ensure the C buffer is properly deinitialized when the builder goes out of scope.
// Ensure the C buffer is properly deinitialized when the builder goes out of
// scope.
impl Drop for IniBuilder {
fn drop(&mut self) {
if !self.value.is_null() {
Expand Down
28 changes: 17 additions & 11 deletions src/builders/sapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ impl SapiBuilder {
///
/// # Parameters
///
/// * `func` - The function to be called when PHP gets an environment variable.
/// * `func` - The function to be called when PHP gets an environment
/// variable.
pub fn getenv_function(mut self, func: SapiGetEnvFunc) -> Self {
self.module.getenv = Some(func);
self
Expand Down Expand Up @@ -196,7 +197,8 @@ impl SapiBuilder {

/// Sets the send headers function for this SAPI
///
/// This function is called once when all headers are finalized and ready to send.
/// This function is called once when all headers are finalized and ready to
/// send.
///
/// # Arguments
///
Expand Down Expand Up @@ -230,7 +232,8 @@ impl SapiBuilder {
///
/// # Parameters
///
/// * `func` - The function to be called when PHP registers server variables.
/// * `func` - The function to be called when PHP registers server
/// variables.
pub fn register_server_variables_function(
mut self,
func: SapiRegisterServerVariablesFunc,
Expand Down Expand Up @@ -291,8 +294,8 @@ impl SapiBuilder {

/// Sets the pre-request init function for this SAPI
///
/// This function is called before request activation and before POST data is read.
/// It is typically used for .user.ini processing.
/// This function is called before request activation and before POST data
/// is read. It is typically used for .user.ini processing.
///
/// # Parameters
///
Expand Down Expand Up @@ -455,7 +458,8 @@ pub type SapiGetUidFunc = extern "C" fn(uid: *mut uid_t) -> c_int;
/// A function to be called when PHP gets the gid
pub type SapiGetGidFunc = extern "C" fn(gid: *mut gid_t) -> c_int;

/// A function to be called before request activation (used for .user.ini processing)
/// A function to be called before request activation (used for .user.ini
/// processing)
#[cfg(php85)]
pub type SapiPreRequestInitFunc = extern "C" fn() -> c_int;

Expand Down Expand Up @@ -485,8 +489,9 @@ mod test {
extern "C" fn test_getenv(_name: *const c_char, _name_length: usize) -> *mut c_char {
ptr::null_mut()
}
// Note: C-variadic functions are unstable in Rust, so we can't test this properly
// extern "C" fn test_sapi_error(_type: c_int, _error_msg: *const c_char, _args: ...) {}
// Note: C-variadic functions are unstable in Rust, so we can't test this
// properly extern "C" fn test_sapi_error(_type: c_int, _error_msg: *const
// c_char, _args: ...) {}
extern "C" fn test_send_header(_header: *mut sapi_header_struct, _server_context: *mut c_void) {
}
extern "C" fn test_send_headers(_sapi_headers: *mut sapi_headers_struct) -> c_int {
Expand Down Expand Up @@ -633,9 +638,10 @@ mod test {
);
}

// Note: Cannot test sapi_error_function because C-variadic functions are unstable in Rust
// The sapi_error field accepts a function with variadic arguments which cannot be
// created in stable Rust. However, the builder method itself works correctly.
// Note: Cannot test sapi_error_function because C-variadic functions are
// unstable in Rust The sapi_error field accepts a function with variadic
// arguments which cannot be created in stable Rust. However, the builder
// method itself works correctly.

#[test]
fn test_send_header_function() {
Expand Down
3 changes: 2 additions & 1 deletion src/closure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ impl Closure {
/// function.
///
/// If the class has already been built, this function returns early without
/// doing anything. This allows for safe repeated calls in test environments.
/// doing anything. This allows for safe repeated calls in test
/// environments.
///
/// # Panics
///
Expand Down
5 changes: 3 additions & 2 deletions src/embed/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,9 @@ mod tests {
#[test]
fn test_eval_bailout() {
Embed::run(|| {
// TODO: For PHP 8.5, this needs to be replaced, as `E_USER_ERROR` is deprecated.
// Currently, this seems to still be the best way to trigger a bailout.
// TODO: For PHP 8.5, this needs to be replaced, as `E_USER_ERROR` is
// deprecated. Currently, this seems to still be the best way
// to trigger a bailout.
let result = Embed::eval("trigger_error(\"Fatal error\", E_USER_ERROR);");

assert!(result.is_err());
Expand Down
9 changes: 6 additions & 3 deletions src/enum_.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! This module defines the `PhpEnum` trait and related types for Rust enums that are exported to PHP.
//! This module defines the `PhpEnum` trait and related types for Rust enums
//! that are exported to PHP.
use std::ptr;

use crate::{
Expand All @@ -19,7 +20,8 @@ pub trait RegisteredEnum {

/// # Errors
///
/// - [`Error::InvalidProperty`] if the enum does not have a case with the given name, an error is returned.
/// - [`Error::InvalidProperty`] if the enum does not have a case with the
/// given name, an error is returned.
fn from_name(name: &str) -> Result<Self>
where
Self: Sized;
Expand Down Expand Up @@ -125,7 +127,8 @@ impl EnumCase {
}
}

/// Represents the discriminant of an enum case in PHP, which can be either an integer or a string.
/// Represents the discriminant of an enum case in PHP, which can be either an
/// integer or a string.
#[derive(Debug, PartialEq, Eq)]
pub enum Discriminant {
/// An integer discriminant.
Expand Down
6 changes: 3 additions & 3 deletions src/types/array/conversions/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//! Collection type conversions for `ZendHashTable`.
//!
//! This module provides conversions between Rust collection types and PHP arrays
//! (represented as `ZendHashTable`). Each collection type has its own module for
//! better organization and maintainability.
//! This module provides conversions between Rust collection types and PHP
//! arrays (represented as `ZendHashTable`). Each collection type has its own
//! module for better organization and maintainability.
//!
//! ## Supported Collections
//!
Expand Down
8 changes: 6 additions & 2 deletions src/types/array/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,17 +446,21 @@ impl ZendHashTable {
}
ArrayKey::String(key) => {
unsafe {
// Use raw bytes directly since zend_hash_str_update takes a length.
// This allows keys with embedded null bytes (e.g. PHP property mangling).
zend_hash_str_update(
self,
CString::new(key.as_str())?.as_ptr(),
key.as_str().as_ptr().cast(),
key.len(),
&raw mut val,
)
};
}
ArrayKey::Str(key) => {
unsafe {
zend_hash_str_update(self, CString::new(key)?.as_ptr(), key.len(), &raw mut val)
// Use raw bytes directly since zend_hash_str_update takes a length.
// This allows keys with embedded null bytes (e.g. PHP property mangling).
zend_hash_str_update(self, key.as_ptr().cast(), key.len(), &raw mut val)
};
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/types/zval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -518,8 +518,8 @@ impl Zval {
self.get_type() == DataType::Ptr
}

/// Returns true if the zval is a scalar value (integer, float, string, or bool),
/// false otherwise.
/// Returns true if the zval is a scalar value (integer, float, string, or
/// bool), false otherwise.
///
/// This is equivalent to PHP's `is_scalar()` function.
#[must_use]
Expand Down
140 changes: 135 additions & 5 deletions src/zend/handlers.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
use std::{ffi::c_void, mem::MaybeUninit, os::raw::c_int, ptr};
use std::{ffi::CString, ffi::c_void, mem::MaybeUninit, os::raw::c_int, ptr};

use crate::{
class::RegisteredClass,
exception::PhpResult,
ffi::{
std_object_handlers, zend_is_true, zend_object_handlers, zend_object_std_dtor,
ext_php_rs_executor_globals, instanceof_function_slow, std_object_handlers,
zend_class_entry, zend_is_true, zend_object_handlers, zend_object_std_dtor,
zend_std_get_properties, zend_std_has_property, zend_std_read_property,
zend_std_write_property,
zend_std_write_property, zend_throw_error,
},
flags::ZvalTypeFlags,
flags::{PropertyFlags, ZvalTypeFlags},
types::{ZendClassObject, ZendHashTable, ZendObject, ZendStr, Zval},
};

Expand Down Expand Up @@ -108,6 +109,19 @@ impl ZendObjectHandlers {

Ok(match prop {
Some(prop_info) => {
// Check visibility before allowing access
let object_ce = unsafe { (*object).ce };
if !unsafe { check_property_access(prop_info.flags, object_ce) } {
let is_private = prop_info.flags.contains(PropertyFlags::Private);
unsafe {
throw_property_access_error(
T::CLASS_NAME,
prop_name.as_str()?,
is_private,
);
}
return Ok(rv);
}
prop_info.prop.get(self_, rv_mut)?;
rv
}
Expand Down Expand Up @@ -158,6 +172,19 @@ impl ZendObjectHandlers {

Ok(match prop {
Some(prop_info) => {
// Check visibility before allowing access
let object_ce = unsafe { (*object).ce };
if !unsafe { check_property_access(prop_info.flags, object_ce) } {
let is_private = prop_info.flags.contains(PropertyFlags::Private);
unsafe {
throw_property_access_error(
T::CLASS_NAME,
prop_name.as_str()?,
is_private,
);
}
return Ok(value);
}
prop_info.prop.set(self_, value_mut)?;
value
}
Expand Down Expand Up @@ -198,7 +225,19 @@ impl ZendObjectHandlers {
if val.prop.get(self_, &mut zv).is_err() {
continue;
}
props.insert(name, zv).map_err(|e| {

// Mangle property name according to visibility for debug output
// PHP convention: private = "\0ClassName\0propName", protected =
// "\0*\0propName"
let mangled_name = if val.flags.contains(PropertyFlags::Private) {
format!("\0{}\0{name}", T::CLASS_NAME)
} else if val.flags.contains(PropertyFlags::Protected) {
format!("\0*\0{name}")
} else {
name.to_string()
};

props.insert(mangled_name.as_str(), zv).map_err(|e| {
format!("Failed to insert value into properties hashtable: {e:?}")
})?;
}
Expand Down Expand Up @@ -309,3 +348,94 @@ impl ZendObjectHandlers {
}
}
}

/// Gets the current calling scope from the executor globals.
///
/// # Safety
///
/// Must only be called during PHP execution when executor globals are valid.
#[inline]
unsafe fn get_calling_scope() -> *const zend_class_entry {
let eg = unsafe { ext_php_rs_executor_globals().as_ref() };
let Some(eg) = eg else {
return ptr::null();
};
let execute_data = eg.current_execute_data;

if execute_data.is_null() {
return ptr::null();
}

let func = unsafe { (*execute_data).func };
if func.is_null() {
return ptr::null();
}

// Access the common.scope field through the union
unsafe { (*func).common.scope }
}

/// Checks if the calling scope has access to a property with the given flags.
///
/// Returns `true` if access is allowed, `false` otherwise.
///
/// # Safety
///
/// Must only be called during PHP execution when executor globals are valid.
/// The `object_ce` pointer must be valid.
#[inline]
unsafe fn check_property_access(flags: PropertyFlags, object_ce: *const zend_class_entry) -> bool {
// Public properties are always accessible
if !flags.contains(PropertyFlags::Private) && !flags.contains(PropertyFlags::Protected) {
return true;
}

let calling_scope = unsafe { get_calling_scope() };

if flags.contains(PropertyFlags::Private) {
// Private: must be called from the exact same class
return calling_scope == object_ce;
}

if flags.contains(PropertyFlags::Protected) {
// Protected: must be called from same class or a subclass
if calling_scope.is_null() {
return false;
}

// Same class check
if calling_scope == object_ce {
return true;
}

// Check if calling_scope is a subclass of object_ce
// or if object_ce is a subclass of calling_scope (for parent access)
unsafe {
instanceof_function_slow(calling_scope, object_ce)
|| instanceof_function_slow(object_ce, calling_scope)
}
} else {
true
}
}

/// Throws an error for invalid property access.
///
/// # Safety
///
/// Must only be called during PHP execution.
///
/// # Panics
///
/// Panics if the error message cannot be converted to a `CString`.
unsafe fn throw_property_access_error(class_name: &str, prop_name: &str, is_private: bool) {
let visibility = if is_private { "private" } else { "protected" };
let message = CString::new(format!(
"Cannot access {visibility} property {class_name}::${prop_name}"
))
.expect("Failed to create error message");

unsafe {
zend_throw_error(ptr::null_mut(), message.as_ptr());
}
}
Loading
Loading