?

Log in

No account? Create an account
программистское - Поклонник деепричастий [entries|archive|friends|userinfo]
Anatoly Vorobey

[ website | Website ]
[ userinfo | livejournal userinfo ]
[ archive | journal archive ]

Links
[Links:| English-language weblog ]

программистское [мар. 27, 2005|11:43 am]
Anatoly Vorobey
(интересно только программистам, знающим C)

Майкрософтовский strlen(), оказывается, не проверяет, не передали ли ему случаем NULL, а по-простому читает то, что по переданному адресу находится, и падает кверху лапками.

Если майкрософтовскому printf'у передать NULL в качестве аргумента, соответствующего %s в строке формата, он это проверит и выдаст "(null)".

Однако у Glib'а (библиотека низкого уровня, лежащая в основе GTK, в основном использующаяся на юниксах, но по идее поддерживающая и платформу Win32) есть внутри своя имплементация printf'а (а точнее, vasnprintf()'а, чтобы покрыть все возможные случаи). Она не делает полностью всю работу printf'а, а всего лишь обрабатывает строку формата и аргументы и строит отдельные более простые вызовы snprintf/sprintf (хотя бы sprintf уже должен быть везде) для каждого аргумента, размещая их по очереди в буфере возврата. Glib использует эту имплементацию в том случае, когда у платформы, на которой её строят, нет своего достаточно хорошего и поддерживающего всё, что нужно, printf'а. Win32 - одна из таких платформ.

Если этому внутреннему Glib'овскому printf'у передать NULL в качестве аргумента, соответствующего %s в форматной строке, то он в определённый момент вызовет strlen() на этот аргумент, чтобы узнать длину строки аргумента - для того, чтобы создать буфер достаточной длины. Увы, авторам этого printf'а не пришло в голову, что strlen на какой-то платформе может упасть, если передать ему NULL. Поэтому они это не проверяют, и в результате, если вызвать что-то вроде: g_printf("foo: %s\n", NULL), то программа упадёт.

Это всё я обнаружил, пытаясь построить и заставить что-то делать ЖЖ-клиент LogJam под Windows XP.

Интересно, кто виноват? Аппликация, передающая какой-либо версии printf'а аргумент NULL для подстановки к %s? Имплементация printf'а, вызывающая strlen(), не проверяя аргумент на ==NULL? Имплементация strlen(), идущая сразу по адресу аргумента, не проверяя на NULL?

P.S. Проверил strlen() на юниксах (glibc). Он тоже, оказывается, не проверяет на NULL. Но там тот же баг не проявлялся потому, что GLib использует системный printf, а не свой, а системный это дело проверяет.
СсылкаОтветить

Comments:
Страница 1 из 2
<<[1] [2] >>
[User Picture]From: valshooter
2005-03-27 10:33 am
Я так подозреваю, что все хороши.
(Ответить) (Thread)
[User Picture]From: stas
2005-03-27 10:36 am
Насколько я знаю, никто не обязывает strlen проверять NULL - NULL не есть легитимная строка. То, что printf печатает (null) - это неожиданная удача (точнее, подарок от авторов libc криворуким программистам ;), но полагаться на это, ПМСМ, ни в коем случае не следует.
В данном случае виновата, конечно, программа, передающая printf-у NULL. Но виноват также и glib, не полностью поддерживающий интерфейс printf, то есть не проверяющий строки на NULL.
(Ответить) (Thread)
[User Picture]From: arbat
2005-03-27 02:18 pm
я бы добавил следующее: и не надо обязывать strlen проверять на NULL. Предположим, проверили, ай-яй-яй, это - NULL. Ну и? Что теперь делать? Exceptions в C не предусмотрены. Возвращаем мы size_t, т.е. совместить возврат осмысленного числа с вовзратом кода ошибки - не удастся. Ну, или - ценой потери половины диапазона заменой типа возврата на int, или ценой возврата, скажем, максимально значения в качестве кода ошибки, надеясь, что программист, который забыл проверить параметр на NULL - не забудет проверить возвращаемый код, да еще не ошибиться - как. Но, главное - это делать абсолютно незачем. Если клиент функции хочет сделать проверку, он может. Если он не хочет, одна из главных идеологий C состоит в том, что клиент не должен платить за то, что он не заказывал. Потому, вполне правильный выбор - не проверять. При вызове с NULL обещать - undefined behavior.

printf... ну, это, предположительно, функция медленная. Ей по барабану потратить чуток времени на NULL проверить, да и ответ на вопрос "что же делать, если это NULL?" там более понятен. Что про нее стандарт говорит - не знаю.
(Ответить) (Parent) (Thread) (Развернуть)
(Удалённый комментарий)
[User Picture]From: mff
2005-03-27 10:39 am
Я бы сказал, что виноват код вызывающий strlen(), не проверяя аргумент на ==NULL. Правильнее один раз упасть на strlen(NULL), чем ловить плавающую ошибку потом.

И еще -- мне кажется, что семантику strlen() нельзя естественным образом расширить на NULL.
(Ответить) (Thread)
[User Picture]From: sobaker
2005-03-27 01:36 pm
можно отдавать -1 :)

ух, какие волшебные спецэффекты это может вызвать!..
(Ответить) (Parent) (Thread)
[User Picture]From: yurri
2005-03-27 10:53 am
Проверка параметра на корректность должна делаться внутри вызываемой функции, а не вне её, и в случае несоответствия функция должна вернуть оговоренное ошибочное значение или разбудить исключение.

Так что виновата самая нижняя в иерархии strlen().

С другой стороны, прикладной код, вызывающий printf с NULL, тоже заслуживает внимания разработчика.
(Ответить) (Thread)
[User Picture]From: zimopisec
2005-03-27 11:16 am
Это было бы правильно для "безопасного" языка типа джавы , C# и прочих, несть им числа, но противоречило бы идеологии низкоуровневого С. Вся суть которого- не делать за программиста ничего лишнего, точнее, вообще ничего, кроме избавления последнего от необходимости писать на ассемблере.
Виновата вызывающая программа, на 100%
(Ответить) (Parent) (Thread) (Развернуть)
[User Picture]From: pendelschwanz
2005-03-27 10:57 am
Настоящие джигиты не падают при нулевых аргументах.
(Ответить) (Thread)
[User Picture]From: stas
2005-03-27 02:17 pm
Как раз наоборот - настоящие джигиты падают немедленно, обнаружив некорректный аргумент, который не предусмотрен интерфейсом функции (поскольку другого способа обработки исключений в C нет). Тогда, запустив программу в отладчике, можно узнать, что же случилось и где оно случилось. Если же вместо этого будет возвращено какое-нибудь левое значение, которое пойдёт гулять дальше по программе, то обнаружить, где же случилась проблема, станет задачей гораздо более сложной.
(Ответить) (Parent) (Thread) (Развернуть)
(Удалённый комментарий)
[User Picture]From: bod_hi
2005-03-27 11:00 am
разумеется, аппликация.
передавать printf`у NULL для %s (ровно как и strlen`у) является нарушением POSIX стандарта
(Ответить) (Thread)
[User Picture]From: avva
2005-03-27 11:07 am
В POSIX'овском определении этих функций ничего такого не написано.
(Ответить) (Parent) (Thread) (Развернуть)
[User Picture]From: mtyukanov
2005-03-27 11:00 am
Главная вина -- на программе.

Хорошая программа не должна прятать свои ошибки. Если strlen будет обрабатывать null, он пойдет все дальше и дальше -- и может в конце концов серьезно попортить юзеру данные. Когда его обрабатывают printfы и пишут (null) -- это хотя бы явно видно, и есть шанс, что ошибка не будет пропущена. А когда он просто крадется незаметно -- может оказаться, что программа будет на вид как живая, только вот иногда невоспроизводимо падать.

Идеально было бы поставить неотключаемый в релизе assert(s). Паша Сенаторов когда-то написал очень хорошую статью в SU.SOFTW, пропагандировавшую такую технику. Я потом поработал с его кодом и кодом на основе его идеологий -- действительно, очень хорошо (ну, там еще и C++ и assert кидает исключение.)
(Ответить) (Thread)
From: 314truha
2005-03-27 11:41 am
На мой неискушённый взгляд проблема не в том что strlen() падает, или в том что printf печатает (null) вместо падения, а в непоследовательности поведения разных функций одной и той же библиотеки - а это в данном случае MSVCRT.
(Ответить) (Thread)
(Удалённый комментарий)
[User Picture]From: sobaker
2005-03-27 01:35 pm
Я согласен, что это макроассемблер, но Boehm GC - очень и очень неплох :)
(Ответить) (Parent) (Thread)
[User Picture]From: mfi
2005-03-27 12:31 pm
Насколько я помню - отсутствие проверки на NULL ( и не принятие NULL как пустой строки ) в str функциях - это ANSI стандарт. Мы в свое время на этом хорошо сыпались при портинге с HP на SUN.

П.С. Оффтопик. Я Вам как то обещал ссылку из медицинской литературы на простуду и ее связь с холодом. Отмена, я неправильно понял своего информатора.
(Ответить) (Thread)
[User Picture]From: dimas
2005-03-27 01:04 pm
1. Берем стандарт и смотрим. Ни про strlen, ни про printf никто не обещал специального поведения для NULL. Итого, кто не проверяет что подсовывает на вход этим фунциям - ССЗБ.

2. В документации на g_printf написано что можно использовать NULL? Если нет - см. п.1.

3. Программисть, использующий недокументированные функции/расширения/отступления от стандарта ССЗБ, а если и не комментирует такие места - увольнять без выходного пособия.

4. Сколько бы не гнобили С++ за его якобы сложность, а вот такого рода ошибок, если не пользоваться С-ным насделием, там в разы меньше.

5. Эх, я так понимаю ошибки "нулевых указателей" и прочих "переполнений буффера" умрут только вместе с Си ...
(Ответить) (Thread)
[User Picture]From: potan
2005-03-28 08:14 am
4. Ох сколько я видел программ, которые проверяют указатель на объект на NULL во всех местах, кроме одного :-))).

В "правильных" языках, типа Cyclone есть несколько типов для указателей - у некоторых NULL входит в диапазан допустимых значений, у некоторых нет. Вот где настоящая защита от ошибок, а не в гнилом C++.
(Ответить) (Parent) (Thread)
[User Picture]From: sobaker
2005-03-27 01:33 pm
Да вроде как никто не проверяет.. Вот кусочек FreeBSD's libc:

size_t strlen(const char *str) {
register const char *s;
for (s = str; *s; ++s);
return(s - str);
}

И мне этот подход в чем-то симпатичен. Не расслабляет.
(Ответить) (Thread)
[User Picture]From: sobaker
2005-03-27 01:34 pm
Справедливости ради (или контраста для?), FreeBSD's printf("%s", NULL) печатает "(null)" :)
(Ответить) (Parent) (Thread)
[User Picture]From: gaius_julius
2005-03-27 03:41 pm
"Аппликацию" в руском языке принято называть приложением.

Забываем-с? (-:
(Ответить) (Thread)
[User Picture]From: avva
2005-03-27 03:50 pm
Забываем :)
(Ответить) (Parent) (Thread)
[User Picture]From: yanis
2005-03-27 04:45 pm
самый верхний уровень виноват, как и всегда в таких случаях. твоя программа или ты чью-то конфигурируешь? если твоя то не передавай нули куда не положено, а если чужая - может перегрузить vasnprintf этот - написать свой отдельный модуль и воткнуть в линк самым первым?
(Ответить) (Thread)
[User Picture]From: avva
2005-03-27 05:17 pm
Не моя, но я просто патч к ней сделаю, не проблема.
(Ответить) (Parent) (Thread)
[User Picture]From: oxfv
2005-03-27 09:46 pm
А вот, например, дебажная и релизная версии программ в ВЦ++ под Win32 по-разному инициализируют неинициализированные переменные. Если strlen проверял бы пойнтер на ноль, то не должен ли бы он был проверять его на 0xcdcdcdcd или чем там инициализируется пойнтер поначалу? А если вспомнить, что разные инструменты типа BoundsChecker'a инициализируют такие переменные чем пожелаешь, становится вообще уныло при мысли, что strlen будет икать от нулевого пойнтера, но падать от другого заведомо бессмысленного пойнтера. Вывод: ответственность за проверку должна быть на вызывающем коде.
(Ответить) (Thread)
Страница 1 из 2
<<[1] [2] >>