From d562f6dd390724f733e4f9e91577e333290fcdaa Mon Sep 17 00:00:00 2001 From: xeropresence <3128949+xeropresence@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:21:06 -0400 Subject: [PATCH 1/3] IDA plugin: Disambiguate exports of same-named databases Diffing two databases with the same basename (e.g. two versions of the same binary in different directories) wrote both .BinExport sidecars to the same path next to the result file, so the secondary overwrote the primary. Loading the result then read the secondary export as both call graphs and reported a hash mismatch against the open IDB. Name the exports _primary.BinExport / _secondary.BinExport in that case, following the convention already used by the BinDiff UI (NewDiffImplementation.createUniqueExportFileName). The names flow through CallGraph::GetFilename() into the file.filename column, so save, load and the UI resolve distinct sidecars. --- ida/main_plugin.cc | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/ida/main_plugin.cc b/ida/main_plugin.cc index 2834c805..484ffdc0 100644 --- a/ida/main_plugin.cc +++ b/ida/main_plugin.cc @@ -236,6 +236,20 @@ absl::StatusOr ExportIdbs() { RemoveAll(secondary_temp_dir).IgnoreError(); ABSL_RETURN_IF_ERROR(CreateDirectories(secondary_temp_dir)); + // The export names end up as the "filename" column of the result file and + // as the names of the .BinExport files saved next to it. Same-named + // databases (e.g. two versions of the same binary in different directories) + // would overwrite each other there, so disambiguate both sides the same way + // the BinDiff UI does. + std::string primary_name = + ReplaceFileExtension(Basename(primary_idb_path), ""); + const std::string secondary_name = + ReplaceFileExtension(Basename(secondary_idb_path), ""); + const bool same_name = primary_name == secondary_name; + if (same_name) { + primary_name += "_primary"; + } + { const auto& config = config::Proto(); auto options = @@ -254,9 +268,8 @@ absl::StatusOr ExportIdbs() { std::thread export_thread( [&status, &exporter]() { status = exporter.Export(); }); - const std::string primary_binexport = JoinPath( - primary_temp_dir, - ReplaceFileExtension(Basename(primary_idb_path), ".BinExport")); + const std::string primary_binexport = + JoinPath(primary_temp_dir, absl::StrCat(primary_name, ".BinExport")); idc_value_t arg = primary_binexport.c_str(); if (qstring errbuf; !call_idc_func( /*result=*/nullptr, "BinExportBinary", &arg, @@ -272,6 +285,18 @@ absl::StatusOr ExportIdbs() { "Export of the secondary database failed: ", status.message())); } } + if (same_name) { + // IdbExporter always names its output after the database. + const std::string exported = JoinPath( + secondary_temp_dir, absl::StrCat(secondary_name, ".BinExport")); + const std::string renamed = + JoinPath(secondary_temp_dir, + absl::StrCat(secondary_name, "_secondary.BinExport")); + if (std::rename(exported.c_str(), renamed.c_str()) != 0) { + return absl::UnknownError(absl::StrCat( + "Failed to rename secondary export to \"", renamed, "\"")); + } + } return true; } From df191d9acc5e6c15cf1a5a568ce80a037cceff76 Mon Sep 17 00:00:00 2001 From: xeropresence <3128949+xeropresence@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:21:06 -0400 Subject: [PATCH 2/3] Reject result files whose primary and secondary BinExport collide DatabaseReader::Read resolves /.BinExport per side. If both filenames are equal, one export has necessarily overwritten the other, so fail with an actionable error instead of silently loading the same file for both sides. --- database_writer.cc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/database_writer.cc b/database_writer.cc index a2efa890..4a3b8667 100644 --- a/database_writer.cc +++ b/database_writer.cc @@ -881,6 +881,15 @@ absl::Status DatabaseReader::Read(CallGraph& call_graph1, .Into(&similarity_) .Into(&confidence_); ABSL_RETURN_IF_ERROR(statement.Execute()); + if (primary_filename_ == secondary_filename_) { + // Both sides would be read from the same .BinExport next to the results + // file; one of them has necessarily been overwritten by the other. + return absl::FailedPreconditionError(absl::StrCat( + "primary and secondary BinExport files both resolve to \"", + JoinPath(path_, primary_filename_ + ".BinExport"), + "\". Results for same-named binaries need to be re-diffed and saved " + "again")); + } // Function matches ABSL_ASSIGN_OR_RETURN( From e9e502b7286a12c762e007c28946e83f4fc9dfd7 Mon Sep 17 00:00:00 2001 From: xeropresence <3128949+xeropresence@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:07:19 -0400 Subject: [PATCH 3/3] IDA plugin: Report a secondary database that is open elsewhere The secondary export runs in a separate IDA process. When the secondary database is open in another IDA instance, that process cannot open the unpacked .id0 and exits after about a second without exporting. IdbExporter ignores IDA's exit code, so the failure only showed up after the in-process primary export (over a minute on large databases), and for same-named databases the rename of the missing export failed first with a misleading "Failed to rename secondary export" error. Check for the secondary's unpacked .id0 before exporting and fail with a message naming the file. Only rename the secondary export if it exists, so other export failures reach the existing "Exporting the secondary database failed" check. --- ida/main_plugin.cc | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/ida/main_plugin.cc b/ida/main_plugin.cc index 484ffdc0..de9ebbca 100644 --- a/ida/main_plugin.cc +++ b/ida/main_plugin.cc @@ -223,6 +223,17 @@ absl::StatusOr ExportIdbs() { "same name in the same directory. Please rename or move one of the " "files."); } + // IDA unpacks an open database next to it. The secondary export runs in a + // separate IDA process, which cannot open such a database and exits without + // exporting. Check now instead of failing after the primary export. + if (const std::string unpacked = + ReplaceFileExtension(secondary_idb_path, ".id0"); + FileExists(unpacked)) { + return absl::FailedPreconditionError(absl::StrCat( + "The secondary database is open in another IDA instance or was not " + "closed properly (found \"", + unpacked, "\").\nPlease close it there and try again.")); + } LOG(INFO) << "Diffing " << Basename(primary_idb_path) << " vs " << Basename(secondary_idb_path); @@ -286,13 +297,17 @@ absl::StatusOr ExportIdbs() { } } if (same_name) { - // IdbExporter always names its output after the database. + // IdbExporter always names its output after the database. It ignores IDA's + // exit code, so a failed secondary export (e.g. the database is open in + // another IDA instance) only shows as a missing file, which the caller + // reports. const std::string exported = JoinPath( secondary_temp_dir, absl::StrCat(secondary_name, ".BinExport")); const std::string renamed = JoinPath(secondary_temp_dir, absl::StrCat(secondary_name, "_secondary.BinExport")); - if (std::rename(exported.c_str(), renamed.c_str()) != 0) { + if (FileExists(exported) && + std::rename(exported.c_str(), renamed.c_str()) != 0) { return absl::UnknownError(absl::StrCat( "Failed to rename secondary export to \"", renamed, "\"")); }