LINUX.ORG.RU

Если вам не хватало UB в C, то вам принесли ещё

 ,


1

3

Привет, мои дорогие любители сишки!

Если вам начало казаться, что разработчики стандарата языка C стали предсказуемыми и больше не могут удивлять вас новыми идеями, то вы ошибались. В новом стандарте C23, комитет постановил:

— zero-sized reallocations with realloc are undefined behavior;

То есть вот это валидный код:

void *ptr = malloc(0);
free(ptr);

А вот это – UB:

void *ptr = malloc(4096);
ptr = realloc(ptr, 0); <-- хаха UB

И это несмотря на то, что в манах уже давно написано следующее:

If size is equal to zero, and ptr is not NULL, then the call is equivalent to free(ptr)

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

В тред призываются известные эксперты по C: @Stanson и @alex1101, возможно они смогут нам объяснить, зачем разработчики стандарта C постоянно пытаются отстрелить себе обе ноги самыми нелепыми способами.



Последнее исправление: cumvillain (всего исправлений: 1)

Ответ на: комментарий от cumvillain

Ладно, возможно я был не прав. Но какие-то костыли для системных хедеров там точно были. Возможно, для -std=c11 они и не нужны. Но pedantic тут не факт что поможет, т.к. там могут быть исключения прописаны чтоб много чего в этих хедерах не действовало.

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

Так. Оптимизации вокруг UB возможны как раз при наличии unsafe без UB. Чтение элементов массива без проверок на длину этого массива, например. Или, в случае LRU, выделение/освобождение памяти в unsafe. Если бы доступ по освобождённому указателю не был UB, его надо было бы при каждом чтении как-то проверять или гарантировать, что доступа к освобождённому никогда не будет.

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

Так. Оптимизации вокруг UB возможны как раз при наличии unsafe без UB. Чтение элементов массива без проверок на длину этого массива, например.

А тебе не кажется что это уже следствие? Компилятор не делает вокруг этого оптимизаций, и выигрыш мы получаем потому что в unsafe этих проверок просто нет изначально.

Или, в случае LRU, выделение/освобождение памяти в unsafe. Если бы доступ по освобождённому указателю не был UB, его надо было бы при каждом чтении как-то проверять или гарантировать, что доступа к освобождённому никогда не будет.

Я как-то разделяю эти две вещи:

  • Я испортил память и теперь программа невалидная
  • Компилятор встретил в коде UB, и на основании этого, сделал неправильные выводы

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

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

из твоего монитора вылезет бородатый мужик и отшлёпает тебя по твоей маленькой жопке

неплохие фантазии, я, кста, не против

Да ты и бородатой собаки из монитора не против…

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

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

В случае Rust unsafe «Undefined behavior affects the entire program.». Если в Java/Python через FFI можно испортить память VM, то в Rust unsafe позволяет передать в LLVM код, который будет оптимизироваться также как C/C++ с выкидыванием «ненужных» блоков кода (даже если эти блоки не в unsafe).

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

Компилятор не делает вокруг этого оптимизаций, и выигрыш мы получаем потому что в unsafe этих проверок просто нет изначально.

На первом этапе - да. А на втором компилятор может убрать недоступный код на основании условий UB или заменить на более быстрый алгоритм. Например, for(int i=a; i!=b; i++) сразу выкидывать, если b меньше, чем a и внутри цикла нет побочных эффектов.

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

то в Rust unsafe позволяет передать в LLVM код, который будет оптимизироваться также как C/C++ с выкидыванием «ненужных» блоков кода (даже если эти блоки не в unsafe).

Не понимаю откуда ты это берешь. Порча памяти и use after free аффектят всю программу вне зависимости от оптимизаций.

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

Порча памяти и use after free аффектят всю программу вне зависимости от оптимизаций.

Да. Но Rust можно заставить вести себя как Си.

if cond() 
{  
  do1();
} else { 
  do2();
  let x = &mut 42;
  let xptr = x as *mut i32;
  let x1 = unsafe { &mut *xptr };
  let x2 = unsafe { &mut *xptr };
  *x1 = 0;
}

При оптимизации может скомпилироваться в безусловный вызов do1().

А в Java аналогичный код в любом случае может испортить память только после вызова do2().

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

Ядро Linux на Си и UB в нём нет.

Linux разваливается в хлам если не останавливать clang в попытках оптимизировать код.

Именно поэтому Linux собирается с

-fno-strict-aliasing
-fno-delete-null-pointer-checks
-fno-allow-store-data-races
-fno-strict-overflow

https://github.com/torvalds/linux/blob/6098d87eaf31f48153c984e2adadf14762520a87/Makefile#L560

https://github.com/torvalds/linux/blob/6098d87eaf31f48153c984e2adadf14762520a87/Makefile#L809

https://github.com/torvalds/linux/blob/6098d87eaf31f48153c984e2adadf14762520a87/Makefile#L824

https://github.com/torvalds/linux/blob/6098d87eaf31f48153c984e2adadf14762520a87/Makefile#L993z

Как минимум эти 4 флага, это оптимизации, которые отключены, потому что Linux разваливается если их включить и разрабы Linux знают об этом и просто не способны избавиться от UB в их коде.

А с этими флагами и примеры UB из этой ветки работают ожидаемо :)

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

Нашёл коммит, когда добавили -fno-delete-null-pointer-checks

https://github.com/torvalds/linux/commit/a3ca86aea507904148870946d599e07a340b39bf

Пример, просто фейспалм.

Вот код,

static void __devexit agnx_pci_remove(struct pci_dev *pdev)
{
    struct agnx_priv *priv = dev->priv;
    if (!dev)
       return;
    // что-то дальше, неважно
}

Очевидно, что использование dev раньше теста на NULL, поэтому компилятор выкинул

if (!dev)
   return;

Они решили что это не секьюрно, так как добавляет дыры в ядро. А так, если отключить оптимизации, то надеются что их пронесёт и хаков будет меньше.

Turning on this flag could prevent the compiler from optimising away some "useless" checks for null pointers.  Such bugs can sometimes become exploitable at compile time because of the -O2 optimisation.


Clearly the 'fix' is to stop using dev before it is tested, but building with -fno-delete-null-pointer-checks flag at least makes it harder to abuse.

Просто пример для @Forum0888 , а то будет верить в святых разработчиков ядра, которые пишут без UB.

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

Не для спора.

Код ядра с UB покажите (да и просто плохой код) ...

Всем.

Ну шо ребята, не пора ли нам код ядра проверить?

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

Вот, например: https://godbolt.org/z/8br67vzbE

fn test(num: i32) -> bool {
    num < 10
}

fn square(num: i32) -> i32 {
    if test(num) {
        0
    } else {
        // BUG: causes UB in safe code if `test()` returns `false`
        unsafe { std::hint::unreachable_unchecked() }
    }
}

Скомпилируется в

square:
        xor     eax, eax
        ret

И никакого сюрприза в этом нет. UB оно и в расте UB

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

В примере же есть. Вот, разыменование NULL указателя https://github.com/torvalds/linux/commit/6bf676723ab2a6ade05feb2cd0b5073930f72d7e

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

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

Супер конечно!

Интересно всё же разобраться «почему?».

Эх, было бы в неделе тридцать дней, а в сутках сто часов ...

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

Потому что Линус ориентировался только на gcc, а разработчики gcc ориентировались на ядро Линукса. В смысле, старались не сломать компиляцию ядра.

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

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

Почему в Rust смогли, а в C – нет?

В Rust свои UB есть:https://doc.rust-lang.org/reference/behavior-considered-undefined.html

В C и Rust разные трактовки термина «Undefined Behaviour». В Rust это означает «если ты обосрался с указателями, сам виноват, мы ничего не гарантируем». В C это означает «мы изнасилуем твой код и ты получишь чудовищный треш и адов холокост».

Проще говоря, Rust не делает диких трансформаций кода с расчётом на это самое UB или его отсутствие, как в примерах про C выше.

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

Текст программы взять с годболта (для поста я выкинул неважные детали вроде pub и #[no_mangle]), записать в test.rs и

rustc --crate-type lib --emit asm -O test.rs

Результат в test.s

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

Проще говоря, Rust не делает диких трансформаций кода с расчётом на это самое UB или его отсутствие, как в примерах про C выше.

Пример же привели для Rust.

Трансформации делает LLVM и они абсолютно те же самые, что и для Си.

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

Вот программа с опечаткой, которая не выводит никаких предупреждений, а просто выкидывает «лишний код»: https://godbolt.org/z/rx79Kv46Y

#[inline(never)]
pub fn search(num: i32, arr: [i32; 10]) -> i32 {
    // returns 1 iff num in arr, else 0
    for i in 0..=10 {
        unsafe {
            if *arr.get_unchecked(i) == num {
                return 1
            }
        }
    }
    0
}

pub fn main () {
    let arr: [i32; 10] = [1,2,3,4,5,6,7,8,9,10];
    print!("{}", search(42, arr));
    print!("{}", search(69, arr));
}

Выведет два раза 1, несмотря на то, что в массиве нет ни 42 ни 69 и чтение arr[10] не может одновременно равняться и тому и другому.

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

Где «там»? У меня в рабочем проекте unsafe, в общем-то, только в FFI. Вызовы функций из сишных библиотек для оптимизатора непрозрачны, так что особого потенциала для неожиданных оптимизаций там нет. В стандартной библиотеке каждый unsafe уже раз по 5 раз перепроверен. В сторонних библиотеках, которые я использую, unsafe’ов тоже не так уж и много.

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

Где «там»?

Среднестатистически, в коде, написанном на Rust.

У меня в рабочем проекте unsafe, в общем-то, только в FFI.

В конечной программе на Си++ обычно тоже ни прямой работы с памятью ни переполнения знаковой арифметики.

В сторонних библиотеках, которые я использую, unsafe’ов тоже не так уж и много.

Ладно. Моё «очень часто» и твоё «не так уж и много» вполне может оказаться одним и тем же числом. Оценка субъективна.

monk ★★★★★
()
Ответ на: комментарий от monk
    for &i in arr.iter().skip(1).step_by(2)
    {
        if i == num {
            return 1
        }
    }

Но тут код получается менее эффективным. Оптимизатор не разворачивает цикл.

Можно сделать так и получить результат идентичный get_unchecked

    for &i in
        arr.iter()
        .enumerate()
        .filter(|(idx, _)| idx%2 == 1)
        .map(|(_, i)| i)

https://godbolt.org/z/ansbofKrj

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

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

Смотря как связывать.

Если через

clang ./clib.c -flto=thin -c -o ./clib.o -O2
# Create a static library from the C code
ar crus ./libxyz.a ./clib.o

# Invoke `rustc` with the additional arguments
rustc -Clinker-plugin-lto -L. -Copt-level=2 -Clinker=clang -Clink-arg=-fuse-ld=lld ./main.rs

то вроде прозрачны.

monk ★★★★★
()