חקירת השחתה ודליפות¶
נניח שהשרת נסגר דווקא בזמן שהוא משחרר רשומה. השורה שמופיעה ב־call stack היא delete, אבל הרשומה נכתבה דקות קודם. מנהל הערמה מגלה עכשיו שהמבנה שהוא ציפה למצוא השתנה. השורה שבה התגלתה השגיאה אינה בהכרח השורה שגרמה לה. המטרה שלנו היא לקצר את המרחק בין הכתיבה הלא חוקית לבין העצירה, ואז להוכיח שהתיקון מתייחס לחוזה שהופר.
נבצע את הניסויים על תוכנת מעבדה נפרדת שלנו. הקוד הבא פגום בכוונה, ואינו קוד שמכניסים לספריית הפרויקט. שמרו אותו בשם overrun.cpp:
#include <cstddef>
int main() {
constexpr std::size_t count = 16;
auto* buffer = new char[count]{};
volatile char* observed = buffer;
observed[count] = 42; // Intentional out-of-bounds write.
delete[] buffer;
}
ההקצאה מכילה את האינדקסים מאפס עד חמש עשרה. הכתיבה באינדקס שש עשרה מפרה את גבולות האובייקט גם אם הכתובת נמצאת בדף ממופה וניתן לכתיבה. השימוש ב־volatile נועד להשאיר בניסוי גישה מפורשת לזיכרון; הוא אינו הופך את הגישה לחוקית ואינו מספק מנגנון סנכרון. אחרי ההתנהגות הלא מוגדרת אסור להסיק שהרצה רגילה חייבת לקרוס, להמשיך או להפיק תוצאה מסוימת.
קירוב נקודת הגילוי באמצעות מכשור¶
פתחו את סביבת הפיתוח של MSVC המתאימה ל־x64 ובנו עותק עם AddressSanitizer. הפקודה מניחה שהרכיב הותקן עם כלי הפיתוח:
cl /nologo /std:c++20 /EHsc /Zi /Od /fsanitize=address overrun.cpp /Fe:overrun-asan.exe /link /INCREMENTAL:NO
overrun-asan.exe
המכשור מוסיף בדיקות ומטא־נתונים שמאפשרים לזהות גישות רבות מחוץ להקצאה או אחרי שחרור. בדוח צפויים סוג הכשל, כתובת הגישה, גודל הגישה ומידע על ההקצאה, לפי הגרסה ואיכות הסמלים. קראו קודם את סוג הגישה ואת גבולות האזור, ורק אחר כך את רשימת הפונקציות. בשלב הזה השאלה היא איזה אינדקס חושב, מאיזה אורך הוא נגזר ומי הבטיח שהאורך חוקי.
התוצאה אינה הוכחת תקינות לכל התוכנית. קוד שלא רץ לא נבדק, ורכיב שלא נבנה עם מכשור עשוי להגביל את האבחון. מקצה פרטי שמחלק בלוק גדול להרבה אובייקטים קטנים יכול להסתיר גבולות פנימיים מן הכלי. גם תמיכת הכלי בארכיטקטורה, באפשרויות בנייה ובסביבת ריצה תלויה בגרסת MSVC. תעדו את הגרסה ואת פקודת הבנייה עם הדוח, במקום להסתפק בצילום של הודעת שגיאה.
קירוב נקודת הגילוי באמצעות הגנת דפים¶
ניסוי שני משתמש ב־PageHeap על גרסה רגילה של אותה תוכנה. נבנה קובץ נפרד כדי שלא נערבב שני מנגנוני אבחון באותה הרצה:
cl /nologo /std:c++20 /EHsc /Zi /Od overrun.cpp /Fe:amit-overrun-lab.exe
gflags /p /enable amit-overrun-lab.exe /full
windbg amit-overrun-lab.exe
הכלי GFlags מגיע עם כלי האבחון של Windows. ייתכן ששינוי ההגדרה דורש הרשאה מתאימה; אין צורך לשנות הגדרות עבור תוכנות אחרות או להפעיל בדיקה כלל מערכתית. ההגדרה נקבעת לפי שם קובץ התמונה ונשמרת גם אחרי שהתהליך נסגר. לכן בסיום הניסוי מבטלים אותה במפורש, ורק תהליך חדש יקבל את ההגדרה המעודכנת:
הבדיקה המלאה יכולה להציב דף בלתי נגיש סמוך להקצאה, כך שחלק מן החריגות נעצרות בעת הגישה. מיקום הבלוק, היישור וסוג החריגה משפיעים על מה שנתפס ומתי. מנגנון כזה משתמש בהרבה יותר זיכרון ומשנה את תזמון התוכנית ואת פריסת ההקצאות. מנגנון הבדיקה הרגיל יכול להשתמש בדפוסי סימון ולגלות הפרה רק בעת בדיקה מאוחרת יותר. ההבדל בין שני המצבים מסביר מדוע אותו פגם מפיק עצירות שונות.
חקירה שאינה נעצרת בשם הפונקציה¶
כאשר התהליך נעצר, שמרו את ה־call stack, כתובת הבלוק וגודל ההקצאה המקורי. מצאו את המקום שבו חושב האורך. בדוגמה שלנו התנאי הנכון ללולאה הוא index < count, אבל בפרוטוקול אמיתי אורך עלול להגיע מקובץ, מחיבור או מחישוב הכולל כפל. תיקון של אינדקס יחיד אינו מספיק אם ההקצאה עצמה קטנה בגלל overflow.
נבחן גם שימוש אחרי שחרור. שתי כתובות בעלות אותו ערך מספרי אינן בהכרח אותו אובייקט לאורך זמן. המקצה יכול למסור את הכתובת לבקשה חדשה. שמירת pointer ישן ואז השוואת ערכו לכתובת קיימת אינה משחזרת בעלות. התיקון יכול להיות העברת בעלות, שינוי משך החיים של הבקשה או יצירת עותק, ולא רק איפוס משתנה מקומי אחד.
הכלי Application Verifier מוסיף בדיקות נוספות לחוזים של תוכנות Windows, למשל שימוש ב־handleים ובערמות. בחרו בדיקה בהתאם להשערה, ולא אוסף אפשרויות ללא הסבר. בפרק האבחון נלמד לחבר dumps, סמלים ואירועים לציר זמן אחד; כבר כאן כדאי לשמור reproducer קטן שמכיל רק את תנאי הכשל.
דליפה אינה כל עלייה בזיכרון¶
נניח שמדד הזיכרון עלה בעשרת המחזורים הראשונים ואז התייצב. ייתכן שהמערכת בונה cache מוגבל, שהמקצה שומר בלוקים לשימוש חוזר או שקוד נטען לראשונה. לעומת זאת, עלייה קבועה במספר אובייקטים שצריכים להסתיים בכל מחזור היא חשד חזק לדליפה. ההבחנה נעשית בעזרת חוזה חיים: אילו אובייקטים אמורים להישאר אחרי שהעבודה הסתיימה.
בדיקת דליפה טובה מבצעת חימום, מודדת נקודת התחלה, מריצה הרבה מחזורים וממתינה לסיום כל הפעולות. לאחר מכן היא משווה מספר הקצאות חיות ואתריהן, לצד מדדי התהליך. ירידה ב־working set בלבד אינה הוכחת שחרור, ועלייה ב־private bytes בלבד אינה מספר האובייקטים הדולפים. הפרידו בין הראיה, ההשערה והניסוי הבא.
קריאה להעמקה¶
ההסבר והניסוי נעזרים בתיעוד שגיאת חריגה מהערמה ובתיעוד ההפעלה וההתמדה של PageHeap. התאימו את אפשרויות הבנייה לתיעוד גרסת הכלים שהתקנתם.