Блог ·
Сканування, що закривало власне вікно
Кожне вдале заплановане сканування позначало власне вікно як прострочене через одне слово з хибного переліку. Дві вади, помітні лише під час вимірювання.
Автор: Provibr
Наше сканування відкритості має планувальник. Він займає вікно, надсилає команду сканування агентові, який дістає машину, і чекає на дві речі: саме вимірювання і завершення команди, що його несла. Коли вікно спливає без відповіді, його позначають як прострочене, і машина зʼявляється з написом «немає свіжого вимірювання».
Кожне вдале сканування позначало власне вікно як прострочене. Чотири з чотирьох, виміряні проти справжнього агента, тимчасом як самі чотири вимірювання зчитувалися та зберігалися бездоганно. Панель повідомляла, що чотири машини давно не скановані, за кілька хвилин після їх сканування.
Одне слово, з іншої таблиці
Обробник, що закриває вікно без відповіді, починався з раннього виходу: якщо команда вдалася, закривати нічого, отже виходимо.
Слово, з яким він порівнював, було succeeded. Такого слова немає в переліку станів команди, який містить pending, sent, completed, failed, timeout і cancelled. Це слово зовсім іншої таблиці, тієї, що стежить за встановленнями агентів, і воно потрапило сюди тому, що хтось писав обробник, дивлячись на стани тієї іншої таблиці, і тому, що параметр був типізований як простий текст.
Простий текст є всією історією. Порівняння збирається проти будь-якого тексту, отже ніщо не заперечує, а збій є не винятком, а гілкою, якої ніколи не беруть. Обробник пройшов свій шлях «відповіді не надійшло» для кожного вдалого сканування і слухняно позначив кожне вікно як прострочене.
Жодне модульне випробування не могло цього знайти. У підробці немає життєвого циклу команди: підробка і є тим, що подає стан, отже стан є тим, що написав автор випробування, а той автор написав те саме хибне слово. Лише справжній агент, що надсилає справжні завершення через справжній розподільник, дає стан, який це викриває. Параметр тепер типізовано на три стани, які справді можуть дійти до цієї функції, як рівнозначний обробник моніторингу був написаний завжди. Дві функції, те саме питання, і лише одна написана дбайливо.
А два повідомлення надходять не в тому порядку
Полагодити порівняння було замало, а причиною є друга річ, яку показує тільки справжній агент.
Агент надсилає два окремі повідомлення: результат сканування і завершення команди. Вони мандрують незалежно. Виміряно, результат надійшов першим, на чотирнадцять мілісекунд раніше за завершення. Цей проміжок достатньо малий, щоб про нього ніхто не думав, і достатньо великий, щоб траплятися завжди.
Коли працює обробник завершення, вікно отже зазвичай уже закрите результатом. Оновлення без умови тоді відкриває завершене вікно і знову позначає його простроченим, а це первісна вада в іншому капелюсі. Тому оновлення затиснуте на двох станах, у яких вікно ще чекає на відповідь. Якщо його вже вирішено, обробник не змінює нічого.
Це загальна форма: коли факт може надійти з двох боків, той, хто пише не першим, має оголосити, з яких станів йому дозволено виходити.
Одна закрита знахідка, що спинила весь флот
Друга вада тихіша, і шкоди вона наробила більше.
Коли джерело вразливостей оновлюється, відкриті знахідки переоцінюють проти нових даних. Знахідку, чий збіг зник, закривають, і закривають як неперевірену, а не як усунену, бо нічого не виміряно: змінився наш висновок, а не машина. Ця різниця і є причиною, чому такий стан існує.
Переоцінка діставала наявні похідні знахідки, щоб відрізнити «онови цю» від «створи нову». Вона діставала лише відкриті. Це фільтр із розважливим виглядом, і він хибний, бо обмеження унікальності під ним нічого не знає про стани: воно стоїть на сервері та на відбитку знахідки, і крапка.
Послідовність така: державна база вразливостей виправляє діапазон версій, наш збіг зникає, знахідка закривається. За кілька тижнів та сама база переглядає діапазон назад, а це справді трапляється, і збіг повертається. Запит не бачить закритого рядка. Код бере гілку «цього ще немає». Вставляння вдаряє в унікальний покажчик і здіймає порушення обмеження.
І тоді це порушення тікає. Той, хто викликав, ловить його як «раунд бюлетенів не вдався», і той раунд не переоцінює взагалі нічого, для кожного клієнта платформи. Умова, що це спричинила, сама собою не зникає, отже кожен наступний раунд хибив так само. Одна знахідка, що закрилася і відкрилася знову, заморозила переоцінку цілого флоту, назавжди, з рядком журналу як єдиним симптомом.
Полагодження мале: беріть і закриті рядки, а коли закрита знахідка знову збігається, відкривайте її з непорушеною хронологією, а не починайте нову. Знову відкрита знахідка є іншим фактом, ніж нова, і той, хто її розбирає, заслуговує побачити, що це повернулося, а не надійшло.
Цикл джерел, виміряний тричі
Процес джерел написали ще до того, як зʼявився спосіб запустити його, не торкаючись справжньої бази, і саме в цю пастку потрапив попередній раунд безпеки: імпорт пакета бази читає середовище і підʼєднується, тож модульне випробування з уприснутою функцією звантаження мовчки записало чотири бюлетені в базу розробки, повідомляючи зелене.
Бік зберігання тепер є швом, і цикл можна випробувати проти обліку в памʼяті, тимчасом як промисловий код без змін користується справжнім. Далі він тричі відпрацював проти справжніх відкритих джерел.
| Раунд | Запити | Що сталося | Час |
|---|---|---|---|
| Холодний | 4 | 7 066 бюлетенів збережено, 31,4 МБ після розпакування | 18,7 с |
| Без змін | 1 | 304 Not Modified, збережені перевірники залишено, нуль записів | 0,3 с |
| Курсор на два дні назад | 3 | 355 бюлетенів оновлено, жодного нового | секунди |
Код 304 і є сенсом вправи. Статично поданий каталог бюлетенів відповідає на умовний запит «нічого не змінилося», а правильна поведінка полягає в тому, щоб зберегти записані перевірники, не писати нічого і зарахувати раунд як вдалий.
Кілька розмірів варто знати, бо саме там і вгадують: каталог активно використовуваних вразливостей важить близько 188 кБ на дроті, бо стиснутий, а сторінка на дві тисячі записів із більшого джерела має приблизно 18,7 МБ. Обидва читають за жорсткою стелею байтів, а не через звичайну допомогу JSON, яка взагалі не має межі; це дві найбільші відповіді, які платформа поглинає, і надходять вони з машин, якими ми не керуємо.
Вікно, що ділиться навпіл замість перестрибувати
Більше джерело гортають сторінками, всередині рухомого часового вікна. Дві речі в тому циклі були хибні, і обидві губили дані, ніколи нічого не сигналізуючи.
Якщо вікно містило більше записів, ніж дозволяла стеля сторінок, цикл спинявся і все одно пересував курсор у кінець вікна. Усе поза стелею мовчки пропускали, і його підхопили б лише тоді, коли інша зміна знову торкнулася б тих самих записів.
А порожня сторінка посеред вікна, коли записи ще нібито були, посувала курсор на цілу сторінку. Дві тисячі записів зникали.
Обидві тепер оброблюють за одним засадничим правилом: якщо відповідь не вміщується, постав менше питання, а якщо відповідь неможлива, не рухай нічого.
Завелике вікно ділять навпіл, а не обрізають, щонайбільше шість разів, до підлоги в одну годину. Шість поділів зводять тиждень до трохи більше за дві години, чого стачить для будь-якого правдоподібного відставання. Нижче за годину воно спиняється: вікно на кілька хвилин, що досі повертає двадцять тисяч записів, є не питанням вікна, а аварією, і не писати, лишивши курсор на місці, виявляється єдиною чесною відповіддю. Порожню сторінку посеред вікна трактують так само, як помилку, що лишає курсор у спокої, аби наступний раунд спробував знову, а не переступив через діру.
Поділ відбувається після запису сторінки, а не перед ним. Те, що ми вже прочитали, належить первісному вікну і є чинним; викинути це означало б проміняти один рід втрати даних на інший.
Дрібниця з того самого кутка, варта повторення, бо під тиском вона легко виходить хибно: ключ доступу йде в заголовок і ніколи в рядок запиту. Адреси опиняються в журналах, у проміжних кешах і у звітах про помилки. Заголовки туди не потрапляють.
Що ми записали б
- Типізуйте параметр, а надто коли це стан. Порівняння тексту зі словом із переліку іншої таблиці збирається, працює і назавжди бере хибну гілку.
- Два повідомлення про одну подію надходять не в тому порядку. Затисніть другорядного записувача на станах, з яких він може виходити.
- Обмеження унікальності, що не зважає на стани, зустріне код, який зважає. Беріть і закриті рядки, інакше перше повторне відкриття стане порушенням.
- Спитайте, що отруює один-єдиний збій. Виняток, що тікає з циклу по елементу, робить із одного поганого рядка зупинку всієї платформи, і ця умова сама не розвіюється.
- Умовний запит, на який відповіли 304, є успіхом. Збережіть перевірники, не пишіть нічого і не рахуйте це за невдачу.
- Якщо вікно завелике, поділіть його; якщо сторінка неможлива, спиніться. Посунути курсор за непрочитані дані є єдиним наслідком без шляху назад.
- Процес, який не працює без бази даних, випробують на вашій базі даних. Шов у зберіганні коштує менше за прибирання.
Для ясності: сканування відкритості побудовано і воно стоїть за вимикачем розгортання, отже жоден клієнт його перед собою не має. Переоцінку, джерела та розподіл по кількох адресах випробувано; недоведеними лишаються звуження вікна проти живого джерела, а не вприснутого, поведінка покажчика на справді повному каталозі та сканування, запущене з іншої машини, ніж та, що його попросила.
Із платформної роботи над Provibr. Чому зіставлювач відмовляється бути певним щодо версії, описано в збіг, що відмовляється бути певним, а чому зонд позаду майже нічого не надсилає, описано в чому сканер вразливостей має слати нуль байтів.