Назначение: определить отпечатки как структурированный проверяемый каталог с правилами согласованности, а не как случайный 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 ранжирует устройства по имени) тогда ранжирует и список страницы. После него проба читает камеру личности на обеих дорогах (--gatePASS; 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)—--gateFAIL. После (0075): и запрос, и страница —HD Pro Webcam C920, OBS нет ни на одной дороге. С файлом камеры в профиле (--camera-file,media.video.sourceпатча 0067) ПРЕДПРОСМОТР запроса играет файл, дорожка страницы названаHD Pro Webcam C920и работает,enumerateDevices()после разрешения даёт то же имя —--gatePASS на обеих дорогах (окно снято--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, потому что устройство, которое страница видит и не может открыть, хуже устройства, которого она не видела. Измеренное следствие, которое стоит знать: собственныйGuessVideoGroupIDChromium сопоставляет камеру со звуковым входом по имени, поэтому, как только в каталоге появляется «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, заявленное имя — метка захваченной дорожки) ЗЕЛЁНЫЙ на ядре, КРАСНЫЙ на стоке; CreepJSdevices (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.
Почему патч отозван. Построены были две формы, обе несогласованные, и шестой гейт нашёл каждую:
- Подмена списка, только до разрешения. В момент, когда выданное разрешение открывало настоящий id устройства, проекция выключалась, и тот же origin видел другую машину, чем мгновением раньше. Кроме того, она заново наполняла список, который Chromium уже отфильтровал по Permissions-Policy, так что документу, которому микрофон запрещён, его могли выдать.
- Только фильтрация — убирать виды, применять всегда. Согласовано и до, и после разрешения, ничего не
выдумывает, и измерилось правильно (сужающее заявление убрало камеру; контроль сохранил набор хоста;
заявленный, но отсутствующий микрофон правильно не выдумывался). Но
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 % из 200win11-laptopи 0 из 200win11-desktop, а первая тройка списка выбора не изменилась, то есть толпа по-прежнему ведёт (ADR-0037). Существующий профиль не двигается: розыгрыш идёт при создании, и выпавшая карта хранится в конфиге профиля, который читает каждое последующее чтение. Четырёх кандидатов отвёл сам пул, а не вкус: наблюдаемые им имена —GTX 1060 3GB / 6GB / 5GBиRadeon RX 570/580 Series, поэтому агрегированные у SteamGTX 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 1119.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 1010.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 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>" | ""}(CLIvflin 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,--gatePASS); официальная сборка 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 (--gatePASS, пять различимых значений для пяти корзин, которые выбрал владелец). Предел кучи отвечает на открытый вопрос собственным правилом 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). СA189fingerprint.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-CHarchitectureследует за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иmaxTouchPoints0, что бы ни было подключено к хосту, — сервер или 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). Измерено, а не выведено: MacBook1440×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 этой страны в отображенииwindowsZonesUnicode CLDR (таблица, которуюA113уже несёт какmachine_locale_data::WINDOWS_ZONES), илиprimaryZonesCLDR (metaZones.txt) для многопоясной страны, называющей один (DE, ES, PT, CN, …); страна, чьи часы зависят от города (US, RU, BR, CA, AU, …), его не имеет, и ничего не двигается, пока выход не измерен. Никогда — умолчание. Языки: CLDRterritoryInfo+IDS_ACCEPT_LANGUAGESChromium (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, если хост случайно не окажется согласованным» (он им является, по построению). На потом остаётся гранулярность на каждое поле (смешатьrealGPU соcustomчасовым поясом в одной конфигурации) и вопрос, должна ли инверсия хоста нести собственноеprovenance.source(например,host-inverted), а не переиспользоватьgenerated+realDeviceRef.