VFLIN

docs · docs/30-core/FINGERPRINT_CONFIG_CONTRACT.md · перевод от 2026-11-07 · с английской ревизии 19d0416f4d3e

Настройка отпечатка

проверяемый каталог идентичностей и его правила согласованности

Английский оригинал изменился после этого перевода (сейчас ревизия 52a5f0fdbdeb), и перевод может отставать. Актуальный текст — на английской странице →

Назначение: определить отпечатки как структурированный проверяемый каталог с правилами согласованности, а не как случайный JSON. Этот контракт реализуем уже сейчас (схема + валидация), даже если глубокое соблюдение в движке ждёт патченого ядра. Одна и та же конфигурация ведёт каждого провайдера.

1. Почему сначала контракт

Обнаружение ведёт несогласованность между сигналами, а не отдельные «плохие» значения. Поэтому задача конфигурации не столько «выбрать случайные значения», сколько «гарантировать согласованную личность устройства». Каталог — это мозг (что показывать); Core Provider — руки (как).

2. Схема конфигурации (v0)

{
  "id": "uuid",
  "label": "win11-chrome-148-desktop",
  "os":        { "family": "windows", "version": "11", "arch": "x86_64" },
  "browser":   { "engine": "chromium", "version": "148.0.x", "uaCh": { /* derived, see §4 */ } },
  "hardware":  { "cores": 8, "memoryGb": 16, "gpu": { "vendor": "...", "renderer": "..." } },
  "screen":    { "width": 1920, "height": 1080, "dpr": 1.0, "colorDepth": 24,
                 "available": { "left": 0, "top": 0, "width": 1920, "height": 1032 } },
  "locale":    { "language": "en-US", "languages": ["en-US","en"], "timezone": "Europe/Berlin",
                 "uiLanguage": "en-US" },               // OPTIONAL (B191/A235): the browser interface a person chose — §2x
  "fonts":     { "set": "win11-default", "additions": [], "removals": [] },
  "media":     { "audioContext": "coherent-noise",       // read by patch 0027 (A67: expressible since 2026-08-31)
                 "audioFft": "legacy",                // OPTIONAL, read by patch 0049 (B94); absent = the engine's own FFT — §2d
                 "devices": "win11-typical" },       // a preset NAME (the catalog's); the core reads the resolved OBJECT — §2c for why
  "webrtc":    { "policy": "proxy-only", "publicIp": "from-proxy", "localIps": "masked" },
  "canvas":    { "mode": "stable-noise", "seed": "per-profile" },
  "rect":      { "mode": "noise" },                  // OPTIONAL, read by patch 0057 (B102); absent = stock DOMRect — §2f
  "identity":  { "passkeys": "profile" },            // OPTIONAL, read by patch 0059 (B101); absent = stock WebAuthn (Windows) — §2g
  "privacy":   { "doNotTrack": true },               // OPTIONAL, read by patch 0060 (B104); absent = stock DNT pref — §2i
  "webgl":     { "vendorMatchesGpu": true, "noise": "stable" },
  "network":   { "proxyRef": "uuid", "quic": true,
                 "tls":   { "ja3": "derived", "ja4": "derived" },           // match claimed browser
                 "http2": { "settings": "derived", "akamaiFp": "derived" } },
  "provenance":{ "source": "catalog|generated|imported", "realDeviceRef?": "..." }
}

2a. screen.available — доступный прямоугольник

screen.avail{Left,Top,Width,Height}, разрешаются каталогом и несутся целиком. Пути через точку для того, кто читает JSON: screen.available.left · .top · .width · .height.

Все четыре или ни одного. Это одно поле-объект, а не четыре ключа, чтобы наполовину заполненный прямоугольник нельзя было записать; отсутствие означает «не несётся» — конфигурация, отчеканенная до появления этого поля, — и потребитель обязан вернуться к собственному выводу, а не читать отсутствие как ноль.

Откуда берутся числа. validate::SCREEN_INSETS в vflin-fingerprint держит поля, которые занимает оформление ОС, как отступы по краям с происхождением у каждой строки (Measured / Assumed, та же дисциплина, что у SCREEN_MODES), а validate::available_rect разрешает их относительно дисплея. Windows резервирует СНИЗУ (48 CSS-пикселей на 11 — измерено на настоящем хосте, 40 на 10); macOS резервирует 25 СВЕРХУ (строка меню; Dock моделируется как автоскрывающийся); Linux — 27 сверху. При инверсии хоста (ADR-0031) РАЗМЕР берётся из собственных availWidth/availHeight хоста, когда заявляется его же дисплей; НАЧАЛО координат по-прежнему из каталога, потому что снимок не читает availLeft/availTop.

Почему это прямоугольник, а не разница. Одно «зарезервировать N px» не может сказать, у какого края эти N px. Патч 0011 держал ровно это — четыре литерала в chrome/browser/vflin/fp_config.cc (AvailHeightDeltaForOsFamily), вычитаемые из screen.height с началом координат, принудительно равным (0, 0), — так что смоделированный Mac резервировал свою строку меню и сообщал о ней ВНИЗУ экрана. Зашитые значения личности — это то, что запрещает инвариант 5; вот где они теперь живут.

2b. media.audioContext и canvas.mode — одно поле, одна поверхность

Положение дел, исправленное A67 (2026-08-31). media.audioContext теперь выразим из продукта: модель в crates/vflin-fingerprint несёт необязательный объект media, а set_audio_noise / audio_noise_on его переключают, повторяя пару для canvas. До этого поле было объявлено здесь и не имело поля в Rust вообще, так что до патча 0027 — отгруженного и работающего — можно было дотянуться только написанным вручную JSON, и именно так две сессии доказали патч, пока разрыв оставался невидимым. Отсутствие означает «выключено», и по умолчанию в продукте выключено (указание владельца: доступно, но не включено по умолчанию).

Перекрыто B96 (патч 0051): media.devices теперь ЧИТАЕТСЯ — как объект, см. §2c. Абзац ниже — состояние до этого. media.devices по-прежнему не читает ничто, и §2c теперь говорит, чего стоит это закрыть. A67 типизировал сторону модели и оставил смысл пресета срезу поверхности; B59 измерил эту поверхность и обнаружил, что механизм нельзя построить на одном лишь слое перечисления. Поле остаётся объявленным и непрочитанным — это честно, в отличие от наполовину реализованного.

Каждое из этих двух полей включает ровно одну поверхность, и ни одно не решает за другое. До патча 0027 (2026-08-30) эта фраза была ложью, и её стоит произнести из-за того, как именно она провалилась.

0022 (audio-noise) отгрузился завязанным на canvas.mode == "stable-noise", так что переключатель с именем canvas включал заодно и возмущение Web Audio, — а media.audioContext, объявленный в схеме выше и названный ключом конфигурации 0022 в плане серии, не читала ни одна ветвь парсера. Задокументированный орган управления не делал ничего; работу делал незадокументированный. ИЗМЕРЕНО K10 вне CreepJS голым OfflineAudioContext: отсчёт 4500 дал -0.14573481678962708 при выключенном переключателе canvas — сток до последней цифры — и -0.14574481546878815 при включённом, из конфигурации, не несущей аудиополя. CreepJS выставил это отдельным счётом как AudioBuffer: "audio is fake".

С 0027 шум аудио включается media.audioContext: "coherent-noise" и больше ничем. Доказано живьём (B55): при canvas.mode: "stable-noise" и без аудиополя отсчёт равен -0.14573481678962708, идентично стоку; при заданном media.audioContext и canvas.mode: off он сдвигается в -0.14572481811046600.

Семя по-прежнему общее, и это намеренно. Возмущение аудио ключуется на canvas.seed, свёрнутом в непересекающейся области "audio", так что две функции никогда не делят ключ: две функции на одном ключе были бы межфункциональной корреляцией, которой воспользовался бы детектор. Остаточное: поэтому аудио нельзя включить без присутствующего canvas.seed, который можно свернуть. Дать аудио собственное семя — это изменение схемы, и оно не сделано.

Всё ещё объявлено и всё ещё не читается: media.devices. B59 попытался и отозвал патч; в §2c записано, что он измерил и чего на самом деле требует закрытие этой поверхности. (До B96: патч 0051 его читает — первый блок §2c.)

2c. media.devices — измерено, испробовано, снято и ЧИТАЕТСЯ патчем 0051 (B96)

Читается с B96 (патч 0051 media-devices), в том месте, которое назвал этот раздел. Проекция живёт в процессе браузера, в MediaDevicesManager::DevicesEnumerated — единственном входе в список, который читают enumerateDevices, выбор устройства у getUserMedia, selectAudioOutput/setSinkId и devicechange, — так что перечисление и захват движутся вместе, чего две формы B59 ниже сделать не могли. Движок читает разрешённый ОБЪЕКТ, никогда не имя пресета:

Поправка, B143 (ISSUE-532, кадр владельца из BrowserScan: «Use available cameras (1)», OBS Virtual Camera, логотип OBS в предпросмотре). Здесь говорилось, что этот список читают и собственные окна выбора Chrome. Запрос разрешения его НЕ читает — как и его предпросмотр камеры и микрофона, списки устройств в сведениях о странице и в настройках: они читают media_effects::MediaDeviceInfo, кэш, который напрямую наполняют службы видеозахвата и звука. Поэтому запрос называл камеру хоста, пока страница перечисляла камеру личности (измерено: core/scripts/camera-prompt-probe.mjs, поддельные устройства Chrome — запрос fake_device_0, страница HD Pro Webcam C920 (046d:082d)). Патч 0075 прогоняет ту же проекцию на этом кэше, построенную так же, как строит свой список MediaDevicesManager, поэтому обе дороги показывают одни и те же устройства в одном порядке под одними именами; выбор запроса (Chrome ранжирует устройства по имени) тогда ранжирует и список страницы. После него проба читает камеру личности на обеих дорогах (--gate PASS; FAIL на сборке до него). Что камера ПОКАЗЫВАЕТ — дело §2k: без выбранного файла это настоящая камера хоста, а на машине, где камера — OBS, собственная картинка OBS.

B144, на собственном классе устройства владельца, а не на подделке. Единственная камера этой машины — OBS Virtual Camera (DirectShow; другой камеры нет, микрофона нет), поэтому camera-prompt-probe.mjs --host-devices воспроизводит сам кадр. До (M155-official): запрос «Use available cameras (1)» → OBS Virtual Camera, страница HD Pro Webcam C920 (046d:082d) — --gate FAIL. После (0075): и запрос, и страница — HD Pro Webcam C920, OBS нет ни на одной дороге. С файлом камеры в профиле (--camera-file, media.video.source патча 0067) ПРЕДПРОСМОТР запроса играет файл, дорожка страницы названа HD Pro Webcam C920 и работает, enumerateDevices() после разрешения даёт то же имя — --gate PASS на обеих дорогах (окно снято --shot). Без файла имя — профиля, а картинка — какую даст камера хоста: здесь OBS не был запущен и устройство не открывалось (NotReadableError и с личностью, и без неё); на машине, где OBS играет, — его картинка.

"media": { "devices": { "audioinput": 1, "videoinput": 0, "audiooutput": 2,          // caps on the host's own devices
                        "labels": { "audioinput": ["…"], "audiooutput": ["…", "…"] } } }  // optional, from real captures
  • Число ограничивает устройства хоста этого вида; 0 скрывает вид (getUserMedia отвечает NotFoundError, как на машине без него) — кроме аудиовыходов, где 0 читается как 1: воспроизведение в выход по умолчанию разрешается вне этого списка (AudioOutputAuthorizationHandler), так что пустой список выходов соседствовал с успешным setSinkId('default') (Codex 0051-2, воспроизведено gated-тестом). Число вне 0..16 или не целое — список хоста.

  • Число больше хостового было хостовым до патча 0086 (B153, ISSUE-543) — устройство не выдумывалось, раз с него нечего захватить. Для МИКРОФОНОВ этой причины больше нет: 0084 рисует звук засеянного профиля в службе звука для любого не-петлевого идентификатора устройства, которое открыли, поэтому микрофоны, которые профиль заявляет, а машина не имеет, теперь ДОБАВЛЯЮТСЯ в список — с идентификаторами и группами в форме Windows, нарисованными из семени профиля, и с виртуальными записями, которые всегда перечисляет машина с микрофонами. Измерено до него (K79, воспроизводится core/scripts/media-devices-probe.mjs): профиль, заявляющий ТРИ микрофона, показывал странице НИ ОДНОГО — вида audioinput нет в enumerateDevices() ни до разрешения, ни после, — тогда как камера того же профиля читалась «HD Pro Webcam C920», а колонки «Speakers (Realtek(R) Audio)». На машине С микрофонами страница прочла бы машинные. После него: все три перечислены под собственными именами профиля, открытая дорожка читается «Default - Microphone Array (Realtek(R) Audio)» и несёт нарисованную комнату (RMS 0.0019, пол 58 % от среднего, две записи совпадают на 0.0 %); с хостом, у которого микрофонов СТОЛЬКО ЖЕ, сколько заявлено, ничего не выдумывается, а устройства хоста сохраняются и переименовываются ровно как раньше. По-прежнему никогда не выдумываются: audiooutput (за ним нет нарисованного воспроизведения) и videoinput (служба захвата держит собственный список устройств — другой шов), и вообще ничего без canvas.seed, потому что устройство, которое страница видит и не может открыть, хуже устройства, которого она не видела. Измеренное следствие, которое стоит знать: собственный GuessVideoGroupID Chromium сопоставляет камеру со звуковым входом по имени, поэтому, как только в каталоге появляется «Microphone (HD Pro Webcam C920)», камера «HD Pro Webcam C920 (046d:082d)» входит в её группу — одна физическая веб-камера с микрофоном, как и читается настоящая.

  • Виртуальные записи Windows default / communications держат устройство, которое называют — найденное по ИМЕНИ (их метка — Default - <that device's name>), в своей группе, и только по группе — когда имя не совпало, — так что Default - X никогда не называет скрытое X, даже если из-за этого остаётся на одно устройство больше числа (Codex 0051-1: два микрофона могут делить группу, и «первое в группе» оставляло не то).

  • Метки, когда даны, переименовывают оставленные устройства по порядку, и виртуальные записи следуют за ними (Default - <new name>); список с любым пустым, длиннее 128 байт или с управляющим символом именем отбрасывается целиком. Без меток остаются имена хоста (после разрешения, как у стока) — бриф говорил «иначе пустые»; пустая метка после выданного разрешения — то, чего не показывает ни один Chrome, и ровно тот список, который хэширует BrowserScan, так что это записано как решение владельца, одна строка на смену. Всё ещё за владельцем (ISSUE-297 (б), A120): продукт меток не пишет (A119), поэтому имена машины — то, что показывает каждый профиль после разрешения, пока он не решит.

  • Идентификаторы устройств не трогаются: Chrome и так хэширует их солью профиля и origin’ом (измерено B96: два профиля на одном origin читают разные id, один профиль читает одно и то же дважды).

  • ИМЯ пресета движком не читается. "win11-typical" остаётся термином каталога; что оно значит в устройствах, каталогу решать по захватам (ADR-0035). Модель несёт обе формы дословно (Media.devices в vflin-fingerprint — JSON-значение с B96, round-trip проверен).

  • Что пишет продукт (A119, ISSUE-297): 0 для каждого из audioinput / videoinput, которого нет в самом новом ОКОННОМ захвате семейства ОС личности (в captures/) — захват идёт до разрешения, так что доказывает, какие виды у машины ЕСТЬ, никогда — сколько их и как зовутся, — и больше ничего: ни audiooutput: 0, ни меток; нет такого захвата — нет поля. Валидатор (A7_MEDIA_DEVICES_*) отказывает форме, которую движок проигнорировал бы или прочёл иначе, и предупреждает об имени пресета.

  • Ручной список (A124, ISSUE-336; нижняя граница 0 с A127, ISSUE-351 — владелец: «не у всех есть все три»): число по каждому виду, 0..3, и по одному имени на каждое оставленное устройство из каталога (fingerprint.mediaDevices) — список имён обязан совпадать с числом (A7_MEDIA_DEVICES_LABELS_COUNT), так что вид с 0 не называет никого. Микрофон и камера опускаются до 0 (вид скрыт), И ВЫХОД ТОЖЕ — с B119/A149 (2026-10-11): кламп, читавший audiooutput: 0 как один, снят из патча 0051, а вместе с ним и отказ демона (A7_MEDIA_DEVICES_OUTPUT_ZERO). Причина, по которой кламп существовал — воспроизведение на выход по умолчанию разрешено вне списка, поэтому разрешающийся setSinkId('default') рядом с пустым списком выглядел противоречием, — ЗАМЕРЕНА на машине, приведённой к состоянию без единого устройства вывода: вида нет вовсе, а setSinkId('default') всё равно разрешается, то есть эта пара и есть ответ такой машины. Чтение каталога называет нижнюю границу каждого вида в minByKind (теперь {audioinput: 0, videoinput: 0, audiooutput: 0}); его manualMin — граница, безопасная для всех видов, — для экрана, который применяет одну границу ко всем трём.

  • Нарисованное число (A141, ISSUE-417/351 — владелец: «чаще 1, иногда 2, редко 3+, иногда 0»): НОВАЯ личность (fingerprint.generate, умолчание генератора и пресет при profile.create) несёт media.devices, нарисованные из её собственного семени — то же семя, тот же розыгрыш — по media_devices::COUNT_SHARES, имена различны и из каталога для её ОС (вид с 0 не называет никого):

    вид 0 1 2 3
    audioinput (микрофон) 5 % 70 % 20 % 5 %
    videoinput (камера) 30 % 58 % 10 % 2 %
    audiooutput (колонки) 2 % 68 % 25 % 5 %

    Три правила поверх таблицы (B181/B182, ISSUE-756, 2026-10-31). (1) Веб-камера приносит свой микрофон: нарисованная камера значит хотя бы один микрофон; БЕЗ камеры микрофон есть с вероятностью media_devices::MIC_WITHOUT_A_CAMERA_PCT (50 %) — тогда его число даёт строка микрофона выше, а 0 читается как 1. (2) Сначала собственные устройства машины (ALWAYS_OWN: у MacBook/iMac все три, у ноутбука на Windows или Linux — его микрофон и колонки; демон отказывается их выключать) и устройства по умолчанию (OWN_BY_DEFAULT: встроенная камера ноутбука на Windows — рисуется всегда, убирается руками; владелец: «для ноутбуков… обязательно ставим камеру» и «можно убрать»). (3) 30 % камеры на 0 — это «шанс, что нету веб-камеры… большой шанс, но не всем» владельца (его «образно говоря, 60 %» как нижняя граница). Замерено на 4 000 розыгрышей на класс: у настольного камера в 70 %, микрофон в 85 %, все три в 69 %, ни камеры, ни микрофона в 15 %, камера без микрофона — 0; у ноутбука на Windows все три в 100 %; у ноутбука на Linux его микрофон и колонки в 100 %, камера в 71 % (своего имени в каталоге для неё пока нет). A191 демона по-прежнему заявляет камеру у каждого нарисованного профиля, пока control-plane не сузит это до машин со своей камерой, — тогда новый профиль понесёт 70 % настольного.

    Эти доли — ПРОДУКТОВОЕ решение словами владельца, а не измерение: ни одна публичная таблица не считает устройства по машинам. Колонки рисуют 0 в 2 % с A149 (движок сохраняет 0 с B119, и setSinkId('default') на такой машине всё равно разрешается). Правда измеренной машины сильнее розыгрыша: вид, которого НЕТ в самом новом оконном захвате семейства ОС, остаётся 0 без имён (правило A119 — случайное число никогда не выдумывает устройства, которого у той машины не было), а число больше хостового — хостовое, как и любое другое. Конфиг, принесённый целиком (инлайном или личностью с этой машины), сохраняет что сказано. Тесты: a_drawn_device_count_follows_the_shares (50 000 розыгрышей в пределах 2 пунктов от каждой доли), every_generated_identity_carries_valid_devices_named_for_its_os и демонский a_new_identity_draws_its_devices_and_a_kind_its_machine_lacks_stays_zero. A183 (ISSUE-511): СОБСТВЕННОЕ устройство машины идёт первым, а виды, которые у неё есть всегда, есть всегда (media_devices::ALWAYS_OWN). MacBook и iMac первыми называют собственные колонки, микрофон и камеру (одна модель на все три), ноутбук Windows — первыми встроенные колонки и микрофонную решётку, а встроенную камеру — первой всякий раз, когда камеру вообще называет; такой вид никогда не рисуется в 0, подключённые устройства добавляются после него. K68 читал личность MacBook с собственными колонками в 38 % случаев, а с микрофоном — в 27 %: B139 сделал встроенное одним из вариантов среди подключённых; теперь 100 % / 100 % (гейт a_machine_names_its_own_devices_first_and_always). Настольная машина, которая не iMac, по-прежнему не называет встроенного устройства: у собственного динамика Mac mini строки пока нет (ISSUE-511).

  • Измерено (B96): без конфига — список хоста ровно как у стока 153; {videoinput: 0, audiooutput: 1} → до разрешения только audiooutput, после него default, communications и выход, который они называют; gated-тест сьюта crates/vflin-consistency/tests/media_devices_projection.rs (поддельные устройства Chrome — скрытая камера отвечает NotFoundError, заявленное имя — метка захваченной дорожки) ЗЕЛЁНЫЙ на ядре, КРАСНЫЙ на стоке; CreepJS devices (2): audio, webcam → devices (1): audio.

  • Всё ещё открыто: вид, которого у хоста НЕТ, нельзя заявить в существование — для этого нужен синтетический источник захвата и происхождение имён устройств (ISSUE-317).

Что было до B96 — оставлено для истории.

B59 построил механизм, измерил, что он работает, и отозвал его. Этот раздел существует, чтобы следующая попытка начиналась с того, чего это стоило, а не со строки в схеме.

Что эта поверхность есть на самом деле. До разрешения Chrome показывает не больше одной записи на вид, с пустыми deviceId, groupId и label — media_devices_manager.cc:1183 выходит из цикла по виду, когда разрешения нет. Так что единственное, что страница может прочитать, не спрашивая, — это какие виды есть; количество обратно не прочитать, а выдача нескольких записей одного вида была бы признаком, какого не даёт ни один настоящий браузер.

Что измерено на этом хосте (B59, M149, и каждая нога дважды):

нога результат
наше ядро, без личности 2 устройства — {videoinput, audiooutput}, без микрофона
наше ядро, с личностью то же самое — конфигурация не сдвинула ничего
стоковый Chrome 152 то же самое

Значит, расхождением со стоком это не было никогда. Дело в том, что каждый профиль на одной машине перечисляет эту машину, что связывает их друг с другом ровно так же, как список голосов (ISSUE-063) и возможности WebGL.

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

  1. Подмена списка, только до разрешения. В момент, когда выданное разрешение открывало настоящий id устройства, проекция выключалась, и тот же origin видел другую машину, чем мгновением раньше. Кроме того, она заново наполняла список, который Chromium уже отфильтровал по Permissions-Policy, так что документу, которому микрофон запрещён, его могли выдать.
  2. Только фильтрация — убирать виды, применять всегда. Согласовано и до, и после разрешения, ничего не выдумывает, и измерилось правильно (сужающее заявление убрало камеру; контроль сохранил набор хоста; заявленный, но отсутствующий микрофон правильно не выдумывался). Но getUserMedia() её никогда не спрашивает: сайт не видит камеры в enumerateDevices(), а потом захватывает видео с камеры хоста. Скрыть вид из перечисления, когда захват его всё равно отдаёт, — это самопротиворечие в одной сессии, а скрывать — единственное, что эта форма умеет.

Что нужно, чтобы это закрыть. Перечисление и захват должны двигаться вместе — enumerateDevices(), getUserMedia() и selectAudioOutput(), отфильтрованные одним решением, а значит, проекции место в процессе браузера рядом с MediaDevicesManager, а не в колбэке результата в Blink. Добавление вида, которого у хоста нет, к тому же требует синтезировать устройство с id, а после разрешения — и с меткой, то есть нужны имена устройств с происхождением: данные каталога по ADR-0035, а не догадка движка.

До тех пор поле остаётся объявленным и непрочитанным, и это честное состояние; альтернативой предлагался механизм, единственное действие которого создавало противоречие острее той связуемости, которую он убирал.

2d. media.audioFft — на каком FFT идёт Web Audio личности (патч 0049, B94)

Продукт несёт поле с A117 (2026-09-27): оно живёт в модели Media и проходит туда и обратно через profile.update (в контракте конфиг — открытый объект); под закреплением версии (C10: заявленный мажор равен мажору движка, 152) валидатор всё ещё ОТКАЗЫВАЕТ "legacy", потому что это заявка на движок старше 152, — слово владельца (OWNER_REVIEW Q9: выбор пользователя, а не значение по умолчанию) делает из этого ПРЕДУПРЕЖДЕНИЕ о согласованности вместо отказа, и это строить A118.

"legacy" заставляет рендереры личности считать Web Audio на FFT, которым Chrome пользовался до 152 (браузер дописывает WebAudioRustFft в --disable-blink-features рендерера); отсутствие поля или любое другое значение — собственный FFT движка, как у стока. MEASURED (core/scripts/audio-sum-probe.mjs, граф FingerprintJS и CreepJS): одна функция Blink — вся разница между двумя суммами, которые знают детекторы: 124.04347776696522 с ней (сток 152/153 и это ядро), 124.04347527516074 без неё (любой движок до 152, значение, которое CreepJS указывает для Chrome), — и все 5000 отсчётов отрендеренного буфера в обоих случаях равны стоковым. Это заявка движка старше 152: детектор, знающий, что 152 перешёл на FFT на Rust, прочтёт старую сумму при UA 152 как противоречие, поэтому поле по умолчанию выключено и задаётся только там, где его просит каталог (слово владельца, 2026-09-26; ISSUE-287 — модель Media продукта ещё не несёт этот ключ, так что конфиг, прошедший через продукт, его теряет).

2e. Глубокие поля формы — пишутся на месте (A121, ISSUE-316/315)

  • imported: [..] (A122, ISSUE-324) — какие поля личности выставил экспорт другой программы (userAgent, screen, language, timezone) через profile.importText. Проверяются правилами этой личности, не измеряются на этой машине: валидатор ГОВОРИТ об этом (IMPORTED_FROM_FILE, предупреждение), никогда не отказывает. Ядро ключ игнорирует.

  • browser.uaManual: true — оператор ВВЁЛ browser.ua (profile.update {userAgent}). C1 тогда говорит о расхождении с выведенным UA как о ПРЕДУПРЕЖДЕНИИ (C1_UA_MANUAL: UA-CH остаётся выведенным из пина версии, и страница, сравнивающая оба, видит два браузера), а не отказывает; починка сохранённых UA при запуске и перенацеливание ядра введённый UA не трогают. "", или profile.fingerprint.regenerate {fields:[userAgent]}, выводит его заново.

  • webrtc.publicIp — "from-proxy" (авто) или IP-литерал: WebRTC «вручную». Запуск передаёт этот адрес ядру как network.udpSubstitute (0040: рефлексивный кандидат страницы — он, ни одна датаграмма не уходит) без точки реле и без QUIC через неё; на ядре без 0040 ничего не пишется.

  • Шум — только как его читает движок: canvas.mode — ОДИН переключатель для canvas, чтения WebGL и чтения WebGPU (0021/0024/0034); media.audioContext — для аудио (0027); canvas.seed питает их все (и подмену шрифтов) в разных доменах. profile.update {noise} принимает real|noise|manual по поверхностям: webgl обязан равняться canvas, два разных ручных семени отвергаются, а rect с A126 принимает real|noise|manual, как аудио: noise пишет в личность rect.mode: "noise" (его читает патч 0057 — §2f; семя — общее canvas.seed), real убирает ключ, так что конфиг с выключенным переключателем байт в байт равен выпущенному до него.

  • (B103: ядро с патчем 0058 само раскладывает страницу при screen.dpr и рисует окно в масштабе хоста — тогда именно форсированный множитель ниже по-прежнему уменьшает профиль на экране 4K, ISSUE-364, и запуск для такого ядра должен его снять; если флаг оставлен, 0058 ничего не включает.)

  • Запуск форсирует --force-device-scale-factor=<screen.dpr> (dpr в 0.5..=4; иначе — ничего), так что раскладка страницы идёт при масштабе, который заявляет devicePixelRatio, на любом хосте — правило владельца после ПК2 (2560×1440 при 125 %), где раскладка при хостовых 1.25 под заявленным 1.0 делала карточку DOMRect у CreepJS красной (ISSUE-315). Граница окна следует за множителем. СГЕНЕРИРОВАННАЯ личность берёт масштаб хоста из захвата, и SCREEN_MODES получила точные масштабы Windows (2048×1152@1.25 — это ПК2 владельца); именованный пресет держит свой. (A130: ядро, чей манифест называет серию 47+ — он несёт 0058, — флага не получает; ядро более старой или неизвестной серии его сохраняет.) С A183 (ISSUE-505/506) экран при этом масштабе РИСУЕТСЯ, а не подбирается. screen_at_scale из A121 брал строку при масштабе хоста с ближайшей ШИРИНОЙ, любой группы и ОС: на машине при 100 % это сажало 46 % ноутбуков Windows на 1280×720 @1 (панель 720p — каждый ноутбук FHD при 150 % и QHD при 200 % шириной 1280), давало ноутбуку Windows при 200 % размер MacBook в 21 % случаев, а MacBook при 125 % — пару от Windows в 100 %. Теперь generate_seeded_for_core_vendor_at(…, scale) рисует среди собственных строк класса при этом масштабе теми же весами (доля панели из опроса, разложенная по масштабам, при которых панель предлагается), а когда у класса там нет ни одной строки, откатывается в таком порядке (generator::ScaleFit): Docked — ноутбук на мониторах своей ОС (MacBook на машине при 100 % — это MacBook за столом на дисплее 1x, и он сохраняет Air или Pro, которым была нарисована его собственная панель, с колонками и микрофоном этой модели); Widened — офисный настольный на любом мониторе своей ОС; OtherOs — такой же дисплей другой ОС (Mac или Linux при 125 %: остаток назван, правило владельца сохранено этой ценой); Unmatched — строки при таком масштабе нет нигде (175 %): личность сохраняет масштаб, при котором была нарисована. Без масштаба розыгрыш побайтно прежний. Демон записывает в журнал подгонку, отличную от Own/Widened, для класса, который рисует.

  • Гейт концентрации (A183, concentration.rs). Какими личности ПОЛУЧАЮТСЯ, нарисованные так же, как их рисует демон, для каждого класса, изготовителя карты (нет / nvidia / amd / intel) и масштаба хоста (1, 1.25, 1.5, 2), по 1 000 розыгрышей на ячейку: ни один экран не больше чем на 5 процентных пунктов выше своей доли в жизни (generator::screen_shares_expected — веса каталога при этом масштабе), ни одного, которого класс при этом масштабе иметь не может, и ни одного продукта подключённого устройства больше чем у трети класса (собственное устройство машины и имя, которое толпа делит по построению, этой чертой не меряются). Две ячейки были над устройственной чертой в день, когда гейт построили, и НАЗВАНЫ в KNOWN_OVER_CEILING (выходы настольного Mac, ISSUE-511); гейт падает, если такая ячейка вырастет, если появится новая и если одна уйдёт под черту, а в списке останется. Прогнаны мутанты: прежний сдвиг по ближайшей ширине, строка монитора обратно, встроенное устройство как один из вариантов, снятая нижняя граница числа, MacBook в доке без своей модели — каждый красный.

  • Никакого переопределения метрик устройства из DevTools на ядре, которое само проецирует экран (A144, ISSUE-423). Применение личности при запуске толкало Emulation.setDeviceMetricsOverride (deviceScaleFactor = screen.dpr, screenWidth/Height) во вкладку, к которой подключалось, на любом ядре. 0058 по устройству уступает DevTools, поэтому в такой вкладке МЕЖСАЙТОВЫЙ фрейм раскладывал свою область просмотра по множителю хоста — измерено через продукт на хосте при 100 %, личность 1536×960 @ 1.25: фрейм, нарисованный в боксе 600×360, сообщил 480×288 (B116: 750×450 в обратную сторону, хост 125 %, личность 1.0), тогда как вкладка, до которой переопределение не дошло, сообщала 600×360. Ядро, чьи возможности глубоко проецируют C6 (screen.* и dpr из 0011; масштаб — 0058 или флагом выше на более старой серии), переопределения больше не получает — после: 600×360 в обеих вкладках, screen/avail/окно/devicePixelRatio без изменений. Сток его сохраняет: там это единственное, что даёт странице screen личности (его межсайтовые фреймы и его другие вкладки по-прежнему читают хостовый — потолок стока).

2f. rect.mode — шум DOMRect (патч 0057, B102, ISSUE-334)

"noise" заставляет текстовый бокс под трансформацией читаться — через Element.getClientRects / getBoundingClientRect и Range.getClientRects / getBoundingClientRect — так, будто масштаб этой трансформации ушёл на несколько десятимиллионных долей: до ~1e-3 px, на профиль, стабильно, одинаково на всех четырёх путях; отсутствие, "real" или любое другое значение — сток. Семя — canvas.seed, свёрнутое под доменом "rect" (свой переключатель — canvas.mode его не решает, правило 0027), так что у профиля может быть шум прямоугольников при выключенном шуме canvas и наоборот; без canvas.seed ничего не сдвигается. Раскладка не трогается.

Сдвигается только то, что страница не может предсказать, потому что для остального карточка DOMRect у CreepJS — ТАБЛИЦА: при devicePixelRatio 1 скрытый бокс 100×100, повёрнутый на 45°, обязан давать хэш 9d9215cc, а известный пустой бокс под scale(1.000999) на стоке читает ровно Math.fround(100 × 1.000999). Поэтому без трансформации на пути не сдвигается ничего (каждый нетрансформированный бокс, текст тоже, читается на сетке раскладки), а под ближайшим трансформированным предком-или-собой — КОРНЕМ — ничего не сдвигается, пока не выполнены все три условия: его трансформация уводит боксы с сетки раскладки (недиадическая линейная 2-D часть или перспектива, решается по матрице — бокс только со сдвигом не двигается никогда), его собственный бокс — прямоугольник, выровненный по осям, и где-нибудь под ним есть текст. Под подходящим корнем каждый четырёхугольник переводится обратно в собственное пространство корня, масштабируется там на 1 ± (1..16) × 2⁻²³ по каждой оси (константа профиля) и снова отображается через трансформацию корня — изменение делается там, где было бы настоящее, так что собственное отображение Blink округляет результат так же, как стоковый: элемент и его собственная строка текста сохраняют общие края (объединение Range из двух равно элементу, как на стоке), никакие два края не меняют порядок, известный угол остаётся, сдвинутый бокс ведёт себя при округлении float как на стоке. ИЗМЕРЕНО пятью собственными проверками CreepJS (core/scripts/creep-lies-probe.mjs, раздел DOMRect), строкой cj-domrect в /lab/ и gated-тестом crates/vflin-consistency/tests/domrect_noise.rs. Остатки, названные: текстовый бокс, РАЗМЕР которого страница задаёт в CSS при известном ей масштабе, читает сдвинутый дальний край, который страница могла бы пересчитать; пустой бокс внутри корня с текстом идёт через отображение корня; боксы под вложенной трансформацией берут отображение своего корня; нетрансформированный текст (самая частая проба шрифтов) не сдвигается — уход с сетки сам был бы признаком; геометрия SVG (getBBox) не сдвигается никогда; boundingClientRect у IntersectionObserver не сдвигается (на стоке он и так расходится с getBoundingClientRect на 23 из 32 трансформированных боксов).

2g. Каталоги A128 (проверка владельца 2026-10-01) — экраны, карты, версии платформы, шрифты, второй режим карты

  • Экраны (ISSUE-355). Строки SCREEN_MODES теперь называют свою группу (laptop / monitor), свою панель (размер × масштаб) и долю этой панели в опросе Steam (август 2026 — доля панели, все масштабы вместе; ни одна публичная статистика не делит панель по масштабам), а также можно ли выпускать на них новую личность (offered). Пары — это панели при масштабе, который им действительно даёт Windows, — таблица DPI Microsoft (Table 1: 13.3″ QHD при 200 %, 15.4″/17″ FHD при 125 %, 24″ QHD при 125 %, 23″ 4K при 200 % …) или точное деление при масштабе, который позволяет её Table 5 (не меньше 720 эффективных строк), — нижняя граница 1024 в ширину и 720 строк. Строка, которая больше не предлагается (подозрительные размеры Mac из ISSUE-048, доаудитная 2048×1152@1), остаётся ВАЛИДНОЙ, так что выпущенный на ней профиль по-прежнему запускается; генератор и перенос под масштаб хоста берут только предлагаемые строки (классы Mac и пресет macos-chrome-laptop перешли на размеры MacBook «looks like» из Steam). fingerprint.screenModes выдаёт group, offered, panelWidth, panelHeight, panelSharePct и оба источника.
  • Карты (ISSUE-381). Рядом с пулом выборки (каталог конкурента, Borrowed) — открытая таблица (gpu_open.rs): каждая модель со своим device id (или несколькими) из pci.ids и своей долей в Steam — NVIDIA 48, AMD 12, Intel 13 моделей, у каждого ≥ 4 архитектур; id Intel прочитаны здесь по одному из pci.ids, NVIDIA/AMD — из объединения K32 за вычетом id HDMI-аудио, который оно прихватило. fingerprint.gpus несёт её как openModels, и каждая карта пула, которую таблица знает, называет свою architecture, pciName и steamSharePct.
  • Карты, которых опрос не видел, сделаны выбираемыми и рисуемыми (A146, ISSUE-431). Владелец: «у нас даже в каталоге нету 5050 карты и других новых». Измерено через fingerprint.gpus, то есть через то, что перечисляет редактор: 253 карты и ни одной RTX 5050 — при том что в пуле розыгрыша уже было десять строк RTX 50, то есть пробел был точечный, а не поколенческий. У пяти моделей NVIDIA/AMD из открытой таблицы не было строки в пуле, и их можно нести честно — RTX 5050 Laptop GPU, RTX 5070 Laptop GPU, RTX 5070 Ti Laptop GPU, RTX 4080, RTX 3050 Ti Laptop GPU (4 SKU), Radeon RX 7800 XT, 12 строк (gpu_open::OPEN_ONLY_WINDOWS), — поэтому fingerprint.gpus отвечает 265, и каждая новая строка называет source: open рядом со своей долей в Steam. Три происхождения в одной строке, и ни одно не Assumed: ФОРМА — Measured (захват этого хоста, и K32 проверил её вид на 170 независимых строках), device id — Derived (pci.ids, правило K32), имя — Derived (собственная строка опроса Steam). Вес розыгрыша — нижняя граница, а не оценка: одна машина на строку — карта, которой этот опрос не видел, рисуется не чаще самой редкой карты, которую он видел. Первая попытка масштабировала долю Steam на 4 462 машины пула, была измерена и отвергнута: она сажала 40 % сгенерированных личностей win11-laptop на новые карты. С нижней границей: 7.0 % из 200 win11-laptop и 0 из 200 win11-desktop, а первая тройка списка выбора не изменилась, то есть толпа по-прежнему ведёт (ADR-0037). Существующий профиль не двигается: розыгрыш идёт при создании, и выпавшая карта хранится в конфиге профиля, который читает каждое последующее чтение. Четырёх кандидатов отвёл сам пул, а не вкус: наблюдаемые им имена — GTX 1060 3GB / 6GB / 5GB и Radeon RX 570/580 Series, поэтому агрегированные у Steam GTX 1060 и AMD Radeon RX 580 — строки, которых не печатает ни один драйвер. 13 моделей Intel, которые есть только в открытой таблице, исключены по той же причине, и доказательство — в самой таблице: пул наблюдал Intel(R) Iris(R) Xe Graphics (0x00009A49) там, где открытая таблица даёт Intel Iris Xe Graphics для ТОГО ЖЕ id — настоящий драйвер Intel украшает имя (Intel(R), Iris(R), Arc(TM), (8GB)), а pci.ids — нет. Они, как и любая форма рендерера macOS или Linux вообще, ждут захвата с машины, у которой такая карта есть (половина ISSUE-431 за recon).
  • Каждый вариант, который называет драйвер, и ни одной серверной карты (A167, ISSUE-381). Владелец: «у нас в каталогах видюх должны быть все видюхи и со всеми вариантами 0x000 … чтобы не добавить ну совсем серверную карту». K58 у recon принёс 2 062 строки, каждую с источником её строки и её id (docs/90-meta/recon/K58-gpu-table.csv); scripts/measure/a167-k58-driver-table.py оставляет 166 и пишет их в gpu_driver.rs (DRIVER_WINDOWS, source: driver): только NVIDIA — имя и id из supportedchips драйвера NVIDIA 580.126.09, измеренная форма, — потому что у 644 строк AMD и 225 строк Intel нет строки драйвера (pci.ids называет семейство чипов, а не то, что печатает драйвер); Kepler и новее (самая старая архитектура, которую наблюдал опрос; vs_5_0 ещё требует уровня возможностей 11); не оспорено K58; не ноутбучный ЧИП под настольным именем (RTX 2060 на TU106BM — настольный рисовался бы по имени); не уже строка пула. Исключение — правило с тестом, а не умолчание: gpu_pool::is_server_or_workstation (Tesla, Quadro, GRID, NVS, RTX A/PRO/… Ada Generation, голые датацентровые имена NVIDIA и майнерские платы, DGX, Radeon Pro / FirePro / Instinct, Arc Pro с номером платы) не пускает такую карту ни в список, ни в розыгрыш ЛЮБОЙ таблицы; проверено против собственного класса K58 — ловит все 281 строку, которые K58 называет рабочей станцией или сервером, и ещё 21. Восемь наблюдённых опросом карт рабочих станций (30 машин) остаются в WINDOWS как наблюдение и уходят из списка. no_server_or_workstation_card_is_offered_or_drawn падает, если авторская таблица такую получит, если изменится список опроса или если хоть один розыгрыш её выдаст. Вес: опрос не видел ни одной из 166, поэтому весь срез вместе весит UNSEEN_MACHINES = 3 машины (правило трёх) — вес теперь машины × 1 000, так что это помещается; измерено 0.20 % настольного дискретного розыгрыша и 1.57 % ноутбучного (нижняя граница в одну машину на каждую дала бы ~40 % ноутбуков). is_laptop_part читает ещё и буквенные ноутбучные части (GT 750M, 940MX, RX 6600M, RX 7600S), что вывело три наблюдённые карты из настольного розыгрыша. Закрепление сдвинулось (3 семени из 8) по той же арифметике, что и в A166; ни один сохранённый профиль не двигается.
  • Камеры и микрофоны (ISSUE-352). media_devices::DEVICE_NAMES получил 9 камер Windows (Logitech C930e, StreamCam, BRIO 4K Stream Edition, C505e, C920 PRO, C525, C170; Microsoft LifeCam Cinema, Studio) и 4 USB-микрофона (HyperX QuadCast, AT2020USB+, ATR2100-USB, RØDE Podcaster), модель и USB id которых подтверждает usb.ids (прочитано 2026-10-01). По-прежнему Assumed, как каждая строка там: ни один захват не содержит метки (в захватах этой машины их нет), и то, что устройство печатает ровно имя из usb.ids, не наблюдалось. A141 (ISSUE-418): первые две строки Measured — прочитаны на настоящей машине (хост этой полосы, ядро s53, enumerateDevices() после выданного разрешения, 2026-10-04): Digital Output (High Definition Audio Device) и <the monitor's model> (NVIDIA High Definition Audio) (звук дисплея по HDMI). То же чтение назвало OBS Virtual Camera и Speakers (Steam Streaming Speakers); обе оставлены за бортом нарочно — виртуальную камеру как раз и ищет проверка на подмену. Генератор рисует имена из этого каталога для ОС личности (§2c, нарисованное число). A183 (ISSUE-507) убрал строку монитора: Windows называет звук дисплея по МОНИТОРУ, поэтому строка была именем одной машины и ехала на 19–21 % всех личностей Windows (K68) — имя, которое наши профили делят друг с другом и ни с кем больше. Держат его снаружи два гейта: one_machine_names (каждая звуковая строка Windows — это конечная точка, которую даёт сама Windows: Speakers, Headphones, Microphone, Microphone Array, Digital Output, Realtek Digital Output) и, на Windows, no_catalog_row_is_one_of_this_machines_own_audio_devices (читает звуковые конечные точки собирающей машины; строка каталога, равная одной из них, обязана быть конечной точкой Windows на общем драйвере). Digital Output (High Definition Audio Device) остаётся: имя драйвера, которое несёт каждая такая машина.
  • hardware.gpu.foreignVendor (ISSUE-375). Необязательное; отсутствие — «так не выбирали» (каждый старый конфиг читается так же). Ставится fingerprint.generate во втором режиме (random) или когда gpuVendor называет другого производителя, и только когда производитель карты отличается от производителя этой машины: строка тогда словами говорит C2_GPU_FOREIGN_VENDOR_CHOSEN — сглаживание — это WebGL-шум ядра, а пока у ядра его нет, WebGL отвечает возможностями этой машины, — вместо флага C2_GPU_FOREIGN_VENDOR. Только на стороне демона: запуск его отбрасывает.
  • Версии платформы (ISSUE-360). PLATFORM_VERSIONS: значения Sec-CH-UA-Platform-Version, которые берёт сгенерированная личность, с релизом за каждым и с происхождением значения и веса по отдельности — Windows 11 19.0.0 (24H2/25H2; измерено на этой машине: 25H2, сборка 26200, Chromium 152), 15.0.0 (22H2/23H2), 14.0.0 (21H2), веса 85/12/3 (Assumed по таблице обслуживания Microsoft — ни одна публичная таблица не делит Windows 11 по релизам); Windows 10 10.0.0; macOS 14.x как раньше. 26H2 (сборка 26300) и 26H1 (сборка 28000) не включены — их значение здесь нигде не измерено. fingerprint.platformVersions его читает.
  • Шрифты (ISSUE-376, половина каталога). fingerprint.fonts: наборы производителей с их источником — шрифты, поставляемые Microsoft с Windows 11, Feature on Demand каждой письменности (ставится вместе с языком этой письменности), необязательный общеевропейский набор; списки macOS Sequoia от Apple. fingerprint.generate с fonts: manual добавляет личности Windows наборы, которые были бы у её машины (поставляемые, письменность её языка, необязательный набор у ~10 % личностей по их id), и заново разрешает presentFamilies и родовые семейства; macOS/Linux ничего не добавляют (ни один источник не говорит, какие из шрифтов их каталога есть у машины). Что заявляет список и что рисует движок — две разные вещи: движок показывает то, что есть у ЭТОЙ машины, — заявленное семейство, которого у машины нет, решает механизм ядра, а не этот список.

2g. identity.passkeys — ключ доступа, привязанный к профилю (патч 0059, B101, ISSUE-311)

"profile" даёт профилю собственный аутентификатор WebAuthn: каждый запрос обычного (не «инкогнито») контекста обслуживает он, учётные данные хранятся в <user-data-dir>/Vflin Passkeys, и в Windows не уходит ничего — ни Windows Hello, ни «Insert your security key», ни USB-ключа, ни телефона, ни Менеджера паролей Google. Отсутствие или любое другое значение — сток (API WebAuthn в Windows). Поле читает только БРАУЗЕР (ни рендерер, ни проекция, видимая странице): страница видит то, что ей показывает настоящий поставщик ключей доступа.

Что видит страница (аутентификатор представляется поставщиком ключей доступа, который поставляется с Chrome): подключение platform, резидентные ключи, только ES256, формат аттестации none, что бы ни просила страница, AAGUID Менеджера паролей Google (ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4), флаги UP + UV + BE + BS, счётчик подписей, закреплённый на 0, случайный id учётных данных длиной 16 байт, транспорты ["hybrid", "internal"]; isUserVerifyingPlatformAuthenticatorAvailable() — true. Что видит человек: собственные окна Chrome — «Create a passkey for » перед созданием, окно аккаунта («Your Chrome profile») перед входом — и ничего от Windows. Проверка пользователя СООБЩАЕТСЯ, а не выполняется: биометрии и PIN нет; подтверждение — кнопка Continue в окне (так делают профильные ключи доступа у конкурентов; материал для ADR, если владельцу нужен PIN). Файл хранит закрытые ключи (PKCS#8, base64url, JSON) — открытым текстом, пока профиль распакован на диске; pack запечатывает его в зашифрованный блоб профиля (ADR-0004), а экспорт и синхронизация переносят его (A133). Ключ доступа не теряется никогда (B105, F1b владельца): рядом с хранилищем Vflin Passkeys.bak-<UTC time> держит исходные байты хранилища, которое пришлось чинить (от более новой сборки, повреждённое, частично нечитаемое, — каждый ключ доступа, который оно ещё отдаёт, сохраняется, и новые записываются), а Vflin Passkeys.unsaved держит изменения, сделанные, пока хранилище нельзя было прочесть (заблокировано), и сливается при следующем чтении (.unsaved, которое не удалось прочесть целиком, копируется в Vflin Passkeys.unsaved.bak-<UTC time> до перезаписи); неудавшаяся запись повторяется через 2/10/60/300 s. Все файлы Vflin Passkeys* принадлежат профилю и переезжают вместе с ним. Измерено в core/PATCHES.md (internal: core/PATCHES.md), строка 0059.

2i. privacy.doNotTrack — Do Not Track (патч 0060, B104/B105, ISSUE-358)

BOOL. true включает Do Not Track профиля; false выключает; отсутствие (или не bool) оставляет собственную настройку Chrome, по умолчанию выключенную. Это СТАНДАРТНАЯ настройка Chrome — ядро само кладёт prefs::kEnableDoNotTrack в хранилище настроек командной строки Chrome на этот запуск (vflin::ApplyDoNotTrackPref; в Preferences профиля не пишется никогда), так что каждый читатель получает одно значение: переключатель chrome://settings, navigator.doNotTrack ("1", когда включено, null, когда выключено) И заголовок запроса DNT: 1. При заданном поле переключатель настроек показывает значение конфига и не даёт его поменять (решает личность); без поля он — Chrome. Ни новой поверхности, ни JS-прослойки; navigator.doNotTrack в обоих случаях — родной геттер. Согласованность: DNT — пользовательская настройка, а не сигнал, выведенный из ОС/GPU, так что ничему в каталоге не противоречит; выставлять её — выбор владельца (профиль, заявляющий DNT, просто выглядит как пользователь, который его включил). ИЗМЕРЕНО (settings-probe.mjs): doNotTrack:true -> navigator.doNotTrack === "1", сервер видит DNT: 1, chrome://settings читает true; отсутствие -> null, нет заголовка, настройки false — байт в байт как у стока (B105, после находки R8, что первая форма оставляла в настройках ВЫКЛ).

2j. webgl.mode — собственный переключатель WebGL (патч 0061, B105, ISSUE-375/376)

СТРОКА, отдельно от canvas.mode. Отсутствие (или любое другое значение) — WebGL следует за canvas: его чтения шумят ровно тогда, когда canvas.mode равен stable-noise, как во всех сборках до 0061. "off" — ни одного WebGL-контекста на canvas или OffscreenCanvas, на странице или в воркере (собственная ошибка создания Chrome; интерфейсы остаются). "real" — собственные чтения WebGL (readPixels, буфер PIXEL_PACK, toDataURL/toBlob WebGL-холста) не шумят никогда. "noise" — шумят, что бы ни говорил canvas.mode, с canvas.seed, свёрнутым ровно так же, как семя canvas: одна маска, так что пути сходятся, когда сходятся переключатели, и переключатель canvas никогда не двигает хэш WebGL; без canvas.seed шуметь нечем. Это не webgl.noise модели продукта (ядро его не читает). Для личности с картой другого производителя (hardware.gpu.foreignVendor из A128) генератору следует писать "noise", чтобы страница никогда не читала хэш рендера карты хоста. Остаток только у СМЕШАННЫХ переключателей: WebGL-холст, нарисованный в 2-D холст, читается там по 2-D переключателю. Измерено в core/PATCHES.md (internal: core/PATCHES.md), строка 0061.

2k. media.video.source — файл камеры профиля (патчи 0067–0070, B109; половина демона — A161)

"media": {"video": {"source": "Vflin Camera/clip.mp4"}} — путь внутри каталога профиля, который ядро играет как камеру (0067 читает Y4M/MJPEG, 0068 — MP4/MOV H.264, WebM/MKV VP8/VP9, MP4 AV1; отказывает AVI, HEVC, MPEG-4 Part 2 и Theora). Отсутствует — настоящая камера.

  • Правило пути — ядра (VflinIsProfileRelativePath): не пустой, не больше 512 байт, без управляющих символов, без :, не начинается со слэша, без части ... Валидатор отказывает всему остальному как MEDIA_VIDEO_SOURCE_OUTSIDE — ядро прочло бы такой путь как отсутствующий и взяло настоящую камеру, то есть конфиг заявлял бы файл, которого страница не получит.
  • Ставится из интерфейса: profile.update {camera: "<path>" | ""} (CLI vflin profile update <id> --camera <path>) — пишется на месте; отказывается, пока профиль работает (ядро берёт конфиг при запуске). Подробности профиля называют это camera.
  • Ставится из собственного окна профиля (кнопка камеры из 0070): окно копирует выбранный файл в <user-data-dir>/Vflin Camera/ и пишет Vflin Camera/camera.json — {"version":1,"base":"<the media.video.source it chose against>","source":"<the file>"} (0069). Ядро читает его при каждом открытии камеры; побеждает тот, кто менял последним: base равен media.video.source конфига → source окна, иначе — файл конфига.
  • Переносится в запись, когда профиль запечатывают (camera_choice::carry, рядом с заметкой окна): по тому же правилу — выбор окна, сделанный после интерфейсного, становится media.video.source записи ("" очищает его). Файл выбора остаётся (это состояние ядра, и при следующем старте он согласуется с записью по тому же правилу).
  • Папка едет вместе с профилем: Vflin Camera/ не исключена из запечатываемого содержимого, поэтому видео и его выбор запечатываются, синхронизируются и выгружаются вместе с профилем, как Vflin Passkeys. Старые копии не подчищаются.
  • Ключ переносится (урок audioFft, ISSUE-287): конфиг, называющий файл камеры, сохраняет его через каждый круг продукта; конфиг без него ключа не пишет.

media.video.realCamera — является ли «нет файла» камерой хоста (патч 0078, B145, ISSUE-532)

"media": {"video": {"realCamera": false}} — БУЛЕВО. false: пока файл не выбран (ни source конфига, ни выбор окна), камера, которую открывает страница или запрос разрешения Chrome, играет собственную нарисованную картинку профиля (патч 0083, ниже), а устройство хоста не создаётся вовсе. Когда рисовать не из чего (нет семени), вместо этого следует отказ — та же ошибка захвата, что и у нечитаемого файла, NotReadableError странице (0078, пол с отказом по умолчанию: камера хоста никогда не бывает запасным вариантом). Отсутствует, true или не булево: нет файла — значит настоящая камера, ровно как до этого поля. Выбранный файл играет в любом случае.

Почему, измерено (core/scripts/camera-prompt-probe.mjs --gate, поддельная камера Chrome вместо хостовой): с 0075 камера НАЗВАНА профильной (HD Pro Webcam C920) на обеих дорогах, но без файла она ИГРАЛА картинку хоста — средний цвет кадра 8,151,2 и в ноге личности, и в ноге хоста. На машине владельца камера хоста — OBS Virtual Camera, а её картинкой может быть его экран.

B145 выбрал отказ вместо собственной картинки, и владелец это развернул 2026-10-19 («так для отпечатка нужно ж»; «надо чтобы картинка подгружаемая шла будто с камеры, которая указана»; и о цене нарисованной картинки — «значит надо узнать, как гармонично сделать, чтобы имитировать реальную камеру»). Чего стоит каждый вариант, с решением в том виде, как оно стоит:

  • Одна наша картинка на все профили — отвергнуто, и не по вкусу: один кадр, общий у всех профилей, — это идентификатор, который связывает каждый профиль, записанный сайтом, то самое связывание, на распутывание которого K72 потратил день. Владелец отказался и от поставки файлов вообще («не удобный вариант, надо чтобы пользователь просто, когда надо, подгружал»).
  • Отказ (выбор B145, теперь пол, а не ответ) — сайт, которому нужна камера, получает NotReadableError, то, что даёт настоящая машина с Windows, когда её камера занята другой программой; getUserMedia({audio, video}) тогда падает целиком, вместе с микрофоном. Это остаётся тем, что получает профиль без семени.
  • Картинка, нарисованная на профиль (выбрано, патч 0083) — страница получает движущуюся картинку, которая принадлежит этому профилю и никакому другому, а связанный запрос {audio, video} работает. Её цена, сказанная прямо: это не комната и не лицо. Сайт, который СМОТРИТ на картинку — проверка личности, видеозвонок, человек на той стороне, — видит расфокусированную поверхность, а не человека; конкуренты вместо этого поставляют зацикленный ролик с настоящим человеком, а это другой размен (одно записанное лицо, общее у всех профилей, которые его играют). Что картинка всё-таки несёт — измерено ниже.

Кто пишет поле: control-plane (A190), и новый профиль рождается с выключенной настоящей камерой.

media.video.modes — паспорт заявленной камеры (A192, ISSUE-532, K76; половина ядра — патч 0085, B151)

"media": {"video": {"modes": [{"width": 1280, "height": 720, "frameRate": 30}]}} — режимы, которые предлагает камера, ЗАЯВЛЕННАЯ профилем: самый большой кадр на каждой ступени частоты, которую называет её изготовитель, сначала самый большой, из каталога (media_devices::CAMERA_MODES, каждая камера с датированным источником или помеченная ASSUMED). Пишется демоном при каждом запуске из имени заявленной камеры (её первой media.devices.labels.videoinput), никогда не хранится вместе с профилем — имя профиля, а паспорт каталога, поэтому в записи они разойтись не могут, — и выбрасывается из приносимого конфига при создании, как fonts.dir. Отсутствует, когда профиль не заявляет камеры или называет такую, у которой нет паспорта.

Почему (K76, измерено): страница читает ИМЯ камеры и её getCapabilities() рядом, и они расходились — под «Microsoft® LifeCam HD-3000» (камерой на 1280x720 при 30 по её паспорту) getCapabilities() предлагал 1920x1080 при 60, режимы устройства, перечисленного машиной (OBS Virtual Camera), потому что нарисованная картинка и файл рисуются «того размера, которого попросили у перечисленной камеры». Имя, подобранное под режимы машины, работать не может: они неизвестны, пока не открыта камера машины, а машина без камеры не перечисляет ничего. Поэтому режимы едут вместе с именем.

Половина ядра — патч 0085 (B151), и она измерена. До него под «Microsoft® LifeCam HD-3000» — камерой на 1280x720 при 30 по её изготовителю — страница читала getCapabilities() 3840x2160 при 20, режимы собственного устройства машины, а запрос 60 кадров в секунду получал ответом машинные 20 (core/scripts/camera-liveness-probe.mjs). После него: w max 1280, h max 720, rate max 30, каждый запрос выше паспорта прижат к 1280x720, а 60 отвечает 30.

Где правило применяется: только там, где играет НЕ камера машины — realCamera: false (патч 0078) или выбранный файл (0067). Когда играет камера машины, её собственные режимы и есть правдивый ответ, а заявить другие значило бы, что страница просит 1080p и получает 720p, — сам по себе признак. Браузер отвечает в одном месте, VideoCaptureManager::GetDeviceSupportedFormats, потому что обе дороги к возможностям страницы кончаются там, а камера, которой у машины нет, доходит туда с пустым списком — и это ровно тот случай, в который попадает спроецированная камера. Остаток: профиль, заявляющий ДВЕ камеры, получает этот один паспорт на обе; конфиг несёт одну заявленную камеру, поэтому вторую не на что опереть (ISSUE-532).

Что читает у камеры ПРОВЕРКА ЖИВОСТИ и три вещи, которые она нашла (патч 0085, B151, ISSUE-532)

K77 прочитал открытую работу за таким детектором (Verigram, arXiv 2512.10653, 30 000+ сессий проверки личности): он не смотрит ни на шум, ни на мерцание. Он даёт камере задания applyConstraints — высоты 11, 22, 340, 640, 1001, 2001, 3001, 10001 и частоты 1, 5, 30, 60, 120, 200 — и записывает, что попросили, что заявляет дорожка, что пришло и сколько заняло ответить. Их числа: настоящей камере 91–210 мс на смену собственного режима, ~2 мс на подстройку внутри одного; OBS — 0.8–1.6 мс. Измерено на нашей (core/scripts/camera-liveness-probe.mjs, поддельная камера Chrome вместо хостовой — у этой машины веб-камеры нет, поэтому таблица статьи — единственный ориентир):

что читает страница поддельное устройство Chrome наша нарисованная картинка, до наш файл, до после 0085 настоящая камера (статья)
СМЕНА режима 1.5–4.1 мс 13–85 мс 15–18 мс 202–222 мс 91–210 мс
подстройка ВНУТРИ режима 0.5–3.7 мс 0.4–1.1 мс 0.5–1.1 мс 0.4–1.1 мс ~2 мс
getCapabilities() 3840x2160@20 3840x2160@20 (машины) машины паспорт: 1280x720@30 собственные камеры
ПЕРВЫЙ кадр трёх открытий одинаковый отличается (открытие ловит другой кадр) одинаковый, побайтно отличается отличается
кривая яркости двух открытий расхождение 0, корреляция 1.000 расхождение 3.1, 0.72 — но 0.974–0.986 ПОСЛЕ СГЛАЖИВАНИЯ расхождение 0.39, 0.999 расхождение 6–9, ≈0 —

Сглаженное число и есть находка. Скользящее среднее по 8 кадрам не стоит детектору ничего и убирает зерно и покадровую охоту экспозиции, а оставалось медленное блуждание экспозиции, проигранное заново от открытия: один прогон даже выдал двум разным СТАРТАМ профиля один и тот же первый кадр. Поэтому 0085 разделяет часы: комната — профиля и начинается одинаково при каждом открытии (лепестки размытия, баланс белого, падение к краям, фиксированный узор сенсора), а всё, что делает электроника, читает часы, идущие от неподвижной эпохи (зерно, блуждание, охота, усиление, сетевая полоса). ФАЙЛ тоже больше не перематывается при каждом открытии — камера показывает то, что перед ней сейчас. Цена разделения, названная: два старта больше не ловят комнату в один и тот же миг её собственного дрейфа, поэтому межстартовые пороги пробы нарисованной картинки пересчитаны по тому, что меряет собственное движение картинки (её форма 8x6 двигается на 0.54–0.91 между кадрами в 400 мс; между стартами читается 0.80–0.87).

ЧТО КАРТИНКА РЕАЛЬНО УСПЕВАЕТ РИСОВАТЬ, измерено (B153). K79 прочитал нашу камеру как заявляющую 60 кадров в секунду и отдающую 35-39. Причина — ПЛОЩАДЬ кадра, а не частота: нарисованная картинка строится пиксель за пикселем, поэтому успевает примерно до 2 мегапикселей и выше этого отстаёт. Посчитанные кадры в секунду при заявленных 20 (core/scripts/camera-liveness-probe.mjs --no-modes, собственные режимы машины):

размер, который попросила страница заявлено отдано
11x11 .. 1780x1001 (до 1.8 Mpx) 20 19.4 .. 19.8
3557x2001 (7.1 Mpx) 20 8.2
3840x2160 (8.3 Mpx) 20 7.8

Паспорт закрывает это там, где он написан. Ни одна камера каталога не предлагает больше 1920x1080, поэтому с media.video.modes при запуске устройство никогда не сидит выше 2 Mpx и отдаёт то, что заявляет: измерено 29.2..29.9 при заявленных 30, а открытие без ограничений — 640x480 @30 заявлено, 29.4 отдано (это заодно отвечает на вторую половину заметки K79 — открытие идёт на 640x480). Без паспорта снова решают режимы машины, и на хосте, чья камера предлагает 4K, страница читает обещание, которого мы не держим. Остаток, названный: само рисование слишком медленно выше ~2 Mpx; сделать его быстрее или отказывать режиму, за которым оно не успевает, — изменение ядра, цену которого никто ещё не считал.

ОДНА ПОПРАВКА К ТОМУ, КАК ЧИТАЛИ ПОВТОР (B153, та же ловушка, которую B152 нашёл в звуке). Межстартовая корреляция кривой яркости САМА ПО СЕБЕ не доказывает, что камера проигрывает запись заново: за 40 кадров, которые записывает эта проба (~1.5 с), блуждание экспозиции (0.03..0.12 Гц, периоды 8..33 с) на прямую не отличить, а две прямые похожего наклона коррелируют около 1 при любой фазе. Измерено на ТЕКУЩЕМ ядре, которое кладёт блуждание на часы эпохи: сглаженная корреляция между двумя стартами 0.943, при том что кривые стоят на 10.1 уровня яркости врозь при размахе 4.2 — далеко по уровню, похоже по форме. Читать расхождение, а не корреляцию, или удлинять окно за целый вдох блуждания.

Что НЕ починено и сделано нарочно: медленная траектория комнаты по-прежнему функция времени с момента открытия — лепестки дрейфуют одинаково при каждом открытии, потому что «одна комната у профиля» — замысел владельца. Что из этого может прочитать детектор — межстартовая корреляция формы выше.

Картинка без файла: рисуется на профиль, из измеренных чисел (патч 0083, B149, ISSUE-532)

При realCamera: false и невыбранном файле камера играет VflinDrawnCamera (media/capture/video/vflin_file_camera.{h,cc}): расфокусированную сцену, построенную из собственного семени профиля — canvas.seed, свёрнутое под доменом "camera", — так что оно не пересекается с ключами канвы, звука, WebGPU, rect, WebGL и шрифтов от того же семени (две функции на одном ключе были бы корреляцией, которой детектор мог бы воспользоваться). Браузер передаёт его службе захвата как --vflin-camera-drawn-seed рядом с переключателем 0078 и только при выключенной настоящей камере. Кадр — чистая функция от (семени, целой миллисекунды прошедшего времени): один и тот же профиль рисует одну и ту же картинку при каждом старте, в любом процессе; два профиля рисуют две картинки; нет семени в конфиге — значит отказ 0078, никогда не камера хоста. Картинка рисуется того размера, которого попросили у перечисленной камеры, поэтому getSettings() и getCapabilities() остаются как у перечисленной камеры, ровно как с файлом, и ничего не масштабируется.

Из чего она сделана и зачем каждая часть: низкочастотное поле яркости (2–4 дрейфующих лепестка — то, что оставляет расфокусировка), падение света к краям на 12–34 %, собственный фиксированный узор сенсора (один уровень в любую сторону, КАЖДЫЙ на пятой части пикселей, так что две пятых несут смещение, — DSNU настоящего сенсора, та часть его шума, которая не меняется от кадра к кадру), треугольное временное зерно, медленное блуждание автоэкспозиции плюс покадровая охота, бегущая сетевая полоса при сети 50 Гц (и никакой при 60 Гц, где 120 Гц бьются ровно против 30 кадров в секунду, — поэтому американская камера на этой частоте не мерцает) и баланс белого на профиль, между тёплой лампой и холодным окном. Яркость держится в 16..235, цветность в 16..240 — студийный размах, который даёт конвейер камеры, а не полный диапазон, который взяло бы рисование.

Сначала измерено, потом построено (core/scripts/camera-stream-measure.mjs, 120 кадров через requestVideoFrameCallback). То, что здесь стоит за «настоящую камеру», — это предел, а не утверждение: у этой машины веб-камеры нет вовсе (B144, K76 — её единственная камера, OBS Virtual Camera владельца, пробой никогда не открывалась), поэтому ориентиры — это настоящая запись, проигранная через то же самое устройство (media/test/data/bear-1280x720.mp4, так что её зерно, её сжатие и её дрейф яркости — камерные) и собственное поддельное устройство Chrome. Шум сенсора и автоэкспозиция живой USB-камеры на этой машине НЕ измерены.

что читает страница запись, через наше устройство собственное устройство хоста нарисованная картинка
getSettings() 640x480 @ 20 640x480 @ 20 640x480 @ 20 — одинаково
частота в getCapabilities() {"max":20,"min":0} {"max":20,"min":0} {"max":20,"min":0} — одинаково
интервал между кадрами (мс) среднее 49.96, ско 7.36, 21/119 мимо больше чем на 20 % среднее 49.89, ско 6.38, 23/119 среднее 49.98, ско 6.68, 28/119
размах яркости за 6 с (luma) 10.01 4.01 5.77
покадровая |разница| 12.97 (свой размах 9.71) 2.46 (3.21) 5.03 (1.47)
зерно (высокочастотная энергия, 64x64 в 1:1) 0.87 (размах 0.21) 0.89 (0.16) 0.85 (0.11)
предложенные размеры и частоты ровно только 640x480@15 из семи спрошенных те же семь, те же ответы те же семь ответов

Три из этих чисел изменили генератор, и ради этого и меряют сначала. (а) Зерно придержали: путь захвата масштабирует картинку к режиму камеры, поэтому высокочастотная энергия, которую страница реально читает с настоящей записи, — 0.87, а попиксельный шум, построенный на глаз, читался примерно вдвое больше, то есть зерна больше, чем даёт камера. (б) Картинка из сцены и одного зерна читалась покадровой разницей около 1.0 — ниже даже 2.46 устройства хоста; охота экспозиции (пространственно ровная, поэтому зерна не стоит) и сетевая полоса несут её вместо этого. (в) Когда оба члена автоэкспозиции медленные, шесть секунд читались размахом яркости 1.3–2.0 против 4.0 и 10.0 у ориентиров; разделение их на свет комнаты (0.03–0.12 Гц) и сам контур экспозиции (0.30–0.90 Гц) — это то, что делает настоящий контур автоэкспозиции, и это то, что подняло её в полосу.

Что страница всё ещё может отличить, измеренное или рассуждённое, и это честный остаток:

  • размах интервала принадлежит ПУТИ ДОСТАВКИ, а не устройству: на этом хосте ориентиры читались как ско 6.4–10.0 мс через requestVideoFrameCallback, и собственная нога записи читалась 9.97 в одном прогоне и 7.36 в следующем — значит, это число не говорит о камере ничего ни в ту, ни в другую сторону, и 6.68 нарисованной картинки ничего не доказывает;
  • нет артефактов сжатия: MJPEG или H.264 настоящей веб-камеры оставляют границы блоков и «москитный» шум; нарисованный кадр — это I420 прямо из генератора, и он не несёт ни того, ни другого;
  • нет бегущего затвора, нет хроматической аберрации объектива, нет горячих пикселей, нет битых пикселей;
  • часы на 30 в секунду были дефектом, и это тот, который поймало независимое чтение (стенограмма Codex к 0083, воспроизведено): камера, отдающая 60 кадров в секунду, клала 5 из каждых 9 соседних кадров на один индекс и получала их побайтно одинаковыми, 7 из 9 при 120. Исправлено на миллисекундные часы, с тестом на шести частотах. В измерении выше это не показалось, потому что единственный режим, который предлагает этот хост, — 20 в секунду, и ровно поэтому остаток ниже имеет значение;
  • нарисованная картинка всё ещё МЕНЯЕТСЯ МЕНЬШЕ, чем любой из ориентиров, от прогона к прогону — её покадровая разница расходится на 1.47 против 3.21 и 9.71, её зерно на 0.11 против 0.16 и 0.21: содержимое настоящего потока меняется, а наше только дрейфует. Блуждание усиления сенсора закрыло большую часть разрыва по зерну, а миллисекундные часы — большую часть другого;
  • и главное, чего не чинит ни одно число: сайт, который смотрит на картинку, не видит ни человека, ни комнаты.

Тесты: capture_unittests VflinDrawnCameraTest.OneProfilesPictureIsItsOwnAndTheSameAtEveryStart, .NoSeedIsNoPicture, VflinFileCameraTest.WithTheRealCameraOffAndASeedNoFileIsTheProfilesPicture, .TheSeedSwitchMakesNoFileThePictureNotARefusal; unit_tests FpConfigParseTest.TheDrawnCamerasSeedIsTheProfilesOwnAndRidesTheCaptureServiceOnlyWithTheRealCameraOff. Проба: core/scripts/camera-drawn-probe.mjs --gate. Кнопка камеры (0070) говорит, что из двух означает «нет файла»: «Камера профиля: файл не выбран — играет своя картинка профиля» при семени, «…— сайтам отказ» без него.

Файл камеры на Windows, когда Chrome просит NV12 (патч 0080, B146). С захватом Media Foundation через D3D11 — по умолчанию у Chrome на Windows — Chrome просит у камеры, предлагающей NV12, именно его и отдаёт устройству буфер GPU; файловая камера писала в этот буфер через отображение, которое на Windows есть копия, до текстуры не доходящая, поэтому каждый кадр приходил ПУСТЫМ (сплошной I420 из нулей: средний цвет 0,136,0, сплошной зелёный предпросмотр). Измерено поддельной камерой Chrome (она предлагает NV12); с --disable-features=MediaFoundationD3D11VideoCapture тот же файл играл, а OBS (DirectShow) играл его всё время. То есть профиль с файлом камеры мог отправить сайту зелёный прямоугольник на ноутбуке, чья веб-камера предлагает NV12, — не только артефакт измерения. 0080 пишет через внутрипроцессный доступ к буферу, как это делает собственное поддельное устройство Chrome. camera-prompt-probe.mjs --gate заваливает пустой кадр.

Чем должен быть «микрофон выключен» — измерено, тремя способами (B147, ISSUE-532)

До: с 0051/0075 микрофон НАЗВАН профильным, а его дорожка несёт звук хоста — пик RMS 0.32, писк 400 Гц поддельного микрофона Chrome, который стоит вместо хостового (core/scripts/mic-probe.mjs). Сайт, который записывает звук, получает комнату. Измерен каждый способ выключить микрофон, рядом с выбором камеры, чтобы был виден связанный запрос (core/scripts/camera-variants-probe.mjs --set mic):

личность… {audio} звук {audio, video} {video}
не заявляет НИ микрофона, ни камеры (audioinput: 0, videoinput: 0) NotFoundError — NotFoundError NotFoundError
не заявляет микрофона, камера с файлом NotFoundError — NotFoundError работает, файл
заявляет микрофон, камеры нет (как оставил A190) работает хоста (0.325, писк) NotFoundError NotFoundError
заявляет один и отказывает ему (припаркованный 0079, измерено в B146) NotReadableError — NotReadableError —
заявляет один и играет файл (вместо него --use-file-for-fake-audio-capture у Chrome — файла микрофона у ядра нет) работает файла (230 Гц) (камеры нет) —

При любом способе «выключено» enumerateDevices() согласуется с ответом и до разрешения, и после, и ни один запрос не просит устройства, которого у профиля нет.

Выбрано: личность не заявляет микрофона (media.devices.audioinput: 0) — машина без него (настольная без гарнитуры), на всех дорогах, и Chrome отвечает ровно так, как отвечает на такой машине. Изменения ядра для этого не нужно: 0051 уже скрывает вид, чьё число 0, и отказывает его захвату. Цена, прямо: сайт, которому нужен микрофон (звонок, проверка голосом), видит компьютер без него; ЛЮБОЙ запрос, который просит звук — один или с картинкой, — получает NotFoundError, поэтому при выбранном файле камеры сайт, спрашивающий {audio, video}, не получает ничего (ему придётся просить одну картинку); при обоих выключенных профиль — машина вообще без устройств захвата, какой личность-НОУТБУК бывает редко. Не выбрано: отказ (микрофон, вечно занятый, страннее отсутствующего, и он так же валит связанный запрос — припаркованная ветка wip/b146-0079-microphone-parked остаётся припаркованной); файл микрофона (целый механизм вроде 0067–0070 в службе звука, а один звук на все профили связал бы их). Кто пишет: control-plane — поле относится к составу устройств, как и у камеры.

B150 прочитал эту таблицу ещё раз, и ответ ПОМЕНЯЛСЯ по той же причине, что и у камеры — машина без микрофона обедняет отпечаток, и разворот владельца про камеру («так для отпечатка нужно ж») слово в слово применим к микрофону. Строка, которая уходит, — пятая, и B147 не мог её измерить, потому что её не существовало:

личность… {audio} звук {audio, video} {video}
заявляет микрофон и даёт ему СВОЮ комнату (0084, по умолчанию для любого засеянного профиля) работает собственный профиля, нарисованный из canvas.seed под доменом "microphone" работает работает

Измерено до и после (core/scripts/mic-probe.mjs, четыре ноги; предел, сказанный прямо: у этой машины микрофона нет, поэтому ни с какой записью настоящей комнаты ничего не сравнивается). До: нога хоста и нога личности совпадали до десятой доли децибела — RMS среднее 0.035/0.035, пик 0.323/0.323, самая сильная полоса 400 Гц у обеих, спектр из восьми полос совпадал полоса в полосу, а пол был РОВНО 0 у обеих семь десятых окна. После (четыре прогона): пол личности 0.0011..0.0014, 71..87 % от её собственного среднего, против 0 % у ноги хоста; форма её спектра отличается от хостовой на 11.3..13.4 дБ, а самая сильная полоса — её собственный резонанс; тот же профиль на втором старте уходит на 0.3..1.2 дБ; профиль с перевёрнутым семенем отличается на 8.2..8.8 дБ.

И одно число, которое спросило только независимое чтение выгрузки. Звук лежит на АБСОЛЮТНОЙ временной оси, а не на счётчике от старта: две записи одного профиля совпадают на 0.0 % сырых отсчётов после выравнивания, тогда как собственное поддельное устройство upstream по-прежнему совпадает на 100 % при сдвиге 0. Первая версия перезапускала индекс при каждом открытии и сама намеряла 100 % — две побайтно одинаковые записи, каких не даёт ни один микрофон и какие страница проверяет двумя строками. Камера (0083) нарочно по-прежнему проигрывает свою картинку при каждом старте; может ли картинка одного профиля меняться между сессиями — решать владельцу, и это названо в ISSUE-532, а не решено здесь.

Для чего теперь четыре прежние строки: не заявляет микрофона — настольная машина без гарнитуры, и сайт, просящий звук, получает NotFoundError — обычное дело, но это валит и {audio, video} целиком; заявляет один и отказывает ему остаётся припаркованным 0079; заявляет один и играет файл отброшено (один звук на все профили связал бы их, а нарисованная комната ровно этого и избегает); заявляет один и несёт звук хоста — то, что делал каждый профиль до 0084.

Цена, и принимать её или нет — решение владельца: нарисованная комната — это КОМНАТА, а не голос. Сайт, который СЛУШАЕТ — звонок, проверка голосом, — слышит тихую пустую комнату, и пути назад к микрофону машины сегодня нет: полем было бы media.audio.realMicrophone (зеркало media.video.realCamera), которого в этом контракте нет. Половина ядра — одна строка; половина продукта — одно поле. Тоже измерено, и чинить это не 0084: ОТКРЫТАЯ дорожка по-прежнему сообщает метку хоста («Fake Default Audio Input»), пока список устройств страницы показывает микрофон профиля, — виртуальная запись «default» из 0051 сохраняет имя хоста вместо устройства, которое называет, и страница сравнивает эти два двумя строками (ISSUE-532).

Чем должна быть «камера выключена» — измерено, тремя способами (B146, ISSUE-532)

core/scripts/camera-variants-probe.mjs, поддельные устройства Chrome вместо хостовых:

личность… enumerateDevices до / после запрос разрешения для {audio, video} {audio, video} {audio} {video}
НЕ заявляет камеры (media.devices.videoinput: 0) камеры нет / камеры нет камеры в нём нет NotFoundError сразу работает NotFoundError
заявляет одну, отказывает ей (realCamera: false, 0078) 1 / HD Pro Webcam C920 камера, с предпросмотром, который не начинается NotReadableError работает NotReadableError
заявляет одну, играет файл (0067–0070, 0080) 1 / HD Pro Webcam C920 камера, файл в предпросмотре работает работает работает

B146 прочитал эту таблицу как «не заявлять камеры», и это БОЛЬШЕ НЕ ответ — владелец развернул это тем же вечером («так для отпечатка нужно ж»: камера — часть отпечатка, и машина без неё его обедняет). Форма, как она стоит, по одному ответу на вопрос: профиль заявляет камеру, которую дал его розыгрыш (её можно и убрать), настоящая камера выключена (realCamera: false), а без выбранного файла играет собственная нарисованная картинка профиля (0083 выше) — поэтому уходит четвёртая строка этой таблицы, которую B146 не мог измерить, потому что её не существовало:

личность… enumerateDevices запрос разрешения {audio, video} {audio} {video}
заявляет одну, играет свою нарисованную картинку (0083, по умолчанию) 1 / HD Pro Webcam C920 камера, картинка в предпросмотре работает работает работает

Для чего теперь остальные три строки: не заявляет камеры — как выглядит профиль, у которого человек камеру убрал: обычно для настольной машины, реже для ноутбука, и сайт, просящий звук и картинку одним запросом, не получает тогда ни того, ни другого; заявляет одну и отказывает ей — пол с отказом по умолчанию, до которого доходит только конфиг без canvas.seed; заявляет одну и играет файл — то, что человек получает, как только его загрузит (дорога 0067–0070, без изменений). Кнопка камеры по-прежнему называет профиль, у которого камеру убрали: «Камера профиля: нет — в профиле нет камеры» (0082).

2l. hardware.gpu.deviceId — PCI device id карты (A166, ISSUE-381; половина ядра — B128)

"deviceId": "0x00002503" — 0x и восемь шестнадцатеричных цифр в верхнем регистре, тот id, который несёт в (0x…) собственная строка рендерера карты (ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 (0x00002503) Direct3D11 …)). Необязательное и добавочное: отсутствует у карты, чей рендерер не называет id (строка Metal у Mac), и в каждом конфиге, написанном до появления поля.

  • Ставится ВМЕСТЕ с картой, никогда отдельной рукой. Каждое место, которое ставит карту, ставит её id из рендерера (device_id_in): сборщик пресетов и генератор (catalog::build), розыгрыш из пула для пресета (draw_preset_card_vendor), собственная карта этой машины (host.rs). У ПРИНЕСЁННОГО конфига id выводится из его рендерера заново перед проверкой (refresh_device_id), потому что редактор пишет изготовителя и рендерер выбранной карты поверх сгенерированного конфига, чей id был от заменённой карты, — по той же причине, по которой там заново выводится доступный прямоугольник. snapshot() заполняет отсутствующий; ЗАПУСК заполняет его для записи, написанной до появления поля, — только в конфиге запуска (саму запись не переписывают — ISSUE-344).
  • Правило (C2, изготовитель ⇄ device id ⇄ модель): C2_GPU_DEVICE_ID_MALFORMED (не 0x + 8 hex); C2_GPU_DEVICE_ID_MISMATCH (не тот id, который называет рендерер, — рендерер И ЕСТЬ заявление о модели, — или рендерер, который не называет никакого); C2_GPU_DEVICE_ID_VENDOR (открытые таблицы — gpu_open, pci.ids; gpu_pool, опрос — говорят, что id принадлежит другому изготовителю, чем заявляет hardware.gpu.vendor; id, которого не знает ни одна таблица, там не судится). Каждый отказ несёт оба значения (Issue.values, A165). До них доходит запись, которая никогда не проходила через создание: файл, другая сборка, правка руками.
  • Сегодня страница видит строку рендерера (UNMASKED_RENDERER_WEBGL, K51/K57); это поле — чтобы ядро читало id, не разбирая ту строку. Должен ли нести его adapter.info.device у WebGPU — вопрос B128 и K57 (сток оставляет пустым).
  • Измерено вживую (A166, ядро s60): конфиг, несущий поле, запускается и применяет свою личность — 2 из 2.

2m. fonts.dir — папка с ФАЙЛАМИ шрифтов, из которой рисует профиль (A168, ISSUE-380; половина ядра — патч 0073)

  • Что это. Абсолютная папка, без ... Патч 0073 просматривает её один раз при старте браузера (.ttf/.otf/.ttc/.otc, рекурсивно, ≤ 8192) и добавляет её файлы к системным шрифтам на каждой дороге, которой страница доходит до шрифтов (поиск семейства в CSS, local(), queryLocalFonts). ВИДИТ ли страница семейство оттуда — по-прежнему дело presentFamilies (патч 0023), ровно как для установленного шрифта. Отсутствует — каждая дорога стоковая.
  • Кто пишет: демон, при запуске, никогда не профиль. Это место на ЭТОЙ машине — установленный модуль шрифтов (modules/fonts, см. ниже), — поэтому запуск ставит её, когда профиль заявляет одно из семейств модуля и модуль здесь, и очищает иначе; значение принесённого конфига выбрасывается при создании. Правило FONTS_DIR_MALFORMED отказывает значению, которое ядро выбросило бы (относительное, .., больше 1 024 байт).
  • Модуль. Свободные семейства со своими файлами — SIL OFL 1.1 / Apache 2.0, файлы без изменений, текст лицензии рядом с каждым семейством, ничего проприетарного, — взяты из github.com/google/fonts на одном закреплённом коммите (modules/fonts/fetch.py), детерминированно упакованы в vflin-fonts-<version>.zip (modules/fonts/package.py), раздаются лентой под modules/fonts/ и ставятся САМИМ ДЕМОНОМ при его старте (A192 — скачивается один раз в фоне из места рядом с источником ядра, предыдущая версия потом удаляется; module.ensure {id: "fonts"} делает то же руками): архив сверяется с хешем, с которым собрана эта сборка (font_module::ARCHIVE_SHA256), каждый файл — со своим sha256 из манифеста, и ничего, кроме файлов шрифтов, текстов лицензий и манифеста, не принимается. Версия 0001: 33 семейства, 142 файла, 45.0 MB шрифтов, архив 22.1 MB (DM Sans и Merriweather оставлены за бортом по измерению: из-за оси оптического размера DirectWrite называет их иначе). Версия 0002 (A191): 117 семейств, 386 файлов, 93.4 MB шрифтов, архив 44.1 MB — ещё шесть отсеяны четырьмя дорогами ядра и названы в modules/fonts/families.json. Переопределение для разработчика: VFLIN_FONTS_SRC.
  • Какие семейства несёт профиль (font_catalog::module_families): личность Windows рисует их у USER_FONTS_PCT = 35 % личностей, по 1..=6 штук (меньше — чаще), каждое с весом по популярности — ASSUMED: K52 измерил одну машину с Windows с 3 семействами Google Fonts, поставленными руками, а сколько машин несёт хоть сколько-нибудь, не публикует никто. Они идут в fonts.additions, поэтому обновление переписи при запуске их сохраняет. Ни одно не является признаком другой ОС на Windows — ни DejaVu, ни Liberation, ни Ubuntu, Arimo, Cousine, Tinos, Chilanka, Jomolhari, Noto Color Emoji, Droid Sans Mono, Dancing Script или Roboto (наша /lab/ и CreepJS читают их как Linux или Android; собственное измерение 0073 показало предупреждение /lab/ fonts.foreign:DejaVu Sans). Личности macOS и Linux не рисуют ни одного.
  • Набор Linux — второй модуль, fonts-linux (A185, ISSUE-509, из B141). Шрифты, которые ставит настольная Ubuntu 22.04, как файлы: 247 файлов из 73 пакетов, 172.4 MB (164.4 MiB), 142 семейства, архив vflin-fonts-linux-0001.zip на 117.2 MB — каждая лицензия свободная, каждый файл рядом с copyright своего пакета (modules/fonts-linux/ (internal: modules/fonts-linux/README.md)). B141 измерил, почему: личность Linux со старыми десятью заявленными семействами показывала у pixelscan 7 из его 97 базовых семейств Linux («Masking detected»); с этими файлами как fonts.dir — 96 из 97 и «No masking detected», без изменения ядра, рисует 0073. Отсюда: ubuntu-default ЕСТЬ 142 семейства модуля (linux_font_module::FAMILY_NAMES, сгенерированы вместе с архивом) плюс семь общепринятых веб-имён; запуск личности LINUX ставит fonts.dir в дерево fonts/ установленного модуля, и никто другой её не получает никогда — цена B141 составляет +0.46 с один раз и +60 мс на каждый новый процесс сайта, а профиль Windows с этой папкой платил бы ~40 мс на процесс за семейства, которые он скрывает. Доставка устроена как у кошелька и у ядра, а не через установщик: первый старт профиля Linux на машине без набора идёт БЕЗ него (сказано в журнале) и один раз просит его, в фоне, из места рядом с собственным источником ядра (…/core → …/modules/fonts-linux); руками — module.ensure {id: "fonts-linux", source}. Проверяется как модуль свободных семейств — вкомпилированный sha256, затем каждый файл по манифесту, и ничего, кроме файлов шрифтов, copyright/<package> и манифеста, не принимается. Переопределение для разработчика: VFLIN_FONTS_LINUX_SRC. Измерено от начала до конца на настоящем архиве (a_linux_launch_fetches_the_real_set_once): первый запуск попросил один раз, набор был на месте через 14 с, следующий запуск его назвал.

2n. fonts.monospaceFamilies — какие из заявленных семейств моноширинные (A171; половина ядра — патч 0074)

  • Что это. Список имён семейств, каждое из presentFamilies: заявленные семейства, которые МОНОШИРИННЫЕ; любое другое заявленное семейство — пропорциональное. Список — даже пустой — делает шаг известным ядру, и тогда оно даёт подставленному семейству (личности другой ОС, чем хост) начертание своего шага: без него личность macOS на хосте Windows рисовала Menlo пропорциональным, а Helvetica Neue моноширинным (B131: 2 и 4 из 7 неверно на двух семенах, 0 с полем). Отсутствует — пул подстановки работает ровно как раньше. См. FP_CONFIG_INJECTION.md §0074.
  • Кто пишет: демон, при каждом запуске, никогда не профиль. Выводится из presentFamilies и vflin_fingerprint::fonts::MONOSPACE_FAMILIES (monospace_of), поэтому обновление переписи или добавленное семейство подхватываются без сохранённой копии, которая успела бы устареть; значение принесённого конфига выбрасывается при создании, как fonts.dir.
  • Что знает таблица и откуда. Измерено как фиксированный шаг на этом хосте с Windows собственной пробой ядра (Consolas, Courier New, Lucida Console, Cascadia Code/Mono и Inconsolata, IBM Plex Mono, Source Code Pro из модуля шрифтов) и моноширинное по построению в списках Apple и Linux (Menlo, Monaco — те два, которыми мерил B131, — Courier, Andale Mono, PT Mono, DejaVu Sans Mono, Liberation Mono, Ubuntu Mono). Семейство не из таблицы не заявляется никак: ядро читает его пропорциональным, каким и является каждое семейство каталога вне таблицы.

2o. network.localNetworkAccess — страница не может просканировать порты этой машины (патч 0076, B143, ISSUE-519)

СТРОКА. "block" — профиль по умолчанию отвечает «Don’t allow» на СОБСТВЕННОЕ разрешение Chrome на доступ к локальной сети для каждого сайта: три умолчания, которыми спрашивает Chrome 155 (local_network_access и расщеплённая пара local_network — частная сеть — и loopback_network — эта машина), стоят в BLOCK в командно-строчном хранилище настроек Chrome на этот запуск, в том же слое, которым пользуется Do Not Track из §2i; в Preferences профиля ничего не пишется. Отсутствует или любое другое значение — сток: Chrome спрашивает.

Почему это важно, измерено (core/scripts/port-scan-probe.mjs: страница, которую Chromium считает ПУБЛИЧНОЙ через --ip-address-space-overrides, прощупывает fetch/WebSocket/<img>/<iframe> по три круга каждым, против отвечающего порта HTTP, молчащего порта TCP, освобождённого порта и 15 серверных портов BrowserScan). Пока Chrome СПРАШИВАЕТ, запрос к открытому петлевому порту ждёт ответа на запрос разрешения, а к закрытому падает за 1–3 мс — эта разница и есть сканирование. Сток Chrome 155 и наше ядро без поля: страница находит 3 из 17 — ровно те три, что открыты (два у пробы и 445 у Windows). С "block": 0 из 17 — открытый порт падает так же быстро, как закрытый, любым способом, и никакого запроса разрешения не показывается. Дорога WebSocket судится по исходу, а не по времени: Chromium задерживает каждый падающий сокет на 1–5 с × 2^min(pending + failed, 16) / 65536 (services/network/websocket_throttler.cc), одинаково для заблокированного открытого и для закрытого.

B144 перемерил со СОБСТВЕННЫМИ портами продукта в списке (loopback-surface.mjs --hold: демон из HEAD на собственном домашнем каталоге, запущенный профиль, прикреплённая дверь — продукт всегда слушает порт TCP своего Local API, а порт двери, пока она прикреплена; ядро — только UDP): без поля страница находит 5 из 10 — два у пробы, Local API, 445 и 8080, ровно те открытые, которые Chrome даёт странице набрать (135 и 22 — в его списке запрещённых); с "block" — 0 из 10, каждая проба падает за 1 мс. Попутно починена сама проба: <img> или fetch, оставленные запросом разрешения в ожидании, оставались в очереди и заставляли каждую следующую картинку выходить по таймауту, поэтому три ЗАКРЫТЫХ порта читались открытыми одним только <img>, — теперь пробы по таймауту отменяются. А конфиг без browser.ua для ядра не конфиг вовсе («no usable browser.ua»), поэтому файл только из {network:{…}} меряет сток — первое «после» здесь делало ровно это.

Что продолжает работать — измерено B144 (core/scripts/lna-own-paths-probe.mjs --gate, собранное ядро, тот же конфиг с полем и без): сайт, доступный через выход SOCKS5 на 127.0.0.1, грузится — и документ, и подресурсы; привязка CDP, вызванная страницей, доходит до клиента как Runtime.bindingCalled (дорога Синхронизатора); проверка STUN у WebRTC приходит в точку network.udpRelay в кадрах (4 датаграммы, обе ноги — проверки LNA у WebRTC в 155 выключены); новая вкладка рисуется с полосой профиля (0039). Сайт, который человек разрешил в сведениях о странице, остаётся разрешённым (исключение сильнее умолчания — список исключений собственный у Chrome). QUIC через ретранслятор (0042) не перемерялся; он соединяется с литералом 198.18/15, публичным для этой проверки.

⚠ Поправка, B144 — B143 писал здесь, что «проксированные соединения unknown = публичны для проверки, поэтому выход SOCKS профиля на петле не затронут». ИЗМЕРЕНО: наоборот, и это решает, делает ли это поле в продукте хоть что-нибудь. Документ, пришедший через прокси на 127.0.0.1, для доступа к локальной сети НЕ публичен — его запрос к открытому петлевому порту грузится за 3–4 мс и с "block", и без него, без запроса разрешения, без отказа (сайт отдан как безопасное происхождение, каким и бывает детектор; страница по http:// отказывается сразу как InsecureLocalNetwork, что бы поле ни говорило). Каждый выход С ПАРОЛЕМ доходит до ядра как ProxyRelay демона на 127.0.0.1 (proxy_relay.rs, launch_stages.rs) — значит, для таких профилей поле сегодня бездействует, и страница отличает открытые порты этой машины от закрытых так же легко, как вовсе без защиты. Выход без пароля отдаётся ядру напрямую и публичен для проверки, поэтому там поле работает. Чинить это запускающей стороне, а не ядру: собственный --ip-address-space-overrides=127.0.0.1:<relay port>=public у Chromium объявляет порт TCP ретранслятора публичным — измерено: без поля локальный запрос тогда ждёт запроса разрешения (поведение стокового Chrome), с полем он отказывается за 3 мс (LocalNetworkAccessPermissionDenied), а выход, привязка, ретранслятор и стартовая страница по-прежнему работают (--exit-public, --gate PASS); официальная сборка M155 этот ключ тоже уважает. Это control-plane, рядом с записью самого поля (ISSUE-519).

И дверь CDP этим полем не защищена ни так, ни эдак. Chrome судит локальный запрос ПОСЛЕ того, как соединение TCP установлено, а дверь отдаётся первому клиенту TCP и закрывает свой слушатель (cdp_door.rs, A89). Измерено: одна проба публичной страницей прикреплённой, но ещё не занятой двери, с "block" — страница читает её закрытой, а автоматизации, которая затем набирает дверь, отказано за 29 мс. Без поля проба страницы держит дверь на время запроса разрешения и делает то же самое. Окно — от POST /profiles/{id}/cdp до набора клиентом; лечить это самой двери (тратить её на завершённое рукопожатие, а не на приём соединения) — control-plane, область cdp.

Чего оно НЕ меняет — красной строки BrowserScan. Его локальное сканирование открывает wss://localhost:<port> на 29 портов, по одному каждые 300 мс, и называет порт ОТКРЫТЫМ, когда сокет падает в пределах 1 с. На этой Windows закрытый петлевой порт падает за миллисекунды, поэтому его правило перечисляет первые ~15 портов как открытые, пока троттлинг WebSocket выше не замедлит остальные за 1 с: 15 из 29 «открыты» и на стоковом Chrome 155, и на нашем ядре, и на нашем ядре с "block", и все они закрыты. Его красное «Порт не закритий і вразливий для атак» показывается, когда непуст ЛИБО его локальный список, ЛИБО nmap его сервера по адресу ВЫХОДА (прокси, scanPorts). Ни то ни другое не является приговором браузеру.

Подтверждено B144 по собственному коду страницы и на живой странице. Компонент (прочитан 2026-10-19): Tt(port) = new WebSocket("wss://localhost:"+port), onerror в пределах 1000 мс → resolve (ОТКРЫТ), позже → reject, таймер 1200 мс → reject; итоговая строка — (local.length || remote.length) ? unprotected : protected; у каждой из двух кнопок («локальне сканування», «Дистанційне сканування») СВОЁ «Жоден порт ще не відкритий». То есть кадр владельца (одно «ни одного порта» и красная строка вместе) — это один список пуст, а другой нет. Вживую, наше ядро через тестовый выход, нажата локальная кнопка: перечислено 15 — 20, 21, 22, 23, 25, 53, 80, 110, 111, 135, 138, 139, 443, 445, 587, — из которых эта машина слушает два (135, 445); повтор того же правила на установленном стоковом Chrome 155.0.8059.82 — 16. Закрытый порт, который быстро отказывает, — это то, что правило называет открытым, поэтому красную строку блокировкой зелёной не сделать: заблокированный запрос тоже падает быстро; прошёл бы только браузер, у которого упавший сокет к localhost занимает больше секунды, а это признак сам по себе. Не то, ради чего стоит патчить.

Половина продукта — A188 (control-plane, 2026-10-19). Измерено через ПРОДУКТ (scripts/measure/a188-lna-door-leg.mjs: собственный демон на собственном домашнем каталоге, выход С ПАРОЛЕМ, чтобы ядро доходило до него через ProxyRelay, ядро out/M155, страница — сайт, доступный через этот выход; «до» — демон из HEAD на том же стенде):

  • Кто пишет: личность профиля, и значение сохраняется как есть. network.localNetworkAccess есть в модели (vflin_fingerprint::Network), поэтому конфиг, который его несёт, доходит до vflin-fp-config.json без изменений — раньше модель выбрасывала его по дороге внутрь. Отсутствие — сток. Любое значение, кроме "block", валидатор отказывает (NETWORK_LOCAL_ACCESS_UNKNOWN): ядро читает его как «не блокировать», так что хранить его значило бы заявлять то, чего движок не делает. Каталог его не пишет никогда. Переключатель оператора на profile.update ждёт круга, в котором контракт свободен (ProfileUpdate/ProfileDetail.localNetworkAccess, block | ask); до тех пор поле ставится вместе с личностью при profile.create.
  • Запускающая сторона объявляет порт петлевого прокси публичным. Рядом с --proxy-server прокси, чей хост — петлевой литерал с настоящим портом, получает --ip-address-space-overrides=<ip>:<port>=public (launch.rs); собственные переопределения оператора доклеиваются после него в один ключ (Chromium берёт первую подходящую запись). Удалённому выходу не нужно ничего.
  • И ретранслятор перестаёт отвечать страницам. Публичный порт — это порт, который странице можно набрать без спроса, а ретранслятор на такой запрос отвечал — через выход HTTP, с его учётными данными, — поэтому с блокировкой страница находила на машине ровно ОДИН открытый порт, ретрансляторный (load 7 ms рядом с error 6–7 ms). Теперь ретранслятор несёт только запрос клиента прокси (CONNECT, абсолютная форма http://, SOCKS5) и закрывает всё остальное без ответа, до того как наберёт выход (proxy_relay.rs).
страница через выход с паролем просит… до (HEAD), любая нога после, без поля после, "block"
открытый порт HTTP этой машины load 6–7 мс ждёт запроса разрешения error 7–8 мс
Local API демона load 6 мс ждёт запроса разрешения error 7–8 мс
собственный порт ретранслятора load 7–8 мс error 6 мс error 7 мс
закрытый порт error 5–8 мс error 6 мс error 7 мс
настоящий сайт HTTPS (example.com) через выход → открытый порт не измерено: страница уже потратила дверь ждёт 3 с error 4 мс
дверь CDP, сперва прощупанная страницей, затем набранная клиентом клиенту отказано за 1–5 мс клиент прикрепился, 6 мс клиент прикрепился, 8 мс

Строка про дверь — это ISSUE-537, закрытая в самой двери (cdp_door.rs): она тратится на первое ЗАВЕРШЁННОЕ обновление до WebSocket на своём пути, а не на первое соединение TCP, а обновление, несущее Origin веб-страницы, отказывается (403) без траты — собственное правило DevTools у Chromium с M111. Что делает ядро без 0076 (серия < 66): игнорирует поле, то есть сток; переопределение и правило ретранслятора держатся на любом ядре.

network.protectedPorts — СПИСОК портов вместо всей локальной сети (патч 0081, B146, ISSUE-519)

"network": {"protectedPorts": [3389, 5900, 7070, …]} — целые 1..65535 (всё прочее пропускается, повторы выбрасываются). Соединение TCP, которое страница — или что угодно в сетевой службе — устанавливает к НЕ публично маршрутизируемому адресу (эта машина, локальная сеть, link-local, CGNAT) на порт из списка, не устанавливается вовсе: ERR_CONNECTION_REFUSED сразу, до единого пакета — то, чем отвечает закрытый порт. Запрос, чьё назначение — такой ЛИТЕРАЛ IP, получает тот же отказ до выбора прокси, поэтому прокси-программа на этой машине, которая сама пересылает частные адреса, его не унесёт. Публичные адреса и порты не из списка не затронуты. Отсутствует или пуст — не защищено ничего.

Почему список — слово владельца, при чужих «Real / Protect» и «Ports to protect»: localNetworkAccess: "block" (0076) закрывает ВСЮ локальную сеть, а заодно режет собственные программы человека на localhost — мост кошелька, сервер разработчика, панель роутера, — и они не спрашивают, они просто падают. Сканер ищет программы УДАЛЁННОГО ДОСТУПА (RDP, VNC и его дисплеи, TeamViewer, AnyDesk…), чтобы назвать машину фермой; список прячет ровно их. 0076 остаётся как сильный режим.

Измерено (core/scripts/port-scan-probe.mjs --protect, страница публична через --ip-address-space-overrides; временный список — рамка конкурента плюс собственные открытые порты пробы): без поля страница находит 4 из 7 (два у пробы, AnyDesk на 7070, служба на 8080); с ним — 1 из 7, то есть 8080, которого в списке нет, и ведёт себя он как у стокового Chrome. Защищённый открытый порт читается как закрытый любым способом: fetch и <img> падают за 1 мс (закрытый порт — 1–3 мс), <iframe> грузится 15–16 мс (25), WebSocket падает под троттлингом Chrome. Наши собственные дороги с полем (lna-own-paths-probe.mjs --protect, порт выхода нарочно внесён в список): сайт через петлевой выход, привязка CDP, датаграммы ретранслятора WebRTC и стартовая страница — всё работает.

  • Собственному выходу профиля никогда не отказывают: браузер вынимает порт петлевого --proxy-server (ретранслятора демона) из списка перед тем, как передать его дальше, — порт ретранслятора случайный и может оказаться одним из перечисленных.
  • Не покрыто: ИМЯ хоста, которое локальный пересылающий прокси разрешает в частный адрес (до прокси судятся только литералы); красная строка BrowserScan (его правило, выше, называет закрытый порт открытым).
  • СОБСТВЕННЫЕ ПОРТЫ ПРОДУКТА ДОБАВЛЯЕТ ЗАПУСК, А НЕ ЛИЧНОСТЬ (A195, ISSUE-542; утечку намерил K79: со страницы внутри профиля порт Local API демона висел 3 с там, где закрытый порт отказывает за 1–6 мс, а найденный наш порт — признак продукта). Демон кладёт два числа в этот список в конфиг запуска — порт собственного Local API (local-api.json) и порт, зарезервированный для двери CDP этого профиля, — а сохранённая личность сохраняет список оператора ровно таким, каким он был: порты случайны и меняются каждый старт, а личность меняться не должна. Пустой список по-прежнему означает «не защищать ничего» про порты МАШИНЫ; наши добавляются независимо от него, потому что это собственные двери продукта. crates/vflin-localapi/src/our_ports.rs несёт рассуждение и таблицу каждого слушателя, который открывает демон: порт TCP ретранслятора в списке быть не может (он И ЕСТЬ выход профиля, и ядро всё равно его вынимает), точка UDP ретранслятора механизмом не покрыта, а мост совместимости — это другая программа на порту, который называет человек. Измерено через продукт (scripts/measure/a195-own-ports.mjs, собранное ядро, выход с паролем, чтобы страница была публичной): запись держит 16 портов по умолчанию, конфиг запуска и собственный --vflin-protected-ports сетевой службы держат эти 16 плюс порт демона и зарезервированный порт двери; со страницы управляющий порт этой машины, который ничем не защищён, выходит по таймауту на 3001–3016 мс, тогда как наш порт Local API отвечает error за 5–6 мс — собственные 5–6 мс закрытого порта, — а порт двери error за 1 мс через fetch и 1–2 мс через WebSocket. Наш собственный клиент, который набирает СНАРУЖИ ядра, куда правило не достаёт, прикрепился через ту же дверь за 21–24 мс и вычислил 6*7 = 42 на странице профиля. Порт двери попадает в список ДО того, как дверь появится, потому что ключ фиксируется при старте процесса: демон берёт номер при запуске (привязанным, но не слушающим — соединение с ним отказывается ровно как с закрытым портом) и отдаёт двери этот сокет, когда её выдают, так что вторая дверь ложится на тот же защищённый номер.
  • 445 (SMB) НАРОЧНО не входит в список по умолчанию (A195, решение, которого бриф просил словами): это не порт программы удалённого доступа, это обычная Windows, и в собственном списке запрещённых портов у Chrome есть 137 и 139, но не 445 — страница, которая прощупает его на нормальной машине, найдёт его открытым. Отказ по нему не сделал бы профиль похожим на нормальную машину, он сделал бы его похожим на более редкую. Оператор может его внести; умолчание аномалии не выдумывает.
  • Список не принадлежит ядру. Что есть каждый порт по документации его собственной службы (ADR-0035) — это работа K75; поле в профиле и его умолчание — A190. K75 измерил: AnyDesk слушает 7070, а RustDesk — 21118 на этой машине; в списке конкурента 6568 и 5938 — ИСХОДЯЩИЕ порты по собственной документации AnyDesk и TeamViewer (на них никто не слушает), а Radmin (4899) и RustDesk (21118) отсутствуют.

2p. hardware.memoryGb — заголовки запросов Device-Memory тоже следуют за ним (патч 0077, B143, ISSUE-522)

navigator.deviceMemory следует за hardware.memoryGb с 0010 (геттер: собственное округление Blink — ближайшая степень двойки, при равенстве вниз — и настольный зажим 2..32; 64 и 128 читаются как 32, ровно как у стокового Chrome на машине со 128 GB). Заголовки запросов Sec-CH-Device-Memory / Device-Memory за ним НЕ следовали, ни на одной из двух своих дорог: заголовки НАВИГАЦИИ делает процесс браузера (content/browser/client_hints/client_hints.cc), а собственные подзапросы страницы — рендерер (FrameFetchContext), и оба читают blink::ApproximatedDeviceMemory, размеренный по машине, — поэтому сайт, спросивший подсказку, читал корзину машины рядом с профильной (измерено на хосте со 128 GB: заголовок 32, геттер 4). Патч 0077 ставит это значение из личности в обоих процессах: в браузере — в конструкторе ChromeContentBrowserClient до любого запроса, в рендерере — когда приходит его конфиг (WebVflinFpConfig::Set); без поля остаётся значение машины. Измерено между половинами: только с браузерной навигация 4, а fetch 32. core/scripts/device-memory-probe.mjs --gate: при каждом заявлении оба заголовка = геттер = корзина заявления. performance.memory.jsHeapSizeLimit остаётся V8-овским, а его V8 размеряет по машине (здесь 4.09 GiB, одинаково при любом заявлении); читает ли настоящая машина МАЛЕНЬКОГО заявленного размера (2, 4 GB) меньший предел, здесь не измерялось — названо как следующее, что стоит измерить, а не починено.

B144, перемерено на собранном ядре и прочитано в исходниках V8. До (M155-official, без 0077): геттер 2/4/8/16/32/32 при заявлениях 2/4/8/16/32/64, оба заголовка каждый раз 32. После: оба заголовка = геттер = 2/4/8/16/32/32 (--gate PASS, пять различимых значений для пяти корзин, которые выбрал владелец). Предел кучи отвечает на открытый вопрос собственным правилом V8 (v8/src/heap/heap.cc, OldGenerationSizeFromPhysicalMemory: физическая ÷ 2 на 64-битном настольном, зажата 256 MB…4 GB): машина от 8 GB и выше читает те же 4.09 GiB, что мы показываем, поэтому заявления 8/16/32 согласованы; у настоящей машины на 4 GB примерно половина этого, а на 2 GB — примерно четверть, то есть заявления 2 и 4 противоречат jsHeapSizeLimit — по исходникам, а не по измерению на такой машине. Розыгрыш (сосредоточен между 8 и 32, A187) туда не попадает; выбранные руками 2 или 4 — попадают. Названо в ISSUE-522, в этом круге не починено.

2q. Что страница видит из шрифтов, ступеней памяти и блокировки портов (A189)

  • Шрифты, которые видит страница, — это не длина presentFamilies. Патч 0023 даёт странице видеть только заявленные семейства, а заявленное семейство, которого у этой машины нет, не рисуется (B129). Личность Windows на машине с Windows заявляет собственные семейства этой машины для своей ОС: profile.create заменяет список переписью машины (fonts::census_for_claim) и кладёт сверху собственный набор личности (A134). С A189 fingerprint.generate делает то же самое, поэтому черновик, который показывает форма, — тот самый список, который сохранит профиль. Раньше черновик нёс манифест ОС из 21 семейства, а сохранённый профиль — перепись: окно читало 21, а страница видела собственные машины (K73: здесь 66 по ширине, у голого Chrome 67). PageFonts (fingerprint.validate, profile.get) называет число для человека: sees — заявленные семейства, которые у этой машины есть или файл которых приносит установленный здесь модуль шрифтов, — declared и notOnThisMachine. Собственный набор личности может заявлять семейства, которых у машины нет (набор письменностей Windows, семейство модуля до того, как модуль скачан): они в declared, но не в sees.
  • fonts.census и fonts.censusMachine — какая это перепись и на каком компьютере (A117; A194, ISSUE-533). census — ключ переписи: список кандидатов и ядро, которое его мерило (fonts::census_key), а НЕ машина. Старт сравнивает его с текущим у машины и, когда они разошлись (после обновления ядра), берёт текущую перепись с собственными дельтами личности сверху. С A194 — только на том компьютере, на котором список снимали: censusMachine — метка того компьютера, 16 шестнадцатеричных цифр sha256 от device_id этого домашнего каталога и MachineGuid установленной Windows (host_census::machine_mark), так что скопированный домашний каталог на другом компьютере — тоже другой компьютер; ни одного из двух id в ней нет. На другом компьютере список остаётся как есть, profile.get говорит fontsFromAnotherMachine, страница видит ту часть списка, которая у этого компьютера есть (pageFonts), а profile.fingerprint.regenerate {fields: [fonts]} (с подтверждением) берёт перепись этого компьютера. До A194 перепись того компьютера бралась молча — изменение личности, о котором никому не говорили. Метка хранится с профилем и выбрасывается из конфига запуска; запись без неё (а это каждая прежняя) получает метку при первом старте под этой сборкой, поэтому профиль, который никуда не переезжал, ведёт себя ровно как раньше (ISSUE-344: необязательное, читается побайтно).
  • Гейт концентрации по наборам шрифтов (concentration::FONT_SET_CEILING_PCT, треть класса): ни один набор шрифтов, который делает КАТАЛОГ, не стоит больше чем на этой доле. Самый частый каталожный набор Windows — на 31 % (голый манифест; остальное разводит A134); оба класса macOS и Linux — на 100 % и НАЗВАНЫ (KNOWN_FONT_SETS_OVER). Набор, сделанный из переписи машины, этой чертой не меряется: он собственный у машины, один и тот же у всех её профилей, — путь B, то, что отдают странице Vision и AdsPower (K73), а K72 измерил, что одни шрифты посетителей не сливают.
  • Память (Catalog.memory): пять ступеней, которые может увидеть страница (2, 4, 8, 16, 32), и, для каждого размера, который принимает C8, то, что читает страница (validate::device_memory_a_page_reads: ближайшая степень двойки, при равенстве вниз, зажата в 2..32 — 24 читается как 16, 64 и 128 как 32).
  • Блокировка портов — это profile.update {localNetworkAccess: block | ask} (поле §2o; ask её снимает). Для выхода с паролем демон объявляет публичным СВОЙ ретранслятор, первым среди дополнительных аргументов запуска (launch_extra_args); прокси оператора — socks5://127.0.0.1:1080 — не объявляется никогда (ISSUE-538).

2r. Что несёт НОВАЯ личность и наборы шрифтов (A190, решения владельца от 2026-10-19)

Каждая дорога, которая делает личность — пресет, принесённый конфиг, захват хоста, ничего не названо (генерация) и черновик формы, — кончается в apply_new_identity_defaults, ПОСЛЕ того как разыграны её устройства. Каждое умолчание — только там, где личность ещё ничего не говорит; профиль, сделанный раньше, сохраняет, что имел (отсутствие остаётся стоком, ISSUE-344):

  • identity.passkeys: profile (A136);
  • network.protectedPorts = список удалённого доступа (catalog::PROTECTED_PORTS_DEFAULT, 16 портов, каждый со своей службой и происхождением из K75; читает патч 0081). [] — это «не защищать ничего» от оператора, и список сохраняется. localNetworkAccess: block (0076, вся локальная сеть) — сильный режим, а не умолчание; ask сохраняется как слово;
  • камера заявлена, и картинки с машинной не идёт: профиль, чьи устройства разыграл демон, ВСЕГДА заявляет камеру — ту, что выпала, или одну, если розыгрыш не дал ни одной (собственную машины, если она есть, иначе веб-камеру своей ОС); список устройств, присланный вызывающим (profile.create {fingerprint} — редактор устройств формы), сохраняется как сказано (ISSUE-351). A190 сделал так, что камера не заявляется вовсе (media.devices.videoinput: 0, форма, которую мерил B146), и владелец развернул это тем же вечером (A191: «для отпечатка нужно», «у Vision камера во всех профилях заявлена по умолчанию, но можно убрать»). Она получает media.video.realCamera: false, и без выбранного файла ядро играет собственную нарисованную картинку профиля (0083, §2k) — отказ (0078, как у занятой камеры) достаётся только конфигу без canvas.seed. realCamera: true сохраняется как слово.
  • Файл камеры приносит свою камеру (A191): выбор файла (profile.update {camera} или собственное окно профиля, перенесённое при запечатывании) даёт профилю, чей розыгрыш камеры не заявил (настольная машина без веб-камеры), ОДНУ — собственную машины, если она есть, иначе веб-камеру своей ОС, — чтобы файлу было через что играть; очистка файла оставляет камеру заявленной (и при realCamera: false она снова играет собственную нарисованную картинку профиля, 0083).
  • Имя камеры (A191, раздел камеры у профиля — «какая камера»): profile.update {cameraName} называет камеру, которую заявляет профиль, из тех, что есть в каталоге для его ОС (fingerprint.mediaDevices), встроенную — только на её собственной машине; cameraName: "" убирает камеру («можно убрать» — videoinput: 0, NotFoundError), и это отказывается для MacBook и iMac и пока выбран файл; profile.get читает обратно имя и cameras, число. Что камера ИГРАЕТ — это файл, а без него собственная нарисованная картинка профиля (0083). Поля для нарисованной картинки не нужно (A192): её семя — canvas.seed, её временная ось — ядра; четыре выбора владельца (по умолчанию картинка профиля, файл, камера машины, камеры нет) — это realCamera, camera и cameraName: "". Режимы, которые читает страница, следуют за именем (A192, media.video.modes, §2k).
  • Микрофон (A191, форма, которую мерил B147): profile.update {microphone: false} не заявляет ни одного (media.devices.audioinput: 0, машина без него — NotFoundError, патч 0051 скрывает вид); true заявляет один. Машине, у которой собственный микрофон есть всегда (MacBook, iMac, ноутбук Windows), в false отказывают. Умолчания нет: новый профиль сохраняет микрофоны, которые дал его розыгрыш, — владелец решил про камеру, а не про микрофон.

Наборы шрифтов (font_catalog::FONT_KITS): свободные семейства модуля приходят на профиль НАБОРАМИ — офисный, дизайнерский, разработческий, творческий, рукописный, кириллический — по роду машины (офисная или игровая, ноутбук или настольная: класс и карта) и языку, каждый набор — устойчивый розыгрыш на id личности, а из набора число семейств — по тому, как часто каждое ставят. Измерено на 33 семействах модуля 0001: примерно половина профилей Windows получает хоть что-то (две трети при кириллическом языке), в среднем 1.5–1.65 семейства, до 16; самый частый каталожный набор падает с 31 % до 22 % класса (потолок гейта наборов — треть). Доли ASSUMED — см. комментарий в документе. Модуль 0002 (A191, собран — FONT_PACK_CANDIDATES.md (internal: docs/30-core/FONT_PACK_CANDIDATES.md)): 117 семейств, добавлен рукописный набор, наборы шире; измерено: примерно половина профилей Windows получает хоть что-то (семь из десяти при кириллическом языке), в среднем 2.0–2.2 семейства (3.0–3.3 с кириллицей), до 20–23; самый частый каталожный набор — 21 % класса; самое частое семейство падает с 14 % до 11–12 % (Montserrat на игровой настольной). Каждое из 117 рисуется под собственным именем на дорогах ширины, канвы и перечисления (core/scripts/font-set-roads.mjs на ядре 155, личность Windows заявляет все 117: W 117/117, M 117/117, queryLocalFonts 117/117, ни одного начертания чужого семейства под именем семейства модуля); шесть скачанных семейств тем прогоном отсеяны — DirectWrite называет их иначе (modules/fonts/families.json, left_out_by_measurement).

B192 — около десяти на КАЖДОМ профиле Windows (владелец, 2026-11-07: «Лучше пускай будет на каждый профиль рандомные шрифты. Лучше их там будет 10 штук… совсем немножко добавляется, то ли 3, то ли 4»). Его счёт был верен: до этого новая личность Windows en-US в 48 % случаев не вытягивала ни одного семейства из наборов, а в остальных — в среднем 3.8 (2.0 в целом, a190_font_numbers, 10 000 вытягиваний). Теперь каждая личность Windows вытягивает font_catalog::HAND_INSTALLED = от 7 до 13, чаще всего десять (hand_installed_count, собственное стабильное вытягивание), НАБОР ЗА НАБОРОМ: наборы, которые допускают её вид и язык, в порядке, вытянутом по их весу для вида (FontKit::pct — теперь вес, а не доля), и каждый даёт до своего max, по тому, как часто каждое семейство установлено, — так что десять идут вместе. Ворота every_windows_identity_draws_about_ten_and_no_two_share_them, 1 000 личностей на каждый вид и язык: среднее 9.95, 1 000 разных наборов из 1 000, ни одного семейства на всех, самое частое — на 34–46 % (Open Sans на офисных машинах, Bebas Neue на игровых, PT Serif с русским). Три слоя, как их назвал владелец: слой ОС (перепись этой машины, только семейства, которые ставит ОС), слой языка (Feature on Demand письменности, WithLanguage) и этот. ПРЕДПОЛОЖЕНО: число — владельца; на двух когда-либо измеренных машинах их три. Шрифты, которые ставят Office, Adobe или игры, — не набор: их файлы лицензированы, модуль не может их нести, а семейства, которое движок не может нарисовать, нет для любой страницы. Профиль, сделанный раньше, держит свой список (история рассказана один раз, A170).

2s. Карта личности Linux и классы Linux (A199, ISSUE-527 — «у нас нету каталога Linux тоже»)

Что читает страница на Linux. Chrome на Linux гоняет WebGL через бэкенд OpenGL у ANGLE (Vulkan по умолчанию выключен), а ANGLE строит рендерер из строк GL драйвера: ANGLE (<GL_VENDOR>, <GL_RENDERER>, OpenGL <first word of GL_VERSION>), запятые убраны, строка AMD обрезана перед , DRM (так что до страницы не доходит ни ядро, ни DRM), и никакого PCI id — токен (0x…) принадлежит D3D. Изготовитель — Google Inc. (<GL_VENDOR>). Прочитано в исходниках Chromium 155 этого дерева (libANGLE/Context.cpp, renderer/gl/DisplayGL.cpp и его юнит-тесты, libANGLE/Display.cpp).

На системе, которую заявляет личность (Ubuntu 22.04, Mesa 23.2.1): Intel — Mesa Intel(R) <name> (<codename>), изготовитель Intel, версия 4.6; AMD — <marketing name> (<chip>, LLVM 15.0.7, …) без префикса radeonsi,, который добавила Mesa 23.3, изготовитель AMD, версия 4.6; NVIDIA — NVIDIA GeForce <model>/PCIe/SSE2, изготовитель NVIDIA Corporation, версия 4.6.0. Каждая форма найдена напечатанной настоящими машинами (glxinfo/inxi в публичных обсуждениях, девять строк несут свой источник в seen); остальные строки построены по тому же правилу. Ни одна не отрисована нами — у полосы нет машины с Linux.

Таблица (vflin-fingerprint::gpu_linux, 53 карты, отдаётся как fingerprint.gpus.linux): платформа Linux из опроса Steam, сентябрь 2026, по доле — без наладонников и консолей (Steam Deck, Steam Machine, одномесячный всплеск Radeon 610M), без сборных строк, которые не называют карту, без карты NVIDIA в ноутбуке (PRIME on-demand у Ubuntu рисует браузер на встроенном GPU — ASSUMED, что профиль не отгружается) и без карт, которых заявленная система не потянет или про которые нельзя показать, что она НАЗЫВАЕТ их так, как написано (RDNA 3 и 4, Battlemage, Meteor Lake, Radeon 680M/780M, Polaris: их маркетинговые имена берутся из системной таблицы libdrm, а про 22.04 не показано, что она их знает).

Классы: linux-desktop (встроенная офисная машина или настольная с дискретной картой; деление — таблицы, 3.54 : 25.94 — население Linux в Steam играет) и linux-laptop (новый: только встроенные). Пресет (ubuntu-chrome-desktop) рисует свою карту из той же таблицы при profile.create, класса и формы шаблона, сперва изготовителя машины, как это делает Windows (A139). C2_GPU_OS_MISMATCH у валидатора отказывает на личности Linux тому, что пишет другая система: D3D, Metal, Apple, PCI id.

Что таблица говорит, но на чём пока ничто не действует: webgpu — предложил бы Chrome 155 на Linux адаптер WebGPU на этой карте (его список блокировки GPU, запись 186: Intel Gen12+ на Mesa 22+, драйвер NVIDIA 535.183.01+; у каждой карты AMD и у каждой Intel до Tiger Lake — нет). На этом хосте с Windows ядро предлагает адаптер при любом заявлении (патч 0025 называет его по заявленной карте) — страница, спрашивающая navigator.gpu.requestAdapter() на личности Linux с AMD, получает адаптер, какого настоящая не дала бы (ISSUE-552, ядро).

Ядро читает это с патча 0091 (B158): webgpu.adapter — булево; false = у заявленной машины нет адаптера WebGPU, и navigator.gpu.requestAdapter() разрешается в null с предупреждением в консоли «No available adapters.», после настоящего круга запроса — собственный отказ Chrome, прочитанный в его исходниках. Отсутствие, true или любое не булево сохраняют адаптер (каждый конфиг до 0091). Демон пишет его из таблицы (gpu_linux::LinuxGpu::webgpu → false для отказанных карт) — это половина control-plane; до тех пор личность Linux с AMD адаптер всё-таки получает. Независимо от поля заявление Linux теперь слышит getPreferredCanvasFormat() = rgba8unorm, как отвечает каждый Chrome на Linux.

Потоки встроенной карты принадлежат её процессору (A200, ISSUE-553). Встроенный GPU сидит на кристалле процессора, поэтому карта называет семейство процессора: gpu_linux::CPUS перечисляет для каждой из 15 встроенных строк и каждой формы, для которой она рисуется, процессоры, которые Mesa называет этой строкой, и их число потоков (спецификации изготовителей; веса между процессорами одной карты — ASSUMED). Каждая дорога, которая делает личность Linux — оба класса в генерации, с изготовителем и без, и пресет Ubuntu при profile.create, — берёт потоки оттуда, а память — из строк таблицы класса при этом числе. До: 68.2 % ноутбуков Linux и 6.8 % настольных Linux заявляли пару, какой нет ни у одной машины (HD Graphics 520, всегда 2 ядра и 4 потока, с 12); после — 0 из 1 000. Цена, сказанная: ноутбук Linux теперь 8 потоков примерно в 60 % розыгрышей, потому что карты, которые бывают у ноутбуков Linux (Iris Xe, UHD 620, Vega 8), сидят на 4-ядерных 8-поточных процессорах — гейт концентрации держит класс Linux по тому же источнику (доля карты × её процессоры), а не по опросу Steam для Windows; K72 измерил, что именно число ядер позволяет fingerprint.com сливать личности на одном выходе, так что мерить дальше надо здесь. Процессор дискретной карты — любой процессор: её потоки остаются из таблицы класса. Личности Windows по-прежнему рисуют свои отдельно от карты — тот же дефект, там ещё не измеренный; имена Windows из пула несут PCI id (Intel(R) Iris(R) Xe Graphics (0x00009A49) — это Tiger Lake, всегда 8 потоков), поэтому та же привязка возможна по device id (ISSUE-553).

Остатки, названные. У ноутбука Linux в каталоге нет имени встроенной камеры (он рисует USB-веб-камеру). screen.available сохраняет верхнюю полосу GNOME в 27 px, которой Chrome на родном Wayland может и не видеть. А всё, что карта решает за пределами своего имени, — пределы, расширения, точности, пиксели — остаётся ответом D3D11 этого хоста (ISSUE-020), на Linux ровно как на Windows.

2t. Каталоги B170 — экраны, которые Windows сообщает на самом деле, Mac на Intel и AMD, и карта, взятая целиком (ISSUE-672, ISSUE-677)

Экраны: 59 строк -> 78. SCREEN_MODES держал только панель, которая делится на масштаб РОВНО, исходя из того, что браузер сообщает целые пиксели только тогда. Chromium говорит иначе: на Windows границы дисплея — gfx::ScaleToEnclosingRect(screen_rect, 1.0f / scale) (ui/display/win/screen_win.cc), дальний край округлён ВВЕРХ в одинарной точности — validate::windows_css_size. Поэтому 2560x1600 при 150 % сообщает 1707x1067, 3840x2160 при 175 % — 2195x1235, 3440x1440 при 150 % — 2294x960: ровно те три размера, которые K98 нашёл в Windows-профилях конкурентов и которые эта таблица отвергала. Строка, где деление не ровное, теперь НАЗЫВАЕТ свою панель (ScreenMode::panel_px) — 1707, умноженное на 1.5, даёт 2560.5, округлённое — 2561, то самое число панели, что было в профиле конкурента, — и гейт держит размер по правилу. Добавлено: 19 пар Windows (панели, у которых здесь уже есть источник, при других масштабах, которые Windows для них предлагает; редкие — 175 % на QHD-мониторе — не взяты, чтобы доля панели не размазывалась) и Linux при 200 % на шести HiDPI-панелях ноутбуков (собственный масштаб GNOME). Посылка гейта A128 переписана на месте — вместе с мутантом, которого он по-прежнему обязан отвергать.

macOS: Mac на Intel и AMD и iMac на M3. gpu_mac::MAC_X86 — 21 карта (5 Intel, 16 AMD), каждая — строка, ВИДЕННАЯ в таблицах macOS у Web3D Survey, оформленная по правилу Metal в ANGLE, прочитанному в исходниках (DisplayMtl.mm, driver_utils.cpp), и каждая стоит в Mac из списка Apple для macOS 14 (support.apple.com/en-us/105113); карты машин 2017 года не взяты (на них macOS заканчивается версией 13). Они нужны потому, что процессор, который читает страница, — процессор машины: личность Mac, выпущенная на x86-машине, заявляет x86, а чип Apple на x86 — та самая несогласованность, которую называет C2_GPU_FOREIGN. Пока они не разыгрываются — генератор не знает процессора машины, а перенос карты внутрь apply_host_architecture переписал бы и карту, которую человек ВЫБРАЛ (эта функция работает и на конфиге, принесённом целиком). Розыгрыш — одно решение там, где демон знает машину: x86 разыгрывает эти, arm — Apple silicon, с потоками и памятью каждой машины. До тех пор они в каталоге для карты, которую выбирает человек или приносит профиль. В настольную таблицу генератора добавлен и 24-дюймовый iMac (2023) на M3.

Карта, взятая целиком (ISSUE-677). vflin_fingerprint::card::find ищет рендерер по всем таблицам (три Windows, Linux, macOS Apple и Intel/AMD), а card::adopt пишет ВЕСЬ блок hardware.gpu из этой одной строки, литералом структуры — поле, добавленное в карту позже, там не скомпилируется, пока его не заполнят. Гейт берёт каждую карту каждой таблицы поверх конфига с картой машины (GTX 1080 Ti, её id и чужое слово) и не находит ничего от машины; карта вне каталога или другой системы отвергается и ничего не двигает. Что ядро не проецирует, сказано, а не спрятано: лимиты WebGL, список расширений и точности — от настоящей видеокарты (ADR-0031); таблицу по картам пришлось бы измерять карту за картой (ADR-0035; B52: заявленная карта несла список расширений хоста, 35 из 35 по порядку). Импорт (localapi) должен вызывать card::adopt для карты, которая есть в каталоге, — это строка control-plane.

Здесь не сделано: имена камер и микрофонов (каждая строка камеры несёт и её режимы съёмки со страницы производителя, поэтому имя без них не добавляется); старые встроенные карты Intel на Windows (единственная недостающая модель K98 — Intel HD Graphics 4400 — и её семейство Haswell); розыгрыш Mac на Intel/AMD.

2u. speech.localVoices — ЛОКАЛЬНЫЕ голоса машины, которую заявляет личность (B184, ISSUE-756; патч 0099)

Поле. speech.localVoices: [{"name", "lang"}, ...] — локальные голоса речи машины в том порядке, в каком их перечисляет страница (speechSynthesis.getVoices()), первый — голос по умолчанию: name — точное имя, которое Windows даёт голосу (описание его токена, оно же voiceURI на странице), lang — тег, который сообщает Chrome. Пишется демоном ПРИ ЗАПУСКЕ и никогда не хранится с профилем (launch_config_for -> vflin_fingerprint::speech_voices::set_launch_local_voices), как fonts.dir: он выводится из языка, который профиль заявляет СЕЙЧАС, и из его canvas.seed, поэтому язык, сменённый позже — выходом или перегенерацией, — никогда не оставляет за собой голосов прежнего. [] говорит, что у машины нет локального голоса. Поля нет — собственные голоса хоста заявленного семейства (0098). speech.voices (настройка 0072) лежит рядом и переносится туда и обратно без потерь; продукт его пока нигде не пишет.

Розыгрыш (crates/vflin-fingerprint/src/speech_voices.rs). Локальные голоса машины с Windows — это голоса её языковых пакетов, а пакет приносит их все, поэтому разыгрываются ПАКЕТЫ: язык ИНТЕРФЕЙСА Windows, который подразумевает заявка (его первый голос — по умолчанию; en-* показывается как en-US, кроме en-GB, es-* — как es-MX, кроме es/es-ES, fr-CA, pt-PT, zh-TW/HK/MO остаются своими, немецкий, арабский и нидерландский показываются как de-DE, ar-SA, nl-NL); рядом — собственный региональный пакет заявки, если он другой (en-CA, en-AU, en-IN, en-IE, de-AT, de-CH, fr-CH, ar-EG). Порядок — порядок токенов голосов (язык, затем имя), а голос по умолчанию переставляется на первое место. Один язык — один список (B185, 2026-11-06): B184 добавлял, с той долей, что у настоящих машин, ещё от одного до трёх пакетов, разыгранных по зерну профиля; замер владельца от 2026-11-01 (ISSUE-773) и бриф B185 поставили планку наоборот — два профиля с одним выходом показывают один набор, как у Vision, — поэтому лишние пакеты больше не разыгрываются, и каждый профиль языка перечисляет набор, который несёт большинство машин этого языка (один пакет интерфейса: 136 из 170 английских машин, 43 из 65 остальных).

У каждого числа есть источник (каждая строка WINDOWS_VOICES называет свой):

  • какие голоса есть в пакете: Microsoft, Appendix A: Supported languages and voices (49 языков);
  • точные имена, порядок и доли: 241 настоящий список голосов Windows (пресеты camoufox, которые поставляются с donutbrowser 0.26.0, presets.windows[].speechVoices — это Firefox, который перечисляет те же токены OneCore, что читает Chrome; как их собирали, файлы не говорят). Посчитано по ним: пакет всегда целый; правило порядка держится в 229 из 230 списков (единственное исключение называет один голос дважды); голос по умолчанию — первый голос своего пакета по ключу во всех; 34 из 170 списков с английским по умолчанию несут ещё пакет (20 %), 22 из 65 с другим по умолчанию (34 %); из этих 56 у 33 — один пакет сверх, у 15 — два, у 8 — три и больше (разыгрываются как три); у 5 из 236 списков Windows 10/11 нет ни одного голоса OneCore;
  • этот хост (K109): Chrome перечисляет George, Hazel и Susan, George — по умолчанию.

Имена, которых не показывает ни один настоящий список (B185). У 17 пакетов Microsoft в настоящих списках не было точного имени. 14 теперь берут имена из собственного каталога Vision, прочитанного из Vision.exe на этой машине (Vision показывает тайский профиль Microsoft Pattara, ISSUE-773): сначала сверено с настоящими списками — строки Vision совпадают с ними байт в байт у 47 из 48 голосов, которые есть и там и там; Heami у Vision — Korean (Korea), а в настоящих списках — Korean (Korean), и настоящие списки сохраняют свою форму. Три пакета остаются неразыгранными и со списком 0098: каталанский и тамильский (у Vision их нет) и шведский (у Vision Microsoft Bengt - Swedish — без региона, в отличие от всех остальных имён, — на веру не взято). Язык, для которого у Windows нет голоса (украинский, казахский, балтийские языки …), получает [] — так и на собственном украинском ноутбуке владельца (B181, обычный Chrome: локального голоса нет). Личности macOS и Linux не получают ничего: их голоса — другой каталог.

Что с этим делает ядро (0099): страница видит эти локальные голоса там, где видела голоса хоста; голос, который у хоста есть (то же имя), говорит сам собой; любой другой озвучивает выданный ему голос хоста — сначала с тем же тегом, потом с тем же языком, потом любой, по одному, пока хватает, а дальше общий, со сдвигом на шаг скорости SAPI, — так что никакие два имени не читают фразу за одно и то же время; на хосте без локального голоса — беззвучно, с событиями локального голоса. Не заявляется: длительность у именованного голоса — это длительность голоса хоста, а не его собственная.

2v. Собственные шрифты машины при запуске — отозвано B192 (было B185, ISSUE-768/772; ISSUE-806)

Теперь: запуск никогда не меняет список шрифтов личности на шрифты машины. fonts::set_launch_machine_fonts (всё ещё вызывается в launch_config_for — строка control-plane на удаление) ничего не меняет; ядро получает из записи presentFamilies, generics, dir модуля и список моноширинных, а патч 0023 прячет любое семейство машины, которого список не заявляет, — среди них и шрифты, которые человек поставил руками.

Что сделал B185 и почему это отменено. Он отправлял личность Windows на машине Windows её версии в ядро БЕЗ списка (presentFamilies пуст, нет generics, нет папки модуля), так что страница видела шрифты машины так, как их показывают собственный Chrome машины и Vision (K98), и называл цену в этом разделе: наборы шрифтов перестали доходить до такого профиля. Верно для одного браузера; для пятидесяти профилей, которые человек запускает на одной машине, это одна общая метка — обкатка её измерила (S72, L5: Fira Mono, Indie Flower, Lato — семейства, поставленные на этой машине руками, на 4 профилях из 4), и владелец решил 2026-11-07: «мы не должны палить основную машину… Это нужно убрать». «Один лишний шрифт» у B185 (CreepJS 12 против 11) — это Source Code Pro / Chakra Petch, семейства, которые добавил набор личности: третий слой (§2r) делал свою работу.

Измерено на пути продукта (core/scripts/font-leak-daemon.mjs: собственный блок L5 обкатки над 3 семействами, которые на этой машине стоят руками, 114 другими семействами модуля и 2, которых нет нигде, — против списка конфига каждого запуска; ядро серии 88, win11-desktop us): дерево без изменения — 4 профиля из 4 показывают все 3 семейства, поставленные руками, при пустом списке; с ним — 0 из 3 на 4 из 4, и каждое семейство модуля, которое профиль заявляет, рисуется, а незаявленное — нет.

2w. Устройства машины: device (B186, ISSUE-684/775; патч 0100)

"device": { "touchPoints": 10, "bluetooth": true, "battery": { "charging": false, "level": 0.42 } }

Что из этого читает страница. touchPoints — navigator.maxTouchPoints, и при значении больше 0 — грубый указатель в any-pointer рядом с точным указателем тачпада (основной указатель остаётся точным, наведение остаётся, ontouchstart остаётся выключенным — собственный ответ Chrome на сенсорном ноутбуке с Windows); bluetooth — navigator.bluetooth.getAvailability(), и без адаптера requestDevice() отказывает, как на машине без него; battery — navigator.getBattery(): поля нет — у машины нет батареи (настольный: заряжается, уровень 1, время зарядки 0, время разрядки +Infinity); поле есть — батарея ноутбука, в том состоянии, которое задают charging/level (без состояния — от сети и полная). Необязательное и разбирается целиком: нет поля или оно испорчено — свои у хоста, как до этого поля (и указатель 0097).

Кто его пишет. ЗАПУСК (vflin_fingerprint::device_traits::set_launch_device_now) — по классу и зерну личности, так что это одна и та же машина при каждом запуске, а состояние батареи разыгрывается заново на каждом запуске. Запись, которая несёт device, сохраняет свой сенсорный экран и адаптер; от запуска — только состояние. Только личности Windows (источники — машины Windows); метка, которая не называет ни настольный, ни ноутбук, ничего не получает.

Доли (пересчитывает core/scripts/device-shares.mjs): сенсорный экран ноутбука — у 46 из 151 масштабированной настоящей машины (10 касаний у 40, иначе 5), у настольного — нет; состояние ноутбука — из 31.09 % настоящих состояний Chrome на Windows, которые показывает только ноутбук (заряжается — 2443, от батареи — 666 базисных пунктов, с шагом в десятые, целый процент); Bluetooth — у 2803 из 3287 ноутбуков и у 1604 из 3049 настольных: машины с модулем Bluetooth, Linux-Hardware.org, декабрь 2025 (recon K111; до B188 — прогноз 2014 года, 75 % / 27 %, в котором доля настольных вдвое меньше измеренной).

2x. locale.uiLanguage — интерфейс браузера, который выбрал человек (B191 вместе с A235, ISSUE-792)

Что это. Код интерфейса, который поставляет Chrome, — один из machine_locale::interface_locales(), 52 ключа IDS_ACCEPT_LANGUAGES (ja, en-GB, pt-BR, es-419). Поля нет — интерфейс на собственном языке личности: так было у каждого профиля до этого; в форме — «по языку профиля».

Что его читает, а что НЕТ. Запуск передаёт --lang = интерфейс, который называет голова locale.languages (machine_locale::interface_switch, launch.rs): цепочка — это то, что видит страница (navigator.languages, Accept-Language), поэтому интерфейс движка, а с ним и общие семейства шрифтов, с ней никогда не расходятся. Это поле — ВЫБОР человека, из которого строится любая цепочка: derive::chain_for(primary, uiLanguage) ставит первой цепочку выбранного интерфейса, а язык личности — после неё (en-US, en, ja-JP, ja), и derive::set_identity_language — одно место, где перевывод (за выходом, за машиной, язык, выбранный руками) пишет locale.language, locale.languages и fonts.generics вместе — так язык личности движется, а выбранный интерфейс остаётся.

Зачем его вообще хранить — по одной голове не отличить «человек выбрал английский» от «личность английская», и цепочка, перестроенная из одного языка, молча теряла выбор (это нашёл A235). И почему оно не может стать вторым источником: C9_INTERFACE называет конфиг, у которого uiLanguage — не интерфейс, который поставляет Chrome, и тот, у которого голова цепочки называет другой интерфейс, — перевывод в обход chain_for тогда громок, в собственной строке профиля. Тесты: derive::tests::following_an_exit_moves_the_identity_and_keeps_the_chosen_interface, …every_country_under_every_interface…, …c9_names_a_forgotten_choice…; machine_locale::tests::no_two_interfaces_begin_their_chains_with_the_same_tag держит утверждение, на котором стоит чтение. Ядро это поле не читает (старое ядро и старая программа его не замечают).

2y. Язык, который даёт страна выхода, — для КАЖДОЙ территории (B192)

country_languages::for_country отвечает для каждой территории territoryInfo CLDR, кроме четырёх (254 строки; NO_LANGUAGE — Антарктида, Клиппертон, остров Херд, ZZ — языка не имеет, CLDR записывает только und). Раньше было 206 строк, и у 48 стран с часами языка не было: их выходы двигали часы и оставляли язык — на деле английский (кадр владельца с Албанией: en-US, en там, где Vision шлёт sq-AL, sq, en-US, en). Правило теперь — программа, core/scripts/gen-country-languages.mjs (--check держит таблицу равной ей байт в байт): официальный язык (де-юре или де-факто) с наибольшей долей, который ICU ядра умеет форматировать (измерено, core/scripts/fixtures/icu-locale-support-157.json: 69 из 228), иначе всё равно наибольший официальный; цепочка Chromium для него, иначе ФОРМА Chromium <lang>-<CC>, <lang>, en-US, en (basis: "synthesized", 23 строки: Албания, Косово, Армения, Грузия, Исландия, Малайзия, Пакистан, Босния …). Она воспроизводит 204 из прежних 206 строк и поправляет две: Гонконг en-US → zh-TW, zh, en-US, en, Макао pt-BR → то же (CLDR: традиционный китайский — 95 % и 98 %). Мальта — en-US, en: английский там официальный, а мальтийский — язык, который это ядро не умеет форматировать; Vision шлёт mt-MT.

Ворота: country_languages::tests::every_territory_has_a_language_or_is_named_without_one — каждая территория CLDR и каждая, которой windowsZones даёт часы, имеет строку или стоит в NO_LANGUAGE; новая валит сборку. Что приносит с собой синтезированная цепочка: derive::languages строит ту же форму для любого языка, для которого Chromium не поставляет цепочки (kk-KZ → kk-KZ, kk, en-US, en; прежняя [tag, base] остаётся цепочкой, которую C9 принимает, так что ни одна запись не сдвигается), а запуск стартует такой профиль в en-US (machine_locale::NO_INTERFACE_FALLBACK), а не в интерфейсе машины: измерено, --lang=sq-AL сам по себе стартует в en-GB этой машины и пишет «Please fill in this field.». Chrome не поставляет албанского интерфейса, так что Chrome настоящего албанца тоже на другом языке — на каком, ПРЕДПОЛОЖЕНО.

3. Правила согласованности (валидатор)

Валидация ПАДАЕТ (а не предупреждает), когда сигналы противоречат друг другу. Примеры:

  • webgl.gpu.vendor должен быть правдоподобен для os.family (никаких GPU Apple на Windows).
  • Рядом с машиной, на которой это работает — сказано, не отказано (A119, ISSUE-300). UA-CH architecture следует за os.arch, процессором машины, на которой личность сделана (страница читает настоящий по знаку NaN), так что личность macOS, сделанная на x86-машине, заявляет x86. Карта Apple не на Mac с Apple silicon — C2_GPU_FOREIGN; личность, чья ОС — не ОС машины, — C8_OS_FOREIGN_HOST: набор веб-API движка — его собственной ОС (CreepJS читает ОС по нему), и никакой патч это не закрывает. Оба — флаги согласованности на строке профиля. Карта другого вендора, чем у машины, — C2_GPU_FOREIGN_VENDOR (A120) — пул не сужается до вендора машины, пока владелец не скажет; а settings.foreignOsPresets: hide убирает личности другой ОС из каталога (A120, вопрос владельца стал переключателем; show — по умолчанию).
  • webgl.gpu.vendor и webgl.gpu.renderer обязаны называть ОДИН И ТОТ ЖЕ токен производителя (C2_GPU_VENDOR_RENDERER_MISMATCH, A99). Скобка в Google Inc. (T) и первое поле ANGLE (T, …) — один токен; у одной карты один производитель. Это A8 владельца — «какого хрена можно поставить GOOGLE Inc. (NVIDIA) и видюху AMD RX 6600» — и правило выше его не ловит: такой renderer говорит Direct3D11, не несёт ни Apple, ни Metal, а ОС — Windows, и это всё, о чём оно когда-либо спрашивало. Каждая половина по отдельности правдоподобна; ложь они только вместе. Правило — это R1/R2 из K32, которое измерили на настоящем хосте и затем проверили на независимо собранном каталоге из 170 строк — оно держится на 169 и не ломается ни на одной (0). Оно не судит то, чего не может прочитать: строка производителя без скобки не называет токена, а у renderer не из ANGLE нет первого поля. Выдумать сравнение там — значит заработать ложные тревоги, а потом ослабить правило, чтобы их заглушить. Уже существующие конфигурации чинятся, а не отвергаются. Путь запуска отвергает всё, что отвергает валидатор, так что одно это правило без починки перестало бы открывать затронутые профили. У несогласованности одно верное разрешение — renderer называет КАРТУ, а строка производителя — её следствие, — поэтому демон переписывает производителя в сторону карты, пишет об этом в журнал один раз на профиль и никогда не трогает саму карту.
  • fonts.set должен соответствовать os.family/os.version (никаких шрифтов macOS на Windows).
  • locale.timezone должен быть достижим из геолокации network.proxy (согласованность часовой пояс↔IP).
  • browser.uaCh должен быть выведен из os + browser.version (никогда не набран руками).
  • B173 (ISSUE-684): указатель следует browser.uaCh.mobile, а не мыши хоста (патч ядра 0097). Личность, которая не мобильная (false или поля нет — любой настольный компьютер и ноутбук, которые делает каталог), показывает странице pointer: fine, any-pointer: fine, hover: hover, any-hover: hover и maxTouchPoints 0, что бы ни было подключено к хосту, — сервер или VPS без мыши раньше давал каждому профилю pointer: none. Мобильная личность оставляет ответ хоста: класса телефонов и сенсорных ноутбуков пока нет, и ему нужны грубый указатель и точки касания как измеренные поля конфига (ISSUE-680), а не значение, которое угадывает ядро.
  • screen.dpr×базовое разрешение должно быть настоящей комбинацией устройства, а не произвольной.
  • screen.available должен помещаться в дисплей (C6_AVAIL_BOUNDS, C6_AVAIL_EMPTY) и класть свой резерв на тот край, с которого резервирует заявленная ОС (C6_AVAIL_RESERVE_SIDE). Второе — несущее: правило порядка пропускает availTop 0 рядом с availHeight = height − 25, потому что два правильно упорядоченных числа всё ещё могут описывать устройство, которого не существует. ВЕЛИЧИНА намеренно не закреплена: Dock настоящего хоста, более высокая панель задач или автоскрывающаяся строка меню реалистичны по построению согласно ADR-0031, и отказ им был бы ложным срабатыванием.
  • A185 (ISSUE-513): у личности macOS обязана быть строка меню (C6_MENU_BAR_MISSING, availHeight == height при availTop 0). Измерено, а не выведено: MacBook 1440×900 @2 из K70 со всем дисплеем в доступной области читался у CreepJS красным по Screen и CSS Media Queries (like-headless 19 → 25), та же личность со своей строкой — зелёным, 19; Vision поставляет 14.8 % своих отпечатков Mac именно так. Поэтому на macOS нулевой запас отказывается — на Windows он остаётся законным (киоск, скрытая панель задач). Код НАРОЧНО не в семействе C6_AVAIL: принесённому целиком конфигу УСТАРЕВШИЙ прямоугольник этого семейства чинят (под ним поменялось разрешение), а этот — заявление, и ему отказывают с его именем (profile.create отвечает 400). Профиль, СОХРАНЁННЫЙ до правила, не брошен: его запуск и его profile.duplicate выводят прямоугольник заново, уже со строкой (validate::repair_menu_bar, один раз в журнал). Гейт a185_tests::no_route_forges_a_mac_without_its_menu_bar вызывает каждый из восьми подделывающих путей, которые нашёл в маршрутизаторе аудит A184, с Mac, чей прямоугольник — весь дисплей, и принимает отказ или личность со строкой — больше ничего.
  • A185 (ISSUE-514): MacBook’и с вырезом под камеру не предлагаются (validate::MENU_BAR_UNMEASURED: 1470×956, 1512×982, 1728×1117, 1440×932 @2). Их строка меню выше тех 25 px, которые здесь получает каждый Mac, — K70 прочитал 32–39 px ровно для этих размеров в каталоге Vision, а это числа другого продукта, а не чтение живой машины, поэтому к нам они не переносятся (ADR-0035). Строки остаются годными (сохранённый профиль на такой по-прежнему запускается), выбор экрана и генератор их больше не выдают, а MacBook на внешнем дисплее (где строка меню И ЕСТЬ 25 px) по-прежнему берёт у них МОДЕЛЬ по долям опроса, так что MacBook’и продукта не все одной модели. Ноутбучный пресет каталога переехал с 14″ MacBook Pro M3 с вырезом (1512×982) на 13″ MacBook Pro M2 (1440×900 @2, без выреза). Разблокировка: vflin node characterize на живом MacBook с вырезом (CONTRIBUTING_A_DEVICE_CAPTURE.md (internal: docs/30-core/CONTRIBUTING_A_DEVICE_CAPTURE.md)) даёт настоящий availTop; тогда эти строки вернутся, вместе с ним.
  • webrtc.publicIp должен равняться выходному IP прокси; localIps — замаскированы.
  • network.tls (JA3/JA4) и network.http2 (SETTINGS/Akamai fp) выводятся из os + browser.version и должны соответствовать заявленному браузеру: личность Chrome-148 выдаёт рукопожатие Chrome-148, а не умолчание Chromium и не переписанное прокси. (Набор C10/C11.) fingerprint.validate возвращает { valid, issues[] } со стабильным code на каждое правило.

4. Вывод, а не свободный ввод

Несколько полей вычисляются, чтобы оставаться согласованными: строка UA и UA Client Hints из os+version; languages[] из основного языка; colorDepth/dpr из профиля устройства. Интерфейс показывает выборы высокого уровня (класс устройства, регион); остальное выводит каталог.

5. Источники значений

  • Видеокарта, которую ВЫБИРАЕТ человек, — это пул, из которого тянет генератор (fingerprint.gpus, A99). До этого это были разные множества, и разрыв между ними — A7 владельца: генератор с зерном тянул из 253 настоящих карт Windows, взвешенных по 4 462 машинам, на которых их наблюдали, а выбрать человек мог только из 6 готовых конфигураций. Его «всего 6 видюх» было правдой о выборе и неправдой о продукте. Эти шесть к тому же были РЕДКИМИ, а это хуже, чем мало. Их три карты Windows встречались на 77 из 4 462 машин — 1.73 %, RTX 3060 с 0x00002503 — на трёх. Выдать человеку редкую карту — ровно то, от чего существует ADR-0037, поэтому fingerprint.gpus упорядочен по распространённости: верх списка — это середина толпы. Верхние 25 карт покрывают 50.6 % этих машин, верхние 100 — 89.7 %. Производитель и renderer ездят одной строкой, как screen-modes: одно маркетинговое имя охватывает несколько id устройств, а один id устройства — несколько имён (0x000067DF — это и RX 570, и RX 580), так что ни одно поле не определяет другое, и два независимых выпадающих списка предлагают только несогласованную пару. Это соседнее чтение, а не замена catalog. Пресет — это личность целиком: экран, шрифты, локаль, часовой пояс, ядра, память, — и карта лишь одно её поле; превратить 6 пресетов в 253 значило бы выдумать остальные пять для каждой карты, а это та догадка, которую отвергает ADR-0035 и на которой ISSUE-047/048 этот продукт уже однажды ловили. Происхождение: Borrowed, никогда не Measured — настоящие наблюдения, сделанные кем-то другим, с названным источником. Только Windows: формы renderer для macOS и Linux ни с чем измеренным здесь не сверены.
  • Профиль, созданный без всякого выбора, ГЕНЕРИРУЕТСЯ целиком (A101, решение владельца от 2026-09-17: «надо, чтобы сразу при нажатии новый профиль был новый фингерпринт, без кнопки генерейт»). profile.create, не называющий ни fingerprintRef, ни встроенного fingerprint, ни fromHost, теперь выполняет тот же код, что и fingerprint.generate, — generate_for_core, затем шум canvas выключен, — поэтому профиль получает своё зерно, свою карту из взвешенного пула, свой экран и своё железо. До этого ветка не возвращала НИЧЕГО, и профиль запускался в личности хоста; пресет, который видел оператор, был выбором редактора, а не умолчанием демона. Зерно чеканится заново, а не выводится из id профиля, хотя бриф просил именно этого и не получил: id профиля облаку виден открыто (он ключ profiles_meta и едет в адресах блоба и синхронизации), так что зерно, равное ему, позволило бы серверу предсказать шум canvas профиля в тот же миг, когда оператор включит переключатель ADR-0037. Воспроизводимость сохранена там, где она безопасна: то же ЗЕРНО пересобирает ту же конфигурацию байт в байт. Привязанный выход выбирает регион — обращением REGION_LOCALES по объявленному часовому поясу выхода, а не каким-то новым правилом: без этого профиль, созданный под немецкий выход, генерируется для us и противоречит собственному IP выхода при первом же запуске (C4). Часовой пояс, которого не предлагает ни один регион, откатывается к умолчанию генератора, а не угадывается. Пресеты не изменились и остаются явным выбором по имени.
  • Профиль, созданный без всякого выбора, берёт карту из того же пула (A100, ISSUE-219). Выбор исправил A99, но нажатие «Создать» всё ещё читало карту пресета как константу, и K34 прочитал её со страницы: RTX 3060 (0x00002503), 3 машины из 4 462. Теперь profile.create вытягивает карту каждого профиля на его собственном зерне через generator::pool_card — функцию, которой fingerprint.generate пользуется с A60, так что оба пути дают одинаковую карту для одного зерна и класса. Вытягивание сохраняет КЛАСС пресета — встроенная или дискретная, — поэтому его экран, ядра и память по-прежнему описывают настоящую машину; пресет macOS или Linux сохраняет свою карту, потому что в пуле нет строк для этих систем. Значит, карта шаблона Windows — это класс, а не та карта, которую получит профиль. Карту, которую держит профиль, читайте из profile.get (gpu) — сохранённые производитель и renderer дословно, плюс machines, если это карта из пула. Карта сопоставляется с пулом по ВСЕЙ строке renderer: у двух строк пула нет id устройства.
  • Часы и языки следуют за страной выхода — любой страны (A121, ISSUE-306/307). Профиль, у которого эти половины не закреплены (identityPinned.timezone / .languages), берёт их у привязанного выхода при создании, при привязке, когда ложится фоновое измерение выхода, и при каждом запуске до сборки конфигурации. Страна выхода — объявленная оператором, если задана (она побеждает измерение, которое остаётся в записи и называется, когда расходится, — перекрывает A102 по стране); иначе измеренная. Пояс: объявленный; иначе измерения, если оно поместило выход в эту страну (пояс города); иначе собственный пояс страны — единственный пояс IANA этой страны в отображении windowsZones Unicode CLDR (таблица, которую A113 уже несёт как machine_locale_data::WINDOWS_ZONES), или primaryZones CLDR (metaZones.txt) для многопоясной страны, называющей один (DE, ES, PT, CN, …); страна, чьи часы зависят от города (US, RU, BR, CA, AU, …), его не имеет, и ничего не двигается, пока выход не измерен. Никогда — умолчание. Языки: CLDR territoryInfo + IDS_ACCEPT_LANGUAGES Chromium (country_languages::for_country), раскрытые цепочкой C9. Код: vflin_fingerprint::country_zones, vflin_localapi::proxy::exit_geo. Спорный адрес (расположен в одной стране, зарегистрирован в другой) — ProxyRecord.geoDisputed и C4_PROXY_GEO_DISPUTED на строке — сказано, полоса сохранена, пояс от места расположения.
  • Пресеты каталога: выверенные согласованные профили устройств (win11-chrome-desktop, macos-safari-…).
  • Сгенерированные: генерация с ограничениями (например, Apify Fingerprint Suite как источник значений), затем прогон через наш валидатор. (Их берём для «что»; применяем через ядро для «как».)
  • Импортированные / выведенные из настоящего устройства: наивысшее доверие; согласованность по построению. (Сюда же ложится «настоящие отпечатки устройств, а не сгенерированные» у отгруженных антидетект-продуктов.) Теперь построено для локального уровня инверсией хоста — см. §5a: аппаратные измерения узла берутся с его СОБСТВЕННОГО настоящего хоста, а не из синтезированной догадки.

Стратегия источника (различитель реализма — blocker перед Phase 2). Сгенерированные комбинации правдоподобны, но не настоящие; отгруженные антидетект-продукты выигрывают на правде настоящего устройства, так что источник данных каталога — несущее решение, а не сноска. Варианты, которые надо закрыть в ADR до Phase 2:

  • Сбор с настоящих устройств — добровольный агент или небольшой парк устройств выдаёт согласованные связки сигналов (без персональных данных): настоящие сочетания GPU+ОС+экран+шрифты+сеть → наивысший реализм; постоянные затраты; нужно покрыть матрицу ОС / GPU / экран / локаль и обновлять по мере обновления устройств. (Этот вариант с центральным пулом теперь отложенный уровень B-fleet; B-local, инверсия хоста — §5a, ADR-0031 — отгружает уровень настоящего устройства БЕЗ пула, беря собственный хост каждого узла, так что стоимость покрытия набора данных для локального применения растворяется.)
  • Лицензировать набор данных — самый быстрый старт; регулярные платежи; покрытие и качество разнятся; риск по условиям использования.
  • Генерировать и валидировать (Apify) — дешёвая широта; ниже доверие; годится для профилей с невысокими ставками. Решено (ADR-0012, Accepted 2026-06-18): основа — выверенный каталог + генератор на Rust с ограничениями сейчас (browserforge как позднейшее расширение широты за этой же схемой); конвейер сбора с настоящих устройств для уровня высокого доверия вырастает позже, под собственным ADR о приватности и согласии. Каждая личность получает оценку доверия по происхождению (§6); обновление пресетов должно следить за Chrome stable (пресеты стареют быстро). → ADR-0012 + CATALOG_SOURCING_COMPARISON.md.

Приор реализма — как тянет генератор с ограничениями (ADR-0012 realism prior, Proposed 2026-07-01). Генератор основы больше не тянет равномерно случайно по плоским таблицам; он тянет взвешенное по частоте СОВМЕСТНОЕ распределение по вручную закодированному графу согласованности (os → chrome_major → gpu → {cores, ram, screen}; region → tz/lang), детерминированно от корневого семени профиля (то же семя ⇒ тот же кортеж; другое семя ⇒ обычно другой). GPU несёт собственные правдоподобные подтаблицы железа и экрана, так что совместно невозможные комбинации (ноутбучный встроенный GPU на настольном кортеже {16 ядер, 32 ГБ, 4K}) невыразимы — рёбра закодированы, а не восстановлены из частных распределений. Это уточняет §4 (генератор выбирает согласованные и правдоподобные по частоте сочетания), не меняя ни схему §2, ни валидатор §3: веса — это const-метаданные времени генерации, а не сериализуемые поля, и каждая вытяжка по построению проходит валидатор. См. ADR-0012 realism prior (internal: docs/10-architecture/adr/0012-realism-prior-weighted-joint.md).

5a. Инверсия хоста — уровень настоящего устройства, по построению (ADR-0031, B-local)

Источник «выведено из настоящего устройства» (§5) построен для локального уровня, но инверсией, а не сбором: вместо центрального набора данных или парка, выдающего связки сигналов, каждый узел характеризует СВОЙ хост (обвязка capture-profile пишет кэш HostFingerprint), и аппаратные измерения профиля берутся с этого настоящего хоста — согласовано по построению, ничего не синтезируется. Поскольку значения настоящие, а не правдоподобные догадки, и признак приора реализма, и межинтерфейсная утечка GPU WebGPU↔WebGL (набор A8) растворяются. Меняются от профиля к профилю только свободные измерения. Разрез (как его строит host_coherent_config):

Настоящее с хоста (из снятого HostFingerprint) Свободное (на профиль — из запроса или отчеканенное)
os.family/arch (navigator.platform + arch из UA-CH)² screen.width/height/dpr/colorDepth — измерение с окном и свободное¹
browser.version и выведенные ua/uaCh (из Chrome/… в UA хоста) locale.timezone · language · выведенные languages[]
hardware.cores (hardwareConcurrency) · memoryGb (deviceMemory) network.proxyRef
hardware.gpu.vendor/renderer (WebGL UNMASKED — настоящий GPU) canvas.seed — чеканится заново на каждый профиль (см. ниже)
fonts.set — набор по умолчанию настоящей ОС (не снимок) id — чеканится на профиль

TLS/HTTP2/QUIC в network принадлежат работающему ядру по построению (ADR-0027 — измерено, а не подделано), так что они автоматически совпадают с браузером хоста. provenance.source = generated с realDeviceRef = "hostinv:<platform>:<renderer>", связывающим конфигурацию обратно с хостом.

Уникальность на профиль — это правило согласованности, а не любезность. host_coherent_config выдаёт фиксированное заполнение canvas.seed; каждый путь создания делает snapshot() результата → свежее семя canvas/WebGL (и id) на профиль. Без этого два профиля, выведенных с ОДНОГО хоста, делили бы одно семя — признак связи между профилями. Поэтому снимок обязателен на пути хоста (как он уже обязателен для каталога и встроенной конфигурации).

Три источника личности во время создания. profile.create (и fingerprint.generate*) принимают не больше одного: fingerprintRef из каталога, встроенный проверенный fingerprint или fromHost (инверсия хоста — только свободные измерения; аппаратные приходят из кэша хоста). Больше одного ⇒ 400. Все три проходят snapshot() и по построению удовлетворяют §3. Явный fromHost на неохарактеризованном устройстве жёстко падает (NOT_FOUND) — он не должен молча вырождаться в профиль без личности, — тогда как неявный путь (личность не запрашивали) — это просто None.

¹ Честные остатки: начальный снимок делается без окна, поэтому он не может прочитать настоящий монитор (его 800×600 не проходит C6 из §3) → screen подаётся на профиль или снимком с окном (в ожидании). B-fleet (центральный пул настоящего железа) и переключатель source: real|from-fp|custom на каждое поле (инверсия хоста сегодня работает на уровне личности — вся конфигурация выводится с хоста, а не сигнал за сигналом) остаются на потом.

² os.version действительно настоящая с хоста (с 2026-07-10, ISSUE-031 c): HostFingerprint несёт снятую ua_platform_version (носитель версии ОС — navigator.platform даёт только семейство), и host_coherent_config выводит os.version из неё (мажор platform-version Win10 10 → "10", Win11 мажор ≥13 → "11"; пустой или старый кэш откатывается к умолчанию семейства). Так что хост на Windows 10 теперь выдаёт "10", а не "11". platformVersion в UA-CH тоже настоящая с хоста (ISSUE-031 c2, с 2026-07-10): конфигурация выдаёт ТОЧНОЕ значение хоста (например, Win11 "19.0.0"), а НЕ общую для парка константу из прямого отображения — то, что каждый профиль Win11 делил один Sec-CH-UA-Platform-Version, само было признаком связи. Валидатор C1 проверяет её на СТРУКТУРНОЕ правдоподобие по каждой ОС (platform_version_plausible: Windows "<major>.0.0" с мажором Win11 ∈ 13..=40 / Win10 == 10; macOS "<major>.<minor>.<patch>", мажор == os.version; Linux пусто) вместо точного равенства, так что настоящее значение хоста проходит, а значение в форме macOS, вне диапазона или неправильной формы по-прежнему отвергается; аномальный снимок откатывается к прямому отображению. browser.version тоже настоящая с хоста — разобрана из Chrome/… в UA хоста. См. ADR-0031 host-inversion (internal: docs/10-architecture/adr/0031-fingerprint-by-host-inversion.md) (аппаратные измерения инверсией хоста) и ADR-0027 network-by-inversion (internal: docs/10-architecture/adr/0027-network-fingerprint-by-inversion.md) (TLS/HTTP2/QUIC по построению).

6. Оценка доверия

Каждая материализованная личность получает оценку доверия из: происхождения, результата валидатора и capabilities работающего ядра (сколько оно на самом деле смогло соблюсти). Запуски с низким доверием помечаются, а не отгружаются молча.

Замечание по интерфейсу (дизайн): это не тщеславное число на профиль — проверенные профили на способном ядре согласованы (у всех вышло бы ~максимум). В таблице оно появляется только как флаг предупреждения о согласованности, когда профиль невозможно сделать согласованным (слабое ядро, несоответствие прокси и часового пояса). Сама оценка (её определяют в основном происхождение × возможности ядра) живёт в подробностях отпечатка, а не в колонке.

7. Отношение к патчам

Авторы патчей (Phase 7) реализуют соблюдение для полей этой схемы. Схема — это стабильный интерфейс между «конфигурацией» и «патчем на C++»: патчи читают конфигурацию и никогда не выдумывают значения. (CHROMIUM_PATCHING_POLICY.md §1.4, §7.)

8. Правка, генерация в один клик и шаблоны (планка UX)

  • 50+ отдельных параметров, каждый настраивается — редактор показывает всю схему (§2) вплоть до отдельных сигналов; каждое поле поддерживает Настоящее ↔ Из отпечатка ↔ Своё (паттерн отгруженных продуктов). Глубина соответствует лучшим на рынке (50+ настраиваемых параметров), но каждое значение по-прежнему проходит валидатор §3.
  • Умная генерация в один клик — одна кнопка производит согласованную проверенную личность из класса устройства и региона (использует генерацию §5 и правила согласованности §3). «Умная» = она подбирает правдоподобные сочетания (GPU↔ОС, шрифты↔ОС, часовой пояс↔IP, TLS↔браузер), а не случайные; результат по построению проходит набор проверок согласованности, так что оператор получает чистый профиль в один клик.
  • Шаблоны — сохранить любую конфигурацию (отпечаток + политика прокси + расширения + домашние страницы + закладки) как переиспользуемый шаблон; создать разом N профилей из шаблона и набора данных (AUTOMATION_ENGINE §4). Пресеты каталога (§5) — это шаблоны только для чтения; пользовательские шаблоны — редактируемые копии.

Открытые вопросы

Откуда берётся выверенный каталог (строить, лицензировать или собирать), как часто пресеты обновляются с выпусками Chrome, формула оценки доверия → OPEN_QUESTIONS.md.

  • source на каждое поле в схеме (показано в интерфейсе, 2026-06-21). §8 и DESIGN_DIRECTION дают каждому сигналу переключатель Настоящее ↔ Из отпечатка ↔ Своё, но схема v0 из §2 несёт только provenance.source на уровне личности. Схема v1 должна моделировать source: real|from-fp|custom на каждое поле (с пометкой, что real рискованно — оно выдаёт устройство хоста и не проходит §3, если хост случайно не окажется согласованным), чтобы переключатель опирался на контракт, а ядро знало по каждому сигналу, читать ли значение хоста, каталога или проверенное своё. Всплыло на макете выезжающей панели отпечатка. Обновление (ADR-0031, 2026-07-04): инверсия хоста (§5a) делает рискованный случай real безопасным на уровне личности — ВСЯ конфигурация согласована с хостом по построению, так что real больше не «не проходит §3, если хост случайно не окажется согласованным» (он им является, по построению). На потом остаётся гранулярность на каждое поле (смешать real GPU со custom часовым поясом в одной конфигурации) и вопрос, должна ли инверсия хоста нести собственное provenance.source (например, host-inverted), а не переиспользовать generated + realDeviceRef.