# Как Unsafe мне в ногу стрелял
Во многих языках программирования, которые предоставляют какие-то гарантии безопасности [0^], есть инструменты чтобы эту самую безопасность сломать. Например рефлексия с C#/Kotlin/etc, или unsafe в Rust. О последнем я сегодня и хочу поговорить.
unsafe в расте даёт много возможностей (на самом деле список достаточно короткий, но мощный), но его корректность не может проверить компилятор. Поэтому о корректности алгоритма используещего unsafe приходится думать програмисту.
Вот простая диограмма показывающая когда вам нужно использовать unsafe:
Дело в том что с unsafe одна ошибка и ты ошибся. Простая опечатка может привести к совершенно катастрофическим ошибкам вроде утечек памяти, UB [1^], сегфолтов и т.д..
Ещё у unsafe есть свойство протекать. Ошибка допущенная в одном месте в
Теперь, когда я рассказал что такое «unsafe» и с чем его едят, перейдём к моему случаю из жизни. Я писал функцию которая должна была инициализировать массив используя функцию, переданную пользователем библиотеки.
Те кто знают что такое unsafe уже напряглись, потому что вызывать в unsafe коде пользовательскую функцию надо с огромной осторожностью. Какие-бы (безопасные) непотребства пользовательская функция не выполняла, твой unsafe код не должен привести к UB и подобным ошибкам.
Но в итоге через пол часа дум, гуглежа, копипасты и ругани матом я написал эту функцию. Получилось примерно 100 строк запутанного unsafe кода в корректности которого я не был до конца уверен. Но функция работала как и было положенно, можно ликовать!
...или нет...
Unsafe может выстрелить совершенно неожиданно. Так и произошло в этом случае. Из 9 тестов этой функции, которые я написал, 2 не прошли. Смысл был в том, что если пользовательская функция паникует или возвращает ошибку, то деструкторы значений которые уже инициализированны, не вызывались. Это могло бы привести к утечкам памяти [2^].
"Но как так?? Я же это предусмотрел, я вызываю деструкторы! Это самая сложная часть кода..." — моя первая мысль. Признаюсь, в начале я думал что неправильно написал тесты (считать дропы и ловить паники это не самые очевидные вещи). Но в тестах ошибок не было. Я пытался сделать mre [3^] этого бага, но ничего не выходило.
Я потратил ещё около 2-х часов (в 4 раза дольше чем я писал функцию!) чтобы понять в чём была моя ошибка, и ещё минут 10 чтобы её починить. А в итоге ошибка была в том, что я забыл вызвать одну функцию, из-за чего я дропал не
Мораль:
0. не пишите unsafe код
1. серьёзно, не пишите
2. не пишите и .
3. хорошо тестируйте unsafe код с разным и неправильным вводом, если вы всё-таки его написали (это то место, где типы не спасают [5^])
4. будьте внимательны и осторожны привыходе из вагонов написании unsafe кода
#вафля_программист
Во многих языках программирования, которые предоставляют какие-то гарантии безопасности [0^], есть инструменты чтобы эту самую безопасность сломать. Например рефлексия с C#/Kotlin/etc, или unsafe в Rust. О последнем я сегодня и хочу поговорить.
unsafe в расте даёт много возможностей (на самом деле список достаточно короткий, но мощный), но его корректность не может проверить компилятор. Поэтому о корректности алгоритма используещего unsafe приходится думать програмисту.
Вот простая диограмма показывающая когда вам нужно использовать unsafe:
Хочешь написать unsafe?
|
|
Нет <--+--> Да
| |
| |
---->+<----
|
|
перехочешь
Дело в том что с unsafe одна ошибка и ты ошибся. Простая опечатка может привести к совершенно катастрофическим ошибкам вроде утечек памяти, UB [1^], сегфолтов и т.д..
Ещё у unsafe есть свойство протекать. Ошибка допущенная в одном месте в
unsafe {} блоке может выстрелить в совершенно другом месте, из-за чего отладка некоректно написанного unsafe-кода становится той ещё головной болью.Теперь, когда я рассказал что такое «unsafe» и с чем его едят, перейдём к моему случаю из жизни. Я писал функцию которая должна была инициализировать массив используя функцию, переданную пользователем библиотеки.
Те кто знают что такое unsafe уже напряглись, потому что вызывать в unsafe коде пользовательскую функцию надо с огромной осторожностью. Какие-бы (безопасные) непотребства пользовательская функция не выполняла, твой unsafe код не должен привести к UB и подобным ошибкам.
Но в итоге через пол часа дум, гуглежа, копипасты и ругани матом я написал эту функцию. Получилось примерно 100 строк запутанного unsafe кода в корректности которого я не был до конца уверен. Но функция работала как и было положенно, можно ликовать!
...или нет...
Unsafe может выстрелить совершенно неожиданно. Так и произошло в этом случае. Из 9 тестов этой функции, которые я написал, 2 не прошли. Смысл был в том, что если пользовательская функция паникует или возвращает ошибку, то деструкторы значений которые уже инициализированны, не вызывались. Это могло бы привести к утечкам памяти [2^].
"Но как так?? Я же это предусмотрел, я вызываю деструкторы! Это самая сложная часть кода..." — моя первая мысль. Признаюсь, в начале я думал что неправильно написал тесты (считать дропы и ловить паники это не самые очевидные вещи). Но в тестах ошибок не было. Я пытался сделать mre [3^] этого бага, но ничего не выходило.
Я потратил ещё около 2-х часов (в 4 раза дольше чем я писал функцию!) чтобы понять в чём была моя ошибка, и ещё минут 10 чтобы её починить. А в итоге ошибка была в том, что я забыл вызвать одну функцию, из-за чего я дропал не
T, а MaybeUninit<T> [4^] что является no-op. В обычной ситуации компилятор раста ударил бы меня по рукам, но в unsafe коде он бессилен.Мораль:
0. не пишите unsafe код
1. серьёзно, не пишите
2. не пишите и .
3. хорошо тестируйте unsafe код с разным и неправильным вводом, если вы всё-таки его написали (это то место, где типы не спасают [5^])
4. будьте внимательны и осторожны при
#вафля_программист
Telegram
Мне не нравится реальность
[0^]: под безопасностью имеется в виду безопасность памяти, публичность/приватность методов и т.д.
[0^]: под безопасностью имеется в виду безопасность памяти, публичность/приватность методов и т.д.
[1^]: UB — Undefined behavior, это код который может привести к чему угодно. Код с UB может просто ничего не делать, вызвать функцию которая в коде ни разу не вызывается, призвать демонов, напечатать 42 в консоль или отключить тормоза в твоей машине. Что угодно. Насколько я это понимаю, UB нужно чтобы компилятор мог проводить более агрессивные оптимизации.
[2^]: Не вызвать деструктор (aka
Drop::drop) в расте считается безопасным поведением. Так что ничего фундаментально плохого я не сделал. Но всё же такое поведение неприемлимо.[4^]: MaybeUninit<_> — специальный тип в расте для работы с неинициализированной памятью. Так как значение этого типа могут быть неинициализированными, то для них деструкторы не вызываются.
P.S. вот сам код с ошибкой. тут видно (тот случаай когда подскаки типов рулят), что создаётся слайс из
Закоментированный метод превращает
MaybeUninit<Item>, который затем дропается (что является no-op из-за MaybeUninit). Закоментированный метод превращает
&mut [MaybeUninit<Item>] в &mut [Item] что решает проблему невызванных деструкторов.Писать людям на английском так страшно... не хочу ошибиться и случайно нагрубить...
Вызов методов в расте би лайк:
(да это всё делает одно и тоже и я не щучу)
let a: A = ...;
a.method();
A::method(&a);
<A as Trait>::method(&a);
<_>::method(&a);
// nightly version
A::method.call((&a,));
<A as Trait>::method.call((&a,));
<_>::method.call((&a,));
A::method.call_mut((&a,));
<A as Trait>::method.call_mut((&a,));
<_>::method.call_mut((&a,));
A::method.call_once((&a,));
<A as Trait>::method.call_once((&a,));
<_>::method.call_once((&a,));
(да это всё делает одно и тоже и я не щучу)
Вспомнил ещё три способа:
Trait::method(&a);Так-же &Антон напомнил что
<_ as Trait>::method(&a);
<A>::method(&a);
call{_once,_mut,} это тоже функции так что...<_>::call_once(<_>::method, (&a,));
Немного сумаществия: (playground)На морозе телефон разряжается так быстро, что я не успеваю включить яндекс карты
Я очень плохой человек если наываю модули с разницей в одну букву?