Skip to content
C.W.K.
Stream
Lesson 03 of 08 · published

Children Inherit the Owner

~12 min · daemon, fork, exec, signals, umask, review

Level 0Spark
0 XP0/48 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

Narrow Is Not Harmless

The first lesson of this track drew the owner by what it refuses: no pixels, no model, no plugin, no provider or SSH credential. That refusal list is real, and on 2026-09-23 a comprehensive review of the whole app showed it is only half the job. A process that starts other processes does not just own them. It hands them things. Across fork and exec, a child inherits its parent's ignored signals, its blocked-signal mask, its file-creation mask, and every descriptor that was not marked close-on-exec. An owner can hold nothing it should not hold and still bequeath a broken world to every pane it spawns.

That is what the review found. The owner ran under a launch agent that set a strict file-creation mask, so its own journals stayed private. It also told the kernel to ignore interrupt and terminate, so a stray signal could not take the PTYs down with it. Both choices were reasonable for the owner. Both were inherited by every shell it started. Ctrl-C did nothing in profile, fleet and SSH panes. Every new file a pane created came out readable by its owner only, and an app bundle built inside a pane came out as a directory nobody else could enter. Nothing in the owner was wrong in isolation. Its children were wrong because of it.

Reset at the Fork

The repair lives in the C shim beside the Swift code, in the one function that starts a pane's process. Between fork and exec, in the child, it puts every signal back to its default disposition, clears the blocked mask, sets a normal file-creation mask, and closes every descriptor from 3 upward, using only calls that are safe in that half-born state. Then it execs. The owner keeps its own protections; the child starts as a terminal child should.

The subtle half is how the owner now protects itself. It no longer ignores interrupt and terminate; it catches them with handlers that do nothing. The difference matters at exec: an ignored signal stays ignored in the new program, while a caught signal resets to its default, because the handler's code no longer exists in the new image. Same effect for the owner, opposite inheritance for the child. That one distinction is the whole bug.

Close Is a Hangup, On Purpose

The same review folded every way of ending a pane into one sweep. An interactive login zsh ignores a terminate signal, and the embedded core's own terminate sends exactly that, so an early build closed a tab and left a coding CLI and its shell running, detached. The sweep captures the pane's descendants together with their birth identities, sends each a hangup, and 0.7 seconds later stops and kills whatever survived. The birth identity is there so a reused process number is never signalled by mistake.

Notice the hangup. Track 2's phone lesson says a pocket event must never send it, because the glass is not the controlling terminal. Close is the one verb that is allowed to send it, because close is the operator saying end this job. The signal is the same; who is allowed to cause it is the product.

The Other Bequests

Two more findings had the same shape. The shell wrapper exported its history-file setting, so a bash -i typed inside a pane inherited it and truncated the user's zsh history to 500 lines; the setting is now local to the shell that needs it. And the owner capped its connections at 32 while every Mac pane holds one, so the 32nd pane silently became an app-owned shell; the cap is now 256 with a typed refusal, and the launch agent raises the open-file limit to match. In every case the owner was narrow on purpose and still leaked a default into the people it served.

An owner is responsible for what its children inherit, not only for what it holds. Reset dispositions, the signal mask, the file-creation mask and stray descriptors at the fork. Protect the owner with handlers, never with ignored signals.

Code

What crosses fork + exec, and what the spawn helper resets·text
inherited across exec          before the repair         after the repair (in the child)
--------------------           -----------------         ------------------------------
signal set to SIG_IGN          INT, TERM ignored         every signal -> SIG_DFL
signal with a handler          (n/a)                     resets to default by itself
blocked-signal mask            whatever the owner had    empty
file-creation mask (umask)     077 from the agent        022
open descriptors >= 3          whatever was open         closed

the owner itself
  before: ignores INT/TERM   -> children inherit "ignore"
  after:  catches INT/TERM with no-op handlers
          -> same protection, nothing inherited

close a pane (one sweep)
  capture descendants + birth identities
  SIGHUP each              (an interactive login zsh ignores SIGTERM)
  wait 0.7 s, stop survivors, SIGKILL
  never signal a pid whose birth identity changed

External links

Exercise

Pick a long-running process you run (a server, an agent, a scheduler) that starts children. List four things a child inherits from it across exec: signal dispositions, the signal mask, the file-creation mask, open descriptors. For each, write what the parent has today and what a child should start with. Mark any row where the parent's protection would silently become the child's defect.
Hint
The dangerous rows are the ones that are correct for the parent: an ignored SIGINT, a private umask, a log file left open. Those are exactly the ones nobody checks in the child.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.