#!/usr/bin/env python3 """ Patch ELF .so/.node files that require GLIBCXX_3.4.30 to use GLIBCXX_3.4.29 instead. Required for AlmaLinux 9 / RHEL 9 which ships GCC 11 (max GLIBCXX_3.4.29). Both the version string AND the vna_hash in .gnu.version_r are updated so the dynamic linker finds the version correctly at runtime. """ import sys import struct def elf_hash(name: str) -> int: """ELF version symbol hash (not GNU hash — used in .gnu.version_r).""" h = 0 for c in name: h = ((h << 4) + ord(c)) & 0xFFFFFFFF g = h & 0xF0000000 if g: h ^= g >> 24 h &= (~g) & 0xFFFFFFFF return h def patch_file(path: str) -> bool: with open(path, 'rb') as f: data = bytearray(f.read()) old_ver = b'GLIBCXX_3.4.30' new_ver = b'GLIBCXX_3.4.11' if old_ver not in data: print(f' skip (no GLIBCXX_3.4.30): {path}') return False # ----- ELF header ----- if data[:4] != b'\x7fELF': print(f' skip (not ELF): {path}') return False ei_class = data[4] # 1=32-bit, 2=64-bit ei_data = data[5] # 1=LE, 2=BE endian = '<' if ei_data == 1 else '>' if ei_class == 2: # 64-bit ELF e_shoff_fmt = f'{endian}Q' ehdr_size = 64 shdr_size = 64 shdr_fmt = f'{endian}IIQQQQIIQQ' # 64-byte shdr (e_shoff,) = struct.unpack_from(f'{endian}Q', data, 40) (e_shentsize, e_shnum, e_shstrndx) = struct.unpack_from(f'{endian}HHH', data, 58) shdr_type_off = 4 shdr_off_off = 24 # sh_offset within shdr shdr_link_off = 48 # sh_link within shdr else: # 32-bit ELF (e_shoff,) = struct.unpack_from(f'{endian}I', data, 32) (e_shentsize, e_shnum, e_shstrndx) = struct.unpack_from(f'{endian}HHH', data, 46) shdr_type_off = 4 shdr_off_off = 16 shdr_link_off = 28 SHT_GNU_verneed = 0x6FFFFFFE patched = False # Target GLIBCXX_3.4.11 — that's where condition_variable::wait actually lives # in AlmaLinux 9's libstdc++.so.6 (same byte length: 14 chars each) old_hash = elf_hash('GLIBCXX_3.4.30') new_hash = elf_hash('GLIBCXX_3.4.11') old_hash_bytes = struct.pack(f'{endian}I', old_hash) new_hash_bytes = struct.pack(f'{endian}I', new_hash) for i in range(e_shnum): shdr_base = e_shoff + i * e_shentsize sh_type = struct.unpack_from(f'{endian}I', data, shdr_base + shdr_type_off)[0] if sh_type != SHT_GNU_verneed: continue # sh_offset and sh_link (index of associated string table section) if ei_class == 2: sh_offset = struct.unpack_from(f'{endian}Q', data, shdr_base + 24)[0] sh_size = struct.unpack_from(f'{endian}Q', data, shdr_base + 32)[0] sh_link = struct.unpack_from(f'{endian}I', data, shdr_base + 40)[0] sh_info = struct.unpack_from(f'{endian}I', data, shdr_base + 44)[0] else: sh_offset = struct.unpack_from(f'{endian}I', data, shdr_base + 16)[0] sh_size = struct.unpack_from(f'{endian}I', data, shdr_base + 20)[0] sh_link = struct.unpack_from(f'{endian}I', data, shdr_base + 24)[0] sh_info = struct.unpack_from(f'{endian}I', data, shdr_base + 28)[0] # String table section referenced by sh_link strtab_shdr = e_shoff + sh_link * e_shentsize if ei_class == 2: strtab_off = struct.unpack_from(f'{endian}Q', data, strtab_shdr + 24)[0] else: strtab_off = struct.unpack_from(f'{endian}I', data, strtab_shdr + 16)[0] # Walk Elf64_Verneed / Elf64_Vernaux entries # Elf64_Verneed: vn_version(2)+vn_cnt(2)+vn_file(4)+vn_aux(4)+vn_next(4) = 16 bytes # Elf64_Vernaux: vna_hash(4)+vna_flags(2)+vna_other(2)+vna_name(4)+vna_next(4) = 16 bytes vneed_pos = sh_offset for _ in range(sh_info): # sh_info = number of Verneed entries vn_cnt = struct.unpack_from(f'{endian}H', data, vneed_pos + 2)[0] vn_aux = struct.unpack_from(f'{endian}I', data, vneed_pos + 8)[0] # +8 (after vn_file) vn_next = struct.unpack_from(f'{endian}I', data, vneed_pos + 12)[0] vaux_pos = vneed_pos + vn_aux for _ in range(vn_cnt): vna_hash = struct.unpack_from(f'{endian}I', data, vaux_pos)[0] vna_name = struct.unpack_from(f'{endian}I', data, vaux_pos + 8)[0] vna_next = struct.unpack_from(f'{endian}I', data, vaux_pos + 12)[0] # Read the version name from the string table name_off = strtab_off + vna_name name_end = data.index(b'\x00', name_off) ver_name = bytes(data[name_off:name_end]) if ver_name == old_ver: # Patch hash struct.pack_into(f'{endian}I', data, vaux_pos, new_hash) # Patch string (same byte length — safe in-place) data[name_off:name_off + len(old_ver)] = new_ver patched = True if vna_next == 0: break vaux_pos += vna_next if vn_next == 0: break vneed_pos += vn_next if patched: with open(path, 'wb') as f: f.write(data) print(f' patched (hash+string): {path}') return True else: print(f' skip (GLIBCXX_3.4.30 not in verneed): {path}') return False if __name__ == '__main__': for path in sys.argv[1:]: patch_file(path)