Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gecko-patches/anti-fingerprint/anti-fp-audio.patch
Original file line number Diff line number Diff line change
Expand Up @@ -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;
+ }();
Expand Down
8 changes: 8 additions & 0 deletions scripts/apply-sourceos-overlays.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
122 changes: 122 additions & 0 deletions scripts/gcp-build-linux.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#!/usr/bin/env bash
# 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/machine/image/bucket
# scripts/gcp-build-linux.sh # build human-secure + tor-mode
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}"
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=""

while [ "$#" -gt 0 ]; do
case "$1" in
--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 ;;
--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

PREFIX="gs://$BUCKET/bearbrowser-builds/$INSTANCE"
created=""
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 (remember to delete it)."
fi
exit "$rc"
}
trap cleanup EXIT INT TERM

# ── Preflight (all free) ────────────────────────────────────────────────────
echo ">> Preflight..."
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'."; 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."; 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 + 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; }
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 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 \
--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"

# ── 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 gcloud storage cat "$PREFIX/DONE" >/tmp/bb-done 2>/dev/null; then
build_rc="$(tr -dc 0-9 </tmp/bb-done)"; build_rc="${build_rc:-0}"
echo " build finished (rc=$build_rc)."; break
fi
if [ "$(date +%s)" -gt "$deadline" ]; then echo "!! build exceeded ${MAX_HOURS}h cap — tearing down."; break; fi
# progress from serial console (no SSH needed)
line="$(gcloud compute instances get-serial-port-output "$INSTANCE" --zone="$ZONE" 2>/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

# ── Collect artifacts from GCS ──────────────────────────────────────────────
mkdir -p "$art_out"
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/^/ /'
exit "$build_rc" # teardown in trap
95 changes: 95 additions & 0 deletions scripts/gcp-remote-build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/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

# 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 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

# ── 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"
43 changes: 43 additions & 0 deletions scripts/gcp-vm-startup.sh
Original file line number Diff line number Diff line change
@@ -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/<ts>
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 ==="
Loading