Repository source: docs/plans/gpg_agent_leak/gpg_agent_leak_step4_implementation_summary.md
Debug mode: enabled (inherited from Step 3)
Step 4: Implementation Summary – GPG agent leak
Stage 1 - Confirm the kill command
Changes:
None (manual repro in
/tmpscratch homes; GnuPG 2.2.27, WSL2).
Verification:
Passed: using a temp home starts one
gpg-agent(0 → 1);gpgconf --homedir <dir> --kill allreturns the count to 0.Passed: the agent socket directory is derived from the home path (
/run/user/1000/gnupg/d.<hash>/), so the kill must name the same--homedir.
Notes:
Risk 1 confirmed: kill is path-keyed; helper must run kill before delete and pass the explicit home.
In this plain shell, 2.2.27 agents exited on their own when the home was deleted, so the reported leak is likely environment-dependent (e.g. missing
XDG_RUNTIME_DIR, or PHP-spawned processes). Explicit kill is correct either way; Stage 5 measures the real suite.
Stage 2 - Shared cleanup helper
Changes:
New
ForumRewrite\Security\GpgHomeCleaner::dispose(string $home): void: runsgpgconf --homedir <home> --kill all(10s timeout, failures ignored), then deletes the tree recursively.New
tests/GpgHomeCleanerTest.php, registered intests/run.php.
Verification:
Passed:
php tests/run.php GpgHomeCleanerTest OpenPgpKeyInspectorTest(8 run, 8 passed);gpg-agentcount 0 afterwards.Not yet run: missing-
gpgconf(stripped PATH) case beyond the swallowed-failure design; covered by@execplus ignored exit status.
Notes:
The recursive delete also fixes the inspector's old
glob('/*')cleanup, which skipped dotfiles and nested dirs such asprivate-keys-v1.d, leaving temp homes behind.
Stage 3 - Inspector and verifier use the helper
Changes:
OpenPgpKeyInspector::cleanupandOpenPgpSignatureVerifier::cleanupnow delegate toGpgHomeCleaner::dispose(verifier's private recursive delete removed).Added
testInspectAndVerifyLeaveNoGpgAgentsBehindtotests/OpenPgpKeyInspectorTest.php(agent count and temp-home count do not grow; waits up to 2s for already-stopped agents to exit).
Verification:
Passed:
php tests/run.php OpenPgpKeyInspectorTest GpgHomeCleanerTest(9 run, 9 passed), three consecutive runs;gpg-agentcount 0 afterwards.Limitation: the new test also passes against the old code on this host, because GnuPG 2.2.27 here exits agents on its own once the home vanishes. It guards against regressions but does not reproduce the reported leak; Stage 5 must measure the real environment.
Notes:
gpgconf --killis asynchronous, so the test tolerates agents still exiting from earlier operations.
Stage 4 - Agent keypair generation uses the helper
Changes:
AgentIdentityService::generateKeypairnow ends withGpgHomeCleaner::dispose($home)in itsfinallyblock, after both exports; the privatedeleteTreewas removed (no other caller).AgentIdentityServiceTestasserts noforum-rewrite-agent-gpg-*temp home remains after bootstrap.
Verification:
Passed: temp-home assertion; the second test in the file;
gpg-agentcount 0 before and after.Failed, pre-existing:
testReplyAgentBootstrapCreatesApprovedIdentityAndPrivateKeyOutsidePublicArtifactsfails at the laterVisible username:</strong> reply-agentprofile assertion. It fails identically on unmodifiedmain(checked), so it is unrelated to this change and not fixed here.
Notes:
Logged as separate work; scope not expanded.
Stage 5 - Acceptance on this host and recovery
Changes:
Found the main leak source with the first full-suite run: test helpers, not app code. All 44 leftover agents were
gpg-agent --homedir /tmp/forum-rewrite-approval-gpg-*, started byWriteApiSmokeTest::createSigningKey. The 1,325 homes already in/tmpat the start had the same prefix.WriteApiSmokeTest:deleteTreeroutesforum-rewrite-approval-gpg-*homes throughGpgHomeCleaner::dispose;createSigningKeyregisters a shutdown dispose, because many tests never delete their signing home;generatePublicKeyand the time-conflict retry usedispose.LocalAppSmokeTest::generatePublicKeyusesdispose.This is within the Stage 5 mitigation ("grep for remaining gpg call sites and extend").
Verification:
Full suite before the test-helper fix: agents 0 → 44, temp homes 1,325 → 1,369 (leak reproduced).
Full suite after: agents 0 → 0, temp homes 0 → 0 (passed).
php tests/run.php: 1335 run, 1329 passed, 6 failed. All 6 also fail on unmodifiedmain(checked in a temporary worktree), so they are not caused by this change:ApprovalSeedCommandTest::testApprovalIsEffectiveForExistingAuthenticatedMemberOnNextRequest,AgentIdentityServiceTest::testReplyAgentBootstrapCreatesApprovedIdentityAndPrivateKeyOutsidePublicArtifacts,LocalAppSmokeTest::testSqliteViewerIncludesPresetReadOnlyQueryContract,ReadModelBuilderTimingTest::testProgressSeparatesParsingDerivationAndWritingWithBoundedCounts,WriteApiSmokeTest::testPreparedFeatureFlagChangeVerifiesSignatureAndCommitsActionEvidence,WriteApiSmokeTest::testOperatorSecurityReviewRequiresAuthenticatedRootAndCorrelatesPolicyChanges.A first attempt at the
deleteTreechange failed 17 more tests because it kept iterating afterdisposehad removed the directory; it now returns early. Final run above.
Notes:
Recovery for already-leaked processes (done once on this host during the stage):
pgrep -f 'gpg-agent --homedir /tmp/forum-rewrite-' | xargs -r kill; rm -rf /tmp/forum-rewrite-*gpg*. Do not usepkill -fwith that pattern from a shell whose own command line contains it, since it kills that shell.During verification, every
gpg-agenton the host was stopped (pgrep -x gpg-agent | xargs kill); a personal agent restarts on demand.