mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(reverse): reconcile cleanup ownership and mutation diagnostics (#2799)
* feat(process): add owned debugger shutdown lifecycle * feat(reverse): add local inspection commands * feat(gdb): add persistent debugger integration * feat(cutter): add Cutter bridge integration * test(reverse): enforce complete integration coverage * fix(process): preserve debugger shutdown ownership * fix(reverse): retain evidence across interrupted commands * fix(gdb): honor captured provider environment * test(process): cover debugger shutdown retries * fix(reverse): retain evidence for interrupted providers * test(process): cover debugger shutdown retries * fix(reverse): retain evidence for interrupted providers * fix(server): snapshot provider environment at creation * fix(reverse): close review gaps with complete coverage * fix(reverse): address inspection review findings * fix(gdb): bound sessions and retain cleanup ownership * fix(ci): require reverse-tools quality lane * fix(process): unref inherited debugger output pipes * fix(reverse): address remaining PR 1960 findings * fix(gdb): address remaining PR 1961 findings * fix(reverse): account for unrestricted provider execution * fix(process): retain debugger ownership until safe shutdown * fix(process): preserve replacement debugger sessions * fix(reverse): preserve admission and output provenance * fix(process): retain exited debugger until output settles * fix(mcp): bound provider admission and debugger cleanup * fix(reverse): preserve failure provenance and classify E2BIG * fix(gdb): preserve startup cleanup and detach safety * fix(process): retain debugger ownership after detach failure * fix(reverse): preserve mutation and failure provenance * fix(gdb): address debugger review findings * fix(reverse): rebind sessions after external mutation * fix(process): cap debugger output after exit * fix(gdb): protect one-shot detach verification * feat(debugger): add reverse tool coverage gate * test(debugger): cover integrated failure paths * fix(gdb): bound and preserve session observations * fix(gdb): address review regressions * style: format merged integration files * fix(reverse): preserve session and output evidence * refactor(gdb): share repeated test and partial result setup * fix(gdb): invalidate TTY proof for Python aliases * fix(reverse): retain failure output byte counts * test(process): synchronize late debugger output * fix(gdb): invalidate TTY proof for wrapped commands * fix(gdb): recognize wit command abbreviation * test(reverse): wait for cancelled descendant readiness * test(reverse): bound descendant cancellation cleanup * style(reverse): format integration tests and adapters * style: format reverse engineering integration files * test(reverse): remove duplicate environment harness * test(gdb): share session startup assertions * fix(gdb): invalidate output proof for command abbreviations * test(reverse): share composition edge case scenarios * fix(ci): recognize verification and mutation tool entries * test(rizin): cover successful fake inferior detach * test(cli): align full output failure envelope * test(cli): isolate reverse command lifecycle service * test(process): share debugger output fixture setup * fix(binary): preserve high-bit Windows signature bytes * style(binary): format Windows signature parser * test(mcp): preserve call coverage in unchanged comparison * fix(ci): include reverse tools Vitest configs in lint cache * fix(mcp): preserve target content on delivery errors * ci: align duplication ceiling with expanded suite * test(reverse): allow signal admission mutants * fix(reverse): bound retained cleanup failures to process owners * fix(test): replace drifting mutation ranges with owned selectors * fix(reverse): admit persistent commands and retain cleanup ownership * fix(cutter): recover timed-out commands from exact receipts * fix(cutter): reject unsupported bridge request kinds * fix(ci): repair client format and shared verification fixtures * fix(native): preserve debugger cleanup ownership and bridge authentication * test(native): retain diagnostics without perfect coverage requirements * chore(licenses): refresh locked dependency inventory and notices * fix(test): exclude transient command locks from mutation sandboxes * fix(gdb): admit only platforms with safe detach verification * fix(cutter): retain bounded completion receipts until acknowledgement * fix(cutter): acknowledge commands rejected before UI dispatch * test(reverse): fail cancellation checks before fixture teardown * test(gdb): clean up the retained broken-stdin fixture owner * docs(test): clarify focused provider verification scope * fix(rizin): retain close cleanup failures * fix(gdb): wait for inferior stop and bound MI buffering * fix(cutter): retain failed receipt acknowledgements * fix(rizin): return retained session identity on rebind failure * fix(gdb): preserve startup cancellation and lossy status * fix(rizin): retain launch provenance and assemble frames once * fix(cutter): recover receipts across callers and bound discovery * docs: describe debugger evidence and bridge recovery * fix(process): keep cleanup incomplete while writers remain Stopping a debugger no longer reports success when inherited output streams stay open, so BinarySession cannot reopen a binary a child is still changing. Concurrent Cutter cleanup retries share one receipt acknowledgement instead of treating a consumed receipt as failure. * fix(debuggers): recover cleanup with captured process identities * fix(analysis): retire sessions after unrestricted commands * fix(cli): preserve completed evidence after cleanup recovery * fix(cutter): bound frame collection and coalesce cleanup * fix(windows): resolve selected provider paths absolutely * fix(gdb): report decoding loss in failed startup evidence * fix(rizin): scope decoding loss to collected command frames * test(mcp): cover snapshot retirement in reverse tools CI --------- Co-authored-by: Daniel Gonçalves Araujo <47118053+dandgabr@users.noreply.github.com> Co-authored-by: REA Maintainers <noreply@github.com> Co-authored-by: OpenAI Codex <codex@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
@@ -123,7 +123,8 @@ napi_value wrap(napi_env env, std::unique_ptr<Resource> resource);
|
||||
Resource& resource(napi_env env, napi_value value, Kind kind);
|
||||
|
||||
std::unique_ptr<File> openFile(const std::wstring& path, DWORD access = GENERIC_READ,
|
||||
bool directory = false);
|
||||
bool directory = false,
|
||||
DWORD finalSharing = FILE_SHARE_READ);
|
||||
napi_value identity(napi_env env, HANDLE handle, const std::wstring& path);
|
||||
std::unique_ptr<Runtime> createRuntime(const std::wstring& parent, const std::wstring& prefix);
|
||||
void closeRuntime(Runtime& runtime);
|
||||
|
||||
@@ -61,7 +61,8 @@ static Handle openComponent(const std::wstring& path, DWORD access, bool directo
|
||||
return handle;
|
||||
}
|
||||
|
||||
std::unique_ptr<File> openFile(const std::wstring& requested, DWORD access, bool directory) {
|
||||
std::unique_ptr<File> openFile(const std::wstring& requested, DWORD access, bool directory,
|
||||
DWORD finalSharing) {
|
||||
auto result = std::make_unique<File>(localPath(requested));
|
||||
result->requestedPath = requested;
|
||||
const auto& path = result->path;
|
||||
@@ -77,7 +78,8 @@ std::unique_ptr<File> openFile(const std::wstring& requested, DWORD access, bool
|
||||
while (offset < path.size()) {
|
||||
auto end = path.find(L'\\', offset);
|
||||
const bool final = end == std::wstring::npos;
|
||||
auto child = openComponent(path.substr(0, end), final ? access : 0, !final || directory);
|
||||
auto child = openComponent(path.substr(0, end), final ? access : 0, !final || directory,
|
||||
final ? finalSharing : FILE_SHARE_READ);
|
||||
// Once the next component is pinned against deletion, its parent cannot
|
||||
// become empty and NTFS cannot convert that parent into a reparse point.
|
||||
// Relax only ancestor write sharing so unrelated child renames can proceed.
|
||||
@@ -238,6 +240,100 @@ static void verifyPrivate(HANDLE handle, const std::wstring& path, bool root) {
|
||||
require(user && system, "Private DACL must allow only current user and SYSTEM", path, ERROR_ACCESS_DENIED);
|
||||
}
|
||||
|
||||
static std::unique_ptr<File> openPrivateBridgeDirectory(const std::wstring& path) {
|
||||
// The retained runtime root handle carries DELETE authority for owned
|
||||
// cleanup. Windows checks sharing in both directions, so this read-only
|
||||
// inspection handle must share DELETE even though it requests no such right.
|
||||
auto directory = openFile(path, FILE_LIST_DIRECTORY, true,
|
||||
FILE_SHARE_READ | FILE_SHARE_DELETE);
|
||||
verifyPrivate(directory->get(), path, true);
|
||||
return directory;
|
||||
}
|
||||
|
||||
static Handle openPrivateBridgeDescriptor(HANDLE parent, const std::wstring& name,
|
||||
const std::wstring& path) {
|
||||
require(!name.empty() && name.find_first_of(L"\\/:\"<>|?*") == std::wstring::npos &&
|
||||
name != L"." && name != L".." && name.size() <= 255,
|
||||
"Private descriptor requires one filename component", path, ERROR_INVALID_PARAMETER);
|
||||
UNICODE_STRING coordinate{};
|
||||
coordinate.Buffer = const_cast<wchar_t*>(name.data());
|
||||
coordinate.Length = static_cast<USHORT>(name.size() * sizeof(wchar_t));
|
||||
coordinate.MaximumLength = coordinate.Length;
|
||||
OBJECT_ATTRIBUTES attributes;
|
||||
InitializeObjectAttributes(&attributes, &coordinate, OBJ_CASE_INSENSITIVE, parent, nullptr);
|
||||
IO_STATUS_BLOCK observation{};
|
||||
HANDLE raw = INVALID_HANDLE_VALUE;
|
||||
// Immutable runtime files retain their original writer handle so later
|
||||
// opens cannot acquire write or delete access. This read-only handle must
|
||||
// share those granted rights for Windows' symmetric share check to pass.
|
||||
const auto status = NtCreateFile(&raw, GENERIC_READ | READ_CONTROL | FILE_READ_ATTRIBUTES | SYNCHRONIZE,
|
||||
&attributes, &observation, nullptr, FILE_ATTRIBUTE_NORMAL,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, FILE_OPEN,
|
||||
FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_REPARSE_POINT |
|
||||
FILE_NON_DIRECTORY_FILE,
|
||||
nullptr, 0);
|
||||
if (status < 0)
|
||||
throw Failure("Open Cutter bridge descriptor relative to its verified directory failed", path,
|
||||
RtlNtStatusToDosError(status));
|
||||
Handle descriptor(raw);
|
||||
require(descriptor.valid() && observation.Information == FILE_OPENED,
|
||||
"Cutter bridge descriptor was not opened as an existing file", path, ERROR_ACCESS_DENIED);
|
||||
FILE_STANDARD_INFO standard{};
|
||||
require(GetFileInformationByHandleEx(descriptor.get(), FileStandardInfo, &standard, sizeof(standard)),
|
||||
"Read Cutter bridge descriptor metadata failed", path);
|
||||
require(!standard.Directory && standard.EndOfFile.QuadPart >= 0 &&
|
||||
standard.EndOfFile.QuadPart <= 64 * 1024,
|
||||
"Cutter bridge descriptor exceeds its byte limit or is not a file", path, ERROR_FILE_TOO_LARGE);
|
||||
FILE_ATTRIBUTE_TAG_INFO fileAttributes{};
|
||||
require(GetFileInformationByHandleEx(descriptor.get(), FileAttributeTagInfo, &fileAttributes, sizeof(fileAttributes)),
|
||||
"Read Cutter bridge descriptor attributes failed", path);
|
||||
require((fileAttributes.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0,
|
||||
"Cutter bridge descriptor cannot be a reparse point", path, ERROR_REPARSE_TAG_INVALID);
|
||||
verifyPrivate(descriptor.get(), path, false);
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
static napi_value verifyCutterBridgeDirectory(napi_env env, const std::vector<napi_value>& args) {
|
||||
require(args.size() == 1, "Cutter bridge directory verification requires one path", L"",
|
||||
ERROR_INVALID_PARAMETER);
|
||||
const auto path = wide(env, args[0]);
|
||||
const auto directory = openPrivateBridgeDirectory(path);
|
||||
auto result = object(env);
|
||||
set(env, result, "privateDacl", boolean(env, true));
|
||||
set(env, result, "identity", identity(env, directory->get(), path));
|
||||
return result;
|
||||
}
|
||||
|
||||
static napi_value readCutterBridgeDescriptor(napi_env env, const std::vector<napi_value>& args) {
|
||||
require(args.size() == 3, "Cutter bridge descriptor read requires directory, name, and byte limit", L"",
|
||||
ERROR_INVALID_PARAMETER);
|
||||
const auto path = wide(env, args[0]);
|
||||
const auto name = wide(env, args[1]);
|
||||
const double maximum = numeric(env, args[2]);
|
||||
require(maximum >= 1 && maximum <= 64 * 1024 && maximum == static_cast<size_t>(maximum),
|
||||
"Cutter bridge descriptor limit is outside the supported range", path, ERROR_INVALID_PARAMETER);
|
||||
auto directory = openPrivateBridgeDirectory(path);
|
||||
auto descriptor = openPrivateBridgeDescriptor(directory->get(), name, path + L"\\" + name);
|
||||
FILE_STANDARD_INFO standard{};
|
||||
require(GetFileInformationByHandleEx(descriptor.get(), FileStandardInfo, &standard, sizeof(standard)),
|
||||
"Read Cutter bridge descriptor size failed", path);
|
||||
require(standard.EndOfFile.QuadPart <= maximum,
|
||||
"Cutter bridge descriptor exceeds the requested byte limit", path, ERROR_FILE_TOO_LARGE);
|
||||
std::vector<unsigned char> bytes(static_cast<size_t>(standard.EndOfFile.QuadPart));
|
||||
size_t offset = 0;
|
||||
while (offset < bytes.size()) {
|
||||
DWORD count = 0;
|
||||
const DWORD requested = static_cast<DWORD>(std::min<size_t>(bytes.size() - offset, MAXDWORD));
|
||||
require(ReadFile(descriptor.get(), bytes.data() + offset, requested, &count, nullptr) && count > 0,
|
||||
"Read Cutter bridge descriptor through its verified handle failed", path);
|
||||
offset += count;
|
||||
}
|
||||
napi_value result;
|
||||
const void* source = bytes.empty() ? static_cast<const void*>("") : bytes.data();
|
||||
check(napi_create_buffer_copy(env, bytes.size(), source, nullptr, &result));
|
||||
return result;
|
||||
}
|
||||
|
||||
std::unique_ptr<Runtime> createRuntime(const std::wstring& parentPath, const std::wstring& prefix) {
|
||||
std::unique_ptr<File> parent;
|
||||
try { parent = openFile(parentPath, 0, true); }
|
||||
@@ -559,6 +655,8 @@ static napi_value snapshot(napi_env env, napi_value rootValue, Runtime& root,
|
||||
}
|
||||
|
||||
napi_value filesystemCall(napi_env env, const std::wstring& operation, const std::vector<napi_value>& args) {
|
||||
if (operation == L"cutter_bridge_verify_directory") return verifyCutterBridgeDirectory(env, args);
|
||||
if (operation == L"cutter_bridge_read_descriptor") return readCutterBridgeDescriptor(env, args);
|
||||
const auto expected = operation == L"open" || operation == L"close" || operation == L"runtime_close" || operation == L"runtime_snapshot_cancel" ? 1
|
||||
: operation == L"runtime_create" || operation == L"runtime_mkdir" || operation == L"runtime_open" ? 2 : 3;
|
||||
require(args.size() == static_cast<size_t>(expected), "Wrong native filesystem argument count", L"", ERROR_INVALID_PARAMETER);
|
||||
|
||||
@@ -464,6 +464,7 @@ napi_value inspect(napi_env env, std::unique_ptr<Runtime>& probe) {
|
||||
set(env, result, "architecture", string(env, std::string("x64")));
|
||||
set(env, result, "filesystem", filesystem);
|
||||
set(env, result, "privateDacl", boolean(env, true));
|
||||
set(env, result, "privateBridgeDescriptorAccess", boolean(env, true));
|
||||
set(env, result, "atomicJobAssignment", boolean(env, true));
|
||||
set(env, result, "killOnOwnerClose", boolean(env, true));
|
||||
set(env, result, "protocolStdin", boolean(env, true));
|
||||
|
||||
Reference in New Issue
Block a user