test_vtable.py 3.5 KB

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