LINUX.ORG.RU

Почему надо проверять malloc на NULL

 , ,


0

2

Вообще выделение памяти проверять.

А вот потому что:

#include <stdio.h>
int main()
{
  unsigned char *a = NULL;
  int b = 0;
  unsigned long n = (unsigned long)&b;
 
  printf("b = %d, n = %lu\n",b,n);
  a[n]=1;
  printf("b = %d\n",b);
  return 0;
}

Запуск

$ gcc nullptr.c -o nullptr
$ ./nullptr 
b = 0, n = 140731726099148
b = 1

Запускалось в 64-битном (x86_64) Linux. Как видно, никаких сегфолтов и прочих ошибок. Конечно, адрес в районе 127 Тб в примере далеко за пределами доступного почти на всех компьютерах, но нет никаких гарантий, что на какой-то системе с каким-то компилятором и настройками среды значение не окажется более доступным. Могут быть и другие архитектуры (32-битные например), если запускать от root'а, то в начало может быть разрешена запись и там иметься память процесса. Или ещё какие-то варианты.

shdown, monk, liksys, Xenius - я думаю вам понравится. Пример сочинился по ходу чтения обсуждения Вышло издание 2,92 книги «Программирование: введение в профессию» А. В. Столярова (комментарий)

★★★★★

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

Там абстрактно говорили, но как раз рабочий и короткий пример для наглядности пришёл в голову.

anonymous_incognito ★★★★★
() автор топика

Потому что ты запихал в n адрес переменной b. Затем a[n] эквивалентно *(a+n), то есть ты просто записал по адресу b единицу. А компилятор с -Wall -Wextra ничего не сказал?

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

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

Потому что ты запихал в n адрес переменной b.

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

А компилятор с -Wall -Wextra ничего не сказал?

Ничего. Тем более ничего, если malloc использовать.

но если к нему добавить достаточно большой индекс, то можно испортить какую-то случайную область памяти.

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

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

Ну прибавил ноль к адресу и что?

Захотелось наглядного примера, что отсутствие проверки на NULL - это грязный код. Хотя вот некоторые даже учебники для начинающих пишут, приучая их к подобному стилю (malloc без проверки)

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

А как в растишке правильно обрабатывать?

В раст сделать побитовую запись в произвольный адрес памяти невозможно без ансейф.

Без ансейф можно делать копию, менять байт и записывать обратно - будет безопасно, работать, но уже не то.

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

У тебя в коде malloc’а вообще нет. Что ты «проверяешь»-то, прости господи?

Какая разница есть или нет? Речь о том, что если указатель равен NULL, что может случиться после вызова malloc, то нельзя надеяться, что программа и так упадёт при первом же разыменовании.

Ну вот тебе с malloc

#include <stdio.h>
#include <stdlib.h>
int main()
{
  unsigned char *a;
  int b = 0;
  
  a = malloc(10000000000000ULL); /* 10 Tb RAM requested */
  if (a==NULL)
  {
     printf("Null pointer!\n");
  }
  
  printf("b = %d\n",b);
  a[(long)&b] = 1;
  printf("b = %d, &b = %llu\n",b,&b);
  return 0;
}

Запуск

Null pointer!
b = 0
b = 1, &b = 140723024629860
anonymous_incognito ★★★★★
() автор топика
Ответ на: комментарий от anonymous_incognito

Да. Из одного указателя залезть на другой процессор не даёт.

Недостаток только тот, что на ошибку памяти прилетает не сегфолт, а недопустимая инструкция.

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

То есть, контролируется, что адрес не залез на чужой участок.

А как там тогда с адресной арифметикой, динамическими массивами, присваиванием одного указателя другому? Например, если нужен указатель, указывающий в середину массива.

P.S. Между прочим, похоже lcc не понимает спецификации %ul

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

Речь о том, что если указатель равен NULL, что может случиться после вызова malloc, то нельзя надеяться, что программа и так упадёт при первом же разыменовании.

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

Поэтому твои примеры вообще ничего для них не доказывают.

r--r--r--
()

Я тут какую-то фигню с указателями вытворяю, поэтому вы проверяйте маллок. Ну гений!

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

Или не слишком большой, если программа от root’а.

А причем здесь от root’а или нет?

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

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

То есть они исходят из того, что malloc вообще никогда не вернёт NULL.

Зря надеются на аварийное завершение, потмоу что, как видно и malloc может вернуть NULL и разыменование нулевого указателя в реальном коде может не привести к ошибке. Есть ulimit. Но и без него если сразу слишком много памяти запросить, то даже при включённом оверкомите будет NULL. В стандартной библиотеке не предусмотрена кросплатформенная функция, чтобы узнать количество доступной памяти, но можно даже сочинить пример поиска бинарным делением допустимого объёма памяти. Как раз с проверкой на NULL в процессе.

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

int main() {
    /* limits in MB */
    size_t low = 0;
    size_t high = 1024 * 1024; // Searchin within 1TB
    size_t success_mb, mid, mbmid, max = 0;
    const size_t MB = 1024 * 1024;

    printf("Binary search to non NULL max malloc...\n");

    while (low <= high) {
        mid = low + (high - low) / 2;
        mbmid = mid * MB; /* real size to allocate */

        
        void *ptr = malloc(mbmid);

        if (ptr != NULL) {
            /* Succes. Search for more */
            success_mb = mid;
            free(ptr); 
            low = mid + 1; 
        } else {
            // NULL. Lowering limits
            if (mid == 0) break; /* Protection from division by 0 / unreachability */
            high = mid - 1;
        }
    }

    printf("Maximum continuous block: %zu Mb\n", success_mb);
    return 0;
}

Ищет. Хотя, конечно чересчур оптимистично из-за оверкомита:

$ ./binmall 
Binary search to non NULL max malloc...
Maximum continuous block: 64203 Mb
anonymous_incognito ★★★★★
() автор топика
Ответ на: комментарий от anonymous_incognito

А как там тогда с адресной арифметикой, динамическими массивами, присваиванием одного указателя другому? Например, если нужен указатель, указывающий в середину массива.

Всё отлично. Указатель помнит от какого он массива и знает границы этого массива. Адресная арифметика меняет смещение. При любом разыменовании процессор проверяет, что смещение не вышло за границы. И ещё проверяется, что значение инициализировано и что не освобождено.

P.S. Между прочим, похоже lcc не понимает спецификации %ul

Да. Потому что должен быть %lu (linuxhint.com).

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

Зачем ты это пишешь?

А чего ты вообще сообщить хочешь?

В конце цепочки ссылок

«в glibc на линуксе malloc всегда возвратит не-NULL»

4.2

Автор как раз и указывает, что если malloc возвратит NULL нельзя полагаться на падение при разыменовании полученного указателя.

А про ulimit написал, так как ты написал, что «исходят из того, что malloc вообще никогда не вернёт NULL».

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

Да. Потому что должен быть %lu (linuxhint.com).

Брр, дурацкая ошибка, причём у себя поправил, раз пример запуска корректный был %-)

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

У рутовых процессов нет никаких дополнительных мапингов по сравнению с процессами непривилегированных пользователей, от UID это не зависит.

Могут права быть на доступ к кодовой секции. В принципе, если с -no-pie компилировать, то адреса секции с кодом получаются короткие (в несколько Мб) и могут оказаться индексом.

anonymous_incognito ★★★★★
() автор топика

Если к адресу прибавить 0, то будет тот же адрес. Вот это открытие.

Пишите еще.

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

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

Потому что такое понятие как «количество доступной памяти» не имеет смысла.

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

Потому что такое понятие как «количество доступной памяти» не имеет смысла.

С чего это? В норме имеет. Когда память действительно выделяется программе, а не только адрес, по которому она будет выдана при записи.

Другое дело, что нынче даже «количество свободных мест в самолёте» до того, как он взлетел, не имеет смысла, так как продают билетов больше чем мест.

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

Права на доступ к кодовой секции определяются битами в заголовке исполняемого файла, или вызовом mprotect().

Ни то, ни другое, от UID никак не зависит.

bigbit ★★★★★
()

Это всё понятно. Но интересно другое - а что достопочтенные доны в принципе предлагают делать если malloc() таки вернул NULL? Ну, помимо «поспать и попытаться ещё несколько раз, и если и это не помогло - упасть с диагностикой»?

bugfixer ★★★★★
()

Development, который мы заслужили.

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

bigbit ★★★★★
()

проверка результата malloc на NULL - зло, по двум причинам

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

2) если он таки NULL, то что ? есть разумный план действий без хипа вообще ?? в С++ даже в деструкторах может быть new

тут либо хардверное исключение «памяти реально нет и переключить контекст на заранее сделанный аварийный» (а как это отладить?) или периодично проверять достаточно/нет памяти на перспективу.

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

Если потом этот указатель индексируется чем-то, что потенциально может иметь большую величину, то лучше проверить.

Потому что теоретически можно например попасть индексом в memory mapped секцию и записать что-то на диск таким образом, испортив файл прежде чем программа упадёт.

Но это отсылает к более широкой проблеме, не к «проверять результат malloc», а к корректности обращения к памяти вообще и тому, что языки с гарантиями безопасности памяти - нужны.

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

Перехватываем libc’шный malloc(), падаем по ошибкам, и вуаля - клиентам больше проверять ничего не надо. В плюсах для этого new_handler имеется. Все довольны, все смеются. Думаю - все так и делают. Я что-то упускаю?

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

Ты точно так же аварийно помрёшь на обращении по адресу, но твой обработчик вообще никогда не выстрелит.

r--r--r--
()

Не знаю, зачем вы меня скастовали. Спасибо r--r--r-- за просвещение населения.

Вместо того, чтобы «в интернете кто-то не прав», покажу следующий фокус: если ты root, можно замаппить нулевой адрес. Тогда при разыменовании NULL на уровне ассемблера не будет сегфолта. Это про «всё равно упадём».

// Compile with:
//      cc -O0 map-zero-page.c -o map-zero-page
// When run as non-root:
//      mmap: Operation not permitted
//      Segmentation fault
// When run as root:
//      (nil)
//      x = 0

#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <sys/mman.h>

int main() {
    void *ptr = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
    if (ptr == MAP_FAILED) {
        perror("mmap");
    } else {
        printf("%p\n", ptr);
    }
    int x = *(int *) NULL;
    printf("x = %d\n", x);
}
shdown ★★
()
Ответ на: комментарий от wandrien

Думойте

Т.е. гномовцы именно это и сделали? Дык, молодцы.

ПыСы. Я не сомневаюсь что при определённой изобретательности систему со включённым overcommit’ом можно поставить раком. Но эти аспекты несколько ортогональны топику «проверять, или не проверять».

bugfixer ★★★★★
()
  • Markdown
Пустая строка (два раза Enter) начинает новый абзац. Знак '>' в начале абзаца выделяет абзац курсивом цитирования.
Внимание: прочитайте описание разметки Markdown.
Используйте Ctrl-Enter для размещения комментария