Подтвердить что ты не робот

Совместимость переменных модификаций и их последствия для безопасности

Я переживаю всплеск интереса к системе с переменным изменением типа C99. Этот вопрос был вдохновлен этим.

Проверяя код из этого вопроса, я обнаружил что-то интересное. Рассмотрим этот код:

int myFunc(int, int, int, int[][100]);

int myFunc(int a, int b, int c, int d[][200]) {
    /* Some code here... */
}

Это, очевидно, не будет (и не будет) компилироваться. Однако этот код:

int myFunc(int, int, int, int[][100]);

int myFunc(int a, int b, int c, int d[][c]) {
    /* Some code here... */
}

компилируется без предупреждения (на gcc).

Это, по-видимому, означает, что тип с измененным изменением массива совместим с любым типом массива без вариаций!

Но это не все. Вы ожидаете, что переменный тип изменен, по крайней мере, с тем, какая переменная используется для установки его размера. Но, похоже, это не так!

int myFunc(int, int b, int, int[][b]);

int myFunc(int a, int b, int c, int d[][c]) {
    return 0;
}

Также компилируется без ошибок.

Итак, мой вопрос: это правильное стандартизованное поведение?

Кроме того, если переменный тип массива действительно совместим с любым массивом, который имеет одинаковые размеры, не означает ли это, что это неприятные проблемы безопасности? Например, рассмотрим следующий код:

int myFunc(int a, int b, int c, int d[][c]) {
    printf("%d\n", sizeof(*d) / sizeof((*d)[0]));
    return 0;
}

int main(){
    int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    myFunc(0, 0, 100, &arr);

    return 0;
}

Компилирует и выводит 100, никаких ошибок или предупреждений, ничего. Как я вижу, это означает, что вы можете просто написать массив без границ, даже если вы строго проверяете размер вашего массива с помощью sizeof, не делая ни одного актера и даже включаете все предупреждения! Или я что-то упускаю?

4b9b3361

Ответ 1

C99, см. раздел 6.7.5.2, где указаны соответствующие правила. В частности,

Строка 6:

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

Предыдущий, теперь удаленный ответ также ссылается на строку 6. Комментарий к этому ответу утверждал, что второе предложение подлежит условию в конце первого, но это кажется маловероятным чтением. Пример 3 этого раздела может уточнить (выдержка):

int c[n][n][6][m];
int (*r)[n][n][n+1];
r=c;   // compatible, but defined behavior only if
       // n == 6 and m == n+1

Это похоже на пример в вопросе: два типа массивов, один из которых имеет постоянный размер, а другой - соответствующий переменный размер и должен быть совместимым. Поведение undefined (за комментарий в примере 3 и одно разумное чтение 6.7.5.2/6), когда во время выполнения переменное измерение отличается от измерения постоянной времени компиляции. И не поведение undefined, что вы ожидаете в любом случае? Иначе зачем поднимать вопрос?

Предположим, что мы можем согласиться с тем, что поведение undefined, когда такое несоответствие происходит, я замечаю, что компиляторы вообще не обязаны распознавать поведение undefined или, возможно, undefined, и не выдавать какой-либо диагностики вообще, если они это признают. В этом случае я надеюсь, что компилятор сможет предупредить о возможном поведении undefined, но он должен успешно скомпилировать код, потому что он синтаксически корректен и удовлетворяет всем применимым ограничениям. Обратите внимание, что компилятор, способный предупреждать о таких возможностях, может не сделать этого по умолчанию.

Ответ 2

#include <stdio.h>

void foo(int c, char d[][c])
{
  fprintf(stdout, "c = %d; d = %p; d + 1 = %p\n", c, d, d + 1);
}

int main()
{
  char x[2][4];
  char y[3][16];
  char (*z)[4] = y;  /* Warning: incompatible types */

  foo(4, x);
  foo(16, y);
  foo(16, x);        /* We are lying about x. What can / should the compiler / code do? */
  foo(4, y);         /* We are lying about y. What can / should the compiler / code do? */

  return 0;
}

Выходы:

c = 4; d = 0x7fff5b295b70; d + 1 = 0x7fff5b295b74
c = 16; d = 0x7fff5b295b40; d + 1 = 0x7fff5b295b50
c = 16; d = 0x7fff5b295b70; d + 1 = 0x7fff5b295b80
c = 4; d = 0x7fff5b295b40; d + 1 = 0x7fff5b295b44

Итак, foo() динамически определяет, как далеко продвигать d на основе c, как показывает ваш код.

Однако часто компилятор не может статически определять, правильно ли вы вызываете foo(). Похоже, что если вы это сделаете, компилятор говорит "ОК, я позволю вам передать все, что вам нужно, как d, если его тип представляет собой двукратно индексированный массив символов. Операции над указателем d будут определены к. Удачи!"

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