written on Thursday, October 25, 2012
Нашов у libre смішний баг. re — це така сишна бібліотечка, яка всередині себе реалізує асинхронний IO, протокол sip, всякі turn/stun, http, парсери sdp з offer/answer і всяку дрібноту простішу, на кшталт rtp і утиліт для sha та crc.
Зовні баг виглядає так: в якийсь момент під час встановлення з'єднання з іншим абонентом, коли додаток вже отримав SDP, але ще не закінчив робити всякі ACK туди-сюди, вхідні пакети перестають приходити. В результаті дзвінок вилітає по таймауту, всі наступні дзвінки взагалі не доходять, а сам додаток не бачить відповідей на власні REGISTER запити.
Зазвичай SIP юзають поверх UDP і один сіповський пакет цілком влазить в юдіпішну датаграму. Я юзаю не таке, а варіант з TLS поверх TCP, де є постійний коннект до проксі, який реюзається для різних запитів і по якому прокся спускає вхідні запити.
На постійному коннекті ніхто вже не вірить в те, що кожен recv() повертає ціле повідомлення, і починається традиційна гра в вгадування меж пакета без явного фреймінгу.
Як це робить libre? Дуже просто. Парсить з вхідного буфера пакет, поки не знайде останній хедер і два переноси рядка. Якщо кінця пакета не видно — парсер випльовує помилку і чекає, поки дійдуть наступні дані. Якщо кінець пакета знайдено, але після нього є ще якісь дані, то ми думаємо, що це наступний пакет, і зсуваємо буфер на його початок.
Якщо в пакеті є тільки хедери, але немає тіла — все просто, але в пакетів, які встановлюють голосовий дзвінок, є тіло (в якому живе SDP), розмір якого вказується в хедері Content-Length. В цьому випадку кінцем пакета вважається кінець хедерів плюс зміщення на цифру, вказану в Content-Length.
Що може статися, якщо в Content-Length написана неправильна цифра? Фреймінг збивається, замість початку наступного пакета ми потрапляємо або в якийсь шматок тіла попереднього, або в середину хедерів наступного пакета. В результаті парсер не знаходить очікуваної мітки SIP/2.0 в першому рядку переданого йому буфера і видає помилку.
Тут можна пробувати знайти старт пакета або викинути нахуй буфер і закрити з'єднання. Але libre робить парадоксальну річ — чекає більше даних. З наступним пакетом ситуація краще не стає, і в буфері накопичується вже кілька повідомлень до тих пір, поки він не виросте до ліміту в 8k, і з'єднання все-таки не дропнеться.
В код серіалізатора пакетів на сервері закрався баг, який робив таке:
INVITE sip:neko@nekonekoneko SIP/2.0 Content-Length: 146;
Додавав зайву крапку з комою, ніби в хедера є параметри. Як це інтерпретував код libre? Парсив з рядка число і говорив, що там 0. Відповідний код обв'язки думав, що якщо начальник сказав нуль, значить нуль, і копіював все тіло пакета в наступний буфер. Парсер дивився в цей буфер, не бачив то початку, то кінця, але просив на всяк випадок більше даних.
Серіалізатор я поправив, крапку з комою прибрав. Тепер треба репортнути авторам бібліотеки про це безобразіє.
Залишилося подумати, що буде, якщо туди спеціально зафігачать якусь кривизну або дуже велике число.