> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apostate.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# How it works

> The profile, how a seed composes it, where the values come from, how each value is changed in Chromium's C++, and which setting wins.

A web page learns about the machine it runs on through hundreds of values: `navigator.hardwareConcurrency`, the WebGL renderer string, `screen.availHeight`, the fonts it can measure, the voices `speechSynthesis` lists, and the HTTP headers the browser sends. A detector reads many of them and checks that they agree with each other.

Apostate changes each of these values in Chromium's C++, at the place Chromium produces it. Every API, worker, iframe and header that reads the value then reads the new one, and Apostate adds nothing to the page that a script could find. The values come from one document, the profile, which the browser composes from a seed when it starts.

## The profile

The profile is a JSON document of the values a machine shows: platform, User-Agent, cores, memory, GPU strings and limits, WebGPU adapter, screen, fonts, voices, audio, media devices, network, battery and a few more. [Profile schema](/reference/profile-schema) lists every section and field. A section the profile leaves out stays the host's.

The browser process composes the profile before it starts any other process. Every child process, whether renderer, GPU process or network service, receives the same profile, base64-encoded, in its `--apostate-profile` switch. No child draws values of its own, so a value computed in two processes agrees. The `Device-Memory` header, which the browser process sends, and `navigator.deviceMemory`, which a renderer answers, read the same number.

Composition runs in `ChromeMainDelegate::PreSandboxStartup`, after the user data directory is known and before any child exists. On Linux that is before the zygote forks, so every renderer forked from it carries the profile. The browser writes nothing to disk apart from the [identity file](/concepts/seeds-and-identity#the-identity-file).

## Composing a machine

The catalogue says what a machine can be. It lives in three places in the repository:

| Path                                                                                                                        | Holds                                                                                |
| --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| [`resources/profiles/catalogue.json`](https://github.com/heretic-tech/apostate/blob/main/resources/profiles/catalogue.json) | The catalogue version, the GPU families, the order of the choices and the seed rules |
| [`resources/profiles/dispersion/`](https://github.com/heretic-tech/apostate/tree/main/resources/profiles/dispersion)        | One table per choice: real options with weights, keyed on earlier choices            |
| [`corpus/anchors/`](https://github.com/heretic-tech/apostate/tree/main/corpus/anchors)                                      | The measured GPU families                                                            |

`scripts/generate-dispersion-tables.py` compiles them into the browser during the build. The browser never reads them from disk.

A machine is a series of weighted choices, made in this order:

1. platform (the persona)
2. OS release
3. GPU family
4. GPU model
5. machine class, such as a desktop tower, a 15-inch laptop or a MacBook Pro with a Studio Display
6. CPU cores
7. memory
8. screen
9. taskbar, menu bar or panel
10. font packs
11. media devices
12. audio
13. network
14. battery
15. voices
16. extensions

Later choices are keyed on earlier ones. Cores and memory depend on the GPU model, and the screen, media devices and battery depend on the machine class. An Apple M1 therefore gets 8 cores, an M4 Max 14 or 16, and a MacBook Air never gets a Studio Display.

For each choice, the browser takes a SHA-256 hash of the seed, the persona and three fixed version epochs, then a second hash of that result with the choice's name, and picks an option from the weights with it. The draw uses no floating point and no modulo, so the same inputs pick the same option on every host. Before each draw, the browser removes the options the host cannot serve, such as more cores or memory than it has. [Seeds and identity](/concepts/seeds-and-identity) covers what that means for stability.

Language and timezone are not drawn. A drawn timezone could not match the IP address a site sees. The packages look up the proxy's exit and pass `--fingerprint-locale` and `--fingerprint-timezone`. See [Locale and timezone](/guides/locale-and-timezone).

`--fingerprint-explain` prints every choice a launch made, the layer that made it and the evidence behind the option, then exits. [Personas](/concepts/personas#read-a-persona) shows the output.

## GPU families

A GPU family is a set of WebGL and WebGPU values measured on real hardware: the WebGL limits, the extension list, shader precision, context attributes, and the WebGPU adapter's features and limits. A persona takes one family whole. Mixing values from two families would describe a device that does not exist.

| Family                         | Measured on                                               | Models a persona can name                 |
| ------------------------------ | --------------------------------------------------------- | ----------------------------------------- |
| Windows, NVIDIA, Direct3D 11   | RTX 3070 Ti, RTX A4500                                    | 45, GTX 10 series to RTX 50 series        |
| Windows, Intel, Direct3D 11    | UHD Graphics 630                                          | 12: UHD 620, 630, 730, 770 and Iris Xe    |
| Windows, Qualcomm, Direct3D 11 | Adreno X2-90                                              | 1: Adreno X2-90                           |
| macOS, Apple, Metal            | M4 Max                                                    | 12: M1 to M4, base, Pro and Max           |
| Linux, NVIDIA, Vulkan          | RTX 4070 Ti SUPER, RTX 4080 SUPER, RTX 3090, RTX PRO 4000 | 11: RTX 3090, RTX 40 series, RTX PRO 4000 |
| SwiftShader software renderer  | a Linux host with no GPU                                  | 1, only with `--fingerprint-anchor`       |

Within a family the WebGL values are identical for every model, so a persona can name any model of the family by its renderer string. The WebGPU adapter's architecture name follows the model's chip generation, and so does the smallest subgroup size on Intel's 12th-generation graphics. [GPU models](/reference/gpu-models) lists the models, and [GPU](/guides/gpu) covers choosing one.

The persona picks the family, not the host's GPU. A Windows persona takes a Direct3D 11 family on any host, including a Linux server with no GPU. The host's CPU family narrows the choice, because a Windows persona reports it. An x86 host gets the NVIDIA or Intel family, and an ARM host gets the Qualcomm family.

## Where the values come from

The project captured real machines with the collector page in [`capture/`](https://github.com/heretic-tech/apostate/tree/main/capture), in headed Chrome, and keeps the results in [`resources/fingerprints/raw/`](https://github.com/heretic-tech/apostate/tree/main/resources/fingerprints/raw):

* macOS on an Apple M4 Max
* Windows with an NVIDIA RTX 3070 Ti, an NVIDIA RTX A4500 and Intel UHD Graphics 630
* Windows 11 on ARM on a Snapdragon X2 Elite laptop with a Qualcomm Adreno X2-90
* Linux with an NVIDIA RTX 3060, RTX 3090, RTX 4070 Ti SUPER, RTX 4080 SUPER and RTX PRO 4000
* stock headless Chrome on Linux and macOS, for comparison

The GPU families, voices, audio buffer sizes, taskbar and menu bar sizes, and the macOS and Linux font lists come from these captures. The Windows core counts and screens are weighted from a large public collection of older Windows fingerprints. That collection is someone else's data, so it stays outside the repository and no build step reads it. The Windows font list is the set of families common to two public Windows 11 font collections. The remaining options, such as which memory sizes Apple sells a chip with, are written from vendor specifications. Each option's `evidence` field says which of these it is.

## A value changed at its source

Every change is a patch to the Chromium code that produces a value. Take the core count. A page reads it from `navigator.hardwareConcurrency`, but Chromium also sizes its thread pools from the same number, in `base::SysInfo::NumberOfProcessors()`. Changing only the JavaScript getter would leave a 32-thread browser claiming 8 cores, and a page that times parallel work in workers would see the difference.

Patch `0004` changes the function the thread pools and the getter both call:

```cpp theme={"system"}
int SysInfo::NumberOfProcessors() {
  if (const apostate::Profile* profile = apostate::Profile::Get()) {
    if (std::optional<int> claimed = profile->cpu_logical_cores()) {
      static int clamped = [claimed] {
        const int real = NumberOfProcessorsImpl();
        if (*claimed > real) {
          LOG(WARNING) << "apostate: profile claims " << *claimed
                       << " logical cores but the host has " << real
                       << "; clamping, since measured parallelism cannot "
                          "exceed the host";
          return real;
        }
        return *claimed;
      }();
      return clamped;
    }
  }
  // ... the upstream code that counts the host's processors
```

The other values follow the same rule:

* `navigator.platform` is set at the constant `NavigatorBase` returns, so workers report the same value as the window.
* The User-Agent Client Hints come from the one `UserAgentMetadata` struct that feeds every `Sec-CH-UA` header and `navigator.userAgentData`.
* The screen is set on the `ScreenInfo` the browser process sends to renderers, so `screen.*`, the CSS media queries and the pixel-ratio client hint agree, in top frames and in out-of-process iframes.
* One predicate hides fonts from canvas and CSS text measurement, `@font-face local()` and `queryLocalFonts()`. `document.fonts.check()` returns true for any family name, installed or not, so it does not read the font list.

Apostate injects no script, redefines no getter and sets no DevTools override. [`patches/series`](https://github.com/heretic-tech/apostate/blob/main/patches/series) lists the patches in the order they apply, and each patch file starts with a description of what it changes. [Patches](/contributing/patches) explains how to write one.

## Which setting wins

Strongest first:

1. `--apostate-profile` describing a device: your [profile](/guides/custom-profiles), nothing composed.
2. `--fingerprint=host`: the host's own values, nothing composed.
3. Per-field switches, such as `--fingerprint-hardware-concurrency`: one value each, on top of the seed's machine.
4. `--fingerprint=SEED`.
5. The seed in `DIR/apostate/identity`, when `--user-data-dir=DIR` is set.
6. A fresh seed from OS entropy.

`--fingerprint=host` refuses to launch together with a persona, a GPU family or a per-field switch. A written profile replaces the seed's machine, so the packages refuse `--fingerprint` in `args` next to a `profile`. [Switches](/reference/switches) lists every switch.

## Host mode

`--fingerprint=host` turns composition off. The browser builds no profile, and every value a persona would change is the host's. Use it to tell whether a problem comes from the persona or from the machine and network under it. [Personas](/concepts/personas#host-mode) has the details.
