חקירת התנהגות מערכת לפי חוזה וגרסה¶
נניח שתוכנית עובדת במחשב הפיתוח ונכשלת בתוך container. הפיתוי הראשון הוא לבדוק את גרסת הקרנל ולהוסיף תנאי על המספר. הבעיה היא שמספר גרסה אינו מתאר את כל הסביבה: יכולות יכולות להיות מושבתות בקונפיגורציה, מוגבלות בהרשאות או מסוננות באמצעות seccomp. אנחנו צריכים לבדוק את הפעולה הדרושה ולהבחין בין היעדר תמיכה לבין איסור להשתמש בה.
נשתמש ב־pidfd_open כדוגמה. הפעולה מחזירה מתאר שמזהה תהליך, במקום להשאיר אותנו רק עם מספר PID שאפשר למחזר. בשיעורים הבאים נשתמש בזהות הזאת לניהול תהליכים, וכאן נתמקד באבחון זמינות. צרו פרויקט Cargo והוסיפו את חבילת libc באמצעות cargo add libc.
use std::io;
use std::os::fd::{FromRawFd, OwnedFd};
fn open_self_pidfd() -> io::Result<Option<OwnedFd>> {
let result = unsafe {
libc::syscall(libc::SYS_pidfd_open, std::process::id(), 0u32)
};
if result >= 0 {
// SAFETY: success returned a new descriptor owned by this call.
return Ok(Some(unsafe { OwnedFd::from_raw_fd(result as i32) }));
}
let error = io::Error::last_os_error();
if error.raw_os_error() == Some(libc::ENOSYS) { Ok(None) }
else { Err(error) }
}
fn main() -> io::Result<()> {
match open_self_pidfd()? {
Some(_fd) => println!("pidfd available for this process"),
None => println!("pidfd call unavailable in this environment"),
}
Ok(())
}
הקוד אינו מסיק שכל ENOSYS מוכיח שהקרנל ישן. גם שכבת סינון יכולה לבחור להחזיר שגיאה כזאת. התוצאה אומרת שהפעולה אינה זמינה במסלול שניסינו בסביבה הזאת. לעומת זאת, שגיאת הרשאה אינה הופכת אוטומטית לתירוץ להשתמש במסלול חלופי חלש יותר בלי שהמשתמש יודע על כך.
הבחירה ב־fallback תלויה בחוזה של המוצר. אם הכלי רק מציג מידע רגעי, שימוש ב־PID יכול להספיק עם תיעוד של מרוץ אפשרי. אם הוא חייב לשלוח אות לאותו תהליך שנבדק קודם, החלפת pidfd במספר PID משנה את הבטחת הזהות. במקרה כזה ייתכן שהתגובה הנכונה היא להחזיר שגיאה ברורה ולא להעמיד פנים שהיכולת נשמרה.
כדי לחבר בין מה שהתוכנית רואה לבין מה שהמערכת עושה, נריץ אותה דרך strace. הפקודה הבאה מתמקדת בקריאה שניסינו ובסגירת המתאר שנוצר.
ההרצה דרך Cargo כוללת גם תהליכים וקבצים של כלי הבנייה. עבור תחקיר נקי יותר בנו תחילה והריצו את הבינארי עצמו תחת strace. זאת הבחנה חשובה: אנחנו לא רוצים לייחס פתיחת קובץ של Cargo לתוכנית שאותה אנחנו חוקרים.
המערכת /proc מספקת תמונת מצב שימושית, אבל אינה snapshot אטומי של כל התהליך. בין קריאת status לקריאת maps מצב התהליך יכול להשתנות. גם רשימת fd משתנה בזמן שהכלי פותח ספרייה כדי לקרוא אותה. נשתמש בה כדי לבנות השערה ולחבר נתונים, ולא כתחליף לחוזה סנכרון.
בדקו עבור תהליך הניסוי את /proc/<pid>/status, את /proc/<pid>/maps ואת הקישורים בתוך /proc/<pid>/fd. רשמו איזה מידע מתאר זהות, איזה מידע מתאר משאב ואיזה מידע הוא מונה שמתעדכן. אם הרשאות או namespaces מסתירים חלק מהנתונים, תעדו את ההגבלה במקום לפרש קובץ חסר כתהליך שאינו קיים.
חשוב להפריד גם בין ABI למימוש פנימי. מספרי syscall והמבנים הציבוריים הם חלק מחוזה משתמש שמערכת לינוקס משתדלת לשמר. שם פונקציה פנימית בקרנל, שדה במבנה פנימי או מסלול ב־scheduler יכולים להשתנות. כאשר נחקור מקור קרנל בפרקים המאוחרים, נציין commit וקונפיגורציה ולא נציג שם פנימי כאילו הוא הבטחה לכל גרסה.
הכינו לכל ניסוי דוח קצר עם kernel release, ארכיטקטורה, גרסת libc, target של ראסט וסביבת בידוד. לאחר מכן שנו משתנה אחד בלבד. כך אפשר להבחין בין בעיית ABI, חסימת הרשאות וחוסר ספרייה בלי לערבב כמה הבדלים באותה הרצה.
החוזה של הפעולה מופיע בעמוד pidfd_open, ומבנה המידע של התהליך מתועד בעמוד proc. הקריאה במסמכים צריכה לענות על שאלה מוגדרת שעלתה מהניסוי, ולא להחליף את מדידת הסביבה בפועל.