test_vtable.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. #!/usr/bin/python3
  2. # tests for vtable computations
  3. import hashlib
  4. import os
  5. import pathlib
  6. import typing
  7. import pytest
  8. import smgdc.angr
  9. from smgdc.validate import LinuxBinary
  10. @pytest.fixture(scope="session")
  11. def game_bin(request) -> LinuxBinary:
  12. # this is tested against the TF2 Linux DS server binary (steam app 232250, depot 232256)
  13. #
  14. # the author will not provide further support in obtaining this file, nor will the author
  15. # update the tests for other game versions
  16. #
  17. # if you have the file: to enable tests that depend on the binary, create a `.env` file with
  18. # TF_LINUX_BINARY set to the binary path (use single quotes on Windows to avoid escaping
  19. # backslashes), then do `just test-extended`
  20. #
  21. # TODO: find a libre binary that we can do similarly extensive tests against in its place
  22. valid_hashes = {
  23. # game version 8622567, manifest 2590057218013527366
  24. "a5a1adde851be2c71f8de73830466cd20f8c34d26ade580d110c4f93c6ef1374",
  25. # game version 8835751, manifest 9155443851166366439?
  26. # this one requires a constraint file
  27. "6a14d6460086836b710aaed901af3304f791b25409c84d182c6d7662a9e09401",
  28. }
  29. path_string = os.environ.get("TF_LINUX_BINARY")
  30. if not path_string:
  31. pytest.skip("No Linux binary given")
  32. bin_path = pathlib.Path(path_string)
  33. with bin_path.open("rb") as f:
  34. h = hashlib.file_digest(f, "sha256")
  35. if h.hexdigest() not in valid_hashes:
  36. pytest.skip("Incorrect Linux binary")
  37. return LinuxBinary(bin_path)
  38. class VTableCheck(typing.NamedTuple):
  39. linux_index: int
  40. windows_index: int | None
  41. symbol_name: str
  42. @pytest.mark.extended
  43. @pytest.mark.parametrize(
  44. "vtsym_name,vtasserts",
  45. [
  46. (
  47. "_ZTV9CTFPlayer",
  48. [
  49. (149, 147, "_ZN20CBaseCombatCharacter8FVisibleERK6VectoriPP11CBaseEntity"),
  50. (490, None, "_ZN9CTFPlayer16ReapplyProvisionEv"),
  51. (493, 486, "_ZN9CTFPlayer13GiveNamedItemEPKciPK13CEconItemViewb"),
  52. ],
  53. ),
  54. (
  55. "_ZTV11CBaseObject",
  56. [
  57. (356, 354, "_ZN11CBaseObject6KilledERK15CTakeDamageInfo"),
  58. (367, 365, "_ZN11CBaseObject17CheckUpgradeOnHitEP9CTFPlayer"),
  59. (383, 382, "_ZN11CBaseObject13CanBeUpgradedEP9CTFPlayer"),
  60. (384, 383, "_ZN11CBaseObject14StartUpgradingEv"),
  61. (413, 381, "_ZNK11CBaseObject13CanBeUpgradedEv"),
  62. ],
  63. ),
  64. (
  65. "_ZTV13CBaseNPCMaker",
  66. [
  67. # on newer binaries the functions are optimized out to 0
  68. # this also affects CBaseObject
  69. (2, 1, "_ZN11CBaseEntity13SetRefEHandleERK11CBaseHandle"),
  70. ],
  71. ),
  72. (
  73. "_ZTV12CTFGameRules",
  74. [
  75. # parent vtable spans are not completely in descending order (some of them increase)
  76. (140, 139, "_ZNK12CTFGameRules15IsHolidayActiveEi"),
  77. ],
  78. ),
  79. ],
  80. )
  81. def test_vtable(game_bin: LinuxBinary, vtsym_name: str, vtasserts: list[VTableCheck]):
  82. # tests vtable translation
  83. vtsym = game_bin.angr.loader.find_symbol(vtsym_name)
  84. assert vtsym, f"Could not find vtable symbol {vtsym_name}"
  85. orig_vtable, *thunk_vtables = game_bin.vtable_disambiguator.get_vtables_from_address(vtsym)
  86. win_vtable = game_bin.vtable_disambiguator.get_windows_vtables_from(vtsym)
  87. for check in map(VTableCheck._make, vtasserts):
  88. symbol = game_bin.angr.loader.find_symbol(check.symbol_name)
  89. assert orig_vtable.index(symbol) == check.linux_index
  90. if check.windows_index is not None:
  91. assert win_vtable.index(symbol) == check.windows_index
  92. else:
  93. assert symbol not in win_vtable