#prog #rust #itsec #article
sudo-rs' first security audit
> Запросили аудит sudo-rs
> Нашли уязвимость в дизайне, которая есть и в оригинальном sudo
sudo-rs' first security audit
> Запросили аудит sudo-rs
> Нашли уязвимость в дизайне, которая есть и в оригинальном sudo
Ferrous-Systems
sudo-rs' first security audit
Thanks to funding from NLNet and ISRG,
the sudo-rs team was able to request an audit from Radically Open Security (ROS).
In this post, we'll share the findings of the audit and our response to those findings.
ROS performed crystal-box penetration...
the sudo-rs team was able to request an audit from Radically Open Security (ROS).
In this post, we'll share the findings of the audit and our response to those findings.
ROS performed crystal-box penetration...
👍16😁12🤯4
#prog #rust #article
Destructing trees safely and cheaply
О том, как дропать глубоко вложенные рекурсивные типы без переполнения стека.
Destructing trees safely and cheaply
О том, как дропать глубоко вложенные рекурсивные типы без переполнения стека.
ismailmaj.github.io
Destructing trees safely and cheaply
👍5
Forwarded from ☕️ Мерлин заваривает τσάι 🐌
Примерно через 3 часа UNIX timestamp достигнет красивой отметки 1700000000
https://www.epochconverter.com/countdown?q=1700000000
https://www.epochconverter.com/countdown?q=1700000000
❤11🔥9🎉6
#prog #rust #article
How I Improved My Rust Compile Times by 75%*
*с инкрементальной сборкой
TL;DR: инкрементальная сборка выигрывает от использования более быстрого линкера (mold для Linux/sold для Macos) и бекенда Cranelift.
How I Improved My Rust Compile Times by 75%*
*с инкрементальной сборкой
TL;DR: инкрементальная сборка выигрывает от использования более быстрого линкера (mold для Linux/sold для Macos) и бекенда Cranelift.
benw.is
How I Improved My Rust Compile Times by 75%
One of Rust's often mentioned pain points is slow compile times. In order to have nice things like the borrow checker, safety guarantees, and zero cost abstractions, we pay in time spent compiling. I was able to decrease that time by 75%.
👍4🔥2
#rust #article
Why Rust in Production?
(советую включить режим чтения в вашем браузере, фон ядовито-оранжевого цвета)
Статья о взгляде на Rust со стороны компаний.
The intent is to provide an honest look at Rust's practicality for production to help decision-makers understand its benefits and challenges.
==========
Один из моментов в статье:
Rust is a relatively young language. Version 1.0 was first released in 2015. This means that the ecosystem is still maturing. Many important libraries did not see their 1.0 release yet.
Это правда, но есть много 0.x-библиотек, которые де-факто стабильны.
Why Rust in Production?
(советую включить режим чтения в вашем браузере, фон ядовито-оранжевого цвета)
Статья о взгляде на Rust со стороны компаний.
The intent is to provide an honest look at Rust's practicality for production to help decision-makers understand its benefits and challenges.
==========
Один из моментов в статье:
Rust is a relatively young language. Version 1.0 was first released in 2015. This means that the ecosystem is still maturing. Many important libraries did not see their 1.0 release yet.
Это правда, но есть много 0.x-библиотек, которые де-факто стабильны.
#prog #rust #моё
Опытные Rust-программисты знают, что в Rust можно сделать мапу, которая индексируется не рантайм-ключами, а типами. Сделать это довольно просто поверх обычной мапы: достаточно хранить значения в виде
С дополнительными обёртками можно делать мапы с отображениями из
Пусть для определённости мы хотим хранить коллбеки вида
Наиболее простой способ сделать это — забоксить замыкание дважды:
Опытные Rust-программисты знают, что в Rust можно сделать мапу, которая индексируется не рантайм-ключами, а типами. Сделать это довольно просто поверх обычной мапы: достаточно хранить значения в виде
Box<dyn
Any>
, а в качестве ключей использовать TypeId (хотя и есть немного неочевидный нюанс). В геттере мы конструируем TypeId
от переданного типа, индексируем им мапу, и если значение есть, то приводим к нужному типа через downcast_ref/downcast_mut.#[derive(Default)]
struct TypeMap(HashMap<TypeId, Box<dyn Any>>);
impl TypeMap {
fn insert<T: 'static>(&mut self, val: T) {
self.0.insert(TypeId::of::<T>(), Box::new(val));
}
fn get<T: 'static>(&self) -> Option<&T> {
self.0.get(&TypeId::of::<T>())?.downcast_ref::<T>()
}
}
fn main() {
let mut map = TypeMap::default();
map.insert(42_i32);
assert_eq!(map.get::<i32>(), Some(&42));
}
С дополнительными обёртками можно делать мапы с отображениями из
T
в тип, параметризованный T
:T
-> Option<T>
T
-> [T; 2]
T
-> Vec<T>
В общем, принцип понятен. Но что делать, если мы хотим хранить в параметризованной типами мапе коллбеки, связанные с типом?Пусть для определённости мы хотим хранить коллбеки вида
FnMut(&mut T)
. Как я уже говорил, в Rust нет общего типа для замыканий одной сигнатуры — есть лишь общий трейт. Для того, чтобы сохранить замыкание в TypeMap
, нам потребуется каким-то образом стереть тип дважды: сначала от конкретного типа замыкания до dyn FnMut(&mut T)
, а потом от него до dyn Any
.Наиболее простой способ сделать это — забоксить замыкание дважды:
// определение `TypeMap` выше
#[derive(Default)]
struct CallbackMap(TypeMap);
impl CallbackMap {
fn insert<T: 'static>(&mut self, f: impl FnMut(&mut T) + 'static) {
let boxed = Box::new(f) as Box<dyn FnMut(&mut T) + 'static>;
self.0.insert(boxed);
}
fn get_by_arg<T: 'static>(&mut self) -> Option<&mut (dyn FnMut(&mut T) + 'static)> {
self.0.get_mut::<Box<dyn FnMut(&mut T) + 'static>>().map(|f| &mut **f)
}
}
fn main() {
let mut map = CallbackMap::default();
let multiplier = 7;
let f = move |x: &mut i32| *x *= multiplier;
map.insert(f);
let mut x = 6;
map.get_by_arg().unwrap()(&mut x);
assert_eq!(x, 42);
}
doc.rust-lang.org
Any in std::any - Rust
A trait to emulate dynamic typing.
👍7❤2🤔2
Решение выше полностью безопасно, но неудовлетворительно ввиду двойной аллокации и двойной индирекции (даже тройной, считая саму мапу). Можно ли сделать лучше? Можно... Но для этого нам придётся задействовать фичи nightly, а именно — ptr_metadata. Почему? Потому что — забегая вперёд — нам потребуется стирать типы у жирных толстых указателей на трейт-объекты, и без этого API это невозможно сделать, не закладываясь на детали реализации текущей версии.
Что можно сказать наверняка — так это то, что один раз боксить всё-таки придётся. Вне зависимости от того, что мы сконструируем, это что-то должно будет хранить
Немного подумаем о том, что из себя представляет указатель на трейт-объект. Так как это — толстый указатель, он состоит из указателя на данные и... DynMetadata.
Казалось бы, дело в шляпе: стираем типы у указателей на данные и у vtable, а при запросе кастим в нужный тип:
Есть только одна небольшая проблема.
Мы не можем нормально дропать
И действительно, если мы скопируем код с
Кажется, мы зашли в тупик. Мы можем легко получить
Что можно сказать наверняка — так это то, что один раз боксить всё-таки придётся. Вне зависимости от того, что мы сконструируем, это что-то должно будет хранить
!Sized
значения. И это что-то само по себе должно быть Sized
, чтобы его можно было положить в мапу. Таким образом, нам потребуется что-то, что будет хранить указатели. Мы, очевидно, не можем хранить просто Box<dyn FnMut(&mut T)>
, поскольку это разные типы для разных T
, а для хранения в мапе тип нужен одинаковый.Немного подумаем о том, что из себя представляет указатель на трейт-объект. Так как это — толстый указатель, он состоит из указателя на данные и... DynMetadata.
DynMetadata
— это особый тип, который хранит в себе указатель на таблицу. Если DynMetadata
инстанцирован неким dyn
-типом, то таблица содержит в себе размер и выравнивание реального значения, его деструктор и методы соответствующего трейта. Методы трейт-объекта являются тонкой обёрткой, которые просто загружают нужный метод из этой таблицы и вызывают его на указателе данных. Что приятно — DynMetadata
имеет одно и то же представление вне зависимости от подставленного типа... И да, именно на это мы и будем полагаться (еееее, больше ансейфа!).Казалось бы, дело в шляпе: стираем типы у указателей на данные и у vtable, а при запросе кастим в нужный тип:
type ErasedVtable = DynMetadata<()>;Из-за стирания типов мы не знаем, какой реально тип мы храним, и должны при даункасте спрашивать его у окружения, но, по крайней мере, этот код работает.
struct ErasedDyn {
data: *mut (),
vtable: ErasedVtable,
}
impl ErasedDyn {
fn new<T, F>(f: F) -> Self
where
T: 'static,
F: FnMut(&mut T) + 'static,
{
let boxed = Box::new(f) as Box<dyn FnMut(&mut T)>;
let (data, vtable) = Box::into_raw(boxed).to_raw_parts();
Self {
data,
vtable: unsafe { std::mem::transmute(vtable) },
}
}
unsafe fn downcast_mut_unchecked<T: 'static>(&mut self) -> &mut dyn FnMut(&mut T) {
let data_ptr = self.data;
let vtable = std::mem::transmute(self.vtable);
&mut *std::ptr::from_raw_parts_mut(data_ptr, vtable)
}
}
#[derive(Default)]
struct CallbackMap {
map: HashMap<TypeId, ErasedDyn>,
}
impl CallbackMap {
fn insert<T: 'static>(&mut self, f: impl FnMut(&mut T) + 'static) {
self.map.insert(TypeId::of::<T>(), ErasedDyn::new(f));
}
fn get<T: 'static>(&mut self) -> Option<&mut dyn FnMut(&mut T)> {
self.map
.get_mut(&TypeId::of::<T>())
.map(|cb| unsafe { cb.downcast_mut_unchecked() })
}
}
Есть только одна небольшая проблема.
Мы не можем нормально дропать
ErasedDyn
.И действительно, если мы скопируем код с
main
выше и запустим его, то он будет работать, но если запустить его с miri, то он услужливо подскажет, что у нас утекла память.Кажется, мы зашли в тупик. Мы можем легко получить
TypeId
по T
, но не наоборот. Возможно, можно было бы хранить деструктор вместе с ErasedDyn
, но если мы добавим возможность удалять коллбеки из мапы, то у нас не выйдет автоматически дропать его вне мапы. Можно добавить деструктор и в сам ErasedDyn
, но тогда он будет занимать размер трёх usize
против двух usize
у нативных толстых указателей. Видимо, выход один: хранить деструктор в памяти, на который указывает ErasedDyn
.GitHub
Tracking Issue for pointer metadata APIs · Issue #81513 · rust-lang/rust
This is a tracking issue for the RFC 2580 "Pointer metadata & VTable" (rust-lang/rfcs#2580). The feature gate for the issue is #![feature(ptr_metadata)]. About tracking issues Trackin...
👍3❤2
Но сначала немного о том, как это деструктор должен выглядеть. С одной стороны, в нём должен быть код для дропа конкретного типа. С другой стороны, он должен принимать на вход стёртые аргументы. Кажущеюся противоречие разрешается за счёт двух вещей: лямбды без захватов могут быть приведены к функциональным указателям и захват типа не ведёт к захвату значения:
Именно, этот unsound код полагается на то, что
Покажу наглядно, как ломается текущий подход:
type Destructor = unsafe fn(*mut (), ErasedVtable);Возникает небольшой вопрос, что подставить вместо
fn make_drop_fn<T>() -> Destructor {
|data, vtable| unsafe {
let dyn_ptr: *mut ??? =
std::ptr::from_raw_parts_mut(data, std::mem::transmute(vtable));
drop(Box::from_raw(dyn_ptr))
}
}
???
. Это не может быть dyn FnMut(&mut T)
, так как нам ещё нужно как-то добавить деструктор. Что ж, сделаем под это тип:#[repr(C)]В
struct WithDrop<T: ?Sized> {
destructor: Destructor,
data: T,
}
make_drop_fn
вместо ???
подставим WithDrop<dyn FnMut(&mut T)>
. Очевидно, конструктор ErasedDyn
придётся немного поменять:impl ErasedDyn {Ну и, раз уж мы сделали деструктор, то, очевидно, надо сделать надлежащую реализацию
fn new<T, F>(f: F) -> Self
where
T: 'static,
F: FnMut(&mut T) + 'static,
{
let with_drop = WithDrop {
destructor: make_drop_fn::<T>(),
data: f,
};
let boxed = Box::new(with_drop) as Box<WithDrop<dyn FnMut(&mut T)>>;
let (data, vtable) = Box::into_raw(boxed).to_raw_parts();
Self {
data,
vtable: unsafe { std::mem::transmute(vtable) },
}
}
}
Drop
:impl Drop for ErasedDyn {Каст из
fn drop(&mut self) {
unsafe {
let destructor = self.data.cast::<Destructor>().read();
destructor(self.data, self.vtable);
}
}
}
data
в указатель на Destructor
валиден за счёт того, что WithDrop
имеет фиксированную раскладку в памяти — #[repr(C)]
— и адрес первого поля совпадает с адресом структуры в целом. Окей, с дропом мы разобрались, но теперь нам нужно поменять метод для даункаста. Теперь нам надо каким-то образом получить из указателя на WithDrop<dyn FnMut(&mut T)>
указатель на data
. Кажется, достаточно просто сместить указатель на размер Destructor
?..impl ErasedDyn {А вот и нет.
unsafe fn downcast_mut_unchecked<T: 'static>(&mut self) -> &mut dyn FnMut(&mut T) {
let data_ptr = self
.data
.byte_offset(std::mem::size_of::<Destructor>() as isize);
let vtable = std::mem::transmute(self.vtable);
&mut *std::ptr::from_raw_parts_mut(data_ptr, vtable)
}
}
Именно, этот unsound код полагается на то, что
data
располагается в памяти сразу после destructor
, без промежутков. И это абсолютно необоснованное предположение! Именно, unsized coercion добавляет vtable к указателям на данные, но, разумеется, никак не трогает данные за указателем. Раскладка в памяти данных за указателем зависит от исходного типа, из которого произошло unsized coercion — и конкретно в случае WithData
тип поля data
вполне мог иметь выравнивание большее, чем у Destructor
! Как следствие, между destructor
и data
может быть паддинг, и, как несложно понять, подсчитать его статически после стирания типов нельзя.Покажу наглядно, как ломается текущий подход:
fn main() {Неправильная реализация
#[repr(align(16))]
struct OverAligned(i32);
let mut map = CallbackMap::default();
let multiplier = OverAligned(7);
// захватываем multiplier целиком, а не только поле
let f = move |x: &mut i32| *x *= { &multiplier }.0;
map.insert(f);
let mut x = 6;
map.get().unwrap()(&mut x);
assert_eq!(x, 42);
}
downcast_mut
выдаёт указатель куда-то в паддинг, в неинициализированные данные. Попытка их прочитать приводит к неопределённому поведению. На практике это означает, что ассерт у вас почти наверняка выстрелит (а в дебажной сборке — может и свалиться с паникой из-за переполнения при умножении). Если запустить этот пример в miri, то он выдаёт ошибку на момент создания ссылки — некорректное выравнивание.❤3👍3
Так как же всё починить? Очевидно, нам нужно хранить не только деструктор, но и смещение до данных. Поменяем
БОНУС: если мы хотим обойтись одной аллокацией и не хотим быть менее эффективным, чем нативные толстые указатели, но готовы немного поступиться типобезопасностью, то хранить коллбеки со стиранием типа можно и сильно проще:
WithDrop
на WithHeader
:#[derive(Clone, Copy)]В конструкторе
struct Header {
data_offset: isize,
destructor: Destructor,
}
#[repr(C)]
struct WithHeader<T: ?Sized> {
header: Header,
data: T,
}
ErasedDyn
нужно также дополнительно высчитывать смещение до собственно данных:impl ErasedDyn {Деструктор почти не поменялся, только вместо вычитывания деструктора напрямую по указателю указатель сначала кастуется в
fn new<T, F>(f: F) -> Self
where
F: FnMut(&mut T) + 'static,
{
let mut with_hdr = WithHeader {
header: Header {
data_offset: 0,
destructor: make_drop_fn::<T>(),
},
data: f,
};
unsafe {
with_hdr.header.data_offset =
<*const _>::byte_offset_from(&with_hdr.data, &with_hdr);
}
let boxed = Box::new(with_hdr) as Box<WithHeader<dyn FnMut(&mut T)>>;
let (data, vtable) = Box::into_raw(boxed).to_raw_parts();
Self {
data,
vtable: unsafe { std::mem::transmute(vtable) },
}
}
}
Header
и из него уже достаётся destructor
. Немного сложнее даункаст:impl ErasedDyn {И вот это уж корректно работает. miri не жалуется даже на это:
unsafe fn downcast_mut_unchecked<T>(&mut self) -> &mut dyn FnMut(&mut T) {
let data_offset = (*self.data.cast::<Header>()).data_offset;
let data_ptr = self.data.byte_offset(data_offset);
let vtable = std::mem::transmute(self.vtable);
&mut *std::ptr::from_raw_parts_mut(data_ptr, vtable)
}
}
fn main() {===============
#[repr(align(128))]
struct A(String);
impl A {
fn get(&self) -> &str {
&self.0
}
}
let mut a = A(String::from("a"));
let f = move |x: &mut i32| {
*x += a.get().len() as i32;
a.0 += "a";
};
let mut map = CallbackMap::default();
map.insert(f);
let mut x = 4;
map.get().unwrap()(&mut x);
assert_eq!(x, 5);
map.get().unwrap()(&mut x);
assert_eq!(x, 7);
}
БОНУС: если мы хотим обойтись одной аллокацией и не хотим быть менее эффективным, чем нативные толстые указатели, но готовы немного поступиться типобезопасностью, то хранить коллбеки со стиранием типа можно и сильно проще:
#[derive(Default)]
struct ErasedCallbackMap(HashMap<TypeId, Box<dyn FnMut(&mut dyn Any)>>);
fn wrap<T: 'static>(mut f: impl FnMut(&mut T) + 'static) -> Box<dyn FnMut(&mut dyn Any)> {
Box::new(move |arg| f(arg.downcast_mut().unwrap()))
}
impl ErasedCallbackMap {
fn insert<T: 'static>(&mut self, f: impl FnMut(&mut T) + 'static) {
self.0.insert(TypeId::of::<T>(), wrap(f));
}
fn get<T: 'static>(&mut self) -> Option<&mut (dyn FnMut(&mut dyn Any) + 'static)> {
self.0.get_mut(&TypeId::of::<T>()).map(|f| &mut **f)
}
}
fn main() {
let mut map = ErasedCallbackMap::default();
let multiplier = 7;
let f = move |x: &mut i32| *x *= multiplier;
map.insert(f);
let mut x = 6;
// аргумент приводится к &mut dyn Any
map.get::<i32>().unwrap()(&mut x);
assert_eq!(x, 42);
}
🔥8👍2❤1