-
-
Notifications
You must be signed in to change notification settings - Fork 405
Open
Description
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:
- Test repository: https://github.com/devarajabc/box64_test_cases
- Test case: 001_fork_in_used_leak
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 │
└───────────────────────────────┘ └───────────────────────────────────────┘
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels