diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 0000000..8a33e0c --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,130 @@ +name: Docs + +# Documentation (Sphinx HTML) build + dual publish, adapted from Penguin's +# docs.yaml. +# +# Difference from Penguin: Penguin builds its docs from a live runtime image +# (its Sphinx run introspects live Python plugins), so it triggers via +# `workflow_run` after the container publish. igloo_driver is a pure C kernel +# module with no runtime doc path — the docs are a plain `sphinx-build` of +# docs/ (Doxygen extracts the C API; see docs/conf.py). That build depends only +# on docs/ + src/, NOT on the (heavy, multi-arch) module build, so we trigger +# directly on pushes to main that touch docs, plus manual dispatch. This keeps +# docs decoupled from the module release, matching Penguin's intent. +# +# The deploy_static job is lifted almost verbatim from Penguin: it publishes the +# rendered HTML to BOTH GitHub Pages and a generated-only `docs` branch. +on: + push: + branches: [main] + paths: + - "docs/**" + - "README.md" + - "src/**" + - ".github/workflows/docs.yaml" + workflow_dispatch: + +permissions: + contents: write # force-push the rendered HTML to the `docs` branch + pages: write + id-token: write + +concurrency: + group: "docs" + cancel-in-progress: false + +jobs: + build_docs: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Doxygen + run: | + sudo apt-get update + sudo apt-get install -y doxygen + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Install documentation dependencies + run: pip install -r docs/requirements.txt + + - name: Build HTML docs + run: | + # conf.py runs Doxygen itself, so a bare sphinx-build is + # self-contained. -W turns warnings into errors to keep the docs + # honest (the build is currently warning-clean). + sphinx-build -b html -W --keep-going docs docs/_build/html + tar -czvf docs.tar.gz -C docs/_build/html . + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: html + path: docs.tar.gz + + deploy_static: + needs: build_docs + runs-on: ubuntu-latest + permissions: + contents: write # force-push the rendered HTML to the `docs` branch + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: html + - name: Unzip HTML + run: | + mkdir html_content + tar -xvf docs.tar.gz -C html_content + + # Also publish the rendered site to a `docs` branch so it can be cloned + # and served directly (git clone --branch docs --depth 1 …), independent + # of the GitHub Pages artifact deploy below. force_orphan keeps `docs` as a + # single-commit, generated-only branch with no shared history — never + # hand-edit it. peaceiris disables Jekyll (drops a .nojekyll) by default, + # which Sphinx's `_static/` dirs require. + - name: Publish HTML to the docs branch + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./html_content + publish_branch: docs + force_orphan: true + + # A re-run does NOT clear artifacts uploaded by a prior attempt. + # upload-pages-artifact always names its artifact "github-pages", so a + # re-run leaves 2+ identically-named artifacts and deploy-pages@v4 aborts + # with "Multiple artifacts named github-pages were unexpectedly found". + # Delete any stale one first so exactly one exists at deploy time. + - name: Delete stale github-pages artifacts from prior attempts + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + ids=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts" \ + --paginate --jq '.artifacts[] | select(.name=="github-pages") | .id') + for id in $ids; do + echo "Deleting stale github-pages artifact $id" + gh api -X DELETE "repos/${GITHUB_REPOSITORY}/actions/artifacts/${id}" || true + done + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: 'html_content' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md new file mode 100644 index 0000000..c40e95b --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +# igloo_driver + +**igloo_driver** is the in-guest half of the [IGLOO / Penguin](https://github.com/rehosting/penguin) +firmware rehosting stack — a small, built-in Linux kernel module that turns an +emulated guest kernel into a **programmable kernel debugger**. From a host-side +PANDA / QEMU analysis plugin (Penguin) you can, at runtime: + +- **Read and write** kernel and user memory (bytes, strings, pointer arrays, + whole process address spaces); +- **Introspect the OS** — walk processes and read their maps, `argv`, `environ`, + open FDs, registers, and executable paths; +- **Install hooks** — kprobes, uprobes, syscall entry/return hooks, and signal + delivery hooks, each reported back to the host (and optionally able to skip a + syscall or drop a signal); +- **Call kernel functions** via an FFI, resolve symbols with `kallsyms`, and + generate trampolines; +- **Synthesize pseudo-files and devices** — `/proc`, `/sys`, `/dev`, `sysctl`, + anonymous inodes, sockets, and MTD flash — backed by host-side models. + +All of this rides on a single cooperative shared-memory protocol, **Portal**, +carried over a tiny per-architecture **hypercall** ABI (~50 operations in all). +The module has no configuration of its own; it is orchestrated entirely from the +host by Penguin. + +> **Guest side vs. host side.** igloo_driver runs *inside the guest*. The Python +> API that drives it (`plugins.portal.read_str(...)`, kprobe/uprobe +> registration, pseudo-file models) lives in Penguin. If you want the host API, +> see Penguin's [Portal](https://github.com/rehosting/penguin/blob/main/docs/portal.md), +> [kprobes](https://github.com/rehosting/penguin/blob/main/docs/kprobes.md), and +> [uprobes](https://github.com/rehosting/penguin/blob/main/docs/uprobes.md) docs. + +## Documentation + +Full documentation — architecture, the Portal operation catalog, the hypercall +ABI, hooks, hyperfs, pseudo-file synthesis, and an auto-extracted C API +reference — is published to **GitHub Pages** and mirrored to the **`docs` +branch** of this repository. + +To build the docs locally: + +```bash +python3 -m venv .venv && . .venv/bin/activate +pip install -r docs/requirements.txt +# Doxygen must also be on PATH (conf.py runs it to extract the C API): +sphinx-build -b html docs docs/_build/html +# open docs/_build/html/index.html +``` + +## Building the module + +igloo_driver is cross-compiled for ~13 architectures against multiple kernel +versions inside a Docker toolchain container. The wrapper is `build.sh`. + +**Prerequisites:** Docker; a toolchain image (default +`rehosting/embedded-toolchains:latest`); and kernel headers as +`local_packages/kernel-devel-all.tar.gz` (published by +[`linux_builder`](https://github.com/rehosting/linux_builder)): + +```bash +mkdir -p local_packages +curl -L -o local_packages/kernel-devel-all.tar.gz \ + https://github.com/rehosting/linux_builder/releases/latest/download/kernel-devel-all.tar.gz +``` + +Then build: + +```bash +./build.sh # all default targets, versions 4.10 & 6.13 +./build.sh --versions "4.10 6.7" \ + --targets "armel mipseb mipsel" # a subset +./build.sh --release # stripped modules +./build.sh --help # all options +``` + +The output is a single archive, `igloo_driver.tar.gz`, containing +`igloo.ko.` per target/version. See +[docs/building.md](docs/building.md) for the full reference. + +## Using it with Penguin + +Drop `igloo_driver.tar.gz` into Penguin's `local_packages/` before a +`./penguin --build` to test a local driver build, or let Penguin fetch the +pinned release. Pushes to `main` publish a versioned GitHub release of the +built modules. + +## Repository layout + +| Path | Contents | +|---|---| +| `src/igloo_hc.c` | Module entry point and subsystem init order. | +| `src/ehypercall.h`, `src/igloo_hypercall_consts.h` | The per-arch hypercall primitive and its numbers. | +| `src/portal/` | The Portal protocol: dispatch loop, op handlers, shared-memory types. | +| `src/hooks/` | Syscall / ioctl / signal / socket / uname / mount / open hooks. | +| `src/hyperfs/` | Host-backed pseudo-filesystem. | +| `src/netdevs/` | Synthetic network devices (`igloonet`). | +| `scripts/` | Build-time helpers (e.g. trampoline codegen). | +| `docs/` | Sphinx documentation sources. | + +## License + +The kernel module is GPL-licensed (`MODULE_LICENSE("GPL")`). diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..9a534b9 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +_build/ +_doxygen/ diff --git a/docs/Doxyfile b/docs/Doxyfile new file mode 100644 index 0000000..24d0bf8 --- /dev/null +++ b/docs/Doxyfile @@ -0,0 +1,72 @@ +# Doxygen configuration for igloo_driver. +# +# We only want the XML backend: Breathe (in conf.py) consumes the XML and +# Sphinx renders the HTML/PDF. No standalone Doxygen HTML is produced. +# +# This file is driven by conf.py, which sets OUTPUT_DIRECTORY / INPUT via +# environment overrides so a bare `sphinx-build docs/ _build/` is fully +# self-contained (Doxygen runs first, then Sphinx). See docs/conf.py. + +PROJECT_NAME = "igloo_driver" +PROJECT_BRIEF = "In-guest kernel agent for IGLOO / Penguin rehosting" + +# INPUT and OUTPUT_DIRECTORY are overridden from conf.py via the environment +# (Doxygen expands $(VAR) at parse time). Defaults keep manual runs working +# from the docs/ directory. +INPUT = $(DOXYGEN_INPUT) +OUTPUT_DIRECTORY = $(DOXYGEN_OUTPUT) + +RECURSIVE = YES +FILE_PATTERNS = *.c *.h + +# `X` is the X-macro helper redefined in several headers (portal_op_list.h +# consumers). Documenting it produces a spurious "Duplicate C declaration" +# across files and carries no reference value — drop it. +EXCLUDE_SYMBOLS = X + +# The source is only sparsely commented, so extract every declaration — a +# signature-level reference is still useful, and documented symbols get their +# prose on top. Undocumented members are included too. +EXTRACT_ALL = YES +EXTRACT_STATIC = YES +EXTRACT_PRIVATE = YES +EXTRACT_LOCAL_CLASSES = YES + +# XML only; Breathe reads this. +GENERATE_HTML = NO +GENERATE_LATEX = NO +GENERATE_XML = YES +XML_PROGRAMLISTING = YES + +# Quiet: warnings about undocumented members would be pure noise here. +QUIET = YES +WARNINGS = NO +WARN_IF_UNDOCUMENTED = NO +WARN_IF_DOC_ERROR = YES +WARN_NO_PARAMDOC = NO + +# Preprocessing. The portal op list is an X-macro; we do NOT try to expand it +# (the generated handle_op_* prototypes are cataloged by hand in portal.md, +# which reads far better than an expanded macro dump). Plain enums, structs and +# functions are extracted literally. +ENABLE_PREPROCESSING = YES +# Expand ONLY the predefined macros below (not the X-macro op list). This lets +# us strip kernel sparse annotations (__user etc.) that the Sphinx C domain +# cannot parse in function-pointer members, while leaving PORTAL_OP_LIST intact. +MACRO_EXPANSION = YES +EXPAND_ONLY_PREDEF = YES +SEARCH_INCLUDES = NO +PREDEFINED = __user= \ + __kernel= \ + __iomem= \ + __force= \ + __rcu= \ + __must_check= \ + __maybe_unused= + +# Keep the layout arch-neutral: the per-arch hypercall asm in ehypercall.h is +# guarded by CONFIG_* — define none so every branch's declaration is visible in +# the parse (Doxygen shows the guarded blocks as documented source regardless). + +OPTIMIZE_OUTPUT_FOR_C = YES +TYPEDEF_HIDES_STRUCT = YES diff --git a/docs/_static/.gitkeep b/docs/_static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/api/hooks_api.md b/docs/api/hooks_api.md new file mode 100644 index 0000000..eea94c6 --- /dev/null +++ b/docs/api/hooks_api.md @@ -0,0 +1,22 @@ +# Hooks + +Syscall, signal, and ioctl hook structures and entry points. See +[Hooks](../hooks.md) for the narrative reference. + +## Syscall hooks — `syscalls_hc.h` + +```{doxygenfile} syscalls_hc.h +:project: igloo_driver +``` + +## Signal hooks — `signal_hc.h` + +```{doxygenfile} signal_hc.h +:project: igloo_driver +``` + +## ioctl hooks — `ioctl_hc.h` + +```{doxygenfile} ioctl_hc.h +:project: igloo_driver +``` diff --git a/docs/api/hypercall_api.md b/docs/api/hypercall_api.md new file mode 100644 index 0000000..4df35fa --- /dev/null +++ b/docs/api/hypercall_api.md @@ -0,0 +1,16 @@ +# Hypercall ABI + +The per-architecture hypercall primitive and the hypercall number constants. +See [The hypercall ABI](../hypercall_abi.md) for the narrative reference. + +## Hypercall numbers — `igloo_hypercall_consts.h` + +```{doxygenfile} igloo_hypercall_consts.h +:project: igloo_driver +``` + +## The hypercall primitive — `ehypercall.h` + +```{doxygenfile} ehypercall.h +:project: igloo_driver +``` diff --git a/docs/api/hyperfs_api.md b/docs/api/hyperfs_api.md new file mode 100644 index 0000000..af31a7f --- /dev/null +++ b/docs/api/hyperfs_api.md @@ -0,0 +1,16 @@ +# hyperfs + +The host-backed pseudo-filesystem operation set and module entry points. See +[hyperfs](../hyperfs.md) for the narrative reference. + +## hyperfs operations — `hyperfs_consts.h` + +```{doxygenfile} hyperfs_consts.h +:project: igloo_driver +``` + +## Module entry points — `hyperfs.h` + +```{doxygenfile} hyperfs.h +:project: igloo_driver +``` diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000..7f0a006 --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,27 @@ +# C API reference + +These pages are **auto-extracted from the source** by Doxygen and rendered with +[Breathe](https://breathe.readthedocs.io/). They reflect the actual +declarations in `src/` at build time — signatures, structs, and enums — with any +in-source documentation comments included. + +```{admonition} Coverage +:class: note +The source is only sparsely commented, so these pages are primarily a +*signature-level* reference. The conceptual explanation of each subsystem lives +in the [guide](../index.md) pages; the operation catalog in +[Portal](../portal.md) is the authoritative human-readable list (the handler +prototypes are generated by an X-macro, which is why they are cataloged by hand +there rather than expanded here). +``` + +```{toctree} +:maxdepth: 1 + +portal_api +types_api +hypercall_api +hooks_api +hyperfs_api +scope_api +``` diff --git a/docs/api/portal_api.md b/docs/api/portal_api.md new file mode 100644 index 0000000..d298a82 --- /dev/null +++ b/docs/api/portal_api.md @@ -0,0 +1,16 @@ +# Portal (internals) + +The Portal dispatch engine and the internal helpers shared by the operation +handlers. + +## `portal.h` + +```{doxygenfile} portal.h +:project: igloo_driver +``` + +## `portal_internal.h` + +```{doxygenfile} portal_internal.h +:project: igloo_driver +``` diff --git a/docs/api/scope_api.md b/docs/api/scope_api.md new file mode 100644 index 0000000..1350597 --- /dev/null +++ b/docs/api/scope_api.md @@ -0,0 +1,8 @@ +# Analysis scoping + +Gates analysis-event emission to the firmware-under-analysis process subtree. +See [Analysis scoping](../architecture.md#analysis-scoping) for the rationale. + +```{doxygenfile} scope.h +:project: igloo_driver +``` diff --git a/docs/api/types_api.md b/docs/api/types_api.md new file mode 100644 index 0000000..602e174 --- /dev/null +++ b/docs/api/types_api.md @@ -0,0 +1,8 @@ +# Portal types + +The shared-memory region layout, operation/response enums, probe types, and the +OS-introspection (OSI) structures. + +```{doxygenfile} portal_types.h +:project: igloo_driver +``` diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..e24d07c --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,89 @@ +# Architecture + +igloo_driver is a **built-in** kernel module (compiled into the guest kernel, +not loaded at runtime — its `cleanup_module` is unreachable). It initializes +early in boot and immediately opens a communication channel to the host analysis +engine. + +## The big picture + +``` + GUEST (emulated firmware) HOST (PANDA / QEMU + Penguin) + ┌───────────────────────────────────┐ ┌────────────────────────────────┐ + │ firmware userland │ │ Penguin pyplugins │ + │ │ syscalls / signals / ... │ │ plugins.portal.read_str() │ + │ ▼ │ │ kprobe / uprobe / syscall │ + │ ┌─────────────────────────────┐ │ hypercall│ pseudo-file models │ + │ │ igloo_driver │◀──┼───────────┼──▶ Portal host handler │ + │ │ hooks · portal · hyperfs │ │ (shared │ │ + │ │ netdevs · scope │ │ memory) │ │ + │ └─────────────────────────────┘ │ │ │ + │ guest Linux kernel │ │ │ + └───────────────────────────────────┘ └────────────────────────────────┘ +``` + +Two layers carry every interaction: + +1. **The hypercall ABI** (`ehypercall.h`, `igloo_hypercall_consts.h`) — a single + privileged/no-op instruction per architecture that traps into the emulator, + passing up to four register arguments. This is the raw signalling primitive. + See [The hypercall ABI](hypercall_abi.md). + +2. **Portal** (`src/portal/`) — a cooperative shared-memory protocol layered on + top of the hypercall. The guest registers a page with the host; the host + writes an *operation* into that page and re-enters the guest, which executes + the operation and writes the result back. This is how the ~50 rich + operations (read/write memory, OS introspection, hook management, pseudo-file + creation, FFI) are carried. See [Portal](portal.md). + +## Module layout + +| Directory / file | Responsibility | +|---|---| +| `src/igloo_hc.c` | Module entry point (`init_module`): reports its load base, then initializes every subsystem in order. | +| `src/ehypercall.h` | The per-architecture hypercall instruction (`igloo_hypercall4`). | +| `src/igloo_hypercall_consts.h` | Hypercall number constants (network setup, uname, syscall/uprobe/kprobe events, memory-region registration, …). | +| `src/portal/` | The Portal protocol: dispatch loop, the op handlers, and the shared-memory types. | +| `src/hooks/` | Guest-kernel hooks: syscalls, ioctl, signals, sockets, uname, mount blocking, open interception. | +| `src/hyperfs/` | Host-backed pseudo-filesystem (`hyperfs`) — files whose reads/writes are answered by the host. | +| `src/netdevs/` | Synthetic network devices (`igloonet`). | +| `src/portal/scope.c` (+ `scope.h`) | **Analysis scoping** — gates event emission to the firmware-under-analysis process subtree. | +| `scripts/gen_portal_tramp.py` | Generates per-architecture trampoline code used by `tramp_generate`. | + +## Initialization order + +`init_module()` (in `src/igloo_hc.c`) runs these steps in sequence; a failure in +any one aborts module init: + +1. `report_base_addr()` — looks up a known `.text` symbol via + `kallsyms_lookup_name()` and reports the module's load base to the host with + the `IGLOO_MODULE_BASE` hypercall, so the host can relocate symbols. +2. `igloo_scope_init()` — captures the **initial UTS namespace** while `current` + is still Penguin's boot context, before `init.sh` unshares a fresh namespace + for the real guest init. See [Analysis scoping](#analysis-scoping). +3. Hook subsystems: `syscalls_hc_init` → `signal_hc_init` → `ioctl_hc_init` → + `sock_hc_init` → `uname_hc_init`. +4. `igloo_portal_init()` — registers the Portal shared-memory region and enables + the interrupt path. +5. `igloo_procfs_compat_init()` — procfs compatibility hooks. +6. `block_mounts_init()`, `igloo_open_init()` — mount blocking and open + interception. +7. `hyperfs_init()` — brings up the host-backed pseudo-filesystem. +8. A final `IGLOO_INIT_MODULE` hypercall tells the host the guest is fully up. + +## Analysis scoping + +Penguin's own infrastructure — boot machinery, the backgrounded VPN / console / +guest-command helpers, and anything launched via `call_usermodehelper` — stays +in the kernel's *initial* UTS namespace. At handoff, `init.sh` unshares a fresh +UTS namespace for the real guest init, so the firmware-under-analysis process +subtree lives *outside* the initial namespace. + +`igloo_scope_init()` captures that initial namespace at module init. Event +emission is then gated on `igloo_in_scope(task)`, so analysis captures only the +firmware subtree and not Penguin's own helpers. Gating is **disabled by +default**: a driver paired with a Penguin that never enables scoping behaves +exactly as before and captures everything. Penguin toggles it at runtime via the +Portal `set_scope_enabled` operation. + +See {doc}`api/scope_api` for the extracted `scope.h` reference. diff --git a/docs/building.md b/docs/building.md new file mode 100644 index 0000000..13b7dd0 --- /dev/null +++ b/docs/building.md @@ -0,0 +1,112 @@ +# Building + +igloo_driver is cross-compiled for ~13 architectures against multiple kernel +versions, so the build runs inside a Docker container that carries the +cross-compilation toolchains. The wrapper is [`build.sh`](https://github.com/rehosting/igloo_driver/blob/main/build.sh); +the work inside the container is done by `_in_container_build.sh`. + +## Prerequisites + +- **Docker**, installed and running. +- **A toolchain image** — by default `rehosting/embedded-toolchains:latest`, + which carries the cross-compilers for every target. +- **Kernel headers** — an extracted `kernel-devel` tree per target/version. + `build.sh` looks for `local_packages/kernel-devel-all.tar.gz` and extracts it, + caching the result under `cache/`. These archives are produced by the kernel + build ([`linux_builder`](https://github.com/rehosting/linux_builder)); its + latest release publishes `kernel-devel-all.tar.gz`. + +```bash +mkdir -p local_packages +curl -L -o local_packages/kernel-devel-all.tar.gz \ + https://github.com/rehosting/linux_builder/releases/latest/download/kernel-devel-all.tar.gz +``` + +## Quick start + +Build every default target for the default kernel versions (`4.10 6.13`): + +```bash +./build.sh +``` + +The result is a single archive in the current directory: + +``` +igloo_driver.tar.gz +``` + +containing the built modules (`igloo.ko.`) organized by kernel version, +plus symbol information when available. + +## Options + +``` +./build.sh [--help] [--versions VERSIONS] [--targets TARGETS] + [--linux-builder PATH] [--kernel-devel-path PATH] + [--image IMAGE] [--release] +``` + +| Option | Meaning | +|---|---| +| `--versions "V1 V2 …"` | Kernel versions to build for. Default: `"4.10 6.13"`. | +| `--targets "T1 T2 …"` | Target architectures. Default: all (see below). | +| `--kernel-devel-path PATH` | Use an already-extracted `kernel-devel` tree instead of extracting the archive. | +| `--linux-builder PATH` | Path to a `linux_builder` checkout (source of kernel artifacts). | +| `--image IMAGE` | Toolchain Docker image. Default: `rehosting/embedded-toolchains:latest`. | +| `--release` | Strip modules after build to reduce size. | +| `--interactive` | Run the build container with `-it` (for debugging). | + +Default targets: + +``` +armel arm64 mipseb mipsel mips64eb mips64el +powerpc powerpcle powerpc64 powerpc64le +loongarch64 riscv64 x86_64 +``` + +## Examples + +```bash +# A couple of versions, a subset of architectures +./build.sh --versions "4.10 6.7" --targets "armel mipseb mipsel mips64eb" + +# One version, using a pre-extracted kernel-devel tree +./build.sh --versions 4.10 --kernel-devel-path /tmp/kernel-devel-x86_64-6.13 + +# Stripped (release) modules +./build.sh --release +``` + +## How it works + +`build.sh` prepares three bind mounts and invokes `_in_container_build.sh` +inside the toolchain image: + +- `/kernel-devel` — the extracted kernel headers (read-only), +- `/app` — this repository (so the container sees `src/` and the build script), +- `/output` — the build output directory (`cache/build`). + +The container compiles `src/` for each requested `target`/`version` pair and +assembles `igloo_driver.tar.gz`. + +```{note} +`build.sh` transparently rewrites bind-mount source paths when the environment +variables `PENGUIN_HOST_MOUNT_FROM` / `PENGUIN_HOST_MOUNT_TO` are set — this is +only relevant when a shared per-node Docker daemon cannot see the caller's +workspace directly (the same mechanism Penguin's wrapper uses). For local +builds and standard CI runners these are unset and behavior is unchanged. +``` + +## Using the module with Penguin + +The `igloo_driver.tar.gz` artifact is consumed by Penguin: drop it into +Penguin's `local_packages/` before a `./penguin --build` to test a local driver +build, or let Penguin download the pinned release. See Penguin's development +documentation for the local-package hand-off contract. + +## Continuous integration + +On pushes to `main` (and `dev_*` tags), CI builds all targets and publishes a +versioned GitHub release containing `igloo_driver.tar.gz`. Pull requests run the +same build as a check without releasing. diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..21cbcee --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,125 @@ +# Sphinx configuration for the igloo_driver documentation. +# +# Design notes: +# * Plain `sphinx-build docs/ _build/` builds the whole site — no Docker, no +# running kernel. This differs from Penguin (which builds its docs from a +# live runtime image to introspect Python plugins); igloo_driver is pure C +# with no runtime doc path, so a static build is correct and simpler. +# * The C API reference is auto-extracted from src/ by Doxygen and rendered +# through Breathe. Doxygen is invoked HERE, at conf import time, so a bare +# sphinx-build is self-contained (RTD-style pattern). +# * Theme/extension choices mirror Penguin's generated conf.py (furo, MyST, +# copybutton, sphinxemoji) so the two sites read as a family. + +import os +import subprocess +from pathlib import Path + +# -- Paths ------------------------------------------------------------------- +DOCS_DIR = Path(__file__).resolve().parent +REPO_ROOT = DOCS_DIR.parent +SRC_DIR = REPO_ROOT / "src" +DOXYGEN_OUTPUT = DOCS_DIR / "_doxygen" +DOXYGEN_XML = DOXYGEN_OUTPUT / "xml" + +# -- Project information ----------------------------------------------------- +project = "igloo_driver" +author = "The IGLOO / Penguin rehosting team" +copyright = "The IGLOO / Penguin rehosting team" + +# Version: prefer an explicitly injected value (CI passes the release tag), +# else leave unset — there is no in-tree version file to read. +release = os.environ.get("IGLOO_DRIVER_VERSION", "") +version = release + +# -- Run Doxygen ------------------------------------------------------------- +def run_doxygen(): + """Generate Doxygen XML from src/ so Breathe has something to read. + + Runs unless IGLOO_DOCS_SKIP_DOXYGEN is set (useful when iterating on prose + and the XML is already present).""" + if os.environ.get("IGLOO_DOCS_SKIP_DOXYGEN"): + return + env = dict(os.environ) + env["DOXYGEN_INPUT"] = str(SRC_DIR) + env["DOXYGEN_OUTPUT"] = str(DOXYGEN_OUTPUT) + DOXYGEN_OUTPUT.mkdir(parents=True, exist_ok=True) + try: + subprocess.run( + ["doxygen", str(DOCS_DIR / "Doxyfile")], + cwd=str(DOCS_DIR), + env=env, + check=True, + ) + except (FileNotFoundError, subprocess.CalledProcessError) as e: + # Don't hard-fail the whole build if Doxygen is missing locally; the + # Breathe directives will emit their own (visible) errors instead. + print(f"WARNING: Doxygen did not run cleanly: {e}") + + +run_doxygen() + +# -- General configuration --------------------------------------------------- +extensions = [ + "myst_parser", + "breathe", + "sphinx_copybutton", + "sphinxemoji.sphinxemoji", + "notfound.extension", +] + +templates_path = ["_templates"] +exclude_patterns = ["_build", "_doxygen", "Thumbs.db", ".DS_Store"] + +source_suffix = { + ".rst": "restructuredtext", + ".md": "markdown", +} +master_doc = "index" +language = "en" + +# -- Breathe ----------------------------------------------------------------- +breathe_projects = {"igloo_driver": str(DOXYGEN_XML)} +breathe_default_project = "igloo_driver" +breathe_domain_by_extension = {"h": "c", "c": "c"} +breathe_show_include = False + +# -- MyST -------------------------------------------------------------------- +myst_enable_extensions = [ + "colon_fence", + "deflist", + "fieldlist", + "attrs_inline", + "substitution", + "linkify", + "smartquotes", + "replacements", + "tasklist", + "html_image", +] +myst_heading_anchors = 3 + +# -- HTML output ------------------------------------------------------------- +html_theme = "furo" +html_static_path = ["_static"] +html_title = "igloo_driver docs" +html_theme_options = { + "sidebar_hide_name": False, + "navigation_with_keys": True, +} + +# -- LaTeX / PDF output ------------------------------------------------------ +latex_engine = "pdflatex" +latex_documents = [ + (master_doc, "igloo_driver.tex", "igloo_driver Documentation", author, "manual"), +] + +# -- Warning hygiene --------------------------------------------------------- +# Keep the build clean without turning off useful signal. These are the same +# categories of noise Penguin suppresses (MyST cross-ref heuristics, Pygments +# highlight failures on pseudo-code). +suppress_warnings = [ + "misc.highlighting_failure", + "myst.header", + "myst.xref_missing", +] diff --git a/docs/hooks.md b/docs/hooks.md new file mode 100644 index 0000000..ca441f5 --- /dev/null +++ b/docs/hooks.md @@ -0,0 +1,88 @@ +# Hooks + +Beyond reading and writing state, igloo_driver can install **runtime hooks** in +the guest kernel and report each hit back to the host. Hooks are registered and +torn down through Portal operations (see [Portal](portal.md#runtime-hooks)); the +kernel-side machinery lives in `src/hooks/` and `src/portal/`. + +Four kinds of hooks are supported: + +| Hook | Registration op | Event hypercall(s) | +|---|---|---| +| **Syscall** (entry / return) | `register_syscall_hook` | `IGLOO_HYP_SYSCALL_ENTER`, `IGLOO_HYP_SYSCALL_RETURN` | +| **Signal delivery** | `register_signal_hook` | `IGLOO_HYP_SIGNAL_DELIVER` | +| **kprobe** (entry / return / both) | `register_kprobe` | `IGLOO_HYP_KPROBE_ENTER`, `IGLOO_HYP_KPROBE_RETURN` | +| **uprobe** (entry / return / both) | `register_uprobe` | `IGLOO_HYP_UPROBE_ENTER`, `IGLOO_HYP_UPROBE_RETURN` | + +kprobe and uprobe direction is selected by `enum portal_type` +(`PORTAL_KPROBE_TYPE_ENTRY/RETURN/BOTH`, and the uprobe equivalents). + +## Syscall hooks + +Syscall hooks are the richest. A `struct syscall_hook` (in +[`src/hooks/syscalls_hc.h`](https://github.com/rehosting/igloo_driver/blob/main/src/hooks/syscalls_hc.h)) +can fire on entry, return, or every syscall (`on_all`), and supports extensive +in-guest **filtering** so that only interesting events cross the hypercall +boundary: + +- **By process** — a specific `pid`, or a process name (`comm`). +- **By argument value** — each argument can carry a `struct value_filter`. +- **By return value** — the return value has its own filter. + +The `value_filter` comparison types cover far more than equality: + +| Category | Types | +|---|---| +| Numeric | `EXACT`, `NOT_EQUAL`, `GREATER`, `GREATER_EQUAL`, `LESS`, `LESS_EQUAL`, `RANGE` | +| Return status | `SUCCESS` (≥ 0), `ERROR` (< 0) | +| Bitmask | `BITMASK_SET`, `BITMASK_CLEAR` | +| String | `STR_EXACT`, `STR_CONTAINS`, `STR_STARTSWITH`, `STR_ENDSWITH` | + +String filters read the argument out of user space carefully — a stack fast-path +for short strings, falling back to a bounded heap allocation up to `PATH_MAX`. + +Syscall names are **normalized** before matching, stripping arch- and +compat-specific prefixes (`sys_`, `_sys_`, `compat_sys_`, `arm64_sys_`, +`riscv_sys_`), so a single hook name matches across architectures. Hooks are +kept in an RCU-protected hash table keyed by normalized name. + +When a hook fires, the host receives a `struct syscall_event` carrying the +arguments, program counter, task, `pt_regs`, return value, and syscall name. The +host can set `skip_syscall` to suppress the real syscall — this is how Penguin +*intervenes* (e.g. faking a syscall result) rather than merely observing. + +### Scoping and interventions + +`syscall_hook.scope_filter_enabled` ties into [analysis +scoping](architecture.md#analysis-scoping): logging (read-only) hooks set it so +they only fire for the firmware-under-analysis subtree, skipping Penguin's own +infrastructure; intervention hooks leave it clear so they apply everywhere. + +### Portalcall fast path + +For very hot paths, a **portalcall fast path** avoids full Portal round trips. +`register_portalcall_magic` registers the magic value that identifies a portal +call, and `set_portalcall_fastpath` toggles the optimization +(`portalcall_fastpath_*` in `syscalls_hc.h`). + +## Signal hooks + +A `struct signal_hook` fires on signal delivery, optionally filtered by signal +number (0 = any), pid, or process name. The resulting `struct signal_event` +carries the signal number, target task and registers, PC, `comm`, and pid — and +a `drop` flag the host can set to **suppress delivery** of the signal. + +## ioctl, uname, mounts, and open + +`src/hooks/` also contains several targeted interception points used during +rehosting: + +- **ioctl** (`ioctl_hc.c`) — reports unhandled ioctls (`igloo_ioctl`) and + ENOENT/ENOTTY conditions so the host can model missing hardware. +- **uname** (`uname_hc.c`) — cooperates with `IGLOO_HYP_UNAME` to let the host + spoof `uname()` results. +- **block_mounts** (`block_mounts.c`) — suppresses mounts that would interfere + with rehosting. +- **igloo_open** (`igloo_open.c`) — open() interception, coordinating with the + `IGLOO_OPEN` hypercall and hyperfs. +- **sockets** (`sock_hc.c`) — socket-related hooks feeding the networking model. diff --git a/docs/hypercall_abi.md b/docs/hypercall_abi.md new file mode 100644 index 0000000..9095c9b --- /dev/null +++ b/docs/hypercall_abi.md @@ -0,0 +1,73 @@ +# The hypercall ABI + +Everything igloo_driver does ultimately rides on a single primitive: a +**hypercall** — one instruction that traps into the PANDA/QEMU emulator, which +recognizes it and hands control to the host analysis engine. Portal (the +shared-memory protocol) is built on top of this; the hypercall itself just moves +a small number of register-sized arguments across the guest/host boundary. + +## The calling convention + +The canonical entry point is `igloo_hypercall4()` in +[`src/ehypercall.h`](https://github.com/rehosting/igloo_driver/blob/main/src/ehypercall.h): + +```c +static inline unsigned long +igloo_hypercall4(unsigned long num, + unsigned long arg1, unsigned long arg2, + unsigned long arg3, unsigned long arg4); +``` + +- `num` is the **hypercall number** (see [Hypercall numbers](#hypercall-numbers)). +- `arg1..arg4` are up to four argument words. +- The return value comes back in the same register the host writes its result + into (the "input and output" register noted per-arch below). + +The instruction chosen per architecture is a **no-op or otherwise-harmless +instruction** that the emulator intercepts — on real hardware it either does +nothing observable or is a coprocessor/model-specific access the emulator claims. +This lets the same kernel run under emulation (where the hypercall is live) and, +in principle, on hardware (where it is inert). + +## Per-architecture instruction map + +| Arch (`CONFIG_*`) | Instruction | Number reg | Arg regs (1–4) | Return reg | +|---|---|---|---|---| +| `ARM64` | `msr S0_0_c5_c0_0, xzr` | `x8` | `x0 x1 x2 x3` | `x0` | +| `ARM` | `mcr p7, 0, r0, c0, c0, 0` | `r7` | `r0 r1 r2 r3` | `r0` | +| `MIPS` | `movz $0, $0, $0` | `v0` | `a0 a1 a2 a3` | `v0` | +| `X86_64` | `outl %eax, $0x88` | `rax` | `rdi rsi rdx r10` | `rax` | +| `I386` | `outl %eax, $0x88` | `eax` | `ebx ecx edx esi` | `eax` | +| `PPC` / `PPC64` | `xori 10, 10, 0` | `r0` | `r3 r4 r5 r6` | `r3` | +| `RISCV` | `xori x0, x0, 0` | `a7` | `a0 a1 a2 a3` | `a0` | +| `LOONGARCH` | `cpucfg $r0, $r0` | `a7` | `a0 a1 a2 a3` | `a0` | + +Architectures not in this list raise a compile-time `#error` — igloo_driver +only builds for targets it can signal from. + +```{note} +On x86 the hypercall is a port I/O to `0x88` — this is why the emulator watches +that port. On the register-file architectures the argument registers follow (or +deliberately diverge from) the platform syscall ABI; see the comments in +`ehypercall.h` for the per-arch rationale (e.g. x86-64 uses `r10` for the fourth +argument to match the syscall ABI). +``` + +## Hypercall numbers + +Hypercall numbers are enumerated in +[`src/igloo_hypercall_consts.h`](https://github.com/rehosting/igloo_driver/blob/main/src/igloo_hypercall_consts.h) +(`enum igloo_hypercall_constants`). They fall into a few families: + +| Family | Examples | Meaning | +|---|---|---| +| General | `IGLOO_OPEN` (100), `IGLOO_IOCTL_ENOTTY` (105) | Open interception, ioctl fallbacks. | +| Networking | `IGLOO_IPV4_SETUP`/`_BIND`/`_RELEASE` (200–205), IPv6 equivalents | Synthetic network bring-up and bind tracking. | +| Hypervisor | `IGLOO_HYP_UNAME` (300), `IGLOO_HYP_ENOENT` (305) | uname spoofing, ENOENT handling. | +| Task / VMA | `HC_TASK_CHANGE` (5900), `HC_VMA_UPDATE`/`IGLOO_HYP_VMA_*` (5910–5914), `IGLOO_HYP_TASK_PSTIME` | Task-switch and VMA-update reporting for host-side OSI. | +| Syscall / signal | `IGLOO_HYP_SYSCALL_ENTER` (0x1338), `IGLOO_HYP_SYSCALL_RETURN` (0x1339), `IGLOO_HYP_SIGNAL_DELIVER` (0x133b), `IGLOO_HYP_SETUP_TASK_COMM` (0x133a) | Syscall and signal hook event delivery. | +| Uprobe / kprobe | `IGLOO_HYP_UPROBE_ENTER/RETURN` (0x6901/0x6902), `IGLOO_HYP_KPROBE_ENTER/RETURN` (0x6903/0x6904) | Probe hit reporting. | +| Portal | `IGLOO_HYPER_REGISTER_MEM_REGION` (0xbebebebe), `IGLOO_HYPER_ENABLE_PORTAL_INTERRUPT` (0x7901), `IGLOO_HYPER_PORTAL_INTERRUPT` (0x7902), `IGLOO_HYP_TRAMP_HIT` (0x7903) | Portal region registration and the interrupt path. | +| Lifecycle / misc | `IGLOO_MODULE_BASE` (0x6408400C), `IGLOO_INIT_MODULE` (0x6408400D), `IGLOO_SYSCALL` (0x6408400B), `IGLOO_HYPERFS_MAGIC`, the `IGLOO_SIGSTOP_*` values | Module base reporting, init-complete signalling, hyperfs magic, kthread sigstop coordination. | + +See the full extracted enum in {doc}`api/hypercall_api`. diff --git a/docs/hyperfs.md b/docs/hyperfs.md new file mode 100644 index 0000000..678df81 --- /dev/null +++ b/docs/hyperfs.md @@ -0,0 +1,53 @@ +# hyperfs + +**hyperfs** (`src/hyperfs/`) is a pseudo-filesystem whose file contents are +served by the *host*. When guest code opens, reads, writes, or `ioctl`s a +hyperfs-backed file, the operation is forwarded over a hypercall to a host-side +model, which computes the answer and returns it. This is how Penguin makes a +device node or a `/proc` entry "exist" and behave sensibly even though no real +hardware or kernel subsystem is behind it. + +hyperfs is loaded last during module init and is declared as a soft dependency +(`MODULE_SOFTDEP("post: hyperfs")`) so it comes up after the rest of +igloo_driver. + +## Protocol + +hyperfs uses a compact operation set on top of the hypercall ABI, distinguished +by the `IGLOO_HYPERFS_MAGIC` value (`crc32("hyperfs")`): + +```c +enum hyperfs_ops { + HYP_FILE_OP, /* perform a file operation */ + HYP_GET_NUM_HYPERFILES, /* how many hyperfiles are registered? */ + HYP_GET_HYPERFILE_PATHS, /* enumerate their paths */ +}; + +enum hyperfs_file_ops { + HYP_READ, HYP_WRITE, HYP_IOCTL, HYP_GETATTR, +}; +``` + +- `HYP_FILE_OP` carries one of the four file operations (`HYP_READ`, + `HYP_WRITE`, `HYP_IOCTL`, `HYP_GETATTR`) to the host model. +- `HYP_GET_NUM_HYPERFILES` / `HYP_GET_HYPERFILE_PATHS` let the host enumerate the + registered hyperfiles (paths up to `HYPERFILE_PATH_MAX` = 1024 bytes). +- `HYP_RETRY` (`0xdeadbeef`) is a sentinel the host can return to ask the guest + to retry an operation. + +## Passthrough + +A hyperfs mount can be given a `passthrough_path=` option. Paths not backed by a +registered hyperfile fall through to the real underlying filesystem, so a +hyperfs overlay can synthesize a handful of files while leaving everything else +untouched. Internally hyperfs resolves and caches `vfs_read`, `vfs_write`, and +`vfs_ioctl` via `kallsyms` to perform the passthrough. + +## Relationship to the pseudo-file Portal ops + +hyperfs is the delivery mechanism behind most of the [synthesized pseudo-files +and devices](pseudofiles.md). The `hyperfs_add_hyperfile` Portal op registers a +new host-backed file; the higher-level `procfs_create_file`, +`sysfs_create_file`, `devfs_create_device`, `sysctl_create_file`, +`anonfs_create_file`, and MTD operations build on the same foundation to place +those files at the right spot in the guest's namespace. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..5c2ee02 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,73 @@ +# igloo_driver + +**igloo_driver** is the in-guest half of the [IGLOO / Penguin][penguin] firmware +rehosting stack: a small, built-in Linux kernel module that turns the emulated +guest kernel into a **programmable kernel debugger**. From the host (a PANDA / +QEMU analysis plugin) you can, at runtime, reach into the running guest and: + +- **Read and write kernel and user memory** — arbitrary addresses, strings, + pointer arrays, whole process address spaces. +- **Introspect the OS** — walk the process list, read a process's maps, argv, + environ, open file descriptors, register state, and executable path. +- **Install hooks** — kprobes, uprobes, syscall entry/return hooks, and signal + delivery hooks, all dispatched back to the host. +- **Call kernel functions** — a foreign-function interface (`ffi_exec`) plus + `kallsyms` lookup and generated trampolines. +- **Synthesize pseudo-files and devices** — create `/proc`, `/sys`, `/dev`, + `sysctl`, anonymous-inode, socket and MTD nodes on demand, backed by + host-side models via **hyperfs**. + +All of this is driven over a single cooperative, shared-memory protocol called +**Portal**, carried on top of a tiny per-architecture **hypercall** ABI. The +module has no configuration of its own — it is orchestrated entirely from the +host by Penguin. + +```{admonition} Where this fits +:class: tip +igloo_driver runs **inside the guest**. The host-side counterpart — the Python +API that issues these operations — lives in Penguin. If you are looking for the +*host* API (`plugins.portal.read_str(...)`, kprobe/uprobe registration from a +pyplugin, pseudo-file models), start with Penguin's +[Portal][penguin-portal], [kprobes][penguin-kprobes] and +[uprobes][penguin-uprobes] documentation. This site documents the guest-side +kernel module those APIs talk to. +``` + +## Start here + +```{toctree} +:maxdepth: 2 +:caption: Guide + +architecture +portal +hypercall_abi +hooks +hyperfs +pseudofiles +building +``` + +```{toctree} +:maxdepth: 2 +:caption: C API reference + +api/index +``` + +## At a glance + +| | | +|---|---| +| **What it is** | A built-in Linux kernel module (`igloo.ko`) | +| **Language** | C (kernel), plus a small trampoline codegen helper in Python | +| **Targets** | ~13 architectures (arm/arm64, mips/mips64 both endians, powerpc 32/64 both endians, x86_64, riscv64, loongarch64) | +| **Kernel versions** | Multiple; regularly built against 4.10 and 6.13 | +| **Host protocol** | Portal (shared-memory) over a per-arch hypercall instruction | +| **Configured by** | Penguin (the module itself is unconfigured) | +| **Build** | `./build.sh` (Docker cross-compile toolchain) → `igloo_driver.tar.gz` | + +[penguin]: https://github.com/rehosting/penguin +[penguin-portal]: https://github.com/rehosting/penguin/blob/main/docs/portal.md +[penguin-kprobes]: https://github.com/rehosting/penguin/blob/main/docs/kprobes.md +[penguin-uprobes]: https://github.com/rehosting/penguin/blob/main/docs/uprobes.md diff --git a/docs/portal.md b/docs/portal.md new file mode 100644 index 0000000..55b07cc --- /dev/null +++ b/docs/portal.md @@ -0,0 +1,205 @@ +# Portal + +**Portal** is the cooperative, shared-memory protocol between the guest kernel +(igloo_driver) and the host analysis engine (Penguin). It is what makes +igloo_driver a *programmable* kernel debugger: rather than the host reaching +blindly into guest physical memory, the guest kernel executes each request in +its own context — with full access to `current`, the page tables, the VFS, and +`kallsyms` — and hands back a well-formed result. + +The host-side API (the Python `plugins.portal.*` calls you write in a Penguin +pyplugin) is documented in Penguin's +[Portal guide](https://github.com/rehosting/penguin/blob/main/docs/portal.md). +This page documents the *guest* side: the protocol and the catalog of +operations the kernel module implements. + +## How Portal works + +### The shared region + +At module init, `igloo_portal_init()` registers a single page-sized region with +the host (`IGLOO_HYPER_REGISTER_MEM_REGION`) and enables an interrupt path +(`IGLOO_HYPER_ENABLE_PORTAL_INTERRUPT`). Every operation is carried in a +`portal_region` — a union of a small fixed header and a raw data buffer that +fills the rest of the page: + +```c +typedef struct { + uint32_t op; // operation type (HYPER_OP_* / HYPER_RESP_*) + uint32_t pid; // process ID, if the op targets a process (CURRENT_PID_NUM = current) + uint64_t addr; // address or primary parameter + uint64_t size; // size or secondary parameter +} region_header; + +typedef union { + region_header header; + uint8_t raw[PAGE_SIZE - sizeof(region_header)]; // data payload +} portal_region; +``` + +The usable payload is `CHUNK_SIZE = PAGE_SIZE - sizeof(region_header)`; larger +transfers are chunked across multiple round trips. + +### The dispatch loop + +`igloo_portal()` (in `src/portal/portal.c`) is the guest-side engine: + +1. Allocate a fresh region page and zero its header. +2. Issue `igloo_hypercall4(num, arg1, arg2, ®ion, region->header.op)` — this + traps to the host, which fills the region with the next operation to perform + (or a "no work" sentinel). +3. `handle_post_memregion()` looks up `region->header.op` in the handler table + and dispatches to `handle_op_()`. Each handler reads its parameters + from the header/payload, performs the work, and writes a response code + (`HYPER_RESP_*`) plus any result data back into the region. +4. Loop back to step 2 until the host signals there is no further work. + +The handler table is built directly from the op list via an X-macro, so adding +an operation is a matter of adding one line to `portal_op_list.h` and +implementing its `handle_op_()`: + +```c +static const portal_op_handler op_handlers[] = { + [HYPER_OP_NONE] = NULL, +#define X(lower, upper) [HYPER_OP_##upper] = handle_op_##lower, + PORTAL_OP_LIST +#undef X +}; +``` + +### The interrupt path + +Between explicit Portal calls, the host may need the guest's attention (for +example, to service a pending operation). igloo_driver exposes a lightweight +`portal_interrupt` flag; `check_portal_interrupt()` notices when it is set and +re-enters the Portal loop via `IGLOO_HYPER_PORTAL_INTERRUPT`. A +**fast path** (`set_portalcall_fastpath`, `register_portalcall_magic`) lets the +host reduce the per-call overhead for hot operations. + +## Operation catalog + +The full set of operations is declared in +[`src/portal/portal_op_list.h`](https://github.com/rehosting/igloo_driver/blob/main/src/portal/portal_op_list.h) +as `PORTAL_OP_LIST`. Each `X(lower, UPPER)` entry generates an enum value +`HYPER_OP_UPPER`, a handler prototype `handle_op_lower()`, and a table slot. +They group into the following families. + +### Memory access + +| Op | Purpose | +|---|---| +| `read` | Read raw bytes from a target address (kernel or, with `pid`, a process). | +| `write` | Write raw bytes to a target address. | +| `read_str` | Read a NUL-terminated string. | +| `read_ptr_array` | Read an array of pointers (e.g. `argv`/`envp` vectors). | +| `dump` | Bulk-dump a memory range. | +| `copy_buf_guest` | Copy a host-provided buffer into guest memory. | + +Implemented in `portal_mem.c` / `portal_dump.c`. The helper +`get_target_task_mm()` safely pins the target process's `mm` for cross-process +reads (see {doc}`api/portal_api`). + +### OS introspection (OSI) + +| Op | Purpose | +|---|---| +| `osi_proc` | Full process descriptor (`struct osi_proc`: pid, ppid, name, memory-layout bounds, times). | +| `osi_proc_all` | Bulk-walk **all** live processes in one operation (slimmer per-process node than `osi_proc`). | +| `osi_proc_handles` | Enumerate live processes as lightweight handles. | +| `osi_mappings` | The process's memory mappings (`struct osi_module` list). | +| `osi_proc_mem` | Access a process's memory via its OSI handle. | +| `osi_proc_exe` | The process's executable path. | +| `osi_proc_ptregs` | The process's saved `pt_regs`. | +| `read_procargs` | The process's `argv`. | +| `read_procenv` | The process's `environ`. | +| `read_fds` | The process's open file descriptors. | +| `read_time` | Read guest time. | + +Implemented in `portal_osi.c`. The OSI structs are defined in `portal_types.h` +and rendered in {doc}`api/types_api`. + +### Files + +| Op | Purpose | +|---|---| +| `read_file` | Read a file from the guest filesystem by path. | +| `write_file` | Write a file to the guest filesystem. | + +Implemented in `portal_fs.c`. + +### Execution & foreign-function interface + +| Op | Purpose | +|---|---| +| `exec` | Execute a program in the guest. | +| `ffi_exec` | Call an arbitrary kernel function with host-supplied arguments (FFI). | +| `kallsyms_lookup` | Resolve a symbol name to an address via `kallsyms`. | +| `tramp_generate` | Generate a per-architecture trampoline (see `scripts/gen_portal_tramp.py`). | + +Implemented in `portal_exec.c` / `portal_ffi.c` / `portal_tramp.c`. + +### Runtime hooks + +| Op | Purpose | +|---|---| +| `register_kprobe` / `unregister_kprobe` | Install/remove a kprobe; hits are reported to the host. | +| `register_uprobe` / `unregister_uprobe` | Install/remove a uprobe. | +| `register_syscall_hook` / `unregister_syscall_hook` | Hook syscall entry/return. | +| `register_signal_hook` / `unregister_signal_hook` | Hook signal delivery. | +| `register_portalcall_magic` | Register the magic value that identifies a portal call. | +| `set_portalcall_fastpath` | Enable/disable the fast dispatch path. | + +Implemented in `portal_kprobe.c` / `portal_uprobe.c` / `portal_syscall.c` and +the `src/hooks/` subsystem. Probe types are enumerated by `enum portal_type` +(entry / return / both, for both kprobes and uprobes). See [Hooks](hooks.md). + +### Synthesized pseudo-files & devices + +| Op | Purpose | +|---|---| +| `hyperfs_add_hyperfile` | Register a host-backed file with hyperfs. | +| `procfs_create_file` / `procfs_create_or_lookup_dir` | Synthesize `/proc` entries. | +| `sysfs_create_file` / `sysfs_create_or_lookup_dir` | Synthesize `/sys` entries. | +| `devfs_create_device` / `devfs_create_or_lookup_dir` | Synthesize `/dev` nodes. | +| `sysctl_create_file` | Synthesize a `sysctl` entry. | +| `anonfs_create_file` | Create an anonymous-inode-backed file. | +| `sockfs_create_socket` | Create a socket-backed pseudo-file. | +| `mtd_create` / `mtd_nuke` | Create / tear down synthetic MTD (flash) devices. | + +Implemented in `portal_procfs.c`, `portal_sysfs.c`, `portal_devfs.c`, +`portal_sysctl.c`, `portal_anon.c`, `portal_net.c`, `portal_mtd.c`, +`portal_hyperfs.c`. See [Pseudo-files & devices](pseudofiles.md) and +[hyperfs](hyperfs.md). + +### Synthetic network devices + +| Op | Purpose | +|---|---| +| `register_netdev` | Register a synthetic network device. | +| `lookup_netdev` | Look up a synthetic device by name. | +| `set_netdev_state` / `get_netdev_state` | Set / query device state. | + +Implemented in `src/netdevs/igloonet.c` and `portal_net.c`. + +### Analysis scoping + +| Op | Purpose | +|---|---| +| `set_scope_enabled` | Enable/disable event-emission gating to the firmware subtree. | + +Implemented in `portal/scope.c`. See +[Analysis scoping](architecture.md#analysis-scoping). + +## Response codes + +Every handler sets a response code in `region->header.op` before returning. +These are defined alongside the operations in `portal_types.h`: + +| Code | Meaning | +|---|---| +| `HYPER_RESP_READ_OK` | Read completed. | +| `HYPER_RESP_READ_PARTIAL` | Read completed partially (more data pending / truncated). | +| `HYPER_RESP_READ_FAIL` | Read failed. | +| `HYPER_RESP_READ_NUM` | Numeric result returned in the header. | +| `HYPER_RESP_WRITE_OK` | Write completed. | +| `HYPER_RESP_WRITE_FAIL` | Write failed / invalid operation. | diff --git a/docs/pseudofiles.md b/docs/pseudofiles.md new file mode 100644 index 0000000..fbe66ec --- /dev/null +++ b/docs/pseudofiles.md @@ -0,0 +1,47 @@ +# Pseudo-files & devices + +A large fraction of firmware rehosting is making the guest believe that hardware +and kernel interfaces it expects are present. igloo_driver can **synthesize** +entries across every major kernel namespace on demand, driven by Portal +operations from the host and backed by [hyperfs](hyperfs.md) models. + +## What can be synthesized + +| Namespace | Portal ops | Backed by | +|---|---|---| +| `/proc` | `procfs_create_file`, `procfs_create_or_lookup_dir` | `portal_procfs.c` | +| `/sys` | `sysfs_create_file`, `sysfs_create_or_lookup_dir` | `portal_sysfs.c` | +| `/dev` | `devfs_create_device`, `devfs_create_or_lookup_dir` | `portal_devfs.c` | +| `sysctl` | `sysctl_create_file` | `portal_sysctl.c` | +| anonymous inodes | `anonfs_create_file` | `portal_anon.c` | +| sockets | `sockfs_create_socket` | `portal_net.c` | +| MTD (flash) | `mtd_create`, `mtd_nuke` | `portal_mtd.c` | + +Each `*_create_file` / `*_create_device` op registers the node and wires its +read/write/ioctl operations back to a host model through hyperfs, so accessing +the file from guest userland produces host-controlled behavior. The +`*_create_or_lookup_dir` variants create intermediate directories idempotently, +so a deep path can be materialized one component at a time. + +## MTD devices + +MTD (Memory Technology Device) nodes model raw flash. `mtd_create` brings up a +synthetic MTD device — letting firmware that reads or writes flash partitions +run against a host-backed model — and `mtd_nuke` tears one down. This is how +NVRAM/flash-backed configuration can be emulated without real flash hardware. + +## Synthetic network devices + +Network interfaces are handled separately from files, in `src/netdevs/` +(`igloonet`), via `register_netdev`, `lookup_netdev`, `set_netdev_state`, and +`get_netdev_state`. Combined with the networking hypercalls +(`IGLOO_IPV4_SETUP`/`_BIND`/`_RELEASE` and IPv6 equivalents), this lets Penguin +present synthetic interfaces and track what the firmware binds to them. + +## Who drives this + +The guest module only implements the *mechanism*. Which pseudo-files exist, +what they contain, and how they respond is decided entirely by the host: Penguin +issues the `*_create_*` operations and answers the subsequent hyperfs reads and +writes. From a rehoster's point of view you configure these through Penguin's +pseudo-file / device models, not by editing the kernel module. diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..d0f462c --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,13 @@ +# Documentation build dependencies for igloo_driver. +# Used by the docs CI workflow and for local builds: +# python3 -m venv .venv && . .venv/bin/activate +# pip install -r docs/requirements.txt +# sphinx-build docs/ docs/_build/html # (needs `doxygen` on PATH too) +sphinx +myst-parser +furo +breathe +sphinx-copybutton +sphinxemoji +sphinx-notfound-page +linkify-it-py