Repository navigation
io patch: guard fiber-scheduler fd dispatches on embedded fds - #140
Merged
Merged
Conversation
With a fiber scheduler installed, io.c hands the fd to the scheduler hook (Async::Scheduler -> io-event's C extension), which issues raw libc fcntl/read on it -- EBADF for an embedded (memfs) fd, a flagged virtual descriptor the kernel does not know (tamatebako/tebako#252). Guard every fiber-scheduler fd dispatch in io_c_tebako_includes.patch with tebako_fd_is_embedded() -- the patch's existing copy_file_range/sendfile idiom. Embedded fds are served synchronously by the in-process engine and never block, so the dispatch is skipped (the direct wrapped path serves the op) and the rb_io_maybe_wait funnel answers "ready" immediately instead of waiting. Sites per line (context verified byte-identical across every release of each line; patches regenerated with git diff --no-index): - 3.2 (5): rb_io_read_memory, rb_io_write_memory, rb_writev_internal, io_read_memory_call (the readpartial/read_nonblock dispatch -- same io_read_memory hook), rb_io_maybe_wait. No pread/pwrite dispatch exists upstream on this line. - 3.3/3.4 (7): the 3.2 set plus the pread/pwrite dispatches (pread_internal_call / internal_pwrite_func, guard on arg->fd). - 4.0 (9): the 3.4 set plus the two dispatches that are live upstream on this line: io_flush_buffer_async and io_close (its hook's raw close(2) would die EBADF and a truthy result would skip the wrapped close, leaking the memfs fd). 3.1 keeps its current surface per the issue disposition; its io.c carries the same read/write/writev/maybe_wait dispatches (noted on the PR). Verified: git apply --check clean on all 35 releases of the touched lines; tools/lint green on all 37 versions (3.1 control untouched); tools/validate_manifests OK; rspec 121 examples 0 failures; compile_smoke green on each line's tip (darwin + msys cross). Closes #138
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #138.
Root-caused downstream in tamatebako/tebako#252 (full repro + evidence chain there): with a fiber scheduler installed, io.c's read/write dispatches hand the fd to the scheduler hook —
Async::Scheduler#io_read→ io-event's C extension — which issues raw libcfcntl/readon it. A VFS-backedFileholds a tagged virtual fd (fd | 0x40000000) that only works because this patch macro-wraps the fd verbs intotfs_*dispatchers inside ruby core's translation units; the extension's calls escape that, the packaged runtime has no interposition tier for them, and the kernel answers EBADF (read(1073742315)— the exact fd on the issue).Fix
Guard every fiber-scheduler fd dispatch in
io_c_tebako_includes.patchwithtebako_fd_is_embedded(...)— the same idiom the patch already uses forcopy_file_range/sendfile. Embedded fds are served synchronously by the in-process engine and never block, so the guard skips the scheduler (the direct wrapped path serves the op — no concurrency is lost), and the wait funnel answers "ready" immediately instead of dispatchingio_wait.Per line (ruby 3.4.8 io.c line numbers cited on the issue; per-line equivalents found by hand, context verified byte-identical across every release of each line):
rb_io_read_memory,rb_io_write_memory,rb_writev_internal,io_read_memory_call(the readpartial/read_nonblock dispatch — sameio_read_memoryhook, needed to cover the whole non-blocking read surface), and therb_io_maybe_waitEAGAIN funnel (coversrb_io_maybe_wait_readable/_writableand every direct caller; embedded fd ⇒ return the requested events immediately). No pread/pwrite dispatch exists on this line (added upstream in 3.3) — nothing to guard there.pread_internal_call/internal_pwrite_func(guard onarg->fd).rb_io_pread/rb_io_pwritedispatches live in the*_internal_call/*_funchelpers).io_closeneeds no guard here — its scheduler dispatch is commented out upstream (io.c:5570).io_flush_buffer_async(buffered-write flush →io_write_memoryhook) andio_close(rb_fiber_scheduler_io_close(scheduler, RB_INT2NUM(fd))— the 4.x re-check the issue asked for: unguarded, the hook's rawclose(2)dies EBADF and a truthy result would skip the wrapped close, leaking the memfs fd).One extra site beyond the issue's literal list is guarded everywhere:
io_read_memory_call(the scheduler dispatch behindIO#readpartial/IO#read_nonblock). It is the sameio_read_memoryhook escaping the same way; leaving it open would keepreadpartialbroken under Async. Recorded here per the issue's "guard every fiber-scheduler fd dispatch".Still out of scope (adjacent gaps recorded on the issue, unchanged):
IO#wait/rb_io_waitandrb_io_wait_readable/_writableon raw fds (selector registration — unreachable on the #252 read path since memfs reads never EAGAIN),IO.select/rb_fiber_scheduler_io_selectvon tagged fds.3.1 line
Checked per the task: 3.1's io.c does carry the same read/write/writev/
io_read_memory_callscheduler dispatches and the samerb_io_maybe_waitfunnel (no pread/pwrite hooks). Per the task disposition the 3.1 line keeps its current surface in this PR; if 3.1 runtimes ever matter for Async workloads, the same guard set applies mechanically (context hashes match 3.2's shape).Verification
git apply --checkof each regenerated patch against every release of its line (35/35 clean: 3.2.4–3.2.11, 3.3.3–3.3.12, 3.4.1–3.4.10, 4.0.0–4.0.7); applied result diffed against the intended tree (byte-identical). Patch bodies regenerated withgit diff --no-indexfrom the pristine tarballs — the pre-existing hunks (include block,copy_file_range,sendfile) are byte-identical modulo@@offsets.tools/validate_manifests— OK.tools/lintfor all 37 versions in versions.yml (3.1 control line included, its patch untouched and byte-identical) — all clean.bundle exec rspec— 121 examples, 0 failures (no selection-metadata change, so no new specs).tools/compile_smoke(io.o + all patched TUs) per changed line's tip version: 3.2.11 / 3.3.12 / 3.4.10 / 4.0.7 on darwin (host) — green; msysx86_64-w64-mingw32cross legs — green. CI runs the full diff-aware smoke matrix (incl. darwin on macos-latest and the msys cross leg) on this PR via lint-patches.read/pread/pwrite/writevpaths (memfs reads never EAGAIN, sorb_io_blocking_region_wait's retry path is never entered).Patch mechanics
Diffs regenerated with
git diff --no-indexagainst the pristine 3.2.11/3.3.12/3.4.10/4.0.7io.c(each line's tip);@@trailing context stripped to match repo style. Guards added as comment + extendedifcondition at each dispatch site — no manifest/selection changes (whole-line features, unchanged names), so selection for every previously-selected version is byte-identical in shape and the 3.1 folder is untouched.