10.3 - מרחבי שמות וזהות יחסית - הרצאה
אותו תהליך מכמה נקודות מבט¶
מרחב שמות משנה את ההקשר שבו תהליך רואה משאב מסוים. הוא אינו יוצר בהכרח מחשב חדש ואינו מפריד אוטומטית את כל משאבי המערכת. תהליך יכול לראות שם מחשב אחר ובאותו זמן לקרוא בדיוק את אותם קבצים שההורה שלו רואה.
ההבחנה הזאת חשובה כשמנתחים סביבת מכולה. במקום לשאול רק האם התהליך נמצא במכולה, נשאל אילו מרחבים הופרדו, אילו מתארים הועברו פנימה, אילו סמכויות נשארו ואילו מגבלות משאבים הוגדרו. תשובה רק לשאלה הראשונה אינה מספיקה כדי להסיק מה התהליך יכול לעשות.
נבנה ניסוי שמפריד מרחב משתמש ומרחב שם מחשב, ומשאיר בכוונה את מערכת הקבצים משותפת. כך נראה גם את ההפרדה וגם את הגבול שלה. לאחר מכן נוסיף למפה את מרחבי התהליכים, הרשת והעיגונים.
מיפוי זהויות במקום שינוי המשתמש בחוץ¶
בתוך מרחב משתמש חדש אפשר למפות את מזהה המשתמש הרגיל בחוץ למזהה אפס בפנים. המספר שמוצג לתהליך משתנה, אבל הוא אינו הופך אוטומטית למנהל המערכת במרחב המשתמש הראשוני. הסמכות נבדקת ביחס למרחב שהמשאב והפעולה קשורים אליו.
המיפויים נכתבים בקבצים שמגדירים טווחי מזהים. כל שורה מתארת התחלה בפנים, התחלה בחוץ ואורך טווח. כתיבת המיפוי כפופה לכללי הרשאה, ואי אפשר לבחור באופן חופשי כל משתמש במערכת רק משום שהצלחנו ליצור מרחב חדש. מיפוי רחב יותר עשוי לדרוש טווחים שהוקצו למשתמש וכלי עזר מוגדרים.
עבור מיפוי קבוצות של משתמש רגיל נדרש בדרך המקובלת למנוע שינוי קבוצות באמצעות הקובץ המתאים לפני כתיבת מפת הקבוצות. הסדר הזה אינו פרט קוסמטי של דוגמה; הוא חלק מהחוזה שמונע שימוש במרחב החדש כדי לשנות משמעות של קבוצות בדרך לא מורשית.
ניסוי של זהות פנימית ושם מחשב¶
הקוד הבא משתמש בתלות libc = "0.2" ומריץ את השינוי בתהליך ילד נפרד. ההורה שומר את שם המחשב שלו, יוצר קובץ ניסוי ואז בודק ששמו לא השתנה. הילד מקבל מזהה פנימי אפס, משנה את שם המחשב במרחב החדש ומוכיח שהקובץ של ההורה עדיין גלוי לו.
use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
fn hostname() -> io::Result<String> {
let mut bytes = [0_u8; 256];
if unsafe { libc::gethostname(bytes.as_mut_ptr().cast(), bytes.len()) } < 0 {
return Err(io::Error::last_os_error());
}
let length = bytes.iter().position(|&byte| byte == 0)
.ok_or_else(|| io::Error::other("hostname not terminated"))?;
Ok(String::from_utf8_lossy(&bytes[..length]).into_owned())
}
fn child(shared: &Path) -> io::Result<()> {
let uid = unsafe { libc::getuid() };
let gid = unsafe { libc::getgid() };
if unsafe { libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWUTS) } < 0 {
return Err(io::Error::last_os_error());
}
fs::write("/proc/self/uid_map", format!("0 {uid} 1\n"))?;
fs::write("/proc/self/setgroups", b"deny\n")?;
fs::write("/proc/self/gid_map", format!("0 {gid} 1\n"))?;
assert_eq!(unsafe { libc::geteuid() }, 0);
let name = b"amit-child";
if unsafe { libc::sethostname(name.as_ptr().cast(), name.len()) } < 0 {
return Err(io::Error::last_os_error());
}
assert_eq!(hostname()?, "amit-child");
assert_eq!(fs::read(shared)?, b"shared-content\n");
println!("child uid={}, hostname={}", unsafe { libc::geteuid() }, hostname()?);
println!("user namespace: {}", fs::read_link("/proc/self/ns/user")?.display());
println!("uts namespace: {}", fs::read_link("/proc/self/ns/uts")?.display());
println!("mount namespace: {}", fs::read_link("/proc/self/ns/mnt")?.display());
Ok(())
}
struct Shared(PathBuf);
impl Drop for Shared {
fn drop(&mut self) { let _ = fs::remove_file(&self.0); }
}
fn main() -> io::Result<()> {
if std::env::args().nth(1).as_deref() == Some("--child") {
let path = std::env::args_os().nth(2).expect("shared file path");
return child(Path::new(&path));
}
if unsafe { libc::geteuid() } == 0 {
return Err(io::Error::other("run as an ordinary non-root user"));
}
let before = hostname()?;
let path = PathBuf::from(format!("namespace-shared-{}", std::process::id()));
let mut file = OpenOptions::new().write(true).create_new(true).open(&path)?;
let shared = Shared(path);
file.write_all(b"shared-content\n")?;
drop(file);
println!("parent hostname={before}");
println!("parent mount namespace: {}", fs::read_link("/proc/self/ns/mnt")?.display());
let status = Command::new(std::env::current_exe()?).arg("--child").arg(&shared.0).status()?;
assert_eq!(hostname()?, before);
if !status.success() { return Err(io::Error::other("namespace child failed; inspect policy/support")); }
Ok(())
}
התוצאה הצפויה בסביבה שמאפשרת את הניסוי היא שם פנימי חדש ומזהה פנימי אפס, לצד שם הורה שלא השתנה. מזהה מרחב העיגונים נשאר משותף, והקריאה לקובץ מצליחה. המספרים שמופיעים בקישורי המרחבים הם נתוני הרצה, ולכן אינם קבועים בדוגמה.
מערכת מסוימת יכולה להגביל יצירת מרחבי משתמש לא מורשים או להפעיל מדיניות נוספת שמונעת את הקריאה. במקרה כזה מתעדים את השגיאה ואת התצורה; אין להציג את הניסוי כאילו הושלם. שגיאת הרשאה לבדה אינה מסבירה בהכרח איזו שכבה החליטה לדחות את הפעולה.
מה כל מרחב משנה¶
מרחב הרשת מפריד בין ממשקים, כתובות, נתיבים וחלק ממצב הפרוטוקולים. מרחב העיגונים מפריד את תמונת נקודות העיגון. מרחב התקשורת הבין־תהליכית מפריד משפחות משאבים מסוימות של תקשורת, אך אינו הופך כל זיכרון שהתהליך מחזיק לפרטי. מרחב שם המחשב משנה את שם המחשב והתחום המתאים.
מרחב קבוצות המשאבים משנה את נקודת המבט על היררכיית הקבוצות, ואינו מגדיר לבדו מכסת מעבד או זיכרון. את המגבלות מגדירים בבקרי המשאבים שנלמד בהמשך. מרחב הזמן יכול לשנות תצוגה של שעונים מסוימים לפי החוזה שלו, ואינו שעון קיר עצמאי לכל תהליך באופן כללי.
הטבלה המושגית הזאת עוזרת לנסח ניסוי שלילה. אם יצרנו רק מרחב רשת, נצפה ששינוי נתיב פנימי לא ישנה את נתיב ההורה, אבל לא נצפה שקובץ שהעברנו יהיה בלתי נגיש. כל טענת בידוד צריכה להיות קשורה למשאב מסוים ולבדיקה מתאימה.
מספר תהליך הוא יחסי¶
תהליך יכול לקבל מספר שונה כאשר מסתכלים עליו מתוך מרחבי תהליכים שונים. המרחב החיצוני יכול לראות תהליכים שנמצאים במרחבים צאצאים, בעוד שהתצוגה הפנימית מוגבלת לפי ההיררכיה. לכן מספר שמופיע בהודעה או ביומן צריך להיות מפורש בהקשר שממנו הגיע.
יצירת מרחב תהליכים באמצעות unshare משפיעה על ילדים שייווצרו לאחר מכן; היא אינה פשוט משנה את המספר של הקורא הנוכחי. התהליך הראשון שנוצר בפנים מקבל תפקיד מיוחד של תהליך אתחול למרחב, כולל אחריות ותנאי אותות מיוחדים. שירות שרץ בתפקיד הזה צריך גם לטפל בילדים יתומים ובאיסוף סיומם לפי התכנון.
כדי שתצוגת התהליכים דרך מערכת הקבצים תתאים למרחב החדש, צריך גם עיגון מתאים של ממשק התהליכים. אחרת אפשר לראות תצוגה שמגיעה מהקשר אחר ולהסיק בטעות שהפרדת המספרים לא עבדה. זאת דוגמה לכך ששני מנגנונים נפרדים צריכים להשתלב כדי ליצור סביבת משתמש עקבית.
מי מחזיק את המרחב בחיים¶
מרחב יכול להישאר קיים כל עוד יש אליו הפניות מתאימות, למשל מתהליך חבר או ממתאר שמצביע אליו. מחיקת שם נוח של מרחב רשת אינה בהכרח השמדת כל המרחב. לכן כלי שמייצר סביבות צריך לנהל גם את חיי התהליכים והמתארים, ולא רק את רשימת השמות.
הצטרפות למרחב קיים באמצעות setns כפופה להרשאות ולמגבלות של סוג המרחב. היא אינה פעולה כללית שמותר לבצע בכל רגע מכל תהליכון. בתכנון שירות נעדיף לקבוע את סביבת ההרצה בשלב אתחול ברור, לפני יצירת עובדים שתלויים בהקשר הקיים.
התיעוד של סוגי המרחבים, מיפויי המשתמש, מרחב התהליכים ופעולת ההפרדה מפרט את החוזים. בהרצאה הבאה נחבר את המרחבים לתמונת מערכת קבצים ולנתיב רשת מוגדרים.