Тонкости использования putenv()

Пару постов назад я писал про класс для работы с переменными окружения. Весь смысл того класса был в способности хранить значения переменных, передаваемых в putenv.

Я выкатил этот класс в наш QA, который работает на множестве платформ (AIX, HP-UX, Solaris, Linux, Windows). И вроде все шло нормально, unit-тесты проходили, основной код не падал. Увы, но QA машины, управляемые Hudson/Jenkins обычно дико перегружены, и обычно на них начинают вылезать самые неожиданные косяки. Через неделю работы стало видно, что на AIX иногда происходят странные падения при вызове std::system(). Причем что-то странное происходило именно внутри этой функции. Но так как чудес не бывает, то необъяснимые вещи обычно являются следствиями проблем с памятью. truss, вставленный в командную строку, передавамую в system(), показал, что иногда блок переменных окружения дочернего процесса имеет разрушенные значения у некоторых переменных.

Начали копать в моем новом классе. Вот его упрощенная версия, но которая содержит коварный баг. Желающие могут сначала подумать, в чем тут может быть проблема, а потом читать дальше. Простой main() внизу позволяет багу проявиться.

#include <vector>
#include <map>
#include <string>

#include <unistd.h>

class EnvironmentVariablesManager {
 public:
  typedef std::vector<char> VariableContainer;
  typedef std::map<std::string, VariableContainer> Variables;
  void put(const std::string& name, const std::string& value) {
    VariableContainer pair;
    PairToContainer(name, value, &pair);
    const std::pair<Variables::iterator, bool> inserted =
      vars_.insert(std::make_pair(name, pair));
    if (!inserted.second)
      inserted.first->second = pair;
    putenv(&inserted.first->second[0]);
  }

 private:
  void PairToContainer(const std::string& name, const std::string& value,
                       VariableContainer* pair) const {
    pair->clear();
    std::copy(name.begin(), name.end(), std::back_inserter(*pair));
    pair->push_back('=');
    std::copy(value.begin(), value.end(), std::back_inserter(*pair));
    pair->push_back('\0');
  }
  Variables vars_;
};

int main() {
  EnvironmentVariablesManager env;
  env.put("DB2_HOME", "a");
  env.put("DB2_HOME", "12345678");
}

Если этот код запустить под valgrind на Линуксе (я тестировал у себя на OSX), вылезает странное сообщение об использовании памяти внутри putenv после ее освобождения.

clang++ -o putenv_check putenv_test.cpp && valgrind ./putenv_check
==1046== Memcheck, a memory error detector
==1046== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al.
==1046== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info
==1046== Command: ./putenv_check
==1046==
--1046-- ./putenv_check:
--1046-- dSYM directory is missing; consider using --dsymutil=yes
==1046== Invalid read of size 1
==1046==    at 0x2A8A3B: __findenv (in /usr/lib/system/libsystem_c.dylib)
==1046==    by 0x232C62: __setenv (in /usr/lib/system/libsystem_c.dylib)
==1046==    by 0x216A7E: putenv (in /usr/lib/system/libsystem_c.dylib)
==1046==    by 0x100001999: EnvironmentVariablesManager::put(std::string const&, std::string const&) (in ./putenv_check)
==1046==    by 0x1000015DE: main (in ./putenv_check)
==1046==  Address 0x100012560 is 0 bytes inside a block of size 11 free'd
==1046==    at 0x563A: free (in /usr/local/Cellar/valgrind/3.8.1/lib/valgrind/vgpreload_memcheck-amd64-darwin.so)
==1046==    by 0x10000208C: __gnu_cxx::new_allocator<char>::deallocate(char*, unsigned long) (in ./putenv_check)
==1046==    by 0x10000201D: std::_Vector_base<char, std::allocator<char> >::_M_deallocate(char*, unsigned long) (in ./putenv_check)
==1046==    by 0x100002483: std::vector<char, std::allocator<char> >::operator=(std::vector<char, std::allocator<char> > const&) (in ./putenv_check)
==1046==    by 0x1000018A8: EnvironmentVariablesManager::put(std::string const&, std::string const&) (in ./putenv_check)
==1046==    by 0x1000015DE: main (in ./putenv_check)
==1046==
==1046==
==1046== HEAP SUMMARY:
==1046==     in use at exit: 2,425 bytes in 34 blocks
==1046==   total heap usage: 58 allocs, 24 frees, 2,824 bytes allocated
==1046==
==1046== LEAK SUMMARY:
==1046==    definitely lost: 18 bytes in 1 blocks
==1046==    indirectly lost: 0 bytes in 0 blocks
==1046==      possibly lost: 0 bytes in 0 blocks
==1046==    still reachable: 2,407 bytes in 33 blocks
==1046==         suppressed: 0 bytes in 0 blocks
==1046== Rerun with --leak-check=full to see details of leaked memory
==1046==
==1046== For counts of detected and suppressed errors, rerun with: -v
==1046== ERROR SUMMARY: 9 errors from 1 contexts (suppressed: 1 from 1)

Сходу, по крайне мере мне, не совсем очевидно, почему это происходит. Причем, если убрать второй вызов env.put("DB2_HOME", "12345678"), то ошибка изчезает. Поэтому подозрение пало на строки:

    if (!inserted.second)
      inserted.first->second = pair;
    putenv(&inserted.first->second[0]);

Если изменить немного код (фактически, мы тоже копируем массив, но иным способом):

    if (!inserted.second)
      inserted.first->second.assign(pair.begin(), pair.end());

то сообщение об ошибке меняется:

--1087-- ./putenv_check:
--1087-- dSYM directory is missing; consider using --dsymutil=yes
==1087== Invalid read of size 1
==1087==    at 0x2A8A3B: __findenv (in /usr/lib/system/libsystem_c.dylib)
==1087==    by 0x232C62: __setenv (in /usr/lib/system/libsystem_c.dylib)
==1087==    by 0x216A7E: putenv (in /usr/lib/system/libsystem_c.dylib)
==1087==    by 0x1000016A9: EnvironmentVariablesManager::put(std::string const&, std::string const&) (in ./putenv_check)
==1087==    by 0x10000129E: main (in ./putenv_check)
==1087==  Address 0x100013560 is 0 bytes inside a block of size 11 free'd
==1087==    at 0x563A: free (in /usr/local/Cellar/valgrind/3.8.1/lib/valgrind/vgpreload_memcheck-amd64-darwin.so)
==1087==    by 0x100001D9C: __gnu_cxx::new_allocator<char>::deallocate(char*, unsigned long) (in ./putenv_check)
==1087==    by 0x100001D2D: std::_Vector_base<char, std::allocator<char> >::_M_deallocate(char*, unsigned long) (in ./putenv_check)
==1087==    by 0x1000022E9: void std::vector<char, std::allocator<char> >::_M_assign_aux<__gnu_cxx::__normal_iterator<char*, std::vector<char, std::allocator<char> > > >(__gnu_cxx::__normal_iterator<char*, std::vector<char, std::allocator<char> > >, __gnu_cxx::__normal_iterator<char*, std::vector<char, std::allocator<char> > >, std::forward_iterator_tag) (in ./putenv_check)
==1087==    by 0x1000021C4: void std::vector<char, std::allocator<char> >::_M_assign_dispatch<__gnu_cxx::__normal_iterator<char*, std::vector<char, std::allocator<char> > > >(__gnu_cxx::__normal_iterator<char*, std::vector<char, std::allocator<char> > >, __gnu_cxx::__normal_iterator<char*, std::vector<char, std::allocator<char> > >, std::__false_type) (in ./putenv_check)
==1087==    by 0x1000020A4: void std::vector<char, std::allocator<char> >::assign<__gnu_cxx::__normal_iterator<char*, std::vector<char, std::allocator<char> > > >(__gnu_cxx::__normal_iterator<char*, std::vector<char, std::allocator<char> > >, __gnu_cxx::__normal_iterator<char*, std::vector<char, std::allocator<char> > >) (in ./putenv_check)
==1087==    by 0x1000015BF: EnvironmentVariablesManager::put(std::string const&, std::string const&) (in ./putenv_check)
==1087==    by 0x10000129E: main (in ./putenv_check)

Теперь более менее ясно, что происходит. Но для полного понимания приведу еще более простой код на С:

#include <unistd.h>
#include <stdlib.h>

int main() {
  char *v1, *v2;
  v1 = malloc(10);
  strcpy(v1, "x=123");
  putenv(v1);
  free(v1);

  v2 = malloc(10);
  strcpy(v2, "x=123");
  putenv(v2);
  free(v2);
  return 0;
}

Данный код, запущенный под valgrind, выдает следующее (этот trace уже с Линукса):

==523== Memcheck, a memory error detector
==523== Copyright (C) 2002-2010, and GNU GPL'd, by Julian Seward et al.
==523== Using Valgrind-3.6.0 and LibVEX; rerun with -h for copyright info
==523== Command: ./putenv_test
==523==
==523== Invalid read of size 1
==523==    at 0x4A07CF9: __GI_strncmp (mc_replace_strmem.c:400)
==523==    by 0x3E1C235649: __add_to_environ (in /lib64/libc-2.12.so)
==523==    by 0x3E1C2353CD: putenv (in /lib64/libc-2.12.so)
==523==    by 0x4A0952D: putenv (mc_replace_strmem.c:1165)
==523==    by 0x400607: main (in /storage2/home3/ademin/test/env/t)
==523==  Address 0x4c28040 is 0 bytes inside a block of size 10 free'd
==523==    at 0x4A0595D: free (vg_replace_malloc.c:366)
==523==    by 0x4005D7: main (in /storage2/home3/ademin/test/env/t)
==523==
==523== Invalid read of size 1
==523==    at 0x4A07D14: __GI_strncmp (mc_replace_strmem.c:400)
==523==    by 0x3E1C235649: __add_to_environ (in /lib64/libc-2.12.so)
==523==    by 0x3E1C2353CD: putenv (in /lib64/libc-2.12.so)
==523==    by 0x4A0952D: putenv (mc_replace_strmem.c:1165)
==523==    by 0x400607: main (in /storage2/home3/ademin/test/env/t)
==523==  Address 0x4c28040 is 0 bytes inside a block of size 10 free'd
==523==    at 0x4A0595D: free (vg_replace_malloc.c:366)
==523==    by 0x4005D7: main (in /storage2/home3/ademin/test/env/t)
==523==
==523== Invalid read of size 1
==523==    at 0x3E1C235652: __add_to_environ (in /lib64/libc-2.12.so)
==523==    by 0x3E1C2353CD: putenv (in /lib64/libc-2.12.so)
==523==    by 0x4A0952D: putenv (mc_replace_strmem.c:1165)
==523==    by 0x400607: main (in /storage2/home3/ademin/test/env/t)
==523==  Address 0x4c28041 is 1 bytes inside a block of size 10 free'd
==523==    at 0x4A0595D: free (vg_replace_malloc.c:366)
==523==    by 0x4005D7: main (in /storage2/home3/ademin/test/env/t)
==523==
==523==
==523== HEAP SUMMARY:
==523==     in use at exit: 0 bytes in 0 blocks
==523==   total heap usage: 3 allocs, 3 frees, 492 bytes allocated
==523==
==523== All heap blocks were freed -- no leaks are possible
==523==
==523== For counts of detected and suppressed errors, rerun with: -v
==523== ERROR SUMMARY: 3 errors from 3 contexts (suppressed: 6 from 6)

Но если убрать первый free(v1); или передвинуть его после второго putenv, то valgrind ничего не находит.

Итак, вывод: функция putenv требует, чтобы в момент ее вызова память, в которой хранится передыдущее значение переменной, обязана быть доступной и все еще содержать правильное старое значение.

Каким-то образом, putenv пытает читать старое значение при установке нового.

Теперь назад к C++. Если оригинальный код:

    if (!inserted.second)
      inserted.first->second = pair;

заменить на следущий:

    if (!inserted.second)
      inserted.first->second.swap(pair);

то проблема изчезает (valgrind более ничего не находит подозрительного).

В чем разница? А в том, что первый код в процессе копирования разрушает (освобождает, перемещает) старое значение. Поэтому последующий вызов putenv’а будет пытаться обратиться к уже освобожденной памяти.

Второй же код, делая swap, перекидывает владение данными старого значения из хранилища (map) во временную переменную pair, а данные из pair отдает хранилищу. А так как переменная pair “изчезнет” только в конце функции, то на момент вызова putenv старое значение будет все еще доступно. Фактически, мы откладываем момент уничтожения старого значения.

Вот такая вот история. Если честно, это один из крутейших багов с памятью, что я лично встречал последнее время.


Disclaimer

Комментарии