From 94bfcf91c8d0c6acaa37ce8db8492cbd68e16358 Mon Sep 17 00:00:00 2001
From: x
Date: Mon, 13 Apr 2026 17:20:42 +0200
Subject: [PATCH 2/2] fix: do not erase uncolored packages from different arch
 families on upgrade
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

addSelfErasures() uses skipColor() to decide whether an installed
package should be erased when a same-name package is upgraded.
skipColor() only distinguishes packages when both have a non-zero
HEADERCOLOR (i.e. contain ELF objects).  Packages that carry no ELF
(typical for -devel sub-packages: headers, symlinks, pkg-config and
libtool files) all have HEADERCOLOR=0, so skipColor() lets the erasure
through regardless of architecture.

On a multilib system with three or more arch families, this causes
cross-family breakage.  For example, with i686 (archcolor 1), x86_64
(archcolor 2), and x32 (archcolor 4) all present, installing
xz-devel.i686 silently erases the already-installed xz-devel.x32 even
though they ship to different paths (/usr/lib vs /usr/libx32) and are
not interchangeable — breaking reverse dependencies like
libmagic-devel.x32 that require libtool(/usr/libx32/liblzma.la).

When both the new and installed package are uncolored, 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) — correct
 - Cross-family multilib: x86_64 (ac=2) coexists with i686 (ac=1)
 - noarch migrations: noarch (ac=0) is always replaceable

Note: checkAdded() and rpmtsTeIterator() use strict arch string
comparison for their respective purposes, but that would be too strict
here — it would prevent valid same-family upgrades (e.g. i586 -> i686)
where the arch strings differ but the archcolor is the same.

Depends on the preceding rpmrc.in commit that adds the missing
archcolor entries for i386-family sub-architectures, without which
rpmGetArchColor("i686") returns -1 and the check has no effect.
---
 lib/depends.cc                  | 30 ++++++++++-
 tests/data/SPECS/colortest.spec | 20 +++++++
 tests/rpmi.at                   | 92 +++++++++++++++++++++++++++++++++
 3 files changed, 141 insertions(+), 1 deletion(-)
 create mode 100644 tests/data/SPECS/colortest.spec

diff --git a/lib/depends.cc b/lib/depends.cc
index 844a4bcf7..80ba7c0a3 100644
--- a/lib/depends.cc
+++ b/lib/depends.cc
@@ -164,11 +164,39 @@ static int addSelfErasures(rpmts ts, rpm_color_t tscolor, int op,
     rpmdbMatchIterator mi = rpmtsInitIterator(ts, RPMDBI_NAME, rpmteN(p), 0);
     int rc = 0;
 
+    /* Pre-compute the new package's arch and archcolor -- they are
+     * constant across all iterations and rpmGetArchColor() acquires
+     * a mutex internally, so avoid calling it in the loop. */
+    const char *parch = rpmteA(p);
+    int pac = parch ? rpmGetArchColor(parch) : -1;
+
     while ((oh = rpmdbNextIterator(mi)) != NULL) {
 	/* Ignore colored packages not in our rainbow. */
-	if (skipColor(tscolor, hcolor, headerGetNumber(oh, RPMTAG_HEADERCOLOR)))
+	rpm_color_t ocolor = headerGetNumber(oh, RPMTAG_HEADERCOLOR);
+	if (skipColor(tscolor, hcolor, ocolor))
 	    continue;
 
+	/* When both the new and installed package are uncolored (no ELF,
+	 * e.g. -devel sub-packages), skipColor cannot tell them apart.
+	 * Compare their architectures' assigned colors instead, so that
+	 * packages from genuinely different arch families (e.g. x32 vs
+	 * i686) are not accidentally erased by each other.  Skip only
+	 * when both archcolors are positive (non-zero) and differ --
+	 * archcolor 0 means noarch which is always replaceable.
+	 *
+	 * Note: checkAdded() and rpmtsTeIterator() use strict arch string
+	 * comparison, but that would be too strict here -- it would
+	 * prevent valid same-family upgrades (e.g. i586 -> i686) where
+	 * the arch strings differ but the archcolor is the same. */
+	if (tscolor && hcolor == 0 && ocolor == 0) {
+	    const char *oarch = headerGetString(oh, RPMTAG_ARCH);
+	    if (oarch) {
+		int oac = rpmGetArchColor(oarch);
+		if (pac > 0 && oac > 0 && pac != oac)
+		    continue;
+	    }
+	}
+
 	/* On reinstall, skip packages with differing NEVRA. */
 	if (op != RPMTE_UPGRADE) {
 	    char * ohNEVRA = headerGetAsString(oh, RPMTAG_NEVRA);
diff --git a/tests/data/SPECS/colortest.spec b/tests/data/SPECS/colortest.spec
new file mode 100644
index 000000000..645bf05d0
--- /dev/null
+++ b/tests/data/SPECS/colortest.spec
@@ -0,0 +1,20 @@
+%{?!ver:%define ver 1.0}
+
+Name:		colortest
+Version:	%{ver}
+Release:	1
+Summary:	Testing uncolored multilib upgrade behavior
+
+Group:		Testing
+License:	GPL
+
+%description
+%{summary}
+
+%install
+mkdir -p %{buildroot}/opt/colortest/
+echo "%{_arch}" > %{buildroot}/opt/colortest/%{_arch}
+
+%files
+%defattr(-,root,root,-)
+/opt/colortest/
diff --git a/tests/rpmi.at b/tests/rpmi.at
index 691fc6393..899c5c3f4 100644
--- a/tests/rpmi.at
+++ b/tests/rpmi.at
@@ -2083,3 +2083,95 @@ runroot rpm -U --nodb --test --ignorearch --ignoreos --nodeps --nosignature \
 [	installing package hello-2.0-1.x86_64 needs 36KB more space on the / filesystem
 ])
 RPMTEST_CLEANUP
+
+# ------------------------------
+# Uncolored (color=0) packages from different arch families must not
+# erase each other on upgrade.  This is the typical case for -devel
+# sub-packages that carry no ELF and thus have HEADERCOLOR=0.
+# Regression test for the addSelfErasures() archcolor check.
+#
+# _transaction_color is a bitmask of archcolors the system supports:
+#   1 = i386 family (32-bit ELF), 2 = x86_64 (64-bit ELF).
+#   3 (1|2) = standard x86_64 multilib as used by Fedora/RHEL.
+RPMTEST_SETUP_RW([rpm -U uncolored multilib upgrade])
+AT_KEYWORDS([install upgrade multilib color])
+
+runroot rpmbuild --quiet -bb --define "ver 1.0" --target i686 \
+	/data/SPECS/colortest.spec
+runroot rpmbuild --quiet -bb --define "ver 1.0" --target x86_64 \
+	/data/SPECS/colortest.spec
+
+# Install i686 variant first
+RPMTEST_CHECK([
+RPMDB_RESET
+runroot rpm -U --ignoreos --ignorearch --nodeps \
+	--define "_transaction_color 3" \
+	/build/RPMS/i686/colortest-1.0-1.i686.rpm
+runroot rpm -q colortest
+],
+[0],
+[colortest-1.0-1.i686
+],
+[])
+
+# Now upgrade with x86_64 variant -- both should coexist
+RPMTEST_CHECK([
+runroot rpm -U --ignoreos --ignorearch --nodeps \
+	--define "_transaction_color 3" \
+	/build/RPMS/x86_64/colortest-1.0-1.x86_64.rpm
+runroot rpm -q colortest | sort
+],
+[0],
+[colortest-1.0-1.i686
+colortest-1.0-1.x86_64
+],
+[])
+RPMTEST_CLEANUP
+
+# ------------------------------
+# Three-way multilib: systems that carry a third arch family (e.g. x32,
+# archcolor=4) use _transaction_color 7 (1|2|4).  Verify that all three
+# uncolored variants survive sequential upgrades.
+#
+# x32 is not in upstream rpmrc, so inject the necessary arch definitions
+# into the test environment to keep the test self-contained.
+RPMTEST_SETUP_RW([rpm -U uncolored three-way multilib upgrade])
+AT_KEYWORDS([install upgrade multilib color])
+
+cat << EOF > ${RPMTEST}/root/.config/rpm/rpmrc
+archcolor: x32 4
+arch_compat: x32: x32 noarch
+arch_canon: x32: x32 1
+buildarchtranslate: x32: x32
+buildarch_compat: x32: noarch
+EOF
+
+runroot rpmbuild --quiet -bb --define "ver 1.0" --target i686 \
+	/data/SPECS/colortest.spec
+runroot rpmbuild --quiet -bb --define "ver 1.0" --target x86_64 \
+	/data/SPECS/colortest.spec
+runroot rpmbuild --quiet -bb --define "ver 1.0" --target x32 \
+	/data/SPECS/colortest.spec
+
+RPMTEST_CHECK([
+RPMDB_RESET
+
+# Install all three sequentially -- none should erase the others
+runroot rpm -U --ignoreos --ignorearch --nodeps \
+	--define "_transaction_color 7" \
+	/build/RPMS/i686/colortest-1.0-1.i686.rpm
+runroot rpm -U --ignoreos --ignorearch --nodeps \
+	--define "_transaction_color 7" \
+	/build/RPMS/x86_64/colortest-1.0-1.x86_64.rpm
+runroot rpm -U --ignoreos --ignorearch --nodeps \
+	--define "_transaction_color 7" \
+	/build/RPMS/x32/colortest-1.0-1.x32.rpm
+runroot rpm -q colortest | sort
+],
+[0],
+[colortest-1.0-1.i686
+colortest-1.0-1.x32
+colortest-1.0-1.x86_64
+],
+[])
+RPMTEST_CLEANUP
-- 
2.53.0

