Repository navigation
core/process: resource limits, real spawn/exec argument handling, the wait family (fail+error 193 -> 40) - #617
Merged
Merged
Conversation
Implements the POSIX half of the Process module that rbgo had no trace of
(`grep RLIMIT internal/vm` found nothing before this change):
- Process::RLIMIT_*, Process::RLIM_INFINITY / RLIM_SAVED_MAX /
RLIM_SAVED_CUR and Process::PRIO_PROCESS / PRIO_PGRP / PRIO_USER,
taken from the host kernel headers through x/sys/unix rather than
written out, since the numbers differ per OS (RLIMIT_NPROC is 6 on
Linux and 7 on Darwin).
- Process.getrlimit / setrlimit, with the full designator coercion.
- Process.getpriority / setpriority.
- Process.getpgid / getpgrp / setpgid / setpgrp / getsid.
- Process.kill.
Read for this (ruby/ruby v3_4_0):
- process.c rlimit_resource_name2int, rlimit_resource_type,
rlimit_resource_value, proc_getrlimit, proc_setrlimit - the
uppercase-only name rule, the #to_str-before-#to_int order, the
INFINITY / SAVED_MAX / SAVED_CUR limit names, and the fact that
proc_setrlimit converts both limits before the resource.
- process.c proc_getpgid / proc_setpgid / proc_getsid /
proc_getpriority / proc_setpriority, including that all of them fail
with rb_sys_fail(0) (no operation name in the message) while the
rlimit pair pass one.
- signal.c signm2signo and rb_f_kill - the leading '-' process-group
form, the optional SIG prefix, "bad signal type <class>", and the
return value of argc-1.
- the ruby/spec expectations under core/process/{constants,getrlimit,
setrlimit,kill,getpgid,getpriority}_spec.rb.
Platform split follows the existing spawn_native/spawn_windows/spawn_wasm
pattern: the kernel tables and the syscall wrappers are the only things
behind a build tag, every coercion and error-shaping branch is in the
shared process.go so the POSIX lanes cover all of it.
One judgement call is documented in getpriorityFailed: getpriority(2)
legitimately returns -1, which Darwin's libc boundary cannot distinguish
from failure, so Go's wrapper reports a stale errno. MRI clears errno
before the call and tests it after (proc_getpriority); Go cannot read
errno, so a -1 result is believed unless the reported errno is one
getpriority(2) documents.
core/process on the pinned corpus: constants 2 -> 18 passing, getrlimit
2 -> 9, setrlimit 0 -> 30, kill 0 -> 4, getpgid 0 -> 3, getpriority
0 -> 4. Verified byte-for-byte against MRI 4.0.5 for every method and
every error message.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d argv0
Process.spawn scored pass=0 on 45 examples because it peeled its
arguments by hand and ran everything through a combined capture. This
replaces that with MRI's own argument model and adds the surface the
rest of core/process was blocked on.
Read for this (ruby/ruby v3_4_0 process.c unless noted):
- rb_exec_getargs: the options Hash comes off the END first, the
environment Hash off the FRONT afterwards - which is why
`Process.spawn({})` is an ArgumentError about the COMMAND, and why
check_hash rules String and Array out of the environment slot while
letting anything with #to_hash in.
- rb_check_argv: the [prog, argv0] command array - its FIRST element
is the file to execute and its second is the name the child is told,
it must hold exactly two elements, and every argument goes through
StringValue then StringValueCStr (#to_str, then "string contains
null byte").
- check_exec_env_i: "environment name contains a equal", the null-byte
rejection on keys and values, and a nil value unsetting a variable.
- rb_execarg_addopt / check_exec_options_i: the option keys, :pgroup's
false/nil/true/0/pgid shapes with "negative process group ID : -1",
and the two distinct errors - "wrong exec option" for a String key,
"wrong exec option symbol: x" for an unknown Symbol.
- check_exec_redirect: :out/:err as a String, a [name, mode] Array, an
IO, an object with #to_io, a descriptor number, :close and
[:child, :out].
- rb_f_spawn / rb_f_exec, and rb_spawn_internal's error path: a
command that cannot be executed reports the errno its child's execve
failed with, naming the command as WRITTEN, and leaves $? at the 127
that child exited with - already reaped, so Process.wait must not
find it.
- proc_wait / proc_wait2 / proc_waitall and the waitpid/waitpid2
aliases (one C function registered twice, so Method#== holds),
rb_last_status_get, and rb_process_status_wait's pid -1 for "no
children".
- rb_proc_times and the rb_cProcessTms Struct; rb_clock_getres;
proc_setmaxgroups, which is a process-local tunable, not the
accepted-and-ignored no-op this had; proc_setproctitle;
rb_proc_warmup; and the Process::UID / Process::GID / Process::Sys
identity modules.
- the ruby/spec expectations under core/process/.
What is now real rather than modelled: the environment the child sees
(including :unsetenv_others), its working directory, the program lookup
against the CHILD's PATH rather than the parent's - os/exec would
otherwise search the wrong one - the files :out and :err name, and the
errno of a command that cannot be run. The execution itself stays
synchronous, as the rest of this file has always been.
core/process on the pinned corpus, this commit and the previous one
together: pass 20 -> 165, fail+error 193 -> 40. Every method and every
error message verified byte-for-byte against MRI 4.0.5. Full suite green
on darwin; builds clean for GOOS=wasip1, windows and linux; internal/vm
coverage unchanged at 99.9% with every new function at 100%.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both Linux arch lanes failed TestProcessRlimitCoercion with
TypeError: can't convert Object to Integer (Object#to_int gives Object)
on `Process.setrlimit(:CORE, *Process.getrlimit(:CORE))`. Darwin cannot
see it: its RLIM_INFINITY is 0x7fff_ffff_ffff_ffff, which fits an int64.
What it actually is, established rather than guessed. configure.ac's
RUBY_REPLACE_TYPE(rlim_t, [int long "long long"], RLIM, ...) resolves
rlim_t to "unsigned long long" on both platforms, so RLIM2NUM is ULL2NUM
and NUM2RLIM is NUM2ULL. ULL2NUM of a value above LONG_MAX yields a
BIGNUM, which is why process.c's own example reads
Process.getrlimit(:CORE) # => [0, 18446744073709551615]
Reproduced here by feeding the Linux value through the getrlimit seam on
Darwin, which printed the same error. That also showed the diagnosis is
narrower than "an element is not an Integer": getrlimit was already
right, returning [0, 18446744073709551615] with BOTH elements answering
is_a?(Integer) — byte-identical to MRI on Linux. The defect was entirely
on the way back in. rlimitResourceValue went through procToInt (NUM2INT,
int64), which rejected the *object.Bignum as a non-integer; the "Object"
in the message is classNameOf not knowing that type.
Fixes:
- rlimToUint implements NUM2RLIM properly: a Bignum within uint64 is
taken at full width, one beyond it is a RangeError naming the C type
MRI names ("bignum too big to convert into 'unsigned long long'"),
and a negative limit wraps as the cast does — MRI hands
setrlimit(:CORE, -1, -1) to the kernel, which answers EPERM.
- procToInt reports a Bignum as a RangeError ("bignum too big to
convert into 'long'"), which is what NUM2INT/NUM2PIDT do, instead of
a TypeError. Any Bignum reaching it is out of int64 range by
construction, since object.NormInt demotes everything that fits.
All three messages checked against MRI 4.0.5.
TestProcessRlimitInfinityRoundTrip pins the Linux representation on
every platform through the seam and asserts the round trip is a no-op,
so this cannot regress from a Darwin-only run again.
TestProcessRlimitSymbolicMatchesTable keeps the real host table checked
separately, since a faked getrlimit cannot speak for it.
This also unblocks setrlimit_spec's 30 examples on Linux, every one of
which is a getrlimit -> setrlimit round trip.
Gates: full go test ./... green; gofmt clean; go vet clean for darwin,
linux/amd64 and linux/arm64 (which type-checks the test files for the
lanes that failed); builds clean for wasip1, windows, linux/amd64,
linux/arm64. internal/vm coverage 99.9%, unchanged, every new function
at 100%.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tannevaled
added a commit
that referenced
this pull request
Sep 21, 2026
#617 pinned `p Process.clock_getres(Process::CLOCK_MONOTONIC)` to "1e-06" and its comment said why: the value matched MRI but the spelling did not, because Float#inspect dropped the ".0" before the exponent. That is the divergence this branch fixes, so the pinned value becomes MRI's — `ruby -e 'p Process.clock_getres(Process::CLOCK_MONOTONIC)'` prints 1.0e-06 here. The case only surfaced after #617 merged: this branch was cut before it, so the full suite was green when the float change was written and red once the two met. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tannevaled
added a commit
that referenced
this pull request
Sep 21, 2026
…11) (#622) * object: lay out a Float the way MRI does, not the way Go's %g does Float#to_s formatted with strconv.FormatFloat(v, 'g', -1, 64) and appended ".0" only when the result held no '.', 'e' or 'E'. That diverged from MRI in two independent ways. The mantissa lost its fraction in exponential notation, because the ".0" was only ever appended in fixed notation: 1e-06 MRI 1.0e-06 rbgo 1e-06 1e+20 MRI 1.0e+20 rbgo 1e+20 5e-324 MRI 5.0e-324 rbgo 5e-324 And the fixed/exponential threshold was Go's, not Ruby's. Go's 'g' verb compares the exponent against the number of significant digits; MRI's flo_to_s (numeric.c) switches on where the decimal point falls — exponential iff decpt < -3 || decpt > DBL_DIG (15). Measured against ruby 4.0.5, the boundaries are exactly: 1e14 100000000000000.0 decpt 15, fixed 1e15 1.0e+15 decpt 16, exponential 1e-4 0.0001 decpt -3, fixed 1e-5 1.0e-05 decpt -4, exponential The visible symptom was 1e6: MRI prints 1000000.0, rbgo printed 1e+06. Take the shortest round-tripping digits and the exponent from 'e' with precision -1, decide the layout by MRI's rule, and in exponential notation give the mantissa a fractional digit. Go's exponent padding already matches (e+06, e-05, e+100, e-323), so nothing there changes. Verified beyond the pinned cases: 200 pseudorandom floats spanning 10^-20 to 10^20 render byte-identically to ruby 4.0.5. Sweeping core/float, core/kernel and language with the pre- and post-fix binaries against the same corpus: 3580 -> 3591 passing, 542 -> 531 failing. Closes #619. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: clock_getres now prints MRI's spelling #617 pinned `p Process.clock_getres(Process::CLOCK_MONOTONIC)` to "1e-06" and its comment said why: the value matched MRI but the spelling did not, because Float#inspect dropped the ".0" before the exponent. That is the divergence this branch fixes, so the pinned value becomes MRI's — `ruby -e 'p Process.clock_getres(Process::CLOCK_MONOTONIC)'` prints 1.0e-06 here. The case only surfaced after #617 merged: this branch was cut before it, so the full suite was green when the float change was written and red once the two met. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
tannevaled
added a commit
that referenced
this pull request
Sep 23, 2026
Seven units since the last bump: #617 core/process: spawn, exec, the rlimits, priorities, process groups, kill, the wait family (pass 20 -> 165) #618 core/kernel + core/string: require_relative with an absolute path, the require/load argument contract, LoadError#path, byteindex/ byterindex, split, chop/chars/each_char (kernel 233 -> 184, string 179 -> 153 failing) #620 language: compound assignment evaluation order, the flip-flop operator, magic-comment encodings, loop break values (pass 1308 -> 1443) #622 object: Float#to_s laid out by MRI's rule rather than Go's %g (+11) #624 the mspec shim: a nested describe can see the helpers of the one around it (+28) #625 deps: parser v0.2.0 (language +333) #626 compiler: for-loop targets, block-local variables, what defined? declares (language +382) Measured with scripts/conformance/rubyspec/run.sh against the pinned corpus at 87b1631, 16-way, TIMEOUT=25, three consecutive runs: 21749, 21750, 21750. The floor is set nine below the lowest, the same margin rule as the last bump: it absorbs the run-to-run jitter without being wide enough to hide the loss of a whole spec file, which is what the ratchet exists to catch. #615 (an intermittent frame-pop crash) is still open and can cost a file when it fires. TWO OF THESE ARE NOT VM CONFORMANCE, and the number should not be read as if they were. #624 fixed our own mspec shim: core/string/valid_encoding/utf_8_spec scored 0/28 under MRI 4.0.5 too, so those 28 were harness noise counted as VM failure. #625 is a dependency bump; the VM did not change, but thirteen language spec files became parseable for the first time and brought 333 already- passing examples into view. Both are real gains in what the measurement can see, not in what the VM can do. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.
What this does
core/processwas the largest unimplemented block left in the suite: pass=20, fail+error=193 on the pinned corpus.grep -rn RLIMIT internal/vm/*.gofound nothing at all, andProcess.spawnscored pass=0 across 45 examples because it peeled its arguments by hand and ran everything through a combined capture.After: pass=165, fail+error=40.
setrlimit_specspawn_specconstants_specgetrlimit_specexec_speckill_specclock_getres_spectimes_spec+tms/*wait/wait2/waitall/waitpid/waitpid2/last_status/Status.waitargv0/warmup/maxgroups/getpgid/getpriority/uid/gid/egid/euidMethods implemented, and the C source read for each
Everything below was read before it was written, not reconstructed.
Resource limits —
process.crlimit_resource_name2int,rlimit_resource_type,rlimit_resource_value,proc_getrlimit,proc_setrlimit.Process::RLIMIT_*,Process::RLIM_INFINITY/RLIM_SAVED_MAX/RLIM_SAVED_CUR,Process.getrlimit,Process.setrlimit. The C gave: the uppercase-only name rule (rlimit_resource_name2intmatches case-insensitively and then rejects any name that is not all-uppercase, so"core"is anArgumentError);rb_check_string_typebeforeNUM2INT, so#to_stris tried before#to_intand an Integer never reaches#to_strat all (T_FIXNUM/T_BIGNUM are their own switch cases); theINFINITY/SAVED_MAX/SAVED_CURlimit names matched by exactstrcmp; and thatproc_setrlimitconverts both limits before the resource. The resource numbers come from the host kernel headers throughx/sys/unixrather than being written out, because they differ per OS (RLIMIT_NPROCis 6 on Linux and 7 on Darwin).Scheduling priority —
process.cproc_getpriority/proc_setpriority.Process::PRIO_PROCESS/PRIO_PGRP/PRIO_USER,Process.getpriority,Process.setpriority.Process groups and sessions —
process.cproc_getpgid/proc_setpgid/proc_getsid.Process.getpgid,getpgrp,setpgid,setpgrp,getsid. The C also settled the message shape: all of these fail withrb_sys_fail(0)— no operation name — while the rlimit pair pass one, soProcess.getpgid(2**30)says"No such process"andProcess.getrlimit(-7)says"Invalid argument - getrlimit".Signals —
signal.csignm2signoandrb_f_kill.Process.kill, including the leading--process-group form, the optionalSIGprefix,"bad signal type <class>"for anything that is neither Integer nor#to_str,"unsupported signal 'SIGFOO'"(MRI prints the same text for both"FOO"and"SIGFOO"), and the return value ofargc-1.spawn / exec argument handling —
process.crb_exec_getargs,rb_check_argv,check_exec_env_i,rb_execarg_addopt,check_exec_options_i,check_exec_redirect,rb_f_spawn,rb_f_exec. The reading is what produced the behaviour:Process.spawn({})is anArgumentErrorabout the command rather than a lone-Hash command;check_hashrulesStringandArrayout of the environment slot but lets anything with#to_hashin;[prog, argv0]command array's first element is the file to execute and its second is the name the child is told — getting that backwards is what madespawn(["/bin/sh", "argv_zero"], "-c", "echo $0")fail in the first draft;StringValuethenStringValueCStr, giving#to_strcoercion then"string contains null byte";"environment name contains a equal : FOO=", and a nil environment value unsetting the variable;:pgroup's false/nil/true/0/pgid shapes, with"negative process group ID : -1", and the two different errors for a bad option key: bare"wrong exec option"for a String key,"wrong exec option symbol: nonesuch"for an unknown Symbol;rb_spawn_internal's error path: a command that cannot be executed raises the errno its child'sexecvefailed with, names the command as written rather than as resolved, and leaves$?reporting the 127 that child exited with — already reaped, soProcess.waitmust not find it.Process.execis now a module function (it was only reachable as the privateKernel#exec) and shares that argument handling.The wait family —
process.cproc_wait/proc_wait2/proc_waitall,rb_last_status_get,rb_process_status_wait.Process.wait,wait2,waitall,last_status,Process::Status.wait, andwaitpid/waitpid2re-pointed atwait/wait2as MRI's aliases (one C function registered twice, soProcess.method(:waitpid) == Process.method(:wait)holds).Process::Status.waitanswers with pid −1 for "no children" instead of raising.Residuals —
process.crb_proc_timesand therb_cProcessTmsStruct,rb_clock_getres,proc_setmaxgroups,proc_setproctitle,rb_proc_warmup, and theProcess::UID/Process::GID/Process::Sysmodule definitions at the foot ofInitVM_process.Process.times+Process::Tms,Process.clock_getres,Process.argv0(frozen, and the same object every call, asrb_prognameis),Process.setproctitle,Process.warmup, andProcess.maxgroups=— which MRI keeps in a static that reads back, not the accepted-and-ignored no-op this had.What is now real rather than modelled
The environment the child sees (including
:unsetenv_others), its working directory, the program lookup against the child'sPATH—os/execwould otherwise search the parent's, which is a different search and is what made theenv['PATH']example fail — the files:outand:errname, and the errno of a command that cannot be run. The execution itself stays synchronous, which is the model the rest ofspawn.gohas always had.What I judged unreachable, and why
These are left failing on purpose. An honest 165 is worth more than a fabricated 193.
kill_spec, 8 examples — every one drivesProcessSpecs::Signalizer, which starts a realrubyin a thread and reads its pid from a file.ruby_exeis unsupported by the corpus shim, so the thread dies and the pid staysnil; the examples then error onProcess.kill(sig, nil)whateverkilldoes. They need a real fork.abort_spec, 6 examples — blocked onIOStub, an mspec helper the corpus shim does not define. Nothing to do withProcess.abort;spec_helper.rbis another agent's file this wave.uid=/euid=/gid=/egid=/groups=/initgroups, 9 examples — these mutate this interpreter's own credentials. There is no child process to confine the change to in a synchronous model, and a spec run must not change the identity of the shell it was started from. Implementing them would also mean a test that callssetuid/setgroupsfor real on every CI run. I deliberately did not implement a version that raisesEPERMwithout attempting the syscall: that would pass the specs by fabricating the result.detach_spec, 4 examples —Process.detachmust return aProcess::Waiter, a liveThreadsubclass carrying the child's status as its value. Two of the four are pure argument validation, but implementing half of it to collect those would leave aProcess.detachthat answers with the wrong kind of object. Left for a wave that can do the thread properly — and the previous wave'sFile.mkfifodeadlock is a standing reason not to add thread machinery in a hurry.fork_spec2,_fork_spec1,daemon_spec1 — (corrected: an earlier revision of this section had the direction of this backwards.) Measured, not inferred: rbgo answersProcess.respond_to?(:fork) == falseand MRI answerstrue, so rbgo takes the specs'guard_notbranch. What fails inside it is that rbgo does defineKernel#forkas a private method — the Puppet fork/exec idiom this file is built on — sorespond_to?(:fork, true)istruewhere the spec wantsfalse, andProcess.forkraisesNoMethodError: private method 'fork' calledwhere it wantsNotImplementedError. DefiningProcess._fork/Process.daemonto raiseNotImplementedErrorwould fix one example in each file and break therespond_to?example beside it — net zero. Closing them properly needs MRI'srb_f_notimplement: a method that is callable but invisible torespond_to?, which lives inobject_model.go, another agent's file this wave.spawn_spec1,wait_spec1 — needruby_cmd/ a genuinely concurrent child.last_status_spec1 —$?is thread-local in MRI; it is one global here.set_proctitle_spec1 — rewriting the real process title needs theargvarea the C runtime owns, which aCGO_ENABLED=0Go program cannot reach.Process.setproctitlereturns the title and leaves$0alone, as documented, butpskeeps showing the original command.clock_gettime_spec2 — an unrelated missing unit check and a missingkernel_version_isguard in the shim.Judgement calls worth a second pair of eyes
getpriorityFailed(process.go).getpriority(2)legitimately returns −1 — a nice value of −1 — which at Darwin's libc boundary is indistinguishable from failure: Go's wrapper decides by the return value and then reports whatevererrnohappened to hold, soProcess.getpriority(PRIO_USER, uid)came back with a staleErrno::ETIMEDOUTon this host. MRI has the same call and the same −1 and resolves it by clearingerrnobefore and testing it after (proc_getpriority); Go cannot reacherrno, so a −1 result is believed unless the reported errno is onegetpriority(2)is documented to set. The rule lives in shared code, not behind a build tag, so both POSIX lanes cover all four of its branches.Process.clock_getres(CLOCK_MONOTONIC)prints1e-06where MRI prints1.0e-06. Same value; this VM'sFloat#inspectdrops the.0before an exponent (p 0.000001diverges the same way). That is a core Float-formatting divergence ininternal/object, not a clock one, and outside this lane's files.Linux fix after the first CI run (third commit)
The first push was red on both Linux arch lanes and green on macOS and wasm.
Process.setrlimit(:CORE, *Process.getrlimit(:CORE))raisedTypeError: can't convert Object to Integer (Object#to_int gives Object).Established from the source rather than guessed:
configure.ac'sRUBY_REPLACE_TYPE(rlim_t, [int long "long long"], RLIM, ...)resolvesrlim_ttounsigned long longon both platforms, soRLIM2NUMisULL2NUMandNUM2RLIMisNUM2ULL— andULL2NUMof anything aboveLONG_MAXis a Bignum. That is whyprocess.c's own doc example readsProcess.getrlimit(:CORE) # => [0, 18446744073709551615]. Darwin cannot see it: itsRLIM_INFINITYis0x7fff_ffff_ffff_ffff, which fits anint64.I reproduced it on macOS by feeding the Linux value through the
getrlimitseam, which printed the identical error — and that showed the diagnosis is narrower than "an element is not an Integer":getrlimitwas already correct, returning[0, 18446744073709551615]with both elements answeringis_a?(Integer), byte-identical to MRI on Linux. The defect was entirely on the way back in —rlimitResourceValuewent throughprocToInt(NUM2INT,int64), which rejected the*object.Bignum; theObjectin the message isclassNameOfnot knowing that type.rlimToUintnow implementsNUM2RLIM: a Bignum withinuint64taken at full width, one beyond it aRangeErrornaming the C type MRI names (bignum too big to convert into 'unsigned long long'), and a negative limit wrapping as the cast does — MRI takessetrlimit(:CORE, -1, -1)to the kernel, which answersEPERM.procToIntreports a Bignum asRangeError: bignum too big to convert into 'long', which is whatNUM2INT/NUM2PIDTdo, rather than aTypeError. Any Bignum reaching it is out ofint64range by construction, sinceobject.NormIntdemotes everything that fits.All three messages checked against MRI 4.0.5.
TestProcessRlimitInfinityRoundTrippins the Linux representation on every platform through the seam and asserts the round trip is a no-op, so a Darwin-only run cannot miss this again;TestProcessRlimitSymbolicMatchesTablekeeps the real host table checked separately, since a fakedgetrlimitcannot speak for it. This also unblockssetrlimit_spec's 30 examples on Linux, every one of which is agetrlimit->setrlimitround trip.Gates
CGO_ENABLED=0 go build ./..., plusGOOS=wasip1 GOARCH=wasm,GOOS=windows,GOOS=linux GOARCH=amd64andGOOS=linux GOARCH=arm64— all clean; andgo vetfor both Linux arches, which type-checks the test files for the lanes that failed. The platform split follows the existingspawn_native/spawn_windows/spawn_wasmpattern: the kernel tables and the syscall wrappers are the only things behind a build tag (procPosixis a const the non-POSIX variants set to false, soProcess.respond_to?(:getrlimit)is false there exactly as MRI'srb_f_notimplementmakes it), and every coercion and error-shaping branch is in the shared file.CGO_ENABLED=0 GOWORK=off go test ./...— full suite green.gofmt -l internal/vm/— empty.go vetclean.internal/vmcoverage unchanged at 99.9%, with every new function at 100% and the below-100% list byte-identical to the pre-change one. The one piece of dead code the rewrite left behind (writeSpawnOutput) is deleted rather than left uncovered.core/processsweep runs in the same ~45s it did before.🤖 Generated with Claude Code