C++ Windows Shell API文件读写修改实现与代码安全评审
问题修复:获取可读写IStream
核心错误原因:调用IShellFolder::BindToStorage时第二个绑定上下文参数传NULL,系统默认以只读模式STGM_READ打开文件流,没有写入权限,因此写入操作返回ACCESS_DENIED。
修复方法:
- 线程调用所有Shell/COM接口前必须先初始化COM:调用
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE),线程退出前调用CoUninitialize()。 - 创建绑定上下文
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);
- 调用
BindToStorage时传入配置好的pBindCtx,替换原来的NULL参数:
// 原代码第二个参数是NULL,替换为pBindCtx if (FAILED(pShellfolder->BindToStorage(pidlRelative, pBindCtx, IID_PPV_ARGS(&pStream)))) { pShellfolder->Release(); CoTaskMemFree(pidl); return false; }
- 写入操作完成后调用
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
相关产品推荐
相关产品推荐

