diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index af8f3f24..8291d8f8 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -48,7 +48,9 @@ jobs: uses: docker/build-push-action@v6 with: context: . - platforms: linux/amd64,linux/arm64 + # linux/386 is not published by the official node images and the napi +# binaries (@node-rs/crc32 via yauzl-promise) have no linux-ia32 build. +platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore index 42616533..dce82f62 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,12 @@ references/ mnt/ tunnel/ deploy.sh + +# SAP engine build chain (frontend/scripts/unicorn-wasm-patch/build.sh outputs) +frontend/scripts/unicorn-wasm-patch/unicorn-src/ +frontend/scripts/unicorn-wasm-patch/build/ +frontend/scripts/unicorn-wasm-patch/dist/ +frontend/src/apple/sap/vendor/unicorn-dbg.* + +# Local agent workspace +.video_agent/ diff --git a/AGENTS.md b/AGENTS.md index 03468082..b807ffc3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,11 +9,12 @@ ## Project Structure -- `backend/` — Node.js/Express server (TypeScript, ESM) -- `frontend/` — React SPA (TypeScript, Vite, Tailwind CSS) -- `e2e/` — Playwright E2E tests (pnpm) -- `references/ApplePackage/` — Swift reference implementation (source of truth) -- Multi-stage Docker build (single container serves both) +- `backend/` — Node.js/Express server (TypeScript, ESM); tests in `backend/tests/` +- `frontend/` — React SPA (TypeScript, Vite, Tailwind CSS); tests in `frontend/tests/` (not collocated with src) +- `cloudflare/` + `wrangler.jsonc` — Cloudflare Workers + Containers deployment wrapper around the Docker image +- `Dockerfile` / `compose.yml` — single container serves both backend and SPA +- `frontend/scripts/unicorn-wasm-patch/` — patches + glue + build script for the Unicorn TCI WASM engine (see SAP section) +- `references/` is gitignored personal infrastructure (never commit); a local ApplePackage checkout may or may not exist ## Architecture — Zero-Trust @@ -57,17 +58,77 @@ The server is a blind TCP proxy. It NEVER sees Apple credentials. **Key invariant**: The server NEVER sees Apple credentials. All Apple TLS terminates at the browser via libcurl.js WASM (Mbed TLS 1.3). The server only receives public CDN URLs and non-secret metadata for IPA compilation. The bag proxy (`/api/bag`) only returns public Apple service URLs — no credentials pass through it. -## Reference Implementation +## Architecture — SAP Request Signing (X-Apple-ActionSignature) + +Apple requires every request to the auth endpoint to carry `X-Apple-ActionSignature: base64(Sign(bodyBytes))`. The signature is produced by obfuscated SAP entry points inside Apple's CommerceKit/CoreFP binaries (the same mechanism ipatool uses). Key property: **the signer's inputs are only the hardware ID (the per-account `deviceIdentifier`) plus public Apple assets — never credentials — but the signature covers the request body, which contains the password.** Therefore signing MUST stay in the browser; the zero-trust invariant is preserved. + +``` +┌─ Browser ────────────────────────────────────────────────────────┐ +│ 1. Bag (via /api/bag) advertises: │ +│ sign-sap-setup → setup exchange endpoint (POST plist) │ +│ sign-sap-setup-cert → certificate endpoint (GET plist) │ +│ sign-sap-version → protocol version (200) │ +│ 2. GET /api/sap-assets/:name → four Apple binaries (backend- │ +│ extracted, digest-pinned, browser-cached in the Cache API) │ +│ 3. SAP signer worker (Web Worker, off the UI thread): │ +│ Unicorn 2.1.4 → TCI interpreter backend → wasm │ +│ Mach-O x86_64 images loaded + dyld-info relocated in TS │ +│ entry points: initialize / exchange / sign / teardown │ +│ 4. Key exchange over the wisp tunnel (main thread): │ +│ GET cert → exchange(state 1) → POST setup → exchange(state 0)│ +│ 5. authenticate() signs each attempt's exact UTF-8 body bytes │ +│ and attaches X-Apple-ActionSignature │ +└──────────────────────────────────────────────────────────────────┘ +``` + +### Frontend modules (`frontend/src/apple/sap/`) + +- `engine.ts` — Unicorn WASM wrapper; all guest addresses cross the JS boundary as doubles (exact below 2^53; the guest map stays below 2^48) +- `machImage.ts` — Mach-O 64 parser: fat-binary slicing, LC_SEGMENT_64, symtab lookup, dyld_info rebase/bind/weak/lazy opcodes. **Opcode tables**: rebase uses nibbles 0x00–0x80 (SET_TYPE 0x10, SET_SEGMENT 0x20, …); bind is shifted down one slot (DO_BIND 0x90) because rebase-only ADD_ADDR_IMM_SCALED occupies bind's 0x90 slot elsewhere. Segment offsets move in uint64 BigInt space — bind ADD_ADDR_ULEB deltas are 64-bit encodings of negative steps. Fixups targeting a segment's BSS tail (within vmsize, past fileSize) are skipped: dyld would zero-fill them. +- `shims.ts` — guest libc/CF/IOKit shims; 64-bit −1 constants are passed as `-1` (wasm's saturating f64→i64 conversion yields 0xFFFF…F, which a JS number cannot represent exactly) +- `machine.ts` — entry-point invocation, scratch/stack layout, output disposal +- `signer.ts` / `client.ts` / `worker.ts` — orchestration; emulation runs in a Web Worker, Apple network calls ride the wisp tunnel on the main thread. `client.ts` keeps the signer as a singleton bound to the deviceIdentifier (rebuilding it means copying the 22.5 MB asset bundle into a fresh worker), with a zustand store (`store/sap.ts`), a warmup hook (`hooks/useSapWarmup.ts`, fires once an account exists), and an inline progress indicator (`components/common/SapStatus.tsx`) +- `protocol.ts` — certificate fetch + setup exchange (plist `` round-trip via appleRequest) +- `assets.ts` — asset download with progress, SHA-256 verification (stripped-file pins), Cache API persistence; accepts both thin and fat Mach-O payloads +- `vendor/unicorn.mjs|.wasm` — prebuilt engine (regenerate via `frontend/scripts/unicorn-wasm-patch/build.sh`) + +### Unicorn TCI WASM build chain (`frontend/scripts/unicorn-wasm-patch/`) + +Unicorn 2.x only ships JIT TCG backends, which cannot execute under WebAssembly (no RWX, no wasm codegen). The patch (`patches/unicorn-2.1.4-tci-wasm.patch` against unicorn 2.1.4, whose QEMU base is 5.0.1): + +1. Restores the QEMU 5.0.1 TCI interpreter (unicorn stripped it) into the tree +2. Forces 64-bit virtual TCI registers on wasm32 — QEMU 5.0's 32-bit TCI path is riddled with TODO() stubs; the register file is virtual state, so 64-bit registers need no 64-bit host pointers +3. Generates uniform-signature helper trampolines (`qemu/target/i386/tci-wasm-tramp.c`): TCI invokes every helper through one cast signature, which wasm's strict indirect-call checks reject +4. Adapts glib-compat GTree comparators (2-arg vs 3-arg) and disables inline hook callbacks for the same reason +5. Replaces the timeout thread (no pthreads in wasm) with a wall-clock deadline checked inside the TCI interpreter loop; mprotect/mmap-based guest RAM becomes aligned malloc + +Build: `bash frontend/scripts/unicorn-wasm-patch/build.sh` (requires docker; emscripten runs in a container). The signed-off artifacts land in `frontend/src/apple/sap/vendor/`. + +### Backend asset pipeline + +`backend/src/services/sapAssets.ts` extracts the four binaries once from Apple's public OSXUpd10.9.pkg: xar TOC parse → HTTP range download of the Payload tail (~380 MB) → bzip2 stream (with a synthetic `BZh9` header from a fixed offset) → cpio (odc and newc formats) → pinned SHA-256 verification → **fat-binary stripping to the x86_64 slice** (the emulated guest architecture; CoreFP ships as an i386+x86_64 universal, so this halves it) → cache under `DATA_DIR/sap-assets`. All data is public Apple content (same trust class as the bag proxy). Specs carry two pin sets: the original Apple digest verifies the extraction; the stripped digest (a deterministic function of the original) verifies what is served and what the browser downloads. Distribution sizes: 37.7 MB original → 22.5 MB stripped → ~14 MB on the wire with the route's gzip response. The bz2 stream is truncated mid-file, so the decoder can emit a late crc error after the wanted members are captured — the pipeline swallows it by design (an unhandled rejection would crash Node). + +On startup `ensureSapAssets` prefers `DATA_DIR/sap-assets`, then seeds from the image-prebaked directory (`BUNDLED_SAP_ASSETS`, default `/opt/asspp/sap-assets`), and only then falls back to network extraction (the bzip2 decoder is imported lazily because cross-built images may lack a matching napi binary for the runtime arch — see Dockerfile). Routes (`backend/src/routes/sapAssets.ts`): `GET /api/sap-assets/status`, `POST /api/sap-assets/prepare`, `GET /api/sap-assets/:name` (gzip when accepted). -The Swift reference at `references/ApplePackage/` is the source of truth for Apple protocol behavior: +### Container image -- Field mappings (iTunes API → Software type) use Swift `CodingKeys` -- Authentication flow, bag endpoint, pod routing, error codes -- Always consult the reference when making protocol changes +The Dockerfile prebakes the stripped SAP assets at build time (a `sap-assets` stage runs `backend/scripts/extract-sap-assets.mts`; Docker layer caching makes it a no-op on rebuilds). Release images ship the assets, so a fresh VPS serves them with zero network use. Build stages run on `$BUILDPLATFORM` (JS artifacts are platform-independent); the runtime image installs production deps per target platform because `yauzl-promise` → `@node-rs/crc32` ships prebuilt napi binaries per arch. Published platforms: `linux/amd64`, `linux/arm64`. **linux/386 is out**: official node images dropped it and `@node-rs/crc32` has no linux-ia32 build. + +### SAP invariants + +- The signer only ever sees the deviceIdentifier and public Apple assets; the password reaches the signer solely as opaque body bytes it signs in-place — it is never transmitted anywhere except through the wisp tunnel inside the auth request itself +- Bag missing the SAP keys → signing is skipped (graceful degradation to the legacy flow) +- SAP session lifetime = the page session: the signer is a singleton per deviceIdentifier, reused across sign-in attempts (2FA retries included); switching accounts rebuilds it. Initialization ≈ 150–300 ms of emulation plus the setup exchange round-trips +- First login downloads ~14 MB over the wire (22.5 MB stripped assets, gzipped); a background warmup and inline progress line cover it. Release images prebake the assets, so the backend serves them instantly +- The live bag currently returns the legacy `MZFinance` authenticate endpoint (see upstream PR discussion); `normalizeAuthURL` is effectively inert, and all three advertised endpoints sit in the SAP-signed list — signing applies regardless + +## Reference Implementation + +The upstream Swift project ApplePackage is the source of truth for Apple protocol behavior (authentication flow, bag endpoint, pod routing, error codes). A local checkout may live at `references/ApplePackage/`, but `references/` is gitignored — it is **not part of this repository** and may be absent. When unavailable, the field mapping below and the existing `frontend/src/apple/*` implementation are the in-repo reference. ### iTunes API Field Mapping -The backend (`backend/src/routes/search.ts`) maps raw iTunes API fields to our `Software` type, matching the Swift CodingKeys in `references/ApplePackage/Sources/ApplePackage/Models/Software.swift`: +The backend (`backend/src/routes/search.ts`) maps raw iTunes API fields to our `Software` type, matching the Swift `CodingKeys` in ApplePackage's `Software.swift`: | iTunes Field | Software Field | | --------------------------- | -------------- | @@ -124,6 +185,7 @@ The backend proxies the bag endpoint via `GET /api/bag?guid=` using No - `tsx` for development, `tsc` for production build - SINF injector also handles optional `iTunesMetadata.plist` injection at IPA root - Bag proxy for `init.itunes.apple.com` +- SAP asset extraction service (xar + bzip2 + cpio) with digest pinning; routes under `/api/sap-assets` ### Backend Shared Utilities @@ -152,6 +214,8 @@ The backend proxies the bag endpoint via `GET /api/bag?guid=` using No - **AppIcon** — 3 sizes (40/56/80px), rounded corners, letter fallback - **Badge** — color-coded status pill - **ProgressBar** — gray track, blue fill, percentage label +- **ToastContainer** / `utils/toast.ts` — toast notifications (incl. account-context helpers) +- **GlobalDownloadNotifier** — global download status notifications - **icons** — shared SVG icon components (`HomeIcon`, `AccountsIcon`, `SearchIcon`, `DownloadsIcon`, `SettingsIcon`, `SunIcon`, `MoonIcon`, `SystemIcon`) used by Sidebar, MobileNav, and MobileHeader ### Frontend Shared Utilities (`utils/`) @@ -159,6 +223,8 @@ The backend proxies the bag endpoint via `GET /api/bag?guid=` using No - `utils/error.ts` — `getErrorMessage(e, fallback)` for standardized catch-block error extraction - `utils/crypto.ts` — AES-GCM encrypt/decrypt for account export/import - `utils/account.ts` — `accountHash()`, `accountStoreCountry()`, `firstAccountCountry()` +- `utils/toast.ts` — toast helpers (pairs with `ToastContainer`) +- `utils/version.ts` — numeric dot-separated version string comparison ### Import Ordering Convention @@ -212,59 +278,52 @@ The settings endpoint (`/api/settings`) must never reflect request headers (`x-f ## Testing -### Unit Tests +### Unit Tests (Vitest) ```bash -cd backend && npx vitest run # Node environment -cd frontend && npx vitest run # jsdom environment with fake-indexeddb +cd backend && npx vitest run # Node environment; tests in backend/tests/ +cd frontend && npx vitest run # jsdom environment with fake-indexeddb; tests in frontend/tests/ ``` -### E2E Tests (Playwright) +Frontend tests mirror the src layout under `frontend/tests/` (`apple/`, `api/`, `store/`, `utils/`) — add new tests there, not next to source files. -```bash -cd e2e && pnpm test # Local (requires Docker on port 8080) -docker compose --profile test run --rm playwright # Docker-based -bash e2e/docker-test.sh # Full: build + test + zero-trust verify -``` +There is no E2E suite or lint script in the repo currently. Real-account Docker verification (2026-02-22): authentication succeeds through Wisp, and backend logs contain only connection/stream metadata (no Apple credentials, password tokens, or cookies). -E2E tests import from `./fixtures` instead of `@playwright/test`. +SAP-specific tests: -WebSocket proxy tests use `location.host` to derive URLs dynamically, so they work both locally (`localhost:8080`) and in Docker (`asspp:8080`). +- `frontend/tests/sap/machImage.test.ts` (vitest) — synthetic Mach-O builder exercising symbol export, rebase/bind opcodes, addends, and BSS-tail fixup tolerance +- `frontend/tests/sap/machine-live.mjs` (manual, `npx tsx`) — full chain against the real Apple assets served by the backend: machine open → Initialize (context matches the native ipatool runtime bit-for-bit: `0x400000000200`) → Sign correctly gated by the key exchange (`-42085` without it, identical to native). Requires `SAP_ASSET_DIR` pointing at a flat copy of the four assets, or the nested extraction layout -Real-account Docker verification (2026-02-22): authentication succeeds through Wisp, and backend logs contain only connection/stream metadata (no Apple credentials, password tokens, or cookies). - -E2E tests cover: - -- Wisp proxy (accepts /wisp/ WebSocket, rejects non-wisp paths) -- Add account flow (device ID field, randomize button, auth) -- Account detail (device ID, pod display) -- Settings page (no global device ID section) -- Search/lookup by bundle ID (verifies iTunes field mapping) -- Downloads API (iTunesMetadata support, backward compatibility) - -### Test Account - -Test credentials are stored in environment variables (`TEST_EMAIL`, `TEST_PASSWORD`, `TEST_DEVICE_ID`, `TEST_BUNDLE_ID`) and must never be committed to the repository. +Test credentials, if ever needed, belong in environment variables (`TEST_EMAIL`, `TEST_PASSWORD`, `TEST_DEVICE_ID`, `TEST_BUNDLE_ID`) and must never be committed. ## Deployment +### Docker Compose (self-host) + ```bash -docker compose up --build -d # Builds and runs on port 8080 +docker compose up -d # Runs prebuilt image ghcr.io/lakr233/assppweb:latest on port 8080 ``` +`compose.yml` pulls the published image (no local build), mounts `./mnt/asspp-data:/data` for `DATA_DIR`, and supports `ACCESS_PASSWORD` / `DOWNLOAD_THREADS` env vars. The `Dockerfile` at the repo root is what CI builds and publishes that image. + Single container serves both the Express backend and the Vite-built React SPA. SPA routes are handled by serving `index.html` for all non-API paths. -### Docker E2E Testing +### Cloudflare Workers + Containers -The `compose.yml` includes a `playwright` service under the `test` profile: +`wrangler.jsonc` + `cloudflare/src/index.ts` deploy the same Docker image as a Cloudflare Container behind a Worker: ```bash -docker compose --profile test run --rm playwright +npx wrangler login +npx wrangler deploy ``` -This runs Playwright inside the official `mcr.microsoft.com/playwright` image, connecting to the app container via Docker internal DNS (`http://asspp:8080`). The `asspp` service has a healthcheck so the test container waits until the app is ready. +- Requires the Cloudflare Workers **Paid** plan (Containers are not on Free) +- All HTTP/WebSocket traffic routes to one named container instance (`main`) to keep state consistent; `max_instances: 1` +- Container filesystem is **ephemeral** — compiled IPAs are lost when the container stops/sleeps (`sleepAfter = "2h"`) +- Health ping endpoint: `/api/settings`; worker injects `x-forwarded-proto: https` when missing to avoid redirect loops +- `wrangler.jsonc` build command installs `@cloudflare/containers` on the fly, so deploys need no persistent devDependency -The `e2e/docker-test.sh` script automates the full flow: build, test, and verify zero-trust by scanning backend logs for credential leaks. +`README.md` documents the full deploy matrix (Cloudflare button, Railway with its Cloudflare-proxy TLS caveat, reverse-proxy WebSocket requirements for `/wisp/`). ## Interface Design System diff --git a/Dockerfile b/Dockerfile index bb5379f3..9bfb4902 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,18 @@ -# Stage 1: Build frontend -FROM node:20-alpine AS frontend-build +# Build stages run on the build machine's native architecture (BUILDPLATFORM) +# and produce platform-independent artifacts; only the runtime layer is built +# per target platform. This keeps linux/386 builds as cheap as amd64/arm64. +FROM --platform=$BUILDPLATFORM node:20-alpine AS frontend-build WORKDIR /app/frontend COPY frontend/package*.json ./ RUN npm ci COPY frontend/ ./ RUN npm run build -# Stage 2: Build backend -FROM node:20-alpine AS backend-build +# Produces dist/ (pure JS) plus a full node_modules used by the asset +# extraction stage below. This node_modules is NOT copied into the runtime +# image: native binaries (napi) must match the target platform, which is +# installed there per-arch instead. +FROM --platform=$BUILDPLATFORM node:20-alpine AS backend-build RUN apk add --no-cache python3 make g++ WORKDIR /app/backend COPY backend/package*.json ./ @@ -15,14 +20,32 @@ RUN npm ci COPY backend/ ./ RUN npm run build -# Stage 3: Runtime +# Downloads the public Apple update package once (~380 MB over range +# requests), verifies the pinned digests, strips fat binaries to their x86_64 +# slices, and emits ~22.5 MB of assets. Docker layer caching makes this a +# no-op on rebuilds unless the pinned digests change. +FROM --platform=$BUILDPLATFORM node:20-alpine AS sap-assets +WORKDIR /app/backend +COPY --from=backend-build /app/backend ./ +ARG SAP_ASSETS_OUT=/out +RUN DATA_DIR=/tmp/sap-extract-work node --import tsx scripts/extract-sap-assets.mts ${SAP_ASSETS_OUT} + FROM node:20-alpine RUN apk add --no-cache zip WORKDIR /app COPY --from=backend-build /app/backend/dist ./dist -COPY --from=backend-build /app/backend/node_modules ./node_modules -COPY --from=backend-build /app/backend/package.json ./ +COPY backend/package*.json ./ +# Native modules install per target platform: @node-rs/crc32 (via +# yauzl-promise) ships prebuilt napi binaries, but bufferutil (via wisp-js) +# has no linux-arm64-musl prebuild and falls back to source compilation. +# The toolchain is added and removed inside one layer, so it never bloats +# the final image. +RUN apk add --no-cache --virtual .node-build python3 make g++ \ + && npm ci --omit=dev \ + && apk del .node-build \ + && npm cache clean --force COPY --from=frontend-build /app/frontend/dist ./public +COPY --from=sap-assets /out /opt/asspp/sap-assets RUN mkdir -p /data/packages EXPOSE 8080 ARG BUILD_COMMIT=unknown diff --git a/backend/package-lock.json b/backend/package-lock.json index 2b1948b4..ae8a8210 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -14,6 +14,7 @@ "bplist-parser": "^0.3.2", "express": "^4.21.2", "plist": "^3.1.0", + "unbzip2-stream": "^1.4.3", "uuid": "^11.0.5", "yauzl-promise": "^4.0.0" }, @@ -26,6 +27,7 @@ "@types/node": "^22.13.1", "@types/plist": "^3.0.5", "@types/supertest": "^6.0.3", + "@types/unbzip2-stream": "^1.4.3", "@types/uuid": "^10.0.0", "@types/ws": "^8.18.1", "@types/yauzl-promise": "^4.0.1", @@ -1712,6 +1714,26 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/through": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz", + "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@types/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-D8X5uuJRISqc8YtwL8jNW2FpPdUOCYXbfD6zNROCTbVXK9nawucxh10tVXE3MPjnHdRA1LvB0zDxVya/lBsnYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/through": "*" + } + }, "node_modules/@types/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", @@ -2040,6 +2062,30 @@ "node": ">= 5.10.0" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/bufferutil": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", @@ -2936,6 +2982,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -3864,6 +3930,12 @@ "dev": true, "license": "MIT" }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -4017,6 +4089,16 @@ "node": ">=14.17" } }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, "node_modules/undici": { "version": "7.21.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.21.0.tgz", diff --git a/backend/package.json b/backend/package.json index 16861660..e3385dbf 100644 --- a/backend/package.json +++ b/backend/package.json @@ -17,6 +17,7 @@ "bplist-parser": "^0.3.2", "express": "^4.21.2", "plist": "^3.1.0", + "unbzip2-stream": "^1.4.3", "uuid": "^11.0.5", "yauzl-promise": "^4.0.0" }, @@ -29,6 +30,7 @@ "@types/node": "^22.13.1", "@types/plist": "^3.0.5", "@types/supertest": "^6.0.3", + "@types/unbzip2-stream": "^1.4.3", "@types/uuid": "^10.0.0", "@types/ws": "^8.18.1", "@types/yauzl-promise": "^4.0.1", diff --git a/backend/scripts/extract-sap-assets.mts b/backend/scripts/extract-sap-assets.mts new file mode 100644 index 00000000..c20c7c10 --- /dev/null +++ b/backend/scripts/extract-sap-assets.mts @@ -0,0 +1,42 @@ +// Build-time SAP asset extraction entrypoint (used by the Dockerfile). +// Downloads the public Apple update package once, verifies the pinned +// digests, strips fat binaries to their x86_64 slices, and writes the four +// files into OUT_DIR so the runtime image can ship them prebaked. + +import { mkdir, copyFile } from "node:fs/promises"; +import path from "node:path"; + +const outDir = process.argv[2]; + +if (!outDir) { + console.error("usage: DATA_DIR= tsx scripts/extract-sap-assets.mts "); + process.exit(1); +} + +if (!process.env.DATA_DIR) { + console.error("DATA_DIR must be set (config.ts reads it at import time)"); + process.exit(1); +} + +// Imported after the DATA_DIR guard: the config module captures the env at +// import time, so a default assigned here would arrive too late. +const { ensureSapAssets, readCachedAsset, SAP_ASSET_SPECS } = await import( + "../src/services/sapAssets.ts" +); + +await ensureSapAssets(); +await mkdir(outDir, { recursive: true }); + +for (const spec of SAP_ASSET_SPECS) { + const data = await readCachedAsset(spec.name); + if (!data) { + throw new Error(`asset ${spec.name} missing after extraction`); + } + await copyFile( + path.join(process.env.DATA_DIR, "sap-assets", spec.name), + path.join(outDir, spec.name), + ); + console.log(`${spec.name}: ${data.length} bytes`); +} + +console.log(`SAP assets prebaked into ${outDir}`); diff --git a/backend/src/index.ts b/backend/src/index.ts index 7151d943..7ccd4e95 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -14,6 +14,7 @@ import packageRoutes from "./routes/packages.js"; import installRoutes from "./routes/install.js"; import settingsRoutes from "./routes/settings.js"; import bagRoutes from "./routes/bag.js"; +import sapAssetRoutes from "./routes/sapAssets.js"; const app = express(); @@ -30,6 +31,7 @@ app.use("/api", packageRoutes); app.use("/api", installRoutes); app.use("/api", settingsRoutes); app.use("/api", bagRoutes); +app.use("/api", sapAssetRoutes); // Serve static frontend files const publicDir = path.resolve(import.meta.dirname, "../public"); diff --git a/backend/src/routes/sapAssets.ts b/backend/src/routes/sapAssets.ts new file mode 100644 index 00000000..db936669 --- /dev/null +++ b/backend/src/routes/sapAssets.ts @@ -0,0 +1,74 @@ +// SAP asset routes: status/prepare for the extraction job and authenticated +// downloads of the four cached Apple binaries. The files are public Apple +// content (extracted from a public software update package, digest-pinned), +// placing them in the same trust class as the bag proxy. + +import { Router, Request, Response } from "express"; +import zlib from "node:zlib"; +import { + SAP_ASSET_SPECS, + ensureSapAssets, + readCachedAsset, + sapAssetsState, +} from "../services/sapAssets.js"; + +const router = Router(); + +router.get("/sap-assets/status", (_req: Request, res: Response) => { + res.json(sapAssetsState()); +}); + +router.post("/sap-assets/prepare", async (_req: Request, res: Response) => { + try { + // Fire-and-observe: the caller polls /status for progress. + const preparation = ensureSapAssets(); + void preparation.catch(() => undefined); + res.json(sapAssetsState()); + } catch (error) { + res.status(500).json({ + error: error instanceof Error ? error.message : String(error), + }); + } +}); + +router.get("/sap-assets/:name", async (req: Request, res: Response) => { + const name = req.params.name as string; + const spec = SAP_ASSET_SPECS.find((candidate) => candidate.name === name); + if (!spec) { + res.status(404).json({ error: "Unknown SAP asset" }); + return; + } + + let data = await readCachedAsset(name); + if (!data) { + try { + await ensureSapAssets(); + data = await readCachedAsset(name); + } catch (error) { + res.status(503).json({ + error: + error instanceof Error ? error.message : "SAP asset extraction failed", + }); + return; + } + } + if (!data) { + res.status(503).json({ error: "SAP asset unavailable" }); + return; + } + + res.setHeader("Content-Type", "application/octet-stream"); + res.setHeader("ETag", `"${spec.strippedSha256}"`); + res.setHeader("Cache-Control", "private, max-age=31536000, immutable"); + // gzip cuts the ~22.5 MiB bundle to ~14 MiB on the wire (CoreFP's obfuscated + // __TEXT compresses at ~50%, the icxs data blob at ~13% of its size). + if (req.headers["accept-encoding"]?.includes("gzip") && data.length > 65536) { + res.setHeader("Content-Encoding", "gzip"); + res.setHeader("Vary", "Accept-Encoding"); + res.send(zlib.gzipSync(data, { level: 9 })); + return; + } + res.send(data); +}); + +export default router; diff --git a/backend/src/services/sapAssets.ts b/backend/src/services/sapAssets.ts new file mode 100644 index 00000000..16f1b99e --- /dev/null +++ b/backend/src/services/sapAssets.ts @@ -0,0 +1,641 @@ +// SAP asset extraction service. +// +// Apple's SAP signer runs four binaries extracted from a public OS X 10.9 +// software update package on swcdn.apple.com. The backend downloads the +// package's xar Payload once via HTTP range requests, decompresses the bzip2 +// stream from a fixed offset, and pulls the four files out of the cpio +// archive — verifying each against its pinned SHA-256 digest before caching +// them under DATA_DIR/sap-assets. All data is public Apple content; no +// credentials are involved at any point. +// +// Wire format details mirror ipatool's internal/sap/assets (and the +// extraction pipeline verified against the pinned digests on 2026-09-02). + +import https from "node:https"; +import zlib from "node:zlib"; +import { createHash } from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { Readable, Transform } from "node:stream"; +import { config } from "../config.js"; + +const UPDATE_URL = + "https://swcdn.apple.com/content/downloads/27/34/041-98128-A_SYPWICN3KH/5dqkl4rqgbsr18yzy61yeie9g3cmjc5hiv/OSXUpd10.9.pkg"; +const BZ2_OFFSET = 0x352f40d5; +const CPIO_SKIP = 0x3a4; +const XAR_HEADER_SIZE = 28; + +export interface SapAssetSpec { + name: string; + /** Path inside the cpio archive. */ + archivePath: string; + /** Original Apple file: size + digest verify the extraction itself. */ + size: number; + sha256: string; + /** + * Distributed file: fat binaries are stripped to their x86_64 slice (the + * emulated guest architecture) before caching, halving CoreFP. The + * stripped digest is a deterministic function of the original — the + * extraction still only accepts bytes matching the Apple digest above. + */ + strippedSize: number; + strippedSha256: string; +} + +export const SAP_ASSET_SPECS: SapAssetSpec[] = [ + { + name: "CommerceKit", + archivePath: + "./System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/CommerceKit", + size: 3271840, + sha256: "b84ff12c21987856c0a17b78f1ad82b73195a6dec5f3b208a17d245555a2c8a2", + // Thin x86_64 image; no fat wrapper to strip. + strippedSize: 3271840, + strippedSha256: + "b84ff12c21987856c0a17b78f1ad82b73195a6dec5f3b208a17d245555a2c8a2", + }, + { + name: "CommerceCore", + archivePath: + "./System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Frameworks/CommerceCore.framework/Versions/A/CommerceCore", + size: 207744, + sha256: "c5401e57402230f3c876409d295319ddf1e61287bc882683c5d61277be7bc1f2", + strippedSize: 115712, + strippedSha256: + "05707cd937798f2b5189471f513672ac6242ffbffc38f06ed2e4fb4345156819", + }, + { + name: "CoreFP", + archivePath: + "./System/Library/PrivateFrameworks/CoreFP.framework/Versions/A/CoreFP", + size: 29014912, + sha256: "f19141336be4198d0f8991bb00017c915efc7aeaece36c345f7faa1237ea6074", + strippedSize: 14904192, + strippedSha256: + "97c899f2fb076bdf7f810fe00ceb335d4af85efab0f2de737ad0aedd991c8277", + }, + { + name: "CoreFP.icxs", + archivePath: + "./System/Library/PrivateFrameworks/CoreFP.framework/Versions/A/CoreFP.icxs", + size: 5288352, + sha256: "473e78af86979f5bd4f6269561caf770b3d16c098d918846eeac8cdd2fe6566a", + // FairPlay data blob; no architecture slices. + strippedSize: 5288352, + strippedSha256: + "473e78af86979f5bd4f6269561caf770b3d16c098d918846eeac8cdd2fe6566a", + }, +]; + +export type SapAssetsState = + | { status: "idle" } + | { + status: "extracting"; + progress: number; + downloadedBytes: number; + totalBytes: number; + } + | { status: "ready" } + | { status: "error"; error: string }; + +let state: SapAssetsState = { status: "idle" }; +let extraction: Promise | null = null; + +export function sapAssetsState(): SapAssetsState { + return state; +} + +function cacheDir(): string { + return path.join(config.dataDir, "sap-assets"); +} + +function assetPath(name: string): string { + const spec = SAP_ASSET_SPECS.find((candidate) => candidate.name === name); + if (!spec) { + throw new Error("unknown SAP asset"); + } + return path.join(cacheDir(), spec.name); +} + +export async function readCachedAsset(name: string): Promise { + const spec = SAP_ASSET_SPECS.find((candidate) => candidate.name === name); + if (!spec) { + return null; + } + try { + const data = await fs.readFile(assetPath(name)); + return verifyStripped(spec, data) ? data : null; + } catch { + return null; + } +} + +function verifyStripped(spec: SapAssetSpec, data: Buffer): boolean { + if (data.length !== spec.strippedSize) { + return false; + } + return ( + createHash("sha256").update(data).digest("hex") === spec.strippedSha256 + ); +} + +/** + * Extracts the x86_64 slice from a fat (universal) Mach-O, returning the + * input untouched for thin images and non-Mach-O data. Mirrors the browser's + * amd64Slice in frontend/src/apple/sap/machImage.ts. + */ +function stripToX86_64(data: Buffer): Buffer { + if (data.length < 8) { + return data; + } + const magic = data.readUInt32BE(0); + if (magic !== 0xcafebabe && magic !== 0xcafebabf) { + return data; + } + const wide = magic === 0xcafebabf; + const count = data.readUInt32BE(4); + const entrySize = wide ? 32 : 20; + for (let index = 0; index < count; index++) { + const entry = 8 + index * entrySize; + const cputype = data.readInt32BE(entry); + if (cputype !== 0x01000007) { + continue; // x86_64 + } + const offset = wide + ? Number(data.readBigUInt64BE(entry + 8)) + : data.readUInt32BE(entry + 8); + const size = wide + ? Number(data.readBigUInt64BE(entry + 16)) + : data.readUInt32BE(entry + 12); + if (offset + size > data.length) { + throw new Error("x86_64 slice exceeds input size"); + } + return data.subarray(offset, offset + size); + } + return data; +} + +/** + * Directory of assets prebaked into the container image (see Dockerfile). + * When present, a fresh volume is seeded from it without any network use. + */ +export function bundledSapAssetsDir(): string { + return process.env.BUNDLED_SAP_ASSETS ?? "/opt/asspp/sap-assets"; +} + +/** Ensures assets are extracted and cached; concurrent callers share the job. */ +export async function ensureSapAssets(): Promise { + for (const spec of SAP_ASSET_SPECS) { + if (!(await readCachedAsset(spec.name))) { + if (await seedFromBundled()) { + break; + } + await extract(); + return; + } + } + state = { status: "ready" }; +} + +async function seedFromBundled(): Promise { + const bundled = bundledSapAssetsDir(); + let any = false; + try { + await fs.access(bundled); + } catch { + return false; + } + for (const spec of SAP_ASSET_SPECS) { + if (await readCachedAsset(spec.name)) { + continue; + } + try { + const data = await fs.readFile(path.join(bundled, spec.name)); + if (!verifyStripped(spec, data)) { + continue; + } + await fs.mkdir(cacheDir(), { recursive: true }); + const target = assetPath(spec.name); + await fs.writeFile(`${target}.tmp`, data); + await fs.rename(`${target}.tmp`, target); + any = true; + } catch { + // bundled file missing/corrupt; fall through to extraction + } + } + return any; +} + +async function extract(): Promise { + if (extraction) { + return extraction; + } + extraction = runExtraction().finally(() => { + extraction = null; + }); + return extraction; +} + +async function runExtraction(): Promise { + state = { status: "extracting", progress: 0, downloadedBytes: 0, totalBytes: 0 }; + + try { + const location = await locatePayload(); + const streamStart = location.heapOffset + BZ2_OFFSET; + const totalBytes = location.length - BZ2_OFFSET; + state = { status: "extracting", progress: 0, downloadedBytes: 0, totalBytes }; + + await fs.mkdir(cacheDir(), { recursive: true }); + + const results = new Map(); + const wanted = new Map void>(); + for (const spec of SAP_ASSET_SPECS) { + wanted.set(spec.archivePath, (data) => results.set(spec.name, data)); + } + + let allFoundResolve: (() => void) | null = null; + const allFound = new Promise((resolve) => { + allFoundResolve = resolve; + }); + const skipper = new SkipStream(CPIO_SKIP); + const extractor = new CpioExtractor(wanted, () => allFoundResolve?.()); + + let downloadedBytes = 0; + const cdnStream = rangeStream(streamStart, streamStart + totalBytes, (bytes) => { + downloadedBytes += bytes; + if (state.status === "extracting") { + state.downloadedBytes = downloadedBytes; + state.progress = Math.min(0.95, downloadedBytes / totalBytes); + } + }); + + // unbzip2-stream pulls a native crc32 accelerator as a transitive + // dependency; cross-built images (buildx BUILDPLATFORM stages) may not + // carry a binary matching the runtime architecture. Load it lazily so + // images with prebaked assets never touch this path. + let decompress: () => import("through").ThroughStream; + try { + decompress = (await import("unbzip2-stream")).default; + } catch { + throw new Error( + "SAP network extraction unavailable on this image (bzip2 decoder missing); " + + "use a prebaked release image or mount assets via BUNDLED_SAP_ASSETS", + ); + } + + // "BZh9" + the raw tail reconstructs the bz2 member the same way the + // reference implementation does. The prepended header must be a real + // byte stream (Readables concatenated as values would corrupt it). + const bz2Stream = new PrependStream(Buffer.from("BZh9", "latin1"), cdnStream); + const pipeline = bz2Stream.pipe(decompress()).pipe(skipper).pipe(extractor); + + const finished = new Promise((resolve, reject) => { + pipeline.on("finish", () => resolve()); + pipeline.on("error", (error: Error) => reject(error)); + }); + + // The extractor resolves as soon as the last wanted member is captured; + // racing it against the stream end aborts the download early. The bz2 + // stream is truncated mid-file (the package heap contains other data + // after it), so the decoder may emit a late crc error once the wanted + // members are already captured — that must never surface as an + // unhandled rejection, which crashes Node outright. + finished.catch(() => undefined); + await Promise.race([ + allFound, + finished.then(() => { + if (results.size !== SAP_ASSET_SPECS.length) { + throw new Error( + `extraction found ${results.size}/${SAP_ASSET_SPECS.length} assets`, + ); + } + }), + ]).finally(() => { + for (const segment of [extractor, skipper, bz2Stream, cdnStream]) { + segment.removeAllListeners("error"); + segment.on("error", () => undefined); + segment.destroy(); + } + }); + + if (results.size !== SAP_ASSET_SPECS.length) { + throw new Error( + `extraction found ${results.size}/${SAP_ASSET_SPECS.length} assets`, + ); + } + + for (const spec of SAP_ASSET_SPECS) { + const original = results.get(spec.name)!; + if (original.length !== spec.size) { + throw new Error(`asset ${spec.name} has unexpected size`); + } + const digest = createHash("sha256").update(original).digest("hex"); + if (digest !== spec.sha256) { + throw new Error(`asset ${spec.name} failed integrity verification`); + } + + const stripped = stripToX86_64(original); + if (!verifyStripped(spec, stripped)) { + throw new Error(`asset ${spec.name} failed post-strip verification`); + } + const target = assetPath(spec.name); + await fs.writeFile(`${target}.tmp`, stripped); + await fs.rename(`${target}.tmp`, target); + } + + state = { status: "ready" }; + } catch (error) { + state = { + status: "error", + error: error instanceof Error ? error.message : String(error), + }; + throw error; + } +} + +function httpsGetRange(url: string, start: number, end: number): Promise { + return new Promise((resolve, reject) => { + const request = https.get( + url, + { headers: { Range: `bytes=${start}-${end}` } }, + (response) => { + if (response.statusCode !== 206) { + request.destroy(); + reject(new Error(`CDN returned ${response.statusCode}`)); + return; + } + const chunks: Buffer[] = []; + response.on("data", (chunk: Buffer) => chunks.push(chunk)); + response.on("end", () => resolve(Buffer.concat(chunks))); + response.on("error", reject); + }, + ); + request.on("error", reject); + }); +} + +async function locatePayload(): Promise<{ heapOffset: number; length: number }> { + const headerAndToc = await httpsGetRange(UPDATE_URL, 0, 64 * 1024 - 1); + if (headerAndToc.subarray(0, 4).toString("latin1") !== "xar!") { + throw new Error("update package is not a xar archive"); + } + const tocLength = Number(headerAndToc.readBigUInt64BE(8)); + const toc = zlib + .inflateSync(headerAndToc.subarray(XAR_HEADER_SIZE, XAR_HEADER_SIZE + tocLength)) + .toString("utf8"); + + const blocks = toc + .split(/ /Payload<\/name>/.test(block)); + if (blocks.length !== 1) { + throw new Error("payload entry not found in xar TOC"); + } + const offsetMatch = blocks[0].match(/(0x[0-9a-f]+|\d+)<\/offset>/); + const lengthMatch = blocks[0].match(/(0x[0-9a-f]+|\d+)<\/length>/); + if (!offsetMatch || !lengthMatch) { + throw new Error("payload extent not found in xar TOC"); + } + return { + heapOffset: XAR_HEADER_SIZE + tocLength + Number(offsetMatch[1]), + length: Number(lengthMatch[1]), + }; +} + +/** A source stream that emits `prefix` before piping through `source`. */ +class PrependStream extends Readable { + private prefixPending: Buffer | null; + private sourcePiped = false; + + constructor( + prefix: Buffer, + private readonly source: Readable, + ) { + super(); + this.prefixPending = prefix; + } + + _read(): void { + if (this.prefixPending) { + const chunk = this.prefixPending; + this.prefixPending = null; + if (!this.push(chunk)) { + return; + } + } + if (!this.sourcePiped) { + this.sourcePiped = true; + this.source.on("data", (chunk: Buffer) => { + if (!this.push(chunk)) { + this.source.pause(); + } + }); + this.source.on("end", () => this.push(null)); + this.source.on("error", (error: Error) => this.destroy(error)); + return; + } + this.source.resume(); + } + + _destroy(error: Error | null, callback: (error?: Error | null) => void): void { + this.source.destroy(); + callback(error); + } +} + +/** Streams bytes [start, end) from the CDN in sequential range requests. */ +function rangeStream( + start: number, + end: number, + onChunk?: (bytes: number) => void, +): Readable { + const chunkSize = 16 << 20; + let offset = start; + let pending: Promise | null = null; + + return new Readable({ + async read() { + try { + for (;;) { + if (offset >= end) { + this.push(null); + return; + } + if (!pending) { + const from = offset; + const to = Math.min(from + chunkSize, end) - 1; + pending = httpsGetRange(UPDATE_URL, from, to).then((buffer) => { + offset = to + 1; + onChunk?.(buffer.length); + return buffer; + }); + } + const buffer = await pending; + pending = null; + if (!this.push(buffer)) { + return; + } + } + } catch (error) { + this.destroy(error instanceof Error ? error : new Error(String(error))); + } + }, + }); +} + +/** Drops the first `count` bytes of the stream. */ +class SkipStream extends Transform { + private skipped = 0; + + constructor(private readonly count: number) { + super(); + } + + _transform( + chunk: Buffer, + _encoding: string, + callback: (error?: Error | null, data?: Buffer) => void, + ): void { + if (this.skipped < this.count) { + const remaining = this.count - this.skipped; + if (chunk.length <= remaining) { + this.skipped += chunk.length; + callback(null); + return; + } + this.skipped = this.count; + callback(null, chunk.subarray(remaining)); + return; + } + callback(null, chunk); + } +} + +/** + * Parses cpio members (odc "070707" and newc "070701" formats) and captures + * the wanted files. macOS package payloads historically use the portable + * ASCII odc format: 76-byte headers, octal text fields, no padding. + */ +class CpioExtractor extends Transform { + private buffer: Buffer = Buffer.alloc(0); + private found = 0; + private format: "odc" | "newc" | null = null; + + constructor( + private readonly wanted: Map void>, + private readonly onAllFound: () => void, + ) { + super(); + } + + _transform( + chunk: Buffer, + _encoding: string, + callback: (error?: Error | null) => void, + ): void { + this.buffer = Buffer.concat([this.buffer, chunk]); + try { + while (this.parseOne()) { + if (this.found === this.wanted.size) { + this.onAllFound(); + this.push(null); + callback(null); + return; + } + } + callback(null); + } catch (error) { + callback(error instanceof Error ? error : new Error(String(error))); + } + } + + /** Parses one member if the buffer holds it completely. */ + private parseOne(): boolean { + if (this.buffer.length < 6) { + return false; + } + const magic = this.buffer.subarray(0, 6).toString("latin1"); + if (!this.format) { + if (magic === "070707") { + this.format = "odc"; + } else if (magic === "070701") { + this.format = "newc"; + } else { + throw new Error(`unexpected cpio magic: ${magic}`); + } + } + + if (this.format === "odc") { + return this.parseOdc(); + } + return this.parseNewc(); + } + + /** odc: 76-byte header, octal ASCII fields, name and data unpadded. */ + private parseOdc(): boolean { + const HEADER_SIZE = 76; + if (this.buffer.length < HEADER_SIZE) { + return false; + } + const octal = (start: number, length: number): number => + parseInt(this.buffer.subarray(start, start + length).toString("latin1"), 8); + + const nameSize = octal(59, 6); + const fileSize = octal(65, 11); + const nameEnd = HEADER_SIZE + nameSize - 1; + if (this.buffer.length < nameEnd + 1) { + return false; + } + const name = this.buffer.subarray(HEADER_SIZE, nameEnd).toString("latin1"); + const dataStart = HEADER_SIZE + nameSize; + const dataEnd = dataStart + fileSize; + if (this.buffer.length < dataEnd) { + return false; + } + + const handler = this.wanted.get(name); + if (handler) { + handler(this.buffer.subarray(dataStart, dataEnd)); + this.found += 1; + } + this.buffer = this.buffer.subarray(dataEnd); + return name !== "TRAILER!!!"; + } + + /** newc: 110-byte header, hex ASCII fields, 4-byte alignment. */ + private parseNewc(): boolean { + const HEADER_SIZE = 110; + if (this.buffer.length < HEADER_SIZE) { + return false; + } + const field = (offset: number): number => + parseInt( + this.buffer.subarray(offset, offset + 8).toString("latin1"), + 16, + ); + + const fileSize = field(54); + const nameSize = field(94); + const nameEnd = HEADER_SIZE + nameSize - 1; + if (this.buffer.length < nameEnd + 1) { + return false; + } + const name = this.buffer + .subarray(HEADER_SIZE, nameEnd) + .toString("latin1"); + const dataStart = Math.ceil((HEADER_SIZE + nameSize) / 4) * 4; + const dataEnd = dataStart + fileSize; + if (this.buffer.length < dataEnd) { + return false; + } + + const handler = this.wanted.get(name); + if (handler) { + handler(this.buffer.subarray(dataStart, dataEnd)); + this.found += 1; + } + this.buffer = this.buffer.subarray( + dataStart + Math.ceil(fileSize / 4) * 4, + ); + return name !== "TRAILER!!!"; + } +} diff --git a/frontend/scripts/unicorn-wasm-patch/build.sh b/frontend/scripts/unicorn-wasm-patch/build.sh new file mode 100755 index 00000000..d4c0d1bb --- /dev/null +++ b/frontend/scripts/unicorn-wasm-patch/build.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Builds the Unicorn TCI WASM engine used by the browser-side SAP signer. +# +# Unicorn 2.x only ships JIT TCG backends, which cannot run under WebAssembly. +# This script clones Unicorn 2.1.4, applies patches/unicorn-2.1.4-tci-wasm.patch +# (restores the QEMU 5.0 TCI interpreter, forces 64-bit virtual registers on +# wasm32, adds uniform-signature helper trampolines, and adapts glib- compat +# comparators for wasm's strict indirect-call checks), then cross-compiles with +# emscripten and links the JS glue into: +# ../src/apple/sap/vendor/unicorn.mjs + unicorn.wasm +# +# Requirements: docker (emscripten runs in a container). +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")" && pwd)" +VENDOR_DIR="$ROOT/../../src/apple/sap/vendor" +SRC="$ROOT/unicorn-src" +UC_VERSION="2.1.4" + +if [ ! -d "$SRC" ]; then + git clone --depth 1 --branch "$UC_VERSION" https://github.com/unicorn-engine/unicorn.git "$SRC" +fi + +if [ ! -f "$SRC/.tci-wasm-patched" ]; then + git -C "$SRC" apply "$ROOT/patches/unicorn-2.1.4-tci-wasm.patch" + touch "$SRC/.tci-wasm-patched" +fi + +mkdir -p "$VENDOR_DIR" + +docker run --rm -v "$ROOT:/work" -w /work emscripten/emsdk:3.1.61 bash -exc ' + apt-get update -qq >/dev/null 2>&1 || true + apt-get install -y -qq pkg-config >/dev/null 2>&1 || true + mkdir -p build dist && cd build + + emcmake cmake ../unicorn-src \ + -DCMAKE_BUILD_TYPE=Release \ + -DUNICORN_ARCH="x86" \ + -DCMAKE_C_FLAGS="-fPIC -O3" + + emmake make -j"$(nproc)" unicorn + + emcc ../glue.c -I../unicorn-src/include \ + libunicorn.a libunicorn-common.a libx86_64-softmmu.a \ + -o ../dist/unicorn.mjs \ + -O3 \ + -sMODULARIZE=1 \ + -sEXPORT_NAME=UnicornModule \ + -sALLOW_MEMORY_GROWTH=1 \ + -sMAXIMUM_MEMORY=4GB \ + -sINITIAL_MEMORY=64MB \ + -sALLOW_TABLE_GROWTH=1 \ + -sENVIRONMENT=web,worker,node \ + -sEXPORTED_FUNCTIONS=_uc2_open,_uc2_close,_uc2_strerror,_uc2_version,_uc2_mem_map,_uc2_mem_unmap,_uc2_mem_protect,_uc2_mem_write,_uc2_mem_read,_uc2_reg_write,_uc2_reg_read,_uc2_emu_start,_uc2_emu_stop,_uc2_hook_add_code,_uc2_hook_del,_uc2_set_code_hook_cb,_uc2_hook_add_mem_invalid,_uc2_set_mem_hook_cb,_uc2_scratch_alloc,_uc2_scratch_free,_malloc,_free \ + -sEXPORT_ES6=1 \ + -sEXPORTED_RUNTIME_METHODS=addFunction,removeFunction,getValue,setValue,HEAPU8,lengthBytesUTF,stringToUTF8,UTF8ToString \ + --no-entry +' + +cp dist/unicorn.mjs dist/unicorn.wasm "$VENDOR_DIR/" +echo "engine built into src/apple/sap/vendor/" diff --git a/frontend/scripts/unicorn-wasm-patch/glue.c b/frontend/scripts/unicorn-wasm-patch/glue.c new file mode 100644 index 00000000..10f515e8 --- /dev/null +++ b/frontend/scripts/unicorn-wasm-patch/glue.c @@ -0,0 +1,181 @@ +/* JS-friendly Unicorn 2.x glue for emscripten. + * + * All 64-bit guest addresses cross the JS boundary as `double` (lossless for + * integers < 2^53; SAP guest addresses stay below 2^48). Buffers are passed as + * wasm-heap offsets allocated by JS via uc2_scratch_alloc. + * + * The code hook is registered in C; the C thunk converts the uint64 address to + * double and forwards to a JS function pointer with signature (double, int), + * avoiding i64-at-the-JS-boundary entirely. + */ +#include +#include +#include +#include +#include + +typedef void (*js_hook_cb)(double address, int size); +typedef double (*js_mem_hook_cb)(double type, double address, double size, double value); + + +static js_hook_cb g_code_cb = NULL; + +static void code_hook_thunk(uc_engine *uc, uint64_t address, uint32_t size, void *user_data) +{ + (void)uc; + (void)user_data; + if (g_code_cb != NULL) { + g_code_cb((double)address, (int)size); + } +} + +double uc2_open(int arch, int mode) +{ + uc_engine *uc = NULL; + if (uc_open((uc_arch)arch, (uc_mode)mode, &uc) != UC_ERR_OK) { + return 0; + } + return (double)(uintptr_t)uc; +} + +int uc2_close(double uc) +{ + return uc_close((uc_engine *)(uintptr_t)uc); +} + +const char *uc2_strerror(int code) +{ + return uc_strerror((uc_err)code); +} + +int uc2_version(int *major, int *minor) +{ + return uc_version(major, minor); +} + +int uc2_mem_map(double uc, double address, double size) +{ + return uc_mem_map((uc_engine *)(uintptr_t)uc, (uint64_t)address, (size_t)size, UC_PROT_ALL); +} + +int uc2_mem_unmap(double uc, double address, double size) +{ + return uc_mem_unmap((uc_engine *)(uintptr_t)uc, (uint64_t)address, (size_t)size); +} + +int uc2_mem_protect(double uc, double address, double size, int perms) +{ + return uc_mem_protect((uc_engine *)(uintptr_t)uc, (uint64_t)address, (size_t)size, (uint32_t)perms); +} + +int uc2_mem_write(double uc, double address, int buffer_offset, int length) +{ + uint8_t *base = (uint8_t *)malloc(length); + if (base == NULL) { + return -99; + } + memcpy(base, (uint8_t *)(uintptr_t)buffer_offset, (size_t)length); + int rc = uc_mem_write((uc_engine *)(uintptr_t)uc, (uint64_t)address, base, (size_t)length); + free(base); + return rc; +} + +int uc2_mem_read(double uc, double address, int buffer_offset, int length) +{ + return uc_mem_read((uc_engine *)(uintptr_t)uc, (uint64_t)address, (uint8_t *)(uintptr_t)buffer_offset, (size_t)length); +} + +int uc2_reg_write(double uc, int regid, double value) +{ + uint64_t raw = (uint64_t)value; + return uc_reg_write((uc_engine *)(uintptr_t)uc, regid, &raw); +} + +double uc2_reg_read(double uc, int regid) +{ + uint64_t raw = 0; + if (uc_reg_read((uc_engine *)(uintptr_t)uc, regid, &raw) != UC_ERR_OK) { + return -1; + } + return (double)raw; +} + +extern double tci_wasm_deadline_ms; + +int uc2_emu_start(double uc, double begin, double until, double timeout_us, double count) +{ + if (timeout_us > 0) { + tci_wasm_deadline_ms = emscripten_get_now() + timeout_us / 1000.0; + } else { + tci_wasm_deadline_ms = 0; + } + return uc_emu_start((uc_engine *)(uintptr_t)uc, (uint64_t)begin, (uint64_t)until, (uint64_t)timeout_us, (size_t)count); +} + +int uc2_emu_stop(double uc) +{ + return uc_emu_stop((uc_engine *)(uintptr_t)uc); +} + +double uc2_hook_add_code(double uc, double begin, double end) +{ + uc_hook handle = 0; + if (uc_hook_add((uc_engine *)(uintptr_t)uc, &handle, UC_HOOK_CODE, (void *)code_hook_thunk, NULL, + (uint64_t)begin, (uint64_t)end) != UC_ERR_OK) { + return 0; + } + return (double)(uintptr_t)handle; +} + +int uc2_hook_del(double uc, double handle) +{ + return uc_hook_del((uc_engine *)(uintptr_t)uc, (uc_hook)(uintptr_t)handle); +} + +void uc2_set_code_hook_cb(int fp_index) +{ + g_code_cb = (js_hook_cb)(uintptr_t)fp_index; +} + +static js_mem_hook_cb g_mem_cb = NULL; + +static int mem_invalid_thunk(uc_engine *uc, uc_mem_type type, + uint64_t address, int size, int64_t value, + void *user_data) +{ + (void)uc; + (void)user_data; + if (g_mem_cb != NULL) { + return (int)g_mem_cb((double)type, (double)address, (double)size, (double)value); + } + return 0; +} + +double uc2_hook_add_mem_invalid(double uc) +{ + uc_hook handle = 0; + if (uc_hook_add((uc_engine *)(uintptr_t)uc, &handle, UC_HOOK_MEM_INVALID, + (void *)mem_invalid_thunk, NULL, 1, 0) != UC_ERR_OK) { + return 0; + } + return (double)(uintptr_t)handle; +} + +void uc2_set_mem_hook_cb(int fp_index) +{ + g_mem_cb = (js_mem_hook_cb)(uintptr_t)fp_index; +} + +int uc2_scratch_alloc(int length) +{ + void *block = malloc((size_t)length); + if (block == NULL) { + return 0; + } + return (int)(uintptr_t)block; +} + +void uc2_scratch_free(int offset) +{ + free((void *)(uintptr_t)offset); +} diff --git a/frontend/scripts/unicorn-wasm-patch/patches/unicorn-2.1.4-tci-wasm.patch b/frontend/scripts/unicorn-wasm-patch/patches/unicorn-2.1.4-tci-wasm.patch new file mode 100644 index 00000000..da5ce489 --- /dev/null +++ b/frontend/scripts/unicorn-wasm-patch/patches/unicorn-2.1.4-tci-wasm.patch @@ -0,0 +1,3085 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 033db09..4dbb92d 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -182,6 +182,10 @@ else() + elseif(UC_COMPILER_VERSION MATCHES "^aarch64.*") + set(UNICORN_TARGET_ARCH "aarch64") + endif() ++ elseif(EMSCRIPTEN) ++ # No native TCG backend can execute under WebAssembly; use the ++ # TCI interpreter backend restored from qemu 5.0.1. ++ set(UNICORN_TARGET_ARCH "tci") + elseif(ANDROID_ABI) + string(FIND "${ANDROID_ABI}" "arm64" UC_RET) + file(WRITE ${CMAKE_BINARY_DIR}/adb.sh "#!/bin/bash\n\n# Auto-generated by CMakeLists.txt\n\nadb shell mkdir -p /data/local/tmp/build\n") +@@ -380,6 +384,9 @@ else() + ${TARGET_LIST} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) ++ if(UNICORN_TARGET_ARCH STREQUAL "tci") ++ file(APPEND ${CMAKE_BINARY_DIR}/config-host.mak "CONFIG_TCG_INTERPRETER=y\n") ++ endif() + execute_process(COMMAND sh ${CMAKE_CURRENT_SOURCE_DIR}/qemu/scripts/create_config + INPUT_FILE ${CMAKE_BINARY_DIR}/config-host.mak + OUTPUT_FILE ${CMAKE_BINARY_DIR}/config-host.h +@@ -519,13 +526,19 @@ set(UNICORN_ARCH_COMMON + qemu/softmmu/unicorn_vtlb.c + ) + ++if(UNICORN_TARGET_ARCH STREQUAL "tci") ++ list(APPEND UNICORN_ARCH_COMMON qemu/tcg/tci.c) ++endif() ++ + if(UNICORN_HAS_X86) + add_library(x86_64-softmmu STATIC + ${UNICORN_ARCH_COMMON} + + qemu/hw/i386/x86.c + ++ + qemu/target/i386/arch_memory_mapping.c ++ qemu/target/i386/tci-wasm-tramp.c + qemu/target/i386/bpt_helper.c + qemu/target/i386/cc_helper.c + qemu/target/i386/cpu.c +diff --git a/glib_compat/gtree.c b/glib_compat/gtree.c +index b2617a3..b188693 100644 +--- a/glib_compat/gtree.c ++++ b/glib_compat/gtree.c +@@ -75,6 +75,18 @@ typedef struct _GTreeNode GTreeNode; + * [balanced binary tree][glib-Balanced-Binary-Trees]. It should be + * accessed only by using the following functions. + */ ++#ifdef __EMSCRIPTEN__ ++/* Adapt a 2-arg GCompareFunc to GCompareDataFunc; the real function pointer ++ rides in key_compare_data so it is invoked with its true wasm signature. ++ wasm enforces indirect-call signatures, so C's traditional cast of a ++ 2-argument comparator into a 3-argument GCompareDataFunc would trap. */ ++static gint gtree_passthrough_data_compare (gconstpointer a, gconstpointer b, ++ gpointer user_data) ++{ ++ return ((GCompareFunc) user_data) (a, b); ++} ++#endif ++ + struct _GTree + { + GTreeNode *root; +@@ -160,8 +172,13 @@ GTree *g_tree_new (GCompareFunc key_compare_func) + { + g_return_val_if_fail (key_compare_func != NULL, NULL); + ++#ifdef __EMSCRIPTEN__ ++ return g_tree_new_full (gtree_passthrough_data_compare, ++ (gpointer) key_compare_func, NULL, NULL); ++#else + return g_tree_new_full ((GCompareDataFunc) key_compare_func, NULL, + NULL, NULL); ++#endif + } + + /** +diff --git a/qemu/configure b/qemu/configure +index cfd344f..a537cae 100755 +--- a/qemu/configure ++++ b/qemu/configure +@@ -396,7 +396,10 @@ int main(void) { return 0; } + EOF + } + +-if check_define __linux__ ; then ++if echo "$cc" | grep -q "emcc"; then ++ # Emscripten no longer predefines __linux__; it models a POSIX/Linux host. ++ targetos="Linux" ++elif check_define __linux__ ; then + targetos="Linux" + elif check_define _WIN32 ; then + targetos='MINGW32' +@@ -848,8 +851,13 @@ case "$cpu" in + # ??? Only extremely old AMD cpus do not have cmpxchg16b. + # If we truly care, we should simply detect this case at + # runtime and generate the fallback to serial emulation. +- CPU_CFLAGS="-m64 -mcx16" +- QEMU_LDFLAGS="-m64 $QEMU_LDFLAGS" ++ if echo "$cc" | grep -q "emcc"; then ++ # wasm32 has no cmpxchg16b; the host TCG backend is TCI anyway. ++ CPU_CFLAGS="" ++ else ++ CPU_CFLAGS="-m64 -mcx16" ++ QEMU_LDFLAGS="-m64 $QEMU_LDFLAGS" ++ fi + ;; + loongarch*) + CPU_CFLAGS="" +diff --git a/qemu/include/exec/exec-all.h b/qemu/include/exec/exec-all.h +index 68c6567..e8dea74 100644 +--- a/qemu/include/exec/exec-all.h ++++ b/qemu/include/exec/exec-all.h +@@ -395,6 +395,9 @@ void tb_exec_unlock(struct uc_struct*); + #ifdef _MSC_VER + #include + # define GETPC() (uintptr_t)_ReturnAddress() ++#elif defined(CONFIG_TCG_INTERPRETER) ++extern uintptr_t tci_tb_ptr; ++# define GETPC() tci_tb_ptr + #else + # define GETPC() \ + ((uintptr_t)__builtin_extract_return_addr(__builtin_return_address(0))) +diff --git a/qemu/include/exec/helper-gen.h b/qemu/include/exec/helper-gen.h +index c77990f..d45712a 100644 +--- a/qemu/include/exec/helper-gen.h ++++ b/qemu/include/exec/helper-gen.h +@@ -6,10 +6,43 @@ + + #include "exec/helper-head.h" + ++#ifdef __EMSCRIPTEN__ ++/* ++ * wasm: TCI invokes helpers through a single cast signature, which wasm's ++ * strict indirect-call checks reject. Redirect every generated call to a ++ * uniform-signature trampoline (defined in target//tci-wasm-tramp.c). ++ * Pre-pass: declare trampolines for the whole helper list. Plain DEF_HELPER_N ++ * entries are aliases of the FLAGS variants defined in helper-head.h, so they ++ * delegate automatically and must not be touched here. ++ */ ++#define TCI_WASM_TRAMP_PROTO(name) \ ++ uint64_t glue(tci_wasm_tramp_, name)(uint64_t, uint64_t, uint64_t, \ ++ uint64_t, uint64_t, uint64_t) ++#define TCI_WASM_CALL_TARGET(name) ((void *)(uintptr_t)glue(tci_wasm_tramp_, name)) ++#define DEF_HELPER_FLAGS_0(name, flags, ret) TCI_WASM_TRAMP_PROTO(name); ++#define DEF_HELPER_FLAGS_1(name, flags, ret, t1) TCI_WASM_TRAMP_PROTO(name); ++#define DEF_HELPER_FLAGS_2(name, flags, ret, t1, t2) TCI_WASM_TRAMP_PROTO(name); ++#define DEF_HELPER_FLAGS_3(name, flags, ret, t1, t2, t3) TCI_WASM_TRAMP_PROTO(name); ++#define DEF_HELPER_FLAGS_4(name, flags, ret, t1, t2, t3, t4) TCI_WASM_TRAMP_PROTO(name); ++#define DEF_HELPER_FLAGS_5(name, flags, ret, t1, t2, t3, t4, t5) TCI_WASM_TRAMP_PROTO(name); ++#define DEF_HELPER_FLAGS_6(name, flags, ret, t1, t2, t3, t4, t5, t6) TCI_WASM_TRAMP_PROTO(name); ++#define DEF_HELPER_FLAGS_7(name, flags, ret, t1, t2, t3, t4, t5, t6, t7) TCI_WASM_TRAMP_PROTO(name); ++#include "helper.h" ++#include "accel/tcg/tcg-runtime.h" ++#undef DEF_HELPER_FLAGS_0 ++#undef DEF_HELPER_FLAGS_1 ++#undef DEF_HELPER_FLAGS_2 ++#undef DEF_HELPER_FLAGS_3 ++#undef DEF_HELPER_FLAGS_4 ++#undef DEF_HELPER_FLAGS_5 ++#undef DEF_HELPER_FLAGS_6 ++#undef DEF_HELPER_FLAGS_7 ++#endif /* __EMSCRIPTEN__ */ ++ + #define DEF_HELPER_FLAGS_0(name, flags, ret) \ + static inline void glue(gen_helper_, name)(TCGContext *tcg_ctx, dh_retvar_decl0(ret)) \ + { \ +- tcg_gen_callN(tcg_ctx, HELPER(name), dh_retvar(ret), 0, NULL); \ ++ tcg_gen_callN(tcg_ctx, TCI_WASM_CALL_TARGET(name), dh_retvar(ret), 0, NULL); \ + } + + #define DEF_HELPER_FLAGS_1(name, flags, ret, t1) \ +@@ -17,7 +50,7 @@ static inline void glue(gen_helper_, name)(TCGContext *tcg_ctx, dh_retvar_decl(r + dh_arg_decl(t1, 1)) \ + { \ + TCGTemp *args[1] = { dh_arg(t1, 1) }; \ +- tcg_gen_callN(tcg_ctx, HELPER(name), dh_retvar(ret), 1, args); \ ++ tcg_gen_callN(tcg_ctx, TCI_WASM_CALL_TARGET(name), dh_retvar(ret), 1, args); \ + } + + #define DEF_HELPER_FLAGS_2(name, flags, ret, t1, t2) \ +@@ -25,7 +58,7 @@ static inline void glue(gen_helper_, name)(TCGContext *tcg_ctx, dh_retvar_decl(r + dh_arg_decl(t1, 1), dh_arg_decl(t2, 2)) \ + { \ + TCGTemp *args[2] = { dh_arg(t1, 1), dh_arg(t2, 2) }; \ +- tcg_gen_callN(tcg_ctx, HELPER(name), dh_retvar(ret), 2, args); \ ++ tcg_gen_callN(tcg_ctx, TCI_WASM_CALL_TARGET(name), dh_retvar(ret), 2, args); \ + } + + #define DEF_HELPER_FLAGS_3(name, flags, ret, t1, t2, t3) \ +@@ -33,7 +66,7 @@ static inline void glue(gen_helper_, name)(TCGContext *tcg_ctx, dh_retvar_decl(r + dh_arg_decl(t1, 1), dh_arg_decl(t2, 2), dh_arg_decl(t3, 3)) \ + { \ + TCGTemp *args[3] = { dh_arg(t1, 1), dh_arg(t2, 2), dh_arg(t3, 3) }; \ +- tcg_gen_callN(tcg_ctx, HELPER(name), dh_retvar(ret), 3, args); \ ++ tcg_gen_callN(tcg_ctx, TCI_WASM_CALL_TARGET(name), dh_retvar(ret), 3, args); \ + } + + #define DEF_HELPER_FLAGS_4(name, flags, ret, t1, t2, t3, t4) \ +@@ -43,7 +76,7 @@ static inline void glue(gen_helper_, name)(TCGContext *tcg_ctx, dh_retvar_decl(r + { \ + TCGTemp *args[4] = { dh_arg(t1, 1), dh_arg(t2, 2), \ + dh_arg(t3, 3), dh_arg(t4, 4) }; \ +- tcg_gen_callN(tcg_ctx, HELPER(name), dh_retvar(ret), 4, args); \ ++ tcg_gen_callN(tcg_ctx, TCI_WASM_CALL_TARGET(name), dh_retvar(ret), 4, args); \ + } + + #define DEF_HELPER_FLAGS_5(name, flags, ret, t1, t2, t3, t4, t5) \ +@@ -53,7 +86,7 @@ static inline void glue(gen_helper_, name)(TCGContext *tcg_ctx, dh_retvar_decl(r + { \ + TCGTemp *args[5] = { dh_arg(t1, 1), dh_arg(t2, 2), dh_arg(t3, 3), \ + dh_arg(t4, 4), dh_arg(t5, 5) }; \ +- tcg_gen_callN(tcg_ctx, HELPER(name), dh_retvar(ret), 5, args); \ ++ tcg_gen_callN(tcg_ctx, TCI_WASM_CALL_TARGET(name), dh_retvar(ret), 5, args); \ + } + + #define DEF_HELPER_FLAGS_6(name, flags, ret, t1, t2, t3, t4, t5, t6) \ +@@ -63,7 +96,7 @@ static inline void glue(gen_helper_, name)(TCGContext *tcg_ctx, dh_retvar_decl(r + { \ + TCGTemp *args[6] = { dh_arg(t1, 1), dh_arg(t2, 2), dh_arg(t3, 3), \ + dh_arg(t4, 4), dh_arg(t5, 5), dh_arg(t6, 6) }; \ +- tcg_gen_callN(tcg_ctx, HELPER(name), dh_retvar(ret), 6, args); \ ++ tcg_gen_callN(tcg_ctx, TCI_WASM_CALL_TARGET(name), dh_retvar(ret), 6, args); \ + } + + #define DEF_HELPER_FLAGS_7(name, flags, ret, t1, t2, t3, t4, t5, t6, t7)\ +@@ -75,7 +108,7 @@ static inline void glue(gen_helper_, name)(TCGContext *tcg_ctx, dh_retvar_decl(r + TCGTemp *args[7] = { dh_arg(t1, 1), dh_arg(t2, 2), dh_arg(t3, 3), \ + dh_arg(t4, 4), dh_arg(t5, 5), dh_arg(t6, 6), \ + dh_arg(t7, 7) }; \ +- tcg_gen_callN(tcg_ctx, HELPER(name), dh_retvar(ret), 7, args); \ ++ tcg_gen_callN(tcg_ctx, TCI_WASM_CALL_TARGET(name), dh_retvar(ret), 7, args); \ + } + + #include "helper.h" +diff --git a/qemu/include/qemu/int128.h b/qemu/include/qemu/int128.h +index d1d1ad4..0654706 100644 +--- a/qemu/include/qemu/int128.h ++++ b/qemu/include/qemu/int128.h +@@ -146,7 +146,7 @@ static inline Int128 bswap128(Int128 a) + #else /* !CONFIG_INT128 */ + + typedef struct Int128 Int128; +-#if !(defined(_MSC_VER) && defined(__clang__)) ++#if !(defined(_MSC_VER) || defined(__clang__)) + typedef Int128 __int128_t; + #endif + +diff --git a/qemu/include/tcg/tcg-op.h b/qemu/include/tcg/tcg-op.h +index 93026d1..63a2957 100644 +--- a/qemu/include/tcg/tcg-op.h ++++ b/qemu/include/tcg/tcg-op.h +@@ -47,7 +47,13 @@ static inline void gen_uc_tracecode(TCGContext *tcg_ctx, int32_t size, int32_t t + }; + + const int hook_type = type & UC_HOOK_IDX_MASK; ++#ifdef __EMSCRIPTEN__ ++ /* wasm cannot inline arbitrary C callbacks as TCG helpers (TCI calls ++ them through a uniform signature); always use the tracecode helper. */ ++ if (0) { ++#else + if (puc->hooks_count[hook_type] == 1 && !(type & UC_HOOK_FLAG_NO_STOP)) { ++#endif + cur = puc->hook[hook_type].head; + + while (cur) { +diff --git a/qemu/target/i386/tci-wasm-tramp.c b/qemu/target/i386/tci-wasm-tramp.c +new file mode 100644 +index 0000000..c70aa68 +--- /dev/null ++++ b/qemu/target/i386/tci-wasm-tramp.c +@@ -0,0 +1,134 @@ ++/* ++ * Uniform-signature trampolines for TCI helpers under WebAssembly. ++ * ++ * With TCI running 64-bit virtual registers (see tcg/tci/tcg-target.h), the ++ * interpreter invokes every helper through uint64_t (*)(uint64_t x6). Native ++ * ABIs tolerate TCI's cast; wasm's strict indirect-call checks do not, so each ++ * helper called from generated code goes through a trampoline with exactly ++ * that signature. Each helper argument occupies exactly one register slot in ++ * this mode, so trampolins simply forward a0..a5 with the proper casts. ++ */ ++#include "qemu/osdep.h" ++#include "cpu.h" ++ ++/* Real helper prototypes first: helper-proto.h declares them and undefines ++ its DEF_HELPER macros afterwards. */ ++#include "exec/helper-proto.h" ++ ++/* Argument extraction: one u64 register per parameter in 64-bit TCI. */ ++#define TW_A_env a0 ++#define TW_A_ptr a0 ++#define TW_A_Reg a0 ++#define TW_A_ZMMReg a0 ++#define TW_A_MMXReg a0 ++#define TW_A_cptr a0 ++#define TW_A_int a0 ++#define TW_A_i32 a0 ++#define TW_A_s32 a0 ++#define TW_A_i64 a0 ++#define TW_A_s64 a0 ++#define TW_A_tl a0 ++ ++/* Final cast of the register value to the real parameter type. */ ++#define TW_C_env(p) ((CPUArchState *)(uintptr_t)(p)) ++#define TW_C_ptr(p) ((void *)(uintptr_t)(p)) ++#define TW_C_Reg(p) ((Reg *)(uintptr_t)(p)) ++#define TW_C_ZMMReg(p) ((ZMMReg *)(uintptr_t)(p)) ++#define TW_C_MMXReg(p) ((MMXReg *)(uintptr_t)(p)) ++#define TW_C_cptr(p) ((const void *)(uintptr_t)(p)) ++#define TW_C_int(p) ((int)(uint32_t)(p)) ++#define TW_C_i32(p) ((int32_t)(uint32_t)(p)) ++#define TW_C_s32(p) ((int32_t)(uint32_t)(p)) ++#define TW_C_i64(p) ((uint64_t)(p)) ++#define TW_C_s64(p) ((int64_t)(p)) ++#define TW_C_tl(p) ((target_ulong)(p)) ++ ++/* Return widening. */ ++#define TW_R_void(e) ((void)(e), (uint64_t)0) ++#define TW_R_int(e) ((uint64_t)(uint32_t)(e)) ++#define TW_R_i32(e) ((uint64_t)(uint32_t)(e)) ++#define TW_R_s32(e) ((uint64_t)(uint32_t)(e)) ++#define TW_R_i64(e) ((uint64_t)(e)) ++#define TW_R_s64(e) ((uint64_t)(e)) ++#define TW_R_tl(e) ((uint64_t)(e)) ++#define TW_R_ptr(e) ((uint64_t)(uintptr_t)(e)) ++#define TW_R_noreturn(e) ((void)(e), (uint64_t)0) ++ ++#define TW_PARAMS uint64_t a0, uint64_t a1, uint64_t a2, \ ++ uint64_t a3, uint64_t a4, uint64_t a5 ++ ++#define DEF_HELPER_FLAGS_0(name, flags, ret) \ ++uint64_t glue(tci_wasm_tramp_, name)(TW_PARAMS) \ ++{ \ ++ (void)a0; (void)a1; (void)a2; (void)a3; (void)a4; (void)a5; \ ++ return TW_R_##ret(glue(helper_, name)()); \ ++} ++ ++#define DEF_HELPER_FLAGS_1(name, flags, ret, t1) \ ++uint64_t glue(tci_wasm_tramp_, name)(TW_PARAMS) \ ++{ \ ++ (void)a1; (void)a2; (void)a3; (void)a4; (void)a5; \ ++ return TW_R_##ret(glue(helper_, name)(TW_C_##t1(a0))); \ ++} ++ ++#define DEF_HELPER_FLAGS_2(name, flags, ret, t1, t2) \ ++uint64_t glue(tci_wasm_tramp_, name)(TW_PARAMS) \ ++{ \ ++ (void)a2; (void)a3; (void)a4; (void)a5; \ ++ return TW_R_##ret(glue(helper_, name)(TW_C_##t1(a0), \ ++ TW_C_##t2(a1))); \ ++} ++ ++#define DEF_HELPER_FLAGS_3(name, flags, ret, t1, t2, t3) \ ++uint64_t glue(tci_wasm_tramp_, name)(TW_PARAMS) \ ++{ \ ++ (void)a3; (void)a4; (void)a5; \ ++ return TW_R_##ret(glue(helper_, name)(TW_C_##t1(a0), \ ++ TW_C_##t2(a1), \ ++ TW_C_##t3(a2))); \ ++} ++ ++#define DEF_HELPER_FLAGS_4(name, flags, ret, t1, t2, t3, t4) \ ++uint64_t glue(tci_wasm_tramp_, name)(TW_PARAMS) \ ++{ \ ++ (void)a4; (void)a5; \ ++ return TW_R_##ret(glue(helper_, name)(TW_C_##t1(a0), \ ++ TW_C_##t2(a1), \ ++ TW_C_##t3(a2), \ ++ TW_C_##t4(a3))); \ ++} ++ ++#define DEF_HELPER_FLAGS_5(name, flags, ret, t1, t2, t3, t4, t5) \ ++uint64_t glue(tci_wasm_tramp_, name)(TW_PARAMS) \ ++{ \ ++ (void)a5; \ ++ return TW_R_##ret(glue(helper_, name)(TW_C_##t1(a0), \ ++ TW_C_##t2(a1), \ ++ TW_C_##t3(a2), \ ++ TW_C_##t4(a3), \ ++ TW_C_##t5(a4))); \ ++} ++ ++#define DEF_HELPER_FLAGS_6(name, flags, ret, t1, t2, t3, t4, t5, t6) \ ++uint64_t glue(tci_wasm_tramp_, name)(TW_PARAMS) \ ++{ \ ++ return TW_R_##ret(glue(helper_, name)(TW_C_##t1(a0), \ ++ TW_C_##t2(a1), \ ++ TW_C_##t3(a2), \ ++ TW_C_##t4(a3), \ ++ TW_C_##t5(a4), \ ++ TW_C_##t6(a5))); \ ++} ++ ++#define DEF_HELPER_0(name, ret) DEF_HELPER_FLAGS_0(name, 0, ret) ++#define DEF_HELPER_1(name, ret, t1) DEF_HELPER_FLAGS_1(name, 0, ret, t1) ++#define DEF_HELPER_2(name, ret, t1, t2) DEF_HELPER_FLAGS_2(name, 0, ret, t1, t2) ++#define DEF_HELPER_3(name, ret, t1, t2, t3) DEF_HELPER_FLAGS_3(name, 0, ret, t1, t2, t3) ++#define DEF_HELPER_4(name, ret, t1, t2, t3, t4) DEF_HELPER_FLAGS_4(name, 0, ret, t1, t2, t3, t4) ++#define DEF_HELPER_5(name, ret, t1, t2, t3, t4, t5) DEF_HELPER_FLAGS_5(name, 0, ret, t1, t2, t3, t4, t5) ++#define DEF_HELPER_6(name, ret, t1, t2, t3, t4, t5, t6) DEF_HELPER_FLAGS_6(name, 0, ret, t1, t2, t3, t4, t5, t6) ++ ++/* Regenerate the full helper list (target helpers + TCG runtime helpers), ++ this time as trampolines. */ ++#include "helper.h" ++#include "accel/tcg/tcg-runtime.h" +diff --git a/qemu/tcg/tci/README b/qemu/tcg/tci/README +new file mode 100644 +index 0000000..386c3c7 +--- /dev/null ++++ b/qemu/tcg/tci/README +@@ -0,0 +1,130 @@ ++TCG Interpreter (TCI) - Copyright (c) 2011 Stefan Weil. ++ ++This file is released under the BSD license. ++ ++1) Introduction ++ ++TCG (Tiny Code Generator) is a code generator which translates ++code fragments ("basic blocks") from target code (any of the ++targets supported by QEMU) to a code representation which ++can be run on a host. ++ ++QEMU can create native code for some hosts (arm, i386, ia64, ppc, ppc64, ++s390, sparc, x86_64). For others, unofficial host support was written. ++ ++By adding a code generator for a virtual machine and using an ++interpreter for the generated bytecode, it is possible to ++support (almost) any host. ++ ++This is what TCI (Tiny Code Interpreter) does. ++ ++2) Implementation ++ ++Like each TCG host frontend, TCI implements the code generator in ++tcg-target.inc.c, tcg-target.h. Both files are in directory tcg/tci. ++ ++The additional file tcg/tci.c adds the interpreter. ++ ++The bytecode consists of opcodes (same numeric values as those used by ++TCG), command length and arguments of variable size and number. ++ ++3) Usage ++ ++For hosts without native TCG, the interpreter TCI must be enabled by ++ ++ configure --enable-tcg-interpreter ++ ++If configure is called without --enable-tcg-interpreter, it will ++suggest using this option. Setting it automatically would need ++additional code in configure which must be fixed when new native TCG ++implementations are added. ++ ++System emulation should work on any 32 or 64 bit host. ++User mode emulation might work. Maybe a new linker script (*.ld) ++is needed. Byte order might be wrong (on big endian hosts) ++and need fixes in configure. ++ ++For hosts with native TCG, the interpreter TCI can be enabled by ++ ++ configure --enable-tcg-interpreter ++ ++The only difference from running QEMU with TCI to running without TCI ++should be speed. Especially during development of TCI, it was very ++useful to compare runs with and without TCI. Create /tmp/qemu.log by ++ ++ qemu-system-i386 -d in_asm,op_opt,cpu -D /tmp/qemu.log -singlestep ++ ++once with interpreter and once without interpreter and compare the resulting ++qemu.log files. This is also useful to see the effects of additional ++registers or additional opcodes (it is easy to modify the virtual machine). ++It can also be used to verify native TCGs. ++ ++Hosts with native TCG can also enable TCI by claiming to be unsupported: ++ ++ configure --cpu=unknown --enable-tcg-interpreter ++ ++configure then no longer uses the native linker script (*.ld) for ++user mode emulation. ++ ++ ++4) Status ++ ++TCI needs special implementation for 32 and 64 bit host, 32 and 64 bit target, ++host and target with same or different endianness. ++ ++ | host (le) host (be) ++ | 32 64 32 64 ++------------+------------------------------------------------------------ ++target (le) | s0, u0 s1, u1 s?, u? s?, u? ++32 bit | ++ | ++target (le) | sc, uc s1, u1 s?, u? s?, u? ++64 bit | ++ | ++target (be) | sc, u0 sc, uc s?, u? s?, u? ++32 bit | ++ | ++target (be) | sc, uc sc, uc s?, u? s?, u? ++64 bit | ++ | ++ ++System emulation ++s? = untested ++sc = compiles ++s0 = bios works ++s1 = grub works ++s2 = Linux boots ++ ++Linux user mode emulation ++u? = untested ++uc = compiles ++u0 = static hello works ++u1 = linux-user-test works ++ ++5) Todo list ++ ++* TCI is not widely tested. It was written and tested on a x86_64 host ++ running i386 and x86_64 system emulation and Linux user mode. ++ A cross compiled QEMU for i386 host also works with the same basic tests. ++ A cross compiled QEMU for mipsel host works, too. It is terribly slow ++ because I run it in a mips malta emulation, so it is an interpreted ++ emulation in an emulation. ++ A cross compiled QEMU for arm host works (tested with pc bios). ++ A cross compiled QEMU for ppc host works at least partially: ++ i386-linux-user/qemu-i386 can run a simple hello-world program ++ (tested in a ppc emulation). ++ ++* Some TCG opcodes are either missing in the code generator and/or ++ in the interpreter. These opcodes raise a runtime exception, so it is ++ possible to see where code must be added. ++ ++* The pseudo code is not optimized and still ugly. For hosts with special ++ alignment requirements, it needs some fixes (maybe aligned bytecode ++ would also improve speed for hosts which support byte alignment). ++ ++* A better disassembler for the pseudo code would be nice (a very primitive ++ disassembler is included in tcg-target.inc.c). ++ ++* It might be useful to have a runtime option which selects the native TCG ++ or TCI, so QEMU would have to include two TCGs. Today, selecting TCI ++ is a configure option, so you need two compilations of QEMU. +diff --git a/qemu/tcg/tci/tcg-target.h b/qemu/tcg/tci/tcg-target.h +new file mode 100644 +index 0000000..93cbaef +--- /dev/null ++++ b/qemu/tcg/tci/tcg-target.h +@@ -0,0 +1,221 @@ ++/* ++ * Tiny Code Generator for QEMU ++ * ++ * Copyright (c) 2009, 2011 Stefan Weil ++ * ++ * Permission is hereby granted, free of charge, to any person obtaining a copy ++ * of this software and associated documentation files (the "Software"), to deal ++ * in the Software without restriction, including without limitation the rights ++ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ++ * copies of the Software, and to permit persons to whom the Software is ++ * furnished to do so, subject to the following conditions: ++ * ++ * The above copyright notice and this permission notice shall be included in ++ * all copies or substantial portions of the Software. ++ * ++ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ++ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ++ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL ++ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ++ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ++ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN ++ * THE SOFTWARE. ++ */ ++ ++/* ++ * This code implements a TCG which does not generate machine code for some ++ * real target machine but which generates virtual machine code for an ++ * interpreter. Interpreted pseudo code is slow, but it works on any host. ++ * ++ * Some remarks might help in understanding the code: ++ * ++ * "target" or "TCG target" is the machine which runs the generated code. ++ * This is different to the usual meaning in QEMU where "target" is the ++ * emulated machine. So normally QEMU host is identical to TCG target. ++ * Here the TCG target is a virtual machine, but this virtual machine must ++ * use the same word size like the real machine. ++ * Therefore, we need both 32 and 64 bit virtual machines (interpreter). ++ */ ++ ++#ifndef TCG_TARGET_H ++#define TCG_TARGET_H ++ ++#define TCG_TARGET_INTERPRETER 1 ++#define TCG_TARGET_INSN_UNIT_SIZE 1 ++#define TCG_TARGET_TLB_DISPLACEMENT_BITS 32 ++ ++#ifdef __EMSCRIPTEN__ ++/* ++ * wasm32 has 32-bit pointers, but qemu 5.0's 32-bit TCI path is incomplete ++ * (TODO() stubs for i64 stores etc.). TCI's register file is virtual state, ++ * so run the interpreter with 64-bit registers regardless of pointer size; ++ * guest addresses fit in u64 and helper calls marshal through trampolines. ++ */ ++# define TCG_TARGET_REG_BITS 64 ++#elif UINTPTR_MAX == UINT32_MAX ++# define TCG_TARGET_REG_BITS 32 ++#elif UINTPTR_MAX == UINT64_MAX ++# define TCG_TARGET_REG_BITS 64 ++#else ++# error Unknown pointer size for tci target ++#endif ++ ++#ifdef CONFIG_DEBUG_TCG ++/* Enable debug output. */ ++#define CONFIG_DEBUG_TCG_INTERPRETER ++#endif ++ ++/* Optional instructions. */ ++ ++#define TCG_TARGET_HAS_bswap16_i32 1 ++#define TCG_TARGET_HAS_bswap32_i32 1 ++#define TCG_TARGET_HAS_div_i32 1 ++#define TCG_TARGET_HAS_rem_i32 1 ++#define TCG_TARGET_HAS_ext8s_i32 1 ++#define TCG_TARGET_HAS_ext16s_i32 1 ++#define TCG_TARGET_HAS_ext8u_i32 1 ++#define TCG_TARGET_HAS_ext16u_i32 1 ++#define TCG_TARGET_HAS_andc_i32 0 ++#define TCG_TARGET_HAS_deposit_i32 1 ++#define TCG_TARGET_HAS_extract_i32 0 ++#define TCG_TARGET_HAS_sextract_i32 0 ++#define TCG_TARGET_HAS_extract2_i32 0 ++#define TCG_TARGET_HAS_eqv_i32 0 ++#define TCG_TARGET_HAS_nand_i32 0 ++#define TCG_TARGET_HAS_nor_i32 0 ++#define TCG_TARGET_HAS_clz_i32 0 ++#define TCG_TARGET_HAS_ctz_i32 0 ++#define TCG_TARGET_HAS_ctpop_i32 0 ++#define TCG_TARGET_HAS_neg_i32 1 ++#define TCG_TARGET_HAS_not_i32 1 ++#define TCG_TARGET_HAS_orc_i32 0 ++#define TCG_TARGET_HAS_rot_i32 1 ++#define TCG_TARGET_HAS_movcond_i32 0 ++#define TCG_TARGET_HAS_muls2_i32 0 ++#define TCG_TARGET_HAS_muluh_i32 0 ++#define TCG_TARGET_HAS_mulsh_i32 0 ++#define TCG_TARGET_HAS_goto_ptr 0 ++#define TCG_TARGET_HAS_direct_jump 1 ++ ++#if TCG_TARGET_REG_BITS == 64 ++#define TCG_TARGET_HAS_extrl_i64_i32 0 ++#define TCG_TARGET_HAS_extrh_i64_i32 0 ++#define TCG_TARGET_HAS_bswap16_i64 1 ++#define TCG_TARGET_HAS_bswap32_i64 1 ++#define TCG_TARGET_HAS_bswap64_i64 1 ++#define TCG_TARGET_HAS_deposit_i64 1 ++#define TCG_TARGET_HAS_extract_i64 0 ++#define TCG_TARGET_HAS_sextract_i64 0 ++#define TCG_TARGET_HAS_extract2_i64 0 ++#define TCG_TARGET_HAS_div_i64 0 ++#define TCG_TARGET_HAS_rem_i64 0 ++#define TCG_TARGET_HAS_ext8s_i64 1 ++#define TCG_TARGET_HAS_ext16s_i64 1 ++#define TCG_TARGET_HAS_ext32s_i64 1 ++#define TCG_TARGET_HAS_ext8u_i64 1 ++#define TCG_TARGET_HAS_ext16u_i64 1 ++#define TCG_TARGET_HAS_ext32u_i64 1 ++#define TCG_TARGET_HAS_andc_i64 0 ++#define TCG_TARGET_HAS_eqv_i64 0 ++#define TCG_TARGET_HAS_nand_i64 0 ++#define TCG_TARGET_HAS_nor_i64 0 ++#define TCG_TARGET_HAS_clz_i64 0 ++#define TCG_TARGET_HAS_ctz_i64 0 ++#define TCG_TARGET_HAS_ctpop_i64 0 ++#define TCG_TARGET_HAS_neg_i64 1 ++#define TCG_TARGET_HAS_not_i64 1 ++#define TCG_TARGET_HAS_orc_i64 0 ++#define TCG_TARGET_HAS_rot_i64 1 ++#define TCG_TARGET_HAS_movcond_i64 0 ++#define TCG_TARGET_HAS_muls2_i64 0 ++#define TCG_TARGET_HAS_add2_i32 0 ++#define TCG_TARGET_HAS_sub2_i32 0 ++#define TCG_TARGET_HAS_mulu2_i32 0 ++#define TCG_TARGET_HAS_add2_i64 0 ++#define TCG_TARGET_HAS_sub2_i64 0 ++#define TCG_TARGET_HAS_mulu2_i64 0 ++#define TCG_TARGET_HAS_muluh_i64 0 ++#define TCG_TARGET_HAS_mulsh_i64 0 ++#else ++#define TCG_TARGET_HAS_mulu2_i32 1 ++#endif /* TCG_TARGET_REG_BITS == 64 */ ++ ++/* Number of registers available. ++ For 32 bit hosts, we need more than 8 registers (call arguments). */ ++/* #define TCG_TARGET_NB_REGS 8 */ ++#define TCG_TARGET_NB_REGS 16 ++/* #define TCG_TARGET_NB_REGS 32 */ ++ ++/* List of registers which are used by TCG. */ ++typedef enum { ++ TCG_REG_R0 = 0, ++ TCG_REG_R1, ++ TCG_REG_R2, ++ TCG_REG_R3, ++ TCG_REG_R4, ++ TCG_REG_R5, ++ TCG_REG_R6, ++ TCG_REG_R7, ++#if TCG_TARGET_NB_REGS >= 16 ++ TCG_REG_R8, ++ TCG_REG_R9, ++ TCG_REG_R10, ++ TCG_REG_R11, ++ TCG_REG_R12, ++ TCG_REG_R13, ++ TCG_REG_R14, ++ TCG_REG_R15, ++#if TCG_TARGET_NB_REGS >= 32 ++ TCG_REG_R16, ++ TCG_REG_R17, ++ TCG_REG_R18, ++ TCG_REG_R19, ++ TCG_REG_R20, ++ TCG_REG_R21, ++ TCG_REG_R22, ++ TCG_REG_R23, ++ TCG_REG_R24, ++ TCG_REG_R25, ++ TCG_REG_R26, ++ TCG_REG_R27, ++ TCG_REG_R28, ++ TCG_REG_R29, ++ TCG_REG_R30, ++ TCG_REG_R31, ++#endif ++#endif ++ /* Special value UINT8_MAX is used by TCI to encode constant values. */ ++ TCG_CONST = UINT8_MAX ++} TCGReg; ++ ++#define TCG_AREG0 (TCG_TARGET_NB_REGS - 2) ++ ++/* Used for function call generation. */ ++#define TCG_REG_CALL_STACK (TCG_TARGET_NB_REGS - 1) ++#define TCG_TARGET_CALL_STACK_OFFSET 0 ++#define TCG_TARGET_STACK_ALIGN 16 ++ ++void tci_disas(uint8_t opc); ++ ++#define HAVE_TCG_QEMU_TB_EXEC ++ ++static inline void flush_icache_range(uintptr_t start, uintptr_t stop) ++{ ++} ++ ++/* We could notice __i386__ or __s390x__ and reduce the barriers depending ++ on the host. But if you want performance, you use the normal backend. ++ We prefer consistency across hosts on this. */ ++#define TCG_TARGET_DEFAULT_MO (0) ++ ++#define TCG_TARGET_HAS_MEMORY_BSWAP 1 ++ ++static inline void tb_target_set_jmp_target(uintptr_t tc_ptr, ++ uintptr_t jmp_addr, uintptr_t addr) ++{ ++ /* patch the branch destination */ ++ atomic_set((int32_t *)jmp_addr, addr - (jmp_addr + 4)); ++ /* no need to flush icache explicitly */ ++} ++ ++#endif /* TCG_TARGET_H */ +diff --git a/qemu/tcg/tci/tcg-target.inc.c b/qemu/tcg/tci/tcg-target.inc.c +new file mode 100644 +index 0000000..4d6fe6a +--- /dev/null ++++ b/qemu/tcg/tci/tcg-target.inc.c +@@ -0,0 +1,897 @@ ++/* ++ * Tiny Code Generator for QEMU ++ * ++ * Copyright (c) 2009, 2011 Stefan Weil ++ * ++ * Permission is hereby granted, free of charge, to any person obtaining a copy ++ * of this software and associated documentation files (the "Software"), to deal ++ * in the Software without restriction, including without limitation the rights ++ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ++ * copies of the Software, and to permit persons to whom the Software is ++ * furnished to do so, subject to the following conditions: ++ * ++ * The above copyright notice and this permission notice shall be included in ++ * all copies or substantial portions of the Software. ++ * ++ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ++ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ++ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL ++ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ++ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ++ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN ++ * THE SOFTWARE. ++ */ ++ ++/* TODO list: ++ * - See TODO comments in code. ++ */ ++ ++/* Marker for missing code. */ ++#define TODO() \ ++ do { \ ++ fprintf(stderr, "TODO %s:%u: %s()\n", \ ++ __FILE__, __LINE__, __func__); \ ++ tcg_abort(); \ ++ } while (0) ++ ++/* Bitfield n...m (in 32 bit value). */ ++#define BITS(n, m) (((0xffffffffU << (31 - n)) >> (31 - n + m)) << m) ++ ++/* Macros used in tcg_target_op_defs. */ ++#define R "r" ++#define RI "ri" ++#if TCG_TARGET_REG_BITS == 32 ++# define R64 "r", "r" ++#else ++# define R64 "r" ++#endif ++#if TARGET_LONG_BITS > TCG_TARGET_REG_BITS ++# define L "L", "L" ++# define S "S", "S" ++#else ++# define L "L" ++# define S "S" ++#endif ++ ++/* TODO: documentation. */ ++static const TCGTargetOpDef tcg_target_op_defs[] = { ++ { INDEX_op_exit_tb, { NULL } }, ++ { INDEX_op_goto_tb, { NULL } }, ++ { INDEX_op_br, { NULL } }, ++ ++ { INDEX_op_ld8u_i32, { R, R } }, ++ { INDEX_op_ld8s_i32, { R, R } }, ++ { INDEX_op_ld16u_i32, { R, R } }, ++ { INDEX_op_ld16s_i32, { R, R } }, ++ { INDEX_op_ld_i32, { R, R } }, ++ { INDEX_op_st8_i32, { R, R } }, ++ { INDEX_op_st16_i32, { R, R } }, ++ { INDEX_op_st_i32, { R, R } }, ++ ++ { INDEX_op_add_i32, { R, RI, RI } }, ++ { INDEX_op_sub_i32, { R, RI, RI } }, ++ { INDEX_op_mul_i32, { R, RI, RI } }, ++#if TCG_TARGET_HAS_div_i32 ++ { INDEX_op_div_i32, { R, R, R } }, ++ { INDEX_op_divu_i32, { R, R, R } }, ++ { INDEX_op_rem_i32, { R, R, R } }, ++ { INDEX_op_remu_i32, { R, R, R } }, ++#elif TCG_TARGET_HAS_div2_i32 ++ { INDEX_op_div2_i32, { R, R, "0", "1", R } }, ++ { INDEX_op_divu2_i32, { R, R, "0", "1", R } }, ++#endif ++ /* TODO: Does R, RI, RI result in faster code than R, R, RI? ++ If both operands are constants, we can optimize. */ ++ { INDEX_op_and_i32, { R, RI, RI } }, ++#if TCG_TARGET_HAS_andc_i32 ++ { INDEX_op_andc_i32, { R, RI, RI } }, ++#endif ++#if TCG_TARGET_HAS_eqv_i32 ++ { INDEX_op_eqv_i32, { R, RI, RI } }, ++#endif ++#if TCG_TARGET_HAS_nand_i32 ++ { INDEX_op_nand_i32, { R, RI, RI } }, ++#endif ++#if TCG_TARGET_HAS_nor_i32 ++ { INDEX_op_nor_i32, { R, RI, RI } }, ++#endif ++ { INDEX_op_or_i32, { R, RI, RI } }, ++#if TCG_TARGET_HAS_orc_i32 ++ { INDEX_op_orc_i32, { R, RI, RI } }, ++#endif ++ { INDEX_op_xor_i32, { R, RI, RI } }, ++ { INDEX_op_shl_i32, { R, RI, RI } }, ++ { INDEX_op_shr_i32, { R, RI, RI } }, ++ { INDEX_op_sar_i32, { R, RI, RI } }, ++#if TCG_TARGET_HAS_rot_i32 ++ { INDEX_op_rotl_i32, { R, RI, RI } }, ++ { INDEX_op_rotr_i32, { R, RI, RI } }, ++#endif ++#if TCG_TARGET_HAS_deposit_i32 ++ { INDEX_op_deposit_i32, { R, "0", R } }, ++#endif ++ ++ { INDEX_op_brcond_i32, { R, RI } }, ++ ++ { INDEX_op_setcond_i32, { R, R, RI } }, ++#if TCG_TARGET_REG_BITS == 64 ++ { INDEX_op_setcond_i64, { R, R, RI } }, ++#endif /* TCG_TARGET_REG_BITS == 64 */ ++ ++#if TCG_TARGET_REG_BITS == 32 ++ /* TODO: Support R, R, R, R, RI, RI? Will it be faster? */ ++ { INDEX_op_add2_i32, { R, R, R, R, R, R } }, ++ { INDEX_op_sub2_i32, { R, R, R, R, R, R } }, ++ { INDEX_op_brcond2_i32, { R, R, RI, RI } }, ++ { INDEX_op_mulu2_i32, { R, R, R, R } }, ++ { INDEX_op_setcond2_i32, { R, R, R, RI, RI } }, ++#endif ++ ++#if TCG_TARGET_HAS_not_i32 ++ { INDEX_op_not_i32, { R, R } }, ++#endif ++#if TCG_TARGET_HAS_neg_i32 ++ { INDEX_op_neg_i32, { R, R } }, ++#endif ++ ++#if TCG_TARGET_REG_BITS == 64 ++ { INDEX_op_ld8u_i64, { R, R } }, ++ { INDEX_op_ld8s_i64, { R, R } }, ++ { INDEX_op_ld16u_i64, { R, R } }, ++ { INDEX_op_ld16s_i64, { R, R } }, ++ { INDEX_op_ld32u_i64, { R, R } }, ++ { INDEX_op_ld32s_i64, { R, R } }, ++ { INDEX_op_ld_i64, { R, R } }, ++ ++ { INDEX_op_st8_i64, { R, R } }, ++ { INDEX_op_st16_i64, { R, R } }, ++ { INDEX_op_st32_i64, { R, R } }, ++ { INDEX_op_st_i64, { R, R } }, ++ ++ { INDEX_op_add_i64, { R, RI, RI } }, ++ { INDEX_op_sub_i64, { R, RI, RI } }, ++ { INDEX_op_mul_i64, { R, RI, RI } }, ++#if TCG_TARGET_HAS_div_i64 ++ { INDEX_op_div_i64, { R, R, R } }, ++ { INDEX_op_divu_i64, { R, R, R } }, ++ { INDEX_op_rem_i64, { R, R, R } }, ++ { INDEX_op_remu_i64, { R, R, R } }, ++#elif TCG_TARGET_HAS_div2_i64 ++ { INDEX_op_div2_i64, { R, R, "0", "1", R } }, ++ { INDEX_op_divu2_i64, { R, R, "0", "1", R } }, ++#endif ++ { INDEX_op_and_i64, { R, RI, RI } }, ++#if TCG_TARGET_HAS_andc_i64 ++ { INDEX_op_andc_i64, { R, RI, RI } }, ++#endif ++#if TCG_TARGET_HAS_eqv_i64 ++ { INDEX_op_eqv_i64, { R, RI, RI } }, ++#endif ++#if TCG_TARGET_HAS_nand_i64 ++ { INDEX_op_nand_i64, { R, RI, RI } }, ++#endif ++#if TCG_TARGET_HAS_nor_i64 ++ { INDEX_op_nor_i64, { R, RI, RI } }, ++#endif ++ { INDEX_op_or_i64, { R, RI, RI } }, ++#if TCG_TARGET_HAS_orc_i64 ++ { INDEX_op_orc_i64, { R, RI, RI } }, ++#endif ++ { INDEX_op_xor_i64, { R, RI, RI } }, ++ { INDEX_op_shl_i64, { R, RI, RI } }, ++ { INDEX_op_shr_i64, { R, RI, RI } }, ++ { INDEX_op_sar_i64, { R, RI, RI } }, ++#if TCG_TARGET_HAS_rot_i64 ++ { INDEX_op_rotl_i64, { R, RI, RI } }, ++ { INDEX_op_rotr_i64, { R, RI, RI } }, ++#endif ++#if TCG_TARGET_HAS_deposit_i64 ++ { INDEX_op_deposit_i64, { R, "0", R } }, ++#endif ++ { INDEX_op_brcond_i64, { R, RI } }, ++ ++#if TCG_TARGET_HAS_ext8s_i64 ++ { INDEX_op_ext8s_i64, { R, R } }, ++#endif ++#if TCG_TARGET_HAS_ext16s_i64 ++ { INDEX_op_ext16s_i64, { R, R } }, ++#endif ++#if TCG_TARGET_HAS_ext32s_i64 ++ { INDEX_op_ext32s_i64, { R, R } }, ++#endif ++#if TCG_TARGET_HAS_ext8u_i64 ++ { INDEX_op_ext8u_i64, { R, R } }, ++#endif ++#if TCG_TARGET_HAS_ext16u_i64 ++ { INDEX_op_ext16u_i64, { R, R } }, ++#endif ++#if TCG_TARGET_HAS_ext32u_i64 ++ { INDEX_op_ext32u_i64, { R, R } }, ++#endif ++ { INDEX_op_ext_i32_i64, { R, R } }, ++ { INDEX_op_extu_i32_i64, { R, R } }, ++#if TCG_TARGET_HAS_bswap16_i64 ++ { INDEX_op_bswap16_i64, { R, R } }, ++#endif ++#if TCG_TARGET_HAS_bswap32_i64 ++ { INDEX_op_bswap32_i64, { R, R } }, ++#endif ++#if TCG_TARGET_HAS_bswap64_i64 ++ { INDEX_op_bswap64_i64, { R, R } }, ++#endif ++#if TCG_TARGET_HAS_not_i64 ++ { INDEX_op_not_i64, { R, R } }, ++#endif ++#if TCG_TARGET_HAS_neg_i64 ++ { INDEX_op_neg_i64, { R, R } }, ++#endif ++#endif /* TCG_TARGET_REG_BITS == 64 */ ++ ++ { INDEX_op_qemu_ld_i32, { R, L } }, ++ { INDEX_op_qemu_ld_i64, { R64, L } }, ++ ++ { INDEX_op_qemu_st_i32, { R, S } }, ++ { INDEX_op_qemu_st_i64, { R64, S } }, ++ ++#if TCG_TARGET_HAS_ext8s_i32 ++ { INDEX_op_ext8s_i32, { R, R } }, ++#endif ++#if TCG_TARGET_HAS_ext16s_i32 ++ { INDEX_op_ext16s_i32, { R, R } }, ++#endif ++#if TCG_TARGET_HAS_ext8u_i32 ++ { INDEX_op_ext8u_i32, { R, R } }, ++#endif ++#if TCG_TARGET_HAS_ext16u_i32 ++ { INDEX_op_ext16u_i32, { R, R } }, ++#endif ++ ++#if TCG_TARGET_HAS_bswap16_i32 ++ { INDEX_op_bswap16_i32, { R, R } }, ++#endif ++#if TCG_TARGET_HAS_bswap32_i32 ++ { INDEX_op_bswap32_i32, { R, R } }, ++#endif ++ ++ { INDEX_op_mb, { } }, ++ { -1 }, ++}; ++ ++static const TCGTargetOpDef *tcg_target_op_def(TCGOpcode op) ++{ ++ int i, n = ARRAY_SIZE(tcg_target_op_defs); ++ ++ for (i = 0; i < n; ++i) { ++ if (tcg_target_op_defs[i].op == op) { ++ return &tcg_target_op_defs[i]; ++ } ++ } ++ return NULL; ++} ++ ++static const int tcg_target_reg_alloc_order[] = { ++ TCG_REG_R0, ++ TCG_REG_R1, ++ TCG_REG_R2, ++ TCG_REG_R3, ++#if 0 /* used for TCG_REG_CALL_STACK */ ++ TCG_REG_R4, ++#endif ++ TCG_REG_R5, ++ TCG_REG_R6, ++ TCG_REG_R7, ++#if TCG_TARGET_NB_REGS >= 16 ++ TCG_REG_R8, ++ TCG_REG_R9, ++ TCG_REG_R10, ++ TCG_REG_R11, ++ TCG_REG_R12, ++ TCG_REG_R13, ++ TCG_REG_R14, ++ TCG_REG_R15, ++#endif ++}; ++ ++#if MAX_OPC_PARAM_IARGS != 6 ++# error Fix needed, number of supported input arguments changed! ++#endif ++ ++static const int tcg_target_call_iarg_regs[] = { ++ TCG_REG_R0, ++ TCG_REG_R1, ++ TCG_REG_R2, ++ TCG_REG_R3, ++#if 0 /* used for TCG_REG_CALL_STACK */ ++ TCG_REG_R4, ++#endif ++ TCG_REG_R5, ++ TCG_REG_R6, ++#if TCG_TARGET_REG_BITS == 32 ++ /* 32 bit hosts need 2 * MAX_OPC_PARAM_IARGS registers. */ ++ TCG_REG_R7, ++#if TCG_TARGET_NB_REGS >= 16 ++ TCG_REG_R8, ++ TCG_REG_R9, ++ TCG_REG_R10, ++ TCG_REG_R11, ++ TCG_REG_R12, ++#else ++# error Too few input registers available ++#endif ++#endif ++}; ++ ++static const int tcg_target_call_oarg_regs[] = { ++ TCG_REG_R0, ++#if TCG_TARGET_REG_BITS == 32 ++ TCG_REG_R1 ++#endif ++}; ++ ++#ifdef CONFIG_DEBUG_TCG ++static const char *const tcg_target_reg_names[TCG_TARGET_NB_REGS] = { ++ "r00", ++ "r01", ++ "r02", ++ "r03", ++ "r04", ++ "r05", ++ "r06", ++ "r07", ++#if TCG_TARGET_NB_REGS >= 16 ++ "r08", ++ "r09", ++ "r10", ++ "r11", ++ "r12", ++ "r13", ++ "r14", ++ "r15", ++#if TCG_TARGET_NB_REGS >= 32 ++ "r16", ++ "r17", ++ "r18", ++ "r19", ++ "r20", ++ "r21", ++ "r22", ++ "r23", ++ "r24", ++ "r25", ++ "r26", ++ "r27", ++ "r28", ++ "r29", ++ "r30", ++ "r31" ++#endif ++#endif ++}; ++#endif ++ ++static bool patch_reloc(tcg_insn_unit *code_ptr, int type, ++ intptr_t value, intptr_t addend) ++{ ++ /* tcg_out_reloc always uses the same type, addend. */ ++ tcg_debug_assert(type == sizeof(tcg_target_long)); ++ tcg_debug_assert(addend == 0); ++ tcg_debug_assert(value != 0); ++ if (TCG_TARGET_REG_BITS == 32) { ++ tcg_patch32(code_ptr, value); ++ } else { ++ tcg_patch64(code_ptr, value); ++ } ++ return true; ++} ++ ++/* Parse target specific constraints. */ ++static const char *target_parse_constraint(TCGArgConstraint *ct, ++ const char *ct_str, TCGType type) ++{ ++ switch (*ct_str++) { ++ case 'r': ++ case 'L': /* qemu_ld constraint */ ++ case 'S': /* qemu_st constraint */ ++ ct->ct |= TCG_CT_REG; ++ ct->u.regs = BIT(TCG_TARGET_NB_REGS) - 1; ++ break; ++ default: ++ return NULL; ++ } ++ return ct_str; ++} ++ ++#if defined(CONFIG_DEBUG_TCG_INTERPRETER) ++/* Show current bytecode. Used by tcg interpreter. */ ++void tci_disas(uint8_t opc) ++{ ++ const TCGOpDef *def = &tcg_op_defs[opc]; ++ fprintf(stderr, "TCG %s %u, %u, %u\n", ++ def->name, def->nb_oargs, def->nb_iargs, def->nb_cargs); ++} ++#endif ++ ++/* Write value (native size). */ ++static void tcg_out_i(TCGContext *s, tcg_target_ulong v) ++{ ++ if (TCG_TARGET_REG_BITS == 32) { ++ tcg_out32(s, v); ++ } else { ++ tcg_out64(s, v); ++ } ++} ++ ++/* Write opcode. */ ++static void tcg_out_op_t(TCGContext *s, TCGOpcode op) ++{ ++ tcg_out8(s, op); ++ tcg_out8(s, 0); ++} ++ ++/* Write register. */ ++static void tcg_out_r(TCGContext *s, TCGArg t0) ++{ ++ tcg_debug_assert(t0 < TCG_TARGET_NB_REGS); ++ tcg_out8(s, t0); ++} ++ ++/* Write register or constant (native size). */ ++static void tcg_out_ri(TCGContext *s, int const_arg, TCGArg arg) ++{ ++ if (const_arg) { ++ tcg_debug_assert(const_arg == 1); ++ tcg_out8(s, TCG_CONST); ++ tcg_out_i(s, arg); ++ } else { ++ tcg_out_r(s, arg); ++ } ++} ++ ++/* Write register or constant (32 bit). */ ++static void tcg_out_ri32(TCGContext *s, int const_arg, TCGArg arg) ++{ ++ if (const_arg) { ++ tcg_debug_assert(const_arg == 1); ++ tcg_out8(s, TCG_CONST); ++ tcg_out32(s, arg); ++ } else { ++ tcg_out_r(s, arg); ++ } ++} ++ ++#if TCG_TARGET_REG_BITS == 64 ++/* Write register or constant (64 bit). */ ++static void tcg_out_ri64(TCGContext *s, int const_arg, TCGArg arg) ++{ ++ if (const_arg) { ++ tcg_debug_assert(const_arg == 1); ++ tcg_out8(s, TCG_CONST); ++ tcg_out64(s, arg); ++ } else { ++ tcg_out_r(s, arg); ++ } ++} ++#endif ++ ++/* Write label. */ ++static void tci_out_label(TCGContext *s, TCGLabel *label) ++{ ++ if (label->has_value) { ++ tcg_out_i(s, label->u.value); ++ tcg_debug_assert(label->u.value); ++ } else { ++ tcg_out_reloc(s, s->code_ptr, sizeof(tcg_target_ulong), label, 0); ++ s->code_ptr += sizeof(tcg_target_ulong); ++ } ++} ++ ++static void tcg_out_ld(TCGContext *s, TCGType type, TCGReg ret, TCGReg arg1, ++ intptr_t arg2) ++{ ++ uint8_t *old_code_ptr = s->code_ptr; ++ if (type == TCG_TYPE_I32) { ++ tcg_out_op_t(s, INDEX_op_ld_i32); ++ tcg_out_r(s, ret); ++ tcg_out_r(s, arg1); ++ tcg_out32(s, arg2); ++ } else { ++ tcg_debug_assert(type == TCG_TYPE_I64); ++#if TCG_TARGET_REG_BITS == 64 ++ tcg_out_op_t(s, INDEX_op_ld_i64); ++ tcg_out_r(s, ret); ++ tcg_out_r(s, arg1); ++ tcg_debug_assert(arg2 == (int32_t)arg2); ++ tcg_out32(s, arg2); ++#else ++ TODO(); ++#endif ++ } ++ old_code_ptr[1] = s->code_ptr - old_code_ptr; ++} ++ ++static bool tcg_out_mov(TCGContext *s, TCGType type, TCGReg ret, TCGReg arg) ++{ ++ uint8_t *old_code_ptr = s->code_ptr; ++ tcg_debug_assert(ret != arg); ++#if TCG_TARGET_REG_BITS == 32 ++ tcg_out_op_t(s, INDEX_op_mov_i32); ++#else ++ tcg_out_op_t(s, INDEX_op_mov_i64); ++#endif ++ tcg_out_r(s, ret); ++ tcg_out_r(s, arg); ++ old_code_ptr[1] = s->code_ptr - old_code_ptr; ++ return true; ++} ++ ++static void tcg_out_movi(TCGContext *s, TCGType type, ++ TCGReg t0, tcg_target_long arg) ++{ ++ uint8_t *old_code_ptr = s->code_ptr; ++ uint32_t arg32 = arg; ++ if (type == TCG_TYPE_I32 || arg == arg32) { ++ tcg_out_op_t(s, INDEX_op_movi_i32); ++ tcg_out_r(s, t0); ++ tcg_out32(s, arg32); ++ } else { ++ tcg_debug_assert(type == TCG_TYPE_I64); ++#if TCG_TARGET_REG_BITS == 64 ++ tcg_out_op_t(s, INDEX_op_movi_i64); ++ tcg_out_r(s, t0); ++ tcg_out64(s, arg); ++#else ++ TODO(); ++#endif ++ } ++ old_code_ptr[1] = s->code_ptr - old_code_ptr; ++} ++ ++static inline void tcg_out_call(TCGContext *s, tcg_insn_unit *arg) ++{ ++ uint8_t *old_code_ptr = s->code_ptr; ++ tcg_out_op_t(s, INDEX_op_call); ++ tcg_out_ri(s, 1, (uintptr_t)arg); ++ old_code_ptr[1] = s->code_ptr - old_code_ptr; ++} ++ ++static void tcg_out_op(TCGContext *s, TCGOpcode opc, const TCGArg *args, ++ const int *const_args) ++{ ++ uint8_t *old_code_ptr = s->code_ptr; ++ ++ tcg_out_op_t(s, opc); ++ ++ switch (opc) { ++ case INDEX_op_exit_tb: ++ tcg_out64(s, args[0]); ++ break; ++ case INDEX_op_goto_tb: ++ if (s->tb_jmp_insn_offset) { ++ /* Direct jump method. */ ++ /* Align for atomic patching and thread safety */ ++ s->code_ptr = QEMU_ALIGN_PTR_UP(s->code_ptr, 4); ++ s->tb_jmp_insn_offset[args[0]] = tcg_current_code_size(s); ++ tcg_out32(s, 0); ++ } else { ++ /* Indirect jump method. */ ++ TODO(); ++ } ++ set_jmp_reset_offset(s, args[0]); ++ break; ++ case INDEX_op_br: ++ tci_out_label(s, arg_label(args[0])); ++ break; ++ case INDEX_op_setcond_i32: ++ tcg_out_r(s, args[0]); ++ tcg_out_r(s, args[1]); ++ tcg_out_ri32(s, const_args[2], args[2]); ++ tcg_out8(s, args[3]); /* condition */ ++ break; ++#if TCG_TARGET_REG_BITS == 32 ++ case INDEX_op_setcond2_i32: ++ /* setcond2_i32 cond, t0, t1_low, t1_high, t2_low, t2_high */ ++ tcg_out_r(s, args[0]); ++ tcg_out_r(s, args[1]); ++ tcg_out_r(s, args[2]); ++ tcg_out_ri32(s, const_args[3], args[3]); ++ tcg_out_ri32(s, const_args[4], args[4]); ++ tcg_out8(s, args[5]); /* condition */ ++ break; ++#elif TCG_TARGET_REG_BITS == 64 ++ case INDEX_op_setcond_i64: ++ tcg_out_r(s, args[0]); ++ tcg_out_r(s, args[1]); ++ tcg_out_ri64(s, const_args[2], args[2]); ++ tcg_out8(s, args[3]); /* condition */ ++ break; ++#endif ++ case INDEX_op_ld8u_i32: ++ case INDEX_op_ld8s_i32: ++ case INDEX_op_ld16u_i32: ++ case INDEX_op_ld16s_i32: ++ case INDEX_op_ld_i32: ++ case INDEX_op_st8_i32: ++ case INDEX_op_st16_i32: ++ case INDEX_op_st_i32: ++ case INDEX_op_ld8u_i64: ++ case INDEX_op_ld8s_i64: ++ case INDEX_op_ld16u_i64: ++ case INDEX_op_ld16s_i64: ++ case INDEX_op_ld32u_i64: ++ case INDEX_op_ld32s_i64: ++ case INDEX_op_ld_i64: ++ case INDEX_op_st8_i64: ++ case INDEX_op_st16_i64: ++ case INDEX_op_st32_i64: ++ case INDEX_op_st_i64: ++ tcg_out_r(s, args[0]); ++ tcg_out_r(s, args[1]); ++ tcg_debug_assert(args[2] == (int32_t)args[2]); ++ tcg_out32(s, args[2]); ++ break; ++ case INDEX_op_add_i32: ++ case INDEX_op_sub_i32: ++ case INDEX_op_mul_i32: ++ case INDEX_op_and_i32: ++ case INDEX_op_andc_i32: /* Optional (TCG_TARGET_HAS_andc_i32). */ ++ case INDEX_op_eqv_i32: /* Optional (TCG_TARGET_HAS_eqv_i32). */ ++ case INDEX_op_nand_i32: /* Optional (TCG_TARGET_HAS_nand_i32). */ ++ case INDEX_op_nor_i32: /* Optional (TCG_TARGET_HAS_nor_i32). */ ++ case INDEX_op_or_i32: ++ case INDEX_op_orc_i32: /* Optional (TCG_TARGET_HAS_orc_i32). */ ++ case INDEX_op_xor_i32: ++ case INDEX_op_shl_i32: ++ case INDEX_op_shr_i32: ++ case INDEX_op_sar_i32: ++ case INDEX_op_rotl_i32: /* Optional (TCG_TARGET_HAS_rot_i32). */ ++ case INDEX_op_rotr_i32: /* Optional (TCG_TARGET_HAS_rot_i32). */ ++ tcg_out_r(s, args[0]); ++ tcg_out_ri32(s, const_args[1], args[1]); ++ tcg_out_ri32(s, const_args[2], args[2]); ++ break; ++ case INDEX_op_deposit_i32: /* Optional (TCG_TARGET_HAS_deposit_i32). */ ++ tcg_out_r(s, args[0]); ++ tcg_out_r(s, args[1]); ++ tcg_out_r(s, args[2]); ++ tcg_debug_assert(args[3] <= UINT8_MAX); ++ tcg_out8(s, args[3]); ++ tcg_debug_assert(args[4] <= UINT8_MAX); ++ tcg_out8(s, args[4]); ++ break; ++ ++#if TCG_TARGET_REG_BITS == 64 ++ case INDEX_op_add_i64: ++ case INDEX_op_sub_i64: ++ case INDEX_op_mul_i64: ++ case INDEX_op_and_i64: ++ case INDEX_op_andc_i64: /* Optional (TCG_TARGET_HAS_andc_i64). */ ++ case INDEX_op_eqv_i64: /* Optional (TCG_TARGET_HAS_eqv_i64). */ ++ case INDEX_op_nand_i64: /* Optional (TCG_TARGET_HAS_nand_i64). */ ++ case INDEX_op_nor_i64: /* Optional (TCG_TARGET_HAS_nor_i64). */ ++ case INDEX_op_or_i64: ++ case INDEX_op_orc_i64: /* Optional (TCG_TARGET_HAS_orc_i64). */ ++ case INDEX_op_xor_i64: ++ case INDEX_op_shl_i64: ++ case INDEX_op_shr_i64: ++ case INDEX_op_sar_i64: ++ case INDEX_op_rotl_i64: /* Optional (TCG_TARGET_HAS_rot_i64). */ ++ case INDEX_op_rotr_i64: /* Optional (TCG_TARGET_HAS_rot_i64). */ ++ tcg_out_r(s, args[0]); ++ tcg_out_ri64(s, const_args[1], args[1]); ++ tcg_out_ri64(s, const_args[2], args[2]); ++ break; ++ case INDEX_op_deposit_i64: /* Optional (TCG_TARGET_HAS_deposit_i64). */ ++ tcg_out_r(s, args[0]); ++ tcg_out_r(s, args[1]); ++ tcg_out_r(s, args[2]); ++ tcg_debug_assert(args[3] <= UINT8_MAX); ++ tcg_out8(s, args[3]); ++ tcg_debug_assert(args[4] <= UINT8_MAX); ++ tcg_out8(s, args[4]); ++ break; ++ case INDEX_op_div_i64: /* Optional (TCG_TARGET_HAS_div_i64). */ ++ case INDEX_op_divu_i64: /* Optional (TCG_TARGET_HAS_div_i64). */ ++ case INDEX_op_rem_i64: /* Optional (TCG_TARGET_HAS_div_i64). */ ++ case INDEX_op_remu_i64: /* Optional (TCG_TARGET_HAS_div_i64). */ ++ TODO(); ++ break; ++ case INDEX_op_div2_i64: /* Optional (TCG_TARGET_HAS_div2_i64). */ ++ case INDEX_op_divu2_i64: /* Optional (TCG_TARGET_HAS_div2_i64). */ ++ TODO(); ++ break; ++ case INDEX_op_brcond_i64: ++ tcg_out_r(s, args[0]); ++ tcg_out_ri64(s, const_args[1], args[1]); ++ tcg_out8(s, args[2]); /* condition */ ++ tci_out_label(s, arg_label(args[3])); ++ break; ++ case INDEX_op_bswap16_i64: /* Optional (TCG_TARGET_HAS_bswap16_i64). */ ++ case INDEX_op_bswap32_i64: /* Optional (TCG_TARGET_HAS_bswap32_i64). */ ++ case INDEX_op_bswap64_i64: /* Optional (TCG_TARGET_HAS_bswap64_i64). */ ++ case INDEX_op_not_i64: /* Optional (TCG_TARGET_HAS_not_i64). */ ++ case INDEX_op_neg_i64: /* Optional (TCG_TARGET_HAS_neg_i64). */ ++ case INDEX_op_ext8s_i64: /* Optional (TCG_TARGET_HAS_ext8s_i64). */ ++ case INDEX_op_ext8u_i64: /* Optional (TCG_TARGET_HAS_ext8u_i64). */ ++ case INDEX_op_ext16s_i64: /* Optional (TCG_TARGET_HAS_ext16s_i64). */ ++ case INDEX_op_ext16u_i64: /* Optional (TCG_TARGET_HAS_ext16u_i64). */ ++ case INDEX_op_ext32s_i64: /* Optional (TCG_TARGET_HAS_ext32s_i64). */ ++ case INDEX_op_ext32u_i64: /* Optional (TCG_TARGET_HAS_ext32u_i64). */ ++ case INDEX_op_ext_i32_i64: ++ case INDEX_op_extu_i32_i64: ++#endif /* TCG_TARGET_REG_BITS == 64 */ ++ case INDEX_op_neg_i32: /* Optional (TCG_TARGET_HAS_neg_i32). */ ++ case INDEX_op_not_i32: /* Optional (TCG_TARGET_HAS_not_i32). */ ++ case INDEX_op_ext8s_i32: /* Optional (TCG_TARGET_HAS_ext8s_i32). */ ++ case INDEX_op_ext16s_i32: /* Optional (TCG_TARGET_HAS_ext16s_i32). */ ++ case INDEX_op_ext8u_i32: /* Optional (TCG_TARGET_HAS_ext8u_i32). */ ++ case INDEX_op_ext16u_i32: /* Optional (TCG_TARGET_HAS_ext16u_i32). */ ++ case INDEX_op_bswap16_i32: /* Optional (TCG_TARGET_HAS_bswap16_i32). */ ++ case INDEX_op_bswap32_i32: /* Optional (TCG_TARGET_HAS_bswap32_i32). */ ++ tcg_out_r(s, args[0]); ++ tcg_out_r(s, args[1]); ++ break; ++ case INDEX_op_div_i32: /* Optional (TCG_TARGET_HAS_div_i32). */ ++ case INDEX_op_divu_i32: /* Optional (TCG_TARGET_HAS_div_i32). */ ++ case INDEX_op_rem_i32: /* Optional (TCG_TARGET_HAS_div_i32). */ ++ case INDEX_op_remu_i32: /* Optional (TCG_TARGET_HAS_div_i32). */ ++ tcg_out_r(s, args[0]); ++ tcg_out_ri32(s, const_args[1], args[1]); ++ tcg_out_ri32(s, const_args[2], args[2]); ++ break; ++ case INDEX_op_div2_i32: /* Optional (TCG_TARGET_HAS_div2_i32). */ ++ case INDEX_op_divu2_i32: /* Optional (TCG_TARGET_HAS_div2_i32). */ ++ TODO(); ++ break; ++#if TCG_TARGET_REG_BITS == 32 ++ case INDEX_op_add2_i32: ++ case INDEX_op_sub2_i32: ++ tcg_out_r(s, args[0]); ++ tcg_out_r(s, args[1]); ++ tcg_out_r(s, args[2]); ++ tcg_out_r(s, args[3]); ++ tcg_out_r(s, args[4]); ++ tcg_out_r(s, args[5]); ++ break; ++ case INDEX_op_brcond2_i32: ++ tcg_out_r(s, args[0]); ++ tcg_out_r(s, args[1]); ++ tcg_out_ri32(s, const_args[2], args[2]); ++ tcg_out_ri32(s, const_args[3], args[3]); ++ tcg_out8(s, args[4]); /* condition */ ++ tci_out_label(s, arg_label(args[5])); ++ break; ++ case INDEX_op_mulu2_i32: ++ tcg_out_r(s, args[0]); ++ tcg_out_r(s, args[1]); ++ tcg_out_r(s, args[2]); ++ tcg_out_r(s, args[3]); ++ break; ++#endif ++ case INDEX_op_brcond_i32: ++ tcg_out_r(s, args[0]); ++ tcg_out_ri32(s, const_args[1], args[1]); ++ tcg_out8(s, args[2]); /* condition */ ++ tci_out_label(s, arg_label(args[3])); ++ break; ++ case INDEX_op_qemu_ld_i32: ++ tcg_out_r(s, *args++); ++ tcg_out_r(s, *args++); ++ if (TARGET_LONG_BITS > TCG_TARGET_REG_BITS) { ++ tcg_out_r(s, *args++); ++ } ++ tcg_out_i(s, *args++); ++ break; ++ case INDEX_op_qemu_ld_i64: ++ tcg_out_r(s, *args++); ++ if (TCG_TARGET_REG_BITS == 32) { ++ tcg_out_r(s, *args++); ++ } ++ tcg_out_r(s, *args++); ++ if (TARGET_LONG_BITS > TCG_TARGET_REG_BITS) { ++ tcg_out_r(s, *args++); ++ } ++ tcg_out_i(s, *args++); ++ break; ++ case INDEX_op_qemu_st_i32: ++ tcg_out_r(s, *args++); ++ tcg_out_r(s, *args++); ++ if (TARGET_LONG_BITS > TCG_TARGET_REG_BITS) { ++ tcg_out_r(s, *args++); ++ } ++ tcg_out_i(s, *args++); ++ break; ++ case INDEX_op_qemu_st_i64: ++ tcg_out_r(s, *args++); ++ if (TCG_TARGET_REG_BITS == 32) { ++ tcg_out_r(s, *args++); ++ } ++ tcg_out_r(s, *args++); ++ if (TARGET_LONG_BITS > TCG_TARGET_REG_BITS) { ++ tcg_out_r(s, *args++); ++ } ++ tcg_out_i(s, *args++); ++ break; ++ case INDEX_op_mb: ++ break; ++ case INDEX_op_mov_i32: /* Always emitted via tcg_out_mov. */ ++ case INDEX_op_mov_i64: ++ case INDEX_op_movi_i32: /* Always emitted via tcg_out_movi. */ ++ case INDEX_op_movi_i64: ++ case INDEX_op_call: /* Always emitted via tcg_out_call. */ ++ default: ++ tcg_abort(); ++ } ++ old_code_ptr[1] = s->code_ptr - old_code_ptr; ++} ++ ++static void tcg_out_st(TCGContext *s, TCGType type, TCGReg arg, TCGReg arg1, ++ intptr_t arg2) ++{ ++ uint8_t *old_code_ptr = s->code_ptr; ++ if (type == TCG_TYPE_I32) { ++ tcg_out_op_t(s, INDEX_op_st_i32); ++ tcg_out_r(s, arg); ++ tcg_out_r(s, arg1); ++ tcg_out32(s, arg2); ++ } else { ++ tcg_debug_assert(type == TCG_TYPE_I64); ++#if TCG_TARGET_REG_BITS == 64 ++ tcg_out_op_t(s, INDEX_op_st_i64); ++ tcg_out_r(s, arg); ++ tcg_out_r(s, arg1); ++ tcg_out32(s, arg2); ++#else ++ TODO(); ++#endif ++ } ++ old_code_ptr[1] = s->code_ptr - old_code_ptr; ++} ++ ++static inline bool tcg_out_sti(TCGContext *s, TCGType type, TCGArg val, ++ TCGReg base, intptr_t ofs) ++{ ++ return false; ++} ++ ++/* Test if a constant matches the constraint. */ ++static int tcg_target_const_match(tcg_target_long val, TCGType type, ++ const TCGArgConstraint *arg_ct) ++{ ++ /* No need to return 0 or 1, 0 or != 0 is good enough. */ ++ return arg_ct->ct & TCG_CT_CONST; ++} ++ ++static void tcg_target_init(TCGContext *s) ++{ ++#if defined(CONFIG_DEBUG_TCG_INTERPRETER) ++ const char *envval = getenv("DEBUG_TCG"); ++ if (envval) { ++ qemu_set_log(strtol(envval, NULL, 0)); ++ } ++#endif ++ ++ /* The current code uses uint8_t for tcg operations. unicorn moved the ++ op-def table into TCGContext, so the historical tcg_op_defs_max bound ++ check no longer applies; NB_OPS stays far below UINT8_MAX. */ ++ ++ /* Registers available for 32 bit operations. */ ++ s->tcg_target_available_regs[TCG_TYPE_I32] = BIT(TCG_TARGET_NB_REGS) - 1; ++ /* Registers available for 64 bit operations. */ ++ s->tcg_target_available_regs[TCG_TYPE_I64] = BIT(TCG_TARGET_NB_REGS) - 1; ++ /* TODO: Which registers should be set here? */ ++ s->tcg_target_call_clobber_regs = BIT(TCG_TARGET_NB_REGS) - 1; ++ ++ s->reserved_regs = 0; ++ tcg_regset_set_reg(s->reserved_regs, TCG_REG_CALL_STACK); ++ ++ /* We use negative offsets from "sp" so that we can distinguish ++ stores that might pretend to be call arguments. */ ++ tcg_set_frame(s, TCG_REG_CALL_STACK, ++ -CPU_TEMP_BUF_NLONGS * sizeof(long), ++ CPU_TEMP_BUF_NLONGS * sizeof(long)); ++} ++ ++/* Generate global QEMU prologue and epilogue code. */ ++static inline void tcg_target_qemu_prologue(TCGContext *s) ++{ ++} +diff --git a/qemu/tcg/tci.c b/qemu/tcg/tci.c +new file mode 100644 +index 0000000..1f330b5 +--- /dev/null ++++ b/qemu/tcg/tci.c +@@ -0,0 +1,1300 @@ ++/* ++ * Tiny Code Interpreter for QEMU ++ * ++ * Copyright (c) 2009, 2011, 2016 Stefan Weil ++ * ++ * This program is free software: you can redistribute it and/or modify ++ * it under the terms of the GNU General Public License as published by ++ * the Free Software Foundation, either version 2 of the License, or ++ * (at your option) any later version. ++ * ++ * This program is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program. If not, see . ++ */ ++ ++#include "qemu/osdep.h" ++ ++/* Enable TCI assertions only when debugging TCG (and without NDEBUG defined). ++ * Without assertions, the interpreter runs much faster. */ ++#if defined(CONFIG_DEBUG_TCG) ++# define tci_assert(cond) assert(cond) ++#else ++# define tci_assert(cond) ((void)0) ++#endif ++ ++#include "qemu-common.h" ++#include "tcg/tcg.h" /* MAX_OPC_PARAM_IARGS */ ++#include "exec/cpu_ldst.h" ++#include "tcg/tcg-op.h" ++ ++#ifdef __EMSCRIPTEN__ ++#include ++/* Wall-clock deadline (ms since epoch; 0 = unlimited) set by the engine glue ++ before uc_emu_start; the interpreter checks it periodically because wasm ++ cannot host unicorn's timeout thread. */ ++double tci_wasm_deadline_ms; ++#endif ++ ++/* qemu 5.0's TCI never defined this global (the extern lives in ++ include/exec/exec-all.h under CONFIG_TCG_INTERPRETER). */ ++uintptr_t tci_tb_ptr; ++ ++ ++/* Marker for missing code. */ ++#define TODO() \ ++ do { \ ++ fprintf(stderr, "TODO %s:%u: %s()\n", \ ++ __FILE__, __LINE__, __func__); \ ++ tcg_abort(); \ ++ } while (0) ++ ++#if MAX_OPC_PARAM_IARGS != 6 ++# error Fix needed, number of supported input arguments changed! ++#endif ++#if TCG_TARGET_REG_BITS == 32 ++typedef uint64_t (*helper_function)(tcg_target_ulong, tcg_target_ulong, ++ tcg_target_ulong, tcg_target_ulong, ++ tcg_target_ulong, tcg_target_ulong, ++ tcg_target_ulong, tcg_target_ulong, ++ tcg_target_ulong, tcg_target_ulong, ++ tcg_target_ulong, tcg_target_ulong); ++#else ++typedef uint64_t (*helper_function)(tcg_target_ulong, tcg_target_ulong, ++ tcg_target_ulong, tcg_target_ulong, ++ tcg_target_ulong, tcg_target_ulong); ++#endif ++ ++static tcg_target_ulong tci_read_reg(const tcg_target_ulong *regs, TCGReg index) ++{ ++ tci_assert(index < TCG_TARGET_NB_REGS); ++ return regs[index]; ++} ++ ++#if TCG_TARGET_HAS_ext8s_i32 || TCG_TARGET_HAS_ext8s_i64 ++static int8_t tci_read_reg8s(const tcg_target_ulong *regs, TCGReg index) ++{ ++ return (int8_t)tci_read_reg(regs, index); ++} ++#endif ++ ++#if TCG_TARGET_HAS_ext16s_i32 || TCG_TARGET_HAS_ext16s_i64 ++static int16_t tci_read_reg16s(const tcg_target_ulong *regs, TCGReg index) ++{ ++ return (int16_t)tci_read_reg(regs, index); ++} ++#endif ++ ++#if TCG_TARGET_REG_BITS == 64 ++static int32_t tci_read_reg32s(const tcg_target_ulong *regs, TCGReg index) ++{ ++ return (int32_t)tci_read_reg(regs, index); ++} ++#endif ++ ++static uint8_t tci_read_reg8(const tcg_target_ulong *regs, TCGReg index) ++{ ++ return (uint8_t)tci_read_reg(regs, index); ++} ++ ++static uint16_t tci_read_reg16(const tcg_target_ulong *regs, TCGReg index) ++{ ++ return (uint16_t)tci_read_reg(regs, index); ++} ++ ++static uint32_t tci_read_reg32(const tcg_target_ulong *regs, TCGReg index) ++{ ++ return (uint32_t)tci_read_reg(regs, index); ++} ++ ++#if TCG_TARGET_REG_BITS == 64 ++static uint64_t tci_read_reg64(const tcg_target_ulong *regs, TCGReg index) ++{ ++ return tci_read_reg(regs, index); ++} ++#endif ++ ++static void ++tci_write_reg(tcg_target_ulong *regs, TCGReg index, tcg_target_ulong value) ++{ ++ tci_assert(index < TCG_TARGET_NB_REGS); ++ tci_assert(index != TCG_AREG0); ++ tci_assert(index != TCG_REG_CALL_STACK); ++ regs[index] = value; ++} ++ ++#if TCG_TARGET_REG_BITS == 64 ++static void ++tci_write_reg32s(tcg_target_ulong *regs, TCGReg index, int32_t value) ++{ ++ tci_write_reg(regs, index, value); ++} ++#endif ++ ++static void tci_write_reg8(tcg_target_ulong *regs, TCGReg index, uint8_t value) ++{ ++ tci_write_reg(regs, index, value); ++} ++ ++static void ++tci_write_reg16(tcg_target_ulong *regs, TCGReg index, uint16_t value) ++{ ++ tci_write_reg(regs, index, value); ++} ++ ++static void ++tci_write_reg32(tcg_target_ulong *regs, TCGReg index, uint32_t value) ++{ ++ tci_write_reg(regs, index, value); ++} ++ ++#if TCG_TARGET_REG_BITS == 32 ++static void tci_write_reg64(tcg_target_ulong *regs, uint32_t high_index, ++ uint32_t low_index, uint64_t value) ++{ ++ tci_write_reg(regs, low_index, value); ++ tci_write_reg(regs, high_index, value >> 32); ++} ++#elif TCG_TARGET_REG_BITS == 64 ++static void ++tci_write_reg64(tcg_target_ulong *regs, TCGReg index, uint64_t value) ++{ ++ tci_write_reg(regs, index, value); ++} ++#endif ++ ++#if TCG_TARGET_REG_BITS == 32 ++/* Create a 64 bit value from two 32 bit values. */ ++static uint64_t tci_uint64(uint32_t high, uint32_t low) ++{ ++ return ((uint64_t)high << 32) + low; ++} ++#endif ++ ++/* Read constant (native size) from bytecode. */ ++static tcg_target_ulong tci_read_i(uint8_t **tb_ptr) ++{ ++ tcg_target_ulong value = *(tcg_target_ulong *)(*tb_ptr); ++ *tb_ptr += sizeof(value); ++ return value; ++} ++ ++/* Read unsigned constant (32 bit) from bytecode. */ ++static uint32_t tci_read_i32(uint8_t **tb_ptr) ++{ ++ uint32_t value = *(uint32_t *)(*tb_ptr); ++ *tb_ptr += sizeof(value); ++ return value; ++} ++ ++/* Read signed constant (32 bit) from bytecode. */ ++static int32_t tci_read_s32(uint8_t **tb_ptr) ++{ ++ int32_t value = *(int32_t *)(*tb_ptr); ++ *tb_ptr += sizeof(value); ++ return value; ++} ++ ++#if TCG_TARGET_REG_BITS == 64 ++/* Read constant (64 bit) from bytecode. */ ++static uint64_t tci_read_i64(uint8_t **tb_ptr) ++{ ++ uint64_t value = *(uint64_t *)(*tb_ptr); ++ *tb_ptr += sizeof(value); ++ return value; ++} ++#endif ++ ++/* Read indexed register (native size) from bytecode. */ ++static tcg_target_ulong ++tci_read_r(const tcg_target_ulong *regs, uint8_t **tb_ptr) ++{ ++ tcg_target_ulong value = tci_read_reg(regs, **tb_ptr); ++ *tb_ptr += 1; ++ return value; ++} ++ ++/* Read indexed register (8 bit) from bytecode. */ ++static uint8_t tci_read_r8(const tcg_target_ulong *regs, uint8_t **tb_ptr) ++{ ++ uint8_t value = tci_read_reg8(regs, **tb_ptr); ++ *tb_ptr += 1; ++ return value; ++} ++ ++#if TCG_TARGET_HAS_ext8s_i32 || TCG_TARGET_HAS_ext8s_i64 ++/* Read indexed register (8 bit signed) from bytecode. */ ++static int8_t tci_read_r8s(const tcg_target_ulong *regs, uint8_t **tb_ptr) ++{ ++ int8_t value = tci_read_reg8s(regs, **tb_ptr); ++ *tb_ptr += 1; ++ return value; ++} ++#endif ++ ++/* Read indexed register (16 bit) from bytecode. */ ++static uint16_t tci_read_r16(const tcg_target_ulong *regs, uint8_t **tb_ptr) ++{ ++ uint16_t value = tci_read_reg16(regs, **tb_ptr); ++ *tb_ptr += 1; ++ return value; ++} ++ ++#if TCG_TARGET_HAS_ext16s_i32 || TCG_TARGET_HAS_ext16s_i64 ++/* Read indexed register (16 bit signed) from bytecode. */ ++static int16_t tci_read_r16s(const tcg_target_ulong *regs, uint8_t **tb_ptr) ++{ ++ int16_t value = tci_read_reg16s(regs, **tb_ptr); ++ *tb_ptr += 1; ++ return value; ++} ++#endif ++ ++/* Read indexed register (32 bit) from bytecode. */ ++static uint32_t tci_read_r32(const tcg_target_ulong *regs, uint8_t **tb_ptr) ++{ ++ uint32_t value = tci_read_reg32(regs, **tb_ptr); ++ *tb_ptr += 1; ++ return value; ++} ++ ++#if TCG_TARGET_REG_BITS == 32 ++/* Read two indexed registers (2 * 32 bit) from bytecode. */ ++static uint64_t tci_read_r64(const tcg_target_ulong *regs, uint8_t **tb_ptr) ++{ ++ uint32_t low = tci_read_r32(regs, tb_ptr); ++ return tci_uint64(tci_read_r32(regs, tb_ptr), low); ++} ++#elif TCG_TARGET_REG_BITS == 64 ++/* Read indexed register (32 bit signed) from bytecode. */ ++static int32_t tci_read_r32s(const tcg_target_ulong *regs, uint8_t **tb_ptr) ++{ ++ int32_t value = tci_read_reg32s(regs, **tb_ptr); ++ *tb_ptr += 1; ++ return value; ++} ++ ++/* Read indexed register (64 bit) from bytecode. */ ++static uint64_t tci_read_r64(const tcg_target_ulong *regs, uint8_t **tb_ptr) ++{ ++ uint64_t value = tci_read_reg64(regs, **tb_ptr); ++ *tb_ptr += 1; ++ return value; ++} ++#endif ++ ++/* Read indexed register(s) with target address from bytecode. */ ++static target_ulong ++tci_read_ulong(const tcg_target_ulong *regs, uint8_t **tb_ptr) ++{ ++ target_ulong taddr = tci_read_r(regs, tb_ptr); ++#if TARGET_LONG_BITS > TCG_TARGET_REG_BITS ++ taddr += (uint64_t)tci_read_r(regs, tb_ptr) << 32; ++#endif ++ return taddr; ++} ++ ++/* Read indexed register or constant (native size) from bytecode. */ ++static tcg_target_ulong ++tci_read_ri(const tcg_target_ulong *regs, uint8_t **tb_ptr) ++{ ++ tcg_target_ulong value; ++ TCGReg r = **tb_ptr; ++ *tb_ptr += 1; ++ if (r == TCG_CONST) { ++ value = tci_read_i(tb_ptr); ++ } else { ++ value = tci_read_reg(regs, r); ++ } ++ return value; ++} ++ ++/* Read indexed register or constant (32 bit) from bytecode. */ ++static uint32_t tci_read_ri32(const tcg_target_ulong *regs, uint8_t **tb_ptr) ++{ ++ uint32_t value; ++ TCGReg r = **tb_ptr; ++ *tb_ptr += 1; ++ if (r == TCG_CONST) { ++ value = tci_read_i32(tb_ptr); ++ } else { ++ value = tci_read_reg32(regs, r); ++ } ++ return value; ++} ++ ++#if TCG_TARGET_REG_BITS == 32 ++/* Read two indexed registers or constants (2 * 32 bit) from bytecode. */ ++static uint64_t tci_read_ri64(const tcg_target_ulong *regs, uint8_t **tb_ptr) ++{ ++ uint32_t low = tci_read_ri32(regs, tb_ptr); ++ return tci_uint64(tci_read_ri32(regs, tb_ptr), low); ++} ++#elif TCG_TARGET_REG_BITS == 64 ++/* Read indexed register or constant (64 bit) from bytecode. */ ++static uint64_t tci_read_ri64(const tcg_target_ulong *regs, uint8_t **tb_ptr) ++{ ++ uint64_t value; ++ TCGReg r = **tb_ptr; ++ *tb_ptr += 1; ++ if (r == TCG_CONST) { ++ value = tci_read_i64(tb_ptr); ++ } else { ++ value = tci_read_reg64(regs, r); ++ } ++ return value; ++} ++#endif ++ ++static tcg_target_ulong tci_read_label(uint8_t **tb_ptr) ++{ ++ tcg_target_ulong label = tci_read_i(tb_ptr); ++ tci_assert(label != 0); ++ return label; ++} ++ ++static bool tci_compare32(uint32_t u0, uint32_t u1, TCGCond condition) ++{ ++ bool result = false; ++ int32_t i0 = u0; ++ int32_t i1 = u1; ++ switch (condition) { ++ case TCG_COND_EQ: ++ result = (u0 == u1); ++ break; ++ case TCG_COND_NE: ++ result = (u0 != u1); ++ break; ++ case TCG_COND_LT: ++ result = (i0 < i1); ++ break; ++ case TCG_COND_GE: ++ result = (i0 >= i1); ++ break; ++ case TCG_COND_LE: ++ result = (i0 <= i1); ++ break; ++ case TCG_COND_GT: ++ result = (i0 > i1); ++ break; ++ case TCG_COND_LTU: ++ result = (u0 < u1); ++ break; ++ case TCG_COND_GEU: ++ result = (u0 >= u1); ++ break; ++ case TCG_COND_LEU: ++ result = (u0 <= u1); ++ break; ++ case TCG_COND_GTU: ++ result = (u0 > u1); ++ break; ++ default: ++ TODO(); ++ } ++ return result; ++} ++ ++static bool tci_compare64(uint64_t u0, uint64_t u1, TCGCond condition) ++{ ++ bool result = false; ++ int64_t i0 = u0; ++ int64_t i1 = u1; ++ switch (condition) { ++ case TCG_COND_EQ: ++ result = (u0 == u1); ++ break; ++ case TCG_COND_NE: ++ result = (u0 != u1); ++ break; ++ case TCG_COND_LT: ++ result = (i0 < i1); ++ break; ++ case TCG_COND_GE: ++ result = (i0 >= i1); ++ break; ++ case TCG_COND_LE: ++ result = (i0 <= i1); ++ break; ++ case TCG_COND_GT: ++ result = (i0 > i1); ++ break; ++ case TCG_COND_LTU: ++ result = (u0 < u1); ++ break; ++ case TCG_COND_GEU: ++ result = (u0 >= u1); ++ break; ++ case TCG_COND_LEU: ++ result = (u0 <= u1); ++ break; ++ case TCG_COND_GTU: ++ result = (u0 > u1); ++ break; ++ default: ++ TODO(); ++ } ++ return result; ++} ++ ++#ifdef CONFIG_SOFTMMU ++# define qemu_ld_ub \ ++ helper_ret_ldub_mmu(env, taddr, oi, (uintptr_t)tb_ptr) ++# define qemu_ld_leuw \ ++ helper_le_lduw_mmu(env, taddr, oi, (uintptr_t)tb_ptr) ++# define qemu_ld_leul \ ++ helper_le_ldul_mmu(env, taddr, oi, (uintptr_t)tb_ptr) ++# define qemu_ld_leq \ ++ helper_le_ldq_mmu(env, taddr, oi, (uintptr_t)tb_ptr) ++# define qemu_ld_beuw \ ++ helper_be_lduw_mmu(env, taddr, oi, (uintptr_t)tb_ptr) ++# define qemu_ld_beul \ ++ helper_be_ldul_mmu(env, taddr, oi, (uintptr_t)tb_ptr) ++# define qemu_ld_beq \ ++ helper_be_ldq_mmu(env, taddr, oi, (uintptr_t)tb_ptr) ++# define qemu_st_b(X) \ ++ helper_ret_stb_mmu(env, taddr, X, oi, (uintptr_t)tb_ptr) ++# define qemu_st_lew(X) \ ++ helper_le_stw_mmu(env, taddr, X, oi, (uintptr_t)tb_ptr) ++# define qemu_st_lel(X) \ ++ helper_le_stl_mmu(env, taddr, X, oi, (uintptr_t)tb_ptr) ++# define qemu_st_leq(X) \ ++ helper_le_stq_mmu(env, taddr, X, oi, (uintptr_t)tb_ptr) ++# define qemu_st_bew(X) \ ++ helper_be_stw_mmu(env, taddr, X, oi, (uintptr_t)tb_ptr) ++# define qemu_st_bel(X) \ ++ helper_be_stl_mmu(env, taddr, X, oi, (uintptr_t)tb_ptr) ++# define qemu_st_beq(X) \ ++ helper_be_stq_mmu(env, taddr, X, oi, (uintptr_t)tb_ptr) ++#else ++# define qemu_ld_ub ldub_p(g2h(taddr)) ++# define qemu_ld_leuw lduw_le_p(g2h(taddr)) ++# define qemu_ld_leul (uint32_t)ldl_le_p(g2h(taddr)) ++# define qemu_ld_leq ldq_le_p(g2h(taddr)) ++# define qemu_ld_beuw lduw_be_p(g2h(taddr)) ++# define qemu_ld_beul (uint32_t)ldl_be_p(g2h(taddr)) ++# define qemu_ld_beq ldq_be_p(g2h(taddr)) ++# define qemu_st_b(X) stb_p(g2h(taddr), X) ++# define qemu_st_lew(X) stw_le_p(g2h(taddr), X) ++# define qemu_st_lel(X) stl_le_p(g2h(taddr), X) ++# define qemu_st_leq(X) stq_le_p(g2h(taddr), X) ++# define qemu_st_bew(X) stw_be_p(g2h(taddr), X) ++# define qemu_st_bel(X) stl_be_p(g2h(taddr), X) ++# define qemu_st_beq(X) stq_be_p(g2h(taddr), X) ++#endif ++ ++/* Interpret pseudo code in tb. */ ++uintptr_t tcg_qemu_tb_exec(CPUArchState *env, uint8_t *tb_ptr) ++{ ++ tcg_target_ulong regs[TCG_TARGET_NB_REGS]; ++ long tcg_temps[CPU_TEMP_BUF_NLONGS]; ++ uintptr_t sp_value = (uintptr_t)(tcg_temps + CPU_TEMP_BUF_NLONGS); ++ uintptr_t ret = 0; ++ ++ regs[TCG_AREG0] = (tcg_target_ulong)env; ++ regs[TCG_REG_CALL_STACK] = sp_value; ++ tci_assert(tb_ptr); ++ ++#ifdef __EMSCRIPTEN__ ++ unsigned tci_deadline_slice = 0; ++#endif ++ for (;;) { ++ TCGOpcode opc = tb_ptr[0]; ++#ifdef __EMSCRIPTEN__ ++ /* Periodic wall-clock deadline enforcement (no timeout thread). */ ++ if (tci_wasm_deadline_ms != 0 && (++tci_deadline_slice & 0xffff) == 0) { ++ if (emscripten_get_now() > tci_wasm_deadline_ms) { ++ uc_emu_stop(env_cpu(env)->uc); ++ return 0; ++ } ++ } ++#endif ++#if defined(CONFIG_DEBUG_TCG) && !defined(NDEBUG) ++ uint8_t op_size = tb_ptr[1]; ++ uint8_t *old_code_ptr = tb_ptr; ++#endif ++ tcg_target_ulong t0; ++ tcg_target_ulong t1; ++ tcg_target_ulong t2; ++ tcg_target_ulong label; ++ TCGCond condition; ++ target_ulong taddr; ++ uint8_t tmp8; ++ uint16_t tmp16; ++ uint32_t tmp32; ++ uint64_t tmp64; ++#if TCG_TARGET_REG_BITS == 32 ++ uint64_t v64; ++#endif ++ TCGMemOpIdx oi; ++ ++#if defined(GETPC) ++ tci_tb_ptr = (uintptr_t)tb_ptr; ++#endif ++ ++ /* Skip opcode and size entry. */ ++ tb_ptr += 2; ++ ++ switch (opc) { ++ case INDEX_op_call: ++ t0 = tci_read_ri(regs, &tb_ptr); ++#if TCG_TARGET_REG_BITS == 32 ++ tmp64 = ((helper_function)t0)(tci_read_reg(regs, TCG_REG_R0), ++ tci_read_reg(regs, TCG_REG_R1), ++ tci_read_reg(regs, TCG_REG_R2), ++ tci_read_reg(regs, TCG_REG_R3), ++ tci_read_reg(regs, TCG_REG_R5), ++ tci_read_reg(regs, TCG_REG_R6), ++ tci_read_reg(regs, TCG_REG_R7), ++ tci_read_reg(regs, TCG_REG_R8), ++ tci_read_reg(regs, TCG_REG_R9), ++ tci_read_reg(regs, TCG_REG_R10), ++ tci_read_reg(regs, TCG_REG_R11), ++ tci_read_reg(regs, TCG_REG_R12)); ++ tci_write_reg(regs, TCG_REG_R0, tmp64); ++ tci_write_reg(regs, TCG_REG_R1, tmp64 >> 32); ++#else ++ tmp64 = ((helper_function)t0)(tci_read_reg(regs, TCG_REG_R0), ++ tci_read_reg(regs, TCG_REG_R1), ++ tci_read_reg(regs, TCG_REG_R2), ++ tci_read_reg(regs, TCG_REG_R3), ++ tci_read_reg(regs, TCG_REG_R5), ++ tci_read_reg(regs, TCG_REG_R6)); ++ tci_write_reg(regs, TCG_REG_R0, tmp64); ++#endif ++ break; ++ case INDEX_op_br: ++ label = tci_read_label(&tb_ptr); ++ tci_assert(tb_ptr == old_code_ptr + op_size); ++ tb_ptr = (uint8_t *)label; ++ continue; ++ case INDEX_op_setcond_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r32(regs, &tb_ptr); ++ t2 = tci_read_ri32(regs, &tb_ptr); ++ condition = *tb_ptr++; ++ tci_write_reg32(regs, t0, tci_compare32(t1, t2, condition)); ++ break; ++#if TCG_TARGET_REG_BITS == 32 ++ case INDEX_op_setcond2_i32: ++ t0 = *tb_ptr++; ++ tmp64 = tci_read_r64(regs, &tb_ptr); ++ v64 = tci_read_ri64(regs, &tb_ptr); ++ condition = *tb_ptr++; ++ tci_write_reg32(regs, t0, tci_compare64(tmp64, v64, condition)); ++ break; ++#elif TCG_TARGET_REG_BITS == 64 ++ case INDEX_op_setcond_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r64(regs, &tb_ptr); ++ t2 = tci_read_ri64(regs, &tb_ptr); ++ condition = *tb_ptr++; ++ tci_write_reg64(regs, t0, tci_compare64(t1, t2, condition)); ++ break; ++#endif ++ case INDEX_op_mov_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r32(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, t1); ++ break; ++ case INDEX_op_movi_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_i32(&tb_ptr); ++ tci_write_reg32(regs, t0, t1); ++ break; ++ ++ /* Load/store operations (32 bit). */ ++ ++ case INDEX_op_ld8u_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r(regs, &tb_ptr); ++ t2 = tci_read_s32(&tb_ptr); ++ tci_write_reg8(regs, t0, *(uint8_t *)(t1 + t2)); ++ break; ++ case INDEX_op_ld8s_i32: ++ TODO(); ++ break; ++ case INDEX_op_ld16u_i32: ++ TODO(); ++ break; ++ case INDEX_op_ld16s_i32: ++ TODO(); ++ break; ++ case INDEX_op_ld_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r(regs, &tb_ptr); ++ t2 = tci_read_s32(&tb_ptr); ++ tci_write_reg32(regs, t0, *(uint32_t *)(t1 + t2)); ++ break; ++ case INDEX_op_st8_i32: ++ t0 = tci_read_r8(regs, &tb_ptr); ++ t1 = tci_read_r(regs, &tb_ptr); ++ t2 = tci_read_s32(&tb_ptr); ++ *(uint8_t *)(t1 + t2) = t0; ++ break; ++ case INDEX_op_st16_i32: ++ t0 = tci_read_r16(regs, &tb_ptr); ++ t1 = tci_read_r(regs, &tb_ptr); ++ t2 = tci_read_s32(&tb_ptr); ++ *(uint16_t *)(t1 + t2) = t0; ++ break; ++ case INDEX_op_st_i32: ++ t0 = tci_read_r32(regs, &tb_ptr); ++ t1 = tci_read_r(regs, &tb_ptr); ++ t2 = tci_read_s32(&tb_ptr); ++ tci_assert(t1 != sp_value || (int32_t)t2 < 0); ++ *(uint32_t *)(t1 + t2) = t0; ++ break; ++ ++ /* Arithmetic operations (32 bit). */ ++ ++ case INDEX_op_add_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri32(regs, &tb_ptr); ++ t2 = tci_read_ri32(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, t1 + t2); ++ break; ++ case INDEX_op_sub_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri32(regs, &tb_ptr); ++ t2 = tci_read_ri32(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, t1 - t2); ++ break; ++ case INDEX_op_mul_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri32(regs, &tb_ptr); ++ t2 = tci_read_ri32(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, t1 * t2); ++ break; ++#if TCG_TARGET_HAS_div_i32 ++ case INDEX_op_div_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri32(regs, &tb_ptr); ++ t2 = tci_read_ri32(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, (int32_t)t1 / (int32_t)t2); ++ break; ++ case INDEX_op_divu_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri32(regs, &tb_ptr); ++ t2 = tci_read_ri32(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, t1 / t2); ++ break; ++ case INDEX_op_rem_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri32(regs, &tb_ptr); ++ t2 = tci_read_ri32(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, (int32_t)t1 % (int32_t)t2); ++ break; ++ case INDEX_op_remu_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri32(regs, &tb_ptr); ++ t2 = tci_read_ri32(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, t1 % t2); ++ break; ++#elif TCG_TARGET_HAS_div2_i32 ++ case INDEX_op_div2_i32: ++ case INDEX_op_divu2_i32: ++ TODO(); ++ break; ++#endif ++ case INDEX_op_and_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri32(regs, &tb_ptr); ++ t2 = tci_read_ri32(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, t1 & t2); ++ break; ++ case INDEX_op_or_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri32(regs, &tb_ptr); ++ t2 = tci_read_ri32(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, t1 | t2); ++ break; ++ case INDEX_op_xor_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri32(regs, &tb_ptr); ++ t2 = tci_read_ri32(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, t1 ^ t2); ++ break; ++ ++ /* Shift/rotate operations (32 bit). */ ++ ++ case INDEX_op_shl_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri32(regs, &tb_ptr); ++ t2 = tci_read_ri32(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, t1 << (t2 & 31)); ++ break; ++ case INDEX_op_shr_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri32(regs, &tb_ptr); ++ t2 = tci_read_ri32(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, t1 >> (t2 & 31)); ++ break; ++ case INDEX_op_sar_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri32(regs, &tb_ptr); ++ t2 = tci_read_ri32(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, ((int32_t)t1 >> (t2 & 31))); ++ break; ++#if TCG_TARGET_HAS_rot_i32 ++ case INDEX_op_rotl_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri32(regs, &tb_ptr); ++ t2 = tci_read_ri32(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, rol32(t1, t2 & 31)); ++ break; ++ case INDEX_op_rotr_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri32(regs, &tb_ptr); ++ t2 = tci_read_ri32(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, ror32(t1, t2 & 31)); ++ break; ++#endif ++#if TCG_TARGET_HAS_deposit_i32 ++ case INDEX_op_deposit_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r32(regs, &tb_ptr); ++ t2 = tci_read_r32(regs, &tb_ptr); ++ tmp16 = *tb_ptr++; ++ tmp8 = *tb_ptr++; ++ tmp32 = (((1 << tmp8) - 1) << tmp16); ++ tci_write_reg32(regs, t0, (t1 & ~tmp32) | ((t2 << tmp16) & tmp32)); ++ break; ++#endif ++ case INDEX_op_brcond_i32: ++ t0 = tci_read_r32(regs, &tb_ptr); ++ t1 = tci_read_ri32(regs, &tb_ptr); ++ condition = *tb_ptr++; ++ label = tci_read_label(&tb_ptr); ++ if (tci_compare32(t0, t1, condition)) { ++ tci_assert(tb_ptr == old_code_ptr + op_size); ++ tb_ptr = (uint8_t *)label; ++ continue; ++ } ++ break; ++#if TCG_TARGET_REG_BITS == 32 ++ case INDEX_op_add2_i32: ++ t0 = *tb_ptr++; ++ t1 = *tb_ptr++; ++ tmp64 = tci_read_r64(regs, &tb_ptr); ++ tmp64 += tci_read_r64(regs, &tb_ptr); ++ tci_write_reg64(regs, t1, t0, tmp64); ++ break; ++ case INDEX_op_sub2_i32: ++ t0 = *tb_ptr++; ++ t1 = *tb_ptr++; ++ tmp64 = tci_read_r64(regs, &tb_ptr); ++ tmp64 -= tci_read_r64(regs, &tb_ptr); ++ tci_write_reg64(regs, t1, t0, tmp64); ++ break; ++ case INDEX_op_brcond2_i32: ++ tmp64 = tci_read_r64(regs, &tb_ptr); ++ v64 = tci_read_ri64(regs, &tb_ptr); ++ condition = *tb_ptr++; ++ label = tci_read_label(&tb_ptr); ++ if (tci_compare64(tmp64, v64, condition)) { ++ tci_assert(tb_ptr == old_code_ptr + op_size); ++ tb_ptr = (uint8_t *)label; ++ continue; ++ } ++ break; ++ case INDEX_op_mulu2_i32: ++ t0 = *tb_ptr++; ++ t1 = *tb_ptr++; ++ t2 = tci_read_r32(regs, &tb_ptr); ++ tmp64 = tci_read_r32(regs, &tb_ptr); ++ tci_write_reg64(regs, t1, t0, t2 * tmp64); ++ break; ++#endif /* TCG_TARGET_REG_BITS == 32 */ ++#if TCG_TARGET_HAS_ext8s_i32 ++ case INDEX_op_ext8s_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r8s(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, t1); ++ break; ++#endif ++#if TCG_TARGET_HAS_ext16s_i32 ++ case INDEX_op_ext16s_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r16s(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, t1); ++ break; ++#endif ++#if TCG_TARGET_HAS_ext8u_i32 ++ case INDEX_op_ext8u_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r8(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, t1); ++ break; ++#endif ++#if TCG_TARGET_HAS_ext16u_i32 ++ case INDEX_op_ext16u_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r16(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, t1); ++ break; ++#endif ++#if TCG_TARGET_HAS_bswap16_i32 ++ case INDEX_op_bswap16_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r16(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, bswap16(t1)); ++ break; ++#endif ++#if TCG_TARGET_HAS_bswap32_i32 ++ case INDEX_op_bswap32_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r32(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, bswap32(t1)); ++ break; ++#endif ++#if TCG_TARGET_HAS_not_i32 ++ case INDEX_op_not_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r32(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, ~t1); ++ break; ++#endif ++#if TCG_TARGET_HAS_neg_i32 ++ case INDEX_op_neg_i32: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r32(regs, &tb_ptr); ++ tci_write_reg32(regs, t0, -t1); ++ break; ++#endif ++#if TCG_TARGET_REG_BITS == 64 ++ case INDEX_op_mov_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r64(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, t1); ++ break; ++ case INDEX_op_movi_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_i64(&tb_ptr); ++ tci_write_reg64(regs, t0, t1); ++ break; ++ ++ /* Load/store operations (64 bit). */ ++ ++ case INDEX_op_ld8u_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r(regs, &tb_ptr); ++ t2 = tci_read_s32(&tb_ptr); ++ tci_write_reg8(regs, t0, *(uint8_t *)(t1 + t2)); ++ break; ++ case INDEX_op_ld8s_i64: ++ TODO(); ++ break; ++ case INDEX_op_ld16u_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r(regs, &tb_ptr); ++ t2 = tci_read_s32(&tb_ptr); ++ tci_write_reg16(regs, t0, *(uint16_t *)(t1 + t2)); ++ break; ++ case INDEX_op_ld16s_i64: ++ TODO(); ++ break; ++ case INDEX_op_ld32u_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r(regs, &tb_ptr); ++ t2 = tci_read_s32(&tb_ptr); ++ tci_write_reg32(regs, t0, *(uint32_t *)(t1 + t2)); ++ break; ++ case INDEX_op_ld32s_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r(regs, &tb_ptr); ++ t2 = tci_read_s32(&tb_ptr); ++ tci_write_reg32s(regs, t0, *(int32_t *)(t1 + t2)); ++ break; ++ case INDEX_op_ld_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r(regs, &tb_ptr); ++ t2 = tci_read_s32(&tb_ptr); ++ tci_write_reg64(regs, t0, *(uint64_t *)(t1 + t2)); ++ break; ++ case INDEX_op_st8_i64: ++ t0 = tci_read_r8(regs, &tb_ptr); ++ t1 = tci_read_r(regs, &tb_ptr); ++ t2 = tci_read_s32(&tb_ptr); ++ *(uint8_t *)(t1 + t2) = t0; ++ break; ++ case INDEX_op_st16_i64: ++ t0 = tci_read_r16(regs, &tb_ptr); ++ t1 = tci_read_r(regs, &tb_ptr); ++ t2 = tci_read_s32(&tb_ptr); ++ *(uint16_t *)(t1 + t2) = t0; ++ break; ++ case INDEX_op_st32_i64: ++ t0 = tci_read_r32(regs, &tb_ptr); ++ t1 = tci_read_r(regs, &tb_ptr); ++ t2 = tci_read_s32(&tb_ptr); ++ *(uint32_t *)(t1 + t2) = t0; ++ break; ++ case INDEX_op_st_i64: ++ t0 = tci_read_r64(regs, &tb_ptr); ++ t1 = tci_read_r(regs, &tb_ptr); ++ t2 = tci_read_s32(&tb_ptr); ++ tci_assert(t1 != sp_value || (int32_t)t2 < 0); ++ *(uint64_t *)(t1 + t2) = t0; ++ break; ++ ++ /* Arithmetic operations (64 bit). */ ++ ++ case INDEX_op_add_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri64(regs, &tb_ptr); ++ t2 = tci_read_ri64(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, t1 + t2); ++ break; ++ case INDEX_op_sub_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri64(regs, &tb_ptr); ++ t2 = tci_read_ri64(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, t1 - t2); ++ break; ++ case INDEX_op_mul_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri64(regs, &tb_ptr); ++ t2 = tci_read_ri64(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, t1 * t2); ++ break; ++#if TCG_TARGET_HAS_div_i64 ++ case INDEX_op_div_i64: ++ case INDEX_op_divu_i64: ++ case INDEX_op_rem_i64: ++ case INDEX_op_remu_i64: ++ TODO(); ++ break; ++#elif TCG_TARGET_HAS_div2_i64 ++ case INDEX_op_div2_i64: ++ case INDEX_op_divu2_i64: ++ TODO(); ++ break; ++#endif ++ case INDEX_op_and_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri64(regs, &tb_ptr); ++ t2 = tci_read_ri64(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, t1 & t2); ++ break; ++ case INDEX_op_or_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri64(regs, &tb_ptr); ++ t2 = tci_read_ri64(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, t1 | t2); ++ break; ++ case INDEX_op_xor_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri64(regs, &tb_ptr); ++ t2 = tci_read_ri64(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, t1 ^ t2); ++ break; ++ ++ /* Shift/rotate operations (64 bit). */ ++ ++ case INDEX_op_shl_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri64(regs, &tb_ptr); ++ t2 = tci_read_ri64(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, t1 << (t2 & 63)); ++ break; ++ case INDEX_op_shr_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri64(regs, &tb_ptr); ++ t2 = tci_read_ri64(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, t1 >> (t2 & 63)); ++ break; ++ case INDEX_op_sar_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri64(regs, &tb_ptr); ++ t2 = tci_read_ri64(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, ((int64_t)t1 >> (t2 & 63))); ++ break; ++#if TCG_TARGET_HAS_rot_i64 ++ case INDEX_op_rotl_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri64(regs, &tb_ptr); ++ t2 = tci_read_ri64(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, rol64(t1, t2 & 63)); ++ break; ++ case INDEX_op_rotr_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_ri64(regs, &tb_ptr); ++ t2 = tci_read_ri64(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, ror64(t1, t2 & 63)); ++ break; ++#endif ++#if TCG_TARGET_HAS_deposit_i64 ++ case INDEX_op_deposit_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r64(regs, &tb_ptr); ++ t2 = tci_read_r64(regs, &tb_ptr); ++ tmp16 = *tb_ptr++; ++ tmp8 = *tb_ptr++; ++ tmp64 = (((1ULL << tmp8) - 1) << tmp16); ++ tci_write_reg64(regs, t0, (t1 & ~tmp64) | ((t2 << tmp16) & tmp64)); ++ break; ++#endif ++ case INDEX_op_brcond_i64: ++ t0 = tci_read_r64(regs, &tb_ptr); ++ t1 = tci_read_ri64(regs, &tb_ptr); ++ condition = *tb_ptr++; ++ label = tci_read_label(&tb_ptr); ++ if (tci_compare64(t0, t1, condition)) { ++ tci_assert(tb_ptr == old_code_ptr + op_size); ++ tb_ptr = (uint8_t *)label; ++ continue; ++ } ++ break; ++#if TCG_TARGET_HAS_ext8u_i64 ++ case INDEX_op_ext8u_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r8(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, t1); ++ break; ++#endif ++#if TCG_TARGET_HAS_ext8s_i64 ++ case INDEX_op_ext8s_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r8s(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, t1); ++ break; ++#endif ++#if TCG_TARGET_HAS_ext16s_i64 ++ case INDEX_op_ext16s_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r16s(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, t1); ++ break; ++#endif ++#if TCG_TARGET_HAS_ext16u_i64 ++ case INDEX_op_ext16u_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r16(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, t1); ++ break; ++#endif ++#if TCG_TARGET_HAS_ext32s_i64 ++ case INDEX_op_ext32s_i64: ++#endif ++ case INDEX_op_ext_i32_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r32s(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, t1); ++ break; ++#if TCG_TARGET_HAS_ext32u_i64 ++ case INDEX_op_ext32u_i64: ++#endif ++ case INDEX_op_extu_i32_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r32(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, t1); ++ break; ++#if TCG_TARGET_HAS_bswap16_i64 ++ case INDEX_op_bswap16_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r16(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, bswap16(t1)); ++ break; ++#endif ++#if TCG_TARGET_HAS_bswap32_i64 ++ case INDEX_op_bswap32_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r32(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, bswap32(t1)); ++ break; ++#endif ++#if TCG_TARGET_HAS_bswap64_i64 ++ case INDEX_op_bswap64_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r64(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, bswap64(t1)); ++ break; ++#endif ++#if TCG_TARGET_HAS_not_i64 ++ case INDEX_op_not_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r64(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, ~t1); ++ break; ++#endif ++#if TCG_TARGET_HAS_neg_i64 ++ case INDEX_op_neg_i64: ++ t0 = *tb_ptr++; ++ t1 = tci_read_r64(regs, &tb_ptr); ++ tci_write_reg64(regs, t0, -t1); ++ break; ++#endif ++#endif /* TCG_TARGET_REG_BITS == 64 */ ++ ++ /* QEMU specific operations. */ ++ ++ case INDEX_op_exit_tb: ++ ret = *(uint64_t *)tb_ptr; ++ goto exit; ++ break; ++ case INDEX_op_goto_tb: ++ /* Jump address is aligned */ ++ tb_ptr = QEMU_ALIGN_PTR_UP(tb_ptr, 4); ++ t0 = atomic_read((int32_t *)tb_ptr); ++ tb_ptr += sizeof(int32_t); ++ tci_assert(tb_ptr == old_code_ptr + op_size); ++ tb_ptr += (int32_t)t0; ++ continue; ++ case INDEX_op_qemu_ld_i32: ++ t0 = *tb_ptr++; ++ taddr = tci_read_ulong(regs, &tb_ptr); ++ oi = tci_read_i(&tb_ptr); ++ switch (get_memop(oi) & (MO_BSWAP | MO_SSIZE)) { ++ case MO_UB: ++ tmp32 = qemu_ld_ub; ++ break; ++ case MO_SB: ++ tmp32 = (int8_t)qemu_ld_ub; ++ break; ++ case MO_LEUW: ++ tmp32 = qemu_ld_leuw; ++ break; ++ case MO_LESW: ++ tmp32 = (int16_t)qemu_ld_leuw; ++ break; ++ case MO_LEUL: ++ tmp32 = qemu_ld_leul; ++ break; ++ case MO_BEUW: ++ tmp32 = qemu_ld_beuw; ++ break; ++ case MO_BESW: ++ tmp32 = (int16_t)qemu_ld_beuw; ++ break; ++ case MO_BEUL: ++ tmp32 = qemu_ld_beul; ++ break; ++ default: ++ tcg_abort(); ++ } ++ tci_write_reg(regs, t0, tmp32); ++ break; ++ case INDEX_op_qemu_ld_i64: ++ t0 = *tb_ptr++; ++ if (TCG_TARGET_REG_BITS == 32) { ++ t1 = *tb_ptr++; ++ } ++ taddr = tci_read_ulong(regs, &tb_ptr); ++ oi = tci_read_i(&tb_ptr); ++ switch (get_memop(oi) & (MO_BSWAP | MO_SSIZE)) { ++ case MO_UB: ++ tmp64 = qemu_ld_ub; ++ break; ++ case MO_SB: ++ tmp64 = (int8_t)qemu_ld_ub; ++ break; ++ case MO_LEUW: ++ tmp64 = qemu_ld_leuw; ++ break; ++ case MO_LESW: ++ tmp64 = (int16_t)qemu_ld_leuw; ++ break; ++ case MO_LEUL: ++ tmp64 = qemu_ld_leul; ++ break; ++ case MO_LESL: ++ tmp64 = (int32_t)qemu_ld_leul; ++ break; ++ case MO_LEQ: ++ tmp64 = qemu_ld_leq; ++ break; ++ case MO_BEUW: ++ tmp64 = qemu_ld_beuw; ++ break; ++ case MO_BESW: ++ tmp64 = (int16_t)qemu_ld_beuw; ++ break; ++ case MO_BEUL: ++ tmp64 = qemu_ld_beul; ++ break; ++ case MO_BESL: ++ tmp64 = (int32_t)qemu_ld_beul; ++ break; ++ case MO_BEQ: ++ tmp64 = qemu_ld_beq; ++ break; ++ default: ++ tcg_abort(); ++ } ++ tci_write_reg(regs, t0, tmp64); ++ if (TCG_TARGET_REG_BITS == 32) { ++ tci_write_reg(regs, t1, tmp64 >> 32); ++ } ++ break; ++ case INDEX_op_qemu_st_i32: ++ t0 = tci_read_r(regs, &tb_ptr); ++ taddr = tci_read_ulong(regs, &tb_ptr); ++ oi = tci_read_i(&tb_ptr); ++ switch (get_memop(oi) & (MO_BSWAP | MO_SIZE)) { ++ case MO_UB: ++ qemu_st_b(t0); ++ break; ++ case MO_LEUW: ++ qemu_st_lew(t0); ++ break; ++ case MO_LEUL: ++ qemu_st_lel(t0); ++ break; ++ case MO_BEUW: ++ qemu_st_bew(t0); ++ break; ++ case MO_BEUL: ++ qemu_st_bel(t0); ++ break; ++ default: ++ tcg_abort(); ++ } ++ break; ++ case INDEX_op_qemu_st_i64: ++ tmp64 = tci_read_r64(regs, &tb_ptr); ++ taddr = tci_read_ulong(regs, &tb_ptr); ++ oi = tci_read_i(&tb_ptr); ++ switch (get_memop(oi) & (MO_BSWAP | MO_SIZE)) { ++ case MO_UB: ++ qemu_st_b(tmp64); ++ break; ++ case MO_LEUW: ++ qemu_st_lew(tmp64); ++ break; ++ case MO_LEUL: ++ qemu_st_lel(tmp64); ++ break; ++ case MO_LEQ: ++ qemu_st_leq(tmp64); ++ break; ++ case MO_BEUW: ++ qemu_st_bew(tmp64); ++ break; ++ case MO_BEUL: ++ qemu_st_bel(tmp64); ++ break; ++ case MO_BEQ: ++ qemu_st_beq(tmp64); ++ break; ++ default: ++ tcg_abort(); ++ } ++ break; ++ case INDEX_op_mb: ++ /* Ensure ordering for all kinds */ ++ smp_mb(); ++ break; ++ default: ++ TODO(); ++ break; ++ } ++ tci_assert(tb_ptr == old_code_ptr + op_size); ++ } ++exit: ++ return ret; ++} +diff --git a/qemu/util/osdep.c b/qemu/util/osdep.c +index 148e37a..a9e5226 100644 +--- a/qemu/util/osdep.c ++++ b/qemu/util/osdep.c +@@ -62,6 +62,11 @@ static int qemu_mprotect__osdep(void *addr, size_t size, int prot) + return -1; + } + return 0; ++#elif defined(__EMSCRIPTEN__) ++ /* WebAssembly has no page protection; the TCI code buffer is plain ++ data (interpreted bytecode), so protection requests are no-ops. */ ++ (void)addr; (void)size; (void)prot; ++ return 0; + #else + if (mprotect(addr, size, prot)) { + // error_report("%s: mprotect failed: %s", __func__, strerror(errno)); +@@ -71,6 +76,13 @@ static int qemu_mprotect__osdep(void *addr, size_t size, int prot) + #endif + } + ++#ifdef __EMSCRIPTEN__ ++#define PROT_NONE 0 ++#define PROT_READ 1 ++#define PROT_WRITE 2 ++#define PROT_EXEC 4 ++#endif ++ + int qemu_mprotect_rwx(void *addr, size_t size) + { + #ifdef _WIN32 +diff --git a/qemu/util/oslib-posix.c b/qemu/util/oslib-posix.c +index 615e477..f470861 100644 +--- a/qemu/util/oslib-posix.c ++++ b/qemu/util/oslib-posix.c +@@ -29,7 +29,11 @@ + #include + #include "qemu/osdep.h" + +-#ifdef CONFIG_LINUX ++#if defined(__EMSCRIPTEN__) ++#include ++#define MAP_SYNC 0x0 ++#define MAP_SHARED_VALIDATE 0x0 ++#elif defined(CONFIG_LINUX) + #include + #else /* !CONFIG_LINUX */ + #define MAP_SYNC 0x0 +@@ -188,6 +192,17 @@ static void *qemu_ram_mmap(struct uc_struct *uc, + void *guardptr; + void *ptr; + ++#ifdef __EMSCRIPTEN__ ++ /* wasm32 has no real mmap (MAP_FIXED cannot be honored), so serve guest ++ RAM from an over-allocated malloc block aligned up in wasm heap. */ ++ { ++ void *raw = malloc(size + align); ++ if (raw == NULL) { ++ return MAP_FAILED; ++ } ++ return (void *)QEMU_ALIGN_UP((uintptr_t)raw, align); ++ } ++#else + /* + * Note: this always allocates at least one extra page of virtual address + * space, even if size is already aligned. +@@ -265,6 +280,7 @@ static void *qemu_ram_mmap(struct uc_struct *uc, + } + + return ptr; ++#endif /* __EMSCRIPTEN__ */ + } + + static void qemu_ram_munmap(struct uc_struct *uc, void *ptr, size_t size) +diff --git a/uc.c b/uc.c +index 75b89a0..2054584 100644 +--- a/uc.c ++++ b/uc.c +@@ -1036,8 +1036,14 @@ static void *_timeout_fn(void *arg) + static void enable_emu_timer(uc_engine *uc, uint64_t timeout) + { + uc->timeout = timeout; ++#ifdef __EMSCRIPTEN__ ++ /* wasm has no threads; the TCI interpreter enforces the wall-clock ++ deadline itself (see tcg/tci.c). */ ++ (void)uc; ++#else + qemu_thread_create(uc, &uc->timer, "timeout", _timeout_fn, uc, + QEMU_THREAD_JOINABLE); ++#endif + } + + static void hook_count_cb(struct uc_struct *uc, uint64_t address, uint32_t size, +@@ -1243,7 +1249,9 @@ uc_err uc_emu_start(uc_engine *uc, uint64_t begin, uint64_t until, + + if (timeout) { + // wait for the timer to finish ++ #ifndef __EMSCRIPTEN__ + qemu_thread_join(&uc->timer); ++#endif + } + + // We may be in a nested uc_emu_start and thus clear invalid_error diff --git a/frontend/src/apple/authenticate.ts b/frontend/src/apple/authenticate.ts index 61550f05..f0ad8893 100644 --- a/frontend/src/apple/authenticate.ts +++ b/frontend/src/apple/authenticate.ts @@ -3,6 +3,7 @@ import { appleRequest } from "./request"; import { buildPlist, parsePlist } from "./plist"; import { extractAndMergeCookies } from "./cookies"; import { fetchBag, defaultAuthURL } from "./bag"; +import { prepareSigner } from "./sap/client"; import i18n from "../i18n"; export class AuthenticationError extends Error { @@ -37,6 +38,16 @@ export async function authenticate( requestHost = authEndpoint.hostname; requestPath = `${authEndpoint.pathname}${authEndpoint.search}`; + // When the bag advertises the SAP signing protocol, every request to the + // auth endpoint must carry X-Apple-ActionSignature over its body bytes. + // The signer sees only the hardware ID and public Apple assets — never the + // password — because signing happens here in the browser. It is kept as a + // singleton between attempts (2FA retries reuse the same session). + let sapSigner = null as Awaited> | null; + if (bag.sapEndpoints) { + sapSigner = await prepareSigner(deviceId, bag.sapEndpoints); + } + let currentAttempt = 0; let redirectAttempt = 0; @@ -59,6 +70,14 @@ export async function authenticate( "Content-Type": "application/x-apple-plist", }; + if (sapSigner) { + // The signature must cover the exact bytes on the wire; libcurl sends + // the body string as UTF-8, so sign its encoded form. + headers["X-Apple-ActionSignature"] = await sapSigner.sign( + new TextEncoder().encode(plistBody), + ); + } + const response = await appleRequest({ method: "POST", host: requestHost, @@ -151,7 +170,9 @@ export async function authenticate( return account; } catch (e) { - if (e instanceof AuthenticationError) throw e; + if (e instanceof AuthenticationError) { + throw e; + } lastError = e instanceof Error ? e : new Error(String(e)); } } diff --git a/frontend/src/apple/bag.ts b/frontend/src/apple/bag.ts index ca4f3bc9..0aaf8969 100644 --- a/frontend/src/apple/bag.ts +++ b/frontend/src/apple/bag.ts @@ -1,8 +1,11 @@ import { authHeaders } from "../api/client"; import { parsePlist } from "./plist"; +import type { SapEndpoints } from "./sap/types"; export interface BagOutput { authURL: string; + /** Present when the bag advertises the SAP signing protocol. */ + sapEndpoints?: SapEndpoints; } export const defaultAuthURL = @@ -57,14 +60,29 @@ export async function fetchBag(deviceId: string): Promise { (dict.authenticateAccount as string | undefined) ?? (urlBag?.authenticateAccount as string | undefined); + const bagValue = (key: string): string | undefined => + (dict[key] as string | undefined) ?? + (urlBag?.[key] as string | undefined); + + const setupURL = bagValue("sign-sap-setup"); + const certificateURL = bagValue("sign-sap-setup-cert"); + const versionText = bagValue("sign-sap-version"); + let sapEndpoints: SapEndpoints | undefined; + if (setupURL && certificateURL && versionText) { + const version = Number.parseInt(versionText, 10); + if (Number.isFinite(version)) { + sapEndpoints = { setupURL, certificateURL, version }; + } + } + if (!authURL) { console.warn( "[Bag] authenticateAccount URL not found in bag, using default auth endpoint", ); - return { authURL: defaultAuthURL }; + return { authURL: defaultAuthURL, sapEndpoints }; } - return { authURL: normalizeAuthURL(authURL) }; + return { authURL: normalizeAuthURL(authURL), sapEndpoints }; } catch (error) { console.warn( `[Bag] Failed to fetch/parse bag, using default auth endpoint: ${ diff --git a/frontend/src/apple/sap/assets.ts b/frontend/src/apple/sap/assets.ts new file mode 100644 index 00000000..1a131073 --- /dev/null +++ b/frontend/src/apple/sap/assets.ts @@ -0,0 +1,161 @@ +// SAP asset delivery: the four Apple binaries (CoreFP, CoreFP.icxs, +// CommerceKit, CommerceCore) are extracted once by the backend from a public +// Apple software update package and served from /api/sap-assets. The browser +// caches them in the Cache API keyed by their pinned SHA-256 digests. + +import { authHeaders } from "../../api/client"; +import type { SapAssetBundle } from "./types"; + +export interface SapAssetSpec { + name: string; + /** Size/digest of the distributed (backend-stripped) x86_64 file. */ + size: number; + sha256: string; +} + +/** + * Digests of the distributed assets. The backend strips fat binaries to their + * x86_64 slice (the emulated guest architecture) after verifying Apple's + * original digests during extraction — see + * backend/src/services/sapAssets.ts for both pin sets. + */ +export const SAP_ASSET_SPECS: SapAssetSpec[] = [ + { + name: "CommerceKit", + size: 3271840, + sha256: "b84ff12c21987856c0a17b78f1ad82b73195a6dec5f3b208a17d245555a2c8a2", + }, + { + name: "CommerceCore", + size: 115712, + sha256: "05707cd937798f2b5189471f513672ac6242ffbffc38f06ed2e4fb4345156819", + }, + { + name: "CoreFP", + size: 14904192, + sha256: "97c899f2fb076bdf7f810fe00ceb335d4af85efab0f2de737ad0aedd991c8277", + }, + { + name: "CoreFP.icxs", + size: 5288352, + sha256: "473e78af86979f5bd4f6269561caf770b3d16c098d918846eeac8cdd2fe6566a", + }, +]; + +const CACHE_NAME = "asspp-sap-assets-v1"; + +async function fetchWithProgress( + url: string, + onProgress?: (loaded: number, total: number) => void, +): Promise { + const response = await fetch(url, { headers: authHeaders() }); + if (!response.ok) { + throw new Error(`SAP asset download failed: HTTP ${response.status}`); + } + const total = Number(response.headers.get("content-length") ?? 0); + if (!response.body) { + const buffer = await response.arrayBuffer(); + return new Uint8Array(buffer); + } + const reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + let loaded = 0; + for (;;) { + const { done, value } = await reader.read(); + if (done) { + break; + } + chunks.push(value); + loaded += value.length; + onProgress?.(loaded, total); + } + const assembled = new Uint8Array(loaded); + let offset = 0; + for (const chunk of chunks) { + assembled.set(chunk, offset); + offset += chunk.length; + } + return assembled; +} + +async function digestMatches(data: Uint8Array, expected: string): Promise { + const view = new Uint8Array(data.length); + view.set(data); + const digest = await crypto.subtle.digest("SHA-256", view.buffer as ArrayBuffer); + const actual = Array.from(new Uint8Array(digest)) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(""); + return actual === expected; +} + +/** + * Loads all four SAP assets, verifying digests. Served bytes come from the + * backend cache (public Apple data); the browser persists them in the Cache + * API so the ~38 MiB transfer happens once. + */ +export async function loadSapAssets( + onProgress?: (loadedBytes: number, totalBytes: number) => void, +): Promise { + const totalBytes = SAP_ASSET_SPECS.reduce((sum, spec) => sum + spec.size, 0); + let loadedBytes = 0; + let cache: Cache | null = null; + try { + cache = await caches.open(CACHE_NAME); + } catch { + // Cache API unavailable (private mode); fall back to direct downloads. + } + + const bundle: Record = {}; + for (const spec of SAP_ASSET_SPECS) { + let data: Uint8Array | null = null; + + if (cache) { + const cached = await cache.match(`/api/sap-assets/${spec.name}`); + if (cached) { + const buffer = await cached.arrayBuffer(); + const cachedBytes = new Uint8Array(buffer); + if (await digestMatches(cachedBytes, spec.sha256)) { + data = new Uint8Array(cachedBytes); + } + } + } + + if (!data) { + data = await fetchWithProgress(`/api/sap-assets/${spec.name}`, (loaded) => + onProgress?.(loadedBytes + loaded, totalBytes), + ); + if (!(await digestMatches(data, spec.sha256))) { + throw new Error(`SAP asset ${spec.name} failed integrity verification`); + } + if (cache) { + const copy = new Uint8Array(data); + await cache.put( + `/api/sap-assets/${spec.name}`, + new Response(copy.buffer as ArrayBuffer, { + headers: { "Content-Type": "application/octet-stream" }, + }), + ); + } + } + + loadedBytes += spec.size; + onProgress?.(loadedBytes, totalBytes); + bundle[spec.name] = data; + } + + return { + commerceKit: bundle["CommerceKit"], + commerceCore: bundle["CommerceCore"], + coreFP: bundle["CoreFP"], + coreFPICXS: bundle["CoreFP.icxs"], + }; +} + +/** Drops the cached assets (used after a digest pin update). */ +export async function clearSapAssets(): Promise { + try { + await caches.delete(CACHE_NAME); + } catch { + // ignore + } +} diff --git a/frontend/src/apple/sap/client.ts b/frontend/src/apple/sap/client.ts new file mode 100644 index 00000000..f548865d --- /dev/null +++ b/frontend/src/apple/sap/client.ts @@ -0,0 +1,258 @@ +// Main-thread SAP signer manager. +// +// The signer is kept as a module-level singleton bound to the hardware id it +// was initialized with: creating the worker copies the ~22.5 MB asset bundle +// into it, and repeating that per sign-in (2FA retries included) is pure +// waste. `prepareSigner` reuses a matching signer or rebuilds for a different +// account; concurrent callers share the same preparation. The zustand store +// in store/sap.ts carries progress for the UI. + +import { SapSigner, type SapMachineDriver } from "./signer"; +import { exchangeSetupBuffer, fetchSetupCertificate } from "./protocol"; +import { loadSapAssets } from "./assets"; +import type { SapEndpoints } from "./types"; +import { useSapStore } from "../../store/sap"; + +interface WorkerResult { + type: "result"; + id: number; + [key: string]: unknown; +} + +interface WorkerError { + type: "error"; + id: number; + message: string; +} + +const SETUP_TIMEOUT_MS = 2 * 60 * 1000; + +class WorkerMachineDriver implements SapMachineDriver { + private nextId = 1; + private readonly pending = new Map< + number, + { resolve: (value: WorkerResult) => void; reject: (error: Error) => void } + >(); + + constructor(private readonly worker: Worker) { + worker.onmessage = (event: MessageEvent) => { + const message = event.data; + const entry = this.pending.get(message.id); + if (!entry) { + return; + } + this.pending.delete(message.id); + if (message.type === "error") { + entry.reject(new Error(message.message)); + } else { + entry.resolve(message); + } + }; + } + + call(request: Record): Promise { + const id = this.nextId++; + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve, reject }); + this.worker.postMessage({ ...request, id }); + }); + } + + async open( + assets: { + commerceKit: Uint8Array; + commerceCore: Uint8Array; + coreFP: Uint8Array; + coreFPICXS: Uint8Array; + }, + wasmBinary: ArrayBuffer, + ): Promise { + await this.call({ + type: "open", + assets: { + commerceKit: assets.commerceKit.slice().buffer, + commerceCore: assets.commerceCore.slice().buffer, + coreFP: assets.coreFP.slice().buffer, + coreFPICXS: assets.coreFPICXS.slice().buffer, + }, + wasmBinary, + }); + } + + async initialize(hardwareID: Uint8Array): Promise { + const copy = hardwareID.slice(); + const result = await this.call({ + type: "initialize", + hardwareID: copy.buffer, + }); + return result.contextValue as number; + } + + async exchange( + version: number, + hardwareID: Uint8Array, + contextValue: number, + input: Uint8Array, + ): Promise<{ output: Uint8Array; state: number }> { + const hw = hardwareID.slice(); + const payload = input.slice(); + const result = await this.call({ + type: "exchange", + version, + hardwareID: hw.buffer, + contextValue, + input: payload.buffer, + }); + return { + output: new Uint8Array(result.output as ArrayBuffer), + state: result.state as number, + }; + } + + async sign(contextValue: number, input: Uint8Array): Promise { + const payload = input.slice(); + const result = await this.call({ + type: "sign", + contextValue, + input: payload.buffer, + }); + return new Uint8Array(result.signature as ArrayBuffer); + } + + async teardown(contextValue: number): Promise { + await this.call({ type: "teardown", contextValue }); + } + + async close(): Promise { + await this.call({ type: "close" }); + this.worker.terminate(); + } +} + +interface PreparedSigner { + signer: SapSigner; + driver: WorkerMachineDriver; + hardwareID: string; + endpoints: SapEndpoints; +} + +let prepared: PreparedSigner | null = null; +let preparation: Promise | null = null; +let wasmBinary: ArrayBuffer | null = null; + +async function loadWorkerWasmBinary(): Promise { + if (wasmBinary) { + return wasmBinary; + } + const url = new URL("./vendor/unicorn.wasm", import.meta.url); + const response = await fetch(url); + if (!response.ok) { + throw new Error(`SAP engine download failed: HTTP ${response.status}`); + } + wasmBinary = await response.arrayBuffer(); + return wasmBinary; +} + +/** + * Returns a ready signer for the hardware id, reusing the current one when + * it matches. Concurrent callers share a single preparation; the setup + * network exchange rides the wisp tunnel on the main thread. + */ +export async function prepareSigner( + hardwareID: string, + endpoints: SapEndpoints, +): Promise { + if ( + prepared && + prepared.hardwareID === hardwareID && + endpointsEqual(prepared.endpoints, endpoints) + ) { + return prepared.signer; + } + if (preparation) { + const underway = await preparation.catch(() => null); + if ( + underway && + underway.hardwareID === hardwareID && + endpointsEqual(underway.endpoints, endpoints) + ) { + return underway.signer; + } + } + + preparation = runPreparation(hardwareID, endpoints); + try { + return (await preparation).signer; + } finally { + preparation = null; + } +} + +async function runPreparation( + hardwareID: string, + endpoints: SapEndpoints, +): Promise { + useSapStore.getState().begin(hardwareID); + + const previous = prepared; + prepared = null; + try { + // Tear down the old signer first: the worker holds ~160 MB of wasm heap. + await previous?.driver.close().catch(() => undefined); + + const assets = await loadSapAssets((loaded, total) => + useSapStore + .getState() + .setAssets(total ? Math.round((loaded / total) * 100) : 0), + ); + useSapStore.getState().setSetup(); + + const worker = new Worker(new URL("./worker.ts", import.meta.url), { + type: "module", + }); + const driver = new WorkerMachineDriver(worker); + const wasm = await loadWorkerWasmBinary(); + await driver.open(assets, wasm); + + const signer = await SapSigner.create( + { + ...endpoints, + hardwareID: new TextEncoder().encode(hardwareID), + assets, + }, + driver, + { + fetchCertificate: () => fetchSetupCertificate(endpoints), + exchange: (input) => exchangeSetupBuffer(endpoints, input), + }, + ); + + const result = { signer, driver, hardwareID, endpoints }; + prepared = result; + useSapStore.getState().setReady(); + return result; + } catch (error) { + useSapStore + .getState() + .setError(error instanceof Error ? error.message : String(error)); + throw error; + } +} + +/** Signs body bytes, preparing the signer on demand. */ +export async function signWithSap( + hardwareID: string, + endpoints: SapEndpoints, + body: Uint8Array, +): Promise { + const signer = await prepareSigner(hardwareID, endpoints); + return signer.sign(body); +} + +function endpointsEqual(left: SapEndpoints, right: SapEndpoints): boolean { + return ( + left.certificateURL === right.certificateURL && + left.setupURL === right.setupURL && + left.version === right.version + ); +} diff --git a/frontend/src/apple/sap/engine.ts b/frontend/src/apple/sap/engine.ts new file mode 100644 index 00000000..eefed4c0 --- /dev/null +++ b/frontend/src/apple/sap/engine.ts @@ -0,0 +1,289 @@ +// Unicorn 2.x WASM engine wrapper (x86_64 guest) for the SAP signer. +// +// The vendored emscripten build exposes a double-based glue API (see +// frontend/scripts/build-unicorn-wasm.sh): all 64-bit guest addresses cross +// the boundary as JS numbers, which is exact for integers below 2^53 — the +// SAP guest memory map stays below 2^48. + +// Unicorn x86_64 register IDs (unicorn/x86.h). +export const X86_REG = { + RAX: 35, + RCX: 38, + RDI: 39, + RDX: 40, + RIP: 41, + RSI: 43, + RSP: 44, + R8: 106, + R9: 107, +} as const; + +export const UC_ARCH_X86 = 4; +export const UC_MODE_64 = 8; + +interface UnicornGlueModule { + _uc2_open(arch: number, mode: number): number; + _uc2_close(uc: number): number; + _uc2_strerror(code: number): number; + _uc2_mem_map(uc: number, address: number, size: number): number; + _uc2_mem_unmap(uc: number, address: number, size: number): number; + _uc2_mem_write( + uc: number, + address: number, + bufferOffset: number, + length: number, + ): number; + _uc2_mem_read( + uc: number, + address: number, + bufferOffset: number, + length: number, + ): number; + _uc2_reg_write(uc: number, regid: number, value: number): number; + _uc2_reg_read(uc: number, regid: number): number; + _uc2_emu_start( + uc: number, + begin: number, + until: number, + timeoutUs: number, + count: number, + ): number; + _uc2_emu_stop(uc: number): number; + _uc2_hook_add_code(uc: number, begin: number, end: number): number; + _uc2_hook_del(uc: number, handle: number): number; + _uc2_set_code_hook_cb(fpIndex: number): void; + _uc2_scratch_alloc(length: number): number; + _uc2_scratch_free(offset: number): void; + addFunction(fn: (...args: unknown[]) => void, signature: string): number; + removeFunction(fpIndex: number): void; + UTF8ToString(ptr: number): string; + HEAPU8: Uint8Array; +} + +type UnicornFactory = ( + config?: Record, +) => Promise; + +let factoryPromise: Promise | null = null; + +async function loadFactory(): Promise { + if (!factoryPromise) { + factoryPromise = import("./vendor/unicorn.mjs").then( + (mod) => mod.default as UnicornFactory, + ); + } + return factoryPromise; +} + +function throwIfError(module: UnicornGlueModule, code: number, what: string) { + if (code === 0) { + return; + } + const messagePtr = module._uc2_strerror(code); + const detail = messagePtr ? module.UTF8ToString(messagePtr) : `code ${code}`; + throw new Error(`${what}: ${detail}`); +} + +export class UnicornEngine { + /** Set by the machine; invoked for every hooked address. */ + onCodeHook: ((address: number, size: number) => void) | null = null; + + private hookPointer = 0; + + private constructor( + private readonly module: UnicornGlueModule, + private readonly handle: number, + ) {} + + static async open( + options?: { wasmBinary?: ArrayBuffer }, + ): Promise { + const factory = await loadFactory(); + const config: Record = {}; + if (options?.wasmBinary) { + config.wasmBinary = options.wasmBinary; + } + const module = await factory(config); + + const handle = module._uc2_open(UC_ARCH_X86, UC_MODE_64); + if (handle === 0) { + throw new Error("uc_open failed"); + } + + return new UnicornEngine(module, handle); + } + + /** Install the engine-wide code hook callback and register its table slot. */ + attachCodeHook(): void { + if (this.hookPointer !== 0) { + return; + } + let engineRef: UnicornEngine | null = null; + this.hookPointer = this.module.addFunction( + ((address: number, size: number) => { + engineRef?.onCodeHook?.(address, size); + }) as unknown as (...args: unknown[]) => void, + "vdi", + ); + this.module._uc2_set_code_hook_cb(this.hookPointer); + engineRef = this; + } + + detachCodeHook(): void { + if (this.hookPointer === 0) { + return; + } + this.module.removeFunction(this.hookPointer); + this.hookPointer = 0; + } + + memMap(address: number, size: number): void { + throwIfError( + this.module, + this.module._uc2_mem_map(this.handle, address, size), + `mem_map(${address.toString(16)})`, + ); + } + + memUnmap(address: number, size: number): void { + throwIfError( + this.module, + this.module._uc2_mem_unmap(this.handle, address, size), + `mem_unmap(${address.toString(16)})`, + ); + } + + memWrite(address: number, data: Uint8Array): void { + if (data.length === 0) { + return; + } + const offset = this.module._uc2_scratch_alloc(data.length); + if (offset === 0) { + throw new Error("scratch alloc failed"); + } + try { + this.module.HEAPU8.set(data, offset); + throwIfError( + this.module, + this.module._uc2_mem_write( + this.handle, + address, + offset, + data.length, + ), + `mem_write(${address.toString(16)}, ${data.length})`, + ); + } finally { + this.module._uc2_scratch_free(offset); + } + } + + memRead(address: number, length: number): Uint8Array { + const output = new Uint8Array(length); + if (length === 0) { + return output; + } + const offset = this.module._uc2_scratch_alloc(length); + if (offset === 0) { + throw new Error("scratch alloc failed"); + } + try { + throwIfError( + this.module, + this.module._uc2_mem_read(this.handle, address, offset, length), + `mem_read(${address.toString(16)}, ${length})`, + ); + output.set(this.module.HEAPU8.subarray(offset, offset + length)); + return output; + } finally { + this.module._uc2_scratch_free(offset); + } + } + + regRead(register: number): number { + const value = this.module._uc2_reg_read(this.handle, register); + if (value === -1) { + throw new Error(`reg_read(${register}) failed`); + } + return value; + } + + regWrite(register: number, value: number): void { + throwIfError( + this.module, + this.module._uc2_reg_write(this.handle, register, value), + `reg_write(${register})`, + ); + } + + emuStart( + begin: number, + until: number, + timeoutUs: number, + count: number, + ): void { + throwIfError( + this.module, + this.module._uc2_emu_start(this.handle, begin, until, timeoutUs, count), + `emu_start(${begin.toString(16)}, ${until.toString(16)})`, + ); + } + + emuStop(): void { + throwIfError( + this.module, + this.module._uc2_emu_stop(this.handle), + "emu_stop", + ); + } + + /** Temporary diagnostic: report unmapped memory accesses from the guest. */ + onMemoryInvalid: + | ((type: number, address: number, size: number, value: number) => number) + | null = null; + + attachMemoryInvalidHook(): void { + const mod = this.module as unknown as { + _uc2_hook_add_mem_invalid(uc: number): number; + _uc2_set_mem_hook_cb(fp: number): void; + }; + if (!mod._uc2_hook_add_mem_invalid) { + return; + } + let engineRef: UnicornEngine | null = null; + const fp = this.module.addFunction( + ((type: number, address: number, size: number, value: number) => + engineRef?.onMemoryInvalid?.(type, address, size, value) ?? 0) as unknown as ( + ...args: unknown[] + ) => number, + "dddd", + ); + mod._uc2_set_mem_hook_cb(fp); + mod._uc2_hook_add_mem_invalid(this.handle); + engineRef = this; + } + + addCodeHook( + begin: number, + end: number, + ): number { + const handle = this.module._uc2_hook_add_code(this.handle, begin, end); + if (handle === 0) { + throw new Error("hook_add(UC_HOOK_CODE) failed"); + } + return handle; + } + + hookDel(handle: number): void { + throwIfError( + this.module, + this.module._uc2_hook_del(this.handle, handle), + "hook_del", + ); + } + + close(): void { + this.detachCodeHook(); + this.module._uc2_close(this.handle); + } +} diff --git a/frontend/src/apple/sap/machImage.ts b/frontend/src/apple/sap/machImage.ts new file mode 100644 index 00000000..3393101d --- /dev/null +++ b/frontend/src/apple/sap/machImage.ts @@ -0,0 +1,818 @@ +// Minimal Mach-O (x86_64) image parser/relocator for the SAP guest images. +// Ported from ipatool's internal/sap/machimage (which uses blacktop/go-macho). +// +// Supports exactly what the SAP guest needs: fat-binary slicing, LC_SEGMENT_64, +// LC_SYMTAB symbol lookup, and classic dyld_info rebase/bind/weak/lazy fixups. + +const MACHO_MAGIC_64 = 0xfeedfacf; +const FAT_MAGIC = 0xcafebabe; +const FAT_MAGIC_64 = 0xcafebabf; +const CPU_TYPE_X86_64 = 0x01000007; + +const LC_SYMTAB = 0x2; +const LC_SEGMENT_64 = 0x19; +const LC_DYLD_INFO = 0x22; +const LC_DYLD_INFO_ONLY = 0x80000022; + +const REBASE_TYPE_POINTER = 1; + +const BIND_TYPE_POINTER = 1; + +const PAGE_SIZE = 0x1000; +const MAX_IMAGE_SPAN = 1 << 30; +const POINTER_SIZE = 8; + +interface Segment { + name: string; + address: number; + size: number; + fileOff: number; + fileSize: number; +} + +export interface MachBind { + segment: string; + segOffset: number; + name: string; + type: number; + addend: number; +} + +interface MachRebase { + segment: string; + offset: number; + type: number; + value: number; +} + +interface Symbol { + name: string; + value: number; +} + +export interface GuestMemory { + memMap(address: number, size: number): void; + memWrite(address: number, data: Uint8Array): void; +} + +function readCString(view: DataView, offset: number): string { + let end = offset; + const limit = view.byteLength; + while (end < limit && view.getUint8(end) !== 0) { + end++; + } + const bytes = new Uint8Array( + view.buffer, + view.byteOffset + offset, + end - offset, + ); + return new TextDecoder("latin1").decode(bytes); +} + +function align(value: number, alignment: number): number { + // Math-based: JS bitwise ops are 32-bit and truncate SAP guest addresses. + return Math.floor((value + alignment - 1) / alignment) * alignment; +} + +class UlebReader { + private offset: number; + + constructor( + private readonly view: DataView, + start: number, + private readonly end: number, + ) { + this.offset = start; + } + + get position(): number { + return this.offset; + } + + get remaining(): number { + return this.end - this.offset; + } + + readUleb(): number { + // Returns the uleb128 value wrapped to uint64, as a JS number (exact up + // to 2^53; larger values keep only their low bits' magnitude and are only + // used for address arithmetic in segments < 2^48). Callers that must + // distinguish negative deltas (bind ADD_ADDR_ULEB) use readUlebBig. + return Number(this.readUlebBig()); + } + + readUlebBig(): bigint { + let result = 0n; + let shift = 0n; + for (;;) { + const byte = this.readByte(); + result |= BigInt(byte & 0x7f) << shift; + if ((byte & 0x80) === 0) { + return BigInt.asUintN(64, result); + } + shift += 7n; + if (shift > 63n) { + throw new Error("dyld uleb overflow"); + } + } + } + + readSleb(): number { + let result = 0; + let shift = 0; + for (;;) { + const byte = this.readByte(); + result |= (byte & 0x7f) << shift; + shift += 7; + if ((byte & 0x80) === 0) { + if (shift < 64 && (byte & 0x40) !== 0) { + result |= -1 << shift; + } + break; + } + if (shift > 63) { + throw new Error("dyld sleb overflow"); + } + } + return result; + } + + readCString(): string { + const bytes: number[] = []; + for (;;) { + const byte = this.readByte(); + if (byte === 0) { + break; + } + bytes.push(byte); + if (bytes.length > 4096) { + throw new Error("dyld symbol name exceeds 4096 bytes"); + } + } + return String.fromCharCode(...bytes); + } + + readByte(): number { + if (this.offset >= this.end) { + throw new Error("dyld opcode stream truncated"); + } + return this.view.getUint8(this.offset++); + } +} + +export class MachImage { + private base: number; + private segments: Segment[] = []; + private symbols: Map = new Map(); + private rebases: MachRebase[] = []; + private binds: MachBind[] = []; + private relocated = false; + private loadedBase = 0; + + private constructor( + public readonly name: string, + private readonly data: Uint8Array, + ) { + const view = new DataView(data.buffer, data.byteOffset, data.byteLength); + + const header = this.parseHeader(view); + this.base = header.base; + this.segments = header.segments; + this.parseSymtab(view, header.symtab); + this.rebases = this.parseRebases(view, header.dyldInfo); + this.binds = this.parseBinds(view, header.dyldInfo); + this.validateSegments(); + } + + static open(name: string, input: Uint8Array): MachImage { + return new MachImage(name, amd64Slice(input)); + } + + export(name: string, loadBase: number): number { + const address = this.symbols.get(name); + if (address === undefined) { + throw new Error(`find ${name} in ${this.name}: symbol not found`); + } + if (address < this.base) { + throw new Error( + `symbol ${name} in ${this.name} precedes image base`, + ); + } + return loadBase + (address - this.base); + } + + relocate( + loadBase: number, + resolve: (name: string) => number, + ): void { + if (this.relocated) { + throw new Error(`${this.name} is already relocated`); + } + + const view = new DataView( + this.data.buffer, + this.data.byteOffset, + this.data.byteLength, + ); + + for (const relocation of this.rebases) { + if (relocation.type !== REBASE_TYPE_POINTER) { + throw new Error( + `${this.name} uses unsupported rebase type ${relocation.type}`, + ); + } + if (relocation.value < this.base) { + throw new Error( + `${this.name} contains a rebase below its image base`, + ); + } + const offset = this.segmentFileOffsetOrBss( + relocation.segment, + relocation.offset, + POINTER_SIZE, + ); + if (offset < 0) { + continue; + } + const address = loadBase + (relocation.value - this.base); + this.putPointer(view, offset, address); + } + + for (const binding of this.binds) { + // Lazy bind streams may omit SET_TYPE; dyld's default is a pointer bind. + if (binding.type !== 0 && binding.type !== BIND_TYPE_POINTER) { + throw new Error( + `${this.name} uses unsupported bind type ${binding.type} for ${binding.name}`, + ); + } + const offset = this.segmentFileOffsetOrBss( + binding.segment, + binding.segOffset, + POINTER_SIZE, + ); + if (offset < 0) { + continue; + } + const resolved = resolve(binding.name); + this.putPointer(view, offset, resolved + binding.addend); + } + + this.relocated = true; + this.loadedBase = loadBase; + } + + /** Test-only: the relocated image payload (diffing hook). */ + debugBytes(): Uint8Array { + return this.data.slice(); + } + + load(memory: GuestMemory): void { + if (!this.relocated) { + throw new Error(`${this.name} must be relocated before loading`); + } + + let span = 0; + for (const segment of this.segments) { + if (segment.name === "__PAGEZERO" || segment.size === 0) { + continue; + } + if (segment.address < this.base) { + throw new Error( + `segment ${segment.name} in ${this.name} precedes image base`, + ); + } + const end = segment.address - this.base + segment.size; + if (end > MAX_IMAGE_SPAN) { + throw new Error( + `segment ${segment.name} makes ${this.name} too large`, + ); + } + span = Math.max(span, end); + } + + span = align(span, PAGE_SIZE); + if (span === 0) { + throw new Error(`${this.name} has no loadable segments`); + } + + memory.memMap(this.loadedBase, span); + + for (const segment of this.segments) { + if (segment.name === "__PAGEZERO" || segment.fileSize === 0) { + continue; + } + if (segment.fileOff + segment.fileSize > this.data.length) { + throw new Error( + `segment ${segment.name} data exceeds ${this.name}`, + ); + } + const address = this.loadedBase + (segment.address - this.base); + memory.memWrite( + address, + this.data.subarray( + segment.fileOff, + segment.fileOff + segment.fileSize, + ), + ); + } + } + + private parseHeader(view: DataView) { + const cputype = view.getInt32(4, true); + const ncmds = view.getUint32(16, true); + + const segments: Segment[] = []; + let symtab: { symoff: number; nsyms: number; stroff: number; strsize: number } | null = null; + let dyldInfo: { + rebaseOff: number; + rebaseSize: number; + bindOff: number; + bindSize: number; + weakOff: number; + weakSize: number; + lazyOff: number; + lazySize: number; + } | null = null; + + let offset = 32; + for (let index = 0; index < ncmds; index++) { + if (offset + 8 > view.byteLength) { + throw new Error(`load command ${index} exceeds ${this.name}`); + } + const cmd = view.getUint32(offset, true); + const cmdsize = view.getUint32(offset + 4, true); + if (cmdsize < 8 || offset + cmdsize > view.byteLength) { + throw new Error(`load command ${index} is malformed in ${this.name}`); + } + + if (cmd === LC_SEGMENT_64) { + const name = readCString(new DataView(view.buffer, view.byteOffset + offset + 8, 16), 0); + segments.push({ + name, + address: Number(view.getBigUint64(offset + 24, true)), + size: Number(view.getBigUint64(offset + 32, true)), + fileOff: Number(view.getBigUint64(offset + 40, true)), + fileSize: Number(view.getBigUint64(offset + 48, true)), + }); + } else if (cmd === LC_SYMTAB) { + symtab = { + symoff: view.getUint32(offset + 8, true), + nsyms: view.getUint32(offset + 12, true), + stroff: view.getUint32(offset + 16, true), + strsize: view.getUint32(offset + 20, true), + }; + } else if (cmd === LC_DYLD_INFO || cmd === LC_DYLD_INFO_ONLY) { + dyldInfo = { + rebaseOff: view.getUint32(offset + 8, true), + rebaseSize: view.getUint32(offset + 12, true), + bindOff: view.getUint32(offset + 16, true), + bindSize: view.getUint32(offset + 20, true), + weakOff: view.getUint32(offset + 24, true), + weakSize: view.getUint32(offset + 28, true), + lazyOff: view.getUint32(offset + 32, true), + lazySize: view.getUint32(offset + 36, true), + }; + } + + offset += cmdsize; + } + + if (cputype !== CPU_TYPE_X86_64) { + throw new Error( + `open ${this.name}: expected x86-64 Mach-O, found cputype ${cputype}`, + ); + } + + const loadable = segments.filter((s) => s.name !== "__PAGEZERO" && s.size > 0); + if (loadable.length === 0) { + throw new Error(`open ${this.name}: no loadable segments`); + } + const base = loadable.reduce( + (minimum, segment) => Math.min(minimum, segment.address), + Infinity, + ); + + return { base, segments, symtab, dyldInfo }; + } + + private parseSymtab( + view: DataView, + symtab: { symoff: number; nsyms: number; stroff: number; strsize: number } | null, + ): void { + if (!symtab) { + return; + } + const { symoff, nsyms, stroff, strsize } = symtab; + if (symoff + nsyms * 16 > view.byteLength) { + throw new Error(`symbol table exceeds ${this.name}`); + } + if (stroff + strsize > view.byteLength) { + throw new Error(`string table exceeds ${this.name}`); + } + + const strings = new DataView(view.buffer, view.byteOffset + stroff, strsize); + for (let index = 0; index < nsyms; index++) { + const entry = symoff + index * 16; + const strx = view.getUint32(entry, true); + const nType = view.getUint8(entry + 4); + const value = Number(view.getBigUint64(entry + 8, true)); + if (value === 0 || (nType & 0x0e) === 0) { + continue; + } + if (strx >= strsize) { + continue; + } + const name = readCString(strings, strx); + if (!name || this.symbols.has(name)) { + continue; + } + this.symbols.set(name, value); + } + } + + private parseRebases( + view: DataView, + info: { + rebaseOff: number; + rebaseSize: number; + bindOff: number; + bindSize: number; + weakOff: number; + weakSize: number; + lazyOff: number; + lazySize: number; + } | null, + ): MachRebase[] { + if (!info || info.rebaseSize === 0) { + return []; + } + + const reader = new UlebReader(view, info.rebaseOff, info.rebaseOff + info.rebaseSize); + const rebases: MachRebase[] = []; + + let type = 0; + let segment: Segment | null = null; + // Offsets move in uint64 space (negative ADD_ADDR_ULEB deltas are legal). + let offset = 0n; + + const readOriginalPointer = (): number => { + // The pre-rebase pointer value lives in the image itself; rebasing + // computes slide + original (go-macho/dyld semantics). + const fileOffset = this.requireCurrentSegmentFileOffset( + segment, + Number(offset), + ); + return Number(view.getBigUint64(fileOffset, true)); + }; + + const rebaseOnce = () => { + rebases.push({ + segment: segment!.name, + offset: Number(offset), + type, + value: readOriginalPointer(), + }); + }; + + while (reader.remaining > 0) { + const opcode = reader.readByte(); + const immediate = opcode & 0x0f; + const action = opcode & 0xf0; + + switch (action) { + case 0x00: // DONE + return rebases; + case 0x10: // SET_TYPE_IMM + type = immediate; + break; + case 0x20: { + // SET_SEGMENT_AND_OFFSET_ULEB + segment = this.requireSegment(immediate); + offset = reader.readUlebBig(); + break; + } + case 0x30: // ADD_ADDR_ULEB + offset = BigInt.asUintN(64, offset + reader.readUlebBig()); + break; + case 0x40: // ADD_ADDR_IMM_SCALED + offset = BigInt.asUintN(64, offset + BigInt(immediate * POINTER_SIZE)); + break; + case 0x50: { + // DO_REBASE_IMM_TIMES + for (let index = 0; index < immediate; index++) { + rebaseOnce(); + offset = BigInt.asUintN(64, offset + BigInt(POINTER_SIZE)); + } + break; + } + case 0x60: { + // DO_REBASE_ULEB_TIMES + const count = reader.readUleb(); + for (let index = 0; index < count; index++) { + rebaseOnce(); + offset = BigInt.asUintN(64, offset + BigInt(POINTER_SIZE)); + } + break; + } + case 0x70: { + // DO_REBASE_ADD_ADDR_ULEB + rebaseOnce(); + offset = BigInt.asUintN( + 64, + offset + reader.readUlebBig() + BigInt(POINTER_SIZE), + ); + break; + } + case 0x80: { + // DO_REBASE_ULEB_TIMES_SKIPPING_ULEB (skip is in bytes) + const count = reader.readUleb(); + const skip = reader.readUleb(); + for (let index = 0; index < count; index++) { + rebaseOnce(); + offset = BigInt.asUintN(64, offset + BigInt(skip + POINTER_SIZE)); + } + break; + } + default: + throw new Error(`unsupported rebase opcode ${opcode.toString(16)}`); + } + } + + return rebases; + } + + private parseBinds( + view: DataView, + info: { + rebaseOff: number; + rebaseSize: number; + bindOff: number; + bindSize: number; + weakOff: number; + weakSize: number; + lazyOff: number; + lazySize: number; + } | null, + ): MachBind[] { + if (!info) { + return []; + } + + const binds: MachBind[] = []; + this.parseBindStream(view, info.bindOff, info.bindOff + info.bindSize, binds); + this.parseBindStream(view, info.weakOff, info.weakOff + info.weakSize, binds); + this.parseBindStream( + view, + info.lazyOff, + info.lazyOff + info.lazySize, + binds, + true, + ); + + return binds; + } + + private parseBindStream( + view: DataView, + start: number, + end: number, + binds: MachBind[], + isLazy = false, + ): void { + const reader = new UlebReader(view, start, end); + + let type = 0; + let addend = 0; + let symbolName = ""; + let segment: Segment | null = null; + // Segment offsets move in uint64 space: ADD_ADDR_ULEB deltas are 64-bit + // encodings of negative steps, so all arithmetic stays in BigInt. + let segOffset = 0n; + + const bind = () => { + this.requireCurrentSegment(segment); + binds.push({ + segment: segment!.name, + segOffset: Number(segOffset), + name: symbolName, + type, + addend, + }); + }; + + while (reader.remaining > 0) { + const opcode = reader.readByte(); + const immediate = opcode & 0xf; + const action = opcode & 0xf0; + switch (action) { + case 0x00: // DONE + if (opcode === 0x00) { + if (!isLazy) { + return; + } + // Lazy streams terminate each entry with DONE and continue with + // the next; reset the per-entry state (go-macho/dyld semantics). + type = 0; + addend = 0; + symbolName = ""; + segment = null; + segOffset = 0n; + break; + } + break; + case 0x10: // SET_DYLIB_ORDINAL_IMM (ordinal unused: resolve by name) + break; + case 0x20: // SET_DYLIB_ORDINAL_ULEB + reader.readUleb(); + break; + case 0x30: // SET_DYLIB_SPECIAL_IMM + break; + case 0x40: // SET_SYMBOL_TRAILING_FLAGS_IMM + symbolName = reader.readCString(); + break; + case 0x50: // SET_TYPE_IMM + type = immediate; + break; + case 0x60: // SET_ADDEND_SLEB + addend = reader.readSleb(); + break; + case 0x70: { + // SET_SEGMENT_AND_OFFSET_ULEB + segment = this.requireSegment(immediate); + segOffset = reader.readUlebBig(); + break; + } + case 0x80: // ADD_ADDR_ULEB + segOffset = BigInt.asUintN(64, segOffset + reader.readUlebBig()); + break; + // NOTE: the bind opcode table has no ADD_ADDR_IMM_SCALED (that is a + // rebase-only opcode), so DO_BIND sits at 0x90, one slot below rebase. + case 0x90: // DO_BIND + bind(); + segOffset = BigInt.asUintN(64, segOffset + BigInt(POINTER_SIZE)); + break; + case 0xa0: // DO_BIND_ADD_ADDR_ULEB + bind(); + segOffset = BigInt.asUintN( + 64, + segOffset + reader.readUlebBig() + BigInt(POINTER_SIZE), + ); + break; + case 0xb0: // DO_BIND_ADD_ADDR_IMM_SCALED + bind(); + segOffset = BigInt.asUintN( + 64, + segOffset + BigInt(immediate * POINTER_SIZE + POINTER_SIZE), + ); + break; + case 0xc0: { + // DO_BIND_ULEB_TIMES_SKIPPING_ULEB + const count = reader.readUleb(); + const skip = reader.readUleb(); + for (let index = 0; index < count; index++) { + bind(); + segOffset = BigInt.asUintN( + 64, + segOffset + BigInt(skip + POINTER_SIZE), + ); + } + break; + } + case 0xd0: // BIND_OPCODE_THREADED (not present in 10.9-era images) + throw new Error( + `threaded bind opcodes are unsupported in ${this.name} (pos=0x${reader.position.toString(16)}, stream=[0x${start.toString(16)},0x${end.toString(16)}], segOffset=${segOffset}, symbol=${symbolName})`, + ); + default: + throw new Error(`unsupported bind opcode ${opcode.toString(16)}`); + } + } + } + + private requireSegment(index: number): Segment { + const segment = this.segments[index]; + if (!segment) { + throw new Error(`fixup references unknown segment index ${index} in ${this.name}`); + } + return segment; + } + + private requireCurrentSegment(segment: Segment | null): void { + if (!segment) { + throw new Error(`fixup in ${this.name} has no segment selected`); + } + } + + private requireCurrentSegmentFileOffset( + segment: Segment | null, + offset: number, + ): number { + this.requireCurrentSegment(segment); + return this.segmentFileOffset(segment!.name, offset, POINTER_SIZE); + } + + private validateSegments(): void { + for (const segment of this.segments) { + if (segment.fileSize > segment.size) { + throw new Error( + `segment ${segment.name} file data exceeds its memory size in ${this.name}`, + ); + } + if (segment.fileOff + segment.fileSize > this.data.length) { + throw new Error(`segment ${segment.name} data exceeds ${this.name}`); + } + } + } + + private segmentFileOffset(name: string, offset: number, size: number): number { + const result = this.segmentFileOffsetOrBss(name, offset, size); + if (result < 0) { + throw new Error( + `fixup at ${offset.toString(16)} lands in the BSS area of segment ${name} in ${this.name}`, + ); + } + return result; + } + + /** + * File offset for a fixup, or -1 when it targets the segment's BSS tail + * (within vmsize but past fileSize). dyld applies such fixups to the + * zero-filled memory at load time; for our purposes the loaded image is + * equally zero there, so callers skip them. + */ + private segmentFileOffsetOrBss( + name: string, + offset: number, + size: number, + ): number { + for (const segment of this.segments) { + if (segment.name !== name) { + continue; + } + if (offset + size > segment.size) { + throw new Error( + `fixup at ${offset.toString(16)} exceeds segment ${name} in ${this.name}`, + ); + } + if (offset + size > segment.fileSize) { + return -1; + } + const result = segment.fileOff + offset; + if (result + size > this.data.length) { + throw new Error(`fixup at ${result.toString(16)} exceeds ${this.name}`); + } + return result; + } + throw new Error(`fixup references unknown segment ${name} in ${this.name}`); + } + + private putPointer(view: DataView, offset: number, value: number): void { + if (offset + 8 > view.byteLength) { + throw new Error(`fixup at ${offset.toString(16)} exceeds ${this.name}`); + } + view.setUint32(offset, value % 4294967296, true); + view.setUint32(offset + 4, Math.floor(value / 4294967296), true); + } +} + + + +function amd64Slice(input: Uint8Array): Uint8Array { + const view = new DataView(input.buffer, input.byteOffset, input.byteLength); + if (input.length < 4) { + throw new Error("Mach-O input is too small"); + } + + const magic = view.getUint32(0, false); + if (magic !== FAT_MAGIC && magic !== FAT_MAGIC_64) { + return input; + } + + const wide = magic === FAT_MAGIC_64; + const count = view.getUint32(4, false); + const entrySize = wide ? 32 : 20; + if (4 + 8 + count * entrySize > view.byteLength) { + throw new Error("fat header exceeds input size"); + } + + for (let index = 0; index < count; index++) { + const entry = 8 + index * entrySize; + const cputype = view.getInt32(entry, false); + if (cputype !== CPU_TYPE_X86_64) { + continue; + } + // fat_arch(32): cputype, cpusubtype, offset, size, align (5 x u32). + // fat_arch_64: cputype, cpusubtype, offset(u64), size(u64), align, reserved. + const offset = wide + ? Number(view.getBigUint64(entry + 8, false)) + : view.getUint32(entry + 8, false); + const size = wide + ? Number(view.getBigUint64(entry + 16, false)) + : view.getUint32(entry + 12, false); + if (offset + size > input.length) { + throw new Error("x86-64 slice exceeds input size"); + } + return input.subarray(offset, offset + size); + } + + throw new Error("universal binary has no x86-64 slice"); +} diff --git a/frontend/src/apple/sap/machine.ts b/frontend/src/apple/sap/machine.ts new file mode 100644 index 00000000..1471f266 --- /dev/null +++ b/frontend/src/apple/sap/machine.ts @@ -0,0 +1,416 @@ +// SAP guest machine: maps the CommerceKit/CommerceCore/CoreFP images into an +// emulated x86_64 Unicorn instance and drives the obfuscated SAP entry points. +// Ported from ipatool's internal/sap/machine/machine.go. + +import { UnicornEngine, X86_REG } from "./engine"; +import { MachImage } from "./machImage"; +import { Shims, HEAP_BASE, HEAP_SIZE } from "./shims"; + +const SAP_GUEST_TIMEOUT_US = 60 * 1000 * 1000; // one minute, wall clock + +const RETURN_ADDRESS = 0x0000000100000000; +const CORE_FP_BASE = 0x0000100000000000; +const COMMERCE_BASE = 0x0000100040000000; +const KIT_BASE = 0x0000100080000000; +const SCRATCH_BASE = 0x0000300000000000; +const SCRATCH_SIZE = 32 << 20; +const STACK_BASE = 0x0000500000000000; +const STACK_SIZE = 8 << 20; +const STACK_END = STACK_BASE + STACK_SIZE; +const PAGE_SIZE = 0x1000; +const MAX_OUTPUT_SIZE = 16 << 20; + +const CORE_EXPORT_NAMES = [ + "_WIn9UJ86JKdV4dM", + "_X46O5IeS", + "_YlCJ3lg", + "_dku592fbFAj", + "_fdjkDSAFjklaf2s", + "_lxpgvVMLd0S7uRl", +]; + +const ENTRY_INITIALIZE = "_cp2g1b9ro"; +const ENTRY_EXCHANGE = "_Mib5yocT"; +const ENTRY_SIGN = "_Fc3vhtJDvr"; +const ENTRY_TEARDOWN = "_IPaI1oem5iL"; +const ENTRY_DISPOSE = "_jEHf8Xzsv8K"; + +export interface SapAssets { + commerceKit: Uint8Array; + commerceCore: Uint8Array; + coreFP: Uint8Array; + coreFPICXS: Uint8Array; +} + +interface EntryPoints { + initialize: number; + exchange: number; + sign: number; + teardown: number; + dispose: number; +} + +function align(value: number, alignment: number): number { + // Math-based: JS bitwise ops are 32-bit and truncate SAP guest addresses. + return Math.floor((value + alignment - 1) / alignment) * alignment; +} + +export function hardwareBlock(hardwareID: Uint8Array): Uint8Array { + if (hardwareID.length === 0 || hardwareID.length > 20) { + throw new Error("hardware ID must contain between 1 and 20 bytes"); + } + const result = new Uint8Array(24); + new DataView(result.buffer).setUint32(0, hardwareID.length, true); + result.set(hardwareID, 4); + return result; +} + +export class SapMachine { + private scratchCursor = 0; + private closed = false; + private readonly entry: EntryPoints; + private readonly services: Shims; + + private constructor( + private readonly engine: UnicornEngine, + entry: EntryPoints, + services: Shims, + ) { + this.entry = entry; + this.services = services; + } + + static async open( + assets: SapAssets, + options?: { wasmBinary?: ArrayBuffer }, + ): Promise { + const coreFP = MachImage.open("CoreFP", assets.coreFP); + const commerceCore = MachImage.open("CommerceCore", assets.commerceCore); + const commerceKit = MachImage.open("CommerceKit", assets.commerceKit); + + const exports = new Map(); + const coreExports: Record = {}; + + for (const name of CORE_EXPORT_NAMES) { + const address = coreFP.export(name, CORE_FP_BASE); + exports.set(name, address); + coreExports[name] = address; + } + + exports.set( + "_get_mac_address", + commerceCore.export("_get_mac_address", COMMERCE_BASE), + ); + + const entryNames = [ + ENTRY_INITIALIZE, + ENTRY_EXCHANGE, + ENTRY_SIGN, + ENTRY_TEARDOWN, + ENTRY_DISPOSE, + ]; + const resolved = new Map(); + for (const name of entryNames) { + const address = commerceKit.export(name, KIT_BASE); + exports.set(name, address); + resolved.set(name, address); + } + + const engine = await UnicornEngine.open(options); + + for (const region of [ + { address: RETURN_ADDRESS, size: PAGE_SIZE }, + { address: SCRATCH_BASE, size: SCRATCH_SIZE }, + { address: HEAP_BASE, size: HEAP_SIZE }, // guest heap (malloc shims) + { address: STACK_BASE, size: STACK_SIZE }, + ]) { + engine.memMap(region.address, region.size); + } + engine.memWrite(RETURN_ADDRESS, new Uint8Array([0xf4])); // HLT + + const services = await Shims.open(engine, coreExports, assets.coreFPICXS); + + + const resolver = (name: string): number => { + const address = exports.get(name); + if (address !== undefined) { + return address; + } + return services.resolve(name); + }; + + for (const item of [ + { name: "corefp", image: coreFP, base: CORE_FP_BASE }, + { name: "commercecore", image: commerceCore, base: COMMERCE_BASE }, + { name: "commercekit", image: commerceKit, base: KIT_BASE }, + ]) { + item.image.relocate(item.base, resolver); + item.image.load(engine); + } + + return new SapMachine( + engine, + { + initialize: resolved.get(ENTRY_INITIALIZE)!, + exchange: resolved.get(ENTRY_EXCHANGE)!, + sign: resolved.get(ENTRY_SIGN)!, + teardown: resolved.get(ENTRY_TEARDOWN)!, + dispose: resolved.get(ENTRY_DISPOSE)!, + }, + services, + ); + } + + initialize(hardwareID: Uint8Array): number { + const hardware = hardwareBlock(hardwareID); + this.beginCall(); + try { + const contextField = this.scratch(8); + const hardwareAddress = this.scratch(hardware); + const status = this.invoke(this.entry.initialize, [ + contextField, + hardwareAddress, + ]); + if (toInt32(status) !== 0) { + throw new Error(`SAP initialization returned ${toInt32(status)}`); + } + const contextValue = this.readUint64(contextField); + if (contextValue === 0) { + throw new Error("SAP initialization returned a null context"); + } + return contextValue; + } finally { + this.clearScratch(); + } + } + + exchange( + version: number, + hardwareID: Uint8Array, + contextValue: number, + input: Uint8Array, + ): { output: Uint8Array; state: number } { + const hardware = hardwareBlock(hardwareID); + this.beginCall(); + try { + const hardwareAddress = this.scratch(hardware); + const inputAddress = this.scratch(input); + const outputField = this.scratch(8); + const lengthField = this.scratch(8); + const resultField = this.scratch(4); + const status = this.invoke(this.entry.exchange, [ + version, + hardwareAddress, + contextValue, + inputAddress, + input.length, + outputField, + lengthField, + resultField, + ]); + if (toInt32(status) !== 0) { + throw new Error(`SAP exchange returned ${toInt32(status)}`); + } + const output = this.consumeOutput(outputField, lengthField); + const result = this.readUint32(resultField); + return { output, state: toInt32(result) }; + } finally { + this.clearScratch(); + } + } + + sign(contextValue: number, input: Uint8Array): Uint8Array { + this.beginCall(); + try { + const inputAddress = this.scratch(input); + const outputField = this.scratch(8); + const lengthField = this.scratch(8); + const status = this.invoke(this.entry.sign, [ + contextValue, + inputAddress, + input.length, + outputField, + lengthField, + ]); + if (toInt32(status) !== 0) { + throw new Error(`SAP signing returned ${toInt32(status)}`); + } + return this.consumeOutput(outputField, lengthField); + } finally { + this.clearScratch(); + } + } + + teardown(contextValue: number): void { + const status = this.invoke(this.entry.teardown, [contextValue]); + if (toInt32(status) !== 0) { + throw new Error(`SAP teardown returned ${toInt32(status)}`); + } + } + + close(): void { + if (this.closed) { + return; + } + this.closed = true; + this.services.close(); + this.engine.close(); + } + + private invoke(functionAddress: number, args: number[]): number { + if (this.closed) { + throw new Error("SAP guest machine is closed"); + } + if (functionAddress === 0) { + throw new Error("SAP guest entry point is unavailable"); + } + + const registers = [ + X86_REG.RDI, + X86_REG.RSI, + X86_REG.RDX, + X86_REG.RCX, + X86_REG.R8, + X86_REG.R9, + ]; + for (let index = 0; index < registers.length; index++) { + this.engine.regWrite(registers[index], args[index] ?? 0); + } + + const extra = Math.max(args.length - registers.length, 0); + let stackPointer = STACK_END - (extra + 1) * 8; + if (stackPointer % 16 !== 8) { + stackPointer -= 8; + } + + this.writeUint64(stackPointer, RETURN_ADDRESS); + for (let index = 0; index < extra; index++) { + this.writeUint64( + stackPointer + 8 + index * 8, + args[registers.length + index], + ); + } + this.engine.regWrite(X86_REG.RSP, stackPointer); + + this.services.resetFault(); + + try { + // SAP's cryptographic routines have input- and host-dependent instruction + // counts; bound execution by wall time rather than a fixed instruction cap. + this.engine.emuStart( + functionAddress, + RETURN_ADDRESS, + SAP_GUEST_TIMEOUT_US, + 0, + ); + } catch (error) { + if (this.services.fault) { + throw this.services.fault; + } + throw error; + } + + if (this.services.fault) { + throw this.services.fault; + } + + const instruction = this.engine.regRead(X86_REG.RIP); + if (instruction !== RETURN_ADDRESS) { + throw new Error( + `SAP guest stopped unexpectedly at ${instruction.toString(16)}`, + ); + } + + return this.engine.regRead(X86_REG.RAX); + } + + private beginCall(): void { + this.scratchCursor = 0; + } + + private scratch(data: Uint8Array | number): number { + const isData = typeof data !== "number"; + const size = isData ? (data as Uint8Array).length : data; + const reserved = align(Math.max(size, 1), 16); + if ( + this.scratchCursor > SCRATCH_SIZE || + reserved > SCRATCH_SIZE - this.scratchCursor + ) { + throw new Error("SAP guest scratch space exhausted"); + } + const address = SCRATCH_BASE + this.scratchCursor; + this.scratchCursor += reserved; + if (size !== 0) { + const bytes = isData ? (data as Uint8Array) : new Uint8Array(size); + if (bytes.length > size) { + throw new Error("scratch data exceeds reservation"); + } + this.engine.memWrite(address, bytes); + } + return address; + } + + private clearScratch(): void { + if (this.scratchCursor !== 0 && !this.closed) { + this.engine.memWrite( + SCRATCH_BASE, + new Uint8Array(this.scratchCursor), + ); + } + this.scratchCursor = 0; + } + + private consumeOutput(pointerField: number, lengthField: number): Uint8Array { + const pointer = this.readUint64(pointerField); + const length = this.readUint64(lengthField); + + let output: Uint8Array | null = null; + let outputError: Error | null = null; + + if (length > MAX_OUTPUT_SIZE) { + outputError = new Error( + `SAP output is ${length} bytes, maximum is ${MAX_OUTPUT_SIZE}`, + ); + } else if (length === 0) { + output = new Uint8Array(0); + } else if (pointer === 0) { + outputError = new Error("SAP returned a null output pointer"); + } else { + output = this.engine.memRead(pointer, length); + } + + if (pointer !== 0) { + const disposeStatus = this.invoke(this.entry.dispose, [pointer]); + if (toInt32(disposeStatus) !== 0 && !outputError) { + outputError = new Error(`SAP storage disposal returned ${toInt32(disposeStatus)}`); + } + } + + if (outputError) { + throw outputError; + } + return output!; + } + + private readUint32(address: number): number { + const data = this.engine.memRead(address, 4); + return new DataView(data.buffer, data.byteOffset).getUint32(0, true); + } + + private readUint64(address: number): number { + const data = this.engine.memRead(address, 8); + return Number( + new DataView(data.buffer, data.byteOffset).getBigUint64(0, true), + ); + } + + private writeUint64(address: number, value: number): void { + const data = new Uint8Array(8); + new DataView(data.buffer).setBigUint64(0, BigInt(value), true); + this.engine.memWrite(address, data); + } +} + +function toInt32(value: number): number { + return value | 0; +} diff --git a/frontend/src/apple/sap/protocol.ts b/frontend/src/apple/sap/protocol.ts new file mode 100644 index 00000000..948b2007 --- /dev/null +++ b/frontend/src/apple/sap/protocol.ts @@ -0,0 +1,62 @@ +// SAP setup protocol: fetches the Apple certificate and performs the key +// exchange. Both endpoints are public Apple services (no credentials); +// requests travel through the wisp tunnel via appleRequest like every other +// Apple API call. Ported from ipatool's internal/sap/protocol.go. + +import { appleRequest } from "../request"; +import { buildPlist, parsePlist } from "../plist"; +import type { SapEndpoints } from "./types"; + +const SETUP_CERTIFICATE_KEY = "sign-sap-setup-cert"; +const SETUP_BUFFER_KEY = "sign-sap-setup-buffer"; +const MAX_SETUP_BODY = 1 << 20; + +function plistBytes(document: string, key: string): Uint8Array { + const values = parsePlist(document) as Record; + const value = values[key]; + if (!(value instanceof Uint8Array) || value.length === 0) { + throw new Error(`Apple plist is missing ${key}`); + } + return value; +} + +export async function fetchSetupCertificate( + endpoints: SapEndpoints, +): Promise { + const response = await appleRequest({ + method: "GET", + host: new URL(endpoints.certificateURL).hostname, + path: `${new URL(endpoints.certificateURL).pathname}${new URL(endpoints.certificateURL).search}`, + }); + if (response.status !== 200) { + throw new Error(`SAP certificate request returned ${response.status}`); + } + if (response.body.length > MAX_SETUP_BODY) { + throw new Error("SAP certificate response exceeds 1 MiB"); + } + return plistBytes(response.body, SETUP_CERTIFICATE_KEY); +} + +export async function exchangeSetupBuffer( + endpoints: SapEndpoints, + input: Uint8Array, +): Promise { + const envelope = buildPlist({ [SETUP_BUFFER_KEY]: input }); + const url = new URL(endpoints.setupURL); + const response = await appleRequest({ + method: "POST", + host: url.hostname, + path: `${url.pathname}${url.search}`, + headers: { + "Content-Type": "application/x-plist", + }, + body: envelope, + }); + if (response.status !== 200) { + throw new Error(`SAP setup exchange returned ${response.status}`); + } + if (response.body.length > MAX_SETUP_BODY) { + throw new Error("SAP setup response exceeds 1 MiB"); + } + return plistBytes(response.body, SETUP_BUFFER_KEY); +} diff --git a/frontend/src/apple/sap/shims.ts b/frontend/src/apple/sap/shims.ts new file mode 100644 index 00000000..e85b3a70 --- /dev/null +++ b/frontend/src/apple/sap/shims.ts @@ -0,0 +1,712 @@ +// Guest service shims for the SAP machine: libc memory functions plus the +// CoreFoundation/IOKit/objc surface the emulated CommerceKit code touches. +// Ported from ipatool's internal/sap/machine (shim_memory.go, shim_platform.go, +// shims.go). Imports resolve to 16-byte stub slots holding a single RET; a code +// hook over the shim area dispatches on the entry address. + +import { UnicornEngine, X86_REG } from "./engine"; + +const SHIM_BASE = 0x0000200000000000; +const SHIM_CODE_SIZE = 0x80000; +const SHIM_SIZE = 0x100000; +const SHIM_SLOT_SIZE = 16; + +const MAX_GUEST_TRANSFER = 64 << 20; +const PAGE_SIZE = 0x1000; + +// machine.go memory layout (heap area is managed by the malloc shims). +export const HEAP_BASE = 0x0000400000000000; +export const HEAP_SIZE = 64 << 20; + +// Passed through regWrite as -1: wasm's saturating f64->i64 conversion turns +// -1 into the full 64-bit 0xFFFF...F (Go's math.MaxUint64), which cannot be +// represented exactly as a JS number. +const FAKE_HANDLE = -1; +const CORE_FP_FILE = 3; +const CORE_FP_PATH = "/System/Library/PrivateFrameworks/CoreFP.framework/CoreFP"; +const ICXS_PATH = "./../CoreFP.icxs"; +const KEY_SERIAL = "IOPlatformSerialNumber"; +const KEY_UUID = "IOPlatformUUID"; +const KEY_BOARD = "board-id"; +const KEYED_MESSAGE = "objectForKey:"; + +interface GuestAllocation { + size: number; + reserved: number; +} + +interface FreeBlock { + address: number; + size: number; +} + +type ShimHandler = () => void; + +function align(value: number, alignment: number): number { + // Math-based: JS bitwise ops are 32-bit and truncate SAP guest addresses. + return Math.floor((value + alignment - 1) / alignment) * alignment; +} + +export class Shims { + private readonly entries = new Map(); + private readonly handlers = new Map(); + readonly symbols = new Map(); + private codeCursor = SHIM_BASE; + private dataCursor = SHIM_BASE + SHIM_CODE_SIZE; + fault: Error | null = null; + private readonly coreExports: Map; + private readonly icxs: Uint8Array; + private icxsOffset = 0; + private errnoAddress = 0; + private heapCursor = 0; + private readonly allocations = new Map(); + private freeBlocks: FreeBlock[] = []; + private iterator = 0; + private hookHandle = 0; + + private constructor( + private readonly engine: UnicornEngine, + coreExports: Record, + icxs: Uint8Array, + ) { + this.coreExports = new Map(Object.entries(coreExports)); + this.icxs = icxs; + } + + static async open( + engine: UnicornEngine, + coreExports: Record, + icxs: Uint8Array, + ): Promise { + engine.memMap(SHIM_BASE, SHIM_SIZE); + + const shims = new Shims(engine, coreExports, icxs); + shims.registerMemoryServices(); + shims.registerPlatformServices(); + + engine.attachCodeHook(); + engine.onCodeHook = (address) => shims.dispatch(address); + shims.hookHandle = engine.addCodeHook( + SHIM_BASE, + SHIM_BASE + SHIM_CODE_SIZE - 1, + ); + + return shims; + } + + close(): void { + if (this.hookHandle) { + this.engine.hookDel(this.hookHandle); + } + } + + resolve(name: string): number { + const existing = this.symbols.get(name); + if (existing !== undefined) { + return existing; + } + return this.addFunction(name, () => { + throw new Error(`guest called unsupported import ${name}`); + }); + } + + private addAliases(names: string[], handler: ShimHandler): void { + for (const name of names) { + this.addFunction(name, handler); + } + } + + private addFunction(name: string, handler: ShimHandler): number { + const existing = this.symbols.get(name); + if (existing !== undefined) { + return existing; + } + if (this.codeCursor + SHIM_SLOT_SIZE > SHIM_BASE + SHIM_CODE_SIZE) { + throw new Error("guest service code area is full"); + } + + const address = this.codeCursor; + this.codeCursor += SHIM_SLOT_SIZE; + + this.engine.memWrite(address, new Uint8Array([0xc3])); // RET + + this.entries.set(address, name); + this.handlers.set(address, handler); + this.symbols.set(name, address); + return address; + } + + private addData(name: string, data: Uint8Array): number { + const existing = this.symbols.get(name); + if (existing !== undefined) { + return existing; + } + this.dataCursor = align(this.dataCursor, 8); + if (this.dataCursor + data.length > SHIM_BASE + SHIM_SIZE) { + throw new Error("guest service data area is full"); + } + const address = this.dataCursor; + this.dataCursor += Math.max(data.length, 8); + this.engine.memWrite(address, data); + this.symbols.set(name, address); + return address; + } + + dispatch(address: number): void { + const handler = this.handlers.get(address); + if (!handler) { + this.fail(new Error(`guest entered unknown service address ${address.toString(16)}`)); + return; + } + try { + handler(); + } catch (error) { + this.fail( + new Error( + `${this.entries.get(address) ?? "shim"}: ${error instanceof Error ? error.message : String(error)}`, + ), + ); + } + } + + private fail(error: Error): void { + if (!this.fault) { + this.fault = error; + } + try { + this.engine.emuStop(); + } catch { + // stopping an already-stopped engine is fine + } + } + + resetFault(): void { + this.fault = null; + } + + private argument(index: number): number { + const registers = [ + X86_REG.RDI, + X86_REG.RSI, + X86_REG.RDX, + X86_REG.RCX, + X86_REG.R8, + X86_REG.R9, + ]; + if (index >= 0 && index < registers.length) { + return this.engine.regRead(registers[index]); + } + if (index < 0) { + throw new Error("negative guest argument index"); + } + const stack = this.engine.regRead(X86_REG.RSP); + const data = this.engine.memRead(stack + 8 + (index - registers.length) * 8, 8); + const view = new DataView(data.buffer, data.byteOffset, data.byteLength); + return Number(view.getBigUint64(0, true)); + } + + private setResult(value: number): void { + this.engine.regWrite(X86_REG.RAX, value); + } + + private readUint32(address: number): number { + const data = this.engine.memRead(address, 4); + return new DataView(data.buffer, data.byteOffset).getUint32(0, true); + } + + private writeUint32(address: number, value: number): void { + const data = new Uint8Array(4); + new DataView(data.buffer).setUint32(0, value, true); + this.engine.memWrite(address, data); + } + + private readUint64(address: number): number { + const data = this.engine.memRead(address, 8); + return Number( + new DataView(data.buffer, data.byteOffset).getBigUint64(0, true), + ); + } + + private writeUint64(address: number, value: number): void { + const data = new Uint8Array(8); + new DataView(data.buffer).setBigUint64(0, BigInt(value), true); + this.engine.memWrite(address, data); + } + + readCString(address: number): string { + const maximum = 4096; + const value: number[] = []; + while (value.length < maximum) { + const item = this.engine.memRead(address + value.length, 1)[0]; + if (item === 0) { + return String.fromCharCode(...value); + } + value.push(item); + } + throw new Error(`guest string exceeds ${maximum} bytes`); + } + + private checkedSize(value: number): number { + if (value > MAX_GUEST_TRANSFER) { + throw new Error(`guest transfer size ${value} exceeds limit`); + } + return value; + } + + // ---- memory services ---- + + private registerMemoryServices(): void { + this.addAliases(["_malloc"], () => { + const size = this.argument(0); + this.setResult(this.allocate(size)); + }); + this.addAliases(["_malloc_good_size"], () => { + const size = this.argument(0); + this.setResult(align(Math.max(size, 1), 16)); + }); + this.addAliases(["_malloc_size"], () => { + const address = this.argument(0); + const allocation = this.allocations.get(address); + this.setResult(allocation ? allocation.reserved : 0); + }); + this.addAliases(["_calloc"], () => { + const count = this.argument(0); + const size = this.argument(1); + const total = count * size; + const address = this.allocate(total); + if (total !== 0) { + this.engine.memWrite(address, new Uint8Array(total)); + } + this.setResult(address); + }); + this.addAliases(["_realloc", "_reallocf"], () => { + this.realloc(); + }); + this.addAliases(["_free"], () => { + const address = this.argument(0); + if (address !== 0) { + this.release(address); + } + this.setResult(0); + }); + this.addAliases(["_memcpy", "_memmove"], () => { + this.memmove(); + }); + this.addAliases(["_memset"], () => { + this.memset(); + }); + this.addAliases(["___bzero"], () => { + this.bzero(); + }); + this.addAliases(["___memcpy_chk"], () => { + const length = this.argument(2); + const capacity = this.argument(3); + if (length > capacity) { + throw new Error("checked copy exceeds destination"); + } + this.memmove(); + }); + this.addAliases(["___memset_chk"], () => { + const length = this.argument(2); + const capacity = this.argument(3); + if (length > capacity) { + throw new Error("checked fill exceeds destination"); + } + this.memset(); + }); + this.addAliases(["_memcmp"], () => { + const left = this.argument(0); + const right = this.argument(1); + const length = this.checkedSize(this.argument(2)); + const a = this.engine.memRead(left, length); + const b = this.engine.memRead(right, length); + let order = 0; + for (let index = 0; index < length; index++) { + if (a[index] !== b[index]) { + order = a[index] < b[index] ? -1 : 1; + break; + } + } + this.setResult(order >>> 0); + }); + this.addAliases(["_strcmp"], () => { + const left = this.readCString(this.argument(0)); + const right = this.readCString(this.argument(1)); + this.setResult(left < right ? -1 : left > right ? 1 : 0); + }); + this.addAliases(["_strncmp"], () => { + this.strncmp(); + }); + this.addAliases(["_strlen"], () => { + const value = this.readCString(this.argument(0)); + this.setResult(value.length); + }); + } + + private allocate(size: number): number { + if (size > MAX_GUEST_TRANSFER) { + throw new Error(`allocation size ${size} exceeds limit`); + } + const reserved = align(Math.max(size, 1), 16); + for (let index = 0; index < this.freeBlocks.length; index++) { + const block = this.freeBlocks[index]; + if (block.size < reserved) { + continue; + } + const address = block.address; + if (block.size === reserved) { + this.freeBlocks.splice(index, 1); + } else { + block.address += reserved; + block.size -= reserved; + } + this.allocations.set(address, { size, reserved }); + return address; + } + if (this.heapCursor > HEAP_SIZE || reserved > HEAP_SIZE - this.heapCursor) { + throw new Error("guest heap exhausted"); + } + const address = HEAP_BASE + this.heapCursor; + this.heapCursor += reserved; + this.allocations.set(address, { size, reserved }); + return address; + } + + private release(address: number): void { + const allocation = this.allocations.get(address); + if (!allocation) { + throw new Error(`free unknown pointer ${address.toString(16)}`); + } + this.engine.memWrite(address, new Uint8Array(allocation.reserved)); + this.allocations.delete(address); + this.freeBlocks.push({ address, size: allocation.reserved }); + this.coalesceFreeBlocks(); + } + + private coalesceFreeBlocks(): void { + this.freeBlocks.sort((left, right) => left.address - right.address); + const merged: FreeBlock[] = []; + for (const block of this.freeBlocks) { + const last = merged[merged.length - 1]; + if (last && last.address + last.size === block.address) { + last.size += block.size; + continue; + } + merged.push({ ...block }); + } + this.freeBlocks = merged; + while (this.freeBlocks.length !== 0) { + const last = this.freeBlocks[this.freeBlocks.length - 1]; + if (last.address + last.size !== HEAP_BASE + this.heapCursor) { + break; + } + this.heapCursor -= last.size; + this.freeBlocks.pop(); + } + } + + private realloc(): void { + const oldAddress = this.argument(0); + const newSize = this.argument(1); + if (oldAddress === 0) { + this.setResult(this.allocate(newSize)); + return; + } + const oldAllocation = this.allocations.get(oldAddress); + if (!oldAllocation) { + throw new Error(`reallocate unknown pointer ${oldAddress.toString(16)}`); + } + if (newSize <= oldAllocation.reserved) { + oldAllocation.size = newSize; + this.setResult(oldAddress); + return; + } + const newAddress = this.allocate(newSize); + const data = this.engine.memRead(oldAddress, oldAllocation.size); + this.engine.memWrite(newAddress, data); + this.release(oldAddress); + this.setResult(newAddress); + } + + private memmove(): void { + const destination = this.argument(0); + const source = this.argument(1); + const length = this.checkedSize(this.argument(2)); + if (length !== 0) { + const data = this.engine.memRead(source, length); + this.engine.memWrite(destination, data); + } + this.setResult(destination); + } + + private memset(): void { + const destination = this.argument(0); + const value = this.argument(1); + const size = this.checkedSize(this.argument(2)); + this.engine.memWrite(destination, new Uint8Array(size).fill(value & 0xff)); + this.setResult(destination); + } + + private bzero(): void { + const destination = this.argument(0); + const size = this.checkedSize(this.argument(1)); + this.engine.memWrite(destination, new Uint8Array(size)); + this.setResult(destination); + } + + private strncmp(): void { + const left = this.argument(0); + const right = this.argument(1); + const length = this.checkedSize(this.argument(2)); + for (let offset = 0; offset < length; ) { + const leftAddress = left + offset; + const rightAddress = right + offset; + const chunk = Math.min( + length - offset, + PAGE_SIZE - (leftAddress % PAGE_SIZE), + PAGE_SIZE - (rightAddress % PAGE_SIZE), + ); + const a = this.engine.memRead(leftAddress, chunk); + const b = this.engine.memRead(rightAddress, chunk); + for (let index = 0; index < a.length; index++) { + if (a[index] !== b[index]) { + this.setResult((a[index] - b[index]) >>> 0); + return; + } + if (a[index] === 0) { + this.setResult(0); + return; + } + } + offset += chunk; + } + this.setResult(0); + } + + // ---- platform services ---- + + private registerPlatformServices(): void { + this.addAliases( + [ + "_CFBundleGetMainBundle", + "_CFDataGetBytePtr", + "_CFDataGetLength", + "_CFStringGetLength", + "_CFStringGetMaximumSizeForEncoding", + "_CFUUIDCreateString", + "_IORegistryEntryFromPath", + "_IORegistryEntrySearchCFProperty", + "_IOServiceMatching", + "_getenv", + "_pthread_self", + ], + () => this.setResult(0), + ); + this.addAliases( + [ + "_CFDictionaryGetValue", + "_DADiskCopyDescription", + "_DADiskCreateFromBSDName", + "_DASessionCreate", + "_IORegistryEntryCreateCFProperty", + ], + () => this.setResult(FAKE_HANDLE), + ); + this.addAliases( + [ + "_CFRelease", + "_IOObjectRelease", + "_close", + "_close$UNIX2003", + "_pthread_mutex_lock", + "_pthread_mutex_unlock", + "_pthread_rwlock_init", + "_pthread_rwlock_init$UNIX2003", + "_pthread_rwlock_unlock", + "_pthread_rwlock_unlock$UNIX2003", + "_pthread_rwlock_wrlock", + "_pthread_rwlock_wrlock$UNIX2003", + ], + () => this.setResult(0), + ); + this.addAliases(["_CFStringCreateWithCString"], () => { + const value = this.readCString(this.argument(1)); + this.setResult( + value === KEY_SERIAL || value === KEY_UUID || value === KEY_BOARD + ? FAKE_HANDLE + : 0, + ); + }); + this.addAliases(["_CFStringCreateWithCStringNoCopy"], () => { + this.setResult(0); + }); + this.addAliases(["_CFStringGetCString"], () => { + const buffer = this.argument(1); + const capacity = this.argument(2); + if (buffer === 0 || capacity === 0) { + this.setResult(0); + return; + } + this.engine.memWrite(buffer, new Uint8Array([0])); + this.setResult(1); + }); + this.addAliases(["_IOIteratorNext"], () => { + this.iterator = (this.iterator + 1) >>> 0; + this.setResult(this.iterator % 2); + }); + this.addAliases(["_IORegistryEntryGetParentEntry"], () => { + const parent = this.argument(2); + if (parent === 0) { + throw new Error("parent registry entry output is null"); + } + this.writeUint32(parent, 0xffffffff); + this.setResult(0); + }); + this.addAliases(["_IOServiceGetMatchingServices"], () => { + const iterator = this.argument(2); + if (iterator === 0) { + throw new Error("matching services iterator output is null"); + } + this.iterator = 0; + this.writeUint32(iterator, 0xffffffff); + this.setResult(0); + }); + this.addAliases(["_IOServiceGetMatchingService"], () => { + this.setResult(0xffffffff); // uc returns uint32 max per Go reference + }); + this.addAliases(["_OSAtomicCompareAndSwap32Barrier"], () => { + const oldValue = this.argument(0); + const newValue = this.argument(1); + const address = this.argument(2); + const current = this.readUint32(address); + if (current !== oldValue) { + this.setResult(0); + return; + } + this.writeUint32(address, newValue >>> 0); + this.setResult(1); + }); + this.addAliases(["___error"], () => { + this.setResult(this.errnoAddress); + }); + this.addAliases(["_abort", "___stack_chk_fail", "dyld_stub_binder"], () => { + throw new Error("guest aborted"); + }); + this.addAliases(["_arc4random"], () => { + const value = new Uint32Array(1); + crypto.getRandomValues(value); + this.setResult(value[0]); + }); + this.addAliases(["_dlopen"], () => { + const path = this.readCString(this.argument(0)); + this.setResult(path === CORE_FP_PATH ? FAKE_HANDLE : 0); + }); + this.addAliases(["_dlsym"], () => { + const name = this.readCString(this.argument(1)); + this.setResult(this.coreExports.get(`_${name}`) ?? 0); + }); + this.addAliases( + ["_fcntl", "_fcntl$UNIX2003", "_lstat$INODE64", "_statfs", "_statfs$INODE64"], + () => this.setResult(-1), // 64-bit -1 (see FAKE_HANDLE note) + ); + this.addAliases(["_gettimeofday"], () => { + this.gettimeofday(); + }); + this.addAliases(["_objc_msgSend"], () => { + const selector = this.readCString(this.argument(1)); + this.setResult(selector === KEYED_MESSAGE ? FAKE_HANDLE : 0); + }); + this.addAliases(["_open", "_open$UNIX2003"], () => { + const path = this.readCString(this.argument(0)); + if (path === ICXS_PATH) { + this.icxsOffset = 0; + this.setResult(CORE_FP_FILE); + return; + } + this.setResult(-1); // open() returns int -1 + }); + this.addAliases(["_pthread_once"], () => { + this.pthreadOnce(); + }); + this.addAliases(["_read", "_read$UNIX2003"], () => { + this.read(); + }); + this.addAliases(["_sysctl"], () => { + this.setResult(-1); // 64-bit -1 + }); + this.addAliases(["_sysctlbyname"], () => { + const lengthAddress = this.argument(2); + if (lengthAddress !== 0) { + this.writeUint64(lengthAddress, 0); + } + this.setResult(0); + }); + + this.errnoAddress = this.addData("guest.errno", new Uint8Array(8)); + this.addData( + "___stack_chk_guard", + new Uint8Array([0xa5, 0x71, 0x3c, 0xd9, 0x86, 0x42, 0xef, 0x10]), + ); + for (const name of [ + "_kCFAllocatorDefault", + "_kCFAllocatorNull", + "_kDADiskDescriptionVolumeUUIDKey", + "_kIOMasterPortDefault", + ]) { + this.addData(name, new Uint8Array(8)); + } + } + + private gettimeofday(): void { + const timeAddress = this.argument(0); + const zoneAddress = this.argument(1); + const now = Date.now(); + if (timeAddress !== 0) { + const data = new Uint8Array(16); + const view = new DataView(data.buffer); + view.setBigUint64(0, BigInt(Math.floor(now / 1000)), true); + view.setUint32(8, (now % 1000) * 1000, true); + this.engine.memWrite(timeAddress, data); + } + if (zoneAddress !== 0) { + this.engine.memWrite(zoneAddress, new Uint8Array(8)); + } + this.setResult(0); + } + + private pthreadOnce(): void { + const control = this.argument(0); + const initializer = this.argument(1); + const value = this.readUint64(control); + if (value === 0) { + this.setResult(0); + return; + } + this.writeUint64(control, 0); + const stack = this.engine.regRead(X86_REG.RSP) - 8; + this.writeUint64(stack, initializer); + this.engine.regWrite(X86_REG.RSP, stack); + this.setResult(0); + } + + private read(): void { + const descriptor = this.argument(0); + const buffer = this.argument(1); + const requested = this.argument(2); + if (descriptor !== CORE_FP_FILE) { + this.setResult(-1); // 64-bit -1 + return; + } + const size = this.checkedSize(requested); + const remaining = this.icxs.length - this.icxsOffset; + const count = Math.min(size, remaining); + if (count !== 0) { + this.engine.memWrite( + buffer, + this.icxs.subarray(this.icxsOffset, this.icxsOffset + count), + ); + this.icxsOffset += count; + } + this.setResult(count); + } +} diff --git a/frontend/src/apple/sap/signer.ts b/frontend/src/apple/sap/signer.ts new file mode 100644 index 00000000..f53b09fe --- /dev/null +++ b/frontend/src/apple/sap/signer.ts @@ -0,0 +1,118 @@ +// SAP signer: sequences the emulated CommerceKit entry points through the +// Apple setup key exchange and per-request signing. The machine driver can be +// the in-process SapMachine (tests) or a Web Worker proxy (production); setup +// network calls run on the caller's thread so they ride the wisp tunnel. + +import { + validateSapSignerOptions, + type SapSignerOptions, +} from "./types"; + +export interface SapMachineDriver { + initialize(hardwareID: Uint8Array): Promise; + exchange( + version: number, + hardwareID: Uint8Array, + contextValue: number, + input: Uint8Array, + ): Promise<{ output: Uint8Array; state: number }>; + sign(contextValue: number, input: Uint8Array): Promise; + teardown(contextValue: number): Promise; + close(): Promise; +} + +export interface SapSignerNetwork { + fetchCertificate: () => Promise; + exchange: (input: Uint8Array) => Promise; +} + +export class SapSigner { + private closed = false; + + private constructor( + private readonly driver: SapMachineDriver, + private readonly context: number, + ) {} + + /** + * Completes the Apple key exchange: initialize -> GET certificate -> + * exchange(state 1) -> POST setup -> exchange(state 0). + */ + static async create( + options: SapSignerOptions, + driver: SapMachineDriver, + network: SapSignerNetwork, + ): Promise { + validateSapSignerOptions(options); + + let context = 0; + try { + context = await driver.initialize(options.hardwareID); + + const certificate = await network.fetchCertificate(); + const first = await driver.exchange( + options.version, + options.hardwareID, + context, + certificate, + ); + if (first.state !== 1) { + throw new Error(`SAP setup entered unexpected state ${first.state}`); + } + if (first.output.length === 0) { + throw new Error("SAP setup message is empty"); + } + + const reply = await network.exchange(first.output); + const second = await driver.exchange( + options.version, + options.hardwareID, + context, + reply, + ); + if (second.state !== 0) { + throw new Error( + `SAP setup completed in unexpected state ${second.state}`, + ); + } + } catch (error) { + await driver.close().catch(() => undefined); + throw error; + } + + return new SapSigner(driver, context); + } + + /** Signs request-body bytes; returns the X-Apple-ActionSignature value. */ + async sign(input: Uint8Array): Promise { + if (this.closed) { + throw new Error("SAP signer is closed"); + } + const signature = await this.driver.sign(this.context, input); + if (signature.length === 0) { + throw new Error("SAP signing produced an empty signature"); + } + return bytesToBase64(signature); + } + + async close(): Promise { + if (this.closed) { + return; + } + this.closed = true; + try { + await this.driver.teardown(this.context); + } finally { + await this.driver.close(); + } + } +} + +function bytesToBase64(bytes: Uint8Array): string { + let binary = ""; + const chunk = 0x8000; + for (let offset = 0; offset < bytes.length; offset += chunk) { + binary += String.fromCharCode(...bytes.subarray(offset, offset + chunk)); + } + return btoa(binary); +} diff --git a/frontend/src/apple/sap/types.ts b/frontend/src/apple/sap/types.ts new file mode 100644 index 00000000..f1ec862a --- /dev/null +++ b/frontend/src/apple/sap/types.ts @@ -0,0 +1,42 @@ +// Shared SAP types. + +export interface SapEndpoints { + /** GET endpoint returning the Apple certificate plist (bag: sign-sap-setup-cert). */ + certificateURL: string; + /** POST endpoint for the setup key exchange (bag: sign-sap-setup). */ + setupURL: string; + /** Protocol version from the bag (sign-sap-version); only 200 is supported. */ + version: number; +} + +export const SUPPORTED_SAP_VERSION = 200; + +export interface SapAssetBundle { + commerceKit: Uint8Array; + commerceCore: Uint8Array; + coreFP: Uint8Array; + coreFPICXS: Uint8Array; +} + +export interface SapSignerOptions extends SapEndpoints { + /** Per-account device identifier bytes (ASCII, 1..20 bytes). */ + hardwareID: Uint8Array; + assets: SapAssetBundle; + wasmBinary?: ArrayBuffer; +} + +/** Endpoint/hardware validation matching the Swift reference. */ +export function validateSapSignerOptions(options: SapSignerOptions): void { + if (options.version !== SUPPORTED_SAP_VERSION) { + throw new Error(`unsupported SAP version ${options.version}`); + } + if (options.hardwareID.length === 0 || options.hardwareID.length > 20) { + throw new Error("SAP hardware ID must contain between 1 and 20 bytes"); + } + for (const url of [options.certificateURL, options.setupURL]) { + const parsed = new URL(url); + if (parsed.protocol !== "https:" || !parsed.hostname || parsed.username) { + throw new Error(`SAP endpoint must be an absolute HTTPS URL: ${url}`); + } + } +} diff --git a/frontend/src/apple/sap/vendor/unicorn.d.mts b/frontend/src/apple/sap/vendor/unicorn.d.mts new file mode 100644 index 00000000..e14e3b45 --- /dev/null +++ b/frontend/src/apple/sap/vendor/unicorn.d.mts @@ -0,0 +1,5 @@ +// Type shim for the emscripten module built by scripts/unicorn-wasm/build.sh. +declare const UnicornModuleFactory: ( + config?: Record, +) => Promise; +export default UnicornModuleFactory; diff --git a/frontend/src/apple/sap/vendor/unicorn.mjs b/frontend/src/apple/sap/vendor/unicorn.mjs new file mode 100644 index 00000000..2d28ccd8 --- /dev/null +++ b/frontend/src/apple/sap/vendor/unicorn.mjs @@ -0,0 +1,16 @@ + +var UnicornModule = (() => { + var _scriptName = import.meta.url; + + return ( +async function(moduleArg = {}) { + var moduleRtn; + +var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string";if(ENVIRONMENT_IS_NODE){const{createRequire:createRequire}=await import("module");var require=createRequire(import.meta.url)}var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var read_,readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("fs");var nodePath=require("path");scriptDirectory=require("url").fileURLToPath(new URL("./",import.meta.url));read_=(filename,binary)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);return fs.readFileSync(filename,binary?undefined:"utf8")};readBinary=filename=>{var ret=read_(filename,true);if(!ret.buffer){ret=new Uint8Array(ret)}return ret};readAsync=(filename,onload,onerror,binary=true)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);fs.readFile(filename,binary?undefined:"utf8",(err,data)=>{if(err)onerror(err);else onload(binary?data.buffer:data)})};if(!Module["thisProgram"]&&process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{read_=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText};if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=(url,onload,onerror)=>{fetch(url,{credentials:"same-origin"}).then(response=>{if(response.ok){return response.arrayBuffer()}return Promise.reject(new Error(response.status+" : "+response.url))}).then(onload,onerror)}}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["quit"])quit_=Module["quit"];var wasmBinary;if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];var wasmMemory;var ABORT=false;var EXITSTATUS;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b)}var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.init.initialized)FS.init();FS.ignorePermissions=false;TTY.init();callRuntimeCallbacks(__ATINIT__)}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function getUniqueRunDependency(id){return id}function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;EXITSTATUS=1;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var dataURIPrefix="data:application/octet-stream;base64,";var isDataURI=filename=>filename.startsWith(dataURIPrefix);var isFileURI=filename=>filename.startsWith("file://");function findWasmBinary(){if(Module["locateFile"]){var f="unicorn.wasm";if(!isDataURI(f)){return locateFile(f)}return f}return new URL("unicorn.wasm",import.meta.url).href}var wasmBinaryFile;function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}function getBinaryPromise(binaryFile){if(!wasmBinary){return new Promise((resolve,reject)=>{readAsync(binaryFile,response=>resolve(new Uint8Array(response)),error=>{try{resolve(getBinarySync(binaryFile))}catch(e){reject(e)}})})}return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&!ENVIRONMENT_IS_NODE&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}return instantiateArrayBuffer(binaryFile,imports,callback)}function getWasmImports(){return{a:wasmImports}}function createWasm(){var info=getWasmImports();function receiveInstance(instance,module){wasmExports=instance.exports;wasmExports=applySignatureConversions(wasmExports);wasmMemory=wasmExports["R"];updateMemoryViews();wasmTable=wasmExports["na"];addOnInit(wasmExports["S"]);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);readyPromiseReject(e)}}if(!wasmBinaryFile)wasmBinaryFile=findWasmBinary();instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult).catch(readyPromiseReject);return{}}var tempDouble;var tempI64;function ExitStatus(status){this.name="ExitStatus";this.message=`Program terminated with exit(${status})`;this.status=status}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};function getValue(ptr,type="i8"){if(type.endsWith("*"))type="*";switch(type){case"i1":return HEAP8[ptr>>>0];case"i8":return HEAP8[ptr>>>0];case"i16":return HEAP16[ptr>>>1>>>0];case"i32":return HEAP32[ptr>>>2>>>0];case"i64":abort("to do getValue(i64) use WASM_BIGINT");case"float":return HEAPF32[ptr>>>2>>>0];case"double":return HEAPF64[ptr>>>3>>>0];case"*":return HEAPU32[ptr>>>2>>>0];default:abort(`invalid type for getValue: ${type}`)}}var noExitRuntime=Module["noExitRuntime"]||true;function setValue(ptr,value,type="i8"){if(type.endsWith("*"))type="*";switch(type){case"i1":HEAP8[ptr>>>0]=value;break;case"i8":HEAP8[ptr>>>0]=value;break;case"i16":HEAP16[ptr>>>1>>>0]=value;break;case"i32":HEAP32[ptr>>>2>>>0]=value;break;case"i64":abort("to do setValue(i64) use WASM_BIGINT");case"float":HEAPF32[ptr>>>2>>>0]=value;break;case"double":HEAPF64[ptr>>>3>>>0]=value;break;case"*":HEAPU32[ptr>>>2>>>0]=value;break;default:abort(`invalid type for setValue: ${type}`)}}var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();function syscallGetVarargI(){var ret=HEAP32[+SYSCALLS.varargs>>>2>>>0];SYSCALLS.varargs+=4;return ret}var syscallGetVarargP=syscallGetVarargI;var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.substr(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.substr(0,dir.length-1)}return root+dir},basename:path=>{if(path==="/")return"/";path=PATH.normalize(path);path=path.replace(/\/$/,"");var lastSlash=path.lastIndexOf("/");if(lastSlash===-1)return path;return path.substr(lastSlash+1)},join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(typeof crypto=="object"&&typeof crypto["getRandomValues"]=="function"){return view=>crypto.getRandomValues(view)}else if(ENVIRONMENT_IS_NODE){try{var crypto_module=require("crypto");var randomFillSync=crypto_module["randomFillSync"];if(randomFillSync){return view=>crypto_module["randomFillSync"](view)}var randomBytes=crypto_module["randomBytes"];return view=>(view.set(randomBytes(view.byteLength)),view)}catch(e){}}abort("initRandomDevice")};var randomFill=view=>(randomFill=initRandomFill())(view);var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).substr(1);to=PATH_FS.resolve(to).substr(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{idx>>>=0;var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var FS_stdin_getChar_buffer=[];var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{outIdx>>>=0;if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++>>>0]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++>>>0]=192|u>>6;heap[outIdx++>>>0]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++>>>0]=224|u>>12;heap[outIdx++>>>0]=128|u>>6&63;heap[outIdx++>>>0]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++>>>0]=240|u>>18;heap[outIdx++>>>0]=128|u>>12&63;heap[outIdx++>>>0]=128|u>>6&63;heap[outIdx++>>>0]=128|u&63}}heap[outIdx>>>0]=0;return outIdx-startIdx};function intArrayFromString(stringy,dontAddNull,length){var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array}var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else if(typeof window!="undefined"&&typeof window.prompt=="function"){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops:ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output,0));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output,0));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output&&tty.output.length>0){err(UTF8ArrayToString(tty.output,0));tty.output=[]}}}};var zeroMemory=(address,size)=>{HEAPU8.fill(0,address,address+size);return address};var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var mmapAlloc=size=>{size=alignMemory(size,65536);var ptr=_emscripten_builtin_memalign(65536,size);if(!ptr)return 0;return zeroMemory(ptr,size)};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16384|511,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,allocate:MEMFS.stream_ops.allocate,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.timestamp=Date.now();if(parent){parent.contents[name]=node;parent.timestamp=node.timestamp}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.timestamp);attr.mtime=new Date(node.timestamp);attr.ctime=new Date(node.timestamp);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){if(attr.mode!==undefined){node.mode=attr.mode}if(attr.timestamp!==undefined){node.timestamp=attr.timestamp}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){throw FS.genericErrors[44]},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){if(FS.isDir(old_node.mode)){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}}delete old_node.parent.contents[old_node.name];old_node.parent.timestamp=Date.now();old_node.name=new_name;new_dir.contents[new_name]=old_node;new_dir.timestamp=old_node.parent.timestamp},unlink(parent,name){delete parent.contents[name];parent.timestamp=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.timestamp=Date.now()},readdir(node){var entries=[".",".."];for(var key of Object.keys(node.contents)){entries.push(key)}return entries},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length>>0)}return{ptr:ptr,allocated:allocated}},msync(stream,buffer,offset,length,mmapFlags){MEMFS.stream_ops.write(stream,buffer,0,length,offset,false);return 0}}};var asyncLoad=(url,onload,onerror,noRunDep)=>{var dep=!noRunDep?getUniqueRunDependency(`al ${url}`):"";readAsync(url,arrayBuffer=>{onload(new Uint8Array(arrayBuffer));if(dep)removeRunDependency(dep)},event=>{if(onerror){onerror()}else{throw`Loading data file "${url}" failed.`}});if(dep)addRunDependency(dep)};var FS_createDataFile=(parent,name,fileData,canRead,canWrite,canOwn)=>{FS.createDataFile(parent,name,fileData,canRead,canWrite,canOwn)};var preloadPlugins=Module["preloadPlugins"]||[];var FS_handledByPreloadPlugin=(byteArray,fullname,finish,onerror)=>{if(typeof Browser!="undefined")Browser.init();var handled=false;preloadPlugins.forEach(plugin=>{if(handled)return;if(plugin["canHandle"](fullname)){plugin["handle"](byteArray,fullname,finish,onerror);handled=true}});return handled};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);function processData(byteArray){function finish(byteArray){preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}onload?.();removeRunDependency(dep)}if(FS_handledByPreloadPlugin(byteArray,fullname,finish,()=>{onerror?.();removeRunDependency(dep)})){return}finish(byteArray)}addRunDependency(dep);if(typeof url=="string"){asyncLoad(url,processData,onerror)}else{processData(url)}};var FS_modeStringToFlags=str=>{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,ErrnoError:class{constructor(errno){this.name="ErrnoError";this.errno=errno}},genericErrors:{},filesystems:null,syncFSRequests:0,FSStream:class{constructor(){this.shared={}}get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.mounted=null;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.node_ops={};this.stream_ops={};this.rdev=rdev;this.readMode=292|73;this.writeMode=146}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){path=PATH_FS.resolve(path);if(!path)return{path:"",node:null};var defaults={follow_mount:true,recurse_count:0};opts=Object.assign(defaults,opts);if(opts.recurse_count>8){throw new FS.ErrnoError(32)}var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i40){throw new FS.ErrnoError(32)}}}}return{path:current_path,node:current}},getPath(node){var path;while(true){if(FS.isRoot(node)){var mount=node.mount.mountpoint;if(!path)return mount;return mount[mount.length-1]!=="/"?`${mount}/${path}`:mount+path}path=path?`${node.name}/${path}`:node.name;node=node.parent}},hashName(parentid,name){var hash=0;for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&512){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type:type,opts:opts,mountpoint:mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name||name==="."||name===".."){throw new FS.ErrnoError(28)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},create(path,mode){mode=mode!==undefined?mode:438;mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode){mode=mode!==undefined?mode:511;mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var i=0;iFS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomLeft=randomFill(randomBuffer).byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16384|511,73);node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path}};ret.parent=ret;return ret}};return node}},{},"/proc/self/fd")},createStandardStreams(){if(Module["stdin"]){FS.createDevice("/dev","stdin",Module["stdin"])}else{FS.symlink("/dev/tty","/dev/stdin")}if(Module["stdout"]){FS.createDevice("/dev","stdout",null,Module["stdout"])}else{FS.symlink("/dev/tty","/dev/stdout")}if(Module["stderr"]){FS.createDevice("/dev","stderr",null,Module["stderr"])}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},staticInit(){[44].forEach(code=>{FS.genericErrors[code]=new FS.ErrnoError(code);FS.genericErrors[code].stack=""});FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS:MEMFS}},init(input,output,error){FS.init.initialized=true;Module["stdin"]=input||Module["stdin"];Module["stdout"]=output||Module["stdout"];Module["stderr"]=error||Module["stderr"];FS.createStandardStreams()},quit(){FS.init.initialized=false;for(var i=0;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")throw new Error("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(typeof XMLHttpRequest!="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url:url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}});function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr:ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var UTF8ToString=(ptr,maxBytesToRead)=>{ptr>>>=0;return ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):""};var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return PATH.join2(dir,path)},doStat(func,path,buf){var stat=func(path);HEAP32[buf>>>2>>>0]=stat.dev;HEAP32[buf+4>>>2>>>0]=stat.mode;HEAPU32[buf+8>>>2>>>0]=stat.nlink;HEAP32[buf+12>>>2>>>0]=stat.uid;HEAP32[buf+16>>>2>>>0]=stat.gid;HEAP32[buf+20>>>2>>>0]=stat.rdev;tempI64=[stat.size>>>0,(tempDouble=stat.size,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+24>>>2>>>0]=tempI64[0],HEAP32[buf+28>>>2>>>0]=tempI64[1];HEAP32[buf+32>>>2>>>0]=4096;HEAP32[buf+36>>>2>>>0]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();tempI64=[Math.floor(atime/1e3)>>>0,(tempDouble=Math.floor(atime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+40>>>2>>>0]=tempI64[0],HEAP32[buf+44>>>2>>>0]=tempI64[1];HEAPU32[buf+48>>>2>>>0]=atime%1e3*1e3;tempI64=[Math.floor(mtime/1e3)>>>0,(tempDouble=Math.floor(mtime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+56>>>2>>>0]=tempI64[0],HEAP32[buf+60>>>2>>>0]=tempI64[1];HEAPU32[buf+64>>>2>>>0]=mtime%1e3*1e3;tempI64=[Math.floor(ctime/1e3)>>>0,(tempDouble=Math.floor(ctime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+72>>>2>>>0]=tempI64[0],HEAP32[buf+76>>>2>>>0]=tempI64[1];HEAPU32[buf+80>>>2>>>0]=ctime%1e3*1e3;tempI64=[stat.ino>>>0,(tempDouble=stat.ino,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+88>>>2>>>0]=tempI64[0],HEAP32[buf+92>>>2>>>0]=tempI64[1];return 0},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};var convertI32PairToI53Checked=(lo,hi)=>hi+2097152>>>0<4194305-!!lo?(lo>>>0)+hi*4294967296:NaN;function ___syscall_fcntl64(fd,cmd,varargs){varargs>>>=0;SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>>1>>>0]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_ioctl(fd,op,varargs){varargs>>>=0;SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:{if(!stream.tty)return-59;return 0}case 21505:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcgets){var termios=stream.tty.ops.ioctl_tcgets(stream);var argp=syscallGetVarargP();HEAP32[argp>>>2>>>0]=termios.c_iflag||0;HEAP32[argp+4>>>2>>>0]=termios.c_oflag||0;HEAP32[argp+8>>>2>>>0]=termios.c_cflag||0;HEAP32[argp+12>>>2>>>0]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17>>>0]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>>2>>>0];var c_oflag=HEAP32[argp+4>>>2>>>0];var c_cflag=HEAP32[argp+8>>>2>>>0];var c_lflag=HEAP32[argp+12>>>2>>>0];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17>>>0])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag:c_iflag,c_oflag:c_oflag,c_cflag:c_cflag,c_lflag:c_lflag,c_cc:c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>>2>>>0]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>>1>>>0]=winsize[0];HEAP16[argp+2>>>1>>>0]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){path>>>=0;varargs>>>=0;SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>{abort("")};var nowIsMonotonic=1;var __emscripten_get_now_is_monotonic=()=>nowIsMonotonic;function __emscripten_memcpy_js(dest,src,num){dest>>>=0;src>>>=0;num>>>=0;return HEAPU8.copyWithin(dest>>>0,src>>>0,src+num>>>0)}var __emscripten_throw_longjmp=()=>{throw Infinity};function __munmap_js(addr,len,prot,flags,fd,offset_low,offset_high){addr>>>=0;len>>>=0;var offset=convertI32PairToI53Checked(offset_low,offset_high);try{var stream=SYSCALLS.getStreamFromFD(fd);if(prot&2){SYSCALLS.doMsync(addr,stream,len,flags,offset)}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var _emscripten_date_now=()=>Date.now();var _emscripten_get_now;_emscripten_get_now=()=>performance.now();var getHeapMax=()=>4294901760;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};function _emscripten_resize_heap(requestedSize){requestedSize>>>=0;var oldSize=HEAPU8.length;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}var alignUp=(x,multiple)=>x+(multiple-x%multiple)%multiple;for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignUp(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false}var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(typeof navigator=="object"&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var stringToAscii=(str,buffer)=>{for(var i=0;i>>0]=str.charCodeAt(i)}HEAP8[buffer>>>0]=0};var _environ_get=function(__environ,environ_buf){__environ>>>=0;environ_buf>>>=0;var bufSize=0;getEnvStrings().forEach((string,i)=>{var ptr=environ_buf+bufSize;HEAPU32[__environ+i*4>>>2>>>0]=ptr;stringToAscii(string,ptr);bufSize+=string.length+1});return 0};var _environ_sizes_get=function(penviron_count,penviron_buf_size){penviron_count>>>=0;penviron_buf_size>>>=0;var strings=getEnvStrings();HEAPU32[penviron_count>>>2>>>0]=strings.length;var bufSize=0;strings.forEach(string=>bufSize+=string.length+1);HEAPU32[penviron_buf_size>>>2>>>0]=bufSize;return 0};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>>2>>>0];var len=HEAPU32[iov+4>>>2>>>0];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>>=0;iovcnt>>>=0;pnum>>>=0;try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doReadv(stream,iov,iovcnt);HEAPU32[pnum>>>2>>>0]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_seek(fd,offset_low,offset_high,whence,newOffset){var offset=convertI32PairToI53Checked(offset_low,offset_high);newOffset>>>=0;try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);tempI64=[stream.position>>>0,(tempDouble=stream.position,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[newOffset>>>2>>>0]=tempI64[0],HEAP32[newOffset+4>>>2>>>0]=tempI64[1];if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>>2>>>0];var len=HEAPU32[iov+4>>>2>>>0];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(typeof offset!="undefined"){offset+=curr}}return ret};function _fd_write(fd,iov,iovcnt,pnum){iov>>>=0;iovcnt>>>=0;pnum>>>=0;try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doWritev(stream,iov,iovcnt);HEAPU32[pnum>>>2>>>0]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var wasmTableMirror=[];var wasmTable;var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){if(funcPtr>=wasmTableMirror.length)wasmTableMirror.length=funcPtr+1;wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var uleb128Encode=(n,target)=>{if(n<128){target.push(n)}else{target.push(n%128|128,n>>7)}};var sigToWasmTypes=sig=>{var typeNames={i:"i32",j:"i64",f:"f32",d:"f64",e:"externref",p:"i32"};var type={parameters:[],results:sig[0]=="v"?[]:[typeNames[sig[0]]]};for(var i=1;i{var sigRet=sig.slice(0,1);var sigParam=sig.slice(1);var typeCodes={i:127,p:127,j:126,f:125,d:124,e:111};target.push(96);uleb128Encode(sigParam.length,target);for(var i=0;i{if(typeof WebAssembly.Function=="function"){return new WebAssembly.Function(sigToWasmTypes(sig),func)}var typeSectionBody=[1];generateFuncType(sig,typeSectionBody);var bytes=[0,97,115,109,1,0,0,0,1];uleb128Encode(typeSectionBody.length,bytes);bytes.push(...typeSectionBody);bytes.push(2,7,1,1,101,1,102,0,0,7,5,1,1,102,0,0);var module=new WebAssembly.Module(new Uint8Array(bytes));var instance=new WebAssembly.Instance(module,{e:{f:func}});var wrappedFunc=instance.exports["f"];return wrappedFunc};var updateTableMap=(offset,count)=>{if(functionsInTableMap){for(var i=offset;i{if(!functionsInTableMap){functionsInTableMap=new WeakMap;updateTableMap(0,wasmTable.length)}return functionsInTableMap.get(func)||0};var freeTableIndexes=[];var getEmptyTableSlot=()=>{if(freeTableIndexes.length){return freeTableIndexes.pop()}try{wasmTable.grow(1)}catch(err){if(!(err instanceof RangeError)){throw err}throw"Unable to grow wasm table. Set ALLOW_TABLE_GROWTH."}return wasmTable.length-1};var setWasmTableEntry=(idx,func)=>{wasmTable.set(idx,func);wasmTableMirror[idx]=wasmTable.get(idx)};var addFunction=(func,sig)=>{var rtn=getFunctionAddress(func);if(rtn){return rtn}var ret=getEmptyTableSlot();try{setWasmTableEntry(ret,func)}catch(err){if(!(err instanceof TypeError)){throw err}var wrapped=convertJsFunctionToWasm(func,sig);setWasmTableEntry(ret,wrapped)}functionsInTableMap.set(func,ret);return ret};var removeFunction=index=>{functionsInTableMap.delete(getWasmTableEntry(index));setWasmTableEntry(index,null);freeTableIndexes.push(index)};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);FS.createPreloadedFile=FS_createPreloadedFile;FS.staticInit();var wasmImports={m:___syscall_fcntl64,J:___syscall_ioctl,K:___syscall_openat,O:__abort_js,L:__emscripten_get_now_is_monotonic,N:__emscripten_memcpy_js,E:__emscripten_throw_longjmp,n:__munmap_js,M:_emscripten_date_now,j:_emscripten_get_now,F:_emscripten_resize_heap,G:_environ_get,H:_environ_sizes_get,Q:_exit,k:_fd_close,I:_fd_read,o:_fd_seek,l:_fd_write,i:invoke_ii,d:invoke_iii,h:invoke_iiii,A:invoke_iij,C:invoke_iijjii,x:invoke_jii,f:invoke_vi,e:invoke_vii,c:invoke_viii,b:invoke_viiii,a:invoke_viiiii,g:invoke_viiiiii,P:invoke_viiiiiii,p:invoke_viiiiiji,q:invoke_viiiiji,r:invoke_viiij,s:invoke_viiiji,w:invoke_viij,D:invoke_viiji,z:invoke_viijj,v:invoke_viijji,y:invoke_viijjj,B:invoke_vij,t:invoke_viji,u:invoke_vijj};var wasmExports=createWasm();var ___wasm_call_ctors=()=>(___wasm_call_ctors=wasmExports["S"])();var _uc2_open=Module["_uc2_open"]=(a0,a1)=>(_uc2_open=Module["_uc2_open"]=wasmExports["T"])(a0,a1);var _uc2_close=Module["_uc2_close"]=a0=>(_uc2_close=Module["_uc2_close"]=wasmExports["U"])(a0);var _uc2_strerror=Module["_uc2_strerror"]=a0=>(_uc2_strerror=Module["_uc2_strerror"]=wasmExports["V"])(a0);var _uc2_version=Module["_uc2_version"]=(a0,a1)=>(_uc2_version=Module["_uc2_version"]=wasmExports["W"])(a0,a1);var _uc2_mem_map=Module["_uc2_mem_map"]=(a0,a1,a2)=>(_uc2_mem_map=Module["_uc2_mem_map"]=wasmExports["X"])(a0,a1,a2);var _uc2_mem_unmap=Module["_uc2_mem_unmap"]=(a0,a1,a2)=>(_uc2_mem_unmap=Module["_uc2_mem_unmap"]=wasmExports["Y"])(a0,a1,a2);var _uc2_mem_protect=Module["_uc2_mem_protect"]=(a0,a1,a2,a3)=>(_uc2_mem_protect=Module["_uc2_mem_protect"]=wasmExports["Z"])(a0,a1,a2,a3);var _uc2_mem_write=Module["_uc2_mem_write"]=(a0,a1,a2,a3)=>(_uc2_mem_write=Module["_uc2_mem_write"]=wasmExports["_"])(a0,a1,a2,a3);var _malloc=Module["_malloc"]=a0=>(_malloc=Module["_malloc"]=wasmExports["$"])(a0);var _free=Module["_free"]=a0=>(_free=Module["_free"]=wasmExports["aa"])(a0);var _uc2_mem_read=Module["_uc2_mem_read"]=(a0,a1,a2,a3)=>(_uc2_mem_read=Module["_uc2_mem_read"]=wasmExports["ba"])(a0,a1,a2,a3);var _uc2_reg_write=Module["_uc2_reg_write"]=(a0,a1,a2)=>(_uc2_reg_write=Module["_uc2_reg_write"]=wasmExports["ca"])(a0,a1,a2);var _uc2_reg_read=Module["_uc2_reg_read"]=(a0,a1)=>(_uc2_reg_read=Module["_uc2_reg_read"]=wasmExports["da"])(a0,a1);var _uc2_emu_start=Module["_uc2_emu_start"]=(a0,a1,a2,a3,a4)=>(_uc2_emu_start=Module["_uc2_emu_start"]=wasmExports["ea"])(a0,a1,a2,a3,a4);var _uc2_emu_stop=Module["_uc2_emu_stop"]=a0=>(_uc2_emu_stop=Module["_uc2_emu_stop"]=wasmExports["fa"])(a0);var _uc2_hook_add_code=Module["_uc2_hook_add_code"]=(a0,a1,a2)=>(_uc2_hook_add_code=Module["_uc2_hook_add_code"]=wasmExports["ga"])(a0,a1,a2);var _uc2_hook_del=Module["_uc2_hook_del"]=(a0,a1)=>(_uc2_hook_del=Module["_uc2_hook_del"]=wasmExports["ha"])(a0,a1);var _uc2_set_code_hook_cb=Module["_uc2_set_code_hook_cb"]=a0=>(_uc2_set_code_hook_cb=Module["_uc2_set_code_hook_cb"]=wasmExports["ia"])(a0);var _uc2_hook_add_mem_invalid=Module["_uc2_hook_add_mem_invalid"]=a0=>(_uc2_hook_add_mem_invalid=Module["_uc2_hook_add_mem_invalid"]=wasmExports["ja"])(a0);var _uc2_set_mem_hook_cb=Module["_uc2_set_mem_hook_cb"]=a0=>(_uc2_set_mem_hook_cb=Module["_uc2_set_mem_hook_cb"]=wasmExports["ka"])(a0);var _uc2_scratch_alloc=Module["_uc2_scratch_alloc"]=a0=>(_uc2_scratch_alloc=Module["_uc2_scratch_alloc"]=wasmExports["la"])(a0);var _uc2_scratch_free=Module["_uc2_scratch_free"]=a0=>(_uc2_scratch_free=Module["_uc2_scratch_free"]=wasmExports["ma"])(a0);var _emscripten_builtin_memalign=(a0,a1)=>(_emscripten_builtin_memalign=wasmExports["oa"])(a0,a1);var _setThrew=(a0,a1)=>(_setThrew=wasmExports["pa"])(a0,a1);var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["qa"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["ra"])();var dynCall_viiji=Module["dynCall_viiji"]=(a0,a1,a2,a3,a4,a5)=>(dynCall_viiji=Module["dynCall_viiji"]=wasmExports["sa"])(a0,a1,a2,a3,a4,a5);var dynCall_iijjii=Module["dynCall_iijjii"]=(a0,a1,a2,a3,a4,a5,a6,a7)=>(dynCall_iijjii=Module["dynCall_iijjii"]=wasmExports["ta"])(a0,a1,a2,a3,a4,a5,a6,a7);var dynCall_viiij=Module["dynCall_viiij"]=(a0,a1,a2,a3,a4,a5)=>(dynCall_viiij=Module["dynCall_viiij"]=wasmExports["ua"])(a0,a1,a2,a3,a4,a5);var dynCall_viji=Module["dynCall_viji"]=(a0,a1,a2,a3,a4)=>(dynCall_viji=Module["dynCall_viji"]=wasmExports["va"])(a0,a1,a2,a3,a4);var dynCall_vij=Module["dynCall_vij"]=(a0,a1,a2,a3)=>(dynCall_vij=Module["dynCall_vij"]=wasmExports["wa"])(a0,a1,a2,a3);var dynCall_viijj=Module["dynCall_viijj"]=(a0,a1,a2,a3,a4,a5,a6)=>(dynCall_viijj=Module["dynCall_viijj"]=wasmExports["xa"])(a0,a1,a2,a3,a4,a5,a6);var dynCall_jii=Module["dynCall_jii"]=(a0,a1,a2)=>(dynCall_jii=Module["dynCall_jii"]=wasmExports["ya"])(a0,a1,a2);var dynCall_viij=Module["dynCall_viij"]=(a0,a1,a2,a3,a4)=>(dynCall_viij=Module["dynCall_viij"]=wasmExports["za"])(a0,a1,a2,a3,a4);var dynCall_vijj=Module["dynCall_vijj"]=(a0,a1,a2,a3,a4,a5)=>(dynCall_vijj=Module["dynCall_vijj"]=wasmExports["Aa"])(a0,a1,a2,a3,a4,a5);var dynCall_viijji=Module["dynCall_viijji"]=(a0,a1,a2,a3,a4,a5,a6,a7)=>(dynCall_viijji=Module["dynCall_viijji"]=wasmExports["Ba"])(a0,a1,a2,a3,a4,a5,a6,a7);var dynCall_iij=Module["dynCall_iij"]=(a0,a1,a2,a3)=>(dynCall_iij=Module["dynCall_iij"]=wasmExports["Ca"])(a0,a1,a2,a3);var dynCall_viijjj=Module["dynCall_viijjj"]=(a0,a1,a2,a3,a4,a5,a6,a7,a8)=>(dynCall_viijjj=Module["dynCall_viijjj"]=wasmExports["Da"])(a0,a1,a2,a3,a4,a5,a6,a7,a8);var dynCall_viiiji=Module["dynCall_viiiji"]=(a0,a1,a2,a3,a4,a5,a6)=>(dynCall_viiiji=Module["dynCall_viiiji"]=wasmExports["Ea"])(a0,a1,a2,a3,a4,a5,a6);var dynCall_viiiiji=Module["dynCall_viiiiji"]=(a0,a1,a2,a3,a4,a5,a6,a7)=>(dynCall_viiiiji=Module["dynCall_viiiiji"]=wasmExports["Fa"])(a0,a1,a2,a3,a4,a5,a6,a7);var dynCall_viiiiiji=Module["dynCall_viiiiiji"]=(a0,a1,a2,a3,a4,a5,a6,a7,a8)=>(dynCall_viiiiiji=Module["dynCall_viiiiiji"]=wasmExports["Ga"])(a0,a1,a2,a3,a4,a5,a6,a7,a8);function invoke_viii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ii(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vii(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiji(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_viiji(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iijjii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return dynCall_iijjii(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vij(index,a1,a2,a3){var sp=stackSave();try{dynCall_vij(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iij(index,a1,a2,a3){var sp=stackSave();try{return dynCall_iij(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijj(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{dynCall_viijj(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijjj(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{dynCall_viijjj(index,a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jii(index,a1,a2){var sp=stackSave();try{return dynCall_jii(index,a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viij(index,a1,a2,a3,a4){var sp=stackSave();try{dynCall_viij(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijji(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{dynCall_viijji(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_vijj(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viji(index,a1,a2,a3,a4){var sp=stackSave();try{dynCall_viji(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiji(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{dynCall_viiiji(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiij(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_viiij(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiji(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{dynCall_viiiiji(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiji(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{dynCall_viiiiiji(index,a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function applySignatureConversions(wasmExports){wasmExports=Object.assign({},wasmExports);var makeWrapper_pp=f=>a0=>f(a0)>>>0;var makeWrapper_ppp=f=>(a0,a1)=>f(a0,a1)>>>0;var makeWrapper_p=f=>()=>f()>>>0;wasmExports["$"]=makeWrapper_pp(wasmExports["$"]);wasmExports["oa"]=makeWrapper_ppp(wasmExports["oa"]);wasmExports["_emscripten_stack_alloc"]=makeWrapper_pp(wasmExports["_emscripten_stack_alloc"]);wasmExports["ra"]=makeWrapper_p(wasmExports["ra"]);return wasmExports}Module["addFunction"]=addFunction;Module["removeFunction"]=removeFunction;Module["setValue"]=setValue;Module["getValue"]=getValue;Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;var calledRun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(){if(runDependencies>0){return}preRun();if(runDependencies>0){return}function doRun(){if(calledRun)return;calledRun=true;Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve(Module);if(Module["onRuntimeInitialized"])Module["onRuntimeInitialized"]();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(function(){setTimeout(function(){Module["setStatus"]("")},1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();moduleRtn=readyPromise; + + + return moduleRtn; +} +); +})(); +export default UnicornModule; diff --git a/frontend/src/apple/sap/vendor/unicorn.wasm b/frontend/src/apple/sap/vendor/unicorn.wasm new file mode 100755 index 00000000..19b6a910 Binary files /dev/null and b/frontend/src/apple/sap/vendor/unicorn.wasm differ diff --git a/frontend/src/apple/sap/worker.ts b/frontend/src/apple/sap/worker.ts new file mode 100644 index 00000000..1da960a5 --- /dev/null +++ b/frontend/src/apple/sap/worker.ts @@ -0,0 +1,143 @@ +// SAP signer worker: hosts the TCI emulation so its synchronous bursts never +// block the UI thread. Speaks a tiny request/response protocol with the main +// thread (see client.ts); all Apple network traffic stays on the main thread. + +import { SapMachine } from "./machine"; +import type { SapAssetBundle } from "./types"; + +interface OpenRequest { + type: "open"; + id: number; + assets: { [K in keyof SapAssetBundle]: ArrayBuffer }; + wasmBinary: ArrayBuffer; +} + +interface InitializeRequest { + type: "initialize"; + id: number; + hardwareID: ArrayBuffer; +} + +interface ExchangeRequest { + type: "exchange"; + id: number; + version: number; + hardwareID: ArrayBuffer; + contextValue: number; + input: ArrayBuffer; +} + +interface SignRequest { + type: "sign"; + id: number; + contextValue: number; + input: ArrayBuffer; +} + +interface TeardownRequest { + type: "teardown"; + id: number; + contextValue: number; +} + +interface CloseRequest { + type: "close"; + id: number; +} + +type WorkerRequest = + | OpenRequest + | InitializeRequest + | ExchangeRequest + | SignRequest + | TeardownRequest + | CloseRequest; + +let machine: SapMachine | null = null; +let hardwareID: Uint8Array | null = null; + +function reply(id: number, payload: Record): void { + self.postMessage({ type: "result", id, ...payload }); +} + +function replyError(id: number, message: string): void { + self.postMessage({ type: "error", id, message }); +} + +self.onmessage = async (event: MessageEvent) => { + const request = event.data; + try { + switch (request.type) { + case "open": { + if (machine) { + machine.close(); + } + machine = await SapMachine.open( + { + commerceKit: new Uint8Array(request.assets.commerceKit), + commerceCore: new Uint8Array(request.assets.commerceCore), + coreFP: new Uint8Array(request.assets.coreFP), + coreFPICXS: new Uint8Array(request.assets.coreFPICXS), + }, + { wasmBinary: request.wasmBinary }, + ); + hardwareID = new Uint8Array(0); + reply(request.id, {}); + break; + } + case "initialize": { + if (!machine) { + throw new Error("SAP machine is not open"); + } + hardwareID = new Uint8Array(request.hardwareID); + const contextValue = machine.initialize(hardwareID); + reply(request.id, { contextValue }); + break; + } + case "exchange": { + if (!machine || !hardwareID) { + throw new Error("SAP machine is not open"); + } + const result = machine.exchange( + request.version, + new Uint8Array(request.hardwareID), + request.contextValue, + new Uint8Array(request.input), + ); + reply(request.id, { + output: result.output.buffer, + state: result.state, + }); + break; + } + case "sign": { + if (!machine) { + throw new Error("SAP machine is not open"); + } + const signature = machine.sign( + request.contextValue, + new Uint8Array(request.input), + ); + reply(request.id, { signature: signature.buffer }); + break; + } + case "teardown": { + machine?.teardown(request.contextValue); + reply(request.id, {}); + break; + } + case "close": { + machine?.close(); + machine = null; + hardwareID = null; + reply(request.id, {}); + break; + } + } + } catch (error) { + replyError( + request.id, + error instanceof Error ? error.message : String(error), + ); + } +}; diff --git a/frontend/src/components/Account/AccountList.tsx b/frontend/src/components/Account/AccountList.tsx index e8fea3d1..78db269c 100644 --- a/frontend/src/components/Account/AccountList.tsx +++ b/frontend/src/components/Account/AccountList.tsx @@ -3,11 +3,13 @@ import { Link, NavLink } from "react-router-dom"; import { useTranslation } from "react-i18next"; import PageContainer from "../Layout/PageContainer"; import { useAccountsStore } from "../../store/accounts"; +import { useSapWarmup } from "../../hooks/useSapWarmup"; import { storeIdToCountry } from "../../apple/config"; export default function AccountList() { const { t } = useTranslation(); const { accounts, loading, loadAccounts } = useAccountsStore(); + useSapWarmup(); useEffect(() => { loadAccounts(); diff --git a/frontend/src/components/Account/AddAccountForm.tsx b/frontend/src/components/Account/AddAccountForm.tsx index 067e4dea..73999df5 100644 --- a/frontend/src/components/Account/AddAccountForm.tsx +++ b/frontend/src/components/Account/AddAccountForm.tsx @@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; import PageContainer from "../Layout/PageContainer"; import Spinner from "../common/Spinner"; +import SapStatus from "../common/SapStatus"; import { useAccounts } from "../../hooks/useAccounts"; import { useToastStore } from "../../store/toast"; import { authenticate, AuthenticationError } from "../../apple/authenticate"; @@ -61,6 +62,7 @@ export default function AddAccountForm() {
+