воскресенье, 7 февраля 2010 г.

Как может код для предотвращения переполнения буфера в итоге вызывать его?

Это перевод How can code that tries to prevent a buffer overflow end up causing one? Автор: Реймонд Чен.

Если вы прочтёте спецификацию языка C, то увидите, что функции ...ncpy имеют очень странную семантику.

Функция strncpy копирует первые count символов из strSource в strDest и возвращает strDest. Если count меньше или равно длине strSource, то к строке не добавляется нулевой символ. Если же count больше длинны строки strSource, то целевая строка заполняется нулевыми символами до своего конца.
На рисунках ниже показаны различные сценарии копирования строк:
strncpy(strDest, strSrc, 5)
strSource
Welcome\0
strDest
Welco
заметьте: нет терминатора
 
strncpy(strDest, strSrc, 5)
strSource
Hello\0
strDest
Hello
заметьте: нет терминатора
 
strncpy(strDest, strSrc, 5)
strSource
Hi\0
strDest
Hi\0\0\0
заметьте: заполнение нулями до конца strDest
Почему же эти функции имеют такое странное поведение?

Вернитесь назад во времени в ранние дни UNIX. Лично я заведу свою машину времени на времена System V. В System V имена файлов могли иметь длину до 14 символов. Любые имена длиннее усекались до 14-ти символов. А поле для хранения имени файла на диске было ровно 14 байт. Не 15. Терминатор только подразумевался. Это экономило один байт.

Вот несколько имён файлов и их представления в каталоге:
passwd
passwd\0\0\0\0\0\0\0\0
newsgroups.old
newsgroups.old
newsgroups.old.backup
newsgroups.old
Заметьте, что newsgroups.old и newsgroups.old.backup фактически являются одинаковыми файлами из-за усечения. Длинные имена усекались автоматически, втихую, без какого-либо сообщения об ошибке. Это исторически было источником множества ошибок непреднамеренной потери данных.

Функция strncpy использовалась системой для хранения имён файлов в каталоге на диске. Это объясняет часть странного поведения этой функции, а именно: почему она не добавляет нуль-терминатор к результату. Потому что терминатор неявно подразумевается концом массива символов (это также объясняет молчаливое усекание имён файлов).

Но зачем дополнять нулями короткие имена?

Потому что это упрощает поиск имён (делает его быстрее). Если вы гарантируете, что все "мусорные байты" будут нулями, то вы можете использовать функцию memcmp для их сравнения.

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

Так что насчёт заголовка сегодняшнего поста? Как может код для предотвращения переполнения буфера в итоге вызывать его?

Вот один пример (к сожалению, я не понимаю японский, так что я действую, исходя только из кода). Заметьте, что он использует _tcsncpy для заполнения lpstrFile и lpstrFileTitle, при этом осторожно следя за размерами буферов. Это прекрасно, но это также приводит к отсутствию нуль-терминатора, если строка слишком длинна. Вызывающий вполне может потом скопировать этот буфер в какой-то другой. Но в lstrFile отсутствует терминатор, поэтому он превышает длину, указанную вызывающим. Результат: переполнение другого буфера.

А вот ещё один пример. Заметьте, что функция использует _tcsncpy для копирования результата в выходной буфер. Автор был осведомлён о причудливом поведении семейства функций strncpy, поэтому он вручную добавил терминатор в конец буфера.

Но что если ccTextMax = 0? Тогда попытка записи терминатора обратится к символам до начала массива, затирая случайный символ.

Какой можно сделать вывод из всего этого?
Ну, лично мой вывод такой: избегать strncpy и всех её друзей, если вы работаете с нуль-терминированными строками. Несмотря на наличие в их имени "str" - эти функции не создают нуль-терминированные строки. Они конвертируют нуль-терминированную строку в символьный raw-буфер. Использование их там, где на выходе ожидается нуль-терминированная строка, просто неверно. Вы не только не получите терминатора в конце строки, но вы также получите бесполезное для вас дополнение нулями, если строка слишком коротка.

1 комментарий:

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.