mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
The guard answered "nothing is holding the payload" from two lookups that can both miss a running instance: Restart Manager does not reliably report a program's own image or the modules it mapped, and the image path of a process running under another account cannot be read from the probe at all. A user log showed exactly that: the first guard passed while cockatrice.exe was running from the directory, the old uninstaller silently skipped the file it could not delete, and the update aborted half an uninstall later with "cancelled by user" after a close attempt the running window ignored. Ask the file itself. A running image is opened without write sharing, so the write open replacing it needs is refused with ERROR_SHARING_VIOLATION for as long as the program runs, while a file that is merely being read still accepts it. When neither lookup named anybody and a payload executable refuses that open, the probe answers that the file is in use and that the program holding it could not be identified - the guard's "could not confirm" dialog, whose Retry works once the user has closed it - instead of passing the payload as free. Only the payload executables are tested, not every file the purge removes: a locked executable proves a program is running and a mapped DLL belongs to a running executable, while a locked DLL or qt.conf can be the asking process itself, which is the loop the guard already had. The open is retried once after 300 ms, and only for the files that refused, so a scanner holding a file for a moment cannot stop an install for nothing. The Windows probe check now covers that answer: a payload executable held by the asking process, so that it cannot be named, has to come back as in use rather than as free. Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
67 lines
2.8 KiB
C++
67 lines
2.8 KiB
C++
#include "payload_locks.h"
|
|
|
|
#include "payload_paths.h"
|
|
|
|
#include <windows.h>
|
|
|
|
namespace
|
|
{
|
|
/// How long the second look waits before it re-tests the files that refused the write open.
|
|
/// Only those are re-tested, so the one answer costs this at most once: a scanner, or a
|
|
/// program that is only starting, holds a file without write sharing for a moment, and that
|
|
/// must not turn a free payload into a run the user has to answer.
|
|
constexpr DWORD RECHECK_DELAY_MILLISECONDS = 300;
|
|
|
|
/// Whether @p path refuses an open that asks to write it.
|
|
///
|
|
/// Windows opens a program's image, and every module loaded into it, with read and delete
|
|
/// sharing but not write sharing, so this is refused for as long as the program runs and by
|
|
/// nothing else that merely reads the file. A read-only file is skipped: it refuses a write
|
|
/// open for its own reason and would answer yes for a payload nobody is running, and the
|
|
/// purge clears that attribute before it deletes anything. Any other error is no answer at
|
|
/// all - access this probe does not have says nothing about whether the file is in use, and
|
|
/// still answering "in use" would stop installs that could have finished.
|
|
bool refusesWrite(const std::wstring &path)
|
|
{
|
|
const DWORD attributes = GetFileAttributesW(path.c_str());
|
|
if (attributes == INVALID_FILE_ATTRIBUTES ||
|
|
(attributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_READONLY)) != 0) {
|
|
return false;
|
|
}
|
|
const HANDLE file = CreateFileW(path.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
|
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
|
if (file == INVALID_HANDLE_VALUE) {
|
|
return GetLastError() == ERROR_SHARING_VIOLATION;
|
|
}
|
|
// Nothing was written: the open is the whole test, and closing the handle leaves the
|
|
// file exactly as it was.
|
|
CloseHandle(file);
|
|
return false;
|
|
}
|
|
} // namespace
|
|
|
|
std::vector<std::wstring> payloadExecutablesInUse(const std::wstring &installDir)
|
|
{
|
|
std::vector<std::wstring> refused;
|
|
for (const wchar_t *name : PAYLOAD_PROGRAMS) {
|
|
const std::wstring path = installDir + L"\\" + name;
|
|
if (refusesWrite(path)) {
|
|
refused.push_back(path);
|
|
}
|
|
}
|
|
if (refused.empty()) {
|
|
return refused;
|
|
}
|
|
|
|
// One re-test of the files that refused, and only those: the answer is shown to a user
|
|
// who then has to close a program and run the installer again, so it has to be the
|
|
// settled state rather than a moment in which something happened to hold the file.
|
|
Sleep(RECHECK_DELAY_MILLISECONDS);
|
|
std::vector<std::wstring> settled;
|
|
for (const std::wstring &path : refused) {
|
|
if (refusesWrite(path)) {
|
|
settled.push_back(path);
|
|
}
|
|
}
|
|
return settled;
|
|
}
|