From 1dc239326a4812831ad8109c4b62b74ea6758a90 Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Sat, 20 Jun 2026 10:36:42 -0400 Subject: [PATCH 1/5] build: hardened GCP Linux build lane (full Gecko + engine patches) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a two-part lane to compile BearBrowser from source on a GCP VM — the machine the 8GB Mac can't be (Gecko link needs 16GB+ RAM). Produces Linux binaries + the real fingerprint scorecard (the first actual compile-verification of the canvas/audio farble + OS-spoof patches). gcp-remote-build.sh : runs on the VM. apt deps + npm ci, then per profile: apply-sourceos-overlays -> make bootstrap (Mozilla toolchain) -> make build -> measure-fingerprint --bin. tor-mode asserts the Windows identity actually compiled in (the OS-spoof verification). Packages each dist; never aborts the other profile on a single failure. gcp-build-linux.sh : orchestrator. Hardened for first-time success: - teardown trap (a hung/failed build can never keep billing) - free --dry-run (validates auth + machine type + image, no VM) - auth preflight with exact 'gcloud auth login' instructions - auto-detects external-IP SSH vs --tunnel-through-iap (org-policy safe) - build runs DETACHED + polled via a status file, so a dropped SSH never kills the build or triggers an early teardown; 5h safety cap - 150GB SSD (two full obj trees fit), live progress, artifact pull Tested offline: both pass bash -n; repo tarball is 11M with all key paths and zero leaked build/node_modules/.git; --dry-run fails gracefully on expired auth. Actual run needs 'gcloud auth login' first (token expired). --- scripts/gcp-build-linux.sh | 142 ++++++++++++++++++++++++++++++++++++ scripts/gcp-remote-build.sh | 92 +++++++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100755 scripts/gcp-build-linux.sh create mode 100755 scripts/gcp-remote-build.sh diff --git a/scripts/gcp-build-linux.sh b/scripts/gcp-build-linux.sh new file mode 100755 index 0000000..1f4c723 --- /dev/null +++ b/scripts/gcp-build-linux.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +# Provisions a GCP VM, builds BearBrowser from source (full Gecko + engine patches) +# for the given profiles, pulls back the binaries + fingerprint scorecards, and +# ALWAYS tears the VM down. The VM is Linux, so this produces Linux binaries (proves +# the patches + gives the scorecard); a runnable macOS app needs Apple hardware. +# +# scripts/gcp-build-linux.sh --dry-run # free: validate auth + resources +# scripts/gcp-build-linux.sh # build human-secure + tor-mode +# scripts/gcp-build-linux.sh --profiles human-secure --keep +# +# The build runs DETACHED on the VM and this script polls a status file, so a +# dropped SSH never kills the build or triggers an early teardown. Run the whole +# thing in the background (it takes ~2h for both profiles). +set -uo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="${BEARBROWSER_HOME:-$(cd "$script_dir/.." && pwd)}" + +INSTANCE="${BB_GCP_INSTANCE:-bearbrowser-build-$(date +%Y%m%d-%H%M%S)}" +ZONE="${BB_GCP_ZONE:-us-central1-a}" +MACHINE="${BB_GCP_MACHINE:-c2d-standard-32}" +IMAGE_FAMILY="${BB_GCP_IMAGE_FAMILY:-ubuntu-2404-lts-amd64}" +IMAGE_PROJECT="${BB_GCP_IMAGE_PROJECT:-ubuntu-os-cloud}" +DISK_GB="${BB_GCP_DISK_GB:-150}" # 2 full obj trees + toolchain fit in 150 +MAX_HOURS="${BB_GCP_MAX_HOURS:-5}" # safety cap; teardown fires regardless +PROFILES="${BB_PROFILES:-human-secure tor-mode}" +art_out="$repo_root/build/gcp-artifacts" +keep=""; dry_run="" + +while [ "$#" -gt 0 ]; do + case "$1" in + --profiles) PROFILES="${2:?}"; shift 2 ;; + --machine) MACHINE="${2:?}"; shift 2 ;; + --zone) ZONE="${2:?}"; shift 2 ;; + --instance) INSTANCE="${2:?}"; shift 2 ;; + --disk-gb) DISK_GB="${2:?}"; shift 2 ;; + --keep) keep="1"; shift ;; + --dry-run) dry_run="1"; shift ;; + -h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) echo "ERROR: unknown arg: $1" >&2; exit 1 ;; + esac +done + +created=""; SSHF="" # SSHF gets "--tunnel-through-iap" if external SSH is blocked +cleanup() { + local rc=$? + if [ -n "$created" ] && [ -z "$keep" ]; then + echo ">> Tearing down $INSTANCE ..." + gcloud compute instances delete "$INSTANCE" --zone="$ZONE" --quiet 2>/dev/null \ + && echo ">> $INSTANCE deleted." \ + || { echo "!! WARNING: delete failed — $INSTANCE MAY STILL BE BILLING."; \ + echo "!! Run: gcloud compute instances delete $INSTANCE --zone=$ZONE --quiet"; } + elif [ -n "$created" ]; then + echo ">> --keep set: $INSTANCE left running in $ZONE (remember to delete it)." + fi + exit "$rc" +} +trap cleanup EXIT INT TERM + +vssh() { gcloud compute ssh "$INSTANCE" --zone="$ZONE" $SSHF --command="$1" 2>/dev/null; } +vscp() { gcloud compute scp $SSHF --zone="$ZONE" "$@" 2>/dev/null; } + +# ── Preflight (all free — no VM yet) ──────────────────────────────────────── +echo ">> Preflight..." +if ! gcloud auth print-access-token >/dev/null 2>&1; then + echo "ERROR: gcloud is not authenticated (token expired)." + echo " Run: gcloud auth login (then: gcloud config set project )" + exit 1 +fi +echo " account: $(gcloud config get-value account 2>/dev/null) project: $(gcloud config get-value project 2>/dev/null)" +gcloud compute machine-types describe "$MACHINE" --zone="$ZONE" --format='value(name)' >/dev/null 2>&1 \ + || { echo "ERROR: machine type '$MACHINE' not available in zone '$ZONE'."; exit 1; } +img="$(gcloud compute images describe-from-family "$IMAGE_FAMILY" --project="$IMAGE_PROJECT" --format='value(name)' 2>/dev/null)" +[ -n "$img" ] || { echo "ERROR: image family '$IMAGE_FAMILY' not found in '$IMAGE_PROJECT'."; exit 1; } +echo " machine: $MACHINE image: $img disk: ${DISK_GB}GB profiles: [$PROFILES]" +if [ -n "$dry_run" ]; then + echo ">> DRY RUN OK — auth + machine type + image valid. No VM created, no cost." + exit 0 +fi + +# ── Package the repo (exclude regen'able / huge dirs) ─────────────────────── +echo ">> Packaging repo..." +tarball="/tmp/bb-repo-$$.tgz" +tar --exclude='./build' --exclude='./node_modules' --exclude='./.git' \ + --exclude='*.tar.gz' --exclude='*.tar.xz' -czf "$tarball" -C "$repo_root" . \ + || { echo "ERROR: repo tar failed"; exit 1; } +echo " $(du -h "$tarball" | cut -f1)" + +# ── Provision ─────────────────────────────────────────────────────────────── +echo ">> Creating VM $INSTANCE ($MACHINE, $ZONE, ${DISK_GB}GB)..." +gcloud compute instances create "$INSTANCE" \ + --zone="$ZONE" --machine-type="$MACHINE" \ + --image-family="$IMAGE_FAMILY" --image-project="$IMAGE_PROJECT" \ + --boot-disk-size="${DISK_GB}GB" --boot-disk-type=pd-ssd --quiet \ + || { echo "ERROR: instance create failed"; exit 1; } +created="1" + +# ── Wait for SSH; auto-detect external vs IAP ─────────────────────────────── +echo ">> Waiting for SSH (auto-detecting external IP vs IAP)..." +ready="" +for i in $(seq 1 36); do + for mode in "" "--tunnel-through-iap"; do + if gcloud compute ssh "$INSTANCE" --zone="$ZONE" $mode --command='echo ready' 2>/dev/null | grep -q ready; then + SSHF="$mode"; ready="1" + echo " SSH up via ${mode:-external-IP}." + break 2 + fi + done + sleep 10 +done +[ -n "$ready" ] || { echo "ERROR: SSH never came up (external or IAP)."; exit 1; } + +# ── Upload + unpack ───────────────────────────────────────────────────────── +echo ">> Uploading repo..." +vscp "$tarball" "$INSTANCE":~/bb-repo.tgz || { echo "ERROR: scp failed"; exit 1; } +vssh 'rm -rf ~/BearBrowser ~/artifacts && mkdir -p ~/BearBrowser ~/artifacts && tar xzf ~/bb-repo.tgz -C ~/BearBrowser && echo unpacked' \ + | grep -q unpacked || { echo "ERROR: unpack failed"; exit 1; } + +# ── Launch build DETACHED (survives SSH drops) ────────────────────────────── +echo ">> Launching detached build [$PROFILES]..." +vssh "cd ~/BearBrowser && nohup sh -c 'bash scripts/gcp-remote-build.sh \"$PROFILES\"; echo \$? > ~/artifacts/DONE' > ~/artifacts/build.log 2>&1 < /dev/null & echo launched" \ + | grep -q launched || { echo "ERROR: failed to launch build"; exit 1; } + +# ── Poll until done (or safety cap) ───────────────────────────────────────── +echo ">> Building (polling every 60s; safety cap ${MAX_HOURS}h)..." +deadline=$(( $(date +%s) + MAX_HOURS*3600 )) +while :; do + if vssh 'test -f ~/artifacts/DONE && echo done' | grep -q done; then echo " build finished."; break; fi + if [ "$(date +%s)" -gt "$deadline" ]; then echo "!! ERROR: build exceeded ${MAX_HOURS}h cap — tearing down."; break; fi + echo " [$(date -u +%H:%M:%S)] $(vssh 'tail -1 ~/artifacts/build.log 2>/dev/null')" + sleep 60 +done +build_rc="$(vssh 'cat ~/artifacts/DONE 2>/dev/null' | tr -dc 0-9)"; build_rc="${build_rc:-1}" + +# ── Pull artifacts ────────────────────────────────────────────────────────── +mkdir -p "$art_out" +echo ">> Downloading artifacts to $art_out ..." +vscp --recurse "$INSTANCE":'~/artifacts/*' "$art_out/" || echo " (no artifacts — check build.log)" +echo ">> Build finished (remote rc=$build_rc). Artifacts:" +ls -lh "$art_out" 2>/dev/null | sed 's/^/ /' +rm -f "$tarball" +exit "$build_rc" # teardown runs in trap diff --git a/scripts/gcp-remote-build.sh b/scripts/gcp-remote-build.sh new file mode 100755 index 0000000..1ddeee9 --- /dev/null +++ b/scripts/gcp-remote-build.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# Runs ON the GCP build VM (Ubuntu). Builds one or more BearBrowser profiles from +# source (full Gecko compile with our engine patches), measures the real binary, +# and stages artifacts in ~/artifacts. Driven by scripts/gcp-build-linux.sh. +# +# Not set -e: one profile failing must not abort the others. Each profile is +# isolated and its failures are logged + collected. +set -uo pipefail + +profiles="${1:-human-secure tor-mode}" +repo="${BB_REPO:-$HOME/BearBrowser}" +art="$HOME/artifacts" +mkdir -p "$art" +log() { echo "[$(date -u +%H:%M:%S)] $*"; } +fail=0 + +log "BearBrowser GCP build — profiles: [$profiles]" +log "cores=$(nproc) mem=$(free -g | awk '/Mem:/{print $2}')GB disk_free=$(df -h "$HOME" | awk 'NR==2{print $4}')" + +# ── Base OS dependencies (Mozilla's own toolchain comes from `mach bootstrap`) ── +log "Installing base apt dependencies..." +sudo apt-get update -qq +sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ + git python3 python3-dev python3-pip python3-venv curl wget mercurial zstd \ + build-essential pkg-config libssl-dev libxml2-dev nodejs npm xz-utils \ + >/dev/null 2>&1 || log "WARN: some apt deps failed (continuing — mach bootstrap covers most)" + +# Node deps for the fingerprint scorecard (geckodriver + selenium are in package.json). +cd "$repo" || { log "FATAL: repo not found at $repo"; exit 1; } +log "Installing node measurement deps..." +npm ci >/dev/null 2>&1 || npm install >/dev/null 2>&1 || log "WARN: npm deps failed (scorecard may be skipped)" + +for p in $profiles; do + log "=================== PROFILE: $p ===================" + pfail=0 + + log "[$p] overlay prep (apply-sourceos-overlays --profile $p)..." + if ! bash scripts/apply-sourceos-overlays.sh --profile "$p" --ref latest > "$art/$p-overlay.log" 2>&1; then + log "[$p] FAIL: overlay prep — see $p-overlay.log"; fail=1; continue + fi + ws="$(find build/workspaces -maxdepth 2 -type d -name source 2>/dev/null | sort | tail -1)" + if [ -z "$ws" ] || [ ! -f "$ws/Makefile" ]; then + log "[$p] FAIL: no workspace/Makefile produced"; fail=1; continue + fi + log "[$p] workspace: $ws (firefox version: $(cat "$ws/version" 2>/dev/null))" + + ( + cd "$ws" || exit 1 + echo "=== make bootstrap (Mozilla toolchain + fetch/extract/patch source) ===" + MOZBUILD_STATE_PATH="$HOME/.mozbuild" make bootstrap || exit 11 + echo "=== make build (compile — the long step) ===" + make build || exit 12 + ) > "$art/$p-build.log" 2>&1 + rc=$? + if [ "$rc" -ne 0 ]; then + log "[$p] FAIL: build rc=$rc — see $p-build.log (tail below)"; tail -25 "$art/$p-build.log"; fail=1; continue + fi + + bin="$(find "$ws" -type f -name firefox -path '*dist/bin*' 2>/dev/null | head -1)" + if [ -z "$bin" ]; then + log "[$p] FAIL: build succeeded but no dist/bin/firefox found"; fail=1; continue + fi + log "[$p] BUILT: $bin" + "$bin" --version > "$art/$p-version.txt" 2>&1 || true + + # ── Scorecard: drive the REAL binary (authoritative) ── + log "[$p] measuring fingerprint scorecard..." + PATH="$repo/node_modules/.bin:$PATH" \ + node scripts/measure-fingerprint.mjs --profile "$p" --bin "$bin" --json \ + > "$art/$p-scorecard.json" 2> "$art/$p-measure.log" || log "[$p] WARN: measurement failed — see $p-measure.log" + + # tor-mode: assert the OS-spoof actually compiled in (Windows identity on this Linux host) + if [ "$p" = "tor-mode" ] && [ -s "$art/$p-scorecard.json" ]; then + if grep -q "Windows NT 10.0" "$art/$p-scorecard.json" && grep -q "Win32" "$art/$p-scorecard.json"; then + log "[$p] ✓ OS-spoof VERIFIED: binary reports Windows identity" + else + log "[$p] ✗ OS-spoof NOT present: binary did not report Windows — the mozconfig flag did not take effect"; fail=1 + fi + fi + + # ── Package the runnable dist for download ── + distbin="$(dirname "$bin")" # .../obj-*/dist/bin + log "[$p] packaging dist..." + tar -C "$(dirname "$distbin")" -czf "$art/bearbrowser-$p-linux-x86_64.tar.gz" bin 2>/dev/null \ + || log "[$p] WARN: packaging failed" + log "=================== DONE: $p (size: $(du -h "$art/bearbrowser-$p-linux-x86_64.tar.gz" 2>/dev/null | cut -f1)) ===================" +done + +log "Artifacts staged in $art:" +ls -lh "$art" 2>/dev/null | sed 's/^/ /' +[ "$fail" -eq 0 ] && log "ALL PROFILES OK" || log "ONE OR MORE PROFILES FAILED (rc collected; see per-profile logs)" +exit "$fail" From 1a65b695003e340e9ae72db03e450b451b013750 Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:01:05 -0400 Subject: [PATCH 2/5] build(gcp): no-SSH GCS build lane (org blocks SSH/OS-Login) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit socioprophet-platform is locked down: outbound :22 blocked, OS Login org- enforced, Workspace disables SSH-key registration, default compute SA missing. SSH to build VMs is impossible. Rework the lane to use GCS + a startup script instead — the VM builds autonomously and pushes results to a bucket; we poll. - gcp-build-linux.sh: upload repo -> GCS, create VM with startup-script metadata + synapseiq-build SA (storage.admin), poll gs://.../DONE, download artifacts, teardown trap + --max-run-duration=DELETE cost cap. Free --dry-run validates auth/machine/image/bucket. - gcp-vm-startup.sh: on the VM — install gcloud, pull repo, build as non-root 'builder', push artifacts + DONE marker to GCS. - gcp-remote-build.sh: per-profile overlay->bootstrap->build->measure (unchanged). Verified: dry-run clean; first real launch compiles (serial console shows overlay prep -> bootstrap in progress). --- scripts/gcp-build-linux.sh | 132 ++++++++++++++++--------------------- scripts/gcp-vm-startup.sh | 43 ++++++++++++ 2 files changed, 99 insertions(+), 76 deletions(-) create mode 100644 scripts/gcp-vm-startup.sh diff --git a/scripts/gcp-build-linux.sh b/scripts/gcp-build-linux.sh index 1f4c723..dc09143 100755 --- a/scripts/gcp-build-linux.sh +++ b/scripts/gcp-build-linux.sh @@ -1,16 +1,13 @@ #!/usr/bin/env bash -# Provisions a GCP VM, builds BearBrowser from source (full Gecko + engine patches) -# for the given profiles, pulls back the binaries + fingerprint scorecards, and -# ALWAYS tears the VM down. The VM is Linux, so this produces Linux binaries (proves -# the patches + gives the scorecard); a runnable macOS app needs Apple hardware. +# Builds BearBrowser from source on a GCP VM and pulls back binaries + fingerprint +# scorecards. NO SSH is used — this org blocks port 22 and OS Login key +# registration, so the VM is driven entirely through GCS + a startup script: +# upload repo -> VM builds autonomously -> VM pushes artifacts + DONE to GCS -> +# we poll GCS, download, and tear the VM down. (Linux binaries: proves the engine +# patches + gives the scorecard; a runnable macOS app needs Apple hardware.) # -# scripts/gcp-build-linux.sh --dry-run # free: validate auth + resources -# scripts/gcp-build-linux.sh # build human-secure + tor-mode -# scripts/gcp-build-linux.sh --profiles human-secure --keep -# -# The build runs DETACHED on the VM and this script polls a status file, so a -# dropped SSH never kills the build or triggers an early teardown. Run the whole -# thing in the background (it takes ~2h for both profiles). +# scripts/gcp-build-linux.sh --dry-run # free: validate auth/machine/image/bucket +# scripts/gcp-build-linux.sh # build human-secure + tor-mode set -uo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -21,8 +18,10 @@ ZONE="${BB_GCP_ZONE:-us-central1-a}" MACHINE="${BB_GCP_MACHINE:-c2d-standard-32}" IMAGE_FAMILY="${BB_GCP_IMAGE_FAMILY:-ubuntu-2404-lts-amd64}" IMAGE_PROJECT="${BB_GCP_IMAGE_PROJECT:-ubuntu-os-cloud}" -DISK_GB="${BB_GCP_DISK_GB:-150}" # 2 full obj trees + toolchain fit in 150 -MAX_HOURS="${BB_GCP_MAX_HOURS:-5}" # safety cap; teardown fires regardless +DISK_GB="${BB_GCP_DISK_GB:-150}" +MAX_HOURS="${BB_GCP_MAX_HOURS:-5}" +SERVICE_ACCOUNT="${BB_GCP_SA:-synapseiq-build@socioprophet-platform.iam.gserviceaccount.com}" +BUCKET="${BB_GCP_BUCKET:-sourceos-artifacts-socioprophet}" PROFILES="${BB_PROFILES:-human-secure tor-mode}" art_out="$repo_root/build/gcp-artifacts" keep=""; dry_run="" @@ -32,8 +31,8 @@ while [ "$#" -gt 0 ]; do --profiles) PROFILES="${2:?}"; shift 2 ;; --machine) MACHINE="${2:?}"; shift 2 ;; --zone) ZONE="${2:?}"; shift 2 ;; + --bucket) BUCKET="${2:?}"; shift 2 ;; --instance) INSTANCE="${2:?}"; shift 2 ;; - --disk-gb) DISK_GB="${2:?}"; shift 2 ;; --keep) keep="1"; shift ;; --dry-run) dry_run="1"; shift ;; -h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; @@ -41,7 +40,8 @@ while [ "$#" -gt 0 ]; do esac done -created=""; SSHF="" # SSHF gets "--tunnel-through-iap" if external SSH is blocked +PREFIX="gs://$BUCKET/bearbrowser-builds/$INSTANCE" +created="" cleanup() { local rc=$? if [ -n "$created" ] && [ -z "$keep" ]; then @@ -51,92 +51,72 @@ cleanup() { || { echo "!! WARNING: delete failed — $INSTANCE MAY STILL BE BILLING."; \ echo "!! Run: gcloud compute instances delete $INSTANCE --zone=$ZONE --quiet"; } elif [ -n "$created" ]; then - echo ">> --keep set: $INSTANCE left running in $ZONE (remember to delete it)." + echo ">> --keep set: $INSTANCE left running (remember to delete it)." fi exit "$rc" } trap cleanup EXIT INT TERM -vssh() { gcloud compute ssh "$INSTANCE" --zone="$ZONE" $SSHF --command="$1" 2>/dev/null; } -vscp() { gcloud compute scp $SSHF --zone="$ZONE" "$@" 2>/dev/null; } - -# ── Preflight (all free — no VM yet) ──────────────────────────────────────── +# ── Preflight (all free) ──────────────────────────────────────────────────── echo ">> Preflight..." -if ! gcloud auth print-access-token >/dev/null 2>&1; then - echo "ERROR: gcloud is not authenticated (token expired)." - echo " Run: gcloud auth login (then: gcloud config set project )" - exit 1 -fi +gcloud auth print-access-token >/dev/null 2>&1 \ + || { echo "ERROR: gcloud not authenticated. Run: gcloud auth login"; exit 1; } echo " account: $(gcloud config get-value account 2>/dev/null) project: $(gcloud config get-value project 2>/dev/null)" gcloud compute machine-types describe "$MACHINE" --zone="$ZONE" --format='value(name)' >/dev/null 2>&1 \ - || { echo "ERROR: machine type '$MACHINE' not available in zone '$ZONE'."; exit 1; } + || { echo "ERROR: machine type '$MACHINE' not available in '$ZONE'."; exit 1; } img="$(gcloud compute images describe-from-family "$IMAGE_FAMILY" --project="$IMAGE_PROJECT" --format='value(name)' 2>/dev/null)" -[ -n "$img" ] || { echo "ERROR: image family '$IMAGE_FAMILY' not found in '$IMAGE_PROJECT'."; exit 1; } -echo " machine: $MACHINE image: $img disk: ${DISK_GB}GB profiles: [$PROFILES]" -if [ -n "$dry_run" ]; then - echo ">> DRY RUN OK — auth + machine type + image valid. No VM created, no cost." - exit 0 -fi +[ -n "$img" ] || { echo "ERROR: image family '$IMAGE_FAMILY' not found."; exit 1; } +gcloud storage ls "gs://$BUCKET" >/dev/null 2>&1 \ + || { echo "ERROR: cannot access bucket gs://$BUCKET"; exit 1; } +echo " machine: $MACHINE image: $img bucket: gs://$BUCKET profiles: [$PROFILES]" +[ -f "$script_dir/gcp-vm-startup.sh" ] || { echo "ERROR: missing gcp-vm-startup.sh"; exit 1; } +if [ -n "$dry_run" ]; then echo ">> DRY RUN OK — auth + machine + image + bucket all valid. No VM, no cost."; exit 0; fi -# ── Package the repo (exclude regen'able / huge dirs) ─────────────────────── -echo ">> Packaging repo..." +# ── Package + upload repo to GCS ──────────────────────────────────────────── +echo ">> Packaging + uploading repo to $PREFIX/bb-repo.tgz ..." tarball="/tmp/bb-repo-$$.tgz" tar --exclude='./build' --exclude='./node_modules' --exclude='./.git' \ --exclude='*.tar.gz' --exclude='*.tar.xz' -czf "$tarball" -C "$repo_root" . \ || { echo "ERROR: repo tar failed"; exit 1; } -echo " $(du -h "$tarball" | cut -f1)" +gcloud storage cp "$tarball" "$PREFIX/bb-repo.tgz" >/dev/null 2>&1 \ + || { echo "ERROR: repo upload failed"; exit 1; } +rm -f "$tarball" +echo " uploaded." -# ── Provision ─────────────────────────────────────────────────────────────── +# ── Provision with startup script (no SSH) ────────────────────────────────── echo ">> Creating VM $INSTANCE ($MACHINE, $ZONE, ${DISK_GB}GB)..." gcloud compute instances create "$INSTANCE" \ --zone="$ZONE" --machine-type="$MACHINE" \ --image-family="$IMAGE_FAMILY" --image-project="$IMAGE_PROJECT" \ - --boot-disk-size="${DISK_GB}GB" --boot-disk-type=pd-ssd --quiet \ - || { echo "ERROR: instance create failed"; exit 1; } + --boot-disk-size="${DISK_GB}GB" --boot-disk-type=pd-ssd \ + --max-run-duration="$(( MAX_HOURS*3600 ))s" --instance-termination-action=DELETE \ + --service-account="$SERVICE_ACCOUNT" --scopes=cloud-platform \ + --metadata="bb-prefix=$PREFIX,bb-profiles=$PROFILES" \ + --metadata-from-file="startup-script=$script_dir/gcp-vm-startup.sh" \ + --quiet || { echo "ERROR: instance create failed"; exit 1; } created="1" -# ── Wait for SSH; auto-detect external vs IAP ─────────────────────────────── -echo ">> Waiting for SSH (auto-detecting external IP vs IAP)..." -ready="" -for i in $(seq 1 36); do - for mode in "" "--tunnel-through-iap"; do - if gcloud compute ssh "$INSTANCE" --zone="$ZONE" $mode --command='echo ready' 2>/dev/null | grep -q ready; then - SSHF="$mode"; ready="1" - echo " SSH up via ${mode:-external-IP}." - break 2 - fi - done - sleep 10 -done -[ -n "$ready" ] || { echo "ERROR: SSH never came up (external or IAP)."; exit 1; } - -# ── Upload + unpack ───────────────────────────────────────────────────────── -echo ">> Uploading repo..." -vscp "$tarball" "$INSTANCE":~/bb-repo.tgz || { echo "ERROR: scp failed"; exit 1; } -vssh 'rm -rf ~/BearBrowser ~/artifacts && mkdir -p ~/BearBrowser ~/artifacts && tar xzf ~/bb-repo.tgz -C ~/BearBrowser && echo unpacked' \ - | grep -q unpacked || { echo "ERROR: unpack failed"; exit 1; } - -# ── Launch build DETACHED (survives SSH drops) ────────────────────────────── -echo ">> Launching detached build [$PROFILES]..." -vssh "cd ~/BearBrowser && nohup sh -c 'bash scripts/gcp-remote-build.sh \"$PROFILES\"; echo \$? > ~/artifacts/DONE' > ~/artifacts/build.log 2>&1 < /dev/null & echo launched" \ - | grep -q launched || { echo "ERROR: failed to launch build"; exit 1; } - -# ── Poll until done (or safety cap) ───────────────────────────────────────── -echo ">> Building (polling every 60s; safety cap ${MAX_HOURS}h)..." +# ── Poll GCS for the DONE marker (no SSH) ─────────────────────────────────── +echo ">> Building on VM (polling GCS every 60s; safety cap ${MAX_HOURS}h)..." deadline=$(( $(date +%s) + MAX_HOURS*3600 )) +build_rc="1" while :; do - if vssh 'test -f ~/artifacts/DONE && echo done' | grep -q done; then echo " build finished."; break; fi - if [ "$(date +%s)" -gt "$deadline" ]; then echo "!! ERROR: build exceeded ${MAX_HOURS}h cap — tearing down."; break; fi - echo " [$(date -u +%H:%M:%S)] $(vssh 'tail -1 ~/artifacts/build.log 2>/dev/null')" + if gcloud storage cat "$PREFIX/DONE" >/tmp/bb-done 2>/dev/null; then + build_rc="$(tr -dc 0-9 /dev/null | grep -aE '===|profile|make build|BUILT|FAIL|OS-spoof' | tail -1)" + echo " [$(date -u +%H:%M:%S)] ${line:-(booting / installing toolchain…)}" sleep 60 done -build_rc="$(vssh 'cat ~/artifacts/DONE 2>/dev/null' | tr -dc 0-9)"; build_rc="${build_rc:-1}" -# ── Pull artifacts ────────────────────────────────────────────────────────── +# ── Collect artifacts from GCS ────────────────────────────────────────────── mkdir -p "$art_out" -echo ">> Downloading artifacts to $art_out ..." -vscp --recurse "$INSTANCE":'~/artifacts/*' "$art_out/" || echo " (no artifacts — check build.log)" -echo ">> Build finished (remote rc=$build_rc). Artifacts:" +echo ">> Downloading artifacts from $PREFIX/ ..." +gcloud storage cp -r "$PREFIX/artifacts/*" "$art_out/" >/dev/null 2>&1 || echo " (no artifacts — see build.log)" +gcloud storage cp "$PREFIX/build.log" "$art_out/build.log" >/dev/null 2>&1 || true +echo ">> Build finished (rc=$build_rc). Artifacts in $art_out:" ls -lh "$art_out" 2>/dev/null | sed 's/^/ /' -rm -f "$tarball" -exit "$build_rc" # teardown runs in trap +exit "$build_rc" # teardown in trap diff --git a/scripts/gcp-vm-startup.sh b/scripts/gcp-vm-startup.sh new file mode 100644 index 0000000..3d7d242 --- /dev/null +++ b/scripts/gcp-vm-startup.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# VM startup script (runs as root at boot via instance metadata). No SSH is used — +# this org blocks port 22 + OS Login key registration, so the VM is fully +# autonomous: pull the repo from GCS, build (as a non-root user), push artifacts + +# a DONE marker back to GCS. The orchestrator polls GCS and tears the VM down. +set -uo pipefail +exec > >(tee /var/log/bb-build.log) 2>&1 # also visible via serial console +md() { curl -s -H 'Metadata-Flavor: Google' "http://metadata.google.internal/computeMetadata/v1/instance/attributes/$1"; } + +BUCKET_PREFIX="$(md bb-prefix)" # gs://bucket/path/ +PROFILES="$(md bb-profiles)" +echo "=== BearBrowser VM build start $(date -u) — prefix=$BUCKET_PREFIX profiles=[$PROFILES] ===" + +# gsutil/gcloud: Ubuntu GCP images don't ship the CLI — install via snap. +if ! command -v gsutil >/dev/null 2>&1; then + echo "=== installing google-cloud-cli ===" + snap install google-cloud-cli --classic || { echo "FATAL: cannot install gcloud cli"; exit 1; } +fi + +mark_done() { echo "$1" | gsutil cp - "$BUCKET_PREFIX/DONE" 2>/dev/null; } +push_log() { gsutil cp /var/log/bb-build.log "$BUCKET_PREFIX/build.log" 2>/dev/null || true; } +trap 'push_log' EXIT + +# Non-root build user (mach refuses to build as root); passwordless sudo for apt + mach bootstrap. +id builder >/dev/null 2>&1 || useradd -m -s /bin/bash builder +echo 'builder ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/builder + +echo "=== fetching repo from $BUCKET_PREFIX/bb-repo.tgz ===" +if ! gsutil cp "$BUCKET_PREFIX/bb-repo.tgz" /tmp/bb-repo.tgz; then echo "FATAL: repo fetch failed"; mark_done 90; exit 1; fi +sudo -u builder mkdir -p /home/builder/BearBrowser +tar xzf /tmp/bb-repo.tgz -C /home/builder/BearBrowser +chown -R builder:builder /home/builder/BearBrowser + +echo "=== building (as builder): profiles [$PROFILES] ===" +su - builder -c "cd ~/BearBrowser && bash scripts/gcp-remote-build.sh '$PROFILES'" +rc=$? +echo "=== build finished rc=$rc ===" + +echo "=== uploading artifacts to $BUCKET_PREFIX/artifacts/ ===" +gsutil -m cp -r /home/builder/artifacts/'*' "$BUCKET_PREFIX/artifacts/" 2>/dev/null || echo "WARN: artifact upload partial" +push_log +mark_done "$rc" +echo "=== DONE (rc=$rc) — orchestrator will collect + tear down ===" From 5a16aafb1ab74f9b449334db22ec7abef4a7415e Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Sun, 21 Jun 2026 10:46:25 -0400 Subject: [PATCH 3/5] tor-mode: drop msix.patch at build time (not just in CI dry-run) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The msix.patch filter block was written but never committed — GCP builds tar the git-tracked state so the fix was absent. Closes the tor-mode build failure on FF140 ESR where msix.patch drifts and aborts make. --- scripts/apply-sourceos-overlays.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/apply-sourceos-overlays.sh b/scripts/apply-sourceos-overlays.sh index dd44bb1..dcfc287 100755 --- a/scripts/apply-sourceos-overlays.sh +++ b/scripts/apply-sourceos-overlays.sh @@ -249,6 +249,14 @@ afp_dir="$repo_root/gecko-patches/anti-fingerprint" patches_txt="$workspace/source/assets/patches.txt" if [ "$profile" = "tor-mode" ]; then echo "feature-layer: tor-mode omits the canvas/audio anti-fp patches (disabled to match cohort; see docs/tor-mode.md)" + # Drop msix.patch (Windows Store packaging) from the tor-mode stack: it's + # irrelevant to a Linux/macOS Tor browser AND it fails to apply on the 140 ESR + # tree (hunk drift), which aborts the whole `make` build. The CI patch-apply + # gate already excludes it; do the same for the real build. + if [ -f "$patches_txt" ]; then + grep -v 'patches/msix\.patch' "$patches_txt" > "$patches_txt.tmp" && mv "$patches_txt.tmp" "$patches_txt" + echo "feature-layer: tor-mode dropped msix.patch (Windows-only; drifts on ESR)" + fi # ...but tor-mode DOES need the OS-spoof patch: Tor forces the Windows identity # on all desktop platforms, which is a cohort REQUIREMENT (not one of our extra # protections). It's gated on -DBEARBROWSER_FORCE_WIN_SPOOF, set below. From b0a2b95cc8345f74d97a0fe8784f31f2f3c2a2dd Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Sun, 21 Jun 2026 10:46:32 -0400 Subject: [PATCH 4/5] fix(anti-fp): RandomUint64OrZero -> RandomUint64().valueOr(0) in audio patch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mozilla::RandomUint64OrZero() was removed in Firefox 150 — only RandomUint64OrDie() and RandomUint64() remain. The audio farble patch adds code to nsRFPService.cpp that called the removed overload, causing the human-secure build to fail at compile. Switch to RandomUint64() with .valueOr(0) fallback (same behavior, correct API). --- gecko-patches/anti-fingerprint/anti-fp-audio.patch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gecko-patches/anti-fingerprint/anti-fp-audio.patch b/gecko-patches/anti-fingerprint/anti-fp-audio.patch index ea81561..e047c5e 100644 --- a/gecko-patches/anti-fingerprint/anti-fp-audio.patch +++ b/gecko-patches/anti-fingerprint/anti-fp-audio.patch @@ -66,7 +66,7 @@ + // fingerprint cannot be linked between sessions — RFP randomizes canvas but + // not audio, and this closes that residual). + static const double sFarbleFactor = []() { -+ uint64_t r = mozilla::RandomUint64OrZero(); ++ uint64_t r = mozilla::RandomUint64().valueOr(0); + double frac = (double)(r % 2000001) / 1000000.0 - 1.0; // [-1, 1] + return 1.0 + frac * 1e-7; + }(); From eb9d7e27b8ace5d67ce6265637d7c41c9407de18 Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Sun, 21 Jun 2026 11:36:45 -0400 Subject: [PATCH 5/5] build(gcp): find branded binary (bearbrowser/librewolf), not just firefox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The compile succeeds but the build names the binary 'bearbrowser' (branded), so the firefox-only finder reported 'no binary' and failed an otherwise-good build. Match bearbrowser/librewolf/firefox; on miss, list dist/bin to debug. Note: human-secure full Gecko compile with our canvas+audio engine patches is now PROVEN ('Your build was successful!') — the audio RandomUint64 fix compiles. --- scripts/gcp-remote-build.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/gcp-remote-build.sh b/scripts/gcp-remote-build.sh index 1ddeee9..619607f 100755 --- a/scripts/gcp-remote-build.sh +++ b/scripts/gcp-remote-build.sh @@ -56,9 +56,12 @@ for p in $profiles; do log "[$p] FAIL: build rc=$rc — see $p-build.log (tail below)"; tail -25 "$art/$p-build.log"; fail=1; continue fi - bin="$(find "$ws" -type f -name firefox -path '*dist/bin*' 2>/dev/null | head -1)" + # The branded build names the binary bearbrowser/librewolf, not firefox. + bin="$(find "$ws" -type f \( -name bearbrowser -o -name librewolf -o -name firefox \) -path '*dist/bin*' 2>/dev/null | head -1)" if [ -z "$bin" ]; then - log "[$p] FAIL: build succeeded but no dist/bin/firefox found"; fail=1; continue + log "[$p] FAIL: build succeeded but no dist/bin binary found (looked for bearbrowser/librewolf/firefox)" + find "$ws" -type f -path '*dist/bin*' 2>/dev/null | grep -viE '\.(so|js|json|txt|xpi|ini)$' | head -20 + fail=1; continue fi log "[$p] BUILT: $bin" "$bin" --version > "$art/$p-version.txt" 2>&1 || true