Skip to content

[DYNAREC] Stale dynablock in_used counters after fork() prevent purging in child process #3470

@devarajabc

Description

@devarajabc

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                        │
└───────────────────────────────┘   └───────────────────────────────────────┘

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions