Strncat от Microsoft читает байты за пределами исходного буфера

Я наблюдаю интересную проблему с реализацией Microsoft strncat. Он касается 1 байта за пределами исходного буфера. Рассмотрим следующий код:

#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <string.h>

void main()
{
    char dstBuf[1024];
    char* src = malloc(112);
    memset(src, 'a', 112);
    dstBuf[0] = 0;
    strncat(dstBuf, src, 112);
}

strncat читает 1 байт после 112-байтового блока. Поэтому, если вам не повезло получить выделение на недопустимой границе страницы, ваше приложение вылетит. Большие приложения могут периодически давать сбой в таких местах. (Обратите внимание, что такое состояние можно смоделировать с помощью параметра gflags PageHeap; размер блока должен быть кратен размеру указателя для правильного выравнивания.)

Это ожидаемое поведение или ошибка? Есть ссылки подтверждающие это? (Я читал несколько описаний strncat, но их можно интерпретировать двояко, в зависимости от вашего первоначального настроя...)

Обновление (для ответов на вопросы о доказательствах): прошу прощения, если из текста выше не понятно, но это экспериментальный факт. Я наблюдаю периодические сбои в приложении по адресу strncat чтения src+srcBufSize. В этом небольшом примере запуск с gflags PageHeap при сбое воспроизводится стабильно (100%). Так что, насколько я вижу, доказательства очень веские.

Update2 (информация о компиляторе) MS Visual Studio 2005, версия 8.0.50727.867. Платформа сборки: 64-битная версия (без воспроизведения для 32-битной версии). ОС, использованная для воспроизведения сбоя: Windows Server 2008 R2.

Обновление 3 Проблема также воспроизводится с двоичным файлом, встроенным в MS Visual Studio 2012 11.0.50727.1.

Обновление 4 Ссылка на выпуск в Microsoft Connect; ссылка на обсуждение на форумах MSDN

Обновление 5. Проблема будет исправлена ​​в следующем выпуске VS. Исправление для старых версий не планируется. См. ссылку «Microsoft Connect» выше.


person glagolig    schedule 30.08.2013    source источник
comment
Каковы ваши доказательства этого утверждения?   -  person user207421    schedule 30.08.2013
comment
Я хотел бы посмотреть на ваш двоичный файл, который ведет себя так. Кроме того, какую версию компилятора/библиотеки вы используете?   -  person Jonathon Reinhart    schedule 30.08.2013
comment
@Jonathon Reinhart: Двоичный файл sdrv.ms/17rN1cF   -  person glagolig    schedule 30.08.2013
comment
Я просил доказательства, а не исполняемый файл. Никто не собирается запускать неизвестный .exe из ненадежного источника, если у них есть хоть какое-то представление о компьютерной безопасности. Каковы ваши доказательства?   -  person user207421    schedule 01.09.2013
comment
@EJP Я отвечал OP, который явно просил двоичный файл. Я предполагаю, что ОП может просматривать дизассемблер без запуска или настройки безопасной песочницы. Я обновил вопрос, чтобы уточнить доказательства.   -  person glagolig    schedule 02.09.2013
comment
Итак, если src указывает на массив указателей, не заканчивающийся NUL, размер которого кратен 8, отличному от 0, читается двойное слово сразу за массивом, хотя прочитанное значение не используется. mov (%rdx),%rax; sub $0x8,%r8; jb 0x400011a7 ← Я могу придумать пару способов изменить это за счет производительности.   -  person ninjalj    schedule 06.09.2013
comment
@OP: поэтому вы не можете воспроизвести его на 32-битной версии. Можете ли вы поделиться 32-битным двоичным файлом?   -  person ninjalj    schedule 06.09.2013
comment
@ninjalj: Готово. Я положил его в той же акции.   -  person glagolig    schedule 07.09.2013
comment
Замена jb на jbe работает, но тогда окончательная копия qword делается байт за байтом. Что касается 32-битной версии, то хотя она и основана на той же идее, детали немного отличаются. В частности, цикл конкатенации выровненного [qd]слова использует счетчик двойных слов и завершается на нуле в 32-разрядной версии, тогда как в 64-разрядной версии используется счетчик байтов, и он завершается при значении ниже нуля (как я сказал, что он мог быть ниже или равен нулю, 64-битная версия имеет резервный код до 8 последних байтов).   -  person ninjalj    schedule 09.09.2013
comment
Понижение mov (%rdx),%rax после jb 0x400011a7 также работает, но некоторые из следующих инструкций зависят от %rax, что может привести к снижению производительности. Резюмируя, я согласен, что это ошибка.   -  person ninjalj    schedule 09.09.2013
comment
Разве Microsoft уже не исправила это в 2008 или 2009 году? См. support.microsoft.com/kb/956420 и connect.microsoft.com/VisualStudio/Downloads/. Так почему же ошибка все еще присутствует в VS 2012?   -  person Joseph Quinsey    schedule 12.02.2014
comment
@JosephQuinsey: обратите внимание, что это исправление было для strncpy, а не для strncat. Этот вопрос касается strncat.   -  person James McNellis    schedule 03.04.2014


Ответы (3)


В документации для strncat указано:

src — указатель на строку байтов с завершающим нулем, из которой нужно скопировать

Следовательно, реализация может предположить, что входной параметр src на самом деле завершается NUL, даже если он длиннее count символов.

Для дальнейшего подтверждения см. собственную документацию Microsoft. состояния:

источник строки

Исходная строка с завершающим нулем.

С другой стороны, фактический стандарт C гласит что-то вроде:

Функция strncat добавляет не более n символов (нулевой символ и символы, следующие за ним, не добавляются) из массива, на который указывает s2, в конец строки, на которую указывает s1.

Как указано в комментариях ниже, это идентифицирует второй параметр s2 как массив, а не строку с нулевым завершением. Однако это все еще неоднозначно по отношению к исходному вопросу, потому что эта документация описывает конечный эффект на s1, а не поведение функции при чтении из s2.

Конечно, это можно решить в отношении конкретной реализации Microsoft, ознакомившись с исходным кодом C Runtime Library.

person Greg Hewgill    schedule 30.08.2013
comment
Не уверен, что покупаюсь на это. В нем говорится, что функция strncat добавляет не более первых count символов strSource в strDest. - person Jonathon Reinhart; 30.08.2013
comment
@JonathonReinhart: это результирующее поведение, но ожидается, что входная строка src будет заканчиваться нулем. - person Greg Hewgill; 30.08.2013
comment
@JonathonReinhart - Если вы укажете строку с ненулевым завершением для выполнения конкатенации, чего вы ожидаете? Этот ответ идеален - person Ed Heal; 30.08.2013
comment
Ожидается, что входная строка будет заканчиваться нулем, если она короче count.. Там нет языка, который говорит, что он может читать за пределами count символов. - person user207421; 30.08.2013
comment
Я видел документацию Microsoft. Я не думаю, что это достаточно ясно. Как я уже писал, интерпретация зависит от вашего первоначального отношения к предмету. Вот другая ссылка, в которой не упоминается нулевое завершение: cplusplus.com/reference/cstring/ стрнкэт - person glagolig; 30.08.2013
comment
@glagolig: Ваша ссылка говорит о строке C. Строки C всегда заканчиваются NUL. - person Greg Hewgill; 30.08.2013
comment
@ Грег, я не согласен. Нет причин, по которым он когда-либо должен читать больше count символов. strncat(3) говорит "The strncat() function is similar, except that * it will use at most n bytes from src; and * src does not need to be null-terminated if it contains n or more bytes." - person Jonathon Reinhart; 30.08.2013
comment
На самом деле, я даже не могу придумать правильную реализацию, которая заботилась бы о том, завершается ли строка нулем. (насколько это не превышает count - как/почему?) - person Jonathon Reinhart; 30.08.2013
comment
@EdHeal char src[3] = { 'a', 'a', 'a' }; char dst[100]; dst[0] = '\0'; strncat(dst, src, 3); printf("%s\n", dst); Я ожидаю, что это будет работать правильно, печатать три символа a и не переполнять буферы. - person Jonathon Reinhart; 30.08.2013
comment
@JonathonReinhart: Вы ссылаетесь на конкретную (glibc) реализацию strncat, которая не является реализацией Microsoft, на которую ссылается OP. - person Greg Hewgill; 30.08.2013
comment
@GregHewgill Право показать, что то, что вы говорите, не имеет смысла. Я также процитировал еще один документ, в котором говорится, что поведение OP не должно происходить. ОП спросил, было ли то, что он видел, ошибкой ... если код, который он показывает, касается src[112], то я бы сказал, что да, в его libc есть ошибка. - person Jonathon Reinhart; 30.08.2013
comment
@GregHewgill: это противоречит стандарту C, который рассматривает исходный операнд как массив: Функция strncat() должна добавлять не более n байтов (нулевой байт и байты, следующие за ним, не добавляются) из указанного массива to на s2 до конца строки, на которую указывает s1. - person ninjalj; 30.08.2013
comment
@ninjalj: Спасибо за это, я обновил свой ответ этой информацией. - person Greg Hewgill; 30.08.2013
comment
Хотя стандарт ничего не говорит о внутренней реализации strnlen, библиотека времени выполнения не имеет права обращаться к байтам после конца массива, указанного (start=s2, length=n). По крайней мере, если он не знает массив, который нужно дополнить, например: до размера двойного слова. - person ninjalj; 30.08.2013
comment
Я принял ответ, так как он имеет ссылку на стандарт C. Теперь я думаю, что то, что я наблюдаю, является ошибкой. Наш код построен на нескольких разных платформах, разработчики не могут просматривать исходники CRT на всех платформах для каждой строковой функции. - person glagolig; 01.09.2013

s2 не является "строкой" в strncat(s1, s2, n).

Поэтому, если Microsoft считывает n байта передачи, это не соответствует C11.

C11 7.24.2.3.1 strcat() упоминает
«добавляет копию строки, на которую указывает s2 (включая завершающий нулевой символ), в конец строки, на которую указывает s1».

C11 7.24.2.3.2 strncat говорит
"Функция strncat добавляет не более n символов (нулевой символ и символы, следующие за ним, не добавляются) из массива, на который указывает s2, к конец строки, на который указывает s1. ... К результату всегда добавляется завершающий нулевой символ"

Ясно, что в случае strncat s2 рассматривается как "массив" со строковыми ограничениями на количество, добавляемое к s1. Таким образом, во время конкатенации нет необходимости проверять s2 больше, чем это абсолютно необходимо. Окончательное написанное \0 исходит из кода, а не s2.

Не знаю о более старом стандарте C99.

person chux - Reinstate Monica    schedule 16.09.2013

Английский — несовершенный язык, в большей степени, чем C.

В документации сказано "максимум n символов" (выделено мной). Нет никаких доказательств того, что strncat копирует более 112 символов. Что заставляет вас верить, что это так?

Код strncat может индексировать за пределами смещения 112, но фактически не ссылаться на смещение 113, что может вызвать ошибку хранения. Такое поведение ptr определено как приемлемое в K&R.

Наконец, опять же, это проблема английского языка/рассуждения, в документации, вероятно, говорится, что строка завершается нулем. Но на самом деле, разве не излишне говорить, что строка завершается нулем? Они по определению, иначе они были бы массивом символов. Таким образом, документация является расплывчатой ​​​​и неконкретной. Программисту остается читать между строк. Документация по программному обеспечению не является юридическим томом, это описания, предназначенные для понимания специалистом в данной области.

person JackCColeman    schedule 30.08.2013
comment
Документация по программному обеспечению не является юридическим томом ←, но стандарты действительно чем-то похожи на юридический фолиант, а стандарт C рассматривает исходный аргумент как массив. strn* предназначены для работы с записями фиксированного размера, поэтому они ведут себя не так, как если бы их предметом были просто строки. - person ninjalj; 30.08.2013
comment
@ninjalj, я согласен с вами, что это записи фиксированного размера, а n определяет длину этой записи. C разрешено/определено индексировать 1 после конца массива, поэтому for(etc; i++) работает! - person JackCColeman; 30.08.2013