Skip to content

test: narrow Qt and BDB LSan suppressions - #7560

Open
PastaPastaPasta wants to merge 1 commit into
dashpay:developfrom
PastaPastaPasta:codex/lsan-suppress-qdbusconnectionmanager
Open

test: narrow Qt and BDB LSan suppressions#7560
PastaPastaPasta wants to merge 1 commit into
dashpay:developfrom
PastaPastaPasta:codex/lsan-suppress-qdbusconnectionmanager

Conversation

@PastaPastaPasta

@PastaPastaPasta PastaPastaPasta commented Aug 7, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

The Linux ASan job exposed two independent dependency-owned lifetime reports:

  1. Qt's process-global DBus connection manager retains 86 bytes across four allocations at shutdown. The same QDBusConnectionManager::executeConnectionRequest report appears on unrelated develop-based PRs.
  2. Berkeley DB 4.8 retains DB_PRIVATE lock objects after reopened environments are torn down and memory-pool file metadata after mock databases are closed.

The previous branch suppressed Berkeley DB's shared __os_malloc allocator and changed Dash wallet environment lifecycle code. The allocator rule was too broad, and the production changes were not the cause of either retained allocation. LeakSanitizer also appends a suppression summary to subprocess stderr, breaking exact stderr checks in wallet_hd.py and tool_wallet.py.

What was done?

  • Enabled DWARF allocation unwinding for LSan so optimized depends libraries expose stable dependency frames.
  • Added narrow rules for QDBusConnectionManager, __lock_open, and __memp_fopen; removed the shared __os_malloc rule.
  • Restored the original Berkeley DB production lifecycle behavior.
  • Added a strict shared parser for a structured trailing LSan suppression summary.
  • Added an opt-in assert_start_raises_init_error() parameter for startup-error tests and made wallet_hd.py declare its exact __lock_open / 1 allocation / 160 bytes entry.
  • Made successful dash-wallet subprocess checks accept only that same exact BDB summary when present. Application stderr, malformed summaries, and different suppression rows still fail.
  • Added framework regression coverage for valid, absent, and malformed summaries.

How Has This Been Tested?

On current develop with the configured macOS depends tree:

  • make -j13
  • ./src/test/test_dash --run_test=wallet_tests
  • QT_QPA_PLATFORM=cocoa ./src/qt/test/test_dash-qt (full app and wallet cases, no skips)
  • test/functional/test_runner.py wallet_hd.py (descriptor and legacy modes)
  • python3 -m unittest test_framework.test_node
  • Python, shell, and whitespace lint (Python lint reports flake8 unavailable locally; the framework unit test imports and executes the changed Python)
  • Independent code review and simplification passes

Using the exact linux64_asan artifact from failed job 93167272068 in an x86 CI-faithful container:

  • Wallet tests pass with exactly 4 allocations / 640 bytes matched by __lock_open.
  • Benchmark sanity passes with exactly 28 allocations / 1,939 bytes matched by __memp_fopen.
  • wallet_hd.py passes in descriptor and legacy modes with the final framework code and suppression file mounted in.
  • Qt tests pass with the narrow Qt rules. Historical unrelated CI jobs 92994161462 and 92967635309 independently reproduce the same DBus-owned 86-byte report.

The next ASan cycle additionally demonstrated that both wallet_hd.py modes pass and that tool_wallet.py receives exactly one 160-byte __lock_open summary; commit 587108a applies the same strict parsing to that subprocess path.

No generic allocator suppression, disabled leak detection, skipped affected test, diagnostic instrumentation, or production wallet behavior change remains.

Breaking Changes

None.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation
  • I have assigned this pull request to a milestone (for repository code-owners and collaborators only)

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@PastaPastaPasta
PastaPastaPasta requested a review from knst August 7, 2026 21:25
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The change configures LeakSanitizer for DWARF-based malloc unwinding and adds Qt DBus and Berkeley DB suppressions. The test framework parses and validates LeakSanitizer suppression summaries. Node and wallet tests check expected suppression data. Framework unit tests cover valid, absent, and malformed summaries.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LSAN
  participant TestNode
  participant StartupAssertion
  LSAN->>TestNode: write suppression summary to stderr
  TestNode->>TestNode: parse trailing summary
  TestNode->>StartupAssertion: compare expected suppression data
  StartupAssertion-->>TestNode: report mismatch before message matching
Loading

Possibly related PRs

  • dashpay/dash#7503: Related LeakSanitizer configuration and suppression handling changes.

Suggested reviewers: knst

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: narrowing Qt and Berkeley DB LeakSanitizer suppressions.
Description check ✅ Passed The description directly explains the LeakSanitizer changes, test handling, regression coverage, and validation performed.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@thepastaclaw

thepastaclaw commented Aug 7, 2026

Copy link
Copy Markdown

⛔ Blockers found — Opus deferred (commit bd4ab64)
Canonical validated blockers: 1

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final validation — Codex + Sonnet

This PR adds a single line, leak:QDBusConnectionManager, to test/sanitizer_suppressions/lsan, following the exact pattern of the three existing Qt/DBus suppressions already in the file. It fixes a documented LSan false-positive in the linux64_asan-build CI job's qt/test/test_dash-qt step, carries no consensus or runtime risk, and both agent lanes as well as CodeRabbit found zero actionable issues. Source: reviewer backend gpt-5.6-sol (codex, general + dash-core-commit-history roles) and claude-sonnet-5 (dash-core-commit-history + general roles); final verifier backend claude-sonnet-5 (final-verifier role). openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and is not reviewer evidence.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — dash-core-commit-history (completed)
  • Verifier: claude-sonnet-5 — final-verifier
  • Sonnet reviewers: claude-sonnet-5 — dash-core-commit-history (completed), claude-sonnet-5 — general (completed)

@PastaPastaPasta
PastaPastaPasta force-pushed the codex/lsan-suppress-qdbusconnectionmanager branch from d005540 to 7ddfc60 Compare August 7, 2026 22:50
@PastaPastaPasta PastaPastaPasta changed the title test: suppress QDBusConnectionManager LSan leak test: suppress QDBusConnectionManager and BDB LSan leaks Aug 7, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/wallet/bdb.cpp`:
- Around line 94-100: Update the failed and exception paths in Open to call
UnlockDirectory for the wallet directory whenever this environment successfully
acquired the lock, including the existing !fDbEnvInit cleanup path. Ensure
cleanup occurs before returning or propagating the exception, while avoiding
unlock attempts when this environment did not acquire the lock.
- Around line 94-100: Update the initialized-close branch in the wallet
environment Close() method to reset dbenv immediately after dbenv->close(0),
preventing reuse of the closed handle and repeated close calls. Add regression
coverage verifying both consecutive Close(); Close(); and Close(); Open();
sequences.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e7701d56-906e-4dde-96ee-57a5f9029d81

📥 Commits

Reviewing files that changed from the base of the PR and between d005540 and 7ddfc60.

📒 Files selected for processing (2)
  • src/wallet/bdb.cpp
  • test/sanitizer_suppressions/lsan
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/sanitizer_suppressions/lsan

Comment thread src/wallet/bdb.cpp Outdated
Comment on lines +94 to +100
if (!fDbEnvInit) {
if (dbenv) {
dbenv->close(0);
dbenv.reset();
}
return;
}

@coderabbitai coderabbitai Bot Aug 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -t f -i 'fs_helpers\.(cpp|h)$' | xargs -r rg -n -C 8 '\b(LockDirectory|UnlockDirectory|dir_locks)\b'
rg -n -C 8 '\b(LockDirectory|UnlockDirectory|fDbEnvInit|dbenv->close|dbenv\.reset)\b' src/wallet/bdb.cpp

Repository: dashpay/dash

Length of output: 10677


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- BerkeleyEnvironment declarations and lifecycle ---'
fd -t f -i 'bdb\.(cpp|h)$' | xargs -r ast-grep outline
rg -n -C 12 '\b(BerkeleyEnvironment::(Open|Close|Reset|~BerkeleyEnvironment)|LockDirectory|UnlockDirectory|g_dbenvs|make_unique<BerkeleyEnvironment>|BerkeleyEnvironment)' src/wallet src | head -n 500

printf '%s\n' '--- Open and Close implementation ---'
sed -n '80,220p' src/wallet/bdb.cpp

printf '%s\n' '--- Build/test references ---'
rg -n -C 5 '\b(bdb|BerkeleyEnvironment|walletlock|LockDirectory|UnlockDirectory)\b' src/wallet src/test src/Makefile* 2>/dev/null | head -n 500

Repository: dashpay/dash

Length of output: 50368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Lock helper tests and ownership behavior ---'
sed -n '1110,1175p' src/test/util_tests.cpp
sed -n '45,95p' src/util/fs_helpers.cpp

printf '%s\n' '--- All Open call sites and failure handling ---'
rg -n -C 5 '\->Open\(|\.Open\(' src/wallet src/test | head -n 300

printf '%s\n' '--- Read-only control-flow check for lock cleanup ---'
python3 - <<'PY'
from pathlib import Path
import re

source = Path("src/wallet/bdb.cpp").read_text()
open_body = source[source.index("bool BerkeleyEnvironment::Open"):source.index("//! Construct an in-memory mock")]
lock_pos = open_body.index("LockDirectory")
tail = open_body[lock_pos:]
print("UnlockDirectory calls after LockDirectory in Open:",
      len(re.findall(r"\bUnlockDirectory\s*\(", tail)))
print("Failure returns after LockDirectory in Open:",
      len(re.findall(r"\breturn\s+false\s*;", tail)))
for match in re.finditer(r"\breturn\s+false\s*;", tail):
    start = max(0, match.start() - 180)
    print("---")
    print(tail[start:match.end()].strip())
PY

Repository: dashpay/dash

Length of output: 10243


Release .walletlock on every failed Open.

Open acquires .walletlock before dbenv->open(), but the failed-open path returns without calling UnlockDirectory. The lock remains in dir_locks and can block another process from loading the wallet. Release the lock on every partial or failed Open, including exception paths, only when this environment acquired it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/wallet/bdb.cpp` around lines 94 - 100, Update the failed and exception
paths in Open to call UnlockDirectory for the wallet directory whenever this
environment successfully acquired the lock, including the existing !fDbEnvInit
cleanup path. Ensure cleanup occurs before returning or propagating the
exception, while avoiding unlock attempts when this environment did not acquire
the lock.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 12 '\b(void BerkeleyEnvironment::(Close|Open)|fDbEnvInit|dbenv->close|dbenv\.reset)\b' src/wallet/bdb.cpp
rg -n -C 5 'BerkeleyEnvironment|ReloadDbEnv|MakeMock' src test --glob '*.{cpp,h}'

Repository: dashpay/dash

Length of output: 38009


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '430,475p' src/wallet/bdb.cpp
sed -n '1,130p' src/wallet/test/db_tests.cpp
rg -n -C 8 'ReloadDbEnv|\.Open\(|\.Close\(|IsInitialized|BerkeleyEnvironment\(' src/wallet/test src/test --glob '*.{cpp,h}'
sed -n '1,90p' src/wallet/bdb.h

Repository: dashpay/dash

Length of output: 11555


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

source = Path("src/wallet/bdb.cpp").read_text()
close = re.search(r"void BerkeleyEnvironment::Close\(\)\n\{(?P<body>.*?)\n\}\n\nvoid BerkeleyEnvironment::Reset", source, re.S)
open_ = re.search(r"bool BerkeleyEnvironment::Open\(bilingual_str& err\)\n\{(?P<body>.*?)\n\}\n\n//! Construct", source, re.S)
reload = re.search(r"void BerkeleyEnvironment::ReloadDbEnv\(\)\n\{(?P<body>.*?)\n\}\n\nbool BerkeleyDatabase::Rewrite", source, re.S)

assert close and open_ and reload
close_body = close.group("body")
open_body = open_.group("body")
reload_body = reload.group("body")

# Current initialized-close path closes the handle without resetting it.
initialized_close = close_body.split("if (!fDbEnvInit)", 1)[1]
assert "int ret = dbenv->close(0);" in initialized_close
assert initialized_close.find("dbenv.reset()") == -1

# Open skips Reset() when dbenv is still non-null.
assert "if (!dbenv) {\n        Reset();\n    }" in open_body

# ReloadDbEnv closes, then resets, then opens.
assert reload_body.find("Flush(true);") < reload_body.find("Reset();") < reload_body.find("Open(open_err);")

# Model the relevant handle state. False means closed/null; True means open.
def close_transition(initialized, handle):
    if not initialized:
        return False  # early branch resets the handle
    return handle     # current initialized branch leaves the closed handle non-null

def open_transition(initialized, handle):
    if initialized:
        return initialized, handle
    if not handle:
        handle = True  # Reset() creates a fresh DbEnv
    return True, handle

initialized, handle = True, True
initialized, handle = False, close_transition(initialized, handle)
assert handle is True, "initialized Close() currently leaves a stale non-null handle"
initialized, handle = open_transition(initialized, handle)
assert initialized and handle, "state model did not reach Open()"
print("PASS: source ordering and state model demonstrate that Close(); Open() reuses the stale non-null DbEnv handle.")
PY

Repository: dashpay/dash

Length of output: 239


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

lines = Path("src/wallet/bdb.cpp").read_text().splitlines()

def line_index(text):
    for i, line in enumerate(lines):
        if text in line:
            return i
    raise AssertionError(f"missing: {text}")

close_start = line_index("void BerkeleyEnvironment::Close()")
reset_start = line_index("void BerkeleyEnvironment::Reset()")
open_start = line_index("bool BerkeleyEnvironment::Open(bilingual_str& err)")
reload_start = line_index("void BerkeleyEnvironment::ReloadDbEnv()")

close_lines = lines[close_start:reset_start]
initialized_lines = lines[line_index("    fDbEnvInit = false;"):reset_start]
open_lines = lines[open_start:reload_start]
reload_lines = lines[reload_start:line_index("bool BerkeleyDatabase::Rewrite")]

assert "            dbenv.reset();" in close_lines, "early Close() branch must reset dbenv"
assert "    int ret = dbenv->close(0);" in initialized_lines
assert "    dbenv.reset();" not in initialized_lines, "initialized Close() branch currently lacks reset"
assert "    if (!dbenv) {" in open_lines
assert "        Reset();" in open_lines
assert reload_lines.index("    Flush(true); // This will flush and close the environment") < reload_lines.index("    Reset();") < reload_lines.index("    Open(open_err);")

# State model: (initialized, non-null handle). A closed handle is represented
# by initialized=False, handle=True.
def close_state(state, fixed=False):
    initialized, handle = state
    if not initialized:
        return False, False  # early branch closes and resets
    handle = False if fixed else handle  # initialized branch closes; fixed code resets
    return False, handle

def open_state(state):
    initialized, handle = state
    if initialized:
        return state
    if not handle:
        handle = True  # Open() calls Reset() only when dbenv is null
    return True, handle

current = close_state((True, True))
assert current == (False, True)
assert open_state(current) == (True, True)
assert close_state(current) == (False, False)

fixed = close_state((True, True), fixed=True)
assert fixed == (False, False)
assert open_state(fixed) == (True, True)
assert close_state(fixed) == (False, False)

print("PASS: the initialized Close() path leaves a stale non-null handle; Open() reuses it; resetting after close removes both stale-handle paths.")
PY

Repository: dashpay/dash

Length of output: 290


Reset dbenv after closing it.

Close() leaves dbenv non-null after the initialized DbEnv::close(0) call. Open() can then reuse the closed handle, and a later Close() can call close() again.

Reset dbenv immediately after the initialized close. Add regression coverage for Close(); Close(); and Close(); Open();.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/wallet/bdb.cpp` around lines 94 - 100, Update the initialized-close
branch in the wallet environment Close() method to reset dbenv immediately after
dbenv->close(0), preventing reuse of the closed handle and repeated close calls.
Add regression coverage verifying both consecutive Close(); Close(); and
Close(); Open(); sequences.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initialized-close finding is valid and more severe than stale-handle reuse alone: DbEnv::close() clears its internal DB_ENV*, so the new !fDbEnvInit branch makes Close(); Close() crash when it invokes close() through the invalidated wrapper. Resetting dbenv after the initialized close fixes both repeated close and reopen. The failed-Open() .walletlock cleanup described in the same comment is pre-existing and is outside this PR's scope.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thepastaclaw, agreed. The initialized-close issue is more severe than stale-handle reuse. Close() must reset dbenv after the initialized DbEnv::close(0) call. This prevents both Close(); Close(); from calling through an invalid wrapper and Close(); Open(); from reusing it.

The failed-Open() .walletlock cleanup is pre-existing. It should not block this PR. Would you like me to open a follow-up GitHub issue for it?


✏️ Learnings added
Learnt from: thepastaclaw
Repo: dashpay/dash PR: 7560
File: src/wallet/bdb.cpp:94-100
Timestamp: 2026-08-07T23:40:23.222Z
Learning: In `src/wallet/bdb.cpp`, `DbEnv::close()` clears the wrapper's internal `DB_ENV*`. `BerkeleyEnvironment::Close()` must reset `dbenv` after closing an initialized environment. Otherwise, a subsequent `Close()` can call through an invalidated `DbEnv` wrapper, and `Open()` can reuse it.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preliminary review — Codex only

The unopened-handle cleanup addresses the reported leak, but it introduces a deterministic repeated-close crash after an initialized environment has already been closed. The generic Berkeley DB allocator suppression also hides unrelated BDB leaks, and the commit subject should acknowledge the production wallet lifecycle change.
Source: reviewer backend gpt-5.6-sol (general and dash-core-commit-history); final verifier backend gpt-5.6-sol. openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and not reviewer evidence.

Validated blockers were found in the Codex precheck. Opus is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — dash-core-commit-history (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 1 blocking | 🟡 1 suggestion(s) | 💬 1 nitpick(s)

1 additional finding(s) omitted (not in diff).

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/wallet/bdb.cpp`:
- [BLOCKING] src/wallet/bdb.cpp:95-97: Reset the initialized environment handle after closing it
  The new uninitialized branch assumes every non-null `dbenv` is an unopened handle, but the initialized branch calls `DbEnv::close(0)` without resetting the wrapper. Berkeley DB clears the wrapper's internal `DB_ENV*` during the first close, so a subsequent `Close()` enters this new branch and calls `close()` through an invalidated handle; this deterministically crashes with Berkeley DB 4.8. Normal wallet lifecycle code can reach this sequence because `Flush(true)` calls `Close()`, while eventual environment destruction calls `Close()` again. Reset `dbenv` immediately after the initialized close. This also ensures the new `Open()` guard creates a fresh handle after `Close(); Open()`. Add regression coverage for both `Close(); Close();` and `Close(); Open()`.

In `test/sanitizer_suppressions/lsan`:
- [SUGGESTION] test/sanitizer_suppressions/lsan:6: Do not suppress Berkeley DB's generic allocator
  `__os_malloc` is Berkeley DB's shared allocation wrapper, with call sites throughout its environment, database, logging, locking, and memory-pool code. LeakSanitizer matches any frame in an allocation stack, so this entry suppresses nearly every BDB-originating leak rather than only the four allocations reported by these tests. It also prevents the ASan job from verifying whether the accompanying `BerkeleyEnvironment::Close()` change actually fixes the target leak. Remove this suppression; if an unavoidable dependency leak remains after correcting the lifecycle, suppress a function specific to that allocation path.

Comment thread src/wallet/bdb.cpp Outdated
Comment on lines +95 to +97
if (dbenv) {
dbenv->close(0);
dbenv.reset();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Reset the initialized environment handle after closing it

The new uninitialized branch assumes every non-null dbenv is an unopened handle, but the initialized branch calls DbEnv::close(0) without resetting the wrapper. Berkeley DB clears the wrapper's internal DB_ENV* during the first close, so a subsequent Close() enters this new branch and calls close() through an invalidated handle; this deterministically crashes with Berkeley DB 4.8. Normal wallet lifecycle code can reach this sequence because Flush(true) calls Close(), while eventual environment destruction calls Close() again. Reset dbenv immediately after the initialized close. This also ensures the new Open() guard creates a fresh handle after Close(); Open(). Add regression coverage for both Close(); Close(); and Close(); Open().

Suggested change
if (dbenv) {
dbenv->close(0);
dbenv.reset();
int ret = dbenv->close(0);
dbenv.reset();
if (ret != 0)
LogPrintf("BerkeleyEnvironment::Close: Error %d closing database environment: %s\n", ret, DbEnv::strerror(ret));

source: ['coderabbit']

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1430201: Close() now resets dbenv after the initialized DbEnv::close(0), so repeated Close() is a no-op and Open() recreates the environment from a fresh handle. This was also the cause of the CI segfaults (DbEnv::close via ~BerkeleyEnvironment). db_tests and wallet_tests pass locally.


🤖 Posted autonomously by Claude on behalf of pasta.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in this update — Reset the initialized environment handle after closing it no longer present.

Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.

Comment thread test/sanitizer_suppressions/lsan Outdated
leak:QDBusConnectionPrivate
leak:QLayoutPrivate
leak:QDBusConnectionManager
leak:__os_malloc

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Do not suppress Berkeley DB's generic allocator

__os_malloc is Berkeley DB's shared allocation wrapper, with call sites throughout its environment, database, logging, locking, and memory-pool code. LeakSanitizer matches any frame in an allocation stack, so this entry suppresses nearly every BDB-originating leak rather than only the four allocations reported by these tests. It also prevents the ASan job from verifying whether the accompanying BerkeleyEnvironment::Close() change actually fixes the target leak. Remove this suppression; if an unavoidable dependency leak remains after correcting the lifecycle, suppress a function specific to that allocation path.

source: ['codex']

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — leak:__os_malloc would mask nearly every BDB-origin leak. Removed in 1430201; the Close() lifecycle fix addresses the actual unopened-environment leak the suppression was papering over.


🤖 Posted autonomously by Claude on behalf of pasta.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in this update — Do not suppress Berkeley DB's generic allocator no longer present.

Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/wallet/bdb.cpp (1)

155-158: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve mock mode when reopening a Berkeley environment.

CreateMockWalletDatabase() constructs an in-memory BerkeleyEnvironment. Close() drops dbenv, and Open() calls Reset(), which clears fMockDb. A reopened mock environment therefore uses the file-backed setup. Preserve the mock configuration across reset, or add a mock-specific recreation path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/wallet/bdb.cpp` around lines 155 - 158, Update the Open() and Reset()
flow so reopening an environment created by CreateMockWalletDatabase() preserves
fMockDb and recreates the in-memory BerkeleyEnvironment instead of switching to
file-backed storage. Keep normal file-backed reopening behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/wallet/bdb.cpp`:
- Around line 155-158: Update the Open() and Reset() flow so reopening an
environment created by CreateMockWalletDatabase() preserves fMockDb and
recreates the in-memory BerkeleyEnvironment instead of switching to file-backed
storage. Keep normal file-backed reopening behavior unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 349f810c-f17e-4ed0-b95e-583065900599

📥 Commits

Reviewing files that changed from the base of the PR and between 7ddfc60 and c497e74.

📒 Files selected for processing (2)
  • src/wallet/bdb.cpp
  • test/sanitizer_suppressions/lsan

@PastaPastaPasta
PastaPastaPasta force-pushed the codex/lsan-suppress-qdbusconnectionmanager branch from ce20bfd to 2498f38 Compare August 9, 2026 16:45
@PastaPastaPasta PastaPastaPasta changed the title test: suppress QDBusConnectionManager and BDB LSan leaks test: narrow Qt and BDB LSan suppressions Aug 9, 2026

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preliminary review — Codex only

The production Berkeley DB lifecycle edits and broad allocator suppression from the prior revision have been removed, resolving all three prior findings. However, the ASan functional suite also runs the legacy-wallet variant of wallet_upgradetohd.py, whose equivalent rejected -usehd=0 startup still performs exact stderr matching without accepting the newly emitted __lock_open suppression summary.
Source: reviewer backend gpt-5.6-sol (general and dash-core-commit-history); final verifier backend gpt-5.6-sol. openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and not reviewer evidence.

Validated blockers were found in the Codex precheck. Opus is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — dash-core-commit-history (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 1 blocking

1 additional finding(s) omitted (not in diff).

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `test/functional/wallet_upgradetohd.py`:
- [BLOCKING] test/functional/wallet_upgradetohd.py:85: Allow the BDB summary in the other HD startup test
  The ASan job runs `wallet_upgradetohd.py --legacy-wallet`, which creates and upgrades a Berkeley DB wallet before invoking the same rejected `-usehd=0` startup path handled in `wallet_hd.py`. Process teardown can therefore append the suppressed `__lock_open` allocation summary after the expected startup error. This call still uses exact full-text matching without `expected_lsan_suppressions`, so the newly enabled suppression summary makes this later functional test fail. Declare the same expected row here; the descriptor-wallet variant remains compatible because the helper intentionally permits the summary to be absent.

@PastaPastaPasta
PastaPastaPasta force-pushed the codex/lsan-suppress-qdbusconnectionmanager branch from 2498f38 to 587108a Compare August 9, 2026 19:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/functional/tool_wallet.py`:
- Around line 51-53: Update the stderr parsing flow around
split_lsan_suppression_summary to pass stderr.rstrip('\n') for parsing, while
retaining the original stderr when no suppression summary is returned. Add a
regression test covering the accepted [('__lock_open', 1, 160)] summary followed
by two trailing newlines, ensuring the empty-stderr assertion still succeeds.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 122a4adc-f9f6-4e30-89e0-d8586fd067ac

📥 Commits

Reviewing files that changed from the base of the PR and between 2498f38 and 587108a.

📒 Files selected for processing (3)
  • test/functional/test_framework/test_node.py
  • test/functional/test_framework/util.py
  • test/functional/tool_wallet.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/functional/test_framework/test_node.py

Comment thread test/functional/tool_wallet.py Outdated

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preliminary review — Codex only

The narrow dependency-frame suppressions are appropriate, but three deterministic Linux ASan functional-test failures remain: the shared parser rejects LSan's native summary boundaries, failing dash-wallet commands do not parse the permitted BDB summary, and wallet_upgradetohd.py omits the expected summary declaration.
Source: reviewer backends gpt-5.6-sol (general and dash-core-commit-history); final verifier backend gpt-5.6-sol. openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and not reviewer evidence.

Validated blockers were found in the Codex precheck. Opus is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — dash-core-commit-history (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 3 blocking

2 additional finding(s) omitted (not in diff).

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `test/functional/wallet_upgradetohd.py`:
- [BLOCKING] test/functional/wallet_upgradetohd.py:85: Allow the BDB summary in the other HD startup test
  The ASan suite runs `wallet_upgradetohd.py --legacy-wallet`. This path creates and upgrades a Berkeley DB wallet, then starts the node with rejected `-usehd=0`; wallet loading opens the BDB environment before the HD compatibility check fails. Teardown can therefore append the suppressed `__lock_open / 1 allocation / 160 bytes` summary. Because this call does not opt into suppression parsing, `assert_start_raises_init_error()` compares the full error plus summary against the expected error and fails. Pass the same expected row used by `wallet_hd.py`; the descriptor-wallet run remains valid because the helper permits the summary to be absent.

In `test/functional/test_framework/util.py`:
- [BLOCKING] test/functional/test_framework/util.py:29-46: Accept LSan's native summary boundaries
  LLVM's `PrintMatchedSuppressions()` emits the opening separator directly and terminates the final separator with exactly two newlines. This parser instead requires a newline before the opening separator and requires the final separator to be the absolute end of the string. Consequently, raw `dash-wallet` stderr containing only a native suppression summary is not recognized, and `assert_tool_output()` subsequently fails its empty-stderr assertion. Parse both summary-only and application-stderr-prefixed forms, accept either the native two-newline trailer or the intentionally stripped representation, and add a regression fixture using LLVM's exact native output.

In `test/functional/tool_wallet.py`:
- [BLOCKING] test/functional/tool_wallet.py:41-46: Allow the BDB summary on failing wallet-tool commands
  Only successful dash-wallet invocations use the new suppression parser. Some expected-error commands in the legacy-wallet run open the BDB environment before returning their application error. For example, the missing-`-dumpfile` test constructs and verifies the existing `todump` database through `MakeDatabase()` before `DumpWallet()` rejects the missing argument. Process teardown can then append the permitted `__lock_open / 1 allocation / 160 bytes` summary, causing the direct `stderr.strip()` comparison to fail. Parse and validate the same exact optional row in this helper so malformed, different, and additional suppression rows still fail.

Comment on lines +29 to +46
def split_lsan_suppression_summary(stderr):
separator = '-' * 53
marker = f'\n{separator}\nSuppressions used:\n count bytes template\n'
if marker not in stderr:
return stderr, None

application_stderr, summary = stderr.rsplit(marker, 1)
footer = f'\n{separator}'
if not summary.endswith(footer):
return stderr, None

suppressions = []
for row in summary[:-len(footer)].splitlines():
match = re.fullmatch(r'\s*(\d+)\s+(\d+)\s+(\S+)', row)
if match is None:
return stderr, None
suppressions.append((match[3], int(match[1]), int(match[2])))
return application_stderr, suppressions

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Accept LSan's native summary boundaries

LLVM's PrintMatchedSuppressions() emits the opening separator directly and terminates the final separator with exactly two newlines. This parser instead requires a newline before the opening separator and requires the final separator to be the absolute end of the string. Consequently, raw dash-wallet stderr containing only a native suppression summary is not recognized, and assert_tool_output() subsequently fails its empty-stderr assertion. Parse both summary-only and application-stderr-prefixed forms, accept either the native two-newline trailer or the intentionally stripped representation, and add a regression fixture using LLVM's exact native output.

Suggested change
def split_lsan_suppression_summary(stderr):
separator = '-' * 53
marker = f'\n{separator}\nSuppressions used:\n count bytes template\n'
if marker not in stderr:
return stderr, None
application_stderr, summary = stderr.rsplit(marker, 1)
footer = f'\n{separator}'
if not summary.endswith(footer):
return stderr, None
suppressions = []
for row in summary[:-len(footer)].splitlines():
match = re.fullmatch(r'\s*(\d+)\s+(\d+)\s+(\S+)', row)
if match is None:
return stderr, None
suppressions.append((match[3], int(match[1]), int(match[2])))
return application_stderr, suppressions
def split_lsan_suppression_summary(stderr):
separator = '-' * 53
marker = f'{separator}\nSuppressions used:\n count bytes template\n'
marker_index = stderr.rfind(marker)
if marker_index == -1 or (marker_index > 0 and stderr[marker_index - 1] != '\n'):
return stderr, None
application_stderr = stderr[:marker_index]
if application_stderr.endswith('\n'):
application_stderr = application_stderr[:-1]
summary = stderr[marker_index + len(marker):]
footer = f'\n{separator}'
if summary.endswith(footer + '\n\n'):
summary = summary[:-2]
elif not summary.endswith(footer):
return stderr, None
suppressions = []
for row in summary[:-len(footer)].splitlines():
match = re.fullmatch(r'\s*(\d+)\s+(\d+)\s+(\S+)', row)
if match is None:
return stderr, None
suppressions.append((match[3], int(match[1]), int(match[2])))
return application_stderr, suppressions

source: ['codex', 'coderabbit']

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in this update — Accept LSan's native summary boundaries no longer present.

Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.

@PastaPastaPasta
PastaPastaPasta force-pushed the codex/lsan-suppress-qdbusconnectionmanager branch from 587108a to 3c4c11e Compare August 9, 2026 22:24

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preliminary review — Codex only

The shared parser now correctly handles native and stripped LeakSanitizer summary boundaries, resolving the prior parser failure. Two legacy-wallet error paths still compare stderr without opting into the new suppression-summary handling, so the Linux ASan functional suite can fail when Berkeley DB emits the permitted __lock_open summary.
Source: reviewers gpt-5.6-sol (general and dash-core-commit-history); final verifier gpt-5.6-sol. openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and not reviewer evidence.

Validated blockers were found in the Codex precheck. Opus is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — dash-core-commit-history (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 2 blocking

2 additional finding(s) omitted (not in diff).

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `test/functional/wallet_upgradetohd.py`:
- [BLOCKING] test/functional/wallet_upgradetohd.py:85: Allow the BDB summary in the other HD startup test
  The ASan suite runs this test with `--legacy-wallet`. The test upgrades a Berkeley DB wallet to HD and then attempts the rejected `-usehd=0` startup. `CWallet::Create()` loads and verifies the database before performing the HD compatibility check at `wallet.cpp:3256-3260`, so Berkeley DB's environment is opened and teardown can emit the suppressed `__lock_open / 1 allocation / 160 bytes` summary. This call does not pass `expected_lsan_suppressions`, leaving the summary attached to stderr and causing the full-text comparison to fail. Declare the same exact row as `wallet_hd.py`; the descriptor run remains valid because the helper allows the summary to be absent.

In `test/functional/tool_wallet.py`:
- [BLOCKING] test/functional/tool_wallet.py:41-46: Allow the BDB summary on failing wallet-tool commands
  Only successful `dash-wallet` invocations pass stderr through `split_lsan_suppression_summary()`. In the legacy-wallet run, expected-error commands can open Berkeley DB before reporting their application error. For example, the missing-`-dumpfile` case calls `MakeDatabase()` with verification enabled; `MakeBerkeleyDatabase()` invokes `BerkeleyDatabase::Verify()`, which opens the environment before `DumpWallet()` rejects the missing argument. Teardown can then append the permitted `__lock_open / 1 allocation / 160 bytes` summary, causing the raw `stderr.strip()` comparison to fail. Parse and validate the same exact optional row in this helper so different, additional, or malformed summaries still fail.

@PastaPastaPasta
PastaPastaPasta force-pushed the codex/lsan-suppress-qdbusconnectionmanager branch from 3c4c11e to b61c7c9 Compare August 10, 2026 01:15

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preliminary review — Codex only

The strict LSan summary parser and the failing dash-wallet command path are correctly handled at the current head. One legacy-wallet startup assertion in wallet_upgradetohd.py still omits the expected Berkeley DB suppression row, so the Linux ASan functional suite can fail despite receiving the expected initialization error.
Source: reviewer backend gpt-5.6-sol (general and dash-core-commit-history); final verifier backend gpt-5.6-sol. openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and not reviewer evidence.

Validated blockers were found in the Codex precheck. Opus is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — dash-core-commit-history (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 1 blocking

1 additional finding(s) omitted (not in diff).

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `test/functional/wallet_upgradetohd.py`:
- [BLOCKING] test/functional/wallet_upgradetohd.py:85: Allow the BDB summary in the other HD startup test
  The ASan suite runs this test with `--legacy-wallet`. After the test upgrades the Berkeley DB wallet to HD, this rejected `-usehd=0` startup verifies and loads the database before reaching the compatibility check at `src/wallet/wallet.cpp:3256-3260`. Berkeley DB teardown can therefore append the suppressed `__lock_open / 1 allocation / 160 bytes` summary. Because this call does not pass `expected_lsan_suppressions`, `assert_start_raises_init_error()` leaves that summary in stderr and the full-text comparison fails. Declare the same exact suppression row used by `wallet_hd.py`; the descriptor-wallet run remains valid because the helper permits the summary to be absent.

@PastaPastaPasta
PastaPastaPasta force-pushed the codex/lsan-suppress-qdbusconnectionmanager branch from b61c7c9 to bd4ab64 Compare August 10, 2026 03:55

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preliminary review — Codex only

The narrow suppression rules and strict parser changes are coherent, and the parser unit test passes. However, the legacy-wallet variant of wallet_upgradetohd.py still performs exact stderr matching without accepting the newly suppressed Berkeley DB summary, leaving a Linux ASan functional-test blocker.
Source: reviewers gpt-5.6-sol (Codex general and dash-core-commit-history); final verifier gpt-5.6-sol (Codex). openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and not reviewer evidence.

Validated blockers were found in the Codex precheck. Opus is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — dash-core-commit-history (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 1 blocking

1 additional finding(s) omitted (not in diff).

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `test/functional/wallet_upgradetohd.py`:
- [BLOCKING] test/functional/wallet_upgradetohd.py:85: Allow the BDB summary in the other HD startup test
  The ASan functional suite runs `wallet_upgradetohd.py --legacy-wallet`. This test upgrades a Berkeley DB wallet to HD and then starts the node with `-usehd=0`; startup verifies the wallet through `BerkeleyDatabase::Verify()`, opens the DB_PRIVATE environment, loads the wallet, and only then rejects the option in `CWallet::Create()`. Teardown can therefore append the suppressed `__lock_open / 1 allocation / 160 bytes` summary. Because this call does not pass `expected_lsan_suppressions`, `assert_start_raises_init_error()` retains the summary in stderr and its full-text comparison fails. Declare the same exact row used by `wallet_hd.py`; the descriptor-wallet run remains valid because the helper permits the summary to be absent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants