Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save costis-t/ae54d3021f32f0e8bc8ff9a88ff19494 to your computer and use it in GitHub Desktop.

Select an option

Save costis-t/ae54d3021f32f0e8bc8ff9a88ff19494 to your computer and use it in GitHub Desktop.
Repro + fix for lxqt_wallet protected-visibility link failure (lxqt_wallet#60 / sirikali#300)
--- a/frontend/lxqt_wallet.h 2024-11-01 20:52:06.000000000 +0100
+++ b/frontend/lxqt_wallet.h 2026-07-26 16:12:09.161971178 +0200
@@ -31,6 +31,13 @@
#ifndef LXQT_WALLET_INTERFACE_H
#define LXQT_WALLET_INTERFACE_H
+#include <QtGlobal>
+#if defined(LXQT_WALLET_BUILDING_LIBRARY)
+#define LXQT_WALLET_API Q_DECL_EXPORT
+#else
+#define LXQT_WALLET_API Q_DECL_IMPORT
+#endif
+
#include <QString>
#include <QStringList>
#include <QObject>
@@ -59,20 +66,20 @@
/*
* Check if there is a support for a backend and return true if the back end is supported.
*/
-Q_DECL_EXPORT bool backEndIsSupported(LXQt::Wallet::BackEnd);
+LXQT_WALLET_API bool backEndIsSupported(LXQt::Wallet::BackEnd);
/*
* Delete a wallet.
* KWallet backend does not use the applicationName argument.
*/
-Q_DECL_EXPORT bool deleteWallet(LXQt::Wallet::BackEnd,
+LXQT_WALLET_API bool deleteWallet(LXQt::Wallet::BackEnd,
const QString &walletName,
const QString &applicationName = QString());
/*
* Check if a particular wallet exists.
*/
-Q_DECL_EXPORT bool walletExists(LXQt::Wallet::BackEnd,
+LXQT_WALLET_API bool walletExists(LXQt::Wallet::BackEnd,
const QString &walletName,
const QString &applicationName = QString());
@@ -82,18 +89,18 @@
*
* nullptr is returned if there is no support for requested backend.
*/
-Q_DECL_EXPORT std::unique_ptr<LXQt::Wallet::Wallet> getWalletBackend(LXQt::Wallet::BackEnd);
+LXQT_WALLET_API std::unique_ptr<LXQt::Wallet::Wallet> getWalletBackend(LXQt::Wallet::BackEnd);
/*
* Return a list of all wallets.
* Returned value is undefined if the backend is not supported.
*/
-Q_DECL_EXPORT QStringList walletList(LXQt::Wallet::BackEnd);
+LXQT_WALLET_API QStringList walletList(LXQt::Wallet::BackEnd);
/*
* Get a list of supported languages.
*/
-Q_DECL_EXPORT QStringList translations();
+LXQT_WALLET_API QStringList translations();
/*
* Set language of texts on GUI elements.
@@ -101,13 +108,13 @@
*
* Setting a language will make a difference only with an internal backend.
*/
-Q_DECL_EXPORT void setTranslationLanguage(const QString &language);
+LXQT_WALLET_API void setTranslationLanguage(const QString &language);
/*
* Below class is the interface that implements various backends.
* See example at the end of this header file to see an example of how to use the interface.
*/
-class Q_DECL_EXPORT Wallet : public QWidget
+class LXQT_WALLET_API Wallet : public QWidget
{
public:
Wallet();
--- a/frontend/CMakeLists.txt 2024-11-01 20:52:06.000000000 +0100
+++ b/frontend/CMakeLists.txt 2026-07-26 16:12:09.162039869 +0200
@@ -132,6 +132,7 @@
endif()
set_target_properties(lxqt-wallet PROPERTIES COMPILE_FLAGS "-D_FILE_OFFSET_BITS=64 -Wextra -Wall -s -fPIC -pedantic ")
+target_compile_definitions(lxqt-wallet PRIVATE LXQT_WALLET_BUILDING_LIBRARY)
set_target_properties(lxqt-wallet PROPERTIES SOVERSION "${LIBRARY_VERSION}")
set_target_properties(lxqt-wallet PROPERTIES OUTPUT_NAME lxqt-wallet)
#!/bin/bash
# Reproduces the lxqt_wallet "protected symbol ... isn't defined" link failure
# (https://github.com/lxqt/lxqt_wallet/issues/60, https://github.com/mhogomchungu/sirikali/issues/300)
# on ANY distro, and demonstrates the header-only fix.
#
# Usage (no setup needed beyond docker):
# curl -fsSL <RAW_GIST_URL> | docker run --rm -i fedora:44 bash # unaffected distro
# curl -fsSL <RAW_GIST_URL> | docker run --rm -i archlinux:latest bash # affected distro
# On an UNAFFECTED distro (Fedora, Debian, ...) step 3 links fine and the failure
# appears in step 4, where the script defines QT_USE_PROTECTED_VISIBILITY itself
# (simulating Gentoo/Arch Qt6). On an AFFECTED distro (Arch, Gentoo) step 3
# already fails with the distro's stock Qt config -- that is the bug, live.
# Also runs directly (bash lxqt-wallet-repro.sh) on any box with g++, cmake, git,
# diffutils and Qt6 base+tools devel packages installed.
#
# What it shows, in order:
# 1. an explicit visibility attribute always beats -fvisibility= (why PR #63's
# CXX_VISIBILITY_PRESET approach cannot work)
# 2. where Q_DECL_EXPORT gets its meaning (QT_USE_PROTECTED_VISIBILITY)
# 3. pristine 4.0.2 with your distro's Qt config as-is
# 4. same source with QT_USE_PROTECTED_VISIBILITY defined explicitly
# 5. the export/import header split under the same macro: link WORKS again
set -e
echo "##### 0. dependencies"
if command -v dnf >/dev/null; then
# RHEL clones (Rocky, AlmaLinux) ship Qt6 devel in EPEL + CRB, not in their base repos
if ! dnf list qt6-qtbase-devel >/dev/null 2>&1; then
dnf install -y epel-release dnf-plugins-core
dnf config-manager --set-enabled crb
fi
rpm -q gcc-c++ cmake git diffutils qt6-qtbase-devel qt6-qttools-devel libgcrypt-devel >/dev/null 2>&1 || \
dnf install -y --setopt=install_weak_deps=False \
gcc-c++ cmake make git binutils diffutils pkgconf-pkg-config \
qt6-qtbase-devel qt6-qttools-devel libgcrypt-devel
elif command -v pacman >/dev/null; then
pacman -Syu --noconfirm --needed \
gcc make cmake git binutils diffutils pkgconf \
qt6-base qt6-tools libgcrypt
elif command -v apt-get >/dev/null; then
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
g++ make cmake git binutils diffutils pkg-config ca-certificates \
qt6-base-dev qt6-tools-dev qt6-tools-dev-tools libgcrypt20-dev
elif command -v zypper >/dev/null; then
zypper --non-interactive install --no-recommends \
gcc-c++ make cmake git binutils diffutils pkgconf-pkg-config \
qt6-base-devel qt6-tools-devel qt6-linguist-devel libgcrypt-devel
fi
WORK=$(mktemp -d /tmp/lxqt-wallet-repro.XXXXXX); cd "$WORK"
QTFLAGS="$(pkg-config --cflags Qt6Widgets Qt6Core)"
QTLIBS="$(pkg-config --libs Qt6Widgets Qt6Core)"
# Qt headers live in /usr/include/qt6 on most distros, but not all
# (Debian/Ubuntu: /usr/include/<arch-triplet>/qt6) -- ask pkg-config
QTINC="$(pkg-config --variable=includedir Qt6Core 2>/dev/null)"
[ -d "$QTINC/QtCore" ] || QTINC=/usr/include/qt6
echo
echo "##### 1. explicit visibility attribute beats -fvisibility= (core mechanism)"
cat > vis.cpp <<'EOF'
__attribute__((visibility("protected"))) int g(int x) { return x + 1; }
EOF
g++ -fPIC -shared -fvisibility=default vis.cpp -o vis.so
readelf --dyn-syms -W vis.so | grep _Z1gi
echo "^^ still PROTECTED although -fvisibility=default was passed"
echo
echo "##### 2. what Q_DECL_EXPORT means depends on QT_USE_PROTECTED_VISIBILITY"
grep -n -B1 -A4 'ifdef QT_USE_PROTECTED_VISIBILITY' "$QTINC"/QtCore/qcompilerdetection.h | head -8
echo "-- your distro's setting of the controlling feature:"
grep -rn 'QT_FEATURE_reduce_relocations' "$QTINC"/QtCore/qconfig*.h | head -2
echo "-- is this distro affected? (does its Qt define the macro?)"
grep -rn '#define QT_USE_PROTECTED_VISIBILITY' "$QTINC"/QtCore/qconfig*.h \
|| echo " not defined -> this distro is unaffected; step 4 simulates an affected one"
echo
echo "##### fetching lxqt_wallet 4.0.2 (pristine)"
git clone -q --depth 1 --branch 4.0.2 https://github.com/lxqt/lxqt_wallet
cat > consumer.cpp <<'EOF'
#include "lxqt_wallet.h"
int main() { return LXQt::Wallet::backEndIsSupported(LXQt::Wallet::BackEnd::internal) ? 0 : 1; }
EOF
build_and_link() {
local srcdir=$1 extra=$2 label=$3 B="$WORK/build-$3"
cmake -S "$WORK/$srcdir" -B "$B" -DCMAKE_BUILD_TYPE=Release \
-DNOSECRETSUPPORT=true -DNOKDESUPPORT=true \
-DCMAKE_CXX_FLAGS="$extra" >/dev/null
make -C "$B" -j"$(nproc)" >/dev/null
echo "--- [$label] PROTECTED symbols in liblxqt-wallet.so.6.0.0:"
readelf --dyn-syms -W "$B"/frontend/liblxqt-wallet.so.6.0.0 | grep -cw PROTECTED || true
readelf --dyn-syms -W "$B"/frontend/liblxqt-wallet.so.6.0.0 | grep walletExists | head -1
echo "--- [$label] linking a consumer of the public header:"
if g++ -std=c++17 $extra $QTFLAGS -I"$WORK/$srcdir/frontend" consumer.cpp \
-L"$B/frontend" -llxqt-wallet $QTLIBS -o "$WORK/consumer-$label" 2>"$WORK/err-$label"; then
echo " LINK OK"
else
echo " LINK FAILED:"
grep -E 'protected symbol|bad value|undefined reference' "$WORK/err-$label" | sed 's/^/ /' | head -4
fi
}
echo
echo "##### 3. pristine 4.0.2, your distro's Qt config as-is (affected distros fail HERE already)"
build_and_link lxqt_wallet "" stock
echo
echo "##### 4. pristine 4.0.2 + QT_USE_PROTECTED_VISIBILITY defined (what Gentoo/Arch Qt6 do implicitly)"
build_and_link lxqt_wallet "-DQT_USE_PROTECTED_VISIBILITY=" gentoo-sim
echo
echo "##### 5. export/import header split, same hostile macro -> fixed"
cp -r lxqt_wallet lxqt_wallet-fixed
# swap the public API to a dedicated macro... (must run BEFORE inserting the block below)
sed -i 's/\bQ_DECL_EXPORT\b/LXQT_WALLET_API/g' lxqt_wallet-fixed/frontend/lxqt_wallet.h
# ...define it: export only while building the library, import for consumers
sed -i 's|^#define LXQT_WALLET_INTERFACE_H$|#define LXQT_WALLET_INTERFACE_H\n\n#include <QtGlobal>\n#if defined(LXQT_WALLET_BUILDING_LIBRARY)\n#define LXQT_WALLET_API Q_DECL_EXPORT\n#else\n#define LXQT_WALLET_API Q_DECL_IMPORT\n#endif|' \
lxqt_wallet-fixed/frontend/lxqt_wallet.h
sed -i 's|^set_target_properties(lxqt-wallet PROPERTIES SOVERSION|target_compile_definitions(lxqt-wallet PRIVATE LXQT_WALLET_BUILDING_LIBRARY)\nset_target_properties(lxqt-wallet PROPERTIES SOVERSION|' \
lxqt_wallet-fixed/frontend/CMakeLists.txt
build_and_link lxqt_wallet-fixed "-DQT_USE_PROTECTED_VISIBILITY=" fixed
echo
echo "##### 5b. ABI check: the fix only changes what CONSUMERS see, not the library"
readelf --dyn-syms -W "$WORK"/build-gentoo-sim/frontend/liblxqt-wallet.so.6.0.0 | awk '{$1="";$2=""; print}' | sort > "$WORK"/dynsym-gentoo-sim
readelf --dyn-syms -W "$WORK"/build-fixed/frontend/liblxqt-wallet.so.6.0.0 | awk '{$1="";$2=""; print}' | sort > "$WORK"/dynsym-fixed
if diff -q "$WORK"/dynsym-gentoo-sim "$WORK"/dynsym-fixed >/dev/null; then
echo "dynamic symbol tables identical (names, sizes, bindings, visibility) -> exported ABI unchanged"
else
echo "dynamic symbol tables DIFFER:"; diff "$WORK"/dynsym-gentoo-sim "$WORK"/dynsym-fixed | head
fi
sha256sum "$WORK"/build-gentoo-sim/frontend/liblxqt-wallet.so.6.0.0 \
"$WORK"/build-fixed/frontend/liblxqt-wallet.so.6.0.0
if cmp -s "$WORK"/build-gentoo-sim/frontend/liblxqt-wallet.so.6.0.0 \
"$WORK"/build-fixed/frontend/liblxqt-wallet.so.6.0.0; then
echo "-> bit-identical libraries"
else
echo "-> library bytes differ in this two-tree build (embedded source paths differ);"
echo " the dynamic-symbol comparison above is the meaningful ABI check"
fi
echo
echo "##### summary"
echo "stock : distro default -> unaffected distros (Fedora, Debian, ...): LINK OK, 0 PROTECTED"
echo " affected distros (Arch, Gentoo): 17 PROTECTED, LINK FAILED"
echo "gentoo-sim : macro defined, no fix -> 17 PROTECTED, LINK FAILED (protected symbol ... isn't defined)"
echo "fixed : macro defined + header fix-> LINK OK (library exports unchanged; consumers see Q_DECL_IMPORT)"

What the reproduction script shows, act by act

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 distro

The 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
Loading

Setting the macro breaks the build. Adding the fix repairs it. That's the whole argument.


Act 1 — a flag can't beat the source

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.

Act 2 — where "protected" comes from

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.)

Acts 3–5 — the experiment

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 failsprotected 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
Loading

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.

Act 5b — the receipt

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.

Bottom line

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.

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