Bug 46
Python IPC test aborts when libmultiprocess is also installed system-wide
- Status
- Fixed
- Symptom
- Crash; worst effect seen: crash
- Where seen
- Bitcoin Core functional tests
- Trigger
- Normal use; no special event is needed
- First reported
- 2025-09-17 (first report)
- Fixed
- 2025-09-18
Summary
The interface_ipc.py functional test aborted at startup with mp/proxy.capnp:0: failed: Duplicate ID @0xcc316e3f71a040fb on machines where libmultiprocess was installed system-wide in the same prefix as Cap'n Proto, because the test's schema import path listed the system include directory before the in-tree one. The test then loaded two copies of the same schema file. The defect was in test code: only the Python test process aborted, no node was involved, and users were not affected. Fixed by bitcoin#33420 ("test: Avoid interface_ipc.py Duplicate ID errors").
What happened
@zaidmstrr ran the new interface_ipc.py on a machine with libmultiprocess installed system-wide, and the test process died right after "Initializing test directory" with terminate called after throwing an instance of 'kj::ExceptionImpl' what(): mp/proxy.capnp:0: failed: Duplicate ID @0xcc316e3f71a040fb. Aborted (core dumped) (bitcoin#33417, 2025-09-17). Removing the system installation made the test pass. ryanofsky reproduced it by pointing the test at an external installation. CI never hit it, since CI machines have no system libmultiprocess.
Root cause
load_capnp_modules in test/functional/interface_ipc.py loads the in-tree mp/proxy.capnp directly with capnp.load(...) and then loads init.capnp and the other Bitcoin Core schemas, whose import "/mp/proxy.capnp" lines are resolved through an imports list. That list was [capnp_dir, src_dir, mp_dir], where capnp_dir is the include directory next to the capnp binary on PATH (for example /usr/local/bin/capnp gives /usr/local/include). If libmultiprocess was installed in that same prefix, the absolute import resolved to the installed proxy.capnp while the direct load had used the in-tree copy. pycapnp's schema loader saw two different files declaring the same file ID and threw, and the uncaught kj exception terminated the interpreter.
Both packages have to share an install prefix, as ryanofsky explained on the fix PR after reviewers who installed only libmultiprocess system-wide could not reproduce it.
The code
Quoted from e9c52272ebd7^. load_capnp_modules in test/functional/interface_ipc.py loads the in-tree mp/proxy.capnp by path, then loads the Bitcoin Core schemas, which import "/mp/proxy.capnp" through the imports search list:
if capnp_bin := shutil.which("capnp"):
# Add the system cap'nproto path so include/capnp/c++.capnp can be found.
capnp_dir = Path(capnp_bin).parent.parent / "include"
# ...
src_dir = Path(self.config['environment']['SRCDIR']) / "src"
mp_dir = src_dir / "ipc" / "libmultiprocess" / "include"
imports = [str(capnp_dir), str(src_dir), str(mp_dir)]
return {
"proxy": capnp.load(str(mp_dir / "mp" / "proxy.capnp"), imports=imports),
"init": capnp.load(str(src_dir / "ipc" / "capnp" / "init.capnp"), imports=imports),
The search list puts the system include directory first. When libmultiprocess is installed under the same prefix as the capnp binary (for example /usr/local/include/mp/proxy.capnp), the import in init.capnp finds that copy, while the "proxy" entry has already loaded the in-tree copy. pycapnp then has two different files declaring file ID @0xcc316e3f71a040fb, throws "Duplicate ID", and the uncaught kj exception aborts the Python test process.
The fix searches the in-tree directory first (e9c52272ebd7):
@@ -35,7 +35,12 @@ class IPCInterfaceTest(BitcoinTestFramework):
mp_dir = src_dir / "ipc" / "libmultiprocess" / "include"
- imports = [str(capnp_dir), str(src_dir), str(mp_dir)]
+ # List of import directories. Note: it is important for mp_dir to be
+ # listed first, in case there are other libmultiprocess installations on
+ # the system, to ensure that `import "/mp/proxy.capnp"` lines load the
+ # same file as capnp.load() loads directly below, and there are not
+ # "failed: Duplicate ID @0xcc316e3f71a040fb" errors.
+ imports = [str(mp_dir), str(capnp_dir), str(src_dir)]
With mp_dir first, import "/mp/proxy.capnp" resolves to the same in-tree file that was loaded directly, so the schema loader sees one file and one ID.
Fix
Commit e9c52272ebd7 reorders the list to [mp_dir, capnp_dir, src_dir], with a comment explaining that the in-tree directory must come first so the imports load the same file as the direct capnp.load() (+6/-1).
Timeline
- 2025-09-05: introduced by commit 8d2ee88fa2a5 ("tests: add functional tests for IPC interface"), merged in bitcoin#33201, which added
interface_ipc.py. - 2025-09-17: bitcoin#33417 reported by zaidmstrr.
- 2025-09-18: fix merged in bitcoin#33420.
- Test code only; no release or user affected.
Lessons
What would have prevented this bug, found it sooner, or made it easier to debug. The patterns page groups these lessons across all bugs.
Pattern CI, toolchain and platform: An installed copy found before the tree under test
- Prevented by a design rule
- Earliest detector code review (existed then)
- Diagnosis easy
- Fix design easy
- Debugging aids a clear assertion message, a reproduction script
- Rust no
Would Rust have prevented it? No. Schema import search-list ordering is the same in capnpc-rust or any other loader; the shadowed copy is a configuration bug the language does not see. More on Rust
Could it have been prevented?
Yes, by one rule the fix comment now states: when a file is loaded by explicit path and
then reached again through a search list, the directory of that file goes first in the
list, so both routes resolve to the same copy. The test loaded the in-tree proxy.capnp
directly but put the system include directory ahead of the tree in imports, so the
import "/mp/proxy.capnp" lines in the other schemas could find a second copy. Stronger
still would be one resolution route only (load every schema through the same search
list), so there is nothing to keep consistent. This was knowable when the test was
written; it is the same shadowing problem as -I order in a C++ build.
Could it have been found sooner?
By review, which is the only check that sees a search list's order; a reviewer who
knows that -I-style lists are searched first to last would ask why the system
directory came before the tree. No runtime check in CI could have: Bitcoin Core's CI
builds the subtree and never installs libmultiprocess system-wide, and the condition is
narrower than that, since Cap'n Proto and libmultiprocess must share an install prefix,
which is why Sjors and fanquake
could not reproduce it
with split prefixes. A CI job that installs both into one prefix, the way a distribution
package would, would have failed on the first run, but no such job exists. The test was
two weeks old when the first developer with such a machine ran it, so the gap was short
anyway.
What made it hard to debug?
Nothing. The exception names the duplicated file ID and the schema, the reporter's own issue already pointed at the import list, ryanofsky reproduced it the same day by pointing the test at an external installation, and the fix was merged the next day. The only obstacle was reviewers failing to reproduce until the shared-prefix condition was spelled out.
Lesson
When the same file can be reached by two routes, an explicit path and a search list, order the search list so it finds the copy you loaded explicitly, or use one route for everything; a search list that starts with system directories silently prefers an installed copy over the tree being tested.
Classification
The survey's classification of this bug. The explorer charts these fields across all bugs; the method page explains them.
| Field | Value |
|---|---|
| Symptom | crash |
| Worst effect observed | crash |
| Seen in | functional-test |
| Scope | test-code |
| Trigger | Normal use (ordinary-path) |
| Phase | startup |
| Side | n/a |
| Component | test-harness |
| Root cause kind | assumption-violated |
| Fix kind | test-only |
| Detection | user-report |
| Needs a peer that is not libmultiprocess C++ | no |
| Needed a sanitizer to see | no |
| Confidence | low |