From 3ee74c69daad17755ce33d1629ed65249f03dfb6 Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Wed, 29 Apr 2026 13:22:56 +0300 Subject: [PATCH] fix: don't conflate ZLIB_NG_* with ZLIB_* in symbol-version policy check `versioned_symbols_policy` was bucketing every version tag by splitting on the first underscore (`symbol.partition("_")`). For zlib-ng's `ZLIB_NG_2.0.0` / `ZLIB_NG_2.1.0` tags this produced namespace `ZLIB`, landing them in the same bucket as stock zlib's `ZLIB_1.2.0`. Policies whose `ZLIB` allowlist did not contain `ZLIB_NG_*` (none do, since libz-ng.so.2 is not a manylinux system library) then rejected the wheel as carrying "too-recent versioned symbols", even though no actual symbol was too recent. Match the namespace with a regex that requires a trailing numeric version (`\d+(?:[._]\d+)*`), so `ZLIB_NG_2.0.0` resolves to namespace `ZLIB_NG` instead of `ZLIB`. Tags whose suffix is not a numeric version (e.g. `GLIBC_PRIVATE`, or fully bare tokens with no `_`) fall back to the original first-underscore split so existing behavior is preserved. Because `ZLIB_NG` does not appear in any policy's `symbol_versions` map, `policy_is_satisfied`'s existing `set(required_vers) & set(policy_sym_vers)` intersection then drops it from the check entirely -- the same way any other unmodelled namespace already is -- and the wheel passes. The bucketing change also disambiguates `CXXABI_LDBL_*.*` from `CXXABI_*.*`, which had the same class of bug. Fixes #613. --- src/auditwheel/policy/__init__.py | 18 +++++++++++++++++- tests/unit/test_policy.py | 27 +++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/auditwheel/policy/__init__.py b/src/auditwheel/policy/__init__.py index 36f82b34..9d5af977 100644 --- a/src/auditwheel/policy/__init__.py +++ b/src/auditwheel/policy/__init__.py @@ -21,6 +21,13 @@ _HERE = Path(__file__).parent _MUSL_POLICY_RE = re.compile(r"^musllinux_\d+_\d+$") +# Match a versioned-symbol tag like "GLIBC_2.17", "ZLIB_NG_2.0.0", +# "CXXABI_LDBL_1.3" or "OPENSSL_1_1_0" and capture the namespace +# (everything before the trailing "_"). The version +# tail is required to be numeric (with `.` or `_` separators) so +# tags whose suffix is a word like "GLIBC_PRIVATE" don't match; +# those fall back to the legacy partition-on-first-underscore split. +_SYMBOL_VERSION_TAG_RE = re.compile(r"^([A-Za-z][A-Za-z0-9_]*?)_(\d+(?:[._]\d+)*)$") logger = logging.getLogger(__name__) @@ -176,7 +183,16 @@ def policy_is_satisfied( required_vers: dict[str, set[str]] = {} for symbols in versioned_symbols.values(): for symbol in symbols: - sym_name, _, _ = symbol.partition("_") + # Bucket the tag by namespace. Tags with a trailing + # numeric version (the common case) use the regex so + # multi-underscore namespaces like ZLIB_NG, CXXABI_LDBL, + # or LIBSSL_1_1 stay distinct from ZLIB / CXXABI / LIBSSL + # rather than being merged with them. Tags without a + # numeric tail (e.g. "GLIBC_PRIVATE", or fully bare + # tokens with no underscore) fall back to splitting on + # the first underscore so existing behavior is kept. + m = _SYMBOL_VERSION_TAG_RE.match(symbol) + sym_name = m.group(1) if m else symbol.partition("_")[0] required_vers.setdefault(sym_name, set()).add(symbol) for p in self._policies[::-1]: policy_sym_vers = { diff --git a/tests/unit/test_policy.py b/tests/unit/test_policy.py index 470058e7..dfefd68c 100644 --- a/tests/unit/test_policy.py +++ b/tests/unit/test_policy.py @@ -266,3 +266,30 @@ def test_policy_checks_glibc(): policy = policies.versioned_symbols_policy({"some_library.so": {"IAMALIBRARY"}}) assert policy == policies.highest assert policies.linux < policies.lowest < policies.highest + + +def test_policy_does_not_conflate_zlib_ng_with_zlib(): + # Regression for https://github.com/pypa/auditwheel/issues/613. + # zlib-ng tags every exported symbol with ZLIB_NG_2.0.0 / ZLIB_NG_2.1.0 + # via its GNU symbol version script. Splitting these tags on the first + # underscore put them in the "ZLIB" namespace and made them fail every + # policy whose ZLIB allowlist did not contain ZLIB_NG_*. The bucketing + # must keep ZLIB_NG distinct from ZLIB so the (unmodelled) ZLIB_NG + # namespace is silently dropped from the policy check, the same way + # any other unrecognised namespace already is. + policies = WheelPolicies(libc=Libc.GLIBC, arch=Architecture.x86_64) + + policy = policies.versioned_symbols_policy( + {"libz-ng.so.2": {"ZLIB_NG_2.0.0", "ZLIB_NG_2.1.0"}}, + ) + assert policy == policies.highest + + # And mixing libz-ng tags with stock-zlib tags must not poison the + # ZLIB bucket -- the ZLIB_1.2.0 check still needs to pass on its own. + policy = policies.versioned_symbols_policy( + { + "libz.so.1": {"ZLIB_1.2.0"}, + "libz-ng.so.2": {"ZLIB_NG_2.0.0"}, + }, + ) + assert policy > policies.linux