unsafe Rust از جایی شروع میشود که Rust دیگر امنیت را تضمین نمیکند
اول باید یک سوءتفاهم قدیمی را همان ابتدای راه حل کنیم.
آیا unsafe در Rust به معنی «غیرفعال کردن امنیت» است؟
نه. اصلاً.
Rust به ذاته زبانی است که روی قراردادها زندگی میکند.
borrow checker، lifetimeها و قوانین aliasing همه ابزارهایی هستند برای اینکه Rust بتواند یک جمله خیلی مهم را با خیال راحت بگوید:
اگر کد کامپایل شد، از نظر حافظه امن است.
اما این جمله فقط تا جایی اعتبار دارد که Rust بتواند بررسی کند.
از جایی به بعد، Rust صادقانه عقب میکشد.
شروع از نقطه کاملاً امن (Safe Rust)
بیایید از یک مثال کاملاً امن شروع کنیم:
fn main() {
let x = 42;
let r = &x;
println!("r = {}", r);
}
اینجا هیچ چیز خاصی اتفاق نمیافتد. borrow checker چند تضمین ساده اما حیاتی میدهد:
xتا زمانی که استفاده میشود زنده است- reference معتبر است
- هیچ alias خطرناکی وجود ندارد
- و مهمتر از همه: هیچ Undefined Behaviorای ممکن نیست
اینجا unsafe اصلاً معنا ندارد. همه چیز داخل قرارداد زبان است.
ساخت raw pointer بدون unsafe
حالا یک قدم جلوتر میرویم:
fn main() {
let x = 42;
let raw = &x as *const i32;
println!("raw pointer address: {:?}", raw);
}
این بخش معمولاً اولین شوک ذهنی را ایجاد میکند. ما یک raw pointer ساختیم، اما هیچ unsafeای ننوشتهایم.
ساختن raw pointer در Rust unsafe نیست. چرا؟ چون هنوز کاری انجام ندادهایم که Rust مجبور باشد چیزی را تضمین کند.
raw pointer یعنی این جمله خیلی مهم:
از این به بعد، Rust چیزی را چک نمیکند.
نه lifetime، نه aliasing، نه معتبر بودن حافظه. اما هنوز هیچ خطری رخ نداده، چون هنوز از pointer استفاده نکردهایم.
اولین unsafe block (اینجا دردسر شروع میشود)
مشکل دقیقاً از جایی شروع میشود که ما دست به استفاده از آن ببریم!
fn main() {
let x = 42;
let raw = &x as *const i32;
unsafe {
println!("value via raw pointer = {}", *raw);
}
}
dereference کردن raw pointer یعنی ورود رسمی به منطقهای که Rust دیگر مسئولیتی قبول نمیکند.
چرا این خط به unsafe نیاز دارد؟
چون Rust دیگر نمیداند:
- این pointer هنوز به حافظه معتبر اشاره میکند یا نه
- آن حافظه drop نشده یا نه
- همزمان mutable alias وجود دارد یا نه
پس Rust این قرارداد را پیشنهاد میدهد:
اگر میخواهی این کار را بکنی، خودت تضمین بده.
و این تعریف واقعی unsafe است:
unsafe یعنی: «من تضمین میکنم این کار امن است»
نه بیشتر، نه کمتر.
Undefined Behavior، بدترین چیزی که میتواند در Rust اتفاق بیفتد
بزرگترین خطر unsafe crash نیست. panic هم نیست. حتی segmentation fault هم نیست.
خطر واقعی چیزی است که Rust اسمش را گذاشته Undefined Behavior.
یک مثال ظاهراً بیخطر، ولی کاملاً اشتباه
fn main() {
let raw_ptr: *const i32;
{
let x = 123;
raw_ptr = &x as *const i32;
}
unsafe {
println!("value = {}", *raw_ptr);
}
}
این کد کامپایل میشود. بدون warning. بدون error.
اما از نظر Rust، این کد کاملاً اشتباه است.
چرا؟
xداخل یک scope داخلی ساخته شده- بعد از بسته شدن scope ما، متغییر drop شده
raw_ptrحالا به حافظهای اشاره میکند که دیگر متعلق به برنامه نیست
وقتی اجراش میکنی:
cargo run
ممکن است:
- عدد درست چاپ شود
- عدد اشتباه چاپ شود
- برنامه crash کند
- یا «فعلاً» درست کار کند
و همین «فعلاً» خطرناکترین بخش ماجراست.
UB یعنی Rust هیچ قولی نمیدهد
این وضعیت panic نیست. Rust هیچ قولی نمیدهد که چه اتفاقی میافتد.
UB یعنی: «هر اتفاقی مجاز است»
کامپایلر میتواند فرض کند چنین حالتی هرگز رخ نمیدهد و بر اساس همین فرض، کل کد اطرافش را بهینهسازی کند. نتیجه میتواند کاملاً غیرقابلپیشبینی باشد.
مقایسه با panic (تفاوت حیاتی)
fn main() {
let v = vec![1, 2, 3];
println!("{}", v[10]);
}
بیاد بیشتر به این قطعه کد panic میکند دقت کنیم:
- پیام خطا داریم
- stack trace داریم
- برنامه متوقف میشود
بد است، ولی امن است.
مثال دوم UB: mutable aliasing
fn main() {
let mut x = 10;
let r1 = &x as *const i32;
let r2 = &mut x as *mut i32;
unsafe {
*r2 = 20;
println!("{}", *r1);
}
}
اینجا همزمان داریم:
- یک reference immutable
- و یک reference mutable
چیزی که borrow checker همیشه جلویش را میگیرد. اما raw pointerها borrow checker را دور میزنند، و نه قوانین مدیریت حافظه.
Rust ساکت است. ولی UB داریم.
borrow checker دشمن نیست؛ محافظ است.
unsafe را مهندسی کن
اگر unsafe فقط خطر بود، Rust آن را ارائه نمیداد. unsafe ابزار ساخت abstraction است، نه shortcut.
هدف این نیست که unsafe همهجا پخش شود. هدف این است که unsafe محصور شود.
ساخت یک abstraction امن با unsafe داخلی
سناریو: میخواهیم یک wrapper ساده بسازیم که داخلش raw pointer داشته باشد، ولی بیرونش کاملاً safe باشد.
struct MyBox<T> {
ptr: *mut T,
}
اینجا Rust هیچ تضمینی نمیدهد. مسئولیت با ماست.
constructor امن
impl<T> MyBox<T> {
fn new(value: T) -> Self {
let boxed = Box::new(value);
let ptr = Box::into_raw(boxed);
Self { ptr }
}
}
از این لحظه به بعد:
- Rust دیگر این حافظه را drop نمیکند
- ما مسئول آزادسازی هستیم
Invariant اول:
ptrهمیشه به حافظه معتبر heap اشاره میکند
دسترسی امن به داده
impl<T> MyBox<T> {
fn get(&self) -> &T {
unsafe {
&*self.ptr
}
}
}
چرا unsafe؟ چون dereference raw pointer است.
چرا امن؟ چون invariantها را خودمان تعریف کردهایم.
Invariant دوم:
تا وقتی MyBox زنده است، ptr dangling نمیشود
آزادسازی حافظه با Drop
impl<T> Drop for MyBox<T> {
fn drop(&mut self) {
unsafe {
drop(Box::from_raw(self.ptr));
}
}
}
اگر این را ننویسیم، memory leak داریم. اگر دوبار انجامش دهیم، UB داریم.
Invariant سوم:
ptr فقط یکبار آزاد میشود
استفاده بیرونی (کاملاً safe)
fn main() {
let b = MyBox::new(42);
println!("value = {}", b.get());
}
هیچ unsafeای اینجا نیست. مصرفکننده حتی نمیتواند اشتباه کند.
جمعبندی نهایی
unsafe Rust هنوز Rust است. ولی دیگر Rust از تو محافظت نمیکند.
unsafe ابزار قدرت است، نه راحتی. اگر invariantها را دقیق نفهمیدی، unsafe ننویس — حتی اگر کدت کار میکند.
unsafe باید مثل ماده رادیواکتیو باشد: کم، محصور، و با هشدار واضح
دیدگاهها
اولین نفری باشید که دیدگاه میگذارد.