
19 июня Торвальдс влил merge, который вычистил из ядра Linux функцию strncpy. Шесть лет работы, 362 коммита, семьдесят человек. Ради одной функции из стандартной библиотеки C.
Звучит как анекдот про бюрократию. На деле это наглядный разбор того, почему «просто заменить небезопасную функцию» в C — совсем не «просто», даже когда принципиального спора о замене нет.
Почему её считают «безопасной»
Из-за буквы n. strcpy копирует до нуля и легко улетает за буфер, а strncpy(dst, src, n) берёт максимум n байт — выглядит как та же функция, но с ремнём безопасности. Её и в самом ядре нашли в сотнях мест; что уж говорить про прикладной код.
Вера ложная. man про strncpy пишет прямым текстом: функция создаёт «null-padded character sequence, not a string». Это не про строки вообще. Появилась она в Edition 7 AT&T Unix около 1979 года — под имена файлов в directory entries, где поле фиксированной ширины и могло содержать ноль, один или несколько хвостовых нулей. Такие fixed-width поля живы и сейчас, но это редкий частный случай — а strncpy сорок лет тащат в обычный код как «strcpy, который не стреляет в ногу».
Стреляет, просто тихо. Если strlen(src) >= n, то strncpy запишет ровно n байт и не поставит завершающий \0. Дальше любой strlen, strcmp или printk("%s", …) уходит читать за буфер: мусор, чужая память, в плохой день — утечка данных. А если наоборот, strlen(src) < n, остаток буфера до n забивается нулями: скопировали четыре байта в буфер на четыре килобайта — получите 4092 лишних записи на ровном месте. В документации ядра API так и назвали — ambiguous и fragile: по одному вызову не понять, чего хотел автор. Строку с нулём? Padding? Поле фиксированной ширины?
Где зарыты шесть лет
Замена — это не «найти и поменять на безопасный аналог». Безопасного аналога нет. Под разные намерения — свой набор: strscpy() для нормальной C-строки с \0, strscpy_pad() — то же с обнулением хвоста, strtomem() / strtomem_pad() для полей фиксированной ширины без терминатора, memcpy_and_pad() для bounded-копии из источника, который может быть не завершён нулём, memtostr() / memtostr_pad() для обратного случая, ну и обычный memcpy(), если это вообще не строка. Восемь функций там, где раньше была одна.
И sed -i 's/strncpy/strscpy/g' тут не работает в принципе, потому что верный ответ зависит от намерения автора:
/* имя в поле фиксированной ширины внутри on-disk структуры — \0 не нужен */strncpy(de->name, name, sizeof(de->name)); // → strtomem_pad()/* а это уйдёт в printk("%s", buf) — без \0 здесь read-overflow */strncpy(buf, src, sizeof(buf)); // → strscpy()
Один и тот же вызов, разный верный ответ. И так 362 раза: каждый надо открыть и прочитать глазами — это строка или бинарное поле, нужен ли ноль, нужен ли padding, завершён ли источник, известен ли размер приёмника на компиляции.
Вот почему тянулось с 2019-го. Не потому что был спор — затея шла под крылом Kernel Self-Protection Project Киса Кука, который выпиливает не отдельные баги, а целые классы уязвимостей. А потому что 362 раза требовалось ручное решение, и одной автоправкой его не выразить. Больше всех разгрёб Джастин Ститт — 211 коммитов из этих 362.
В вашей C/C++ базе strncpy скорее всего живёт прямо сейчас, и кто-то когда-то вписал её именно как «безопасный вариант». Это не баг в трекере — это код, который надо перечитать. И если соберётесь чистить, главная работа будет не в замене функции, а в том, чтобы понять, что в каждом буфере вообще лежит. Ядру на это хватило шести лет и семидесяти человек — на API, который десятилетиями выглядел как безопасная версия strcpy.
ссылка на оригинал статьи https://habr.com/ru/articles/1050746/