Чи є технічно невизначена поведінка «хакерства»?


111

Про що я запитую - це добре відомий трюк "останній член структури має змінну довжину". Це щось подібне:

struct T {
    int len;
    char s[1];
};

struct T *p = malloc(sizeof(struct T) + 100);
p->len = 100;
strcpy(p->s, "hello world");

Через те, що структура закладена в пам’яті, ми можемо накладати структуру на більший, ніж потрібно, блок і ставитися до останнього члена так, ніби він більший за 1 charвказаний.

Тож питання: Чи є ця техніка технічно невизначеною поведінкою? . Я би сподівався, що це так, але мені було цікаво, що про це говорить стандарт.

PS: Я знаю про підхід C99 до цього, я хотів би, щоб відповіді дотримувалися конкретно версії хитрості, як перераховано вище.


33
Це здається цілком чітким, розумним і перш за все відповідальним питанням. Не бачачи причини закритого голосування.
cHao

2
Якби ви представили компілятор "ansi c", який не підтримував хак-струм, більшість програмістів c, яких я знаю, не погодилися б, щоб ваш компілятор "працював правильно". Не розуміючи, що вони будуть приймати суворе читання стандарту. Комітет просто пропустив одного з цього питання.
dmckee --- кошеня колишнього модератора

4
@james Хак працює, розміщуючи об'єкт, достатньо великий для масиву, який ви маєте на увазі, незважаючи на те, що оголосив мінімальний масив. Таким чином, ви отримуєте доступ до виділеної пам'яті за межами суворого визначення структури. Писання минулого виділення не є помилковою помилкою, але це відрізняється від написання у вашому виділенні, але поза "stru".
dmckee --- кошеня колишнього модератора

2
@James: Негабаритний малик тут критичний. Він гарантує, що є пам'ять --- пам'ять з юридичною адресою та "належить" структурі (тобто незаконним користуванням будь-якого іншого суб'єкта господарювання) --- поза номінальним кінцем структури. Зауважте, що це означає, що ви не можете використовувати хакер структури для автоматичних змінних: вони повинні бути динамічно розподілені.
dmckee --- кошеня колишнього модератора

5
@detly: Простіше виділити / розмістити одну річ, ніж виділити / розібрати два речі, тим більше, що останній має два способи невдачі, з якими вам потрібно боротися. Для мене це важливіше, ніж гранична економія витрат / швидкості.
jamesdlin

Відповіді:


52

Як говориться в FAQ на C :

Не зрозуміло, чи це легально чи портативно, але це досить популярно.

і:

... офіційне тлумачення визнало, що воно не суворо відповідає стандарту C, хоча, здається, працює у всіх відомих реалізаціях. (Компілятори, які ретельно перевіряють межі масиву, можуть видавати попередження.)

Обгрунтування, що стоїть за бітом "строго відповідності", міститься в розділі J.2 Невизначена поведінка , що входить до списку невизначеної поведінки:

  • Підрядник масиву знаходиться поза діапазоном, навіть якщо об’єкт, мабуть, доступний із заданим підрядним індексом (як у виразі lvalue, що a[1][7]дається декларацією int a[4][5]) (6.5.6).

Пункт 8 розділу 6.5.6 Оператори добавок також зазначають, що доступ за межами визначених меж масиву не визначений:

Якщо і операнд вказівника, і результат вказують на елементи одного і того ж об’єкта масиву або один минулий останній елемент об’єкта масиву, оцінка не повинна створювати переповнення; в іншому випадку поведінка не визначена.


1
У коді ОП p->sніколи не використовується як масив. Він передається strcpy, і в цьому випадку він перетворюється на рівнину char *, яка вказує на об'єкт, який юридично може бути інтерпретований як char [100];всередині виділеного об'єкта.
R .. GitHub СТОП ДОПОМОГА ВІД

3
Можливо, інший спосіб дивитися на це полягає в тому, що мова може мислити обмеження доступу до фактичних змінних масивів, як описано в J.2, але немає можливості зробити такі обмеження для об'єкта, виділеного malloc, коли ви просто перетворили повернений void *на вказівник на [структуру, що містить] масив. Це все ще дійсно для доступу до будь-якої частини виділеного об'єкта за допомогою вказівника на char(або бажано unsigned char).
R .. GitHub ЗАСТАНОВИТИ ДІЯ

@R. - Я бачу, як J2 може не покривати це, але хіба він також не охоплюється 6.5.6?
detly

1
Звичайно, це могло! Інформація про тип і розмір може бути вбудована в кожен покажчик, і будь-яка помилкова арифметика вказівника може бути зроблена в пастку - див., Наприклад, CCured . Більш філософський рівень не має значення, чи не зможе вас зачепити жодна можлива реалізація , це все-таки невизначена поведінка (є, наприклад, випадки невизначеної поведінки, які потребують оракул для вирішення проблеми зупинки - саме тому вони не визначені).
zwol

4
Об'єкт не є об'єктом масиву, тому 6.5.6 не має значення. Об'єктом є блок пам'яті, виділений malloc. Знайдіть "об'єкт" у стандарті перед тим, як ви викидаєте bs.
R .. GitHub ЗАСТОСУЄТЬСЯ ДОПОМОГА

34

Я вважаю, що технічно це невизначена поведінка. Стандарт (імовірно) не звертається до нього безпосередньо, тому він підпадає під "або упущення будь-якого явного визначення поведінки". пункт (§ 4/2 C99, § 3,16 / 2 C89), який говорить, що це не визначена поведінка.

Вище "можливо" залежить від визначення оператора, який підписує масив. Зокрема, він говорить: "Вираз постфікса, за яким слід вираз у квадратних дужках [], - це підписане позначення об'єкта масиву." (C89, §6.3.2.1 / 2).

Ви можете стверджувати, що тут порушується "об’єкт масиву" (оскільки ви підписуєтеся за межами визначеного діапазону об'єкта масиву), і в цьому випадку поведінка (трохи більше) явно невизначена, а не просто невизначена ввічливість нічого, що цілком визначає це.

Теоретично я можу уявити компілятор, який здійснює перевірку меж масиву і (наприклад) буде припиняти програму, коли / якщо ви намагаєтесь використовувати індекс під діапазоном. Насправді я не знаю такого, що існує, і враховуючи популярність цього стилю коду, навіть якщо компілятор намагався примусово виконувати підписки за якихось обставин, важко уявити, що хтось би змирився з цим. ця ситуація.


2
Я також можу уявити компілятор, який міг би вирішити, що якщо масив має розмір 1, то він arr[x] = y;може бути переписаний як arr[0] = y;; для масиву розміром 2 arr[i] = 4;може бути переписаний, оскільки i ? arr[1] = 4 : arr[0] = 4; хоча я ніколи не бачив, щоб компілятор виконував такі оптимізації, у деяких вбудованих системах вони можуть бути дуже продуктивними. На PIC18x, використовуючи 8-бітні типи даних, код для першого оператора буде шістнадцять байтів, другого, двох або чотирьох, а третього, восьми або дванадцяти. Непогана оптимізація, якщо законна.
supercat

Якщо стандарт визначає доступ до масиву за межами меж масиву як невизначене поведінку, то хакерство структури теж є. Якщо, однак, стандарт визначає доступ до масиву як синтаксичний цукор для арифметики вказівника ( a[2] == a + 2), він не робить. Якщо я правильно, усі стандарти C визначають доступ до масиву як арифмічний вказівник.
yyny

13

Так, це невизначена поведінка.

Звіт про дефекти мови № 051 дає остаточну відповідь на це питання:

Ідіома, хоча є загальною, не є суворо узгодженою

http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_051.html

У документі з обгрунтування С99 Комітет додає:

Дійсність цієї конструкції завжди була сумнівною. У відповідь на один звіт про дефекти Комітет вирішив, що це не визначена поведінка, оскільки масив p-> елементів містить лише один елемент, незалежно від того, чи існує пробіл.


2
+1 за те, що це знайшов, але я все ще стверджую, що це суперечливо. Два вказівники на один і той же об’єкт (у даному випадку даний байт) рівні, а один вказівник на нього (вказівник на масив представлення всього об'єкта, отриманий malloc) є дійсним у доповненні, тож як ідентичний вказівник, отриманий іншим маршрутом, недійсний у додатку? Навіть якщо вони хочуть стверджувати, що це UB, це досить безглуздо, оскільки обчислювально немає способу реалізації, щоб розрізнити чітко визначене використання та нібито невизначене використання.
R .. GitHub ЗАСТАНОВИТИ ДІЙ

Дуже погано, що компілятори C почали забороняти оголошення нульових масивів; якби не ця заборона, багатьом компіляторам не довелося б робити жодних спеціальних операцій, щоб змусити їх працювати так, як вони "повинні", але все-таки мали б можливість використовувати спеціальний код для масивів з одноелементами (наприклад, якщо *fooмістить одноелементний масив boz, вираз foo->boz[biz()*391]=9;можна спростити як biz(),foo->boz[0]=9;). На жаль, відхилення компіляторів масивів із нульовими елементами означає, що багато кодів використовує замість цього одноелементні масиви, і це буде порушено цією оптимізацією.
supercat

11

Цей конкретний спосіб робити це не визначено явно в жодному стандарті C, але C99 все ж включає в себе "хакерство" як частину мови. У C99 останній член структури може бути "гнучким членом масиву", оголошеним як char foo[](з будь-яким типом, який ви хочете замість char).


Щоб бути педантичним, це не хакерство. Структурний хак використовує масив із фіксованим розміром, а не гнучкий елемент масиву. Структурний хак - це те, про що питали, і це UB. Члени гнучких масивів просто здаються спробою заспокоїти того, хто бачив у цій темі, скаржачись на цей факт.
підкреслюй_d

7

Це не визначена поведінка , незалежно від того, що хтось, чиновник чи інше , говорить, тому що це визначено стандартом. p->s, за винятком випадків, коли використовується як значення, оцінює покажчик, ідентичний (char *)p + offsetof(struct T, s). Зокрема, це дійсний charвказівник усередині об’єкта malloc'd, і за ним відразу 100 (або більше залежних від міркувань вирівнювання) послідовних адрес, які також є дійсними як charоб'єкти всередині виділеного об'єкта. Те, що вказівник було отримано, використовуючи ->замість того, щоб явно додавати зміщення до покажчика, повернутого malloc, переданим char *, не має значення.

Технічно p->s[0]- це єдиний елемент charмасиву всередині структури, наступні декілька елементів (наприклад, p->s[1]наскрізь p->s[3]) - це, ймовірно, байти підкладки всередині структури, які можуть бути пошкоджені, якщо ви виконувати присвоєння структурі в цілому, але не якщо ви просто отримаєте доступ до окремих члени та інші елементи - це додатковий простір у виділеному об’єкті, яким ви вільні користуватися, але вам подобається, якщо ви підкоряєтесь вимогам вирівнювання (і charне має вимог вирівнювання).

Якщо ви стурбовані тим, що можливість перекриття із заповненням байт в структурах можете якої - то чином Invoke носових демонів, ви могли б уникнути цього, замінивши 1в [1]багатозначно , яке гарантує , що немає оббивки в кінці структури. Простий, але марнотратний спосіб зробити це - зробити структуру з однаковими членами, за винятком масиву в кінці, і використовувати s[sizeof struct that_other_struct];для масиву. Тоді p->s[i]чітко визначається як елемент масиву в структурі для i<sizeof struct that_other_structта як об'єкт char за адресою, що знаходиться в кінці структури для i>=sizeof struct that_other_struct.

Редагувати: Насправді, у наведеному вище трюку для отримання потрібного розміру вам може знадобитися також поставити об'єднання, що містить кожен простий тип перед масивом, щоб переконатися, що масив починається з максимального вирівнювання, а не посередині прокладки інших елементів . Знову ж таки, я не вважаю, що щось із цього є необхідним, але я пропоную це для найбільш параноїдальних мовних юристів.

Редагування 2: Перекриття байтами з підкладкою, безумовно, не є проблемою через іншу частину стандарту. C вимагає, що якщо дві структури узгоджуються в початковій підпорядкованості своїх елементів, до загальних початкових елементів можна отримати доступ через вказівник на будь-який тип. Як наслідок, якщо було оголошено структуру, ідентичну, struct Tале з більшим кінцевим масивом, елемент s[0]повинен був би збігатися з елементом s[0]в struct T, і наявність цих додаткових елементів не могла б вплинути або вплинути на доступ до загальних елементів більшої структури за допомогою вказівника на struct T.


4
Ви маєте рацію, що природа арифметики вказівника не має значення, але ви помиляєтесь у доступі за межами заявленого розміру масиву. Дивіться N1494 (остання загальнодоступна розробка проекту C1x), розділ 6.5.6, пункт 8 - вам навіть не дозволяється робити додавання, яке займає вказівник більше ніж на один елемент, проголошений розміром масиву, і ви не можете його знецінити, навіть якщо це лише один елемент минулого.
zwol

1
@Zack: це правда, якщо об'єктом є масив. Це неправда, якщо об'єкт - це об'єкт, виділений за допомогою mallocякого доступ до масиву, або якщо це більша структура, доступ до якої здійснюється через вказівник на меншу структуру, елементи якої є іншим набором елементів більшої структури, серед інших справ.
R .. GitHub ЗАСТОСУЄТЬСЯ ДОПОМОГА

6
+1 Якщо mallocне виділяється діапазон пам'яті, до якої можна отримати арифметику вказівника, яке б це користь? І якщо p->s[1]це визначено стандартом , як синтаксичний цукор для арифметики з покажчиками, то ця відповідь : всього знову стверджує , що mallocє корисним. Що ще залишається для обговорення? :)
Daniel Earwicker

3
Ви можете стверджувати, що це чітко визначено скільки завгодно, але це не змінює факту, що це не так. Стандарт дуже чіткий щодо доступу за межі масиву, і межа цього масиву є 1. Це точно так само просто.
Гонки легкості на орбіті

3
@ R .., я думаю, ваше припущення, що два покажчики, що порівнюють рівних, повинні поводитись однаково, є помилковим. Розглянемо int m[1]; int n[1]; if(m+1 == n) m[1] = 0;припущення, що ifфілія введена. Це UB (і не гарантовано ініціалізувати n) відповідно до 6.5.6 p8 (останнє речення), як я його прочитав. Пов’язано: 6.5.9 p6 із виноскою 109. (Посилання на C11 n1570.) [...]
mafso

7

Так, це технічно невизначена поведінка.

Зауважте, що існує щонайменше три способи реалізації "struct hack":

(1) Оголошення проміжного масиву розміром 0 (найпопулярніший спосіб у застарілому коді). Це, очевидно, UB, оскільки декларації масиву нульового розміру завжди незаконні в C. Навіть якщо він компілюється, мова не дає гарантій щодо поведінки будь-якого коду, що порушує обмеження.

(2) Оголошення масиву з мінімальним юридичним розміром - 1 (ваш випадок). У цьому випадку будь-які спроби взяти покажчик p->s[0]і використовувати його для арифметики вказівника, що виходить за рамки, p->s[1]є невизначеною поведінкою. Наприклад, реалізація налагодження дозволена для створення спеціального вказівника із вбудованою інформацією про діапазон, який буде ловитись щоразу, коли ви намагаєтесь створити покажчик за його межами p->s[1].

(3) Оголошення масиву, наприклад, "дуже великого" розміру, наприклад 10000. Ідея полягає в тому, що оголошений розмір повинен бути більшим за все, що вам може знадобитися в реальній практиці. Цей метод не містить UB щодо масиву доступу до масиву. Однак на практиці ми, звичайно, завжди будемо виділяти менший об'єм пам'яті (лише стільки, скільки реально потрібно). Я не впевнений у законності цього, тобто мені цікаво, наскільки законним є виділення об’єкту менше пам'яті, ніж оголошений розмір об'єкта (якщо припустити, що ми ніколи не отримуємо доступу до "не виділених" членів).


1
У (2), s[1]не визначена поведінка. Це те саме *(s+1), що і те саме *((char *)p + offsetof(struct T, s) + 1), що є дійсним вказівником на a charу виділеному об'єкті.
R .. GitHub СТОП ДОПОМОГА ВІД

З іншого боку, я майже впевнений (3) у невизначеній поведінці. Щоразу, коли ви виконуєте будь-яку операцію, яка залежить від такої структури, що знаходиться за цією адресою, компілятор може генерувати машинний код, який читає з будь-якої частини структури. Це може бути марним, або може бути функцією безпеки для суворої перевірки розподілу, але немає причин, що впровадження цього не може зробити.
R .. GitHub ЗАСТАНОВИТИ ДІЯ

R: Якщо в масиві було оголошено розмір (це не тільки foo[]синтаксичний цукор для *foo), то будь-який доступ, що перевищує менший його оголошений розмір і призначений розмір - це UB, незалежно від того, як робилася арифметика вказівника.
zwol

1
@Zack, ти помилився з кількох речей. foo[]в структурі не є синтаксичним цукром для *foo; це гнучкий член масиву С99. Для решти дивіться мою відповідь та коментарі до інших відповідей.
R .. GitHub СТОП ДОПОМОГА ВІД

6
Проблема полягає в тому, що деякі члени комітету відчайдушно хочуть, щоб цей "хак" був UB, оскільки вони передбачають якусь казкову країну, де впровадження C могло б забезпечити межі покажчика. На краще чи гірше, однак, це буде суперечити іншим частинам стандарту - таким, як здатність порівнювати покажчики на рівність (якщо межі були закодовані в самому покажчику) або вимога, щоб будь-який об'єкт був доступний через уявний накладений unsigned char [sizeof object]масив . Я відстоюю своє твердження, що гнучкий член масиву "рубить" для pre-C99 має чітко визначену поведінку.
R .. GitHub ЗАСТОСУЄТЬСЯ ДОПОМОГА

3

У стандарті цілком зрозуміло, що ви не можете отримати доступ до речей біля кінця масиву. (а перехід через покажчики не допомагає, оскільки вам не дозволяється навіть збільшувати покажчики минулого після закінчення масиву).

І за «роботу на практиці». Я бачив оптимізатор gcc / g ++, що використовує цю частину стандарту, таким чином генеруючи неправильний код при зустрічі з цим недійсним C.


ви можете навести приклад?
Тал

1

Якщо компілятор приймає щось подібне

typedef structure {
  int len;
  char dat [];
};

Я думаю, що цілком зрозуміло, що він повинен бути готовим прийняти підписник на 'dat' понад його довжину. З іншого боку, якщо хтось кодує щось на кшталт:

typedef structure {
  int що завгодно;
  char dat [1];
} MY_STRUCT;

а потім пізніше звертається до деякоїструктури-> дат [х]; Я не думаю, що компілятор не бере жодного зобов'язання використовувати адрес-обчислювальний код, який буде працювати з великими значеннями x. Я думаю, якби хтось хотів бути справді безпечним, правильна парадигма була б більше схожа на:

#define LARGEST_DAT_SIZE 0xF000
typedef structure {
  int що завгодно;
  char dat [LARGEST_DAT_SIZE];
} MY_STRUCT;

а потім зробіть балот (sizeof (MYSTRUCT) -LARGEST_DAT_SIZE + бажаний_матриця_ довжина) байтів (маючи на увазі, що якщо бажана_арра__ довжина більша за LARGEST_DAT_SIZE, результати можуть бути невизначені).

Між іншим, я думаю, що рішення заборонити масиви нульової довжини було невдалим (деякі старі діалекти, такі як Turbo C підтримують його), оскільки масив нульової довжини може розглядатися як знак того, що компілятор повинен генерувати код, який буде працювати з більшими індексами .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.