Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

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

Select an option

Save costis-t/5d7f46f2b9c61d97b5ab5723b3850cd9 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). 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.)

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