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-Lenght написана неправильная цифра? Фрейминг сбивается, вместо начала следующего пакета мы пападаем или в какой-то кусок тела предыдущего или в середину хедереов следующего пакета. В результате парсер не находит ожидаемой метки SIP/2.0 в первой строке переданного ему буфера и выдает ошибку.
Тут можно пытаться найти старт пакета или выкинуть нафиг буфер и закрыть соединение. Но libre делает парадоксальную вещь - ждет больше данных. Со следующим пришедшим пакетом ситуация лучше не становится и в буфере накапливается уже несколько сообщений до тех пор, пока он не вырастет до лимита в 8k и соединение все таки не дропнется.
В код сериализатора пакетов на сервере закрался баг, который делал вот такое:
INVITE sip:neko@nekonekoneko SIP/2.0 Content-Length: 146;
Добавлял лишнюю точку с запятой, как будто у хедера есть параметры. Как это интерпретировал код libre? Парсил из строки число и говорил, что там 0. Соответствующий код обвязки думал, что если начальник сказал ноль, значит ноль и копировал все тело пакета в следующий буфер. Парсер смотрел в этот буфер, не видел то-ли начала, то-ли конца, но просил на всякий случай больше данных.
Сериализатор я поправил, точку с запятой убрал. Теперь надо репортнуть авторам библиотеки про это безобразие.
Осталось подумать, что будет, если туда специально зафигачат какую-то кривизну или очень большое число.