Companion to lxqt-wallet-reproduction.sh in this gist. Run it with nothing but docker — grab the script's raw url from its Raw button above, then:
curl -fsSL <raw url of lxqt-wallet-reproduction.sh> | docker run --rm -i fedora:44 bash # unaffected distro
curl -fsSL <raw url of lxqt-wallet-reproduction.sh> | docker run --rm -i archlinux:latest bash # affected distroThe script also runs cleanly on debian:13, ubuntu:24.04, opensuse/tumbleweed, rockylinux:9 and almalinux:9 — all unaffected, same flow as Fedora: the pristine build links fine, and the link failure appears only in act 4, where the script triggers it deliberately by defining the macro. That act-4 failure comes out identical on Qt 6.4.2, 6.6.2, 6.8.2 and 6.11.1, so this is not a recent Qt regression.
Context: lxqt/lxqt_wallet#60 / mhogomchungu/sirikali#300 — on some distros (Gentoo, Arch) any program linking against the system liblxqt-wallet.so dies with ld: protected symbol ... isn't defined / final link failed: bad value, while on others (Fedora, Debian, ...) the same code links fine.
The script warms up with two small demonstrations, then runs one experiment: build the same library three times, changing one thing each time. (The Complete! near the top is just the package install finishing — ignore it.)
flowchart TD
S0["Act 0 — setup<br/>install compiler and Qt<br/>(ends with 'Complete!')"]
S1["Act 1 — the rule<br/>an attribute in the source beats<br/>a -fvisibility flag on the command line"]
S2["Act 2 — the trigger<br/>in your Qt headers: Q_DECL_EXPORT<br/>is 'protected' only if a macro is set"]
S0 --> S1 --> S2 --> S3
subgraph exp["The experiment: same library, three builds"]
S3["Act 3 — stock<br/>your distro's Qt config, untouched"]
S4["Act 4 — gentoo-sim<br/>+ set the macro<br/>(what Gentoo/Arch Qt does)"]
S5["Act 5 — fixed<br/>macro still on<br/>+ the header fix"]
R3(["unaffected distro: links fine<br/>affected distro: FAILS right here"])
R4(["link FAILS — the #300 bug"])
R5(["links fine — library unchanged"])
S3 --> R3
S4 --> R4
S5 --> R5
S3 -.->|"set the macro"| S4
S4 -.->|"add the fix"| S5
end
R5 --> S5b["Act 5b — the receipt<br/>same checksum, same symbols<br/>→ the fix has zero ABI risk"]
style R4 fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
style R5 fill:#dcfce7,stroke:#16a34a,color:#14532d
style S5b fill:#dcfce7,stroke:#16a34a,color:#14532d
Setting the macro breaks the build. Adding the fix repairs it. That's the whole argument.
5: ... FUNC GLOBAL PROTECTED 4 _Z1gi
^^ still PROTECTED although -fvisibility=default was passed
A tiny test function is marked "protected" in its source code, then compiled with a flag saying "make everything default". The readelf line shows the source won: still PROTECTED.
This is why adding CXX_VISIBILITY_PRESET default (lxqt/lxqt_wallet#63) can't change anything — it only adds that flag, and the flag loses.
195:# ifdef QT_USE_PROTECTED_VISIBILITY
196:# define Q_DECL_EXPORT __attribute__((visibility("protected")))
Straight from the container's own Qt headers: the Qt macro Q_DECL_EXPORT — which lxqt_wallet.h puts on its whole public API — means "protected" only when QT_USE_PROTECTED_VISIBILITY is set. Then the script checks the machine it's running on:
#define QT_FEATURE_reduce_relocations -1
not defined -> this distro is unaffected
That's what an unaffected distro looks like (Fedora above; Debian, Rocky and Alma print the same -1), so nothing bad happens there. On an affected one — archlinux:latest, and Gentoo — the same line reads 1 and the "not defined" message is replaced by the real thing: #define QT_USE_PROTECTED_VISIBILITY. Watch for that define rather than the feature number: Ubuntu 24.04's Qt 6.4.2 prints 1 yet never emits the define, so it's unaffected too.
That single packaging choice is the entire difference between "works for me" and "broken" — and why this bug looked unreproducible for so long.
(The git warnings that follow — "detached HEAD" and friends — are just noise from cloning a release tag. Harmless.)
| act | macro | fix | protected symbols | link |
|---|---|---|---|---|
| 3 stock | your distro's setting | – | 0 (Fedora) / 17 (Arch) | works (Fedora) / fails (Arch) |
| 4 gentoo-sim | forced on | no | 17 | fails — protected symbol ... isn't defined |
| 5 fixed | forced on | yes | 17 | works |
The surprise is act 5: the library still has 17 protected symbols, yet linking works. Because the problem was never the library — it was what the header told the program using it:
flowchart LR
subgraph broken["Act 4 — broken"]
C1["consumer program<br/>(the script's consumer.cpp;<br/>sirikali in real life)<br/>asks for the symbol<br/>with a 'protected' label"] -->|"linker: a protected request must be<br/>answered from the requester's OWN file —<br/>but the answer is in the library ✗"| L1["liblxqt-wallet.so"]
end
subgraph fixedg["Act 5 — fixed"]
C2["consumer program<br/>asks for the symbol<br/>the normal way"] -->|"normal request,<br/>answered fine ✓"| L2["liblxqt-wallet.so<br/>(unchanged!)"]
end
style broken fill:#fef2f2,stroke:#dc2626,color:#7f1d1d
style fixedg fill:#f0fdf4,stroke:#16a34a,color:#14532d
style C1 fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
style L1 fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
style C2 fill:#dcfce7,stroke:#16a34a,color:#14532d
style L2 fill:#dcfce7,stroke:#16a34a,color:#14532d
lxqt_wallet.h uses Q_DECL_EXPORT unconditionally, so programs including the header also get "protected" stamped on their references — and the linker rejects a protected reference that points outside its own file (bad value). The fix is the standard Qt library pattern: Q_DECL_EXPORT while building the library, Q_DECL_IMPORT for everyone else. Protected symbols in a library are fine by themselves — Qt's own core library ships ~7500 of them and everything links.
6f51e1e0... build-gentoo-sim/.../liblxqt-wallet.so.6.0.0
6f51e1e0... build-fixed/.../liblxqt-wallet.so.6.0.0
-> bit-identical libraries
(Your two digests will be some other value — and a different one on each distro. The point is only that the two lines match each other.)
The broken-era and fixed-era libraries have the same checksum — the fix compiles to the exact same bytes. It only changes the header that other programs read. No risk to anything already built.
Set the macro → the bug appears. Add the fix (lxqt-wallet-4.0.2-export-import-split.patch, also in this gist — applies clean on the 4.0.2 tag and current master) → the bug is gone, and the library never changed.
AI-assisted and reviewed by me, but I'm not that experienced.