שמות פתיחה וזהות קובץ¶
נניח שתוכנית בדקה שהנתיב שייך לתיקיית העבודה שלה, ואז פתחה אותו. בין הבדיקה לפתיחה מישהו יכול לשנות קישור או שם. גם בלי גורם עוין, שני שמות יכולים להפנות לאותו קובץ ושם אחד יכול להפנות לקובץ אחר בזמן מאוחר יותר. לכן נתיב הוא הוראה למציאת אובייקט, ולא הזהות היציבה של האובייקט שכבר נפתח.
נבחין בין שלוש שכבות: המחרוזת שהמשתמש מסר, השם שהמערכת פירשה בזמן פתיחה והקובץ שאליו ה־handle מתייחס. ההבחנה תלווה אותנו בבדיקות הרשאה, בתיקון מידע ובניסיונות חוזרים אחרי כשל.
בדיקת האובייקט שנפתח¶
התוכנית הבאה פותחת קובץ או תיקייה לקריאת מאפיינים ומדפיסה מזהה ונתיב מתוך ה־handle. היא אינה משנה את היעד:
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <iomanip>
#include <iostream>
#include <vector>
int wmain(int argc, wchar_t** argv) {
if (argc != 2) return 2;
HANDLE file = CreateFileW(argv[1], FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
if (file == INVALID_HANDLE_VALUE) {
std::cerr << "open: " << GetLastError() << '\n';
return 1;
}
FILE_ID_INFO identity{};
if (!GetFileInformationByHandleEx(
file, FileIdInfo, &identity, sizeof(identity))) {
std::cerr << "identity: " << GetLastError() << '\n';
CloseHandle(file);
return 1;
}
std::cout << "volume=" << identity.VolumeSerialNumber << " id=";
for (unsigned char byte : identity.FileId.Identifier)
std::cout << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<unsigned>(byte);
std::cout << std::dec << '\n';
std::vector<wchar_t> path(512);
for (;;) {
const DWORD count = GetFinalPathNameByHandleW(
file, path.data(), static_cast<DWORD>(path.size()),
FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);
if (count == 0) {
std::cerr << "path: " << GetLastError() << '\n';
CloseHandle(file);
return 1;
}
if (count < path.size()) {
std::wcout << path.data() << L'\n';
break;
}
if (count > 1024 * 1024) {
CloseHandle(file);
return 1;
}
path.resize(static_cast<std::size_t>(count) + 1);
}
CloseHandle(file);
}
הדוגמה מתמקדת בחוזי הזיהוי. במוצר נעטוף את ה־handle ב־RAII כדי שגם חריגת הקצאה של vector תסגור אותו. התמיכה ב־FileIdInfo ובפורמט השם תלויה בסוג מערכת הקבצים ובספק, ולכן כישלון נשמר כמידע ולא מוחלף במזהה מומצא.
מזהה קובץ יחד עם מזהה volume מאפשר להשוות אובייקטים בהקשר המתאים. אין להניח שהוא מזהה עולמי נצחי שאי אפשר למחזר אחרי מחיקה או שינוי מערכת הקבצים. עבור פעולה מתמשכת, ה־handle הפתוח הוא העוגן לבעלות על האובייקט.
מרחבי שמות אינם רק קידומות¶
הנתיב C:\\work\\item.bin שייך לצורת השמות המוכרת של Win32. נתיב UNC מתאר שרת ושיתוף. הקידומת \\\\?\\ משנה את אופן עיבוד השם על ידי שכבת Win32, ונתיבי התקנים כגון \\\\.\\ מיועדים לממשקים שתומכים בהם. מתחת לשכבה הזו קיימים שמות NT ומיפויים של שמות DOS.
הקידומות אינן שיטה כללית לעקיפת כל מגבלת אורך או כללי מערכת הקבצים. תמיכת API, manifest ומדיניות מערכת משפיעים על התנהגות נתיבים ארוכים. גם רגישות לאותיות גדולות, קישורים ושמות חלופיים אינם זהים בכל תיקייה ובכל filesystem. הקוד צריך להשתמש בחוזה של היעד, לא בשרשרת החלפות טקסט שנחשבת בעינינו "ניקוי נתיב".
שימו לב להבדל בין C:notes.txt לבין C:\\notes.txt. הראשון יכול להיות יחסי לתיקייה הנוכחית בכונן, והשני מציין נתיב מן השורש. שימוש בנתיב יחסי בתוך שירות או callback עלול להישען על מצב תהליך שלא התכוונו אליו.
פתיחה היא בקשה עם כמה החלטות¶
השדה desired access מתאר מה הקורא מבקש לעשות. שדות share mode מתארים לאילו גישות נוספות הוא מוכן להסכים בזמן שה־handle חי. שדה creation disposition קובע האם ליצור, לפתוח בלבד, להיכשל אם קיים או להחליף תוכן. אלה החלטות שונות, ואין להסיק מהרשאת קריאה שהפתיחה משתפת קוראים אחרים באופן אוטומטי.
בדיקת השיתוף נעשית ביחס לפתיחות קיימות וחדשות. קובץ יכול להיות מותר לקריאה לפי ה־ACL ועדיין להחזיר sharing violation בגלל חוזה של handle אחר. הגדלת הרשאות התהליך אינה התיקון הרגיל למצב כזה.
מחיקה ושינוי שם תלויים גם בהרשאות השיתוף ובסמנטיקה של מערכת הקבצים. במצבים מסוימים שם נעלם או מסומן למחיקה בעוד handle קיים עדיין מתייחס לאובייקט. לכן קריאת "האם הנתיב קיים" אינה בדיקה מספקת לחיי הקובץ שכבר פתחנו.
בדיקה ושימוש באותה זהות¶
נרמול מחרוזת יכול להסיר כפילויות תחביריות, אבל אינו פותר מרוץ שבו רכיב נתיב מוחלף בקישור. גם FILE_FLAG_OPEN_REPARSE_POINT אינו כלל קסם שמונע כל מעבר דרך רכיב ביניים. צריך להגדיר האם הקוד בודק את הקישור עצמו או את היעד, ואיזו מדיניות נאכפת על האובייקט שנפתח.
עבור כלי מעבדה אפשר להגביל את הקלט לתיקייה שבבעלות המשתמש ולהימנע מפעולות הרסניות. עבור שירות בעל סמכויות, יש לתכנן בדיקות מבוססות handle וחוזה הרשאות ברור, ולבחון את מרוצי הפתיחה. בפרק האבטחה נרחיב את ההבדל בין שמות, tokens והרשאות.
קריאה להעמקה¶
התיעוד של שמות ומרחבי שמות ושל פתיחת קובץ מגדיר את גבולות הפירוש. אין להניח שהדפסת נתיב מנורמל היא אישור אבטחה.