LINUX.ORG.RU

glibc небезопасна

 , ,


0

7

Привет, ЛОР!

Наткнулся тут на прекрасную историю о том, как использование казалось бы обычных функций из libc может оторвать жопу. И даже Rust не поможет!

Ссылка: https://www.edgedb.com/blog/c-stdlib-isn-t-threadsafe-and-even-safe-rust-didn-t-save-us

Для Ъ:

Функции getenv() и setenv() небезопасны в многопоточных программах. В частности, вызов setenv() во время выполнения getenv() в другом потоке может привести к порче памяти и падению программы. Это особенно релевантно на ARM64, потому как архитектура предоставляет меньше гарантий по части очерёдности выполнения команд.

И если бы это можно было списать на кривые руки авторов статьи, проблемы бы не было. Но тут суть в том, что огромное количество библиотек дёргают getenv() и setenv() под капотом с разными целями, в том числе гнутое gettext и прочие openssl.

Скажи, ЛОР, сишники совсем разучились писать безопасный код? Как с этим жить-то вообще?

UPD:

Подробный пост об этой проблеме: https://www.evanjones.ca/setenv-is-not-thread-safe.html

Прекрасное оттуда:

glibc uses an array to hold pointers to the "NAME=value" strings. It holds a lock in setenv() when changing this array, but not in getenv(). If a thread calling setenv() needs to resize the array of pointers, it copies the values to a new array and frees the previous one. This can cause other threads executing getenv() to crash, since they are now iterating deallocated memory.

То есть, вызов getenv() из glibc потенциально является use-after-free в многопоточной программе и от этого никак нельзя защититься.

★★★★★

Последнее исправление: hateyoufeel (всего исправлений: 3)

Это ты ещё glibc1 не видел.

Shadow ★★★★★
()

Не надо писать многопоточный код. Это всё от лукавого. Если бы ты прочитал книгу А. В. Столярова «Программирование: введение в профессию», то не задавал бы таких вопросов.

vbr ★★★★★
()
Последнее исправление: vbr (всего исправлений: 4)

Так у тебя и без этого большинство структур в Си не являются потокобезопасными. И Rust не ставит целью обеспечить защиту от многопоточных косяков.

Тут помогут только всяки ЯП вроде Erlang, но уже ценой наличия виртуальной машины, потери производительности и прочих расходов на абстракции.

maxcom ★★★★★
()

Интересно, но я нигде не встречал требований, что (get/set)env дожны быть потокобезопасными.

Как и многие другие системные функции, например malloc/free. Не выделяте память - это опасно. И Rust не поможет.

her_s_gory
()
Ответ на: комментарий от maxcom

Тут помогут только всяки ЯП вроде Erlang, но уже ценой наличия виртуальной машины, потери производительности и прочих расходов на абстракции.

Да нифига. Microsoft вот починили этот баг, добавив getenv_s(), которая лочит env и делает копию значения. А перцы из glibc не починили. Авторам стандарта тоже слегка пофигу.

hateyoufeel ★★★★★
() автор топика

Подробный пост об этой проблеме

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

LamerOk ★★★★★
()

А какой смысл получать getenv внутри потока? Просто получаешь нужные переменные окружения до создания потоков и нет проблем.

Надуманная на мой взгляд проблема.

unDEFER ★★★★★
()
Ответ на: комментарий от zurg

В musl другая залупа. Если переменная уже существует, setenv() меняет указатель на значение, старый указатель скармливается в free(). То есть, указатель, который тебе вернул getenv(), вполне может указывать на освобождённую память, но по немного другой причине, и нужно сразу же делать strdup(). Но сам getenv() скорее всего не упадёт (не факт).

hateyoufeel ★★★★★
() автор топика
Ответ на: комментарий от unDEFER

Надуманная на мой взгляд проблема.

Это не «надуманная проблема». Это закономерное следствие того, что при создании юниксовых API о многопоточности никто не думал. Такого по всему POSIX'у хоть жопой жуй.

LamerOk ★★★★★
()

С getenv понятно, но кто это дёргает setenv? Вроде бы без явного желания автора проги ничего туда не должно писаться. И вообще, на мой взгляд, setenv это почти что легаси-функция, я не вижу ей нормального применения, кроме, может быть, шеллов, но в них тредов нет.

Но то, что glibc не засунули туда, как положено, rwlock, с их стороны не очень хорошо.

Скажи, ЛОР, сишники совсем разучились писать безопасный код? Как с этим жить-то вообще?

И опять ты этот троллинг вставляешь, ну хорошо, напишу ещё раз, что не надо отдельных личностей обобщать на «сишников».

firkax ★★★★★
()
Ответ на: комментарий от vbr

Не надо так категорично, мультитред конечно опасен и желательно без обоснованных аргументов его нужности его не делать, но иногда он всё-таки действительно нужен. Но, разумеется, без setenv внутри.

firkax ★★★★★
()
Ответ на: комментарий от unDEFER

А какой смысл получать getenv внутри потока?

По ссылке ходил? Куча библиотек это делает, потому что это единственный способ передавать в них разные опции снаружи (для дебага, например).

Та же glibc проверяет переменную MALLOC_CHECK_ и включает отладку аллокатора, если та включена.

hateyoufeel ★★★★★
() автор топика
Ответ на: комментарий от firkax

И опять ты этот троллинг вставляешь

Я ради этого троллинга весь пост написал, чувак!

ну хорошо, напишу ещё раз, что не надо отдельных личностей обобщать на «сишников».

Тут авторы glibc (и других libc тоже) обосрались жидко. Если glibc пишут какие-то неправильные сишники, то где правильные сишники-то живут? Они вообще существуют?

hateyoufeel ★★★★★
() автор топика

Функции getenv() и setenv() небезопасны в многопоточных программах.

Это буквально написано в мануале. Какой-то растоман наконец нашел знакомые буквы в первом абзаце и высрал статью? По ссылке не ходил.

PPP328 ★★★★★
()
Ответ на: комментарий от hateyoufeel

Хм, так получается там не только в мультитреде use-after-free. Просто делаешь getenv, где-то запоминаешь его ответ, потом делаешь setenv - и всё, прошлый ответ getenv-а уже, возможно, битый. Но решение я уже выше написал - не использовать ненужный setenv.

firkax ★★★★★
()
Ответ на: комментарий от LamerOk

Это не «надуманная проблема». Это закономерное следствие того, что при создании юниксовых API о многопоточности никто не думал. Такого по всему POSIX’у хоть жопой жуй.

Многопоток: существует уже 35 лет.

Разрабы UNIX/POSIX/Linux: НЕЕЕЕЕЕЕТ! ЭТО СЛИШКОМ НОВАЯ ТЕХНОЛОГИЯ! МЫ ТАК БЫСТРО НЕ МОЖЕМ!!!

hateyoufeel ★★★★★
() автор топика

Скажи, ЛОР, сишники совсем разучились писать безопасный код?

Всегда.

ox55ff ★★★★★
()
Ответ на: комментарий от PPP328

Это буквально написано в мануале.

Нет, не написано. В man 3 getenv написано, что они сами по себе non-reentrant. Про то, что параллельный вызов getenv и setenv портит память внутри glibc, там не написано.

hateyoufeel ★★★★★
() автор топика

Разве этот массив не thread-local?

mittorn ★★★★★
()
Ответ на: комментарий от hateyoufeel

Любая non-reentrant функция является потоконебезопасной по определению. Что она там портит это уже не так важно.

vbr ★★★★★
()
Ответ на: комментарий от hateyoufeel

Тут авторы glibc (и других libc тоже) обосрались жидко

Да вроде бы нет, и правда в мануале всё описано - указано что значение из getenv валидно только до следующего вызова setenv/putenv/unsetenv, и, внимание, даже getenv. А вот те кто это всё игнорирует, да, делают баги.

Но повторюсь, я не знаю нормального применения setenv в современном софте, и сам его ни разу не использовал. А применение getenv - в основном для временных костылей и отладки.

firkax ★★★★★
()
Ответ на: комментарий от firkax

Да, оно ещё нереентерабильно.
Раньше кстати последовательная пара вызовов getenv не ломала друг друга, но ломалась setenv из другого потока, но в какой-то версии glibc сделали getenv безопаснее, теперь он выделяет thread-local буффер под возврат, но следующий getenv перетирает результат предыдущего

mittorn ★★★★★
()
Ответ на: комментарий от vbr

не любая. non-reentrant функция может хранить состояние в TLS, тогда она остаётся потокобезопасной

mittorn ★★★★★
()
Ответ на: комментарий от hateyoufeel

Многопоток: существует уже 35 лет.
Разрабы UNIX/POSIX/Linux:

Так как ты не подарил машину с многопотоком в Bell Labs в 1977-ом, то получай, что заслужил.

LamerOk ★★★★★
()

Я очень давно с этим столкнулся – у нас движок крашился при запуске, где-то в дебрях явно не нашего кода.

Дело оказалось в двух вызовах setenv перед SDL_Init( SDL_INIT_AUDIO ), поменял их на SDL-ные обертки, краши прекратились.

setenv там был нужен чтобы пульсаудио передать роль и имя приложения, ради корректно вывода во всяких микшерах.

a1ba ★★
()
Ответ на: комментарий от LamerOk

Так как ты не подарил машину с многопотоком в Bell Labs в 1977-ом, то получай, что заслужил.

Сорян, мои родители тогда ещё в школу ходили и знакомы не были :(

Да, написано. Но для тех, кто умеет читать.

Покажи, пожалуйста. Мы посмотрим.

hateyoufeel ★★★★★
() автор топика
Последнее исправление: hateyoufeel (всего исправлений: 1)
Ответ на: комментарий от hateyoufeel

Покажи, пожалуйста.

Ну, раз «пожалуйста», то ладно. Вот тебе сеанс чтения мануала вслух и с выражением:

attributes(7)

...
       env    Functions marked with env as an MT-Safety issue access the
              environment with getenv(3) or similar, without any guards
              to ensure safety in the presence of concurrent
              modifications.

              We do not mark these functions as MT-Unsafe, however,
              because functions that modify the environment are all
              marked with const:env and regarded as unsafe.  Being
              unsafe, the latter are not to be called when multiple
              threads are running or asynchronous signals are enabled,
              and so the environment can be considered effectively
              constant in these contexts, which makes the former safe.
...

getenv(3)

...
Thread safety │ MT-Safe env
...

setenv(3)
...
Thread safety │ MT-Unsafe const:env
...
LamerOk ★★★★★
()

и от этого никак нельзя защититься

А почему нельзя то?

cobold ★★★★★
()
Ответ на: комментарий от LamerOk

Ага. getenv() помечена как safe. Там не написано, что она может пасть жертвой порченой памяти.

Тут реально вся проблема в том, что в getenv() не используется лок. И это довольно просто починить было бы, но пришёл LamerOk и рассказывает, что портить память – это фича.

Хуже того, getenv() и setenv() используются повсюду в библиотеках. И если свой код можно поправить, то чужой – уже куда сложнее.

hateyoufeel ★★★★★
() автор топика
Последнее исправление: hateyoufeel (всего исправлений: 1)
Ответ на: комментарий от hateyoufeel

Ага. getenv() помечена как safe. Там не написано, что она может пасть жертвой порченой памяти.

Читай ещё раз.

LamerOk ★★★★★
()
Ответ на: комментарий от vbr

нужно сразу же делать strdup()

А если не успеешь? (:

Сохраняться надо!

Gonzo ★★★★★
()
Ответ на: комментарий от Lrrr

это не сишники разучились, а бездари-растоманы никогда и не умели

Сишные библиотеки вызывают setenv() и getenv() почём зря. Виноваты растоманы. Ооооок!

hateyoufeel ★★★★★
() автор топика
Ответ на: комментарий от firkax

А применение getenv - в основном для временных костылей и отладки.

Нет. getenv позволяет использовать разные ветки в зависимости от настроек окружения, что крайне полезно, когда ты используешь CI/CD и/или распределенный запуск. Например у тебя есть 10 инстансов, каждый должен коннектиться к разным серверам, тогда удобно при развертке в контейнере устанавливать env типа INSTANCE_SERVER=bic15.irrpo.net чтобы программа брала его оттуда. В этих случаях конфигурационные файлы неудобны тем, что системе деплоя нужно автоматизированно каким-то образом их редактировать вместо просто передачи env.

Но повторюсь, я не знаю нормального применения setenv в современном софте

Есть, но редко. Например в программе есть код, который в случае установленного env SERVER использует его. Но бывают случаи, когда на данной машине в данных обстоятельствах его использовать нельзя (т.е. необходимо заоверрайдить то, что написано в env), к примеру, если программа запущена с опцией –secure, которая (для примера) запрещает выход за пределы контура. Тогда, вместо того, чтобы лепить в коде, который использует SERVER какие-то проверки удобнее на этапе инициализации сделать setenv(SERVER, ). Да, это меняет ожидаемое поведение, но иногда необходимо.

PPP328 ★★★★★
()
Ответ на: комментарий от hateyoufeel

Хуже того, getenv() и setenv() используются повсюду в библиотеках. И если свой код можно поправить, то чужой – уже куда сложнее.

Так в каких же библиотеках setenv используется? Чтобы знать, какое легаси надо избегать. Ну или фиксить, если очень нужно.

firkax ★★★★★
()

Никогда такого не было и вот опять…

Поднимите issue, сделайте баг репорт, на это требуется примерно столько же энергии, что и посту на ЛОР. Еще лучше сделать фикс и закоммитить, но на это уже будет требоваться гораздо больше энергии, понимаю, поэтому не требую.

Lain_Iwakura
()
Ответ на: комментарий от PPP328

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

А чем централизованное задание переменных окружения существенно проще централизованного создания конфигурационных файлов, благо такие файлы почти всегда нужны?

LamerOk ★★★★★
()
Ответ на: комментарий от PPP328

Нет. getenv позволяет использовать разные ветки в зависимости от настроек окружения,

Это и есть костыли и отладка.

тогда удобно при развертке в контейнере устанавливать env типа INSTANCE_SERVER=bic15.irrpo.net чтобы программа брала его оттуда

Это надо в конфиг-файле указывать а не в env. Ну или аргументом командной строки иногда.

В этих случаях конфигурационные файлы неудобны тем, что системе деплоя нужно автоматизированно каким-то образом их редактировать вместо просто передачи env.

Ну да, используешь дефективную «систему деплоя», а виноваты оказываются конфиги.

Есть, но редко. Например в программе есть код, который в случае установленного env SERVER использует его. Но бывают случаи, когда на данной машине в данных обстоятельствах его использовать нельзя (т.е. необходимо заоверрайдить то, что написано в env), к примеру, если программа запущена с опцией –secure, которая (для примера) запрещает выход за пределы контура. Тогда, вместо того, чтобы лепить в коде, который использует SERVER какие-то проверки удобнее на этапе инициализации сделать setenv(SERVER, ). Да, это меняет ожидаемое поведение, но иногда необходимо.

Я писал что нормального применения не знаю, а ты в ответ в пример приводишь ненормальное. Во-первых, уже описанное выше костыльное использование env вместо конфига/аргументов. Во-вторых, неосиливание этот env заменить в вызвающем скрипте и вместо этого накидывание ещё костылей в бинарник. В-третьих, вместо того чтобы на старте программы спарсить все эти дефективные env, раскидать по переменным и пользоваться уже ими - подмена env чтобы кто-то где-то внутри его потом парсил. Ужас.

-----

env - это средство задания общесистемных настроек для заранее неизвестного круга процессов, которые кто-то когда-то будет запускать. Абузить их для передачи проге её индивидуальных настроек - костыль и плохо. Абузить их для передачи информации между разными функциями внутри одного бинарника - это вообще дичь.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)
Ответ на: комментарий от LamerOk

А чем централизованное задание переменных окружения существенно проще централизованного создания конфигурационных файлов, благо такие файлы почти всегда нужны?

{
    "server" : "bon14.isske.net",
    "port" : 2814,
    "key" : "ffb6439372838abb5affb"
}
ENV_SERVER=bon14.isske.net
ENV_PORT=2814
ENV_KEY=ffb6439372838abb5affb

На лету CI/CD такой файл не создаст (а кроме того его еще и в нужное место надо деплоить). Если файл уже существует - то заменить в нем например port - уже нетривиальная задача. Плюс проблемы доступа к файлу, права и прочее.

А установить env на контейнер - это ерунда, доступная всегда и везде.

PPP328 ★★★★★
()

@hateyoufeel, уважаю тебя. Такому постоянству, с которым ты закидываешь обсеры сишников и плюсовиков, я думаю, позавидуют многие.

P.S. Для остальных, я сам пишу на плюсах, я не отрицаю что я могу писать говнокод, но чужие ошибки это возможность научится чему то новому

snake266 ★★★
()
Ответ на: комментарий от hateyoufeel

да, именно так. В данном случае наговнокодили и не заметили, что тут не тредсейф, именно растоманы.

Lrrr ★★★★★
()
Ответ на: комментарий от PPP328

На лету CI/CD такой файл не создаст (а кроме того его еще и в нужное место надо деплоить).

Перевожу на русский: «у меня обновлением софта на сервере занимается какая-то дефективная чушь, которая прибита гвоздями к env, поэтому я использую именно его».

firkax ★★★★★
()
Ответ на: комментарий от snake266

hateyoufeel, уважаю тебя. Такому постоянству, с которым ты закидываешь обсеры сишников и плюсовиков, я думаю, позавидуют многие.

Это просто мои любимые шизофреники на ЛОРе. Так-то я сам на Си и плюсцах пишу.

hateyoufeel ★★★★★
() автор топика
Ответ на: комментарий от PPP328

На лету CI/CD такой файл не создаст

Я не понимаю этого утверждения. CI/CD создаёт не то, что файл, а вообще новый инстанс виртуалки / докера. Какие силы зла мешают ему создать несчастный файл?

Если файл уже существует - то заменить в нем например port - уже нетривиальная задача.

Файл заменяется целиком, а не по частям.

Плюс проблемы доступа к файлу, права и прочее.

У нормального CI/CD - отдельно существует образ среды исполнения, отдельно в него разворачивается билд. Я не вижу никакой разницы между копированием файлов сборки и копированием конфига, кроме необходимости программного сгенерировать какое-то значение.

В этом смысле между переменными окружения и файлами разницы нет примерно никакой - основной гемморой это как раз генерация нужного значения. А дальше полученную строку можно как присвоить переменной так и записать в файл.

LamerOk ★★★★★
()
Ответ на: комментарий от hateyoufeel

А вот кстати, хачкеля это тоже касается, он с чем собирается?

zurg
()
Для того чтобы оставить комментарий войдите или зарегистрируйтесь.