ניהול מיפויים והגנת עמודים¶
נניח שאנחנו רוצים אזור עבודה שהחיים שלו מנוהלים כמו Vec, אבל המקור שלו הוא mmap. אם נחזיר למשתמש מצביע גולמי ונבקש ממנו לזכור לקרוא munmap, כל מסלול שגיאה יהפוך להזדמנות לדליפה או לביטול מוקדם. נבנה בעלים שמייצג מיפוי אנונימי פרטי ואינו מאפשר להחזיק פרוסה מעבר לחייו.
הדוגמה הבאה דורשת libc. היא מגבילה את האורך לטווח שאפשר לייצג בפרוסת ראסט, מטפלת בתוצאת MAP_FAILED, ומחזירה הפניה משתנה רק דרך השאלה משתנה של הבעלים.
use std::{io, ptr::NonNull};
struct Mapping { pointer: NonNull<u8>, len: usize }
impl Mapping {
fn new(len: usize) -> io::Result<Self> {
if len == 0 || len > isize::MAX as usize {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid length"));
}
let raw = unsafe { libc::mmap(std::ptr::null_mut(), len,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS, -1, 0) };
if raw == libc::MAP_FAILED { return Err(io::Error::last_os_error()); }
let Some(pointer) = NonNull::new(raw.cast::<u8>()) else {
unsafe { libc::munmap(raw, len); }
return Err(io::Error::other("null mapping unsupported by this wrapper"));
};
Ok(Self { pointer, len })
}
fn bytes(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.pointer.as_ptr(), self.len) }
}
fn bytes_mut(&mut self) -> &mut [u8] {
unsafe { std::slice::from_raw_parts_mut(self.pointer.as_ptr(), self.len) }
}
}
impl Drop for Mapping {
fn drop(&mut self) {
unsafe { libc::munmap(self.pointer.as_ptr().cast(), self.len); }
}
}
fn main() -> io::Result<()> {
let mut memory = Mapping::new(4096)?;
assert!(memory.bytes().iter().all(|&b| b == 0));
memory.bytes_mut()[10] = 42;
assert_eq!(memory.bytes()[10], 42);
Ok(())
}
הבייטים של מיפוי anonymous חדש מאותחלים לאפס, ולכן יצירת פרוסת u8 אינה קוראת זיכרון לא מאותחל. העטיפה אינה ממפה קובץ שמשתנה מבחוץ, אינה משכפלת בעלות ואינה חושפת מתודה לביטול מיפוי דרך &self. אלה חלקים מההוכחה, ולא תכונות שוליות של הדוגמה.
המתודה bytes_mut קשורה ל־&mut self. בזמן שהפרוסה שלה עוד בשימוש, קוד בטוח אינו יכול להשמיד את Mapping או לבקש ממנו פרוסה משתנה נוספת. העובדה שהקרנל מאפשר munmap אינה אומרת שהעטיפה צריכה לחשוף אותו בכל רגע. הממשק הבטוח מצמצם את הפעולות כך שהחוזה של הקרנל והחוזה של השפה נשארים מסונכרנים.
הגנות עמודים מוסיפות ממד אחר. הפעולה mprotect משנה הרשאות של טווח מיושר בהתאם לכללי המערכת. אם נהפוך אזור לקריאה בלבד, אסור להשאיר ממשק בטוח שמחזיר ממנו &mut [u8] ומבטיח כתיבה. אפשר לייצג מצבי הרשאה בטיפוסים שונים, או לצרוך את הבעלים בעת מעבר מצב ולהחזיר בעלים חדש עם ממשק מצומצם.
עמוד guard הוא אזור ללא גישה שממוקם ליד אזור עבודה. גישה שחוצה את גבול האזור יכולה לפגוש אותו ולקבל fault במקום לדרוס בשקט נתון סמוך. זאת שכבת אבחון שימושית, אבל היא אינה תחליף לבדיקת גבולות: חריגה קטנה בתוך אותו עמוד עדיין יכולה לפגוע בנתונים אחרים בלי להגיע ל־guard.
כדי לבדוק הפרת גישה, הריצו תוכנית עזר נפרדת שממפה שלושה עמודים, משאירה את הראשון והאחרון ללא הרשאות ומאפשרת גישה רק לאמצעי. את הגישה המכוונת ל־guard מבצעים רק בתוכנית שנועדה להיכשל, לא מתוך תהליך שמנסה להמשיך כאילו לא קרה דבר. האב יכול לבדוק את אות הסיום בלי שהניסוי יפיל את סביבת הבדיקות כולה.
המדיניות W^X מבקשת למנוע מצב שבו אותו אזור ניתן גם לכתיבה וגם לביצוע. יוצר קוד דינמי יכול לכתוב במצב מתאים ואז לעבור להרשאת ביצוע, אבל הוא חייב לנהל גם כללי מטמון הוראות, מקביליות וחיי מצביעי הקוד לפי היעד. שינוי ביט הרשאה אינו לבדו מערכת JIT נכונה.
הקוד שלנו אינו בודק שגיאת munmap מתוך Drop, משום שאין שם ערוץ החזרה נוח. עבור מוצר אפשר להוסיף פעולת סגירה מפורשת שמחזירה תוצאה ולשמור את Drop כניקוי אחרון. בכל מקרה אסור להשאיר הפניות פעילות כאשר החלטנו לבטל את המיפוי.
הכללים של שינוי הרשאות נמצאים בעמוד mprotect. השאלה המרכזית בסקירת העטיפה היא אם לקוח שמשתמש רק בקוד בטוח יכול להחזיק הפניה שהרשאותיה או חייה כבר אינם תואמים למצב הקרנל.