Skip to content

feat: add experimental Sentry Formula runtime and CRIU round trip - #195

Closed
MeteorsLiu wants to merge 2 commits into
xgo-dev:mainfrom
MeteorsLiu:codex/sentry-runtime-mvp
Closed

MeteorsLiu wants to merge 2 commits into
xgo-dev:mainfrom
MeteorsLiu:codex/sentry-runtime-mvp

Conversation

@MeteorsLiu

@MeteorsLiu MeteorsLiu commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Add isolated MVPs for running ixgo Formulas under Sentry and preserving Go callback state across a native → Sentry → native round trip. Close #194

The implementation includes:

  • experimental/sentry-gate: standalone Sentry startup with DirectFS, an in-process LISAFS service, and an ixgo Formula guest.
  • An LLAR-owned Context.Switch wrapper that inspects syscall arguments after the underlying platform returns and before Sentry dispatches the syscall; non-syscall events pass through unchanged.
  • experimental/criu-import: conversion between native CRIU images and pinned Sentry state, including memory, registers, thread identities, signals, and the supported descriptor state.
  • runSandbox: capture the native caller, execute its callback in Sentry, export the completed state, and restore a new native continuation without a fixed business-result ABI.
  • Reproduction scripts and probes for context changes, new allocations and aliases, GC/timers, pipe/epoll I/O, and continuation after the callback.

Both experiments remain separate Go modules and leave production LLAR execution unchanged. The round-trip MVP currently uses Linux ARM64/ptrace; real Formula integration into that round trip, systrap migration, and production hardening remain unfinished.

@codecov

codecov Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Sentry runtime MVP (gVisor / CRIU round-trip)

Thorough, well-structured experimental PR (~3.5k lines across two nested modules). Error handling is careful, recover() is used appropriately around wire-graph traversal, ref-counting discipline is correct, and the READMEs are unusually accurate (sample outputs match the exact Printf format strings; the verify-linux.sh gate assertion matches the nativeResumed constant).

Trust model note: most parsing is best-effort with panic/recover, and the harness runs as container root with broad caps and seccomp=unconfined. That is acceptable while checkpoint/state images are self-generated in the same run, but the size/offset math on image-supplied values (findings below) should be hardened before this path ever ingests images it did not itself produce.

Highest-value items: two write-handle FD leaks that also risk reading back unflushed data, and missing overflow/ordering validation on untrusted page/VMA size math. Remaining findings are minor. Nothing here is wired into the main llar build.

Additional findings

  • experimental/criu-import/internal/sentryimport/files_linux.go:1449: [P3] Backing-file path from image opened without confinement: reg.GetName() is an absolute path taken verbatim from the CRIU files.img and opened as container root (with O_RDWR when a writable shared mapping matches). A tampered image could direct the runtime to open/write any host file reachable in the mount namespace. Fine for self-generated images; once images can come from an untrusted source, confine backing files to an allowlisted root and reject paths outside it.

Comment on lines +114 to +122
stateFile, err := os.Create(statePath)
if err != nil {
return err
}
if err := k.SaveTo(ctx, stateFile, nil, nil, false, true); err != nil {
return err
}
k.BeforeResume(ctx)
f, err := os.Open(statePath)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] stateFile write handle leaked and reopened before flush

stateFile (os.Create(statePath)) is written by k.SaveTo(...) but never closed. Immediately after, the same path is reopened as f for reading (os.Open(statePath)) and passed to readGraph. The write handle leaks, and the reopen relies on unflushed buffered data having reached disk. Close stateFile (checking the error) after SaveTo/BeforeResume and before the os.Open, so the data is flushed before it is read back.

Comment on lines +193 to +202
raw, err := os.Create(statePath + ".template")
if err != nil {
return err
}
k.Pause()
if err := k.SaveTo(ctx, raw, nil, nil, false, true); err != nil {
return fmt.Errorf("save template: %w", err)
}
k.BeforeResume(ctx)
if err := rewriteIDs(statePath+".template", statePath, ids, s.process); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] template write handle leaked and reopened before flush

Same pattern as the sentry.state path: raw (os.Create(statePath+".template")) is written by k.SaveTo(...) but never closed, and rewriteIDs then reopens statePath+".template" for reading. The write handle leaks and the reopen depends on unflushed data reaching disk. Close raw (checking the error) after SaveTo/BeforeResume and before calling rewriteIDs.

Comment on lines +68 to +76
count := e.GetNrPages()
if e.NrPages == nil {
count = uint64(e.GetCompatNrPages())
} // CRIU 3.17 stores field 2.
data := make([]byte, count*hostarch.PageSize)
if _, err := io.ReadFull(pages, data); err != nil {
return err
}
if n, err := m.CopyOut(ctx, hostarch.Addr(e.GetVaddr()), data, usermem.IOOpts{}); err != nil || n != len(data) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Unchecked page-count math on image-supplied pagemap entry

count comes straight from the deserialized pagemap image and count*hostarch.PageSize is unchecked: a malformed/hostile entry can overflow uint64 or request an enormous allocation, and e.GetVaddr() is not validated to fall inside a mapped VMA / the user address range before the CopyOut. The VMA-range check in run_linux.go validates VMAs, not pagemap entries. Validate count against a sane maximum and confirm [vaddr, vaddr+count*PageSize) lies within a known VMA before allocating and copying. Acceptable while images are self-generated, but worth hardening before untrusted images can reach this path.

Comment on lines +99 to +100
os.Exit(0)
return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Unreachable return after os.Exit(0)

os.Exit(0) intentionally retires the supervisor and bypasses deferred cleanup, but the trailing return nil (and the stray blank line before the closing brace) is unreachable dead code. Remove it.

}
for _, v := range s.memory.Vmas {
if v.GetStatus()&8 != 0 { // CRIU VMA_AREA_VDSO.
data := make([]byte, v.GetEnd()-v.GetStart())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] VMA length math lacks End >= Start validation

Start/End are read from the deserialized mm image without checking End >= Start. If End < Start, v.GetEnd()-v.GetStart() wraps to a near-2^64 length, producing a giant make([]byte, ...) (VDSO branch, line 82) or an out-of-range mmap/mprotect length (lines 38, 123). The range check in run_linux.go compares each endpoint to platform min/max independently and does not enforce ordering. Add an explicit End >= Start (and page-alignment) check when reading the checkpoint.

return fmt.Errorf("unexpected non-private PMA")
}
pageEntries = append(pageEntries, &pagemap.PagemapEntry{Vaddr: proto.Uint64(start), CompatNrPages: proto.Uint32(uint32((end - start) / 4096)), Flags: proto.Uint32(4)})
buf := make([]byte, 64<<10)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] 64 KiB copy buffer reallocated per PMA segment

buf := make([]byte, 64<<10) is allocated fresh inside the per-segment export loop, though its contents are fully overwritten by each CopyIn. Hoist the allocation above the loop to drop one 64 KiB heap allocation per segment. Minor at current scale; scales with segment count.

@MeteorsLiu

Copy link
Copy Markdown
Collaborator Author

Closed, see #194

@MeteorsLiu MeteorsLiu closed this Sep 20, 2026
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.

Proposal: LLAR Formula Sandbox

1 participant