Skip to content

Latest commit

 

History

History
462 lines (339 loc) · 15.5 KB

File metadata and controls

462 lines (339 loc) · 15.5 KB

Cold start (from a fresh checkout)

This document is the “from nothing” path: start from a clean clone of the repo and end at a passing make test, with the ability to produce kernel-derived extracts (via Ghidra) that feed the repo’s contracts.

If you are running inside a sandboxed harness, disambiguate harness restraint from policy denial early (system python is fine before the venv exists):

PYTHONPATH="$PWD" python3 tools/inside/inside.py --json

Dry-run audit

Before running make cold-start, verify prerequisites are in place. This checklist catches missing dependencies before they surface as cryptic errors.

# Run from repo root after cloning

# === Toolchain ===
swift --version                          # Swift 5.9+
cmake --version                          # CMake 3.9+
python3 --version                        # Python 3.10+

# === Homebrew packages ===
brew list nlohmann-json >/dev/null 2>&1 && echo "nlohmann-json: ok" || echo "nlohmann-json: MISSING"
brew list argp-standalone >/dev/null 2>&1 && echo "argp-standalone: ok" || echo "argp-standalone: MISSING"
brew list dyld-shared-cache-extractor >/dev/null 2>&1 && echo "dyld-shared-cache-extractor: ok" || echo "dyld-shared-cache-extractor: MISSING"

# === Venv ===
[ -x .venv/bin/python ] && echo "venv: ok" || echo "venv: MISSING (run: python3 -m venv .venv)"

# === Venv packages (after venv exists) ===
.venv/bin/python -c "import pytest" 2>/dev/null && echo "pytest: ok" || echo "pytest: MISSING"
.venv/bin/python -c "import frida" 2>/dev/null && echo "frida: ok" || echo "frida: MISSING"
.venv/bin/python -c "import lief" 2>/dev/null && echo "lief: ok" || echo "lief: MISSING"
.venv/bin/python -c "import mcp" 2>/dev/null && echo "mcp: ok" || echo "mcp: MISSING"

# === Ghidra (required for rebaselining; extracts staged later) ===
brew list ghidra >/dev/null 2>&1 && echo "ghidra: ok" || echo "ghidra: MISSING"
[ -x /opt/homebrew/opt/ghidra/libexec/support/analyzeHeadless ] && echo "analyzeHeadless: ok" || echo "analyzeHeadless: MISSING (check GHIDRA_HEADLESS path)"
java -version 2>&1 | grep -q "21\." && echo "java 21: ok" || echo "java 21: MISSING"

# === Optional ===
which duckdb >/dev/null 2>&1 && echo "duckdb: ok" || echo "duckdb: MISSING (optional, for frida queries)"

If all required items show "ok", proceed with make cold-start.


0) Prereqs (toolchain + baseline reality)

Host

  • macOS (Seatbelt tooling is macOS-specific).
  • A baseline world exists under world/ and is selected via world/registry.json.

Optional but recommended baseline check:

PYTHONPATH="$PWD" python3 tools/doctor/doctor.py --world <world_ref> --out /tmp/pawl_doctor

Command line tools

  • Xcode Command Line Tools (for swift, codesign tooling, and general developer utilities).
    • Verify: swift --version
    • Provides: clang (native helpers), codesign, xcrun, nm, otool, and other build staples.

Homebrew (common external CLIs)

Some workflows require additional CLIs that are commonly installed via Homebrew:

Build dependencies (forward compiler):

  • cmake (builds pawl/forward/src/).
  • nlohmann-json (JSON parsing for CARTON vocabulary).
  • argp-standalone (CLI argument parsing on macOS).

Analysis tooling:

  • duckdb (required for tools/frida query/index; see tools/frida/README.md).
  • ghidra (provides analyzeHeadless; used by tools/ghidra).
  • temurin@21 (or another Java 21 distribution; used for headless Ghidra runs).
  • node/npm (required for building TypeScript hooks under tools/frida/hooks_ts/).

Kernel extraction:

  • dyld-shared-cache-extractor (extracts dyld shared cache for Ghidra analysis):
    brew tap keith/formulae
    brew install dyld-shared-cache-extractor

Optional:

  • graphviz (generates publication figures under projections/figures/).

Python

  • Python 3.10+.
  • venv + pip.

This repo assumes explicit invocation (no shell activation dependency):

  • PYTHONPATH="$PWD" ./.venv/bin/python …

Swift (CARTON graph build)

  • Swift 5.9+ (integration/carton/runtime/graph/Package.swift is swift-tools-version: 5.9).
  • No network fetch is expected for the Swift graph package (no SwiftPM deps).

Ghidra + Java (kernel extraction)

Ghidra is required for rebaselining on a new macOS version. The actual extraction is staged later (section 3) because outputs are large and host-specific, but the tooling must be installed upfront.

Kernel extraction runs headlessly and writes to integration/evidence/ghidra/local/ (gitignored).

Required:

  • Java 21 (Temurin 21 is used in repo examples; see tools/ghidra/README.md).
  • Ghidra with a usable analyzeHeadless path (Homebrew default used in repo examples).
  • dyld-shared-cache-extractor in PATH (used by tools.ghidra.core.stage; override via --dyld-extractor).

PolicyWitness runners (runtime probe execution)

Runtime probes execute via PolicyWitness under runtime/. Built-in runner works immediately for SBPL-only probes; entitlement-bearing probes require installed external runners:

  • BYOXPC (PWRunner.byoxpc.xpc)
  • MachMe (PWRunner as a Mach service)

These require a GUI session (launchd bootstrap) and a signing path (it can be ad hoc signing). The install commands are in runtime/PolicyWitness.md and the safe template path is described in runtime/external/README.md.


1) Clone and create a repo-local venv

From repo root:

python3 -m venv .venv
PYTHONPATH="$PWD" ./.venv/bin/python -m pip install -U pip

Install the minimum Python dependencies for make test:

PYTHONPATH="$PWD" ./.venv/bin/python -m pip install \
  pytest==7.4.3 \
  frida \
  lief \
  mcp

Notes:

  • pytest==7.4.3 is a known-good version in this repo.
  • frida is required for tools/frida (spawn/attach capture).
  • lief is required for pawl/reverse/ops_extract.py Mach‑O scanning.
  • mcp is required for the convergence MCP server (orchestration/mcp_server.py).

2) First green: run the unified harness

From repo root (recommended for cold starts):

make cold-start

This runs venv-check, builds the forward compiler, then runs the full test harness.

Alternatively, if you've already built the forward compiler:

make test

What make test does (high level):

  • Validates CARTON (PYTHONPATH="$PWD" ./.venv/bin/python -m integration.carton build)
  • Runs pytest (PYTHONPATH="$PWD" ./.venv/bin/python -m pytest)
  • Builds the Swift graph package (integration/carton/runtime/graph/swift_build.py)

If tests skip due to missing private Ghidra outputs, that’s expected on a fresh checkout; the next section describes how to generate the minimal extracts that turn those skips into passes.

2.1) Optional native helpers (debugging + second implementations)

These are not required for make test, but they are part of the repo’s instrumentation surface.

Build a minimal libsandbox compile probe:

PYTHONPATH="$PWD" ./.venv/bin/python -m pawl.structure compile integration/fixtures/pawl/sample.sb --out /tmp/sample.sb.bin

Build and ad-hoc sign the sandbox_check() validator tools:

./tools/check/build.sh
./tools/check/hold_open/build.sh

2.2) Forward compiler (container_profile)

The forward lane (pawl/forward/) contains container_profile, a C tool that reconstructs SBPL from container metadata. make cold-start builds this automatically; this section is for manual builds or troubleshooting.

Prerequisites (Homebrew):

brew install cmake nlohmann-json argp-standalone

Manual build:

make forward-build

Or directly:

cd pawl/forward/src
mkdir -p ../build && cd ../build
cmake ../src && make

The resulting binary lands in pawl/forward/build/bin/container_profile.

Vocabulary and macOS version compatibility

container_profile loads operation and filter vocabulary from the CARTON bundle (integration/carton/contract/bundle/.../vocab/). There are no frozen per-release tables—the tool only supports --platforms host (CARTON).

If you are building on a macOS version that differs from the repo's pinned baseline (see world/registry.json), the CARTON vocabulary may be stale:

Symptom Likely cause
Unknown operation errors New ops added in your macOS version
Filter argument mismatches Filter signature changed
Scheme alias failures Surface name mappings drifted

To update CARTON vocabulary for a new host:

  1. Run make carton-refresh to regenerate mappings
  2. If ops/filters changed, re-extract from Ghidra (section 3)
  3. Re-run make test to verify

If you're on an unreleased macOS version, expect vocabulary drift until you complete the extraction workflow in section 3.


3) Kernel extraction (Ghidra): stage → run → export

This section is the “repo learns from the kernel” path. It is intentionally separate from the core test harness: local extracts are host-bound and gitignored, and public packs are sanitized outputs used to prove extraction fidelity and to seed downstream work.

Start with the connector guide:

  • tools/ghidra/README.md

3.1 Configure environment

Always set Java explicitly. PyGhidra headless execution is sensitive to Java discovery; missing or implicit Java can produce flaky, non-reproducible behavior and stale outputs. All automation invoking Ghidra must pass a known JDK explicitly.

# Required: explicit Java and Ghidra paths
export JAVA_HOME="$(/usr/libexec/java_home -v 21)"
export GHIDRA_HEADLESS="/opt/homebrew/opt/ghidra/libexec/support/analyzeHeadless"

Verify Java is correctly set before running Ghidra tasks:

echo "JAVA_HOME=$JAVA_HOME"
$JAVA_HOME/bin/java -version

PyGhidra requirement: The Ghidra scripts in this repo are Python scripts. If headless runs fail with Ghidra was not started with PyGhidra. Python is not available, you need to run Ghidra through PyGhidra instead of plain analyzeHeadless. Create a wrapper script:

#!/bin/bash
# Save as e.g. /usr/local/bin/pyghidra_headless
exec /opt/homebrew/opt/ghidra/libexec/support/pyghidraRun -H "$@"

Then set GHIDRA_HEADLESS to point at this wrapper, or invoke it directly.

Post-run verification: After Ghidra runs, verify expected output files actually changed. A "successful" run that produces stale or empty outputs is a common failure mode when Java or paths are misconfigured.

3.2 Stage BYO inputs into the local extract tree

Staging materializes host inputs (KC, libs, profiles) under the private tree. Do not commit staged inputs; they are intentionally gitignored.

PYTHONPATH="$PWD" ./.venv/bin/python -m tools.ghidra.core.stage --build-id <build_id>

3.3 Run the minimal extraction set used by tests

Some tests are wired to skip unless specific private outputs exist (see integration/support.py).

Run the following tasks (examples; adapt build id as needed):

PYTHONPATH="$PWD" ./.venv/bin/python tools/ghidra/run_task.py kernel-imports --build <build_id> --exec
PYTHONPATH="$PWD" ./.venv/bin/python tools/ghidra/run_task.py sandbox-kext-conf-scan --build <build_id> --exec
PYTHONPATH="$PWD" ./.venv/bin/python -m tools.ghidra.commands.refresh_canonical --name offset_inst_scan_0xc0_write_classify
PYTHONPATH="$PWD" ./.venv/bin/python -m tools.ghidra.commands.refresh_canonical --name kernel_collection_symbols_canary

3.4 Export a sanitized public pack

Export writes a public pack under integration/evidence/ghidra/public/<build_id>/ containing sanitized metadata only.

PYTHONPATH="$PWD" ./.venv/bin/python -m tools.ghidra.commands.export_extract --build-id <build_id>

3.5 Re-run tests

make test

If you staged/extracted for a build id that differs from the baseline fixtures, you may still see skips. At that point you're in "rebaseline" territory: see the next section for how to detect and understand drift.

3.6 Detect evidence drift (when moving to a new baseline)

When you move to a new macOS version or libsandbox changes, PAWL's pinned evidence may become stale. The forward rebaseline command helps you understand what drifted.

When to run:

  • After a macOS upgrade
  • When tests fail with evidence staleness errors
  • To characterize a new host before classifying artifacts

Run the rebaseline check:

PYTHONPATH="$PWD" ./.venv/bin/python -m pawl.pawl forward rebaseline

What the output tells you:

Drift type What it means
Probes changed Compiled blob bytes differ — libsandbox compilation behavior changed
Groups changed Operation membership in group-ops changed — e.g., a new op falls under file-read*
Fallbacks changed The _operation_info fallback chain structure changed

The command writes a JSON report to .../op_table/sb/rebaseline/rebaseline_report.json with full details. Use --force to recompile even if outputs exist.

What to do with drift:

This is a checkpoint, not a gate. The first time you rebaseline on a new host will be a learning process — you're discovering what changed, not fixing it automatically.

  • Review the drift report to understand the scope of changes
  • If probes changed: the pinned blobs need updating (re-run atlas builders)
  • If groups changed: Source-Evaluate assumptions may need updating
  • If fallbacks changed: re-extract from Ghidra, update canonical tables

For the full technical story (authority hierarchy, group derivation, fallback chains), see pawl/forward/README.md section "Forward Rebaseline".

For world-based artifact management plans and theories, see world/BASELINING.md.


4) Runtime readiness (PolicyWitness)

Start with:

  • runtime/README.md
  • runtime/PolicyWitness.md

Upstream:

This repo vendors a pinned bundle at runtime/bin/PolicyWitness.app (symlinked from runtime/PolicyWitness.app for compatibility).

4.1 Verify bundle + registry

PYTHONPATH="$PWD" ./.venv/bin/python -m runtime.core.status

4.2 Install external runners (BYOXPC + MachMe)

Follow the authoritative install flows in runtime/PolicyWitness.md:

  • “Install a BYOXPC runner”
  • “Install a MachMe runner”

When installed, verify:

PW="$PWD/runtime/bin/PolicyWitness.app/Contents/MacOS/policy-witness"
$PW runner verify --service-name <service_name> --timeout-ms 2000

5) Orchestration plan → runtime execution

Start here:

  • orchestration/api.py (plan/run facade)
  • orchestration/probe_plan_builder.py (probe-plan generation)
  • runtime/core/executor.py (runtime execution contract)

Build a plan from a casefile and persist it under a local output root:

PYTHONPATH="$PWD" ./.venv/bin/python - <<'PY'
from orchestration.probe_plan_builder import build_probe_plan_for_casefile

result = build_probe_plan_for_casefile(
    "path/to/casefile",
    persist_root="/tmp/pawl_plan/example_plan",
)
print(result.plan_ref)
PY

Build a full orchestration plan envelope from the same casefile:

PYTHONPATH="$PWD" ./.venv/bin/python - <<'PY'
from orchestration import plan_casefile

plan = plan_casefile(
    "path/to/casefile",
    persist_root="/tmp/pawl_plan/example_orchestration",
)
print(plan.probe_plan_path)
print(plan.transport_status)
PY

6) Final verification

From repo root:

make test

If make test fails:

  • Re-run tools/inside to confirm you are not interpreting harness restraint as policy denial.
  • Re-run PYTHONPATH="$PWD" ./.venv/bin/python -m runtime.core.status to confirm runtime readiness.
  • Re-run integration/carton build directly to isolate CARTON drift from pytest failures: PYTHONPATH="$PWD" ./.venv/bin/python -m integration.carton build