?

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:
[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: stas
2005-03-27 04:45 pm
В printf дело скорее не в том, что она медленная, а в том, что большого вреда от неё не будет, если в "непределённых" случаях она будет писать, что захочет. Хотя если вспомнить историю с %n...
(Ответить) (Parent) (Thread)
[User Picture]From: arbat
2005-03-27 05:18 pm
ну, да. СОбственно, для printf - NULL не есть имманентно "неправильный" вход. Для strlen - это однозначно за пределами допустимого диапазона. А printf, да - может вполне последовательно и разумно NULL обработать.
(Ответить) (Parent) (Thread)
(Удалённый комментарий)
[User Picture]From: avva
2005-03-27 06:47 pm
Отлично ;)
(Ответить) (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: yurri
2005-03-27 11:20 am

Re: Reply to your comment...

Если с этой позиции - то вы правы.
(Ответить) (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: pendelschwanz
2005-03-27 03:21 pm
так или эдак = не знаю, но если на С креста нет, то справиться с ним могут только настоящие джигиты!
(Ответить) (Parent) (Thread)
[User Picture]From: avva
2005-03-27 03:34 pm
Но есть и другая философия: "Be strict when sending and tolerant when receiving."
(Ответить) (Parent) (Thread)
[User Picture]From: stas
2005-03-27 04:43 pm
Эта философия хороша, но не всегда. Иногда у неё могут быть очень неприятные последствия - вот например, free, принимающий уже освобождённые блоки, приводит к тому, что поймать случай double free крайне трудно (да, я знаю про debug malloc, valgrind и т.п. - но для примера предположим, что их нет). В то же время, если бы он сразу падал в такой ситуации, то поймать его было бы элементарно - просто запустить под отладчиком и посмотреть backtrace.
Я думаю, можно сказать так - эта философия удобна для пользователя, но не для программиста. Чёткую раницу между этими двумя ипостасями я бы проводить не взялся.
(Ответить) (Parent) (Thread)
[User Picture]From: igorlord
2005-03-27 04:55 pm
This is just one of the philosophies. There are two:
1) In case of an error, try your best to continue working. Reason: a user would rather see a mostly-working program than not working at all.

2) In case of an error, abort completely and immediately. "It takes a human to make mistakes, but a computer to REALLY screw things up". If there is an error already, there is not telling how much that error can propogate and multiply if ignored or masked.

Obviously, which philosophy would would chose depedn on many factors, including:
1) How critical it is to get correct results (very critical in core financials or defense calculations; not too critical in GUI front ends).

2) What is the penalty for aborting (and restarting) and application? (Big penalty for active air defense, for example, and not too bad for computing some financial numbers).
(Ответить) (Parent) (Thread)
(Удалённый комментарий)
[User Picture]From: igorlord
2005-03-27 07:17 pm
No, you completely missed the point.

Of course, no matter what the application is, yuo always want to catch and abort on all errors during debugging (except if you are debugging error recovery).

What I was writing about is how you release the product to your customers. For example, for a GUI application, your cutomers would be more pleased if you were to try to hide errors. It is better if, due to a program bug, one button stops to operate than if the whole application crashes.

However, for databases (the business I am in), it is 1000 times better if the whole things crashes (and either restarted automatically or by the DBA) than if there is even a chance that an error has happened, and some data is lost or currupted without the user noticing it.
(Ответить) (Parent) (Thread)
From: (Anonymous)
2005-03-28 10:05 am
"Be strict when sending and tolerant when receiving." - e'to pro protokoly, an ne pro biblioteki dlja progrmistov, raznica odnako.
(Ответить) (Parent) (Thread)
[User Picture]From: avva
2005-03-28 10:26 am
Есть разница, не спорю.
(Ответить) (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: michk
2005-03-27 12:42 pm
А разве это не предполагается по умолчанию в C? Мне всегда казалось, что ни одна функция не обязана проверять на NULL, а если кто-то проверяет, то это "подарок", и никак не стандарт.
(Ответить) (Parent) (Thread)
[User Picture]From: arbat
2005-03-27 02:12 pm
это не подарок, а нарушение стандарта :-)
(Ответить) (Parent) (Thread)
From: lazyreader
2005-04-05 05:58 pm
Неопределённое поведение не может нарушить стандарт по определению :)
(Ответить) (Parent) (Thread)
[User Picture]From: gdy
2005-03-27 01:52 pm
C99 7.1.4
Each of the following statements applies unless explicitly stated otherwise in the detailed
descriptions that follow: If an argument to a function has an invalid value (such as a value
outside the domain of the function, or a pointer outside the address space of the program,
or a null pointer, or a pointer to non-modifiable storage when the corresponding
parameter is not const-qualified) or a type (after promotion) not expected by a function
with variable number of arguments, the behavior is undefined. If a function argument is
described as being an array, the pointer actually passed to the function shall have a value
such that all address computations and accesses to objects (that would be valid if the
pointer did point to the first element of such an array) are in fact valid.

В 7.9.6.1 (fprintf) ничего про NULL не видно
(Ответить) (Parent) (Thread)
[User Picture]From: gdy
2005-03-27 02:02 pm
А posix это повторяет
Each of the following statements shall apply unless explicitly stated otherwise in the detailed descriptions that follow:
  1. If an argument to a function has an invalid value (such as a value outside the domain of the function, or a pointer outside the address space of the program, or a null pointer), the behavior is undefined.
(Ответить) (Parent) (Thread)
[User Picture]From: avva
2005-03-27 02:07 pm
Да, спасибо, я тоже уже эту цитату нашёл в каком-то споре по этому поводу сетевом. Но всё равно спасибо ;)
(Ответить) (Parent) (Thread)
[User Picture]From: dvv
2005-03-27 08:16 pm
А не проще цитаты из стандарта искать в стандарте, а не в каких–то спорах?
(Ответить) (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: gdy
2005-03-27 02:27 pm
хи-хи
(Ответить) (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: avva
2005-03-27 02:10 pm
А вот glibc'шный ;)
size_t
strlen (const char *str)
{
  int cnt;

  asm("cld\n"			/* Search forward.  */
      /* Some old versions of gas need `repne' instead of `repnz'.  */
      "repnz\n"			/* Look for a zero byte.  */
      "scasb" /* %0, %1, %3 */ :
      "=c" (cnt) : "D" (str), "0" (-1), "a" (0));

  return -2 - cnt;
}


Правда, есть ещё специализированные версии для i486 и i586, там ассемблер посложнее ;)
(Ответить) (Parent) (Thread)
[User Picture]From: sobaker
2005-03-27 05:48 pm
А, пардон, в FreeBSD i386 тоже repnz scasb, а то, что я привел - portable-версия :)
(Ответить) (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)
[User Picture]From: dimrub
2005-03-27 10:20 pm
Мне кажется, тут ведь не уголовный суд, и главное не установить, кто прав, кто виноват, а сделать так, чтобы свести до минимума количество ошибок, и время на отладку в ситуациях, когда ошибка все же произошла. В данной ситуации (поскольку strlen не возвращает статус) сделать ничего нельзя, пожалуй (то есть, виновата вызывающая сторона). Но если бы я разрабатывал данный интерфейс с нуля, я бы скорее всего сделал так, как сделали разработчики COM: ВСЕ методы возвращают статус. Тогда бы у меня была возможность как минимум "to be liberal with the inputs I accept & to be conservative with outputs I produce".
(Ответить) (Thread)