When a multi-threaded process calls fork() while other threads are executing inside dynarec blocks, the child process inherits the in_used counters from those threads. Since those threads don't exist in the child, the counters become permanently stale, preventing PurgeDynarecMap() from ever freeing those blocks.
The atfork_child_custommem() handler only reinits mutexes.
It doesn't reset in_used.
This issue has been verified with a reproducible test case:
Box64 v0.4.1 on Apple M1 | 8 workers, 4 dynarec blocks
[Diagnostics] at fork:
+-----------------+------------------+
| Dynarec Block | Expected in_used |
+-----------------+------------------+
| hot_compute_0 | 2 |
| hot_compute_1 | 2 |
| hot_compute_2 | 2 |
| hot_compute_3 | 2 |
+-----------------+------------------+
| TOTAL STALE | 8 |
+-----------------+------------------+
CHILD after fork:
- Inherited 4 blocks with in_used > 0
- Child has 0 worker threads
- All counters are permanently STALE
- PurgeDynarecMap() skips these blocks forever
Approach 1: Walk all dynarec blocks at fork time and reinitialize each block’s in_used counter.
However, this approach has an O(N) time complexity.
What is your opinion on this? Is it acceptable to incur an O(N) cost at fork time to reinitialize every block’s in_used counter?
┌─────────────────────────────────────────────────────────────────────────────┐
│ PARENT PROCESS (before fork) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Thread 1 ──────► my_fork() ──► emu->fork=1 ──► exits block ──► fork() │
│ (caller) [deferred fork ensures Thread 1 exits its block first] │
│ │
│ Thread 2 ══════► [INSIDE dynablock A] ══════► in_used = 1 │
│ Thread 3 ══════► [INSIDE dynablock A] ══════► in_used = 2 │
│ Thread 4 ══════► [INSIDE dynablock B] ══════► in_used = 1 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
│
fork()
│
┌───────────────┴───────────────┐
▼ ▼
┌───────────────────────────────┐ ┌───────────────────────────────────────┐
│ PARENT (continues) │ │ CHILD (new process) │
├───────────────────────────────┤ ├───────────────────────────────────────┤
│ │ │ │
│ Thread 2,3,4 eventually │ │ Only Thread 1 exists │
│ exit their blocks │ │ Threads 2,3,4 DON'T EXIST │
│ │ │ │
│ in_used → 0 ✓ │ │ dynablock A: in_used = 2 (STALE!) │
│ Blocks can be purged ✓ │ │ dynablock B: in_used = 1 (STALE!) │
│ │ │ │
│ │ │ Counters NEVER decrement │
│ │ │ Blocks can NEVER be purged │
│ │ │ Memory leak │
└───────────────────────────────┘ └───────────────────────────────────────┘
When a multi-threaded process calls
fork()while other threads are executing inside dynarec blocks, the child process inherits thein_usedcounters from those threads. Since those threads don't exist in the child, the counters become permanently stale, preventingPurgeDynarecMap()from ever freeing those blocks.The
atfork_child_custommem()handler only reinits mutexes.It doesn't reset
in_used.This issue has been verified with a reproducible test case:
Approach 1: Walk all dynarec blocks at fork time and reinitialize each block’s in_used counter.
However, this approach has an O(N) time complexity.
What is your opinion on this? Is it acceptable to incur an O(N) cost at fork time to reinitialize every block’s
in_usedcounter?