Альтернативный заголовок: "В любой непонятной ситуации возвращай Out of memory".

Давеча решил я запустить свой самописный сервер веселья ради, как я делал это тысячу раз до этого, и каково же было моё удивление, когда я внезапно увидел следующую строчку в консоли: Error when parsing "example_proj.xml": 1:0 out of memory.

Для парсинга конфигурационных файлов в проекте используется сторонняя библиотека (назовём её LibCustomConfig), которая в свою очередь использует широко распространённую libexpat.

Итак. Out of memory? На XML в 50 строчек? Сказать, что я был ошарашен - это не сказать ничего. "Но ведь раньше всё работало". Ни код сервера, ни так называемый LibCustomConfig никак не менялись.

Будучи абсолютно сбитым с толку, я решил свалить всю вину на последнее обновление последней Ubuntu, после которого проблема и появилась. А вместе с ней появились и другие проблемы: падения и подвисания разных приложений, как будто сам Chaos Monkey дотянулся до меня своими лапами. Может где-то в недрах поломался ABI?

Пересаживаюсь на свежую LTS версию Ubuntu, пересобираю сервер и... опять out of memory.

Пришло время расчехлять GDB. Кто-то скажет, что с этого и надо было начать, но дебажить сторонние либы очень не хотелось.

LibCustomConfig использует libexpat примерно так:

Parser::Parser(/*...,*/ char separator = ':')
{
  if (separator)
    /* Constructs a new parser and namespace processor.  Element type
   		 names and attribute names that belong to a namespace will be
   		 expanded. */
    parser_ = XML_ParserCreateNS(/*encoding = */ encoding_,
                                 /*namespaceSeparator = */ separator);
  else
    { /*...*/ }
}

Запоминаем, что сепаратор по умолчанию - ':' (корректно это или нет, я не знаю). Далее файл вычитывается в буфер и парсится стандартным методом из libexpat - XML_ParseBuffer:

XMLPARSEAPI(enum XML_Status)
  XML_ParseBuffer(XML_Parser parser, int len, int isFinal);

При ошибке парсинга возвращается ненулевой статус, а код ошибки можно получить с помощью XML_GetErrorCode.

После получаса дебаггинга наконец нахожу, где же внутри libexpat фейлится парсинг (смотреть на GitHub):

Этот код был добавлен 12 февраля (коммит).

В качестве подопытного XML-файла используется просто <conf></conf>.

В данном случае parser->m_ns == 1 (root parser), uri == "http://www.w3.org/XML/1998/namespace" (автоматически устанавливается libexpat'ом), parser->m_namespaceSeparator == ':' (установлено в LibCustomConfig). Получается любой простой файл не будет парситься, если сепаратором указать двоеточие, т.к. этот символ присутствует в URI.

Но почему же я получаю Out of memory error, если возвращается XML_ERROR_SYNTAX?? Смотрим call stack:

  • сейчас мы находимся внутри функции addBinding, которая нам возвращает XML_ERROR_SYNTAX (смотреть на GitHub)

  • вызывается она внутри функции setContext, которая внезапно возвращает bool. В данном случае XML_False (смотреть на GitHub)

  • далее startParsing, которая возвращает то же булевское значение (смотреть на GitHub)

  • а затем знакомая XML_ParseBuffer, которая по возвращенному XML_False должна понять, что же произошло (смотреть на GitHub)

И как же XML_ParseBuffer понимает, что произошло? А очень просто:

if (parser->m_parentParser == NULL && ! startParsing(parser)) {
  parser->m_errorCode = XML_ERROR_NO_MEMORY;
  return XML_STATUS_ERROR;
}

В любой непонятной ситуации возвращай XML_ERROR_NO_MEMORY. Прикольно.

Комментарии (12)


  1. mayorovp
    23.02.2022 17:18
    +4

    Запоминаем, что сепаратор по умолчанию — ':' (корректно это или нет, я не знаю).

    Совершенно некорректно, документация по libexpat прямо говорит не использовать символ который может встречаться в XML:


    Constructs a new parser that has namespace processing in effect. Namespace expanded element names and attribute names are returned as a concatenation of the namespace URI, sep, and the local part of the name. This means that you should pick a character for sep that can't be part of an URI. Since Expat does not check namespace URIs for conformance, the only safe choice for a namespace separator is a character that is illegal in XML. For instance, '\xFF' is not legal in UTF-8, and '\xFFFF' is not legal in UTF-16.


    1. andr_s Автор
      23.02.2022 17:27
      +8

      Спасибо большое за уточнение

      Тем не менее суть статьи не меняется: возвращать XML_ERROR_NO_MEMORY в этом случае - нонсенс


      1. domix32
        24.02.2022 12:27

        Но Out of memory на 50 строчках потенциально случится может тем не менее. См billion laughts


  1. amarao
    23.02.2022 17:54
    +10

    Ну, для меня сообщение от программы out of memory обычно означает, что это программа неправа, а память есть. Потому что когда памяти нет, то программа обычно ничего не сообщает, а OOM зачитывает некролог с выражением. Чаще всего той программе, которая "виновата", но иногда и кому-то непричастному.


    1. blind_oracle
      24.02.2022 12:29

      Зависит от языка. Да, если это Си то нужно аккуратно всё проверять. А если что то чуть выше уровнем вроде Го где аллоцирует рантайм, то там по крайней мере сообщит перед паникой.

      Но, конечно, пенять на ОС это последнее дело ????


      1. amarao
        24.02.2022 12:54
        +2

        За вычетом java (где свои лимиты памяти), обычные нативные бинарники под линукс ENOMEM на нормальной конфигурации никогда не получают, т.к. есть оверкоммит по памяти. Когда память заканчивается for real, на машине уже обычно есть выделенной памяти в 2-3 раза больше, чем есть физической, так что память заканчивается не в момент её запроса, а в момент попытки использовать. И тогда ядро решает, кого убить, чтобы освободить память. Называется oom (out of memory), плюс особо всеми любимый oom killer.

        Так что для go, C, C++, Rust, python и т.д. получить ENOMEM практически не возможно (если только не запросить нереально большое количество памяти с самого начала).


        1. blind_oracle
          24.02.2022 13:05
          +1

          Кроме нехватки памяти есть еще cgroups в которых память бывает зажимают.

          так что память заканчивается не в момент её запроса, а в момент попытки использовать.

          Да, malloc() в линуксе почти никогда не фейлится, но это зависит от настроек оверкоммита в sysctl. Тем не менее...

          Так что для go, C, C++, Rust, python и т.д. получить ENOMEM практически не возможно (если только не запросить нереально большое количество памяти с самого начала).

          ...конкретно в Go я бывало встречал панику по memory allocation failure ещё до того как в ядре сработал OOM.

          Судя по исходнику рантайма это именно ENOMEM:

          https://github.com/golang/go/blob/bb7fb8a5fac1ad9570c554c366826d649350acbe/src/runtime/mem_linux.go#L189


  1. olehorg
    23.02.2022 18:02

    ERROR_NO_MEMORY выдается не только в случае отсутствия памяти но и в случае кучи других рантаймовых ошибок - например, если в результате работы объем выделенной области памяти равен 0 или если объем запрашиваемой памяти ошибочно отрицателен и еще в массе разных случаев.

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


    1. mayorovp
      23.02.2022 19:33
      +3

      Написано же, что для ошибки достаточно вот такого файла:


      <conf></conf>


  1. Cheater
    23.02.2022 18:46

    Пересаживаюсь на свежую LTS версию Ubuntu

    как я переустанавливал ОС из-за libexpat

    WAT. В чём была проблема даунгрейднуть libexpat или собрать другую версию в песочнице? Зачем переставлять всю систему?


    1. andr_s Автор
      23.02.2022 18:55
      +7

      Я ведь не знал заранее, что проблема в обновлении libexpat. К тому же, как и написано в статье, вся система в целом стала очень нестабильной после обновления. Добавьте к этому очень странный код ошибки, и в этоге получаем вывод, что надо сносить систему

      Признаю, что сделал это скорее из нежелания нормально разбираться, что не так))


  1. mayorovp
    23.02.2022 19:37

    Кстати, заметил тут вот ещё одну ошибку:


    В данном случае parser->m_ns == 1 (root parser), uri == "http://www.w3.org/XML/1998/namespace" (автоматически устанавливается libexpat'ом)

    Это пространство имён — вовсе не пространство имён по умолчанию, а пространство имён связанное с префиксом xml. По умолчанию же пространство имён — пустая строка, и именно пустая строка должна была оказаться в массиве uri.