[CrashReporting] Name the files and processes behind a failed installer purge (#7590)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions

* [CrashReporting] Log which files and processes blocked the installer purge

A purge that fails aborts the upgrade with a message that can only guess
at the cause, and the log only listed which DLLs survived. Record what
Windows reports for each survivor and which processes still have the
install directory mapped, then repeat the holder list in the abort
message.

The diagnostics are additive and run only on the abort path: they open
each survivor for DELETE access and close it again without deleting, so a
failed upgrade can never end up half-removed. The purge itself is
unchanged.

* [CrashReporting] Name the holders of a failed purge with Restart Manager

The process-and-module scan could only name a holder whose image lives in the
install directory or that loaded one of its DLLs, so the case this diagnostic
exists for - an antivirus scanner holding a plain handle - was reported as "no
holder named, only a code". Reaching the Restart Manager from the NSIS script
would have meant marshalling RM_PROCESS_INFO through System::Call as a table of
hand-computed offsets, which cannot be checked without a Windows toolchain, and
that is why it was dropped when this diagnostics was written.

Move the call into tools/installer-holder-probe: a small executable the same
Windows build compiles, linking nothing from this project so it still starts
when the installed runtime is what the purge could not replace. It rides inside
the installer and is extracted to $PLUGINSDIR only when the purge has already
failed, so the successful path is unchanged and nothing new is installed.

.ci/compile.sh runs it against a file held with FileShare.None on the Windows
runners, so the lookup is exercised on real Windows instead of only compiled.

* [CrashReporting] Fix the holder probe check and cap the holder line

The probe block ran after the package steps, where the working directory is
already the build dir, so its $BUILD_DIR-prefixed paths named build/build and
the PowerShell file could not be written. Move it ahead of install/package,
where the build dir is still the cwd, and name both files relative to it.
Guard it on MAKE_NO_CLIENT, since the probe is only built with the client.

Cap the rendered holder line at HOLDER_LINE_CHAR_MAX: the count limit alone
does not keep long service names inside the NSIS string that also carries the
abort text and the log path. Whole entries are kept and the rest counted, with
the first holder always shown.

Also run cmake-format on the probe's CMakeLists so the lint job accepts it.

* [CrashReporting] Report a read-only purge survivor and bound the probe check

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-10-24 00:46:54 +02:00 • committed by GitHub
parent ad4320cd7b
commit bbc6003f0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 793 additions and 1 deletions

View file

@ -0,0 +1,78 @@
#include "holder_line.h"
#include "restart_manager_holders.h"
#include <string>
#include <vector>
#include <windows.h>
namespace
{
/// The installer sets this, the same variable the running-instance guard uses, instead
/// of passing the directory on the command line: a path with spaces, quotes or a
/// trailing backslash then never has to survive NSIS quoting or a command line.
constexpr wchar_t INSTALL_DIR_VARIABLE[] = L"COCKATRICE_GUARD_DIR";
/// Exit codes the installer reads. "Something was found" is deliberately not 0, so an
/// answer of "nothing holds the files" stays distinguishable from a probe that failed.
enum ExitCode
{
EXIT_NO_HOLDERS = 0,
EXIT_HOLDERS_FOUND = 1,
EXIT_NOT_CONFIGURED = 2,
EXIT_PROBE_FAILED = 3,
};
/// Writes one line to the pipe nsExec reads. Narrowed to ASCII on purpose: the bytes
/// come back to the installer as an ANSI string, so a non-ASCII process name would be
/// mojibake in the log; a '?' at least stays honest. Control characters are dropped
/// too, so a name can never forge a second log line.
void writeLine(const std::wstring &text)
{
std::string narrow;
narrow.reserve(text.size() + 2);
for (const wchar_t character : text) {
narrow.push_back(character >= 0x20 && character < 0x7F ? static_cast<char>(character) : '?');
}
narrow += "\r\n";
DWORD written = 0;
WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), narrow.data(), static_cast<DWORD>(narrow.size()), &written, nullptr);
}
std::wstring installDirFromEnvironment()
{
for (DWORD size = MAX_PATH; size <= 32768U; size *= 2) {
std::vector<wchar_t> buffer(size);
const DWORD length = GetEnvironmentVariableW(INSTALL_DIR_VARIABLE, buffer.data(), size);
if (length == 0) {
return {};
}
if (length < size) {
return std::wstring(buffer.data(), length);
}
}
return {};
}
} // namespace
int main()
{
const std::wstring installDir = installDirFromEnvironment();
if (installDir.empty()) {
writeLine(L"the probe was not given the install directory");
return EXIT_NOT_CONFIGURED;
}
std::vector<Holder> holders;
const unsigned long error = findProcessesUsingFiles(collectProbeFiles(installDir), holders);
if (error != ERROR_SUCCESS) {
writeLine(L"the Restart Manager lookup failed with win32 error " + std::to_wstring(error));
return EXIT_PROBE_FAILED;
}
if (holders.empty()) {
// No output: the installer words the "nothing is holding them" case, and it
// owns the message either way.
return EXIT_NO_HOLDERS;
}
writeLine(formatHolders(holders));
return EXIT_HOLDERS_FOUND;
}