When two packages share the same name but differ in architecture, and neither contains ELF binaries (HEADERCOLOR=0 — typical for -devel sub-packages), rpm -U of one architecture silently erases the other. Colored packages (with ELF) coexist correctly; uncolored ones do not.
Build a trivial package for two architectures. The package contains only a text file — no ELF, so HEADERCOLOR=0:
$ cat colortest.spec
Name: colortest
Version: 1.0
Release: 1
Summary: Uncolored multilib test
License: GPL
%description
%{summary}
%install
mkdir -p %{buildroot}/opt/colortest/
echo "%{_arch}" > %{buildroot}/opt/colortest/%{_arch}
%files
/opt/colortest/
$ rpmbuild -bb --target i686 colortest.spec
$ rpmbuild -bb --target x86_64 colortest.spec
Install i686 first, then upgrade with x86_64. On a real Fedora/RHEL x86_64 system, multilib is enabled by default — _transaction_color is set to 3 via the platform macros (/usr/lib/rpm/platform/x86_64-linux/macros, generated from RPMCANONCOLOR in platform.in:23):
$ rpm --eval '%_transaction_color'
3
So the reproducer is simply:
# rpm -U colortest-1.0-1.i686.rpm
# rpm -q colortest
colortest-1.0-1.i686
# rpm -U colortest-1.0-1.x86_64.rpm
# rpm -q colortest
colortest-1.0-1.x86_64 ← i686 silently erased
Expected: both architectures installed, as happens with ELF-carrying packages.
For comparison, the pre-built hello test packages (which contain ELF) coexist correctly when installed the same way:
# rpm -U hello-2.0-1.i686.rpm
# rpm -U hello-2.0-1.x86_64.rpm
# rpm -q hello
hello-2.0-1.i686
hello-2.0-1.x86_64 ← both present
In rpm’s test suite or a minimal chroot, platform macros may not be present, so _transaction_color defaults to 0 (multilib disabled). To reproduce the bug in such environments, pass it explicitly:
rpm -U --define "_transaction_color 3" colortest-1.0-1.i686.rpm
rpm -U --define "_transaction_color 3" colortest-1.0-1.x86_64.rpm
_transaction_color is a bitmask of archcolor values that tells rpm which ELF families can coexist: 1 = 32-bit (i386 family), 2 = 64-bit (x86_64), 3 (1|2) = both.
On Fedora/RHEL, dnf typically installs both arches in a single transaction, avoiding this code path. But rpm -U is a user-facing tool and should behave correctly on its own. The bug affects:
rpm -U usage by users or scriptsrpm --upgrade sequentiallyThis is the same underlying bug as rpm-software-management/rpm#2837 (“Strange behavior for multilib”), reported by @j-mracek (DNF maintainer) in January 2024. That issue demonstrates the same symptom: rpm -U B-2-2.i686.rpm erases B-2-2.x86_64 and B-1-1.x86_64 when package B contains no files (HEADERCOLOR=0).
@pmatilai bisected that issue to a regression introduced by commit 21836bc7 (“Use an erase element to delete packages with same NEVRA”), which landed between rpm 4.14 and 4.16. However, the regression and the root cause are two different things — see below.
The fundamental problem is in skipColor() (lib/depends.cc:154), which has been blind to uncolored cross-arch packages since e10b348c6 (2003).
addSelfErasures() (lib/depends.cc:160) iterates over all installed packages with the same name and uses skipColor() to decide which to erase:
// lib/depends.cc:169
if (skipColor(tscolor, hcolor, headerGetNumber(oh, RPMTAG_HEADERCOLOR)))
continue;skipColor() only skips when both packages have non-zero HEADERCOLOR and the colors don’t overlap:
// lib/depends.cc:156
return tscolor && color && ocolor && !(color & ocolor);When either HEADERCOLOR is 0, skipColor() returns 0 (don’t skip), and addSelfErasures() schedules the installed package for erasure with no further architecture check.
For colored packages this works: i686 ELF (color=1) and x86_64 ELF (color=2) don’t overlap, so skipColor keeps them separate. But -devel packages carry no ELF — both have HEADERCOLOR=0, so skipColor cannot distinguish them and falls through to erasure.
checkAdded() (lib/depends.cc:285) does have an architecture check (strict arch string comparison when tscolor is set), which is why same-transaction installs work. addSelfErasures() lacks the equivalent for installed-vs-new comparison.
Prior to 21836bc7, addSelfErasures() had a same-NEVR guard for upgrades:
cmp = rpmVersionCompare(h, oh);
/* On upgrade, skip packages that contain identical NEVR. */
if ((op == RPMTE_UPGRADE) && (cmp == 0))
continue;This partially masked the skipColor() blindness: same-version cross-arch uncolored packages survived because the cmp == 0 check skipped them before reaching removePackage(). After 21836bc7 removed this guard, same-version packages are also erased — which is the specific regression reported in #2837.
The skipColor() limitation existed since 2003, but prior to 4.16 it only caused problems when upgrading to a different version across architectures. After 21836bc7, it affects all versions including identical NEVR.
The fix addresses the fundamental skipColor() blindness rather than restoring the removed same-NEVR guard. This fixes both the pre-4.16 different-version case and the post-4.16 same-version regression (#2837), and does so without re-introducing the problems that 21836bc7 was trying to solve (broken --replacepkgs behavior with non-identical same-NEVR headers).
Two patches attached:
rpmGetArchColor() (lib/rpmrc.cc:1842) resolves an architecture name to its archcolor from the archcolor: table in rpmrc. The table only had an entry for the base i386 (color 1). Sub-architectures i486, i586, i686, athlon, geode, pentium3, and pentium4 were missing.
The INSTARCH table has hasTranslate=0, so lookupInDefaultTable() (lib/rpmrc.cc:351) cannot map “i686” to “i386” the way buildarchtranslate does for the BUILDARCH table. Without explicit entries, rpmGetArchColor("i686") returns -1.
Every other arch family already lists all sub-architectures: ARM has armv3l through armv8l, e2k has e2kv4/v5/v6/etc., x86_64 has x86_64_v2/v3/v4. The i386 family was the only one incomplete.
This also fixes a latent bug in rpmfcCheckPackageColor() (build/rpmfc.cc:1650): the build-time check that warns when ELF binaries don’t match the package architecture was silently skipped for all i386-family targets because arch_color <= 0 triggered an early return at build/rpmfc.cc:1666.
Includes a test verifying rpm --showrc reports archcolor: 1 for an i686 platform.
When both the new and installed package have HEADERCOLOR=0, compare rpmGetArchColor() of their architecture strings. Skip the erasure only when both archcolors are positive (non-zero, i.e. not noarch) and differ.
This preserves: - Same-arch upgrades: i686 (ac=1) replaces i586 (ac=1) — same family - Cross-family multilib: x86_64 (ac=2) coexists with i686 (ac=1) — different families - noarch → arch migrations: noarch has archcolor 0, so the check doesn’t apply
Strict arch string comparison (as checkAdded() at lib/depends.cc:285 and rpmtsTeIterator() at lib/depends.cc:132 use) would be too strict here — it would prevent valid same-family upgrades (e.g. i586 → i686).
Includes regression tests for two-way (i686 + x86_64, _transaction_color 3) and three-way multilib. The three-way test injects custom arch definitions via ~/.config/rpm/rpmrc and is self-contained.