check_comimage.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. #!/usr/bin/env python3
  2. """check_comimage.py -- the image layout is described in exactly one file.
  3. Any check that reads a linked .COM has to know where its parts are: the entry
  4. jump is three bytes, the header is sixteen, initmem's first bytes sit at ENT_SZ
  5. and the header is findable by its own signature. Before tests/comimage.py
  6. existed that knowledge lived in three places - comtest.py had a find_header,
  7. the independent checker inside run_com_tests.sh had a second copy of it, and
  8. check_framedisp.py had a third answer of an entirely different kind: a literal
  9. RT_SZ that had drifted from the runtime it claimed to describe.
  10. Three copies are three chances to drift, and drift is SILENT here, because each
  11. copy is self-consistent on its own terms: every one of them reports confidently
  12. and only the world disagrees. So the layout lives in one file, and this asserts
  13. that it stays that way:
  14. A. comimage.py defines each part exactly once, and imports. Counting rather
  15. than merely searching is the point: a rule that matches nothing looks
  16. exactly like a rule that passes (the lesson in tests/rt_exec.py's
  17. "matched nothing" case), so a deleted definition would otherwise be a green
  18. report about a subject nobody examined.
  19. B. no other test source defines any of those names.
  20. C. every test source that CALLS find_header takes it from comimage rather
  21. than from some other consumer: `from comtest import find_header' would be
  22. one helper reached through two of them, and the second of those two is a
  23. copy waiting to drift. Definitions quoted inside a mutation string are
  24. not calls, so `def find_header(d):' written as a case's target does not
  25. count - nonvacuity.sh would otherwise fail this audit for naming the very
  26. thing it is breaking.
  27. Scope, stated so this is not read as more than it is
  28. ----------------------------------------------------
  29. This cannot see a check that HAND-ROLLS the layout from literals - check_8086
  30. used to write `hdr = 3 + rt_size', which defines none of these names and would
  31. have passed here untouched. Numbers written inline are caught a different way:
  32. check_8086.program_code_region and check_framedisp.main both state the layout
  33. twice, from two independent sources (the probe-measured runtime size against
  34. the header's own equation; the entry jump against the header's end), and
  35. require the two to agree. A disagreement is reported instead of swept, and
  36. tests/nonvacuity.sh turns each of those red on purpose.
  37. Usage: check_comimage.py (from shell/)
  38. """
  39. import os
  40. import re
  41. import sys
  42. HERE = os.path.dirname(os.path.abspath(__file__))
  43. COMIMAGE = os.path.join(HERE, "comimage.py")
  44. # The names that make up the layout of a linked image.
  45. LAYOUT = ["ENT_SZ", "HDR_SZ", "LOAD_BIAS", "HEAD", "HDR_DS_WORD",
  46. "HDR_HEAP_WORD"]
  47. # rt_exec.py states a load bias of its own, and deliberately so: the address
  48. # space it works in is the boot image IT builds (see build_image, and
  49. # exec/rtdrv.s, which carries a .set of the same value for the same reason).
  50. # Those two are independent statements checked against each other by the cases
  51. # failing if they disagree - and rtdrv.s cannot import a Python module. It is a
  52. # different artefact's bias, not a copy of this one.
  53. EXEMPT = {("rt_exec.py", "LOAD_BIAS")}
  54. DEF_FN = re.compile(r"^\s*def\s+find_header\b")
  55. IMPORTS = re.compile(r"^\s*(?:import\s+comimage\b|from\s+comimage\s+import\b)")
  56. # A CALL, not a definition. The lookbehind is what lets nonvacuity.sh quote
  57. # `def find_header(d):' as its mutation target without this audit reporting the
  58. # harness itself as a second copy of the helper.
  59. MENTIONS = re.compile(r"(?<!def )\bfind_header\s*\(")
  60. def sources():
  61. """every test source this audit reads, as (relative path, lines)"""
  62. out = []
  63. for root, dirs, files in os.walk(HERE):
  64. dirs[:] = sorted(d for d in dirs if not d.startswith("."))
  65. for f in sorted(files):
  66. if f == "comimage.py" or not (f.endswith(".py")
  67. or f.endswith(".sh")):
  68. continue
  69. path = os.path.join(root, f)
  70. with open(path, "r") as fh:
  71. out.append((os.path.relpath(path, HERE),
  72. fh.read().splitlines()))
  73. return out
  74. def main():
  75. problems = []
  76. # A: the helper exists, is complete, and loads. The import runs
  77. # comimage's own module-level assertion (initmem's displacements against
  78. # the header word offsets), so a broken helper is caught here too.
  79. if not os.path.exists(COMIMAGE):
  80. print("FAIL: comimage.py does not exist")
  81. return 1
  82. try:
  83. sys.path.insert(0, HERE)
  84. import comimage # noqa: F401
  85. except Exception as exc: # noqa: BLE001
  86. print("FAIL: comimage.py does not import: %s" % exc)
  87. return 1
  88. # Each name is matched on its own. A single alternation would count one
  89. # line for every name in it - six definitions each, from six lines total -
  90. # which reports the layout as six times duplicated while it is defined once.
  91. defs = dict((name, re.compile(r"^\s*%s\s*=" % re.escape(name)))
  92. for name in LAYOUT)
  93. with open(COMIMAGE) as fh:
  94. comimage_lines = fh.read().splitlines()
  95. for name in LAYOUT:
  96. n = sum(1 for ln in comimage_lines if defs[name].match(ln))
  97. if n != 1:
  98. problems.append("comimage.py defines %s %d times, want exactly 1"
  99. % (name, n))
  100. nfn = sum(1 for ln in comimage_lines if DEF_FN.match(ln))
  101. if nfn != 1:
  102. problems.append("comimage.py defines find_header %d times, want "
  103. "exactly 1" % nfn)
  104. # B and C: every other test source.
  105. for rel, lines in sources():
  106. for ln in lines:
  107. if DEF_FN.match(ln):
  108. problems.append("%s defines its own find_header" % rel)
  109. for name in LAYOUT:
  110. if (rel, name) in EXEMPT:
  111. continue
  112. if defs[name].match(ln):
  113. problems.append("%s defines its own %s" % (rel, name))
  114. mentions = [ln for ln in lines if MENTIONS.search(ln)]
  115. if mentions and not any(IMPORTS.match(ln) for ln in lines):
  116. problems.append("%s calls find_header without importing it from "
  117. "comimage" % rel)
  118. if problems:
  119. print("FAIL: the image layout is not described in exactly one file")
  120. for p in problems:
  121. print(" - %s" % p)
  122. return 1
  123. print("image layout: defined once in comimage.py and imported by every "
  124. "reader (%d names, %d sources scanned)"
  125. % (len(LAYOUT) + 1, len(sources()) + 1))
  126. return 0
  127. if __name__ == "__main__":
  128. sys.exit(main())