# Своя сборка Camoufox: холст, привязанный к профилю

Здесь лежит `canvas-seed.patch` — единственная наша правка в чужом браузере.
Она берётся за беду, которую **нельзя** решить снаружи: без неё профили
второго уровня на одной машине отдают одинаковый холст там, где не участвуют
шрифты, и площадка связывает их одним запросом.

**Правка не готова к выпуску.** Она делает ровно то, что задумано, с холстом
без текста, но ломает холст с текстом. Подробности ниже, в разделе «Что
остаётся». В поставке продукта её нет.

## Зачем понадобилась своя сборка

Замерено до правки, три профиля с разными наборами шрифтов:

| что рисуем | prf_A | prf_B | prf_C |
|---|---|---|---|
| текст явным Arial | одинаково | одинаково | одинаково |
| текст общим стеком | одинаково | одинаково | одинаково |
| одна геометрия, без текста | одинаково | одинаково | одинаково |

Firefox умеет шуметь холстом сам (`nsRFPService::RandomizePixels`), но включает
это только под `resistFingerprinting`, а Camoufox его не использует: RFP
перебил бы её собственную подмену. Плюс ключ шума Firefox берёт из «банки кук»
и **меняет каждую сессию** — для приватного браузера это правильно, для
аккаунта губительно: один и тот же профиль отдавал бы новый холст при каждом
входе, а это признак сильнее, чем общий холст.

Из JavaScript это не чинится (см. `docs/competitors.md`): подменённый
`toDataURL` виден сравнением с `OffscreenCanvas` в фоновом потоке.

## Что делает патч

Три места, все — на языке C++ внутри дерева Firefox:

1. `dom/canvas/CanvasUtils.cpp`, `ImageExtractionResult` для обычного холста;
2. то же для `OffscreenCanvas` — иначе холст в фоновом потоке разойдётся с
   холстом в окне, и само расхождение станет приметой;
3. `toolkit/components/resistfingerprinting/nsRFPService.cpp`,
   `GenerateCanvasKeyFromImageData` — ключ шума берётся из нашего `canvas:seed`
   вместо сеансового ключа банки кук.

Порядок в первых двух местах важен: наша проверка стоит **до** веток Firefox.
Первая из них, `EfficientCanvasRandomization`, иначе выигрывает, а она шумит
только при кодировании картинки (`toDataURL`), не трогает `getImageData` и
берёт сеансовый ключ. Замерено: один профиль отдавал разный `toDataURL` при
каждом запуске, а `getImageData` был одинаков у всех профилей — обе половины
неверны.

Сам шум и то, что он смешан с хешем нарисованного (одна картинка всегда даёт
один результат), оставлены как у Mozilla. Мы меняем только ключ и условие
включения.

## Результат после сборки

Замерено на собранном браузере скриптом `check_canvas.py`, `canvas:seed`
выведен из идентификатора профиля. Мерились ДВА холста, и ведут они себя
по-разному — общий ответ скрыл бы это.

**Геометрия** (три круга, ни одной буквы) — ровно наш шум:

```
профили различаются:            да
одна на все сайты и загрузки:   да   (127.0.0.1 и localhost, по 4 загрузки)
та же после перезапуска:        да
окно и фоновый поток совпали:   да
```

Это и есть то, ради чего правка писалась: дыра была именно здесь — до неё
геометрический холст был побайтово одинаков у всех профилей.

**Текст** — то, чем площадки пользуются на самом деле:

```
prf_A: 8 загрузок одного сайта → 2 разных значения
prf_B: 8 загрузок одного сайта → 2 разных значения
```

## Что остаётся

**Текстовый холст принимает два значения вместо одного.** Это надо чинить до
выпуска: настоящий браузер на одном сайте отдаёт один холст, а два значения —
примета сама по себе.

Полная картина по восьми загрузкам одного сайта, чтобы не приписывать правке
чужого и не снимать с неё своего:

| | холст с текстом | холст без текста |
|---|---|---|
| без `canvas:seed` (как в поставке) | 1 значение | **2 значения** |
| с `canvas:seed` (наша правка) | **2 значения** | 1 значение |

То есть неустойчив сам браузер, и в обоих случаях — просто в разных местах.
Наша правка чинит холст без текста и ломает холст с текстом. Считать это
готовым нельзя ни в какую сторону.

Что уже проверено и **не** является причиной:

* не гонка загрузки шрифтов: замер ждёт `document.fonts.load('14px "Arial"')`,
  и без посева те же загрузки стабильны;
* не выравнивание строк поверхности: холсты шириной 296, 300, 304, 320 и 384
  (строка от 1184 до 1536 байт, часть кратна 32, часть нет) дают одинаково
  два значения;
* не разные сайты: два значения появляются при перезагрузке ОДНОГО сайта;
* не наш ключ: он выводится из посева одной и той же арифметикой, а дальше
  `RandomizeElements` детерминирован — в его же комментарии Mozilla пишет, что
  результат обязан совпадать для одного холста в одном сеансе.

Значит на вход шума в разных загрузках попадает разная картинка, хотя без шума
она одинакова. Где именно она расходится — не установлено; выдумывать причину
вместо замера здесь нельзя, на этом уже дважды терялось время.

## Как собрать

Нужна машина с 8 ГБ памяти (лучше больше), ~40 ГБ диска и терпение: полная
сборка идёт около полутора часов на четырёх ядрах.

```bash
git clone https://github.com/daijro/camoufox && cd camoufox
make setup-minimal && make mozbootstrap && make dir   # исходники + их патчи
patch -p1 -d camoufox-*/ < путь/к/canvas-seed.patch
```

Патч трогает только два файла с кодом. Ещё нужна одна строка сборки — её
добавляют отдельно, потому что контекст в `moz.build` меняется от версии к
версии, и куском патча она перестаёт ложиться:

```bash
grep -q camoucfg camoufox-*/toolkit/components/resistfingerprinting/moz.build || \
  echo 'LOCAL_INCLUDES += ["/camoucfg"]' >> \
  camoufox-*/toolkit/components/resistfingerprinting/moz.build
```

В `dom/canvas/moz.build` эта строка у свежих версий Camoufox уже есть — её
добавлять не нужно, и попытка сделать это патчем ломает наложение целиком.

В `camoufox-*/mozconfig` дописать строку — без неё сборка падает на шаге
`gkrust` по нехватке памяти (ядро убивает процесс, в журнале SIGKILL без
объяснения):

```
ac_add_options --disable-debug-symbols
```

Дальше:

```bash
cd camoufox-*/ && ./mach build -j4
```

**Под Windows и macOS собирают с Linux**, их же средством — исходники Firefox
кросс-компилируются:

```bash
python3 multibuild.py --target windows --arch x86_64
make setup-macos-sdk && python3 multibuild.py --target macos --arch x86_64
```

Готовый браузер — в `obj-*/dist/bin/camoufox`. Рядом обязаны лежать
`properties.json`, `camoufox.cfg` и каталог `fonts`: когда путь к браузеру
задан явно, Camoufox ищет описание своих настроек именно рядом с бинарём.

Если памяти меньше 8 ГБ, добавьте подкачку — иначе шаг с оптимизацией на всю
программу не влезает:

```bash
fallocate -l 8G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
```

## Готовая сборка

Собранный браузер сохранён, чтобы не тратить полтора часа заново:

```
$MANTIA_HOME/browsers/camoufox-linux-mantia.tar.gz   (87 МБ)
```

Внутри каталог `bin` целиком — бинарь, `properties.json`, `camoufox.cfg` и
шрифты. Разворачивается куда угодно, запускается через `executable_path`.
Только для Linux x86-64.

## Чего здесь нет

Собранного браузера под Windows и macOS. Патч проверен на сборке для Linux;
на боевых системах продукт пока работает со сборкой Camoufox из их поставки,
то есть **без** этой правки. Пока это так, продукт обязан сообщать в карте
покрытия, что холст второго уровня общий, — и сообщает (`identity/coverage.py`,
состояние `DEGRADED`).
