LINUX.ORG.RU

Оказывается, snprintf() таки небезопасна

 ,


2

5

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

Так вот, оказывается snprintf() не умеет писать строку саму в себя. Зачем это нужно? Ну вот например код:

// connectUrl - имя/IP сервера (поле класса)
// connectPort - порт сервера (поле класса)

bool TcpChannel::open(const char *url, int port)
{
    // Запоминаются URL и порт
    snprintf(connectUrl, TCP_CHANNEL_MAX_URL_LEN, url);
    connectPort=port;

    // Устанавливается соединение (возвращает void)
    tcpSocket->connectToHost(connectUrl, connectPort);
    ...
}

void TcpChannel::reConnect()
{
    ... всякая логика ...

    // Переустанавливается соединение
    open(connectUrl, connectPort);
}

Тут поле connectUrl из reConnect() передается в open() и должно записаться само в себя, то есть, вообще ничего не должно измениться по-хорошему. А вот болт. Первый байт строки будет обнулен и получится пустая строка. Не ожидал я такого. Как люди пишут на сях с такими строками - не понимаю.

★★★★★

Последнее исправление: cetjs2 (всего исправлений: 1)
Ответ на: комментарий от anonymous

Причем тут принтф вообще? Если тебе нужен он — выше показали (в том числе и я) что он там вообще не нужен, там культи, поэтому крути как хоти.

deep-purple ★★★★★
()
Ответ на: комментарий от deep-purple

Ну ты расскажи, что тут произойдёт. А мы оценим твои познания.

snprintf("http://example.com/%d0%d0%d0%d0", TCP_CHANNEL_MAX_URL_LEN, url);

Допустим, что url менять нельзя, его тебе на стороне выдали такой. Он допустимый и ничем не хуже остальных.

anonymous
()
Ответ на: комментарий от anonymous

мы оценим твои познания. Допустим

Тогда допустим что я не буду вообще спринтф использовать, а возьму то что есть в культях, в том числе и вот такой подход: https://stackoverflow.com/a/9666531/3558278

deep-purple ★★★★★
()
Ответ на: комментарий от deep-purple

Тебе сказали, что в URL может содержать %. И это нормально. Т.е валидация это не исправит. В тоже время функции *printf если видят в строке формата %c, %d, %f пытаются напечатать аргумент который находится на стеке за форматом. В этом вызове эти аргументы не передаются, что может привсти к чтению мусора, или к обращению за пределы стека. А это может привести к падению приложения. Если какой-то программист будет вызвать эту функцию для печати URL, ему будет нужно не валидировать а эскейпить URL особым образом. Преобразовывать все % в %% в строке. Что глупо, потребует выделения дополнительного буфера, и ресурсов процессора. Решить эту пробелму можно вызовом strncpy. Также использование snprintf вместо strncpy в этой функции также приводит к падению производительности, т.к. snprintf работает медленнее.

vromanov ★★★
()
Ответ на: комментарий от vromanov

Нет. Тебе нужно иметь свой внутренний формат представления адреса. Я бы влепил даже структуру, раз мы говорим о таком уровне, сишном, где все разбито на протокол, хост, айпи, порт, квери стринг (да, уже урлДЕкодирована). Эта структура была бы готова только после парсинга строки и валидации полученных частей.

Но, как я выше комментарием ответил. Зачем это все, когда тут культи?

UPD:

ВСЕ, РЕБЯТА, НАДО УБЕГАТЬ ПО ДЕЛАМ, ПИШИТЕ ЕСИЧО, ОТВЕЧУ ПОЗДНЕЕ.

deep-purple ★★★★★
()
Последнее исправление: deep-purple (всего исправлений: 1)
Ответ на: комментарий от deep-purple

А что не так? Показал окошко: «извините, вы ввели херню» и не работаешь дальше по этим данным.

А то, что у ТС там переменная называется url, что намекает на URL, в котором и в валидном внезапно могут быть %, которые внезапно особым образом интерпретируются в snprintf, что может внезапно вызвать баги.

На троллинг твои сообщения не похожи. Ты что, правда не был в курсе особенностей URL и printf?

i-rinat ★★★★★
()

Я вот не пойму одного, зачем писать такое гавно, а не использовать надежный boost + asio? Цель работы научится программировать?

Bobby_
()
Ответ на: комментарий от ox55ff

Зачем в qt крестах вообще сишные танцы со строками?

Он же написал:

Но вот понадобилось сделать интерфейс с такими строками.

Т.е. ему кто-то извне передаёт сишные строки. Я бы, правда, в таком случае всё равно первым делом сделал QString::fromLocal8Bit() и дальше бы уже оперировал кутешными строками.

hobbit ★★★★★
()
Ответ на: комментарий от Bobby_

О, а вот и эксперты по говну подтянулись.

hobbit ★★★★★
()

Мне кажется, ты бы сделал карьеру как тестер. У тебя талант пользоваться всем не так как оно было задумано.

anonymous
()
Ответ на: комментарий от i-rinat

На троллинг твои сообщения не похожи

Потому что я совсем не о том что там за проблемы у принтф. Если так хочется использовать именно его — ясно что нужно гнать к виду чтобы не споткнулось, например «ud2hex(s) ( s >= '0' && s <= '9' ? s - '0' : s - '7' )».

Про урлкодированые я давно знал, а принтф никогда в таком урл-контексте не использовал, и, как видно, правильно.

deep-purple ★★★★★
()

Да, такое случается, когда люди пытаются погромировать интуитивно, а не сверяясь с документацией.

Virtuos86 ★★★★★
()
Ответ на: комментарий от Virtuos86

Я настолько отвык от этих сишных нелепостей, что вообще не ожидал такой подставы. Пару дней ковырялся в инглишдоках, наконец нашел примечание. Вольный перевод:


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

sprintf ( dst, «% s и% s» , dst, t ) ; // <- broken: undefined behavior

Xintrea ★★★★★
() автор топика
Ответ на: комментарий от Xintrea

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

anonymous
()
Ответ на: комментарий от Xintrea

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

Ох, бро.

Ещё раз, это не тайное знание. Это написано в прототипе функции.

int snprintf( char *restrict buffer, size_t bufsz,
              const char *restrict format, ... );

Я специально дал ссылку на описание ключевого слова restrict, чтобы в следующий раз, ты смотрел прототип используемой функции, чтуь более внимательно. Именно оно и указывает, что buffer и format не должны перекрываться(в том числе и с необязательными параметрами (...))

fsb4000 ★★★★★
()
Последнее исправление: fsb4000 (всего исправлений: 1)
Ответ на: комментарий от fsb4000

Ещё раз, это не тайное знание. Это написано в прототипе функции.

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

Но у меня проблема в том, что я англоязычную документацию воспринимаю очень неоднозначно. Иногда я вижу, что документация написана предельно четко. А иногда я вижу, что в документации допускают некоторые вольности. И я никогда не могу определить точно, что имеется в виду, и каков стиль изложения. И я вечно с этим путаюсь.

Мне вот например показалось, что restrict buffer - это одна сущность «обособленный буфер». Я даже не знал, что в сях есть ключевое слово restrict, и даже искать его не стал.

У меня была такая же проблема с консольным zip. Я несколько лет не мог понять как им пользоваться, использовал по образцам команд из интернета, но не мог понять, почему они такие. А дело оказалось в том, что в форматной строке в официальной документации написано:

zip [-options] [-b path] [-t mmddyyyy] [-n suffixes] [zipfile list] [-xi list]

Так вот этот [zipfile list] я переводил и как «списочный zip-файл» и как «список zip-файлов» (думал там «s» не хватает), но в любом случае я эту часть воспринимал как часть команды, которая работает с какими-то списками в zip-файле, что мне было ненужно. Мне нужно было уметь упаковать один файл, или файлы в одной директории. И именно этого я не мог понять как делать, пока до меня не дошло, что [zipfile list] следует рассматривать как [имя_zip-файла список_файлов_помещаемых_в_архив].

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

Xintrea ★★★★★
() автор топика
Ответ на: комментарий от ckotinko

выделить немного байтиков с помощью alloca.

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

KennyMinigun ★★★★★
()
Ответ на: комментарий от anonymous

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

Это от неправильного понимания языка. И со временем вы от него огребете.

anonymous
()
Ответ на: комментарий от vromanov

И трансформатую будку не закрывать и не огораживать. КПП же есть!

Нет, лучше дополнительно оградить и запереть каждый трансформатор, провод, и рубильник внутри трансформаторной будки

asyncron2018
()
Ответ на: комментарий от vromanov

Да и провода высоковольтные можно не изолировать, если есть документация что их нельзя трогать.

Их и так не всегда изолируют

asyncron2018
()
Ответ на: комментарий от anonymous

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

Нет, конструкции — это строка на вход транслятора

asyncron2018
()
Ответ на: комментарий от anonymous

Аргументированно

Так же как у тебя. А аргументов было выше крыши хотя бы в тредах про UB.

anonymous
()
Ответ на: комментарий от anonymous

в тредах про UB

Можно ссылочку? Или что тебе конкретно не нравится в виде кейворда для гугла.

anonymous
()
Ответ на: комментарий от asyncron2018

Это никогда никого не останавливает.

anonymous
()
Ответ на: комментарий от asyncron2018

Если вы оба про undefined behavior, то я про него знаю. Не подразумевал, что транслируется однозначно.

anonymous
()
Ответ на: комментарий от Xintrea

Тебе лучше вообще не программировать, если ты искал в доках то что написанно в САМОМ начале обычного мана по этим функциям.

Dudraug ★★★★★
()
Ответ на: комментарий от Xintrea

А иногда я вижу, что в документации допускают некоторые вольности.

Какие б**дь вольности, что ты несешь? Ты тут вроде давно. Да просто читая много раз англо доку(может даже по несколько раз) со словарем можно за год можно привыкнуть к стилю и выучить необходимый словарный запас для чтения манов.

Dudraug ★★★★★
()
Ответ на: комментарий от Xintrea

zip [-options] [-b path] [-t mmddyyyy] [-n suffixes] [zipfile list] [-xi list]

ты где такую доку нашел? У меня на системе и в гугле только

zip [-aABcdDeEfFghjklLmoqrRSTuvVwXyz!@$] [--longoption ...] [-b path] [-n suffixes] [-t date] [-tt date] [zipfile [file ...]] [-xi list]

Dudraug ★★★★★
()

Так вот, оказывается snprintf() не умеет писать строку саму в себя

Я замечу, что даже не имея документации, чисто интуитивно можно предположить, что такие низкоуровневые штуки типа snprintf едва ли будут делать что-то подобное. Им важна скорость, чтобы было меньше лишнего.

Если нужен такой функционал, то пишется функция xintreasprintf и там уже хоть что можно делать. А по дефолту разумного ожидать от snprintf (и прочих базовых функций) минимальных возможностей и максимальной производительности, коей копирование строки саму в себя никак не способствует.

I-Love-Microsoft ★★★★★
()
Ответ на: комментарий от anonymous

С(++) нужно воспринимать как(часто тонкий) слой сахара над ассемблером.

Много вас таких по весне в UB-тредах оттаивает.

anonymous
()
Ответ на: комментарий от I-Love-Microsoft

Я замечу, что даже не имея документации, чисто интуитивно можно предположить, что такие низкоуровневые штуки типа snprintf едва ли будут делать что-то подобное. Им важна скорость, чтобы было меньше лишнего.

Если бы я писал такие низкоуровневые штуки, мне бы в голову не пришло не предусмотреть _одну_ проверку двух циферок (проверку указателей на равенство), при которой ничего не надо делать. Но Си - это особая вселенная, и разработчиков его я не понимаю.

Xintrea ★★★★★
() автор топика
Ответ на: комментарий от Xintrea

Ну проверил ты. А дальше что?

anonymous
()
Ответ на: комментарий от Xintrea

Если бы я писал такие низкоуровневые штуки, мне бы в голову не пришло не предусмотреть _одну_ проверку двух циферок (проверку указателей на равенство)

Какое равенство? Там ub для перекрывающихся регионов, а не для одинаковых региновов, то есть ub даже если у для регинов длиной в 100500 байт только один байт общий.

Да и что значит ничего не делать? В мане эта особенность в первом же абзаце описана, то что ты читать не умеешь - это не проблема си.

Dudraug ★★★★★
()
Ответ на: комментарий от Dudraug

Какое равенство? Там ub для перекрывающихся регионов, а не для одинаковых региновов, то есть ub даже если у для регинов длиной в 100500 байт только один байт общий.

А я говорю про одинаковые регионы. Совершенно не понимаю, в чем проблема это элементарное предусмотреть. Ведь с данными даже делать ничего не надо.

Xintrea ★★★★★
() автор топика
Ответ на: комментарий от Xintrea

А я говорю про одинаковые регионы.

Ты предлагаешь вставить лишнюю проверку там где она в 99.9% случаев не нужна.

Dudraug ★★★★★
()
Последнее исправление: Dudraug (всего исправлений: 1)
Ответ на: комментарий от Xintrea

Просто у си своя кухня, надо с ней смириться.

Dudraug ★★★★★
()
Ответ на: комментарий от Dudraug

В cygwin так

Copyright (C) 1990-1993 Mark Adler, Richard B. Wales, Jean-loup Gailly
and Kai Uwe Rommel. Type 'zip -L' for the software License.

Zip 2.0.1 (Sept 18th 1993). Usage:
zip [-options] [-b path] [-t mmddyy] [-n suffixes] [zipfile list] [-xi list]
  The default action is to add or replace zipfile entries from list, which
  can include the special name - to compress standard input.
  If zipfile and list are omitted, zip compresses stdin to stdout.
  -f   freshen: only changed files  -u   update: only changed or new files
  -d   delete entries in zipfile    -m   move into zipfile (delete files)
  -k   simulate PKZIP made zipfile  -g   allow growing existing zipfile
  -r   recurse into directories     -j   junk (don't record) directory names
....
....
[\code]

vromanov ★★★
()
Ответ на: комментарий от Xintrea

Какая нахрен одна проверка? Ты хоть понимаешь как printf рабоатет? Чтобы понять, сколько он символов выведет, надо практически всю функцию выполнить, а не просто сравнить указатели

vromanov ★★★
()

snprintf(connectUrl, TCP_CHANNEL_MAX_URL_LEN, url);

Мощно! strncpy уже предлагали?)

sergej ★★★★★
()
Ответ на: комментарий от Xintrea

мне бы в голову не пришло не предусмотреть _одну_ проверку двух циферок (проверку указателей на равенство)

А зачем каждый раз делать эту одну проверку там, где оно не нужно? Если тебе нужно, то ты и делай. А у тех, кому не нужно, всё будет работать быстрее. Это си. Его используют для написания эффективного низкоуровневого среднеуровневого кода. А если тебе на каждом чихе нужны проверки, а скорость и потребление памяти не критичны, то используй яву.

А кроме того, как ты в snprintf просто оставишь без изменений строку вида «%i%u%f», если snprintf обязана заменить её на строковое представление аргументов типа int, unsigned и float, указанных далее. Для простого копирования строк существует strncpy, а для копирования перекрывающихся строк memmove (предварительно выяснив размер строки с помощью strlen). Эти функции гораздо быстрее snprintf, если функционал snprintf не нужен и достаточно простого копирования.

aureliano15 ★★
()
Ответ на: комментарий от Xintrea

А я говорю про одинаковые регионы.

И что, по-твоему, должна делать sprintf(tgt, tgt)? А sprintf(tgt, tgt + 3)?

i-rinat ★★★★★
()
Ответ на: комментарий от asyncron2018

Их и так не всегда изолируют

Вооот. И какого C программисты до сих пор терпят перила на лестницах? Какой простор для оптимизации перемещения пропадает. Поставить табличку на входе «Перил нет», и пускать только сишников, чтобы инцидентов не было.

red75prim ★★★
()
Ответ на: комментарий от Dudraug

Так можно и проверку на NULL/nul_ptr из delete убрать. Типа, пусть будет UB, а кому нужно, пусть в своем коде проверяют.

Xintrea ★★★★★
() автор топика
Ответ на: комментарий от aureliano15

Для простого копирования строк существует strncpy, а для копирования перекрывающихся строк memmove (предварительно выяснив размер строки с помощью strlen).

Меня все время сбивает с толку, что к сишным строкам нельзя относиться как к строкам в нормальных языках. Семантически это все что угодно, но не строки. Я себе все время твержу: это не строки, это набор байт с нуллтерм байтом, относисиь к ним так. Но блин, все функции имеют в названии str, и когда долго не пользуешься, начинаешь думать: ну это строка, такая извращенная, но строка. Будем работать единообразно, и тут бах - нет, товарищ, ты определиь что делаешь, подумай где оно в памяти лежит, как будет содержимое интерпретироваться, предусмотри краевые случаи. И это для какого-нибудь элементарного действия.

Xintrea ★★★★★
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.