← Back to Blog

February 6, 2026 · Engineering

JID Synchronization: When Parent Overwrites Child

When implementing the jail syscalls (jail_set, jail_get, jail_attach, jail_remove), we hit a state management bug that was subtle enough to pass initial testing and only surface under real-world usage patterns.

The Architecture

BSDulator uses a shared state file (/tmp/bsdulator_jails.dat) to persist jail information across processes. When jail_set creates a jail, it writes the jail's metadata (JID, name, path, IP addresses, namespace file descriptors, etc.) to this file. When jail_get or jls queries jail information, it reads from the same file.

The jail structure (bsd_jail_t) contains all the fields needed to reconstruct a jail's state: JID, active flag, name, path, hostname, IP addresses, VNET status, namespace FDs, and attached process count.

The Bug

The problem appeared when running jexec (jail_attach) followed by jls (jail_get). The jexec command creates a child process that attaches to the jail and updates the state file (incrementing the attached process count). But the parent BSDulator process — which had loaded the state file at startup — still had the old version in memory.

When the parent process subsequently handled another syscall that triggered a state write, it would overwrite the file with its stale copy, erasing the child's updates. The attached process count would revert to zero, and the jail would appear to have no processes attached.

The Root Cause

Structure alignment made it worse. We initially used a simplified jail structure for quick prototyping that had slightly different field ordering than the actual bsd_jail_t. When the parent wrote its stale data with the simplified structure and the child read it expecting the full structure, fields would be misinterpreted. A JID field might read as an IP address count, or a namespace FD might read as a hostname offset.

The Fix

Two changes:

  1. Structure alignment: Unified all code paths to use the same bsd_jail_t structure with identical field ordering and sizes. No more simplified structures.
  2. State reload: After any child process operation that might modify state, the parent process reloads the state file before performing its next write. This ensures the parent always has the most current version before overwriting.

The broader lesson: in multi-process systems where processes share state through files, every write must be preceded by a read, and every structure must be identical across all readers and writers.

← The TLS Bug That Took Three Days to FindBuilding Docker Compose for FreeBSD Jails →