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). On an affected one — archlinux:latest — the same line reads 1 and the "not defined" message is replaced by the actual #define QT_USE_PROTECTED_VISIBILITY. Watch for the define, not the feature number: Ubuntu 24.04's Qt 6.4.2 prints 1 yet still never emits the define, so it's unaffected too.
Fedora builds Qt with that feature off, so nothing bad happens. Gentoo and Arch build Qt with it on. 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.