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.

Комментарии (129)


  1. ch1971
    23.06.2026 07:49

    да и вообще эти null-terminated string это вещь про которую можно сказать что если бы её никогда не существовало то всем на белом свете было бы легче жить)

    Помню они меня так достали в C++ что я написал класс обёртку c операторами приведения во все разумные типы чтобы вообще никогда их не использовать... а потом пол года ещё дебажил сам этот класс)


    1. wl2776
      23.06.2026 07:49

      Дыкть, сам null назван его автором, Тони Хоаром, ошибкой на миллиард долларов.


      1. tenzink
        23.06.2026 07:49

        Там речь про nullable значения, а не про null-terminated строки


      1. ch1971
        23.06.2026 07:49

        А вот с этим как раз не соглашусь. NPE это сигнал что программа работает не так как должна. И чем раньше это выяснится тем лучше. А вот если NPE будет както замаскированно или автоисправлено или ещё чтото подобное и программа с ошибкой "на борту" похромает кудато дальше то кончиться всё это может хуже чем просто поиск ошибки в коде.


        1. wl2776
          23.06.2026 07:49

          Я тут смешал две больших проблемы (которые существовали, кажется, ещё до моего рождения:) ): как обозначить, где кончается строка, и как обозначить, что указатель никуда не указывает.

          Для решения обеих чаще всего используется 0 (или null).


        1. andreymal
          23.06.2026 07:49

          И чем раньше это выяснится тем лучше.

          Раньше — это во время компиляции. NPE, вывалившийся в рантайме на проде — это слишком поздно. (И ещё хорошо если в принципе вывалившийся, а не по-тихому портящий память...)


          1. ch1971
            23.06.2026 07:49

            Конечно на этапе компиляции это ещё лучше чем при выполнении. Но разговор то про то что сама концепция вредоносна. Вон например в Котлине есть возможность завести типы которые не могут быть null то есть вынуждают заполнять неинициализированные ссылки ссылками на какой нибудь dumb а потом какой нибудь модуль может решить что этот dumb вполне себе "живой" объект и начать с ним работать ну и пошло поехало...

            кстати ну вот не припомню чтобы null поганил память если можно обрисуйте механику...


            1. andreymal
              23.06.2026 07:49

              У котлина null safety из коробки, в нём такой проблемы изначально нет. Ну а если кто-то принципиально игнорирует null safety и лепит dumb объекты — ну штош, защита от дурака не спасает от долб слишком изобретательных дураков ¯\_(ツ)_/¯


            1. wl2776
              23.06.2026 07:49

              типы которые не могут быть null то есть вынуждают заполнять неинициализированные ссылки ссылками на какой нибудь dumb

              Почему обязательно ссылки и обязательно на dumb? Деталей Вы не указали, но я бы к архитектуре вопросы поднял, мне такой подход странным кажется.

              кстати ну вот не припомню чтобы null поганил память если можно обрисуйте механику...

              В каких-нибудь embedded адрес 0x00 вполне себе легальный адрес.

              И вот еще, статья 10 летней давности на тему разыменования null.

              upd. В той статье выводов мало, но вот есть еще: https://habr.com/ru/companies/contentai/articles/205070/

              В целом, есть варианты, когда null пролезет и притворится валидным указателем.


              1. ch1971
                23.06.2026 07:49

                Спасибо за информацию. Никогда не знал что через null можно обратиться к методу. Как страшно жить)...

                а про ссылки равные null ну например разреженный массив но чтобы не в виде коллекции с перегруженным [] а именно с индексным доступом.


    1. legolegs
      23.06.2026 07:49

      Не соглашусь. Это очень изящное и экономичное решение для множества задач (т.е. кроме текстовых редакторов). А ещё переход всего FOSS на UTF-8 не дался бы так легко с другими более сложными типами строк (см винду и трудности с её UTC32, _w -функциями в API и т.д.).


  1. Deosis
    23.06.2026 07:49

    null-terminated string появились во времена, когда даже 2 байт на длину строки было многовато по памяти.


    1. MinimumLaw
      23.06.2026 07:49

      А олды помнят еще и $-терминированные строки. Как по мне вопрос не в размере, вопрос в однобайтовой ASCII кодировке. В те времена, когда она была достаточной этого хватало. Сегодня... Сегодня сложно. Местами давно надо заменить, но сказать проще чем сделать.


      1. blind_oracle
        23.06.2026 07:49

        Мне кажется кодировка тут ортогональна.

        Различия просто в том как делать: length-value или null-terminated.

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


        1. CitizenOfDreams
          23.06.2026 07:49

          Различия просто в том как делать: length-value или null-terminated.

          Наверное, length-value... если мы уверены, что эта length будет подсчитываться правильно. Сколько, говорите, байтов занимает одна буква?

          Или все-таки null-terminated... если мы уверены, что не забыли поставить этот null в нужном месте. Сколько, говорите, байтов занимает одна буква?


          1. randomsimplenumber
            23.06.2026 07:49

            С нулем прелесть в том, что некоторые функции гарантируют 0 в конце буфера, некоторые нет, некоторые забивают нулями буфер до конца. Походу проектировались они разными людьми.


          1. unreal_undead2
            23.06.2026 07:49

            Сколько, говорите, байтов занимает одна буква?

            str* работают с последовательностями байтов, кодировки на следующем уровне абстракции.


          1. blind_oracle
            23.06.2026 07:49

            В Си никаких уникодов нет и не было, особенно когда эти null-terminated изобретались.

            А сейчас да, до сих пор умудряются проблемы не этом получать. К примеру в Расте есть у String (который хранит UTF-8) безобидный метод truncate(n), который работает не по кодовым точкам, а блин по байтам (чтобы быть O(1)) и если ему указать байт посреди кодовой точки - просто паникует.

            Хотя могли бы сделать вот что-нибудь такое и оно было бы тоже O(1):

            pub fn truncate(s: &str, n: usize) -> &str {
                let n = s.len().min(n);
                let m = (0..=n)
                    .rfind(|m| s.is_char_boundary(*m))
                    .unwrap_or_default();
                &s[..m]
            }

            Резало бы по ближайшей кодовой точке.


        1. Apoheliy
          23.06.2026 07:49

          Подозреваю, что дело не только в паре байт для указания длины.

          null-terminated строки позволяли делать разбор текста (например, когда его вычитали из файла в память) не тратясь на дополнительное копирование, прямо на живом буфере. Натыкал нулей вместо разделителей - и вот тебе отдельные строки (привет, strtok). В те далёкие времена (да и сейчас, в общем-то) такая экономия была существенной.

          Если же строки будут с полем длины, то такой финт уже не провернёшь и придётся копировать-перекладывать много-много данных.

          (По правильному, ) один из хороших вариантов: для строки хранить два указателя (типа, как структура): начало и символ-за-концом. Но что-то "не взлетает".


          1. Belarus
            23.06.2026 07:49

            Но что-то "не взлетает".

            В каком смысле, если так и устроены вектры в С++?


            1. tenzink
              23.06.2026 07:49

              А еще span, string_view и половина stl, построенная вокруг пар итераторов


          1. strvv
            23.06.2026 07:49

            нет, скорее всего упрощение программирования - проверка не нуль одна из базовых функций АЛУ, поэтому есть везде. вот и реализуема. попробуй на другой символ-терминатор - заклюют и заплюют.


        1. winorun
          23.06.2026 07:49

          Если мы используем length-value мы имеем максимальный размер строки. null-terminated же позволяет иметь строки не ограниченной длинны.


          1. Wesha
            23.06.2026 07:49

            null-terminated же позволяет иметь строки не ограниченной длинны.

            Вот только память, падла, не позволяет.


            1. strvv
              23.06.2026 07:49

              особенно страничная!


              1. unreal_undead2
                23.06.2026 07:49

                Ограничения адресного пространства никуда не деваются.


          1. unreal_undead2
            23.06.2026 07:49

            Понятно, что правильный тип length - это size_t или подобное.


            1. strvv
              23.06.2026 07:49

              а если длина больше чем 2**size_t-1?


              1. unreal_undead2
                23.06.2026 07:49

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


            1. winorun
              23.06.2026 07:49

              size_t на 128 битах будет 8 байт, на каждую строку. Даже если она 2 байта. И тут можно дойти до идеи коротких и длинных строк.


              1. unreal_undead2
                23.06.2026 07:49

                Про влияние оверхеда на хранение длины на выбор null-terminated vs length-based здесь в коментах уже больше десятка раз написали )

                А так то для коротких строк в плюсах давно уже SSO придумали, оно и лишнюю аллокацию в куче убирает.


                1. winorun
                  23.06.2026 07:49

                  Я тут имел ввиду не оверхет, а разрастание количества типов.


        1. strvv
          23.06.2026 07:49

          Я сейчас не вспомню, почти 30 лет назад, но имхо у DEC VAX/VMS штатным типом строки было 2 байта префикса (длина) и потом строка. И, скорее всего, в PDP11 осях также, оттуда скорее всего пошло. так что в принципе, не такой и большой оверхед. это просто проще программировать - проверка на 0 в АЛУ - базовая, и есть во всех ассемблерах.


          1. unreal_undead2
            23.06.2026 07:49

            C null-terminated можно дёшево работать с подстроками - для обработки хвоста можно просто передать указатель на первый символ посередине оригинала, в случаях посложнее можно временно занулить какой-нибудь символ. С явной длиной это подороже.


            1. strvv
              23.06.2026 07:49

              Опять же это благодаря условию если не ноль в алу.


            1. strvv
              23.06.2026 07:49

              Не сильно. Если глядеть на весь оверхеэд с этими гигантскими библиотеками-фреймворками о всём и ничем, из которых используется пара функций, некоторые ранее просто замещались макросом в инклюде… То не сильно. В принципе в любом решении можно найти как плюсы так минусы. Так и выбор подстроки не до конца в твоём предложении дает неоднозначный эффект, а в случае с префиксом длины унифицированный вариант. Код проще и быстрее. Просто например в с++ вызывается конструктор, в него по принципу CoW передается указатель на начало строки, и заполняется префикс=если длина старой - позиция < длины подстроки ? Разница : длина подстроки. И всё


              1. unreal_undead2
                23.06.2026 07:49

                Это сейчас библиотеки-фреймворки, а когда то каждый нормальный программист считал байты и такты.


                1. Wesha
                  23.06.2026 07:49

                  Ну, хоть кто-то меня нормальным назвал.


          1. Wesha
            23.06.2026 07:49

            И, скорее всего, в PDP11 осях также

            Нет, не так. PDP-11 MACRO-11 Language Reference Manual , стр. 6-21, параграф 6.3.5


            1. strvv
              23.06.2026 07:49

              Спасибо, напомнили о теплых ламповых документах, с шрифтом под печатную машинку


      1. xenon
        23.06.2026 07:49

        А олды помнят еще и $-терминированные строки

        "Я не вспоминал int 21h N дней."

        Сбрасываю счетчик на ноль. Снова 20-30 лет его копить буду....


        1. Revertis
          23.06.2026 07:49

          Ну и зачем вы нам о нём напомнили? :)


          1. strvv
            23.06.2026 07:49

            чтобы и Вы сбросили ватчдог. а то переполнение или ещё какое-либо прерывание невовремя прилетит по данному факту?


            1. Revertis
              23.06.2026 07:49

              Ладно :-/


    1. LeraKholod
      23.06.2026 07:49

      Сейчас мы расплачиваемся за эту экономию бесконечными buffer overflow уязвимостями. Длина в начале строки решила бы кучу проблем с безопасностью


      1. Wesha
        23.06.2026 07:49

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


        1. unreal_undead2
          23.06.2026 07:49

          Это вы про Эльбрус? ;)


          1. Wesha
            23.06.2026 07:49

            Это вы про Эльбрус? ;)

            Нет, это я не про гору.

            Это я чуть менее, чем про все эксплоиты, заточенные на переполнение локальных массивов или строк.

            А с «Эльбрусом» не работал, увы.


            1. unreal_undead2
              23.06.2026 07:49

              Я "Эльбрус" живьём только в музее Яндекса грязными руками трогал, но другого железа с раздельными стеками физически вроде не видел.


              1. Wesha
                23.06.2026 07:49

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

                Так вот, если временные переменные и адреса возврата будут на разных стеках, то такой финт ушами не проканает.


                1. unreal_undead2
                  23.06.2026 07:49

                  Это понятно, мысль не новая - но с реализациями в железе негусто. На iapx432 (1981 год) вроде тоже такое было - но это скорее экземпляр для кунсткамеры.


                  1. Wesha
                    23.06.2026 07:49

                    А зачем обязательно в железе? «Стек данных» можно и программно двигать.


                    1. randomsimplenumber
                      23.06.2026 07:49

                      Без аппаратного SP уныло будет


                      1. Wesha
                        23.06.2026 07:49

                        А в чём проблема-то? Заводим ячейку, назваем её SPd; обычно для выделения места под временные переменные добавляем требуемый объём к SP — а тут будем добавлять к SPd, всего-то. Да, будет работать чуть-чуть медленнее. Зато управление не будет передаваться куда не положено.


                      1. randomsimplenumber
                        23.06.2026 07:49

                        Возможность испортить данные в другом фрейме никуда не денется.


                      1. Wesha
                        23.06.2026 07:49

                        Совершенно верно — но испорченные данные в гораздо меньшем количестве случаев могут привести к эффектам, к которым приводит переход по указанному адресу.


                      1. unreal_undead2
                        23.06.2026 07:49

                        Да, будет работать чуть-чуть медленнее

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


                      1. Wesha
                        23.06.2026 07:49

                        если какая то функция вызывается каждую тысячу-другую тактов

                        ...то у вас есть две проблемы.


                1. strvv
                  23.06.2026 07:49

                  Да, но надо слишком многое переделывать, в первую очередь компиляторы. На дековских можно было любой регистр использовать косвенный с автоинкрементом/декрементом, А здесь вместо push/pop городить конструкции


                  1. Wesha
                    23.06.2026 07:49

                    Так пусть они определятся, что им важнее — ускорить выполнение программы на 0,5% или не дать китайским хакерам получить доступ к документам Пентагона.


                    1. strvv
                      23.06.2026 07:49

                      А это им надо, защищать? Это на низовых уровнях думают, морщат мозг или спрашивают чатгпт. А на верхних как сова-стратег.


                      1. Wesha
                        23.06.2026 07:49

                        А, ну да — все заняты, все при деле...


              1. beerchaser
                23.06.2026 07:49

                Да ладно… Любой процессор Гарвардской архитектуры. Микроконтроллеры Pic например. Куда их только не засовывали.


                1. Wesha
                  23.06.2026 07:49

                  Куда их только не засовывали.

                  «Господа гусары, ВСЕМ МОЛЧАТЬ!!!» ©


                1. unreal_undead2
                  23.06.2026 07:49

                  Там код и данные разделяются, насчёт стеков не уверен - не писал под них.


    1. Mox
      23.06.2026 07:49

      Ходят легенды про язык тех времен, где в строке первый байт определял ее размер.

      Я думаю что это реально неплохое решение по сравнению с null terminated, сразу точно знаешь сколько памяти выделено.


      1. randomsimplenumber
        23.06.2026 07:49

        256 байтов тогда всем хватало ;)

        Зато любая операция со строками == неочевидная пляска с выделением памяти под капотом. Или как там оно внутри устроено?


        1. unC0Rr
          23.06.2026 07:49

          Тип строки имел фиксированный размер. Т.е. строка без указания максимального размера занимала 256 байт, с указанием размера - этот размер + 1. Преобразования между разными типами очевидны.


      1. dvmuratov
        23.06.2026 07:49

        Паскаль? Так и сейчас в Delphi размер перед данными строки но по отрицательному смещению + магия компилятора чтобы всю эту кухню скрыть с глаз долой.


        1. LAutour
          23.06.2026 07:49

          Заодно это дает возможность нулевые коды в любом месте строки использовать..


      1. SpiderEkb
        23.06.2026 07:49

        Это "паскалевские строки".

        Последние 8 лет работаю с языком, где два типа строк - char и varchar.

        char - обычный буфер заданной длины. И больше туда не впихнуть

        dcl-s s1 char(5);
        dcl-s s2 char(10);
        
        s2 = '0123456789';
        s1 = s2; // s1 = '01234' - что не влезло, то не влезло
        
        s2 = s1; // s2 = '01234     ' - остаток забивается пробелами

        При все желании за границу не вылезешь.

        Но...

        %len(s1); // всегда 5
        %len(s2); // всегда 10

        а чтобы узнать реальную длину (без хвостовых пробелов) надо

        %len(%trimr(s2));

        varchar - фактически структура

        dcl-s vs varchar(10);

        в С будет представлено как

        struct t_varchar {
          unsugned short len;
          char data[10];
        } vs;

        и тут уже %len вернет реальное значение поля vs.len

        И все это достаточно безопасно. Особенно с учетом того, что здесь не бывает неинициализированных переменных - компилятор автоматически инициализирует любую объявленную переменную дефолтным для ее типа значением (если явно не указано иное значение) - для char это "пустая" (заполненная пробелами) строка, для varchar это строка с len = 0, для числовых типов 0.


    1. AndreyDmitriev
      23.06.2026 07:49

      null-terminated string появились во времена, когда даже 2 байт на длину строки было многовато по памяти.

      В общем да, хотя в 80-е был Паскаль и там уже были строки (хотя в оригинальном Паскале семидесятых их не было), а потом Ansi появились, в девяностых вроде, я как раз тогда Си учил после Паскаля, и сишные нуль-термиированные строки вызвали некоторое недоумение, было дело. Потом я на Дельфи работал. В общем дело привычки и аккуратности.


  1. kunix
    23.06.2026 07:49

    Че-то прошли мимо того факта, что strncpy работает с байтами, а не с UTF-8 и поэтому может разрезать символ посредине. Хотя да, по сравнению с отсутствием нуля это мелочи. Ужасная функция :)


    1. denisgrigoriev04
      23.06.2026 07:49

      Не думаю, что в 95% кода всего ядра нужно что-то кроме ascii


      1. randomsimplenumber
        23.06.2026 07:49

        Драйвер ФС может работать с utf8.


        1. unreal_undead2
          23.06.2026 07:49

          Кодировки для FS в ядре - это костыль для FAT и подобного, чтобы вместо CP866 показывать KO8-R. В нативных FS имена файлов - просто последовательности байтов, скажем никто не мешает использовать невалидную utf-8 последовательность.


        1. glebliutsko
          23.06.2026 07:49

          В linux драйвер ФС работает с байтами. Разрешены любые байты кроме 0x00 и 0x2F (Слеш /)

          Название файла может даже не быть корректной кодировкой. Все преобразования уже userspace происходят.


    1. aulitin
      23.06.2026 07:49

      Не может, байты продолжения в UTF8 всегда с 10хххххх начинаются


      1. kunix
        23.06.2026 07:49

        Ну ок.
        А если буфер src закончился посреди символа?
        Каким образом эта функция учтет UTF-8?
        https://github.com/torvalds/linux/commit/079a028d6327e68cfa5d38b36123637b321c19a7#diff-caf1d936b395dcac087bd2b6d8585de0e06695cfe00c899d9299dc9cfec2a118L91

        char *strncpy(char *dest, const char *src, size_t count)
        {
        	char *tmp = dest;
        
        	while (count) {
        		if ((*tmp = *src) != 0)
        			src++;
        		tmp++;
        		count--;
        	}
        	return dest;
        }


        1. aulitin
          23.06.2026 07:49

          если закончится, то коррапченый символ в utf-8 это меньшая проблема :-)


          1. kunix
            23.06.2026 07:49

            Все же потенциальное отсутствие '\0' - явно задокументированная особенность и с ней умеют работать через конструкцию вида strncpy(buf, ..., sizeof(buf)-1).
            Как здесь например:
            https://github.com/torvalds/linux/commit/340ff3216799a947fe0b07bed8f0409ffc716be9#diff-81db6161fb0345ecabeac4f346089871ab4d62d9e8ee1fdb04c73757b8e8bbb8L133

            А вот фигня с UTF-8 - более тонкая, неочевидная, и непредсказуемая.


      1. randomsimplenumber
        23.06.2026 07:49

        Может. 10 байт могут попасть в середину символа.


  1. Dhwtj
    23.06.2026 07:49

    Хоронили strncpy, порвали два баяна


    1. Belarus
      23.06.2026 07:49

      Но ведь таки похоронили?


      1. Dhwtj
        23.06.2026 07:49

        Только в ядре Линукс.

        В библиотеке осталась


        1. Belarus
          23.06.2026 07:49

          Не только в библиотеке, а везде. Я изначально подумал про хоронение в ядре.

          Ну и такие новости заставят задуматса и других. Я вот буду теперь знать про её небезопасность.


        1. strvv
          23.06.2026 07:49

          А сколько ещё неизведанного!


  1. nomorewar
    23.06.2026 07:49

    Там же вроде ядро линукса на раст переписывают? Эта проблема сама бы не ушла, если использовать не С-строки?


    1. Answer_M
      23.06.2026 07:49

      Ядро не равно модули ядра. Да и модули как я помню не переписываются, а пишутся новые под новое железо или специфические кейсы


    1. blind_oracle
      23.06.2026 07:49

      Ничего там не переписывается. Добавили тулчейн чтобы можно было писать что-то в ядре на Расте да и всё.

      Переписать всё ядро - понадобится, небось, миллион человеко-лет.


      1. Revertis
        23.06.2026 07:49

        Или один Claude Code и несколько месяцев.


        1. Wesha
          23.06.2026 07:49

          ...но есть нюанс...


        1. blind_oracle
          23.06.2026 07:49

          Угу, сжечь токенов но 100 миллионов долларов и получить в конце кусок говна :)

          Тут одни уже Bun переписывали на Расте...


          1. Revertis
            23.06.2026 07:49

            Прочитал. Там одни предполодения, без каких-либо фактов.


            1. blind_oracle
              23.06.2026 07:49

              Факты? Факты будут когда покажут что это ИИ-поделие работает, его легко поддерживать и так далее. А не просто куча `unsafe {}` блоков повсюду.


              1. Revertis
                23.06.2026 07:49

                Согласен. Надо ждать, а не охать и ахать заранее.


    1. LeraKholod
      23.06.2026 07:49

      Проблема Си-строк никуда не уйдет, пока жив сишный ABI. Любой язык, который общается с ядром, вынужден подстраиваться под эти нули в конце


    1. Mox
      23.06.2026 07:49

      Ядро не будут переписывать на rust, только драйвера
      Для тех кто хочет переписать на Rust есть https://github.com/asterinas/asterinas


  1. Spiritschaser
    23.06.2026 07:49

    то strncpy запишет ровно n байт и не поставит завершающий \0. Дальше любой ... уходит читать за буфер

    О, спасибо, что экскурс в историю описали, как он появился. Всегда когда ещё писал на древнем до-ANSI С поражался этой разнице с strcpy. Даже был какой-то паттерн: использовать объявленные константы заданного размера и в них делать strncpy - дикое уродство, но типа безопаснее указателя с strcpy.


    1. unreal_undead2
      23.06.2026 07:49

      Занулить последний байт после вызова strncpy - это то, что пальцы сами набирают автоматом ) Вот от оверхеда на зануление хвоста на маленьких строках никуда не денешься.


  1. lrlunin
    23.06.2026 07:49

    Это не про строки вообще.

    Замена — это не «найти и поменять на безопасный аналог».

    Это не баг в трекере — это код, который надо перечитать.

    Постить ИИ текст — это не про качественный постинг.


    1. trinxery
      23.06.2026 07:49

      Автора таки забанили в рид-онли, правда снесли только последнюю статью (под которой, в отличие от остальных, таки поднялось возмущение по поводу ИИшности).


  1. randomsimplenumber
    23.06.2026 07:49

    Зато он избавил нас от цыган strncpy ! (С придыханием) (ц) ;)

    Ну, функция стремная, да. Но она протестирована вдоль и поперек? Почему бы не оставить как есть и просто обьявить deprecated ? Она же и в glibc, и в куче программ продолжает торчать. 6 лет переписывали то что и так работает.


    1. farafonoff
      23.06.2026 07:49

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


      1. strvv
        23.06.2026 07:49

        Согласен, сколько недавно, в апреле-мае вытащили багов с эскалацией привилегий через модули шифрования (algif?) С 17 года были.


    1. LeraKholod
      23.06.2026 07:49

      Deprecated не мешает людям копипастить старый код. Единственный способ избавиться от проблемы - физически удалить ее источник


  1. Wesha
    23.06.2026 07:49

    Self-Protection Project Киса Кука,

    СОВПАДЕНИЕ? НЕ ДУМАЮ! /s


  1. enabokov
    23.06.2026 07:49

    Как всё запущено.


  1. LeraKholod
    23.06.2026 07:49

    Проблема strncpy не только в терминаторе, она еще и кэш убивает своим забиванием нулями хвоста буфера, если строка короткая..


  1. 0xC0CAC01A
    23.06.2026 07:49

    Зачем вообще кто-то придумал строки, заканчивающиеся нулём? Не лучше ли просто хранить длину строки? А ещё хорошо бы и размер буфера хранить. И тогда внезапно все эти переполнения буфера, донимающие нас уже пол-века, были бы не страшны. Не говоря уже про strlen(), выполняющийся за О(1).


    1. Wesha
      23.06.2026 07:49

      А что ещё Вы хотите похранить на машине с 16 КБ памяти?


    1. slonopotamus
      23.06.2026 07:49

      И что, теперь куча старого софта тупо перестала компилироваться?

      Нет, функцию убрали из кода ядра. В libc она никуда не делась.


    1. Belarus
      23.06.2026 07:49

      В те времена память была дорого́й, экономили как могли. Видимо, создатели языка много работали с текстом (компиляцыя кода?), и там это удобно: што-то делаеш в любом месте текста и просто сохраняеш указатель на это место.


  1. JBFW
    23.06.2026 07:49

    С интересом наблюдаю за изобретающими Pascal-строки...

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


    1. GROMILOKBIG
      23.06.2026 07:49

      Зачем тратить лишние когнитивные ресурсы и оставлять риск человеческого фактора?


      1. JBFW
        23.06.2026 07:49

        Затем, что если не думать над архитектурой (а даже несколько возможных вариантов решения какой-нибудь сортировки -тоже архитектура), и просто тупо лепить один на один кирпичики - получается большой, тормозной и глючный Франкенштейн, который развалится не от переполнения буфера, а из-за своей кривизны и непродуманности.


  1. alexartrider
    23.06.2026 07:49

    Автор не разобрался в теме, не понял почему и где баг - проблема этой функции не в том, что, есть в конце '\0' или нет, на это есть документация, а в UTF-8. Размер символа сегодня может быть 1,2,3,4 байта, и эта функция разрезает последний символ на части. А если не разрезать, а копировать полностью то получается переполнение буфера при копировании. Как то так выходит на самом деле.


    1. artden111
      23.06.2026 07:49

      То, что strncpy может не поставить завершающий \0 гораздо хуже, чем если \0 разрежет UTF8-символ. Хотя и в разрезании символа нету ничего хорошего


      1. alexartrider
        23.06.2026 07:49

        Читаем стандарт, она не должна ставить '\0'. Нет проблемы здесь, надо уметь читать мануалы.


    1. kunix
      23.06.2026 07:49

      Говно еще в том, что strncpy принятно использовать без проверки на ошибки.
      Было переполнение или нет - коду пофиг, он молотит дальше.

      Когда-то были дыры из-за того, что например PHP видит строку "evil\0innocent" и она проходит проверку, а нативный код видит "evil\0".
      Даже если отдельно порешать проблему с '\0', расширив буфер, проблема с неоднозначностью строк из-за обрезки остается.
      Я даже подозреваю, что если хорошенько поиграться со шрифтами с UTF-8, можно из двух неудачно обрезанных последовательностей собрать UTF-8-франкенштейна с новыми символами.

      Короче, strncpy - непредсказуемое говно.


  1. dude_figvam
    23.06.2026 07:49

    дак на что поменяли? strncpy_sнадеюсь?


    1. DGG
      23.06.2026 07:49

      На разное, в зависимости от контекста.

      В статье написано, в частности, почему эту функцию нельзя было заменить на что-то одно.


  1. zoiaylinsk
    23.06.2026 07:49

    Зашли обсудить один коммит в ядре Linux, а в итоге судим Кернигана и Ритчи за решения 1972 года


    1. SpiderEkb
      23.06.2026 07:49

      Тут скорее камень в огород тех, кто никак не может закопать стюардессу. И продолжает на фундаменте 72-го года пытаться строить hi-tech небоскреб.