Projected File System

A little-known feature in modern Windows is the ability to expose hierarchical data using the file system. This is called Windows Projected File System (ProjFS), available since Windows 10 version 1809. There is even a sample that exposes the Registry hierarchy using this technology. Using the file system as a “projection” mechanism provides a couple of advantages over a custom mechanism:
现代 Windows 中一个鲜为人知的功能是能够使用文件系统公开分层数据。这称为 Windows 投影文件系统 (ProjFS),自 Windows 10 版本 1809 起可用。甚至还有一个示例使用此技术公开注册表层次结构。与自定义机制相比,使用文件系统作为“投影”机制具有以下几个优势:

  • Any file viewing tool can present the information such as Explorer, or commands in a terminal.
    任何文件查看工具都可以在终端中显示资源管理器或命令等信息。
  • “Standard” file APIs are used, which are well-known, and available in any programming language or library.
    使用“标准”文件 API,这些 API 是众所周知的,并且可用于任何编程语言或库。

Let’s see how to build a Projected File System provider from scratch. We’ll expose object manager directories as file system directories, and other types of objects as “files”. Normally, we can see the object manager’s namespace with dedicated tools, such as WinObj from Sysinternals, or my own Object Explorer:
让我们看看如何从头开始构建投影文件系统提供程序。我们将对象管理器目录公开为文件系统目录,将其他类型的对象公开为“文件”。通常,我们可以使用专用工具查看对象管理器的命名空间,例如 Sysinternals 中的 WinObj 或我自己的对象资源管理器:

Projected File System

WinObj showing parts of the object manager namespace
显示对象管理器命名空间的各个部分的 WinObj

Here is an example of what we are aiming for (viewed with Explorer):
以下是我们的目标示例(使用 Explorer 查看):

Projected File System

Explorer showing the root of the object manager namespace
显示对象管理器命名空间根目录的资源管理器

First, support for ProjFS must be enabled to be usable. You can enable it with the Windows Features dialog or PowerShell:
首先,必须启用对 ProjFS 的支持才能使用。可以使用“Windows 功能”对话框或 PowerShell 启用它:

Enable-WindowsOptionalFeature -Online -FeatureName Client-ProjFS -NoRestart

We’ll start by creating a C++ console application named ObjMgrProjFS; I’ve used the Windows Desktop Wizard project with a precompiled header (pch.h):
我们将首先创建一个名为 ObjMgrProjFS 的 C++ 控制台应用程序;我使用了带有预编译标头 (pch.h) 的 Windows 桌面向导项目:

#pragma once
#include <Windows.h>
#include <projectedfslib.h>
#include <string>
#include <vector>
#include <memory>
#include <map>
#include <ranges>
#include <algorithm>
#include <format>
#include <optional>
#include <functional>

projectedfslib.h is where the ProjFS declarations reside. projectedfslib.lib is the import library to link against. In this post, I’ll focus on the main coding aspects, rather than going through every little piece of code. The full code can be found at https://github.com/zodiacon/objmgrprojfs. It’s of course possible to use other languages to implement a ProjFS provider. I’m going to attempt one in Rust in a future post Projected File System
projectedfslib.h 是 ProjFS 声明所在的位置。projectedfslib.lib 是要链接的导入库。在这篇文章中,我将重点介绍主要的编码方面,而不是逐一介绍每一小段代码。完整的代码可以在 https://github.com/zodiacon/objmgrprojfs 找到。当然,可以使用其他语言来实现 ProjFS 提供程序。我将在以后的帖子 Projected File System 中尝试在 Rust 中尝试一个

The projected file system must be rooted in a folder in the file system. It doesn’t have to be empty, but it makes sense to use such a directory for this purpose only. The main function will take the requested root folder as input and pass it to the ObjectManagerProjection class that is used to manage everything:
投影的文件系统必须根植于文件系统中的文件夹中。它不必为空,但仅将这样的目录用于此目的是有意义的。该 main 函数将请求的根文件夹作为输入,并将其传递给用于管理所有内容的 ObjectManagerProjection 类:

int wmain(int argc, const wchar_t* argv[]) {
    if (argc < 2) {
        printf("Usage: ObjMgrProjFS <root_dir>\n");
        return 0;
    }
    ObjectManagerProjection omp;
    if (auto hr = omp.Init(argv[1]); hr != S_OK)
        return Error(hr);
    if (auto hr = omp.Start(); hr != S_OK)
        return Error(hr);
    printf("Virtualizing at %ws. Press ENTER to stop virtualizing...\n", argv[1]);
    char buffer[3];
    gets_s(buffer);
    omp.Term();
    return 0;
}

Let start with the initialization. We want to create the requested directory (if it doesn’t already exist). If it does exist, we’ll use it. In fact, it could exist because of a previous run of the provider, so we can keep track of the instance ID (a GUID) so that the file system itself can use its caching capabilities. We’ll “hide” the GUID in a hidden file within the directory. First, create the directory:
让我们从初始化开始。我们想要创建请求的目录(如果尚不存在)。如果它确实存在,我们将使用它。事实上,它可能由于提供程序的先前运行而存在,因此我们可以跟踪实例 ID(GUID),以便文件系统本身可以使用其缓存功能。我们将 GUID “隐藏”在目录中的隐藏文件中。首先,创建目录:

HRESULT ObjectManagerProjection::Init(PCWSTR root) {
    GUID instanceId = GUID_NULL;
    std::wstring instanceFile(root);
    instanceFile += L"\\_obgmgrproj.guid";
    if (!::CreateDirectory(root, nullptr)) {
        //
        // failed, does it exist?
        //
        if (::GetLastError() != ERROR_ALREADY_EXISTS)
            return HRESULT_FROM_WIN32(::GetLastError());

If creation fails not because it exists, bail out with an error. Otherwise, get the instance ID that may be there and use that GUID if present:
如果创建失败不是因为它存在,则使用错误进行纾困。否则,请获取可能存在的实例 ID,并使用该 GUID(如果存在):

    auto hFile = ::CreateFile(instanceFile.c_str(), GENERIC_READ,
        FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
    if (hFile != INVALID_HANDLE_VALUE && ::GetFileSize(hFile, nullptr) == sizeof(GUID)) {
        DWORD ret;
        ::ReadFile(hFile, &instanceId, sizeof(instanceId), &ret, nullptr);
        ::CloseHandle(hFile);
    }
}

If we need to generate a new GUID, we’ll do that with CoCreateGuid and write it to the hidden file:
如果我们需要生成新的 GUID,我们将使用 CoCreateGuid 并将其写入隐藏文件:

if (instanceId == GUID_NULL) {
    ::CoCreateGuid(&instanceId);
    //
    // write instance ID
    //
    auto hFile = ::CreateFile(instanceFile.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_HIDDEN, nullptr);
    if (hFile != INVALID_HANDLE_VALUE) {
        DWORD ret;
        ::WriteFile(hFile, &instanceId, sizeof(instanceId), &ret, nullptr);
        ::CloseHandle(hFile);
    }
}

Finally, we must register the root with ProjFS:
最后,我们必须向 ProjFS 注册根目录:

auto hr = ::PrjMarkDirectoryAsPlaceholder(root, nullptr, nullptr, &instanceId);
if (FAILED(hr))
    return hr;
m_RootDir = root;
return hr;

Once Init succeeds, we need to start the actual virtualization. To that end, a structure of callbacks must be filled so that ProjFS knows what functions to call to get the information requested by the file system. This is the job of the Start method:
一旦 Init 成功,我们需要开始实际的虚拟化。为此,必须填充回调结构,以便 ProjFS 知道要调用哪些函数来获取文件系统请求的信息。这是 Start 该方法的工作:

HRESULT ObjectManagerProjection::Start() {
    PRJ_CALLBACKS cb{};
    cb.StartDirectoryEnumerationCallback = StartDirectoryEnumerationCallback;
    cb.EndDirectoryEnumerationCallback = EndDirectoryEnumerationCallback;
    cb.GetDirectoryEnumerationCallback = GetDirectoryEnumerationCallback;
    cb.GetPlaceholderInfoCallback = GetPlaceholderInformationCallback;
    cb.GetFileDataCallback = GetFileDataCallback;
    auto hr = ::PrjStartVirtualizing(m_RootDir.c_str(), &cb, this, nullptr, &m_VirtContext);
    return hr;
}

The callbacks specified above are the absolute minimum required for a valid provider. PrjStartVirtualizing returns a virtualization context that identifies our provider, which we need to use (at least) when stopping virtualization. It’s a blocking call, which is convenient in a console app, but for other cases, it’s best put in a separate thread. The this value passed in is a user-defined context. We’ll use that to delegate these static callback functions to member functions. Here is the code for StartDirectoryEnumerationCallback:
上面指定的回调是有效提供程序所需的绝对最小值。 PrjStartVirtualizing 返回一个虚拟化上下文,该上下文标识我们的提供程序,我们在停止虚拟化时(至少)需要使用它。这是一个阻止调用,这在控制台应用中很方便,但对于其他情况,最好放在单独的线程中。传入的 this 值是用户定义的上下文。我们将使用它来将这些静态回调函数委托给成员函数。以下是以下代码 StartDirectoryEnumerationCallback :

HRESULT ObjectManagerProjection::StartDirectoryEnumerationCallback(const PRJ_CALLBACK_DATA* callbackData, const GUID* enumerationId) {
    return ((ObjectManagerProjection*)callbackData->InstanceContext)->DoStartDirectoryEnumerationCallback(callbackData, enumerationId);
}

The same trick is used for the other callbacks, so that we can implement the functionality within our class. The class ObjectManagerProjection itself holds on to the following data members of interest:
同样的技巧也用于其他回调,这样我们就可以在类中实现该功能。该类 ObjectManagerProjection 本身保留以下感兴趣的数据成员:

struct GUIDComparer {
    bool operator()(const GUID& lhs, const GUID& rhs) const {
        return memcmp(&lhs, &rhs, sizeof(rhs)) < 0;
    }
};
struct EnumInfo {
    std::vector<ObjectNameAndType> Objects;
    int Index{ -1 };
};
std::wstring m_RootDir;
PRJ_NAMESPACE_VIRTUALIZATION_CONTEXT m_VirtContext;
std::map<GUID, EnumInfo, GUIDComparer> m_Enumerations;

EnumInfo is a structure used to keep an object directory’s contents and the current index requested by the file system. A map is used to keep track of all current enumerations. Remember, it’s the file system – multiple directory listings may be happening at the same time. As it happens, each one is identified by a GUID, which is why it’s used as a key to the map. m_VirtContext is the returned value from PrjStartVirtualizing.
EnumInfo 是用于保存对象目录的内容和文件系统请求的当前索引的结构。映射用于跟踪所有当前枚举。请记住,这是文件系统 – 多个目录列表可能同时发生。碰巧的是,每个都由 GUID 标识,这就是它被用作映射键的原因。 m_VirtContext 是 的 PrjStartVirtualizing 返回值。

ObjectNameAndType is a little structure that stores the details of an object: its name and type:
ObjectNameAndType 是一个存储对象详细信息的小结构:它的名称和类型:

struct ObjectNameAndType {
    std::wstring Name;
    std::wstring TypeName;
};

The Callbacks 回调

Obviously, the bulk work for the provider is centered in the callbacks. Let’s start with StartDirectoryEnumerationCallback. Its purpose is to let the provider know that a new directory enumeration of some sort is beginning. The provider can make any necessary preparations. In our case, it’s about adding a new enumeration structure to manage based on the provided enumeration GUID:
显然,提供程序的批量工作集中在回调中。让我们从 StartDirectoryEnumerationCallback 开始。其目的是让提供程序知道某种新的目录枚举正在开始。提供者可以做任何必要的准备。在本例中,它是关于添加新的枚举结构以根据提供的枚举 GUID 进行管理:

HRESULT ObjectManagerProjection::DoStartDirectoryEnumerationCallback(const PRJ_CALLBACK_DATA* callbackData, const GUID* enumerationId) {
    EnumInfo info;
    m_Enumerations.insert({ *enumerationId, std::move(info) });
    return S_OK;
}

We just add a new entry to our map, since we must be able to distinguish between multiple enumerations that may be happening concurrently. The complementary callback ends an enumeration which is where we delete the item from the map:
我们只需向地图添加一个新条目,因为我们必须能够区分可能同时发生的多个枚举。互补回调结束了一个枚举,这是我们从映射中删除项目的地方:

HRESULT ObjectManagerProjection::DoEndDirectoryEnumerationCallback(const PRJ_CALLBACK_DATA* callbackData, const GUID* enumerationId) {
    m_Enumerations.erase(*enumerationId);
    return S_OK;
}

So far, so good. The real work is centered around the GetDirectoryEnumerationCallback callback where actual enumeration must take place. The callback receives the enumeration ID and a search expression – the client may try to search using functions such as FindFirstFile / FindNextFile or similar APIs. The provided PRJ_CALLBACK_DATA contains the basic details of the request such as the relative directory itself (which could be a subdirectory). First, we reject any unknown enumeration IDs:
目前为止,一切都好。实际工作以回调为中心, GetDirectoryEnumerationCallback 必须在回调中进行实际枚举。回调接收枚举 ID 和搜索表达式 – 客户端可以尝试使用 FindFirstFile / FindNextFile 或类似 API 等函数进行搜索。提供的 PRJ_CALLBACK_DATA 包含请求的基本详细信息,例如相对目录本身(可以是子目录)。首先,我们拒绝任何未知的枚举 ID:

HRESULT ObjectManagerProjection::DoGetDirectoryEnumerationCallback(
    const PRJ_CALLBACK_DATA* callbackData, const GUID* enumerationId,
    PCWSTR searchExpression, PRJ_DIR_ENTRY_BUFFER_HANDLE dirEntryBufferHandle) {
    auto it = m_Enumerations.find(*enumerationId);
    if(it == m_Enumerations.end())
        return E_INVALIDARG;
    auto& info = it->second;

Next, we need to enumerate the objects in the provided directory, taking into consideration the search expression (that may require returning a subset of the items):
接下来,我们需要枚举所提供目录中的对象,同时考虑搜索表达式(可能需要返回项目的子集):

if (info.Index < 0 || (callbackData->Flags & PRJ_CB_DATA_FLAG_ENUM_RESTART_SCAN)) {
    auto compare = [&](auto name) {
        return ::PrjFileNameMatch(name, searchExpression);
        };
    info.Objects = ObjectManager::EnumDirectoryObjects(callbackData->FilePathName, nullptr, compare);
    std::ranges::sort(info.Objects, [](auto const& item1, auto const& item2) {
        return ::PrjFileNameCompare(item1.Name.c_str(), item2.Name.c_str()) < 0;
        });
    info.Index = 0;
}

There are quite a few things happening here. ObjectManager::EnumDirectoryObjects is a helper function that does the actual enumeration of objects in the object manager’s namespace given the root directory (callbackData->FilePathName), which is always relative to the virtualization root, which is convenient – we don’t need to care where the actual root is. The compare lambda is passed to EnumDirectoryObjects to provide a filter based on the search expression. ProjFS provides the PrjFileNameMatch function we can use to test if a specific name should be returned or not. It has the logic that caters for wildcards like * and ?.
这里发生了很多事情。 ObjectManager::EnumDirectoryObjects 是一个辅助函数,它对给定根目录 ( callbackData->FilePathName ) 的对象管理器命名空间中的对象进行实际枚举,该目录始终相对于虚拟化根目录,这很方便 – 我们不需要关心实际根目录在哪里。 compare 传递 lambda EnumDirectoryObjects 以提供基于搜索表达式的筛选器。ProjFS 提供了可用于测试是否应返回特定名称的 PrjFileNameMatch 函数。它具有迎合通配符(如 * 和 ?)的逻辑。

Once the results return in a vector (info.Objects), we must sort it. The file system expects returned files/directories to be sorted in a case insensitive way, but we don’t actually need to know that. PrjFileNameCompare is provided as a function to use for sorting purposes. We call sort on the returned vector passing this function PrjFileNameCompare as the compare function.
一旦结果以向量 ( info.Objects ) 返回,我们必须对其进行排序。文件系统期望返回的文件/目录以不区分大小写的方式进行排序,但我们实际上并不需要知道这一点。 PrjFileNameCompare 作为用于排序目的的功能提供。我们调用 sort 返回的向量,将此函数 PrjFileNameCompare 作为 compare 函数传递。

The enumeration must happen if the PRJ_CB_DATA_FLAG_ENUM_RESTART_SCAN is specified. I also enumerate if it’s the first call for this enumeration ID.
如果指定了 , PRJ_CB_DATA_FLAG_ENUM_RESTART_SCAN 则必须进行枚举。我还枚举了它是否是对此枚举 ID 的第一次调用。

Now that we have results (or an empty vector), we can proceed by telling ProjFS about the results. If we have no results, just return success (an empty directory):
现在我们有了结果(或一个空向量),我们可以通过告诉 ProjFS 结果来继续。如果我们没有结果,只需返回 success(一个空目录):

if (info.Objects.empty())
    return S_OK;

Otherwise, we must call PrjFillDirEntryBuffer for each entry in the results. However, ProjFS provides a limited buffer to accept data, which means we need to keep track of where we left off because we may be called again (without the PRJ_CB_DATA_FLAG_ENUM_RESTART_SCAN flag) to continue filling in data. This is why we keep track of the index we need to use.
否则,我们必须调用 PrjFillDirEntryBuffer 结果中的每个条目。但是,ProjFS 提供了一个有限的缓冲区来接受数据,这意味着我们需要跟踪我们离开的地方,因为我们可能会再次被调用(没有 PRJ_CB_DATA_FLAG_ENUM_RESTART_SCAN 标志)以继续填充数据。这就是我们跟踪需要使用的索引的原因。

The first step in the loop is to fill in details of the item: is it a subdirectory or a “file”? We can also specify the size of its data and common times like creation time, modify time, etc.:
循环的第一步是填写项目的详细信息:它是子目录还是“文件”?我们还可以指定其数据的大小和常用时间,如创建时间、修改时间等:

while (info.Index < info.Objects.size()) {
    PRJ_FILE_BASIC_INFO itemInfo{};
    auto& item = info.Objects[info.Index];
    itemInfo.IsDirectory = item.TypeName == L"Directory";
    itemInfo.FileSize = itemInfo.IsDirectory ? 0 :
        GetObjectSize((callbackData->FilePathName + std::wstring(L"\\") + item.Name).c_str(), item);

We fill in two details: a directory or not, based on the kernel object type being “Directory”, and a file size (in case of another type object). What is the meaning of a “file size”? It can mean whatever we want it to mean, including just specifying a size of zero. However, I decided that the “data” being held in an object would be text that provides the object’s name, type, and target (if it’s a symbolic link). Here are a few example when running the provider and using a command window:
我们填写两个细节:目录与否,基于内核对象类型为“目录”,以及文件大小(如果是另一种类型的对象)。“文件大小”是什么意思?它可以表示我们想要的任何含义,包括仅指定零的大小。但是,我决定在对象中保存的“数据”将是提供对象名称、类型和目标(如果它是符号链接)的文本。下面是运行提供程序和使用命令窗口时的一些示例:

C:\objectmanager>dir p*
 Volume in drive C is OS
 Volume Serial Number is 18CF-552E

 Directory of C:\objectmanager

02/20/2024  11:09 AM                60 PdcPort.ALPC Port
02/20/2024  11:09 AM                76 PendingRenameMutex.Mutant
02/20/2024  11:09 AM                78 PowerMonitorPort.ALPC Port
02/20/2024  11:09 AM                64 PowerPort.ALPC Port
02/20/2024  11:09 AM                88 PrjFltPort.FilterConnectionPort
               5 File(s)            366 bytes
               0 Dir(s)  518,890,110,976 bytes free

C:\objectmanager>type PendingRenameMutex.Mutant
Name: PendingRenameMutex
Type: Mutant

C:\objectmanager>type powerport
Name: PowerPort
Type: ALPC Port

Here is PRJ_FILE_BASIC_INFO: 这里是 PRJ_FILE_BASIC_INFO :

typedef struct PRJ_FILE_BASIC_INFO {
    BOOLEAN IsDirectory;
    INT64 FileSize;
    LARGE_INTEGER CreationTime;
    LARGE_INTEGER LastAccessTime;
    LARGE_INTEGER LastWriteTime;
    LARGE_INTEGER ChangeTime;
    UINT32 FileAttributes;
} PRJ_FILE_BASIC_INFO;

What is the meaning of the various times and file attributes? It can mean whatever you want – it might make sense for some types of data. If left at zero, the current time is used.
各种时间和文件属性的含义是什么?它可以意味着您想要的任何东西 – 它可能对某些类型的数据有意义。如果保留为零,则使用当前时间。

GetObjectSize is a helper function that calculates the number of bytes needed to keep the object’s text, which is what is reported to the file system.
GetObjectSize 是一个帮助程序函数,用于计算保留对象文本所需的字节数,这是报告给文件系统的内容。

Now we can pass the information for the item to ProjFS by calling PrjFillDirEntryBuffer:
现在,我们可以通过调用 PrjFillDirEntryBuffer 以下命令将项目的信息传递给 ProjFS:

    if (FAILED(::PrjFillDirEntryBuffer(
        (itemInfo.IsDirectory ? item.Name : (item.Name + L"." + item.TypeName)).c_str(),
        &itemInfo, dirEntryBufferHandle)))
        break;
    info.Index++;
}

The “name” of the item is comprised of the kernel object’s name, and the “file extension” is the object’s type name. This is just a matter of choice – I could have passed the object’s name only so that it would appear as a file with no extension. If the call to PrjFillDirEntryBuffer fails, it means the buffer is full, so we break out, but the index is not incremented, so we can provide the next object in the next callback that does not requires a rescan.
项的“名称”由内核对象的名称组成,“文件扩展名”是对象的类型名称。这只是一个选择问题——我本可以只传递对象的名称,以便它显示为没有扩展名的文件。如果调用 PrjFillDirEntryBuffer 失败,则表示缓冲区已满,因此我们中断,但索引没有递增,因此我们可以在下一个回调中提供不需要重新扫描的下一个对象。

We have two callbacks remaining. One is GetPlaceholderInformationCallback, whose purpose is to provide “placeholder” information about an item, without providing its data. This is used by the file system for caching purposes. The implementation is like so:
我们还剩下两个回调。一种是 GetPlaceholderInformationCallback ,其目的是提供有关项目的“占位符”信息,而不提供其数据。文件系统将其用于缓存目的。实现如下:

HRESULT ObjectManagerProjection::DoGetPlaceholderInformationCallback(const PRJ_CALLBACK_DATA* callbackData) {
    auto path = callbackData->FilePathName;
    auto dir = ObjectManager::DirectoryExists(path);
    std::optional<ObjectNameAndType> object;
    if (!dir)
        object = ObjectManager::ObjectExists(path);
    if(!dir && !object)
        return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND);
    PRJ_PLACEHOLDER_INFO info{};
    info.FileBasicInfo.IsDirectory = dir;
    info.FileBasicInfo.FileSize = dir ? 0 : GetObjectSize(path, object.value());
    return PrjWritePlaceholderInfo(m_VirtContext, callbackData->FilePathName, &info, sizeof(info));
}

The item could be a file or a directory. We use the file path name provided to figure out if it’s a directory kernel object or something else by utilizing some helpers in the ObjectManager class (we’ll examine those later). Then the structure PRJ_PLACEHOLDER_INFO is filled with the details and provided to PrjWritePlaceholderInfo.
该项可以是文件或目录。我们使用提供的文件路径名来确定它是目录内核对象还是其他东西,方法是利用 ObjectManager 类中的一些帮助程序(我们稍后会检查这些)。然后,在结构 PRJ_PLACEHOLDER_INFO 中填充详细信息并提供给 PrjWritePlaceholderInfo 。

The final required callback is the one that provides the data for files – objects in our case:
最后一个必需的回调是为文件提供数据的回调 – 在我们的例子中是对象:

HRESULT ObjectManagerProjection::DoGetFileDataCallback(const PRJ_CALLBACK_DATA* callbackData, UINT64 byteOffset, UINT32 length) {
    auto object = ObjectManager::ObjectExists(callbackData->FilePathName);
    if (!object)
        return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND);
    auto buffer = ::PrjAllocateAlignedBuffer(m_VirtContext, length);
    if (!buffer)
        return E_OUTOFMEMORY;
    auto data = GetObjectData(callbackData->FilePathName, object.value());
    memcpy(buffer, (PBYTE)data.c_str() + byteOffset, length);
    auto hr = ::PrjWriteFileData(m_VirtContext, &callbackData->DataStreamId, buffer, byteOffset, length);
    ::PrjFreeAlignedBuffer(buffer);
    return hr;
}

First we check if the object’s path is valid. Next, we need to allocate buffer for the data. There are some ProjFS alignment requirements, so we call PrjAllocateAlignedBuffer to allocate a properly-aligned buffer. Then we get the object data (a string, by calling our helper GetObjectData), and copy it into the allocated buffer. Finally, we pass the buffer to PrjWriteFileData and free the buffer. The byte offset provided is usually zero, but could theoretically be larger if the client reads from a non-zero position, so we must be prepared for it. In our case, the data is small, but in general it could be arbitrarily large.
首先,我们检查对象的路径是否有效。接下来,我们需要为数据分配缓冲区。有一些 ProjFS 对齐要求,因此我们调用 PrjAllocateAlignedBuffer 以分配正确对齐的缓冲区。然后我们获取对象数据(一个字符串,通过调用我们的帮助程序 GetObjectData ),并将其复制到分配的缓冲区中。最后,我们将缓冲区传递给 PrjWriteFileData 缓冲区并释放缓冲区。提供的字节偏移量通常为零,但理论上如果客户端从非零位置读取,则可能会更大,因此我们必须为此做好准备。在我们的例子中,数据很小,但一般来说,它可以任意大。

GetObjectData itself looks like this:
GetObjectData 本身看起来像这样:

std::wstring ObjectManagerProjection::GetObjectData(PCWSTR fullname, ObjectNameAndType const& info) {
    std::wstring target;
    if (info.TypeName == L"SymbolicLink") {
        target = ObjectManager::GetSymbolicLinkTarget(fullname);
    }
    auto result = std::format(L"Name: {}\nType: {}\n", info.Name, info.TypeName);
    if (!target.empty())
        result = std::format(L"{}Target: {}\n", result, target);
    return result;
}

It calls a helper function, ObjectManager::GetSymbolicLinkTarget in case of a symbolic link, and builds the final string by using format (C++ 20) before returning it to the caller.
如果是符号链接, ObjectManager::GetSymbolicLinkTarget 它会调用帮助程序函数,并使用 format (C++ 20) 生成最终字符串,然后再将其返回给调用方。

That’s all for the provider, except when terminating:
这就是提供程序的全部内容,但终止时除外:

void ObjectManagerProjection::Term() {
    ::PrjStopVirtualizing(m_VirtContext);
}

The Object Manager 对象管理器

Looking into the ObjectManager helper class is somewhat out of the focus of this post, since it has nothing to do with ProjFS. It uses native APIs to enumerate objects in the object manager’s namespace and get details of a symbolic link’s target. For more information about the native APIs, check out my book “Windows Native API Programming” or search online. First, it includes <Winternl.h> to get some basic native functions like RtlInitUnicodeString, and also adds the APIs for directory objects:
研究 ObjectManager 帮助程序类在某种程度上超出了本文的重点,因为它与 ProjFS 无关。它使用本机 API 来枚举对象管理器命名空间中的对象,并获取符号链接目标的详细信息。有关本机 API 的更多信息,请查看我的书“Windows 本机 API 编程”或在线搜索。首先,它包含 来获取一些基本的本机函数,例如 RtlInitUnicodeString ,并且还添加了目录对象的 API:

typedef struct _OBJECT_DIRECTORY_INFORMATION {
    UNICODE_STRING Name;
    UNICODE_STRING TypeName;
} OBJECT_DIRECTORY_INFORMATION, * POBJECT_DIRECTORY_INFORMATION;
#define DIRECTORY_QUERY  0x0001
extern "C" {
    NTSTATUS NTAPI NtOpenDirectoryObject(
        _Out_ PHANDLE hDirectory,
        _In_ ACCESS_MASK AccessMask,
        _In_ POBJECT_ATTRIBUTES ObjectAttributes);
    NTSTATUS NTAPI NtQuerySymbolicLinkObject(
        _In_ HANDLE LinkHandle,
        _Inout_ PUNICODE_STRING LinkTarget,
        _Out_opt_ PULONG ReturnedLength);
    NTSTATUS NTAPI NtQueryDirectoryObject(
        _In_  HANDLE hDirectory,
        _Out_ POBJECT_DIRECTORY_INFORMATION DirectoryEntryBuffer,
        _In_  ULONG DirectoryEntryBufferSize,
        _In_  BOOLEAN  bOnlyFirstEntry,
        _In_  BOOLEAN bFirstEntry,
        _In_  PULONG  EntryIndex,
        _Out_ PULONG  BytesReturned);
    NTSTATUS NTAPI NtOpenSymbolicLinkObject(
        _Out_  PHANDLE LinkHandle,
        _In_   ACCESS_MASK DesiredAccess,
        _In_   POBJECT_ATTRIBUTES ObjectAttributes);
}

Here is the main code that enumerates directory objects (some details omitted for clarity, see the full source code in the Github repo):
下面是枚举目录对象的主要代码(为清楚起见,省略了一些细节,请参阅 Github 存储库中的完整源代码):

std::vector<ObjectNameAndType> ObjectManager::EnumDirectoryObjects(PCWSTR path,
    PCWSTR objectName, std::function<bool(PCWSTR)> compare) {
    std::vector<ObjectNameAndType> objects;
    HANDLE hDirectory;
    OBJECT_ATTRIBUTES attr;
    UNICODE_STRING name;
    std::wstring spath(path);
    if (spath[0] != L'\\')
        spath = L'\\' + spath;
    std::wstring object(objectName ? objectName : L"");
    RtlInitUnicodeString(&name, spath.c_str());
    InitializeObjectAttributes(&attr, &name, 0, nullptr, nullptr);
    if (!NT_SUCCESS(NtOpenDirectoryObject(&hDirectory, DIRECTORY_QUERY, &attr)))
        return objects;
    objects.reserve(128);
    BYTE buffer[1 << 12];
    auto info = reinterpret_cast<OBJECT_DIRECTORY_INFORMATION*>(buffer);
    bool first = true;
    ULONG size, index = 0;
    for (;;) {
        auto start = index;
        if (!NT_SUCCESS(NtQueryDirectoryObject(hDirectory, info, sizeof(buffer), FALSE, first, &index, &size)))
            break;
        first = false;
        for (ULONG i = 0; i < index - start; i++) {
            ObjectNameAndType data;
            auto& p = info[i];
            data.Name = std::wstring(p.Name.Buffer, p.Name.Length / sizeof(WCHAR));
            if(compare && !compare(data.Name.c_str()))
                continue;
            data.TypeName = std::wstring(p.TypeName.Buffer, p.TypeName.Length / sizeof(WCHAR));
            if(!objectName)
                objects.push_back(std::move(data));
            if (objectName && _wcsicmp(object.c_str(), data.Name.c_str()) == 0 ||
                _wcsicmp(object.c_str(), (data.Name + L"." + data.TypeName).c_str()) == 0) {
                objects.push_back(std::move(data));
                break;
            }
        }
    }
    ::CloseHandle(hDirectory);
    return objects;
}

NtQueryDirectoryObject is called in a loop with increasing indices until it fails. The returned details for each entry is the object’s name and type name.
NtQueryDirectoryObject 在索引递增的循环中调用,直到失败。每个条目返回的详细信息是对象的名称和类型名称。

Here is how to get a symbolic link’s target:
以下是获取符号链接目标的方法:

std::wstring ObjectManager::GetSymbolicLinkTarget(PCWSTR path) {
    std::wstring spath(path);
    if (spath[0] != L'\\')
        spath = L"\\" + spath;
    HANDLE hLink;
    OBJECT_ATTRIBUTES attr;
    std::wstring target;
    UNICODE_STRING name;
    RtlInitUnicodeString(&name, spath.c_str());
    InitializeObjectAttributes(&attr, &name, 0, nullptr, nullptr);
    if (NT_SUCCESS(NtOpenSymbolicLinkObject(&hLink, GENERIC_READ, &attr))) {
        WCHAR buffer[1 << 10];
        UNICODE_STRING result;
        result.Buffer = buffer;
        result.MaximumLength = sizeof(buffer);
        if (NT_SUCCESS(NtQuerySymbolicLinkObject(hLink, &result, nullptr)))
            target.assign(result.Buffer, result.Length / sizeof(WCHAR));
        ::CloseHandle(hLink);
    }
    return target;
}

See the full source code at https://github.com/zodiacon/ObjMgrProjFS.
请参阅完整的源代码,网址为 https://github.com/zodiacon/ObjMgrProjFS。

Conclusion 结论

The example provided is the bare minimum needed to write a ProjFS provider. This could be interesting for various types of data that is convenient to access with I/O APIs. Feel free to extend the example and resolve any bugs.
提供的示例是编写 ProjFS 提供程序所需的最低限度。对于方便使用 I/O API 访问的各种类型的数据,这可能很有趣。随意扩展示例并解决任何错误。

In the next post, we’ll look at some aspects of ProjFS implementation details.
在下一篇文章中,我们将介绍 ProjFS 实现细节的一些方面。

原文始发于Reilly:Projected File System

版权声明:admin 发表于 2024年2月22日 下午12:06。
转载请注明:Projected File System | CTF导航

相关文章