לדלג לתוכן

9.2 - העברת משאבים וזיכרון בין תהליכים - הרצאה

העברת סמכות במקום העברת מספר

מספר מתאר הוא מפתח בטבלה של תהליך מסוים. אם תהליך אחד שולח לתהליך אחר את המספר שבע בתוך הודעה רגילה, המקבל אינו מקבל גישה לקובץ של השולח. אצלו המספר שבע יכול להתייחס למשאב אחר או לא להיות תקף כלל. כדי להעביר גישה למשאב צריך לבקש מהקרנל ליצור עבור המקבל הפניה מתאימה.

בסוקט מקומי אפשר להעביר מתארים כחלק מנתוני הבקרה של הודעה באמצעות SCM_RIGHTS. הקרנל אינו שומר בהכרח את אותו מספר, אלא מתקין מספר חדש בטבלת המקבל. ההפניה החדשה קשורה לתיאור הקובץ הפתוח, ולכן חלק מהמצב, כגון מיקום קובץ משותף, יכול להיות משותף בין הצדדים.

העברת מתאר היא גם העברת סמכות. שירות שפותח קובץ רגיש ואז מוסר את המתאר מאפשר למקבל לבצע את הפעולות שהמתאר מאפשר, גם אם המקבל לא היה יכול לפתוח את הנתיב בעצמו. לכן צריך לאמת מי רשאי לקבל איזה משאב. העובדה שההעברה מתבצעת מקומית אינה הופכת אותה לפעולה חסרת משמעות אבטחתית.

גוף הודעה ונתוני בקרה

הפונקציות sendmsg ו־recvmsg מאפשרות להעביר לצד הנתונים הרגילים גם רצף רשומות בקרה. כל רשומה כוללת סוג, רמה ואורך. הרשומות דורשות יישור מתאים, והאורך שלהן אינו בהכרח זהה לכמות הזיכרון שצריך להקצות כולל ריפוד. לכן משתמשים בממשקי החישוב של מערכת היעד במקום לכתוב קבוע מספרי שנראה נכון במחשב אחד.

המקבל צריך לטפל בקטיעה של נתוני הבקרה. אם הגיעו יותר מתארים ממה שהבאפר מסוגל להכיל, אסור להמשיך כאילו ההודעה התקבלה בשלמותה. כמו כן, מתארים שכבר התקבלו צריכים לקבל בעלים מיד, כדי שמסלול שגיאה מאוחר יותר לא ידלוף אותם.

הדגל MSG_CMSG_CLOEXEC מבקש שהמתארים המתקבלים ייווצרו עם התכונה המתאימה למניעת ירושה בעת החלפת תוכנית. קביעה מאוחרת באמצעות קריאה נוספת משאירה חלון מול תהליכון אחר שמבצע יצירת תהליך והחלפת תוכנית. זהו אותו סוג מרוץ שכבר ראינו ביצירת מתארים אחרים.

עזר לשליחה ולקבלה של מתאר אחד

הקוד הבא מיועד ללינוקס עם תלות libc = "0.2". נשתמש בסוקט מקומי מסוג הודעות כדי לשמור על גבול ההודעה בניסוי. הפונקציה המקבלת מאפשרת מקום לארבעה מתארים, עוטפת את כל המתארים שנקלטו בבעלות ואז דורשת שבפרוטוקול שלנו התקבל בדיוק אחד.

use std::fs::File;
use std::io::{self, Write};
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::os::unix::fs::FileExt;
use std::os::unix::net::UnixDatagram;

fn control_storage(count: usize) -> (Vec<usize>, usize) {
    let bytes = unsafe { libc::CMSG_SPACE((count * std::mem::size_of::<RawFd>()) as _) } as usize;
    let words = bytes.div_ceil(std::mem::size_of::<usize>());
    (vec![0; words], bytes)
}

fn send_fd(socket: &UnixDatagram, descriptor: RawFd) -> io::Result<()> {
    let mut tag = [b'F'];
    let mut vector = libc::iovec { iov_base: tag.as_mut_ptr().cast(), iov_len: tag.len() };
    let (mut control, bytes) = control_storage(1);
    let mut message: libc::msghdr = unsafe { std::mem::zeroed() };
    message.msg_iov = &mut vector;
    message.msg_iovlen = 1;
    message.msg_control = control.as_mut_ptr().cast();
    message.msg_controllen = bytes;
    unsafe {
        let header = libc::CMSG_FIRSTHDR(&message);
        (*header).cmsg_level = libc::SOL_SOCKET;
        (*header).cmsg_type = libc::SCM_RIGHTS;
        (*header).cmsg_len = libc::CMSG_LEN(std::mem::size_of::<RawFd>() as _) as usize;
        std::ptr::write(libc::CMSG_DATA(header).cast::<RawFd>(), descriptor);
    }
    loop {
        let count = unsafe { libc::sendmsg(socket.as_raw_fd(), &message, 0) };
        if count == 1 { return Ok(()); }
        if count >= 0 { return Err(io::Error::other("unexpected payload length")); }
        let error = io::Error::last_os_error();
        if error.kind() != io::ErrorKind::Interrupted { return Err(error); }
    }
}

fn receive_fd(socket: &UnixDatagram) -> io::Result<OwnedFd> {
    let mut tag = [0_u8; 1];
    let mut vector = libc::iovec { iov_base: tag.as_mut_ptr().cast(), iov_len: tag.len() };
    let (mut control, bytes) = control_storage(4);
    let mut message: libc::msghdr = unsafe { std::mem::zeroed() };
    message.msg_iov = &mut vector;
    message.msg_iovlen = 1;
    message.msg_control = control.as_mut_ptr().cast();
    let count = loop {
        message.msg_controllen = bytes;
        let count = unsafe { libc::recvmsg(socket.as_raw_fd(), &mut message, libc::MSG_CMSG_CLOEXEC) };
        if count >= 0 { break count; }
        let error = io::Error::last_os_error();
        if error.kind() != io::ErrorKind::Interrupted { return Err(error); }
    };
    let mut descriptors = Vec::new();
    unsafe {
        let mut header = libc::CMSG_FIRSTHDR(&message);
        while !header.is_null() {
            if (*header).cmsg_level == libc::SOL_SOCKET && (*header).cmsg_type == libc::SCM_RIGHTS {
                let base = libc::CMSG_LEN(0) as usize;
                let length = (*header).cmsg_len.checked_sub(base)
                    .ok_or_else(|| io::Error::other("bad control length"))?;
                if length % std::mem::size_of::<RawFd>() != 0 {
                    return Err(io::Error::other("bad descriptor array"));
                }
                let data = libc::CMSG_DATA(header).cast::<RawFd>();
                for index in 0..length / std::mem::size_of::<RawFd>() {
                    let raw = std::ptr::read(data.add(index));
                    descriptors.push(OwnedFd::from_raw_fd(raw));
                }
            }
            header = libc::CMSG_NXTHDR(&message, header);
        }
    }
    if count != 1 || tag != [b'F'] || message.msg_flags & (libc::MSG_CTRUNC | libc::MSG_TRUNC) != 0
        || descriptors.len() != 1 {
        return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid descriptor message"));
    }
    Ok(descriptors.pop().unwrap())
}

באפר הבקרה נשען על מערך מילים כדי לספק יישור מתאים למבני הבקרה של יעד המעבדה. הפענוח מתבסס על רשומות שסיפק הקרנל, ולא על מבנה שרירותי שהועתק ישירות מבתים שהעמית בנה. עדיין אנחנו בודקים את חוזה הפרוטוקול שלנו ואת דגלי הקטיעה. אם ההודעה נדחית, הווקטור משמיד את כל המתארים שכבר קיבל בעלות עליהם.

פרסום תוכן שאינו משתנה

ניצור קובץ אנונימי בזיכרון, נכתוב תוכן ואז נחתום אותו מפני כתיבה ושינוי גודל. רק אחרי החתימה נשלח את המתאר. כך המקבל יכול לקרוא תוכן יציב בלי לבנות מיד פרוטוקול סנכרון של זיכרון משותף משתנה.

הפונקציה הראשית הבאה מצטרפת לשתי פונקציות העזר. היא מבצעת קריאה במיקום מפורש, משום שסמן הקובץ של השולח כבר נמצא אחרי הנתונים.

fn main() -> io::Result<()> {
    let raw = unsafe { libc::memfd_create(c"published-data".as_ptr(),
                                        libc::MFD_CLOEXEC | libc::MFD_ALLOW_SEALING) };
    if raw < 0 { return Err(io::Error::last_os_error()); }
    let mut original = unsafe { File::from_raw_fd(raw) };
    original.write_all(b"shared-data")?;
    let seals = libc::F_SEAL_WRITE | libc::F_SEAL_GROW | libc::F_SEAL_SHRINK | libc::F_SEAL_SEAL;
    if unsafe { libc::fcntl(original.as_raw_fd(), libc::F_ADD_SEALS, seals) } < 0 {
        return Err(io::Error::last_os_error());
    }
    let (sender, receiver) = UnixDatagram::pair()?;
    send_fd(&sender, original.as_raw_fd())?;
    let received = File::from(receive_fd(&receiver)?);
    let observed_seals = unsafe { libc::fcntl(received.as_raw_fd(), libc::F_GET_SEALS) };
    if observed_seals < 0 { return Err(io::Error::last_os_error()); }
    assert_eq!(observed_seals & seals, seals);
    assert_eq!(received.metadata()?.len(), 11);
    let mut buffer = [0_u8; 11];
    assert_eq!(received.read_at(&mut buffer, 0)?, 11);
    assert_eq!(&buffer, b"shared-data");
    drop(original);
    assert_eq!(received.read_at(&mut buffer, 0)?, 11);
    println!("received independent descriptor to sealed content");
    Ok(())
}

החתימה מתבצעת לפני יצירת מיפויים ניתנים לכתיבה. הניסיון להוסיף חתימת כתיבה במצבים אחרים עלול להיכשל, ולכן יש לבדוק את התוצאה ולא להניח שהקובץ נעשה קבוע. השם של הקובץ האנונימי נועד לאבחון ואינו מנגנון זהות או הרשאה.

זהות עמית ומצב משותף

אפשר לקבל פרטי זהות של העמית באמצעות ממשקים מתאימים לסוקט המקומי. פרטים אלה עונים על שאלה של זהות כפי שהקרנל מציג אותה, אך אינם מחליפים מדיניות שמחליטה מה אותה זהות רשאית לקבל. בפרק הבידוד נבדוק גם כיצד מרחבי משתמש משנים את הפרשנות של מזהים.

אם התוכן נשאר משתנה, מסירת המתאר אינה מפרסמת באופן קסום גרסה עקבית של כל השדות. צריך להגדיר סדר כתיבה, סדר פרסום ואופן המתנה. מצביעים רגילים מתוך מרחב כתובות אחד אינם תקפים במרחב האחר, ולכן מבנים משותפים משתמשים בדרך כלל בהיסטים ובגדלים בדוקים.

גם מיפוי לקריאה בלבד אצל המקבל אינו מונע משולח אחר לשנות או לקצר את הקובץ. לכן עטיפה שמחזירה פרוסה רגילה ארוכת חיים צריכה להוכיח שהשינוי החיצוני אינו מפר את החוזה שלה. החתימות שבניסוי הן דרך לצמצם את הבעיה, ואינן תחליף לבדיקה של סוג המשאב והזכויות שהתקבלו.

התיעוד של הודעות קבלה, הסוקט המקומי והקובץ האנונימי מגדיר את פרטי ההעברה. בכל שירות נוסיף מעליו חוזה עסקי שמגדיר בדיוק איזה מתאר מותר לקבל ומה עושים אם ההודעה אינה מתאימה.