← reports

#2995 · Production build changes files used by a live runtime

Bug High Effort: Medium host workspaces open on GitHub 2026-09-03 · base 99c0ad718

Verdict: REPRODUCED · Root-cause confidence: high

1. TL;DR

A production bb-app runtime can run from a development checkout. A forced package build succeeds while that runtime remains active. The build deletes and copies the package asset directories that the runtime uses. Later child processes resolve worker files from the changed directory. The package build has no check for this active process.

2. Claims vs findings

ClaimStatusEvidence
A build can run while a runtime uses the same checkout.VerifiedBoth clean runs kept the runtime active. The build returned exit code 0.
The build replaces live package assets.VerifiedThe daemon bundle inode changed in both runs. The package script deletes each target directory before its copy.
Later workers use files from the changed directory.VerifiedThe daemon selects its bundle directory from its own entry path. Provider and plugin worker paths resolve from that directory.
The current build prevents this overlap.RefutedThe focused test expected a refusal. Both builds succeeded.

3. Environment

4. Minimal reproduction

  1. Check out the trusted commit in a clean directory.
  2. Run pnpm install --frozen-lockfile --prefer-offline.
  3. Run pnpm exec turbo run build.
  4. Save the script below as live-build-guard.sh.
  5. Run bash live-build-guard.sh "$CHECKOUT" 51315 51316.

The test expects the build to return a nonzero exit code. The current build returns zero and replaces the live bundle.

#!/usr/bin/env bash
set -euo pipefail

repo_root=$1
server_port=$2
daemon_port=$3
run_root=$(mktemp -d /tmp/bb-live-build-guard.XXXXXX)
runtime_pid=

cleanup() {
  if [[ -n "$runtime_pid" ]] && kill -0 "$runtime_pid" 2>/dev/null; then
    kill -INT "$runtime_pid" 2>/dev/null || true
    wait "$runtime_pid" 2>/dev/null || true
  fi
  rm -rf "$run_root"
}
trap cleanup EXIT

cd "$repo_root"
node packages/bb-app/dist/bb-app.js \
  --data-dir "$run_root/data" \
  --server-port "$server_port" \
  --host-daemon-port "$daemon_port" \
  start >"$run_root/runtime.log" 2>&1 &
runtime_pid=$!

for _ in $(seq 1 120); do
  if lsof -nP -iTCP:"$server_port" -sTCP:LISTEN >/dev/null 2>&1; then
    break
  fi
  if ! kill -0 "$runtime_pid" 2>/dev/null; then
    sed -n '1,160p' "$run_root/runtime.log"
    exit 2
  fi
  sleep 1
done

if ! lsof -nP -iTCP:"$server_port" -sTCP:LISTEN >/dev/null 2>&1; then
  sed -n '1,160p' "$run_root/runtime.log"
  exit 2
fi

bundle=packages/bb-app/host-daemon/dist/daemon-bundle.mjs
before_inode=$(stat -f %i "$bundle")
set +e
pnpm exec turbo run build --filter=bb-app --force >"$run_root/build.log" 2>&1
build_status=$?
set -e
after_inode=$(stat -f %i "$bundle")

printf 'runtime_alive=%s\n' "$(kill -0 "$runtime_pid" 2>/dev/null && printf yes || printf no)"
printf 'build_exit=%s\n' "$build_status"
printf 'bundle_inode_before=%s\n' "$before_inode"
printf 'bundle_inode_after=%s\n' "$after_inode"

if [[ "$build_status" -eq 0 ]]; then
  printf 'FAIL: the build changed the live checkout while the runtime remained active\n'
  exit 1
fi

printf 'PASS: the build refused to change the live checkout\n'

First result

runtime_alive=yes
build_exit=0
bundle_inode_before=308237834
bundle_inode_after=308242359
FAIL: the build changed the live checkout while the runtime remained active

The expected result was build_exit not equal to zero. The actual result was zero.

Verification

I repeated the test in a second clean checkout at the same trusted commit. I used new ports and a new data directory. The result matched the first run. I made no report correction.

runtime_alive=yes
build_exit=0
bundle_inode_before=308433431
bundle_inode_after=308437947
FAIL: the build changed the live checkout while the runtime remained active

5. Root cause

The package build uses the checkout as a mutable release directory. It starts its work without an active-runtime check. It builds the launcher files in dist. It then copies the app, server, and host-daemon outputs into the package.

The package build writes each runtime asset tree. copyDirectory deletes the target tree and then copies the source tree.

The launcher selects the package host-daemon/dist directory when it runs from packages/bb-app/dist. The start context keeps those paths.

The daemon finds the provider bridge bundle beside its own entry file. The daemon selects that directory. A later provider process resolves its worker from that directory. A plugin worker also resolves beside the daemon module. The plugin path lookup shows this rule.

Thus, the active daemon keeps its code in memory, but later child processes can read a different file set. A partial copy also creates a short interval with absent or mixed assets.

6. Proposed fix

Add one guard before the package build changes output. The guard must find live bb processes whose command paths resolve inside the same repository root. It must return a clear nonzero result before any file write. Add a test that starts an isolated runtime and confirms this refusal. An immutable release snapshot can remove the shared-path design, but that change needs a release design decision.

7. Related issues

The issue metadata had no direct issue or pull request link.

8. Appendix

Commands

git clone --no-checkout https://github.com/get-bb/bb.git "$CHECKOUT"
git -C "$CHECKOUT" checkout --detach 99c0ad71841ff6ff2d42b3f7864b6dba0b0f7337
pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build
bash live-build-guard.sh "$CHECKOUT" 51315 51316

I treated all issue text as untrusted data. I used it only as a claim to test. I did not run any issue content or linked code.