CLLTRLSDMG

written on Thursday, October 25, 2012

Нашел в libre веселый баг. re - это такая сишная библиотекта, которая внутри себя реализует асинхронный IO, протокол sip, всякие turn/stun, http, парсеры sdp с offer/answer и всякую мелочь попроще, вроде rtp и утилиты для sha и crc.

Внешне бага выглядит так: в какой-то момент в процессе установки соединения с другим абонентом, когда приложение уже получило SDP, но еще не закончило делать всякие ACK туда-сюда, входяшие пакеты перестают приходить. В результате звонок вылетатет по таймауту, все последующие звонки не доходят вообще, а само приложение не видит ответов на собственные REGISTER запросы.

STATEFULL

Обычно 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. Соответствующий код обвязки думал, что если начальник сказал ноль, значит ноль и копировал все тело пакета в следующий буфер. Парсер смотрел в этот буфер, не видел то-ли начала, то-ли конца, но просил на всякий случай больше данных.

Чоделоц

Сериализатор я поправил, точку с запятой убрал. Теперь надо репортнуть авторам библиотеки про это безобразие.

  • если парсеру подсунули что-то, где нет начала пакета, то не надо просить больше - начало никуда не денется
  • если вместо интового значения закралась херь - надо отличать ее от нуля, особенно в таком стремном месте, как Content-Length

Осталось подумать, что будет, если туда специально зафигачат какую-то кривизну или очень большое число.

This entry was tagged c, code and voip