`Child.takeStdin` loses `setsid`, so `Child.kill` only signals the child, not its process group
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 72/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- cpp
- Domain
- operating-systems
Research direction
The bug is in src/runtime/process.cpp: spawn writes the setsid byte at scalar offset 3 * sizeof(object *) + sizeof(pid_t), kill reads it, and takeStdin allocates only sizeof(pid_t) and copies only the pid. Start by reading those three spots, then add the byte copy to takeStdin as the issue suggests. Done means the repro prints grandchild survived kill: without takeStdin = false, after takeStdin = false, and the process-group kill behaviour is unchanged for the non-take path.
Written by the indexing model from the issue text.
Description
Prerequisites
- Check that your issue is not already filed:
https://github.com/leanprover/lean4/issues - Reduce the issue to a minimal, self-contained, reproducible test case.
Avoid dependencies to Mathlib or Batteries. - Test your test case against the latest nightly release, for example on
https://live.lean-lang.org/#project=lean-nightly
(You can also use the settings there to switch to “Lean nightly”)
Description
Child.kill's docstring promises to kill the child's entire process group when it was started with
SpawnArgs.setsid. After Child.takeStdin it does not: the returned Child has a scalar area of only
sizeof(pid_t) and just the pid is copied, so setsid is lost and grandchildren survive — even though
takeStdin's docstring presents take-stdin-then-kill/wait as the intended flow.
Context
Supervising a spawned process tree: setsid := true, then takeStdin, then kill, expecting the whole
group to die.
Steps to Reproduce
Repro.lean:
def round (take : Bool) : IO Bool := do
let child ← IO.Process.spawn
{ cmd := "sh", args := #["-c", "sleep 30 & echo $!; wait"],
stdin := .piped, stdout := .piped, setsid := true }
let grandchild := (← child.stdout.getLine).trimAscii.toString
IO.sleep 100
if take then
let (_, child2) ← child.takeStdin
child2.kill
let _ ← child2.wait
else
child.kill
let _ ← child.wait
IO.sleep 300
let alive := (← IO.Process.output { cmd := "kill", args := #["-0", grandchild] }).exitCode == 0
let _ ← IO.Process.output { cmd := "kill", args := #["-KILL", grandchild] }
return alive
def main : IO Unit := do
let ctl ← round false
let take ← round true
IO.println s!"grandchild survived kill: without takeStdin = {ctl}, after takeStdin = {take}"
lean --run Repro.lean(or compile:lean Repro.lean -c Repro.c && leanc -O1 -o Repro Repro.c && ./Repro).
Expected behavior: grandchild survived kill: without takeStdin = false, after takeStdin = false —
the sleep is in the child's process group in both rounds.
Actual behavior:
grandchild survived kill: without takeStdin = false, after takeStdin = true
Versions
Lean (version 4.36.0-nightly-2026-10-13, x86_64-unknown-linux-gnu, commit b7725c34da98e6e2f6323d7f382448ddf31477fb, Release)
Linux 7.0.0-38-generic (x86_64)
Additional Information
spawn writes the setsid byte at scalar offset 3 * sizeof(object *) + sizeof(pid_t) (process.cpp:543,546); kill reads it there (process.cpp:395); takeStdin allocates only sizeof(pid_t) and copies only the pid (process.cpp:550-553) — so the kill path reads zeroed padding.
Fix — copy the byte too:
object_ref child2 = mk_cnstr(0, object_ref(box(0)), cnstr_get_ref(child, 1), cnstr_get_ref(child, 2),
sizeof(pid_t) + sizeof(uint8_t));
cnstr_set_uint32(child2.raw(), 3 * sizeof(object *), cnstr_get_uint32(child.raw(), 3 * sizeof(object *)));
cnstr_set_uint8(child2.raw(), 3 * sizeof(object *) + sizeof(pid_t), cnstr_get_uint8(child.raw(), 3 * sizeof(object *) + sizeof(pid_t)));
Impact
Add 👍 to issues you consider important. If others are impacted by this issue, please ask them to add 👍 to it.
- Dominant language
- Lean
- Stars
- 9.5k
- Forks
- 1k
- Avg merge
- 23h 14m
- Merged PRs (30d)
- 273
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from leanprover/lean4
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
leanprover/lean4#15692 · 3 reactions ·
Maintainers usually reply within 1 day
-
`IO.FS.realPath` reports every failure as `noFileOrDirectory` instead of classifying by `errno`Openbug
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
leanprover/lean4#15683 ·
Maintainers usually reply within 1 day
-
`Child.pid` leaks the `Child` and its pipe file descriptors (owned argument treated as borrowed)Openbug
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
leanprover/lean4#15681 ·
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
leanprover/lean4#15677 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
leanprover/lean4#15626 ·
Maintainers usually reply within 1 day
All issues in leanprover/lean4
Similar issues
-
System calls: getcwdOpengood first issue kernel
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
JackFurton/who-would-build-a-kernel-in-java#208 ·
Maintainers usually reply within 1 day
-
libc
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
llvm/llvm-project#232380 · 1 comment ·
Maintainers usually reply within 1 day
-
oss/0 pending team/windows-products
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
DataDog/datadog-agent#58197 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
omacom/omarchy-mac#728 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
bytecodealliance/rustix#1706 · 1 comment ·
Maintainers usually reply within 11 days