Jesse A. Green / notebook

PHP Interpreter (php-src) — Admin & Architecture Guide

Generated June 2026 from php/php-src at 95b5b48

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

This guide is the deep reference for the php-src codebase — the C implementation of the PHP interpreter. It is written for administrators deploying PHP runtimes, engineers building or extending PHP, and architects who need to understand how the interpreter is structured internally.

It covers the Zend Engine, the OPcache bytecode cache and JIT, the SAPI layer, the build system, production configuration (php.ini, FPM pools), security, operations, and the implementation internals down to module level.

Everything here is derived directly from the source tree at commit 95b5b487, which builds PHP 8.6.0-dev.

Note

For day-to-day use of the binaries (php, php-fpm, phpdbg) and basic configuration, start with the companion User Guide. This Admin Guide assumes that material and goes deeper.


Table of Contents

  1. Executive Summary
  2. Architecture Overview
  3. The Zend Engine
  4. OPcache & the JIT
  5. The SAPI Layer & Request Lifecycle
  6. Repository Structure
  7. Build System & Requirements
  8. Configuration Mastery
  9. PHP-FPM Operations
  10. Security
  11. Operations & Observability
  12. Extension Development Internals
  13. Testing & Quality Assurance
  14. Troubleshooting
  15. Contributing
  16. Potential Improvements
  17. Quick Reference

1. Executive Summary

PHP is a general-purpose scripting language implemented in C. The php-src repository contains the entire interpreter: the Zend Engine (compiler + virtual machine), the core runtime (main/), the bundled extension library (ext/), the server interface bindings (sapi/), and the thread-safety layer (TSRM/).

Attribute Value
Language C (with some C++ in select extensions/JIT)
Builds PHP 8.6.0-dev (Zend Engine v4.6)
License BSD-3-Clause (Modified BSD)
Build system Autotools (buildconf → configure → make) + Windows nmake
Source size ~27.9M chars across ~2,400 code files; 26,483 files total
Bundled extensions ~70 in ext/
SAPIs cli, cgi, fpm, phpdbg, embed, apache2handler, litespeed, fuzzer
Threading NTS (non-thread-safe) or ZTS (Zend Thread Safety)
JIT IR-based, embedded in OPcache (ext/opcache/jit)

Design in one paragraph: PHP source is lexed and parsed into an AST, compiled to Zend opcodes, and executed by a register-based virtual machine. OPcache persists the compiled opcodes in shared memory so they are reused across requests, and its JIT compiler can translate hot opcode sequences into native machine code via an embedded IR framework. The same engine is wrapped by multiple SAPIs that adapt it to a host (command line, FastCGI, Apache, an embedding C program). Memory is managed per-request by a custom allocator (Zend MM) that is bulk-freed at request shutdown.

Important

PHP does not provide a sandbox. Executing untrusted PHP code is always unsafe by design (see Security). Treat every PHP process as fully trusting the code it runs.


2. Architecture Overview

                         ┌──────────────────────────────────────────┐
   Request / invocation  │                 SAPI                      │
   ───────────────────▶  │  cli │ cgi │ fpm │ apache2handler │ embed │
                         └───────────────────┬──────────────────────┘
                                              │  sapi_module_struct hooks
                         ┌────────────────────▼──────────────────────┐
                         │             main/ (PHP core)               │
                         │  startup · php.ini · streams · output ·    │
                         │  superglobals · SAPI glue (main/SAPI.c)    │
                         └────────────────────┬──────────────────────┘
            ┌─────────────────────────────────┼─────────────────────────────────┐
            ▼                                  ▼                                  ▼
 ┌────────────────────┐         ┌──────────────────────────┐        ┌────────────────────┐
 │   ext/ extensions  │         │      Zend/ (engine)       │        │  ext/opcache       │
 │ standard, pdo,     │◀───────▶│ scanner→parser→AST→       │◀──────▶│  SHM opcode cache  │
 │ mbstring, curl,... │  zend   │ compiler→opcodes→VM       │  zend  │  + JIT (IR)        │
 └────────────────────┘  API    │ MM allocator · GC · OO    │  ext   └────────────────────┘
                                 └──────────────────────────┘
                         ┌────────────────────────────────────────────┐
                         │   TSRM/ (thread-safe resource manager)      │  ← ZTS builds only
                         └────────────────────────────────────────────┘

The four layers

  1. Zend Engine (Zend/) — the language core: lexer, parser, AST, compiler, opcode VM, memory manager, garbage collector, object model, and the C API that everything else builds on.
  2. PHP core (main/) — process and request lifecycle, php.ini handling, the stream abstraction, output buffering, superglobals, and the SAPI bridge (main/SAPI.c, main/SAPI.h).
  3. Extensions (ext/) — the function/class library exposed to userland, from the always-present standard/spl to optional database, crypto, and string extensions.
  4. SAPIs (sapi/) — thin adapters that embed the engine into a host environment and implement the sapi_module_struct callbacks.

TSRM/ (Thread-Safe Resource Manager) is a cross-cutting layer that, in ZTS builds, gives each thread its own copy of global state.

Note

Extensions and OPcache both talk to the engine through the Zend API (Zend/zend_API.h) and the extension hook structs (zend_module_entry, zend_extension). OPcache is itself a Zend extension that hooks the compiler.


3. The Zend Engine

The Zend Engine lives in Zend/ and is the heart of PHP. Its job: turn PHP source text into executable opcodes and run them.

3.1 The compilation pipeline

PHP source
   │
   ▼  Zend/zend_language_scanner.l   (re2c-generated lexer)
Tokens
   │
   ▼  Zend/zend_language_parser.y    (bison-generated parser)
Abstract Syntax Tree (AST)           Zend/zend_ast.c
   │
   ▼  Zend/zend_compile.c            (compiler)
Opcode arrays (zend_op_array)        Zend/zend_opcode.c
   │
   ▼  Zend/zend_execute.c            (the virtual machine)
Result
Stage File Role
Lexer zend_language_scanner.l Tokenizes source. Generated by re2c at build time.
Parser zend_language_parser.y LALR grammar. Generated by bison at build time.
AST zend_ast.c Builds and walks the abstract syntax tree.
Compiler zend_compile.c Lowers the AST into opcode arrays; handles scoping, type info, inheritance prep.
Opcodes zend_opcode.c, zend_vm_opcodes.h The opcode set and the per-function zend_op_array.

Important

The lexer and parser are generated files. After changing *.l or *.y you must have re2c and bison installed and re-run the build; ./buildconf and make regenerate them.

3.2 The virtual machine

The executor in zend_execute.c is a register-based VM over the opcode arrays. Crucially, the VM dispatch loop is itself generated:

Zend/zend_vm_def.h        ── opcode handler definitions (source of truth)
        │
        ▼  Zend/zend_vm_gen.php   (code generator, run at build time)
Zend/zend_vm_execute.h    ── the generated dispatch loop
Zend/zend_vm_opcodes.c    ── generated opcode metadata

zend_vm_gen.php specializes each opcode for its operand types (CONST/TMP/VAR/CV) to avoid runtime type checks in the hot path. This is why zend_vm_execute.h is enormous and machine-generated rather than hand-written.

Warning

Never edit zend_vm_execute.h directly — it is overwritten. Edit zend_vm_def.h (and the helpers) and regenerate with php Zend/zend_vm_gen.php.

The VM supports three dispatch models (selected at build time): CALL (function per handler), SWITCH, and the default GOTO threading where the compiler supports computed gotos (--enable-re2c-cgoto aids the related scanner path).

3.3 Values: zval and the type system

Every PHP value is a zval (Zend/zend_types.h) — a small tagged union carrying a type tag and either an inline scalar or a pointer to a reference-counted structure (string, array, object, resource). Reference-counted types share a common zend_refcounted header.

Key supporting subsystems:

Subsystem File Purpose
Hash tables zend_hash.c The HashTable — backs PHP arrays, symbol tables, and most internal maps.
Strings zend_string.h Interned, length-prefixed, reference-counted strings.
Objects zend_objects.c, zend_object_handlers.c Object storage and the per-class handler table (property access, casting, comparison).
Classes/inheritance zend_inheritance.c, zend_API.c Class linking, interface/trait composition, method tables.
Closures zend_closures.c First-class closures and Closure binding.
Enums zend_enum.c Backed/pure enumerations.
Constants zend_constants.c Compile-time and runtime constants.

3.4 Memory management

PHP uses a custom request-scoped allocator, Zend MM, in Zend/zend_alloc.c:

  • Memory is organized into chunks → pages → bins (ZEND_MM_PAGE_SIZE-based). Small allocations come from size-class bins; large ones are page-runs; huge ones go to the OS directly.
  • The default heap maps memory and is reset cheaply at request end — most per-request memory is reclaimed in bulk rather than individually freed.
  • A request's allocations are bounded by the memory_limit INI directive; exceeding it raises a fatal error.

On top of the allocator sits the cycle-collecting garbage collector (zend_gc.c). Reference counting reclaims most values immediately; the GC exists only to break reference cycles. It runs when the configurable root buffer fills (controlled by zend.enable_gc).

Tip

zend.enable_gc = On (the default) is correct for almost all workloads. Long-running CLI daemons that build large cyclic graphs are the main case to monitor; gc_collect_cycles() can be called manually there.

3.5 Concurrency primitives

Feature File Notes
Fibers zend_fibers.c Stackful coroutines (PHP 8.1+); the substrate for async libraries.
Generators zend_generators.c Lazy iterators via yield.
Observer API zend_observer.c Hook function entry/exit — used by profilers, debuggers, tracing extensions.
Atomics zend_atomic.c Low-level atomic operations.
Call stack zend_call_stack.c Stack-limit detection to convert C stack overflow into a catchable PHP error.

Note

The Observer API (zend_observer.c) is the modern, low-overhead extension point for APM/tracing tools (e.g. OpenTelemetry, Datadog, Tideways). Prefer it over older opcode-overriding hacks.


4. OPcache & the JIT

OPcache (ext/opcache/) is a Zend extension that eliminates recompilation and, optionally, JIT-compiles hot code. It is the single most important performance component in a production deployment.

4.1 The shared-memory bytecode cache

On first request, PHP compiles each script to opcodes. OPcache persists those opcodes in shared memory (SHM) so every subsequent request — across all FPM workers — reuses them without re-parsing or re-compiling.

Component File Role
Accelerator core ZendAccelerator.c Hooks the compiler; manages the SHM cache lifecycle.
SHM backends shared_alloc_mmap.c, shared_alloc_posix.c, shared_alloc_shm.c, shared_alloc_win32.c Platform-specific shared-memory allocation (opcache.preferred_memory_model).
Persist zend_persist.c, zend_persist_calc.c Serialize/deserialize opcode arrays into/out of SHM.
File cache zend_file_cache.c On-disk second-level cache (opcache.file_cache), survives restarts.
Interned strings within accelerator Deduplicate strings across scripts in SHM.
Blacklist zend_accelerator_blacklist.c Exclude files from caching (opcache.blacklist_filename).

Validation model: with opcache.validate_timestamps=1 (the default), OPcache checks each file's mtime every opcache.revalidate_freq seconds and recompiles changed files. In production you typically set validate_timestamps=0 and explicitly reset the cache on deploy.

Caution

With opcache.validate_timestamps=0, OPcache will not notice changed files. You must reload FPM (or call opcache_reset()) on every deploy, or users will keep running stale code.

4.2 Preloading

opcache.preload points at a PHP script run once at server startup that loads classes/functions permanently into SHM, available to every request with zero per-request loading cost. opcache.preload_user specifies the user to run preload as (required when starting as root).

4.3 The JIT compiler

The JIT lives in ext/opcache/jit/ and is built when --enable-opcache-jit is set (default when OPcache builds). It does not generate machine code directly from opcodes; instead it lowers them to an intermediate representation and uses an embedded IR framework (ext/opcache/jit/ir/) to optimize and emit native code.

Hot opcodes
   │
   ▼  zend_jit.c / zend_jit_trace.c    (function & tracing JIT)
   ▼  zend_jit_ir.c                    (lower to IR)
   ▼  ext/opcache/jit/ir/ir_emit.c     (IR optimizer + native codegen)
Native machine code  (x86_64 / i386 / arm64)
File Role
zend_jit.c JIT entry, function-mode compilation, configuration.
zend_jit_trace.c Tracing JIT: records hot execution traces and compiles them.
zend_jit_ir.c Translates opcodes/traces into the IR.
jit/ir/ir.c, ir_emit.c, ir_cfg.c, ir_gcm.c The IR framework: SSA construction, optimization, register allocation, code emission.

The JIT mode is configured by the 4-digit opcache.jit value (CPU/register/trigger/optimization). The common, recommended value is tracing (alias for 1254); function (1205) compiles whole functions instead.

Important

The JIT requires both an explicit opcache.jit_buffer_size (e.g. 64M) and opcache.enable=1. Without a non-zero buffer the JIT is inert even if opcache.jit is set.

Tip

The JIT helps CPU-bound code (math, image processing, tight loops) the most. Typical web workloads — already I/O-bound and well-served by the opcode cache — gain less. Benchmark your own workload before assuming a win.

4.4 JIT tuning directives

The JIT exposes a large set of trigger/threshold directives (opcache.jit_hot_loop, opcache.jit_hot_func, opcache.jit_max_trace_length, opcache.jit_blacklist_*, etc.). These rarely need changing — see the Quick Reference for the full list.


5. The SAPI Layer & Request Lifecycle

A SAPI adapts the engine to a host. Each SAPI in sapi/ provides a sapi_module_struct (main/SAPI.h) of callbacks the core invokes at well-defined points.

5.1 The sapi_module_struct hooks

Hook When called Typical job
startup Once, at process start Initialize the SAPI, register the module.
shutdown Once, at process end Tear down.
activate At the start of each request Per-request setup.
deactivate At the end of each request Per-request flush/cleanup.
ub_write When PHP produces output Write bytes to the client/stdout.
flush On flush Push buffered output.
send_header / send_headers Before body output Emit HTTP headers (no-op for CLI).
read_post Reading the request body Provide POST data.
read_cookies Reading cookies Provide the Cookie header.
getenv Env lookup SAPI-specific environment.
sapi_error On error Error reporting path.

The core exposes sapi_startup(), sapi_activate(), sapi_deactivate(), and sapi_shutdown() (main/SAPI.c) that drive these hooks.

5.2 The five lifecycle phases

1. Module Startup (MINIT)     once per process
     └─ each extension's MINIT: register classes, constants, INI entries
2. Request Startup (RINIT)    once per request
     └─ each extension's RINIT: per-request init, reset superglobals
3.   ── script execution ──   compile (or fetch from OPcache) + run opcodes
4. Request Shutdown (RSHUTDOWN) once per request
     └─ each extension's RSHUTDOWN; Zend MM bulk-frees request memory
5. Module Shutdown (MSHUTDOWN) once per process
     └─ each extension's MSHUTDOWN; persistent resources released

This is the model that makes PHP's "shared-nothing" concurrency work: state created in RINIT is destroyed in RSHUTDOWN, so each request starts clean. Long-lived workers (FPM) keep MINIT state (including OPcache SHM) across thousands of requests.

Note

Extensions implement these phases via PHP_MINIT_FUNCTION, PHP_RINIT_FUNCTION, PHP_RSHUTDOWN_FUNCTION, PHP_MSHUTDOWN_FUNCTION, and PHP_GINFO/globals macros. See Extension Development Internals.

5.3 SAPI inventory

SAPI Directory Host Concurrency model
cli sapi/cli Terminal One process per invocation; long-lived for -S/-a.
cgi sapi/cgi CGI/FastCGI servers Process per request (CGI) or FastCGI loop.
fpm sapi/fpm nginx/Apache via FastCGI Master + worker pool (process manager).
apache2handler sapi/apache2handler Apache httpd In-process module (mod_php); threads in worker MPM (needs ZTS).
embed sapi/embed Any C/C++ program Library; host controls the lifecycle.
litespeed sapi/litespeed LiteSpeed/OpenLiteSpeed LSAPI worker.
phpdbg sapi/phpdbg Terminal Interactive debugger.
fuzzer sapi/fuzzer libFuzzer harness Security fuzzing only (--enable-fuzzer).

Tip

For modern production, FPM behind nginx is the standard. mod_php ties PHP to Apache's process model and requires a thread-safe (ZTS) build under threaded MPMs; FPM decouples them and gives you process-pool control.


6. Repository Structure

The triage scan identified 121 high-priority module groups. The largest and most important:

Path Size (chars) What it contains
Zend/ ~3.78M The engine: scanner, parser, AST, compiler, VM, MM, GC, OO, API.
ext/standard/ ~2.48M Core function library (strings, arrays, math, files, URLs, var, …).
ext/pcre/pcre2lib/ ~2.25M Bundled PCRE2 regex library (third-party).
ext/dom/ ~1.03M DOM extension (built on Lexbor).
Zend/Optimizer/ ~0.80M Opcode optimizer (constant folding, DCE, SCCP, etc.).
ext/mysqlnd/ ~0.75M MySQL native driver.
main/ ~0.74M PHP core runtime + SAPI glue + streams.
ext/phar/ ~0.62M PHP Archive (phar) support.
ext/soap/ ~0.62M SOAP client/server.
ext/spl/ ~0.62M Standard PHP Library (data structures, iterators).
ext/hash/ ~0.48M Hashing algorithms.
ext/opcache/ ~0.47M Bytecode cache + JIT.
ext/openssl/ ~0.45M TLS/crypto via OpenSSL.
sapi/phpdbg/ ~0.44M Interactive debugger.
ext/fileinfo/libmagic/ ~0.40M Bundled libmagic (file type detection).
ext/ffi/ ~0.39M Foreign Function Interface.
ext/uri/uriparser/ ~0.40M Bundled uriparser (the new uri extension).
ext/reflection/ ~0.38M Reflection API.
main/streams/ ~0.37M Stream wrappers, filters, transports.
TSRM/ ~0.06M Thread-Safe Resource Manager.

Top-level directories

php-src/
├── Zend/          The Zend Engine (compiler + VM + runtime core)
│   ├── Optimizer/ Opcode optimizer passes
│   └── asm/       Fiber context-switch assembly per platform
├── main/          PHP core: startup, php.ini, output, SAPI bridge
│   └── streams/   Stream abstraction (wrappers, filters, transports)
├── ext/           ~70 bundled extensions
│   └── opcache/   Bytecode cache + JIT (jit/, jit/ir/)
├── sapi/          Server API bindings (cli, cgi, fpm, phpdbg, embed, …)
├── TSRM/          Thread-Safe Resource Manager (ZTS)
├── win32/         Windows-specific platform code
├── build/         Autotools macros and build helpers
├── scripts/       Dev tooling (phpize, php-config, gdb macros)
├── tests/         Cross-cutting language/integration .phpt tests
├── docs/          Internal implementation docs (Sphinx/reStructuredText)
├── benchmark/     Benchmark harness (incl. WordPress docker-compose)
├── configure.ac   Autotools entry point
├── php.ini-development / php.ini-production   INI templates
└── README.md / CONTRIBUTING.md / CODING_STANDARDS.md / SECURITY.md / UPGRADING

Note

Several ext/ directories vendor third-party libraries (PCRE2, libmagic, uriparser, libgd, libbcmath, Lexbor, the IR JIT framework). These carry their own licenses and are synced from upstream — see ext/lexbor/patches/README.md for an example of the patch-on-sync workflow.


7. Build System & Requirements

7.1 The Autotools flow

configure.ac + ext/*/config.m4 + sapi/*/config.m4
        │  ./buildconf  (runs autoconf)
        ▼
    configure
        │  ./configure [flags]
        ▼
    Makefile + main/php_config.h
        │  make
        ▼
    binaries (sapi/*/...) + libphp
        │  make install
        ▼
    installed PHP
  • buildconf regenerates configure from the .m4 files. Required on Git checkouts; use --force after build-system changes.
  • Each extension/SAPI declares its options and dependencies in its own config.m4 (Unix) and config.w32 (Windows).
  • main/php_config.h is the generated header capturing the resolved build configuration (including whether this is a ZTS build).

7.2 Build prerequisites

Requirement Minimal Git build Default build
C toolchain + make Required Required
autoconf Required Required
bison Required (parser) Required
re2c Required (lexer) Required
pkg-config Recommended Recommended
libxml2 dev headers — Required
libsqlite3 dev headers — Required

7.3 NTS vs ZTS

  • NTS (non-thread-safe) — the default and the right choice for CLI and FPM (process-based concurrency). No locking overhead.
  • ZTS (Zend Thread Safety) — required for threaded SAPIs (e.g. Apache worker MPM) and the parallel extension. Each thread gets its own globals via TSRM, at a small performance cost.

php -v prints (NTS) or (ZTS).

7.4 Windows

Windows builds use buildconf.bat, configure.bat, and nmake with the PHP SDK, plus win32/ for platform code. Prebuilt Windows binaries are available from windows.php.net.

7.5 Cross-architecture and 32-bit

The repo documents building 32-bit on x86_64 (./configure --disable-all --build=i686-pc-linux-gnu with -m32 flags) and cross-testing arm64 via Docker buildx (see ext/opcache/jit/README.md). The JIT supports x86_64, i386, and arm64.

Tip

For sanitizer builds use --enable-debug together with --enable-address-sanitizer (or --enable-undefined-sanitizer/--enable-memory-sanitizer). These are invaluable when chasing memory bugs in the engine or extensions.


8. Configuration Mastery

8.1 Configuration resolution order

  1. Built-in defaults (compiled in).
  2. Main php.ini — directory set by --with-config-file-path, overridable at runtime with -c or the PHPRC environment variable.
  3. Scan-directory *.ini fragments — --with-config-file-scan-dir (e.g. distro conf.d//php.d/ files, loaded alphabetically).
  4. Per-context overrides: CLI -d key=value; FPM php_value/php_admin_value; userland ini_set() for PHP_INI_USER/PHP_INI_ALL directives.
php --ini          # which files are loaded, and from where
php --ini=diff     # which entries differ from built-in defaults

8.2 Directive changeability (access level)

Every INI directive has an access level controlling where it may be set:

Level Settable in Example
PHP_INI_SYSTEM php.ini, php_admin_value only opcache.*, disable_functions
PHP_INI_PERDIR + .htaccess/.user.ini upload_max_filesize
PHP_INI_USER + ini_set() at runtime display_errors, memory_limit
PHP_INI_ALL anywhere default_charset

Important

OPcache settings are PHP_INI_SYSTEM: they must be in php.ini/an FPM php_admin_value and take effect at startup. You cannot change them per-request. This is the root of the "cannot be changed when OPcache is already set up" warning (issue #22216).

8.3 Production-hardening checklist

  • [ ] display_errors = Off, display_startup_errors = Off
  • [ ] log_errors = On with a writable error_log
  • [ ] expose_php = Off (remove the X-Powered-By header)
  • [ ] allow_url_include = Off (keep allow_url_fopen off too unless needed)
  • [ ] opcache.enable = 1, opcache.validate_timestamps = 0 (+ reload on deploy)
  • [ ] opcache.memory_consumption sized for your codebase (e.g. 256M)
  • [ ] disable_functions for dangerous primitives if your app does not need them
  • [ ] open_basedir to confine filesystem access where appropriate
  • [ ] session.cookie_secure = 1 (HTTPS), cookie_httponly = 1, cookie_samesite = Lax/Strict
  • [ ] memory_limit and max_execution_time sized to your workload

8.4 Key directive groups

See the Quick Reference for the full production-default tables. The groups that matter most operationally:

  • Resource limits: memory_limit, max_execution_time, max_input_time, post_max_size, upload_max_filesize, max_file_uploads.
  • Error handling: error_reporting, display_errors, log_errors, error_log, zend.exception_ignore_args.
  • OPcache/JIT: opcache.enable, opcache.memory_consumption, opcache.max_accelerated_files, opcache.validate_timestamps, opcache.revalidate_freq, opcache.preload, opcache.jit, opcache.jit_buffer_size.
  • Sessions: session.save_handler, session.cookie_*, session.gc_maxlifetime, session.use_strict_mode.
  • Security: disable_functions, disable_classes, open_basedir, allow_url_include, expose_php.

9. PHP-FPM Operations

php-fpm is configured by a global section plus one or more pools. Global config: php-fpm.conf (which by default just includes php-fpm.d/*.conf). Each pool file (template: www.conf) defines a worker pool.

9.1 Global directives ([global])

From sapi/fpm/fpm/fpm_conf.c:

Directive Purpose
pid Master PID file.
error_log FPM master error log path.
log_level alert/error/warning/notice/debug.
log_limit, log_buffering Log line limit and buffering.
daemonize Run in background (no under systemd/Docker).
emergency_restart_threshold / emergency_restart_interval Auto-restart workers if too many crash in a window.
process_control_timeout Time to wait for workers to react to signals.
process.max, process.priority Global process cap and nice level.
events.mechanism Event backend (epoll/kqueue/select).
systemd_interval systemd watchdog ping interval.
syslog.facility, syslog.ident syslog routing.

9.2 Pool directives (per [pool])

Directive Default (template) Purpose
user / group build-time OS identity workers run as.
listen 127.0.0.1:9000 TCP host:port or a Unix socket path.
listen.owner/group/mode — Permissions for a Unix-socket listener.
listen.allowed_clients — IP allowlist for the listener.
listen.backlog 511 Socket listen backlog.
pm dynamic Process manager: static, dynamic, or ondemand.
pm.max_children 5 Hard cap on worker processes.
pm.start_servers 2 Workers spawned at start (dynamic).
pm.min_spare_servers 1 Min idle workers (dynamic).
pm.max_spare_servers 3 Max idle workers (dynamic).
pm.max_spawn_rate 32 Max workers spawned per cycle.
pm.process_idle_timeout 10s Idle worker reaping (ondemand/dynamic).
pm.max_requests 500 (commented) Recycle a worker after N requests (leak mitigation).
pm.status_path / pm.status_listen /status Status endpoint.
ping.path / ping.response — Health-check endpoint.
access.log / access.format — Per-request access log.
slowlog / request_slowlog_timeout — Log stack traces of slow requests.
request_terminate_timeout 0 Hard kill a request after N seconds.
rlimit_files / rlimit_core — Per-worker resource limits.
chroot / chdir — Filesystem confinement / working dir.
catch_workers_output no Capture worker stdout/stderr to the FPM log.
clear_env yes Clear the environment for workers (set no to inherit).
security.limit_extensions .php … Only execute these file extensions.
env[NAME] — Set environment variables for workers.
php_value[] / php_flag[] — Override changeable INI directives.
php_admin_value[] / php_admin_flag[] — Override INI directives non-overridably.

9.3 Sizing pm.max_children

pm.max_children ≈  (RAM available to PHP)  /  (average worker RSS)

Measure average worker memory under load (e.g. via pm.status_path), leave headroom for the OS and other services, and set pm.max_children accordingly. Too high → swapping/OOM; too low → requests queue and latency spikes.

Important

Always php-fpm -t before reloading. Reload gracefully with SIGUSR2 (or systemctl reload php-fpm) so in-flight requests finish.

9.4 Monitoring endpoints

  • Status: enable pm.status_path = /status; query ?json, ?full, or ?html. Reports active/idle/total processes, queue depth, slow requests, accepted connections.
  • Ping: ping.path = /ping returns ping.response (pong) — a cheap liveness probe.

Note

Issue #14212 documents that the status page can transiently report active processes above pm.max_children during scaling. Treat status as a sampled signal; alert on sustained saturation, not single snapshots.

9.5 Worker output and slow requests

  • catch_workers_output = yes + decorate_workers_output = no routes worker stderr into the FPM log (essential for seeing fatal errors).
  • slowlog + request_slowlog_timeout = 5s dumps a PHP backtrace for any request exceeding the threshold — the fastest way to find a hung endpoint.
  • request_terminate_timeout is the hard backstop that kills a runaway request (set it above slowlog timeout).

10. Security

10.1 The trust model — read this first

PHP's security posture is explicit in SECURITY.md:

PHP does not offer sandboxing, and the execution of untrusted code is always considered unsafe.

This means:

  • Executing attacker-controlled PHP is game over — memory-safety violations from malicious destructors, __toString(), error handlers, etc., are classified as bugs, not security issues.
  • Passing unsanitized input to functions that explicitly expect trusted input (e.g. mysqli_query()) is the caller's responsibility, not a PHP vulnerability.
  • The hardening boundary is at input (escapeshellarg(), prepared statements, validation), not at execution.

Caution

Never run user-supplied PHP, never eval() untrusted strings, and never include/require attacker-controlled paths. There is no safe sandbox to fall back on.

10.2 Reporting vulnerabilities

Report via GitHub Security Advisories: https://github.com/php/php-src/security/advisories/new, or [email removed]. Reports stay private until a fix ships; reporters are credited (MITRE Credit System).

10.3 Runtime hardening surface

Control Directive / mechanism Effect
Disable risky functions disable_functions = exec,system,... Remove dangerous primitives from userland.
Confine filesystem open_basedir = /var/www Restrict file access to a subtree.
Block URL includes allow_url_include = Off Prevent remote code inclusion.
Hide version expose_php = Off Drop the X-Powered-By header.
Limit executable files FPM security.limit_extensions Only run intended extensions.
Drop privileges FPM user/group, run master non-root Least privilege per pool.
Chroot workers FPM chroot Filesystem isolation per pool.
Strip exception args zend.exception_ignore_args = On Keep sensitive args out of stack traces/logs.
Secure sessions session.cookie_secure/httponly/samesite, use_strict_mode Harden session cookies.

10.4 Crypto and TLS

ext/openssl provides TLS and crypto; ext/sodium provides modern libsodium primitives. Password hashing (password_hash()) supports Argon2 when built with --with-password-argon2. Prefer sodium/Argon2 for new code.

10.5 Fuzzing

sapi/fuzzer (--enable-fuzzer) builds libFuzzer harnesses for parsers and unserializers; PHP is continuously fuzzed via OSS-Fuzz. When hardening a parser-adjacent extension, add a fuzzer target.

10.6 Notable issues from the tracker

Issue Area Operational takeaway
#22218 SOAP SoapServer::handle() segfault from a $_SERVER lookup outside an IS_ARRAY guard — keep SOAP endpoints patched.
#22208 Zend INI NULL-deref in zend_ini_bool_literal() for non-existent INIs — validate INI files.
#22167 SOAP Out-of-range XML Schema ints silently accepted (atoi()), input-validation gap.
#22198 / #22200 curl / streams Memory leaks (callback exceptions, news streams) — watch long-lived worker RSS; use pm.max_requests.

11. Operations & Observability

11.1 Inspecting a running build

php -v                        # version, Zend version, NTS/ZTS
php -i                        # full phpinfo (build flags, all INI values, loaded ext)
php -m                        # loaded modules
php --ri opcache              # OPcache + JIT runtime status
php -i | grep 'Configure Command'   # exact ./configure line

11.2 OPcache health

Monitor OPcache via opcache_get_status() in a small endpoint (or php --ri opcache):

  • Cache full / restarts: if opcache.memory_consumption or opcache.max_accelerated_files is too small, OPcache thrashes (frequent restarts). Size both generously.
  • Wasted memory: rises with validate_timestamps=1 deployments; an explicit opcache_reset() on deploy is cleaner.
  • Interned strings buffer: size with opcache.interned_strings_buffer.

11.3 FPM operations

Task Command
Validate config php-fpm -t
Graceful reload kill -USR2 <master_pid> / systemctl reload php-fpm
Reopen logs kill -USR1 <master_pid>
Status (JSON) curl 'http://127.0.0.1/status?json' (via the web server)
Liveness curl http://127.0.0.1/ping → pong

11.4 Logging

  • FPM master log: error_log (global). Set log_level and enable catch_workers_output to capture worker fatals.
  • Application errors: PHP error_log directive (file or syslog).
  • Slow requests: slowlog + request_slowlog_timeout.
  • Access log: per-pool access.log with access.format placeholders (%R, %t, %m, %r, %s, %{milli}d, %{kilo}M, %C).

11.5 Profiling & tracing

  • Use the Observer API (zend_observer.c)-based tools (XHProf successors, OpenTelemetry, Tideways) for function-level tracing with low overhead.
  • phpdbg -p disassembles opcodes for inspection.
  • scripts/gdb/php_gdb.py provides gdb macros (zval dumping, etc.) for debugging the C side. Build with --enable-debug for usable symbols.

Warning

Memory leaks in long-lived FPM workers accumulate across requests (several are tracked upstream — see §10.6). Mitigate with pm.max_requests to recycle workers, and monitor per-worker RSS.


12. Extension Development Internals

Extensions are the primary way to extend PHP in C. They register through zend_module_entry and implement the lifecycle hooks.

12.1 Anatomy of an extension

ext/my_ext/
├── config.m4        Unix build config (PHP_ARG_ENABLE/WITH, PHP_NEW_EXTENSION)
├── config.w32       Windows build config
├── php_my_ext.h     Module header, globals struct
├── my_ext.c         Implementation (functions, MINIT/RINIT/...)
├── my_ext.stub.php  Function/class signatures (generates arginfo)
└── tests/*.phpt     Tests

Scaffold a new one:

php ext/ext_skel.php --ext my_ext

12.2 The lifecycle macros

Macro Maps to Use
PHP_MINIT_FUNCTION Module startup Register classes, constants, INI entries.
PHP_MSHUTDOWN_FUNCTION Module shutdown Release process-wide resources.
PHP_RINIT_FUNCTION Request startup Per-request init.
PHP_RSHUTDOWN_FUNCTION Request shutdown Per-request cleanup.
PHP_MINFO_FUNCTION phpinfo() section Report config/version.
ZEND_DECLARE_MODULE_GLOBALS Globals Thread-safe per-request globals.

12.3 Userland-facing definitions: stubs & arginfo

Modern extensions declare their function/method signatures in a .stub.php file; build/gen_stub.php generates the *_arginfo.h with the C zend_function_entry/argument metadata. This keeps the reflection-visible signature and the C registration in sync.

Important

Edit the .stub.php and regenerate arginfo (build/gen_stub.php); do not hand-edit *_arginfo.h. Signature drift between stub and arginfo is a common review rejection.

12.4 The Zend API

Zend/zend_API.h is the surface for extension authors: parameter parsing (ZEND_PARSE_PARAMETERS_* / the older zend_parse_parameters), zval manipulation, class/object construction, exception throwing, and the RETURN_*/RETVAL_* result macros. UPGRADING.INTERNALS documents API changes between releases — read it when porting an extension across PHP versions.

12.5 phpize for out-of-tree extensions

Extensions built against an installed PHP use phpize (in scripts/) to generate a build system without the full source tree:

cd my_ext
phpize
./configure --with-php-config=$(which php-config)
make && make install

13. Testing & Quality Assurance

13.1 The .phpt test format

Tests are .phpt files driven by run-tests.php. Each has sections — --TEST--, --FILE--, and one of --EXPECT--/--EXPECTF--/--EXPECTREGEX--, plus optional --SKIPIF--, --INI--, --ENV--. The runner raises a BorkageException for malformed tests (missing --TEST--, missing --FILE--, unknown sections).

make test                              # full suite
make TEST_PHP_ARGS=-j$(nproc) test     # parallel
make TESTS=ext/json test               # one extension

13.2 Environment-driven extension tests

Database and network extensions need a live service and credentials via environment variables (documented in each ext/<name>/tests/README.md):

Extension Key env vars
mysqli MYSQL_TEST_HOST/USER/PASSWD/DB/PORT/SOCKET/ENGINE
pdo_mysql PDO_MYSQL_TEST_DSN/HOST/DB/USER/PASS/...
pgsql local PostgreSQL + a test database
pdo_dblib PDO_DBLIB_TEST_DSN/USER/PASS (SQL Server)
ldap LDAP_TEST_HOST/BASE/USER/PASSWD (+ Vagrant setup)
snmp SNMP_HOSTNAME/PORT/COMMUNITY/MIBDIR (+ snmpd)

13.3 Sanitizers, valgrind, and the JIT

# AddressSanitizer build (needs rebuild, fast at runtime)
./configure --enable-debug --enable-address-sanitizer && make -j$(nproc)

# valgrind at test time (no rebuild, slower)
make test TEST_PHP_ARGS="-m --show-mem -j$(nproc)"

# JIT stress
make test TESTS="-d opcache.jit_buffer_size=16M -d opcache.enable=1 \
  -d opcache.enable_cli=1 -d opcache.protect_memory=1 \
  -d opcache.jit=tracing --repeat 2 -j$(nproc) ext/opcache Zend"

opcache.protect_memory=1 makes the JIT trap writes to read-only memory — a frequent root cause of JIT bugs. --repeat 2 catches trace bugs that only appear on the second execution.

13.4 CI and fuzzing

The project runs GitHub Actions on every push and is continuously fuzzed via OSS-Fuzz. scripts/dev/bless_tests.php regenerates expected output for --EXPECTF-- tests after intentional changes.


14. Troubleshooting

Error: bison/re2c not found during build

Cause: Parser/lexer generators missing (or bison too old on macOS). Solution: Install recent bison and re2c; put them on PATH before ./buildconf. Prevention: Install the full prerequisite list; on macOS source Homebrew's bison path.

Warning: opcache.X cannot be changed when OPcache is already set up (#22216)

Cause: OPcache directives are PHP_INI_SYSTEM; some are locked once OPcache initializes (notably on ZTS). Solution: Set all opcache.* values in php.ini/FPM php_admin_value, applied at startup — never via -d/ini_set() after init. Prevention: Treat OPcache config as static, per-process startup config.

Symptom: code changes don't take effect

Cause: opcache.validate_timestamps=0 and the cache was not reset on deploy. Solution: Reload FPM (systemctl reload php-fpm) or call opcache_reset() after deploying. Prevention: Wire a cache reset into your deploy pipeline.

Symptom: FPM returns 502/504 under load

Cause: pm.max_children too low → request queue saturates; or request_terminate_timeout killing long requests. Solution: Inspect pm.status_path; raise pm.max_children within your memory budget; profile slow endpoints via slowlog. Prevention: Size the pool from RAM / avg_worker_RSS; set slowlog/request_terminate_timeout.

Symptom: worker memory grows over time

Cause: A leak accumulating across requests in a long-lived worker (several tracked upstream — #22198, #22200). Solution: Set pm.max_requests to recycle workers; identify the leaking extension; upgrade. Prevention: Monitor per-worker RSS; keep extensions current.

Error: No input file specified.

Cause: FastCGI SCRIPT_FILENAME points to a missing file, or security.limit_extensions blocks it. Solution: Align the web server's SCRIPT_FILENAME with the real path; check security.limit_extensions. Prevention: Keep document-root and FastCGI params consistent.

Crash: segfault only with the JIT enabled

Cause: A JIT trace bug (often surfacing on repeated execution). Solution: Reproduce with --repeat 2 -d opcache.protect_memory=1; rebuild --enable-debug --enable-address-sanitizer; report upstream. Prevention: Run the JIT stress test before deploying a JIT build; consider opcache.jit=function if tracing is unstable for your code.

Build fails after git pull

Cause: Stale generated parser/lexer/configure. Solution: ./buildconf --force, re-./configure, make. If needed, make clean/git clean -xfd. Prevention: Regenerate the build system after upstream build-file changes.

Tests BorkageException: missing section --FILE--

Cause: A malformed .phpt (missing required section). Solution: Ensure the test has --TEST--, --FILE--, and an --EXPECT*--. Use --SKIPIF-- for conditional skips. Prevention: Copy an existing well-formed .phpt as a template.


15. Contributing

Item Where
Source / PRs github.com/php/php-src
Contributing guide CONTRIBUTING.md
Coding standards CODING_STANDARDS.md
Internal docs php.github.io/php-src + docs/ (Sphinx)
Upgrading notes UPGRADING (userland), UPGRADING.INTERNALS (C API)
Mailing list [email removed]

Process:

  • Bug fixes don't require an RFC. Reference issues as GH-NNNNNN (or #NNNNNN for the legacy bugs.php.net tracker) in the commit message.
  • New features/language changes require an RFC accepted by a developer vote.
  • Fork → branch → PR; merges follow the Git workflow.

Building the internal docs:

cd docs
pip install -r requirements.txt   # Sphinx + sphinx-design + sphinxawesome-theme + rstfmt
make html                         # output in docs/build/html/index.html

Note

The C code follows the conventions in CODING_STANDARDS.md (tabs for indentation, specific brace style, naming). PRs that violate them are routinely sent back. Generated files (zend_vm_execute.h, *_arginfo.h, scanner/parser) must be regenerated from their sources, not hand-edited.


16. Potential Improvements

These are observations for maintainers and downstream packagers, derived from the source and the issue tracker — not defects in your deployment.

  • OPcache config rigidity on ZTS (#22216): the "cannot be changed when already set up" path could fail more gracefully or document the constraint inline. Packagers should ship sane static opcache.* defaults.
  • Long-lived worker leaks (#22198, #22200): periodic leaks in callback/exception and stream paths argue for keeping pm.max_requests set as a routine mitigation until each is fixed upstream.
  • SOAP input validation (#22167, #22218): ext/soap has had $_SERVER-guard and atoi()-range issues; deployments exposing SOAP should track these closely or front the endpoint with validation.
  • JIT observability: the JIT's many opcache.jit_* tuning knobs are powerful but opaque; richer runtime introspection (beyond opcache_get_status()) would help operators decide between tracing and function modes.
  • Vendored third-party libs: PCRE2, libmagic, libgd, Lexbor, uriparser, and the IR framework are vendored and patched on sync (see ext/lexbor/patches/). Track upstream CVEs for these independently of PHP releases.

17. Quick Reference

Build essentials

./buildconf [--force]                 # regenerate configure
./configure --enable-debug            # development build
./configure                           # production build
./configure --disable-all --enable-debug   # minimal engine-hacking build
make -j$(nproc)                       # compile
make install                          # install (may need sudo)
make TEST_PHP_ARGS=-j$(nproc) test    # run test suite

Binaries & SAPIs

php (cli)   php-cgi (cgi)   php-fpm (fpm)   phpdbg   libphp (embed/apache2handler)   litespeed

Key engine files

File Role
Zend/zend_language_scanner.l Lexer (re2c)
Zend/zend_language_parser.y Parser (bison)
Zend/zend_ast.c AST
Zend/zend_compile.c Compiler
Zend/zend_vm_def.h + zend_vm_gen.php VM definition + generator
Zend/zend_execute.c Virtual machine
Zend/zend_alloc.c Zend MM allocator
Zend/zend_gc.c Cycle-collecting GC
Zend/zend_hash.c HashTable (arrays)
Zend/zend_API.c/.h Extension C API
main/main.c Process/request lifecycle
main/SAPI.c/.h SAPI bridge
ext/opcache/ZendAccelerator.c SHM bytecode cache
ext/opcache/jit/zend_jit*.c + jit/ir/ JIT compiler + IR framework

OPcache directives (full list)

opcache.enable                       opcache.enable_cli
opcache.memory_consumption           opcache.interned_strings_buffer
opcache.max_accelerated_files        opcache.max_wasted_percentage
opcache.max_file_size                opcache.validate_timestamps
opcache.revalidate_freq              opcache.revalidate_path
opcache.use_cwd                      opcache.save_comments
opcache.enable_file_override         opcache.optimization_level
opcache.dups_fix                     opcache.blacklist_filename
opcache.force_restart_timeout        opcache.error_log
opcache.log_verbosity_level          opcache.preferred_memory_model
opcache.protect_memory               opcache.restrict_api
opcache.mmap_base                    opcache.cache_id
opcache.file_cache                   opcache.file_cache_only
opcache.file_cache_consistency_checks  opcache.file_cache_fallback
opcache.file_cache_read_only         opcache.huge_code_pages
opcache.lockfile_path                opcache.file_update_protection
opcache.preload                      opcache.preload_user
opcache.record_warnings              opcache.validate_permission
opcache.validate_root
# JIT
opcache.jit                          opcache.jit_buffer_size
opcache.jit_debug                    opcache.jit_bisect_limit
opcache.jit_prof_threshold           opcache.jit_max_root_traces
opcache.jit_max_side_traces          opcache.jit_max_exit_counters
opcache.jit_hot_loop                 opcache.jit_hot_func
opcache.jit_hot_return               opcache.jit_hot_side_exit
opcache.jit_blacklist_root_trace     opcache.jit_blacklist_side_trace
opcache.jit_max_loop_unrolls         opcache.jit_max_recursive_calls
opcache.jit_max_recursive_returns    opcache.jit_max_polymorphic_calls
opcache.jit_max_trace_length         opcache.opt_debug_level

FPM process-manager cheat-sheet

pm = static     → fixed pool of pm.max_children (predictable, lowest latency)
pm = dynamic    → scale between min/max_spare_servers up to max_children
pm = ondemand   → spawn only on request, reap after pm.process_idle_timeout

Lifecycle phases

MINIT → RINIT → [execute] → RSHUTDOWN → MSHUTDOWN
(once per process) (per request)        (once per process)

Production hardening (one-liner audit)

php -i | grep -iE 'display_errors|expose_php|allow_url_include|opcache.enable|opcache.validate_timestamps'