Skip to content

pcntl_fork() in worker thread creates full Go runtime copy that never exits, causing unbounded process accumulation #2331

@skomarchukmojam

Description

@skomarchukmojam

Environment

  • FrankenPHP version: 1.12.1
  • PHP version: 8.4
  • OS: Linux (Debian bookworm, Docker container)
  • Caddy config: num_threads 2, max_threads 8

Description

When pcntl_fork() is called inside a FrankenPHP worker thread (during an HTTP request),
the child process becomes a full copy of the FrankenPHP Go runtime and never terminates —
even after calling exit(0) from PHP.

Each request that calls pcntl_fork() leaves one additional frankenphp process
in S (sleeping) state permanently. These processes accumulate indefinitely.

Reproduction

Minimal controller:

$pid = pcntl_fork();
if ($pid === 0) {                                                                                                                                                                                                                 
    usleep(100); // 100 microseconds
    exit(0);                                                                                                                                                                                                                      
}               

Steps:                                                                                                                                                                                                                            
1. Run FrankenPHP with num_threads 2, max_threads 8
2. Hit the endpoint 20 times                                                                                                                                                                                                      
3. Check ps aux inside the container
                                                                                                                                                                                                                                  
Before (20 requests):
3 processes                                                                                                                                                                                                                       
                
After:                                                                                                                                                                                                                            
23 processes — all `frankenphp run` with status `S`, never exit
                                                               
Root cause (hypothesis)                                                                                                                                                                                                           
                                                                                                                                                                                                                                  
pcntl_fork() in a Go-based runtime duplicates the entire Go runtime — goroutines,                                                                                                                                                 
mutexes, thread pools. The child process inherits locks held by threads that no                                                                                                                                                   
longer exist in the child, causing the Go runtime to deadlock on shutdown.                                                                                                                                                        
PHP's exit(0) is called but the Go process cannot clean up and terminate.                                                                                                                                                         
                                                                                                                                                                                                                                  
Impact                                                                                                                                                                                                                            
                                                                                                                                                                                                                                  
In production under load, these zombie-like sleeping processes accumulate rapidly:                                                                                                                                                
- PID exhaustion
                                                                                                                                                                                                                                  
Notes
                                                                                                                                                                                                                                  
- tini as PID 1 does not help — processes are in S state, not Z (zombie),                                                                                                                                                         
so tini has nothing to reap
- Process::run() (Symfony/Laravel, uses proc_open) does not reproduce the issue                                                                                                                                                   
- Only direct pcntl_fork() call reproduces it                                                                                                                                                                                                                                                                                                                                   
                                                                                                                                                                                                                                  
Question                                                                                                                                                                                                                          
                                                                                                                                                                                                                                  
Is pcntl_fork() expected to be unsupported inside FrankenPHP worker threads?                                                                                                                                                      
Should FrankenPHP disable pcntl_fork or emit a warning when called in worker context?
                                                                                                                                                                                                                                  
---             

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions