Баг в функции BackupSeek

BackupSeek неправильно пропускает разреженный блок




Во время разработки следующей версии программы NTFS Stream Explorer я столкнулся с багом в WinAPI-функции BackupSeek. Для чтения потоков NTFS, содержащихся в файле можно использовать две стандартные функции BackupRead и BackupSeek. Эти функции перечисляют все его содержащиеся в нём системные потоки. Информация о каждом потоке содержится в структуре WIN32_STEAM_ID. Я написал вспомогательную утилиту NTFS BackupRead Dumper, которая умеет делать дамп всех потоков файла при помощи BackupRead и восстанавливать файл из дампа.

typedef struct _WIN32_STREAM_ID {
        DWORD          dwStreamId ;
        DWORD          dwStreamAttributes ;
        LARGE_INTEGER  Size ;
        DWORD          dwStreamNameSize ;
        WCHAR          cStreamName[ ANYSIZE_ARRAY ] ;
} WIN32_STREAM_ID, *LPWIN32_STREAM_ID ;

Три самых главных поля в структуре WIN32_STREAM_ID это dwStreamId, Size и dwStreamNameSize. Анализ файла происходит следующим образом: с помощью BackupRead читается структура WIN32_STREAM_ID, по полю dwStreamId выясняется, что за поток перед нами, далее его можно либо прочитать с помощью той же BackupRead, либо пропустить его с помощью BackupSeek и перейти к следующему. Если мы решили читать поток, то в первую очередь надо проверить, именованый он или неименованый. Среднестатистический файл содержит несколько системных потоков, причём все из них безымянные, соответственно dwStreamNameSize равно 0. Некоторые файлы содержат именованные потоки типа BACKUP_ALTERNATE_DATA, это те самые «альтернативные файловые потоки», о которых много информации в сети. Если dwStreamNameSize не равно нулю, считываем имя потока, если Size тоже не равно нулю, значит поток не пустой, и его содержимое можно прочитать.

Для примера был создан пустой файл размером 64 Кб, ему был присвоен разреженный атрибут и задан диапазон разреженности 0-65536. У файла был создан альтернативный поток, названный STREAM с содержимым AAAA. После этого файлу был присвоен один расширенный атрибут, названный ATTR и содержащий BBBB. Расширенный атрибут был присвоен с помошью утилиты EA.EXE, а альтернативный поток был создан прямо из командной строки.

D:\TMP>fsutil file createnew test.dat 65536
Файл D:\TMP\test.dat создан
D:\TMP>fsutil sparse setflag test.dat

D:\TMP>fsutil sparse setrange test.dat 0 65536

D:\TMP>echo AAAA > test.dat:stream

D:\TMP>ea set test.dat ATTR BBBB

На картинке показано, как BackupRead видит файл, содержащий все эти атрибуты. Цветами выделены заголовки атрибутов, соответствующие структуре WIN32_STREAM_ID.

Структура потоков NTFS
Двоичное представление потоков NTFS

Прежде чем решить, с каким потоком взаимодействовать, нужно получить полный список потоков файла, чтобы знать, какие из них есть в наличии, а какие отсутствуют. Перечисление всех потоков упрощённо выглядит так:

#define BackupClose(x,y) BackupRead(x, NULL, 0, NULL, TRUE, FALSE, y)
  
WIN32_STREAM_ID sid;
LPVOID lpContext = NULL;

while (TRUE)
{
  BackupRead(hFile, (LPBYTE)&sid, dwStreamHeaderSize,
                  &dwRead, FALSE, TRUE, &lpContext);

  // Условие выхода из цикла
  if (0 == dwRead)
  {
    break;
  }

  // Тут можно вывести какую-либо информацию о потоке, взятую
  // из WIN32_STREAM_ID

  // Если поток именованный
  if (sid.dwStreamNameSize > 0)
  {
    // с помощью BackupRead можно прочитать имя потока
  }

  // Если поток имеет не нулевой размер
  if (sid.Size.QuadPart > 0)
  {
    // с помощью BackupRead можно прочитать содержимое потока
    
    // или, если это не требуется, переместиться к следующему потоку
    BackupSeek(hFile, sid.Size.LowPart, sid.Size.HighPart,
                &dw1, &dw2, &lpContext);
  }

  //Очищаем структуру перед следующим проходом цикла
  ZeroMemory(&sid, sizeof(WIN32_STREAM_ID));
}
  BackupClose(hFile, &lpContext);

Как выяснилось, функция BackupSeek в таком алгоритме прекрасно работает со всеми видами потоков, за исключением потока типа BACKUP_SPARSE_BLOCK. Такие потоки существуют у разреженных файлов NTFS. Баг в функции BackupSeek приводит к тому, что указатель для чтения перемещается в некорректную позицию. Из-за этого следующий в цикле вызов BackupRead прочитает не структуру WIN32_STREAM_ID, а часть содержимого потока разреженного файла. Соотвественно, алгоритм не может распознать в структуре тип потока и не может далее производить их перечисление. Все потоки, стоящие в файле после BACKUP_SPARSE_BLOCK становятся невидимыми для программы, которая занимается перечислением потоков. Саму операционную систему Windows это не затрагивает, так как она, разумеется, обрабатывает внутренности файла далеко не с помощью функций резервного копирования.

Вывод: функция BackupSeek не может корректно пропустить поток, если он имеет тип BACKUP_SPARSE_BLOCK.

Решение проблемы

Чтобы осуществить корректное перечисление потоков, приходится добавить в алгоритм дополнительное условие.

if (BACKUP_SPARSE_BLOCK == sid.dwStreamId)
{
  // Баг в BackupSeek не позволяет пропустить разреженный блок.
  // Читаем его, не обрабатывая.

  sparse_buf = (LPBYTE) GlobalAlloc(GPTR, BUF_SIZE);
  BackupRead(hFile, sparse_buf, sid.Size.LowPart, &dwRead,
                FALSE, TRUE, &lpContext);
  GlobalFree(sparse_buf);
} else 
{
  //Перемещаемся к следующему потоку
  BackupSeek(hFile, sid.Size.LowPart, sid.Size.HighPart,
              &dw1, &dw2, &lpContext);
}

В отличие от BackupSeek, функция BackupRead читает именно столько байт, сколько указано в sid.Size, и после операции чтения указатель в файле оказывается на правильной позиции. Это позволяет перечислять потоки дальше.

В итоге в бета-версии NTFS Stream Explorer 1.03 сделано корректное отображение потоков, даже если открыт разреженный файл.

Потоки в NTFS Stream Explorer
Список потоков в NTFS Stream Explorer

Кратко

Ошибка:
Неправильная работа функции BackupSeek при обработке разреженных файлов

Применение:
Провоцирование ошибок в программах, использующих функцию для перечисления потоков NTFS, а также в программах, осуществляющих резервное копирование данных.

Операционная система:
ОС Windows семейства NT

Компонент:
Библиотека kernel32.dll, функция BackupSeek

По теме разреженных файлов также есть следующее:



Автор: амдф
Дата: 12.03.2011


При копировании материалов хорошим тоном будет указание авторства и ссылка на сайт.