Skip to content

fix: gcroot janet_vm.top_dyns stop plugin-worker SIGSEGV - #830

Merged
yogthos merged 3 commits into
mainfrom
fix/janet-plugin-ctd
Aug 31, 2026
Merged

fix: gcroot janet_vm.top_dyns stop plugin-worker SIGSEGV#830
yogthos merged 3 commits into
mainfrom
fix/janet-plugin-ctd

Conversation

@nikolap

@nikolap nikolap commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

mirror_capture_buffers_into_top_dyns created the top-level dyn table via janet_setdyn but only rooted the buffers inside it. Janet's collector never marks top_dyns (absent from the mark phase in 1.37.2 and 1.41.3 alike), so the first full GC freed the table and the next no-fiber janet_dyn (the stack-trace printer's :err-color lookup) read freed memory, faulting in janet_dict_find.

  • Root the table itself after the mirror; access the TLS janet_vm through janet_local_vm().
  • Add plugin_worker_uaf_stress regression test (env-driven, #[ignore]-gated) replaying the hook sequence that preceded the crash; previously SIGSEGV'd in 3 turns, now passes 3/30.

Refs: dirge-eona

`mirror_capture_buffers_into_top_dyns` created the top-level dyn table
via `janet_setdyn` but only rooted the buffers inside it. Janet's
collector never marks `top_dyns` (absent from the mark phase in 1.37.2
and 1.41.3 alike), so the first full GC freed the table and the next
no-fiber `janet_dyn` (the stack-trace printer's `:err-color` lookup)
read freed memory, faulting in `janet_dict_find`.

- Root the table itself after the mirror; access the TLS `janet_vm`
  through `janet_local_vm()`.
- Add `plugin_worker_uaf_stress` regression test (env-driven,
  `#[ignore]`-gated) replaying the hook sequence that preceded the
  crash; previously SIGSEGV'd in 3 turns, now passes 3/30.

Refs: dirge-eona
@nikolap
nikolap requested a review from yogthos August 31, 2026 11:44
nikolap and others added 2 commits August 31, 2026 14:00
- Collapse nested `if load_user_plugins { if let Ok(home) ... }` into a
  let-chain condition (clippy::collapsible-if).
- Reflow three long lines the way rustfmt 1.98.0 (CI stable) formats
  them; the local stable-aarch64 rustfmt is older and accepted the
  single-line forms.

Refs: dirge-eona
The fix rests on top_dyns sitting at offset 8 of a struct that is private
to janet.c, and janetrs depends on evil-janet "1", so a cargo update can
move it. A wrong offset wouldn't fail the build — it would hand
janet_gcroot whatever now lives there. The stress harness can't catch
that: it's #[ignore]d and needs the user's plugin dir, so CI had no
coverage of the fix at all.

top_dyns_offset_is_stable runs in the normal suite on a bare VM of its
own. It leans on the null-before-first-setdyn transition to discriminate
the field (abstract_registry is already non-null after janet_init;
core_env stays null and never gains dyn entries), then checks the entry
count and that janet_dyn reads the same table. Verified it fails when a
field is inserted ahead of top_dyns.

Also use the janet_local_vm already in the bindings instead of
re-declaring it. Two extern declarations of one symbol with different
return types only compiled because they sit in different crates, so
clashing_extern_declarations never fired. The pointer read moves into
top_dyns_ptr, which is what the test drives.
@yogthos
yogthos merged commit 60d5abd into main Aug 31, 2026
15 checks passed
@yogthos
yogthos deleted the fix/janet-plugin-ctd branch August 31, 2026 14:45
yogthos pushed a commit that referenced this pull request Aug 31, 2026
#830 landed without an entry; it's a user-facing hard crash.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants