You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C++ Windows Shell API文件读写修改实现与代码安全评审

问题修复:获取可读写IStream

核心错误原因:调用IShellFolder::BindToStorage时第二个绑定上下文参数传NULL,系统默认以只读模式STGM_READ打开文件流,没有写入权限,因此写入操作返回ACCESS_DENIED。

修复方法:

  1. 线程调用所有Shell/COM接口前必须先初始化COM:调用CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE),线程退出前调用CoUninitialize()。
  2. 创建绑定上下文IBindCtx,配置打开模式为读写:
CComPtr<IBindCtx> pBindCtx;
HRESULT hr = CreateBindCtx(0, &pBindCtx);
if (FAILED(hr)) {
    CoTaskMemFree(pidl);
    return false;
}
BIND_OPTS2 bindOpts = {sizeof(BIND_OPTS2)};
bindOpts.grfMode = STGM_READWRITE | STGM_SHARE_DENY_NONE; // 读写模式,允许其他进程共享访问
pBindCtx->SetBindOptions(&bindOpts);
  1. 调用BindToStorage时传入配置好的pBindCtx,替换原来的NULL参数:
// 原代码第二个参数是NULL,替换为pBindCtx
if (FAILED(pShellfolder->BindToStorage(pidlRelative, pBindCtx, IID_PPV_ARGS(&pStream))))
{
    pShellfolder->Release();
    CoTaskMemFree(pidl);
    return false;
}
  1. 写入操作完成后调用pStream->Commit(STGC_DEFAULT),确保修改持久化到磁盘,不会停留在缓存中。
代码评审与问题修正

原代码存在多处资源泄漏、逻辑错误和不规范实现,具体问题如下:

  • 严重资源泄漏
    • 获取到的IStream* pStream全程没有调用Release()递减COM引用计数,每次执行函数都会泄漏流对象和对应的文件句柄。
    • 所有错误返回分支(如fopen_s失败、流读取失败)都没有释放已获取的pStream,中途退出时资源直接泄漏。
    • 没有处理new操作抛出std::bad_alloc异常的场景,此时已经申请的pidl、pShellfolder不会被释放,造成泄漏。
  • 错误检查缺失
    • 没有校验IStream_Size、Seek、Read、fwrite、Write等调用的返回值,一旦中间步骤失败,后续逻辑会使用非法值:比如获取文件大小失败时size.QuadPart是随机值,new会申请异常大的内存直接崩溃。
    • 没有判断目标文件大小是否为0,空文件场景下new unsigned char[0]行为是编译器实现定义的,可能出现不可预期问题。
  • 逻辑错误
    • 打印Relative IDL path的代码段错误传入了绝对路径的pidl,应该传入相对路径pidlRelative,否则两次打印的路径完全一致,调试信息无效。
    • 写入内容后没有调用IStream::Commit,修改可能停留在系统缓存中,程序退出后修改丢失。
  • 兼容性与规范问题
    • 路径缓冲区固定为MAX_PATH + 1长度,不支持Windows超过260字符的长路径,会出现路径获取失败。
    • 手动管理所有COM对象、内存、文件句柄,没有使用RAII机制,后续修改代码很容易引入新的资源泄漏,建议使用CComPtr管理COM对象、std::unique_ptr管理堆内存、std::unique_ptr配合自定义删除器管理文件句柄。
    • fopen_s使用窄字符接口,如果工作目录包含非ASCII字符会打开文件失败,建议使用宽字符版本_wfopen_s。
    • 读取文件时没有校验actualRead和预期长度是否一致,磁盘损坏、文件被占用等场景下读取长度不足,后续写入副本文件会把缓冲区未初始化的垃圾内容写入文件。
修复后完整参考实现
#include <windows.h>
#include <shlobj.h>
#include <atlbase.h>
#include <cstdio>
#include <memory>
#include <string>

bool OpenStream(const std::wstring& fileName)
{
    // 实际项目中应在线程初始化阶段统一调用COM初始化,不要在业务函数里重复调用
    HRESULT comInitHr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
    if (FAILED(comInitHr) && comInitHr != RPC_E_CHANGED_MODE) {
        return false;
    }

    // 用智能指针自动管理PIDL内存
    struct CoTaskMemFreeDeleter {
        void operator()(void* p) const { CoTaskMemFree(p); }
    };
    std::unique_ptr<std::remove_pointer_t<PIDLIST_ABSOLUTE>, CoTaskMemFreeDeleter> pidlGuard;
    PIDLIST_ABSOLUTE pidl = nullptr;
    if (FAILED(::SHILCreateFromPath(fileName.c_str(), &pidl, nullptr))) {
        if (SUCCEEDED(comInitHr)) CoUninitialize();
        return false;
    }
    pidlGuard.reset(pidl);

    // 支持长路径的动态缓冲区
    std::wstring buffer(MAX_PATH + 1, L'\0');
    if (::SHGetPathFromIDListW(pidl, buffer.data()))
    {
        ::OutputDebugString(L"File IDL path: ");
        ::OutputDebugString(buffer.c_str());
        ::OutputDebugString(L"\r\n");
    }

    CComPtr<IShellFolder> pShellfolder;
    LPCITEMIDLIST pidlRelative = nullptr;
    HRESULT hr = ::SHBindToParent(pidl, IID_PPV_ARGS(&pShellfolder), &pidlRelative);
    if (FAILED(hr)) {
        if (SUCCEEDED(comInitHr)) CoUninitialize();
        return false;
    }

    // 修复:打印相对路径时传入pidlRelative
    buffer.resize(MAX_PATH + 1, L'\0');
    if (::SHGetPathFromIDListW(pidlRelative, buffer.data()))
    {
        ::OutputDebugString(L"Relative IDL path: ");
        ::OutputDebugString(buffer.c_str());
        ::OutputDebugString(L"\r\n");
    }

    // 创建绑定上下文,配置读写权限
    CComPtr<IBindCtx> pBindCtx;
    hr = CreateBindCtx(0, &pBindCtx);
    if (FAILED(hr)) {
        if (SUCCEEDED(comInitHr)) CoUninitialize();
        return false;
    }
    BIND_OPTS2 bindOpts = {sizeof(BIND_OPTS2)};
    bindOpts.grfMode = STGM_READWRITE | STGM_SHARE_DENY_NONE;
    pBindCtx->SetBindOptions(&bindOpts);

    CComPtr<IStream> pStream;
    hr = pShellfolder->BindToStorage(pidlRelative, pBindCtx, IID_PPV_ARGS(&pStream));
    if (FAILED(hr)) {
        if (SUCCEEDED(comInitHr)) CoUninitialize();
        return false;
    }

    // 获取文件大小,检查返回值
    STATSTG stat = {};
    hr = pStream->Stat(&stat, STATFLAG_NONAME);
    if (FAILED(hr)) {
        if (SUCCEEDED(comInitHr)) CoUninitialize();
        return false;
    }
    ULONGLONG fileSize = stat.cbSize.QuadPart;

    LARGE_INTEGER pos = {0};
    hr = pStream->Seek(pos, STREAM_SEEK_SET, nullptr);
    if (FAILED(hr)) {
        if (SUCCEEDED(comInitHr)) CoUninitialize();
        return false;
    }

    // 空文件直接跳过读取逻辑
    std::unique_ptr<unsigned char[]> pBuffer;
    ULONG actualRead = 0;
    if (fileSize > 0) {
        pBuffer = std::make_unique<unsigned char[]>(fileSize);
        hr = pStream->Read(pBuffer.get(), static_cast<ULONG>(fileSize), &actualRead);
        if (FAILED(hr)) {
            if (SUCCEEDED(comInitHr)) CoUninitialize();
            return false;
        }
    }

    // 用智能指针管理文件句柄
    struct FileCloser {
        void operator()(FILE* p) const { if (p) fclose(p); }
    };
    std::unique_ptr<FILE, FileCloser> fileGuard;
    FILE* pFile = nullptr;
    if (_wfopen_s(&pFile, L"__Copy.bin", L"wb") != 0 || !pFile) {
        if (SUCCEEDED(comInitHr)) CoUninitialize();
        return false;
    }
    fileGuard.reset(pFile);

    if (actualRead > 0) {
        std::size_t writeCount = std::fwrite(pBuffer.get(), sizeof(unsigned char), actualRead, pFile);
        if (writeCount != actualRead) {
            if (SUCCEEDED(comInitHr)) CoUninitialize();
            return false;
        }
    }
    fileGuard.reset(); // 提前关闭文件

    hr = pStream->Seek(pos, STREAM_SEEK_SET, nullptr);
    if (FAILED(hr)) {
        if (SUCCEEDED(comInitHr)) CoUninitialize();
        return false;
    }

    const char testContent[] = "Test-test-test-test";
    ULONG actualWrite = 0;
    hr = pStream->Write(testContent, sizeof(testContent) - 1, &actualWrite); // 减1去掉字符串末尾的\0
    if (FAILED(hr) || actualWrite != sizeof(testContent) -1) {
        if (SUCCEEDED(comInitHr)) CoUninitialize();
        return false;
    }

    // 提交修改,确保持久化到磁盘
    hr = pStream->Commit(STGC_DEFAULT);
    if (FAILED(hr)) {
        if (SUCCEEDED(comInitHr)) CoUninitialize();
        return false;
    }

    if (SUCCEEDED(comInitHr)) CoUninitialize();
    return true;
}

内容的提问来源于stack exchange,提问作者Jean-Milost Reymond

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 13:57:32