понедельник, 1 декабря 2014 г.

Перезапись "чужой" памяти без segfault

Столкнулся с очень неприятным и неожиданным багом. Суть: при большом уровне вложенности функций становится возможным перезаписать соседнюю (от переменной) область памяти.

Условия:
Компилятор mpif90 (MacPorts gcc48 4.8.3_2)
ОС MacOS X Yosemite 10.10.1 со всеми обновлениями.
Программный код на фортране с очень большим стеком вызовов (глубина вложенности подпрограмм).

Исходные данные: 

В глубине кода есть функция function1, в которой объявлены два массива рациональных чисел с локальной (только внутри этой функции) областью видимости. Нигде в остальном коде переменных с такими названиями нет.
real, dimension(x) :: f0, f1 ! x - некая глобальная константа
Оба этих массива явно инициализируются:
f0 = c(x,y,z,:); f1 = f0 !c(x,y,z,:) определён, размерность 4-го измерения массива c равна размерности f0 и f1
Затем вызывается функция function2, которая принимает в качестве аргумента только f0 (который изменяется в процессе выполнения процедуры). При этом перед и после вызова функции стоит распечатка массива f1.
print *, 'f1', f1
temp = function2(f0) !temp - переменная для записи результата функции, f0 изменяется в функции
print *, 'f1', f1
Вывод на консоль (стабильно воспроизводимый результат, сборка кода сохранена):
f1 0.000 0.000 0.000 0.000 0.000 0.000
f1 1.000 0.000 0.000 0.000 0.000 0.000
То есть, происходит изменение значения массива, который не был передан функции в качестве аргумента!

Разборка показала, что в функции function2 массив для аргумента объявлен как:
real, dimension(y) :: farray ! y - некая глобальная константа, y > x, это было очень удобно для организации кода
Который дальше явно инициализируется целиком:
farray = 1. ! Массив при такой записи инициализируется полностью
Ситуацию исправило объявление:
real, dimension(x) :: farray ! x - первая глобальная константа
Вывод: function1 размещает массивы f0 и f1 рядом в памяти. Функция function2 принимает ссылку на адрес в памяти первого массива. Поскольку массив farray объявлен большим, чем f0, то при попытке записи значений в массив f0 "хвост" записывается в соседнюю ячейку памяти, перезаписывая попутно следующий массив (f1) в памяти.

В нормальной операционной системе (а это, имхо, бага именно операционки и системы защиты памяти в ней) должен произойти segmentation fault или page fault - защита памяти от несанкционированной перезаписи. Тут он не происходит, и мы можем спокойно пользоваться такими перезаписанными значениями.

Я пытался воспроизвести баг на "маленьком" коде типа:
module functions

    integer :: size = 35

    contains

    real function modify (n)
        implicit none
        real, dimension(size), intent(inout) :: n
        integer :: i
   
        do i = 1, size
            n(i) = n(i) + real(i)
        enddo
        !print *, n
        modify = sum(n)
    end function modify

end module functions

program checkbug
    use functions
    implicit none
    real, dimension(3) :: a,b
    real :: t

    a = (/-1.,-2.,-3./)
    b = a
   
    t = modify (a)

    print *, a
    print *, b
    read *
end program checkbug
При значении переменной size до 5 программа не перезаписывает массив и не происходит segfault, при 6 и более - не перезаписывает массив и при завершении программы происходит segfault (end program checkbug). То есть он спокойно всё распечатывает на экран, дожидается ввода с клавиатуры (аналог pause), и только на стадии завершения и сборки мусора падает с segfault.

То есть, видимо, есть какая-то критическая длина стека вызова в MacOS, при превышении которой перестаёт работать система защиты памяти. Что весьма странно, и, вообще говоря, какой-то полный провал с точки зрения безопасности.

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

2 комментария:

  1. Это похоже на проблему языка Си (и подобных ему) - по умолчанию отключена проверка на конец массива при записи.
    Если у тебя проблема внутри одной программы, то это не проблема операционки, а вот если программа (не подпрограмма) залезает в память другой программы, то тогда да, проблема операционки.

    ОтветитьУдалить
    Ответы
    1. Не знал про такую тонкость, спасибо!
      Только вот тут ( http://stackoverflow.com/questions/1021021/c-element-beyond-the-end-of-an-array ), например, пишется, что доступа при этом не даётся...

      Удалить