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
- Executive Summary
- Architecture Overview
- The Zend Engine
- OPcache & the JIT
- The SAPI Layer & Request Lifecycle
- Repository Structure
- Build System & Requirements
- Configuration Mastery
- PHP-FPM Operations
- Security
- Operations & Observability
- Extension Development Internals
- Testing & Quality Assurance
- Troubleshooting
- Contributing
- Potential Improvements
- 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
- 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. - PHP core (
main/) — process and request lifecycle,php.inihandling, the stream abstraction, output buffering, superglobals, and the SAPI bridge (main/SAPI.c,main/SAPI.h). - Extensions (
ext/) — the function/class library exposed to userland, from the always-presentstandard/splto optional database, crypto, and string extensions. - SAPIs (
sapi/) — thin adapters that embed the engine into a host environment and implement thesapi_module_structcallbacks.
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
*.lor*.yyou must havere2candbisoninstalled and re-run the build;./buildconfandmakeregenerate 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.hdirectly — it is overwritten. Editzend_vm_def.h(and the helpers) and regenerate withphp 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_limitINI 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 callopcache_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) andopcache.enable=1. Without a non-zero buffer the JIT is inert even ifopcache.jitis 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, andPHP_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_phpties 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 — seeext/lexbor/patches/README.mdfor 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
buildconfregeneratesconfigurefrom the.m4files. Required on Git checkouts; use--forceafter build-system changes.- Each extension/SAPI declares its options and dependencies in its own
config.m4(Unix) andconfig.w32(Windows). main/php_config.his 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
parallelextension. 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-debugtogether 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
- Built-in defaults (compiled in).
- Main
php.ini— directory set by--with-config-file-path, overridable at runtime with-cor thePHPRCenvironment variable. - Scan-directory
*.inifragments —--with-config-file-scan-dir(e.g. distroconf.d//php.d/files, loaded alphabetically). - Per-context overrides: CLI
-d key=value; FPMphp_value/php_admin_value; userlandini_set()forPHP_INI_USER/PHP_INI_ALLdirectives.
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 inphp.ini/an FPMphp_admin_valueand 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 = Onwith a writableerror_log - [ ]
expose_php = Off(remove theX-Powered-Byheader) - [ ]
allow_url_include = Off(keepallow_url_fopenoff too unless needed) - [ ]
opcache.enable = 1,opcache.validate_timestamps = 0(+ reload on deploy) - [ ]
opcache.memory_consumptionsized for your codebase (e.g.256M) - [ ]
disable_functionsfor dangerous primitives if your app does not need them - [ ]
open_basedirto confine filesystem access where appropriate - [ ]
session.cookie_secure = 1(HTTPS),cookie_httponly = 1,cookie_samesite = Lax/Strict - [ ]
memory_limitandmax_execution_timesized 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 -tbefore reloading. Reload gracefully withSIGUSR2(orsystemctl 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 = /pingreturnsping.response(pong) — a cheap liveness probe.
Note
Issue #14212 documents that the status page can transiently report active processes above
pm.max_childrenduring 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 = noroutes worker stderr into the FPM log (essential for seeing fatal errors).slowlog+request_slowlog_timeout = 5sdumps a PHP backtrace for any request exceeding the threshold — the fastest way to find a hung endpoint.request_terminate_timeoutis the hard backstop that kills a runaway request (set it aboveslowlogtimeout).
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 neverinclude/requireattacker-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_consumptionoropcache.max_accelerated_filesis too small, OPcache thrashes (frequent restarts). Size both generously. - Wasted memory: rises with
validate_timestamps=1deployments; an explicitopcache_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). Setlog_leveland enablecatch_workers_outputto capture worker fatals. - Application errors: PHP
error_logdirective (file orsyslog). - Slow requests:
slowlog+request_slowlog_timeout. - Access log: per-pool
access.logwithaccess.formatplaceholders (%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 -pdisassembles opcodes for inspection.scripts/gdb/php_gdb.pyprovides gdb macros (zvaldumping, etc.) for debugging the C side. Build with--enable-debugfor usable symbols.
Warning
Memory leaks in long-lived FPM workers accumulate across requests (several are tracked upstream — see §10.6). Mitigate with
pm.max_requeststo 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.phpand 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#NNNNNNfor 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_requestsset as a routine mitigation until each is fixed upstream. - SOAP input validation (#22167, #22218):
ext/soaphas had$_SERVER-guard andatoi()-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 (beyondopcache_get_status()) would help operators decide betweentracingandfunctionmodes. - 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'
Reference links
- Internal docs: php.github.io/php-src
- RFC process: wiki.php.net/rfc
- Security advisories: github.com/php/php-src/security/advisories/new
- Issues: github.com/php/php-src/issues
- IR JIT framework: github.com/dstogov/ir