reversion_glibc.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. #!/usr/bin/env python3
  2. # Copyright 2021 The Chromium Authors. All rights reserved.
  3. # Use of this source code is governed by a BSD-style license that can be
  4. # found in the LICENSE file.
  5. """Rewrite incompatible default symbols in glibc.
  6. """
  7. import re
  8. import subprocess
  9. import sys
  10. # This constant comes from https://crbug.com/580892
  11. MAX_ALLOWED_GLIBC_VERSION = [2, 17]
  12. VERSION_PATTERN = re.compile('GLIBC_([0-9\.]+)')
  13. SECTION_PATTERN = re.compile(r'^ *\[ *[0-9]+\] +(\S+) +\S+ + ([0-9a-f]+) .*$')
  14. # Some otherwise disallowed symbols are referenced in the linux-chromeos build.
  15. # To continue supporting it, allow these symbols to remain enabled.
  16. SYMBOL_ALLOWLIST = {
  17. 'fts64_close',
  18. 'fts64_open',
  19. 'fts64_read',
  20. 'memfd_create',
  21. }
  22. # The two dictionaries below map from symbol name to
  23. # (symbol version, symbol index).
  24. #
  25. # The default version for a given symbol (which may be unsupported).
  26. default_version = {}
  27. # The max supported symbol version for a given symbol.
  28. supported_version = {}
  29. # The file name of the binary we're going to rewrite.
  30. BIN_FILE = sys.argv[1]
  31. # Populate |default_version| and |supported_version| with data from readelf.
  32. stdout = subprocess.check_output(['readelf', '--dyn-syms', '--wide', BIN_FILE])
  33. for line in stdout.decode("utf-8").split('\n'):
  34. cols = re.split('\s+', line)
  35. # Skip the preamble.
  36. if len(cols) < 9:
  37. continue
  38. index = cols[1].rstrip(':')
  39. # Skip the header.
  40. if not index.isdigit():
  41. continue
  42. index = int(index)
  43. name = cols[8].split('@')
  44. # Ignore unversioned symbols.
  45. if len(name) < 2:
  46. continue
  47. base_name = name[0]
  48. version = name[-1]
  49. # The default version will have '@@' in the name.
  50. is_default = len(name) > 2
  51. if version.startswith('XCRYPT_'):
  52. # Prefer GLIBC_* versioned symbols over XCRYPT_* ones. Set the version to
  53. # something > MAX_ALLOWED_GLIBC_VERSION so this symbol will not be picked.
  54. version = [float('inf')]
  55. else:
  56. match = re.match(VERSION_PATTERN, version)
  57. # Ignore symbols versioned with GLIBC_PRIVATE.
  58. if not match:
  59. continue
  60. version = [int(part) for part in match.group(1).split('.')]
  61. if version < MAX_ALLOWED_GLIBC_VERSION:
  62. old_supported_version = supported_version.get(base_name, ([-1], -1))
  63. supported_version[base_name] = max((version, index), old_supported_version)
  64. if is_default:
  65. default_version[base_name] = (version, index)
  66. # Get the offset into the binary of the .gnu.version section from readelf.
  67. stdout = subprocess.check_output(['readelf', '--sections', '--wide', BIN_FILE])
  68. for line in stdout.decode("utf-8").split('\n'):
  69. if match := SECTION_PATTERN.match(line):
  70. section_name, address = match.groups()
  71. if section_name == '.gnu.version':
  72. gnu_version_addr = int(address, base=16)
  73. break
  74. else:
  75. print('No .gnu.version section found', file=sys.stderr)
  76. sys.exit(1)
  77. # Rewrite the binary.
  78. bin_data = bytearray(open(BIN_FILE, 'rb').read())
  79. for name, (version, index) in default_version.items():
  80. # No need to rewrite the default if it's already an allowed version.
  81. if version <= MAX_ALLOWED_GLIBC_VERSION:
  82. continue
  83. if name in SYMBOL_ALLOWLIST:
  84. continue
  85. elif name in supported_version:
  86. _, supported_index = supported_version[name]
  87. else:
  88. supported_index = -1
  89. # The .gnu.version section is divided into 16-bit chunks that give the
  90. # symbol versions. The 16th bit is a flag that's false for the default
  91. # version. The data is stored in little-endian so we need to add 1 to
  92. # get the address of the byte we want to flip.
  93. #
  94. # Disable the unsupported symbol.
  95. old_default = gnu_version_addr + 2 * index + 1
  96. assert (bin_data[old_default] & 0x80) == 0
  97. bin_data[old_default] ^= 0x80
  98. # If we found a supported version, enable that as default.
  99. if supported_index != -1:
  100. new_default = gnu_version_addr + 2 * supported_index + 1
  101. assert (bin_data[new_default] & 0x80) == 0x80
  102. bin_data[new_default] ^= 0x80
  103. open(BIN_FILE, 'wb').write(bin_data)