test_vtable.py 3.5 KB

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