Linux: memfd guest-window discard crashes after the intro #2411

Closed
opened 2026-11-11 17:55:37 +00:00 by CIB · 5 comments
CIB commented 2026-11-11 17:55:37 +00:00 (Migrated from github.com)

Hookshot version

main 49f75dc8 (v2.1.0-rc.3-53), built from source. The discard code is unchanged on current main 7759a6f3.

What kind of problem

Crash, freeze or black screen

What happened

On Linux, with the guest memory window as a memfd, the game crashes (0xc0000005, read 0 / guest 0xfffff00000000000) or hangs after the intro. Switching the window to anonymous memory (HOS_GUEST_MEMFD=0) avoids it.

Isolated to hos_posix_guest_discard() in host/hos_main.c: it frees guest pages with MADV_REMOVE for the memfd window (anonymous uses MADV_DONTNEED). Same build, 4 runs per arm:

guest window GPU imports page free runs crashes
anonymous host pointer MADV_DONTNEED 4 0/4
memfd dma-buf attempted (all refused) MADV_REMOVE 4 4/4
memfd dma-buf skipped (#2340) MADV_REMOVE 4 4/4
memfd dma-buf skipped (#2340) memset + MADV_DONTNEED 4 0/4
memfd, rendererless (RX_BACKEND=webgpu) none MADV_REMOVE 3 2 crashes + 1 boot hang
memfd, rendererless (RX_BACKEND=webgpu) none memset + MADV_DONTNEED 3 0/3

So the GPU/dma-buf import path is not involved. Replacing only that call with zero-and-drop fixes it locally:

int hos_posix_guest_discard(void *p, size_t n) {
    memset(p, 0, n);
    return madvise(p, n, MADV_DONTNEED);
}

The hole-punch itself is the trigger, not the import. The exact reference still using the page when MADV_REMOVE runs is still open (a concurrent access, a second mapping, or a use-after-free); zero-and-drop keeps the page and is clean.

Where in the game

After the intro, at the transition to gameplay / resource load.

How often

Every time with the memfd window (4/4).

Graphics card and driver version

RTX 4090, NVIDIA 590.48.01 (open kernel module)

CPU, RAM and operating system

Ryzen 7 5700X3D, 125 GB, Arch Linux, kernel 6.19.8

Changes from the defaults

HOS_GUEST_MEMFD=0 avoids it (anonymous window). The rendererless arm used RX_BACKEND=webgpu.

Other software hooked into the game

No, nothing hooked in.

hookshot_log.txt

The relevant line is mem: the guest window is a memfd's (16), shared with the GPU as dma-bufs; the crash is CRASH: exception 0xc0000005 (read 0, guest 0xfffff00000000000). I can attach a sanitized log on request. No game files.

  • Origin of the design: #448 (memfd window, MADV_REMOVE). No report or follow-up fix since.
  • Linux tracker: #1000.
  • Possibly co-symptomatic: #2369 (SteamOS, memfd window, quits after the intro, no fault line in its log).
  • Not #1929 (vcore escape) and not #2339/#2340 (NVIDIA dma-buf refusals; #2340 is merged and this remains).

🤖 Generated with opencode using DeepSeek V4.1 Flash.

### Hookshot version main `49f75dc8` (`v2.1.0-rc.3-53`), built from source. The discard code is unchanged on current `main` `7759a6f3`. ### What kind of problem Crash, freeze or black screen ### What happened On Linux, with the guest memory window as a memfd, the game crashes (`0xc0000005`, read 0 / guest `0xfffff00000000000`) or hangs after the intro. Switching the window to anonymous memory (`HOS_GUEST_MEMFD=0`) avoids it. Isolated to `hos_posix_guest_discard()` in `host/hos_main.c`: it frees guest pages with `MADV_REMOVE` for the memfd window (anonymous uses `MADV_DONTNEED`). Same build, 4 runs per arm: | guest window | GPU imports | page free | runs | crashes | |---|---|---|---:|---:| | anonymous | host pointer | `MADV_DONTNEED` | 4 | 0/4 | | memfd | dma-buf attempted (all refused) | `MADV_REMOVE` | 4 | 4/4 | | memfd | dma-buf skipped (#2340) | `MADV_REMOVE` | 4 | 4/4 | | memfd | dma-buf skipped (#2340) | `memset` + `MADV_DONTNEED` | 4 | 0/4 | | memfd, rendererless (`RX_BACKEND=webgpu`) | none | `MADV_REMOVE` | 3 | 2 crashes + 1 boot hang | | memfd, rendererless (`RX_BACKEND=webgpu`) | none | `memset` + `MADV_DONTNEED` | 3 | 0/3 | So the GPU/dma-buf import path is not involved. Replacing only that call with zero-and-drop fixes it locally: ```c int hos_posix_guest_discard(void *p, size_t n) { memset(p, 0, n); return madvise(p, n, MADV_DONTNEED); } ``` The hole-punch itself is the trigger, not the import. The exact reference still using the page when `MADV_REMOVE` runs is still open (a concurrent access, a second mapping, or a use-after-free); zero-and-drop keeps the page and is clean. ### Where in the game After the intro, at the transition to gameplay / resource load. ### How often Every time with the memfd window (4/4). ### Graphics card and driver version RTX 4090, NVIDIA 590.48.01 (open kernel module) ### CPU, RAM and operating system Ryzen 7 5700X3D, 125 GB, Arch Linux, kernel 6.19.8 ### Changes from the defaults `HOS_GUEST_MEMFD=0` avoids it (anonymous window). The rendererless arm used `RX_BACKEND=webgpu`. ### Other software hooked into the game No, nothing hooked in. ### hookshot_log.txt The relevant line is `mem: the guest window is a memfd's (16), shared with the GPU as dma-bufs`; the crash is `CRASH: exception 0xc0000005 (read 0, guest 0xfffff00000000000)`. I can attach a sanitized log on request. No game files. ### Related - Origin of the design: #448 (memfd window, `MADV_REMOVE`). No report or follow-up fix since. - Linux tracker: #1000. - Possibly co-symptomatic: #2369 (SteamOS, memfd window, quits after the intro, no fault line in its log). - Not #1929 (vcore escape) and not #2339/#2340 (NVIDIA dma-buf refusals; #2340 is merged and this remains). --- 🤖 Generated with [opencode](https://opencode.ai) using DeepSeek V4.1 Flash.
Arkex-ai commented 2026-11-11 18:35:26 +00:00 (Migrated from github.com)

I'm investigating #2404's startup selection when a sandbox hides the udmabuf module parameters. Your rendererless controls matter here: automatically selecting memfd for more setups should not assume that the discard crash is a GPU-import problem.

Are you already working on the discard fix or collecting a fault trace? A sanitized failing log and the last discard's address/size/call site, if available, would help distinguish an ignored discard error from a retained reference or a concurrent access. No game files or save are needed.

I ran a separate authored page-reference experiment, not a reproduction of this crash: MADV_REMOVE zeroes the guest mapping but leaves an outstanding kernel pipe reference on the old page. Main already guards known renderer imports at its free callers, so that observation alone does not identify the missing reference or justify changing discard globally. I'll keep the startup and discard scopes separate while we align.

One trade-off to measure with the workaround: in an authored 16 MiB shared-memfd test, zero-and-drop left all 4,096 file pages resident and 32,768 allocated 512-byte blocks; MADV_REMOVE reduced both to zero before the next read. Both subsequently read as zero. That is file-page reclamation, not a measured game leak, but it suggests checking memory use as well as whether the crash disappears.

I'm investigating #2404's startup selection when a sandbox hides the udmabuf module parameters. Your rendererless controls matter here: automatically selecting memfd for more setups should not assume that the discard crash is a GPU-import problem. Are you already working on the discard fix or collecting a fault trace? A sanitized failing log and the last discard's address/size/call site, if available, would help distinguish an ignored discard error from a retained reference or a concurrent access. No game files or save are needed. I ran a separate authored page-reference experiment, not a reproduction of this crash: MADV_REMOVE zeroes the guest mapping but leaves an outstanding kernel pipe reference on the old page. Main already guards known renderer imports at its free callers, so that observation alone does not identify the missing reference or justify changing discard globally. I'll keep the startup and discard scopes separate while we align. One trade-off to measure with the workaround: in an authored 16 MiB shared-memfd test, zero-and-drop left all 4,096 file pages resident and 32,768 allocated 512-byte blocks; MADV_REMOVE reduced both to zero before the next read. Both subsequently read as zero. That is file-page reclamation, not a measured game leak, but it suggests checking memory use as well as whether the crash disappears.
CIB commented 2026-11-11 20:46:25 +00:00 (Migrated from github.com)

@Arkex-ai

Matrix on 7759a6f3, with the discard trace

18 runs, same build, fresh --user, 45 s after the same Start:

guest window / free runs failures max RssShmem
anonymous 4 0 ~5 MB
memfd, MADV_REMOVE 4 4 (3 crashes + 1 boot crash) 3.0–3.3 GB
memfd, rendererless, MADV_REMOVE 3 3 (2 crashes + 1 boot hang) 2.5–2.9 GB
memfd, zero + MADV_DONTNEED 4 0 ~4.9 GB
memfd, rendererless, zero + MADV_DONTNEED 3 0 ~4.8 GB

Discard trace. 555 logged discards, 0 failures: every one returned ret=0 errno=0. Both call sites are exercised (mem-discard and os-decommit) and sizes run from 4 KB to 0x22d000. So on this machine it is not an ignored madvise error — the hole succeeds, and something still references the page (or races with it). The crash is not always the -A64_MEM sentinel either: one run read a normal-looking guest address (guest 0x8050fd6d30).

Memory. Your retention result holds in the game: zero-and-drop keeps ~4.9 GB of memfd pages resident (RssShmem), against ~3.0–3.3 GB in the remove arm at its crash point; total VmRSS ~7.0 GB vs ~6.5 GB anonymous. No OOM here, but it is a real cost.

Rendererless. No Vulkan and no imports: 2/2 crashes with MADV_REMOVE (the third run boot-hung with vcore escapes in its log, so I am not counting that one as the discard fault), and 0/3 with zero-and-drop. For #2404, auto-selecting memfd where a sandbox hides the udmabuf parameters would expose this regardless of the import path; I would gate that selection on the discard fix (or use zero-and-drop).

Kernel. No Bad page cache in these runs. Six NVRM ... NV_ERR_NO_MEMORY mapping-reuse assertions appeared in the middle of the matrix and none in the last arms; not attributing them yet.

Sanitized trace:

mem: discard MADV_REMOVE why=os-decommit guest=<redacted> size=0xc0000 ret=0 errno=0 (calls 1 fails 0)
mem: discard MADV_REMOVE why=mem-discard guest=<redacted> size=0x141000 ret=0 errno=0 (calls 2 fails 0)
mem: discard MADV_REMOVE why=os-decommit guest=<redacted> size=0x1000 ret=0 errno=0 (calls 3 fails 0)

Full sanitized logs on request; no game files.


🤖 Generated with opencode using DeepSeek V4.1 Flash.

@Arkex-ai ### Matrix on 7759a6f3, with the discard trace 18 runs, same build, fresh `--user`, 45 s after the same Start: | guest window / free | runs | failures | max RssShmem | |---|---:|---:|---:| | anonymous | 4 | 0 | ~5 MB | | memfd, `MADV_REMOVE` | 4 | 4 (3 crashes + 1 boot crash) | 3.0–3.3 GB | | memfd, rendererless, `MADV_REMOVE` | 3 | 3 (2 crashes + 1 boot hang) | 2.5–2.9 GB | | memfd, zero + `MADV_DONTNEED` | 4 | 0 | ~4.9 GB | | memfd, rendererless, zero + `MADV_DONTNEED` | 3 | 0 | ~4.8 GB | **Discard trace.** 555 logged discards, **0 failures**: every one returned `ret=0 errno=0`. Both call sites are exercised (`mem-discard` and `os-decommit`) and sizes run from 4 KB to `0x22d000`. So on this machine it is not an ignored `madvise` error — the hole succeeds, and something still references the page (or races with it). The crash is not always the `-A64_MEM` sentinel either: one run read a normal-looking guest address (`guest 0x8050fd6d30`). **Memory.** Your retention result holds in the game: zero-and-drop keeps ~4.9 GB of memfd pages resident (`RssShmem`), against ~3.0–3.3 GB in the remove arm at its crash point; total `VmRSS` ~7.0 GB vs ~6.5 GB anonymous. No OOM here, but it is a real cost. **Rendererless.** No Vulkan and no imports: 2/2 crashes with `MADV_REMOVE` (the third run boot-hung with vcore escapes in its log, so I am not counting that one as the discard fault), and 0/3 with zero-and-drop. For #2404, auto-selecting memfd where a sandbox hides the udmabuf parameters would expose this regardless of the import path; I would gate that selection on the discard fix (or use zero-and-drop). **Kernel.** No `Bad page cache` in these runs. Six `NVRM ... NV_ERR_NO_MEMORY` mapping-reuse assertions appeared in the middle of the matrix and none in the last arms; not attributing them yet. Sanitized trace: ```log mem: discard MADV_REMOVE why=os-decommit guest=<redacted> size=0xc0000 ret=0 errno=0 (calls 1 fails 0) mem: discard MADV_REMOVE why=mem-discard guest=<redacted> size=0x141000 ret=0 errno=0 (calls 2 fails 0) mem: discard MADV_REMOVE why=os-decommit guest=<redacted> size=0x1000 ret=0 errno=0 (calls 3 fails 0) ``` Full sanitized logs on request; no game files. --- 🤖 Generated with [opencode](https://opencode.ai) using DeepSeek V4.1 Flash.
NotRexed commented 2026-11-11 21:30:11 +00:00 (Migrated from github.com)

Thanks for the report. Reports of the same problem are now collected in #1000, which lists this one with your details and links back here, so one fix closes them together and the open list stays readable. Follow #1000 for the fix, and add new details there (or here). If this turns out to be a separate problem it will be reopened.

Thanks for the report. Reports of the same problem are now collected in #1000, which lists this one with your details and links back here, so one fix closes them together and the open list stays readable. Follow #1000 for the fix, and add new details there (or here). If this turns out to be a separate problem it will be reopened.
Owner

@Arkex-ai commented on GitHub · 2026-11-12 06:05 UTC · original

Thanks, this narrows it down: successful MADV_REMOVE calls and the rendererless failures argue against treating this as an import-only problem or an ignored syscall error. The higher shared-page residency also confirms that zero-and-drop has a real memory trade-off on your setup.

I'll keep #2431 in draft behind this discard-safety gate; passing the startup-selector tests isn't enough to expand automatic memfd selection safely. I won't fold a speculative discard workaround into it.

Could you share the sanitized fault trace for one rendererless crash, including the faulting thread/backtrace and the last discard's address range relative to the fault? That would help check a retained reference or concurrent access without assuming either is the cause. No game files or saves are needed. Keeping #1000 linked as the tracking issue.

> [@Arkex-ai](https://github.com/Arkex-ai) commented on GitHub · 2026-11-12 06:05 UTC · [original](https://github.com/NotRexed/Hookshot/issues/2411#issuecomment-6816083235) Thanks, this narrows it down: successful `MADV_REMOVE` calls and the rendererless failures argue against treating this as an import-only problem or an ignored syscall error. The higher shared-page residency also confirms that zero-and-drop has a real memory trade-off on your setup. I'll keep #2431 in draft behind this discard-safety gate; passing the startup-selector tests isn't enough to expand automatic memfd selection safely. I won't fold a speculative discard workaround into it. Could you share the sanitized fault trace for one rendererless crash, including the faulting thread/backtrace and the last discard's address range relative to the fault? That would help check a retained reference or concurrent access without assuming either is the cause. No game files or saves are needed. Keeping #1000 linked as the tracking issue. <!-- github-sync:c:6816083235 -->
Owner

@CIB commented on GitHub · 2026-11-12 11:39 UTC · original

Rendererless fault trace with the last discards

I added a 16-entry discard ring that the crash handler dumps, so this is the exact last sequence, not the first-32 sample. One rendererless run (RX_BACKEND=webgpu, memfd window, MADV_REMOVE) crashed:

  • Thread: BaseProcCreateAndDeleteThread
  • Exception: 0xc0000005 read at host 0 (guest 0xfffff00000000000)
  • Backtraces: 24 host frames (hookshot + libc; offsets omitted), 16 guest LR frames (addresses omitted)

The ring, in order, with each discard's age at the fault. Every call returned ret=0 errno=0; the method is MADV_REMOVE for all of them.

age 314.2 ms  guest 0x7fa5bd1000  size 0x3000   os-decommit
age 313.8 ms  guest 0x7fa5bd1000  size 0x3000   os-decommit
age 313.5 ms  guest 0x7fa5bd1000  size 0x3000   os-decommit
age 313.2 ms  guest 0x7fa5c24000  size 0x1000   os-decommit
age 313.2 ms  guest 0x7fa5bd1000  size 0x3000   os-decommit
age 310.4 ms  guest 0x7fa5a80000  size 0x10000  mem-discard
age 307.8 ms  guest 0x7fa5a60000  size 0x2f000  mem-discard
age 304.4 ms  guest 0x7fa5a30000  size 0x59000  mem-discard
age 301.5 ms  guest 0x7fa5a50000  size 0x2f000  mem-discard
age 297.6 ms  guest 0x7fa59e0000  size 0x59000  mem-discard
age 290.7 ms  guest 0x7fa5990000  size 0xb0000  mem-discard
age 284.4 ms  guest 0x7fa59a0000  size 0x9d000  mem-discard
age 281.2 ms  guest 0x7fa59e0000  size 0x59000  mem-discard
age 279.8 ms  guest 0x7fa59e0000  size 0x59000  mem-discard
age 258.6 ms  guest 0x7fa59e0000  size 0x59000  mem-discard
age   0.3 ms  guest 0x7fa595e000  size 0x1000   os-decommit

What it says, and what it does not:

  • The last discard is 0.3 ms before the fault: 0x7fa595e000..0x7fa595f000. The fault is at host 0, so it is not inside that range, nor inside any of the previous 15 ranges.
  • Four os-decommit calls of the same 3-page range, 0x7fa5bd1000..0x7fa5bd4000, appear 314.2, 313.8, 313.5 and 313.2 ms before the fault. That repetition is worth looking at by itself.
  • The previous 15 discards are 258.6–314.2 ms old; the fault is 0.3 ms after a separate 4 KB decommit. So this is consistent with a concurrent access or a retained reference around that decommit, but it does not show the discarded range being dereferenced: the ring records only discards, and the fault is at host 0 rather than a guest heap address it could compare.

The ring is global; the log prefix is the faulting thread only because the dump runs in its crash handler. I can run a second rendererless trace to see whether the 0.3 ms adjacency and the repeated decommit reproduce.


🤖 Generated with opencode using DeepSeek V4.1 Flash.

> [@CIB](https://github.com/CIB) commented on GitHub · 2026-11-12 11:39 UTC · [original](https://github.com/NotRexed/Hookshot/issues/2411#issuecomment-6822566431) ### Rendererless fault trace with the last discards I added a 16-entry discard ring that the crash handler dumps, so this is the exact last sequence, not the first-32 sample. One rendererless run (`RX_BACKEND=webgpu`, memfd window, `MADV_REMOVE`) crashed: - **Thread:** `BaseProcCreateAndDeleteThread` - **Exception:** `0xc0000005` read at host 0 (guest `0xfffff00000000000`) - **Backtraces:** 24 host frames (hookshot + libc; offsets omitted), 16 guest LR frames (addresses omitted) The ring, in order, with each discard's age at the fault. Every call returned `ret=0 errno=0`; the method is `MADV_REMOVE` for all of them. ```log age 314.2 ms guest 0x7fa5bd1000 size 0x3000 os-decommit age 313.8 ms guest 0x7fa5bd1000 size 0x3000 os-decommit age 313.5 ms guest 0x7fa5bd1000 size 0x3000 os-decommit age 313.2 ms guest 0x7fa5c24000 size 0x1000 os-decommit age 313.2 ms guest 0x7fa5bd1000 size 0x3000 os-decommit age 310.4 ms guest 0x7fa5a80000 size 0x10000 mem-discard age 307.8 ms guest 0x7fa5a60000 size 0x2f000 mem-discard age 304.4 ms guest 0x7fa5a30000 size 0x59000 mem-discard age 301.5 ms guest 0x7fa5a50000 size 0x2f000 mem-discard age 297.6 ms guest 0x7fa59e0000 size 0x59000 mem-discard age 290.7 ms guest 0x7fa5990000 size 0xb0000 mem-discard age 284.4 ms guest 0x7fa59a0000 size 0x9d000 mem-discard age 281.2 ms guest 0x7fa59e0000 size 0x59000 mem-discard age 279.8 ms guest 0x7fa59e0000 size 0x59000 mem-discard age 258.6 ms guest 0x7fa59e0000 size 0x59000 mem-discard age 0.3 ms guest 0x7fa595e000 size 0x1000 os-decommit ``` What it says, and what it does not: - The **last discard is 0.3 ms before the fault**: `0x7fa595e000..0x7fa595f000`. The fault is at host 0, so it is **not inside that range**, nor inside any of the previous 15 ranges. - Four `os-decommit` calls of the **same** 3-page range, `0x7fa5bd1000..0x7fa5bd4000`, appear 314.2, 313.8, 313.5 and 313.2 ms before the fault. That repetition is worth looking at by itself. - The previous 15 discards are 258.6–314.2 ms old; the fault is 0.3 ms after a separate 4 KB decommit. So this is consistent with a concurrent access or a retained reference around that decommit, but it does **not** show the discarded range being dereferenced: the ring records only discards, and the fault is at host 0 rather than a guest heap address it could compare. The ring is global; the log prefix is the faulting thread only because the dump runs in its crash handler. I can run a second rendererless trace to see whether the 0.3 ms adjacency and the repeated decommit reproduce. --- 🤖 Generated with [opencode](https://opencode.ai) using DeepSeek V4.1 Flash. <!-- github-sync:c:6822566431 -->
Commenting is not possible because the repository is archived.
No milestone
No assignees
2 participants
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Hookshot#2411
No description provided.