gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/vdso/check_vdso.py (about)

     1  # Copyright 2018 The gVisor Authors.
     2  #
     3  # Licensed under the Apache License, Version 2.0 (the "License");
     4  # you may not use this file except in compliance with the License.
     5  # You may obtain a copy of the License at
     6  #
     7  #     http://www.apache.org/licenses/LICENSE-2.0
     8  #
     9  # Unless required by applicable law or agreed to in writing, software
    10  # distributed under the License is distributed on an "AS IS" BASIS,
    11  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  # See the License for the specific language governing permissions and
    13  # limitations under the License.
    14  
    15  """Verify VDSO ELF does not contain any relocations and is directly mmappable.
    16  """
    17  
    18  import argparse
    19  import logging
    20  import re
    21  import subprocess
    22  
    23  PAGE_SIZE = 4096
    24  
    25  
    26  def PageRoundDown(addr):
    27    """Rounds down to the nearest page.
    28  
    29    Args:
    30      addr: An address.
    31  
    32    Returns:
    33      The address rounded down to the nearest page.
    34    """
    35    return addr & ~(PAGE_SIZE - 1)
    36  
    37  
    38  def Fatal(*args, **kwargs):
    39    """Logs a critical message and exits with code 1.
    40  
    41    Args:
    42      *args: Args to pass to logging.critical.
    43      **kwargs: Keyword args to pass to logging.critical.
    44    """
    45    logging.critical(*args, **kwargs)
    46    exit(1)
    47  
    48  
    49  def CheckSegments(vdso_path):
    50    """Verifies layout of PT_LOAD segments.
    51  
    52    PT_LOAD segments must be laid out such that the ELF is directly mmappable.
    53  
    54    Specifically, check that:
    55    * PT_LOAD file offsets are equivalent to the memory offset from the first
    56      segment.
    57    * No extra zeroed space (memsz) is required.
    58    * PT_LOAD segments are in order (required for any ELF).
    59    * No two PT_LOAD segments share part of the same page.
    60  
    61    The readelf line format looks like:
    62    Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
    63    LOAD           0x000000 0xffffffffff700000 0xffffffffff700000 0x000e68 0x000e68 R E 0x1000
    64  
    65    Args:
    66      vdso_path: Path to VDSO binary.
    67    """
    68    output = subprocess.check_output(["readelf", "-lW", vdso_path]).decode()
    69    lines = output.split("\n")
    70  
    71    segments = []
    72    for line in lines:
    73      if not line.startswith("  LOAD"):
    74        continue
    75  
    76      components = line.split()
    77  
    78      segments.append({
    79          "offset": int(components[1], 16),
    80          "addr": int(components[2], 16),
    81          "filesz": int(components[4], 16),
    82          "memsz": int(components[5], 16),
    83      })
    84  
    85    if not segments:
    86      Fatal("No PT_LOAD segments in VDSO")
    87  
    88    first = segments[0]
    89    if first["offset"] != 0:
    90      Fatal("First PT_LOAD segment has non-zero file offset: %s", first)
    91  
    92    for i, segment in enumerate(segments):
    93      memoff = segment["addr"] - first["addr"]
    94      if memoff != segment["offset"]:
    95        Fatal("PT_LOAD segment has different memory and file offsets: %s",
    96              segments)
    97  
    98      if segment["memsz"] != segment["filesz"]:
    99        Fatal("PT_LOAD segment memsz != filesz: %s", segment)
   100  
   101      if i > 0:
   102        last_end = segments[i-1]["addr"] + segments[i-1]["memsz"]
   103        if segment["addr"] < last_end:
   104          Fatal("PT_LOAD segments out of order")
   105  
   106        last_page = PageRoundDown(last_end)
   107        start_page = PageRoundDown(segment["addr"])
   108        if last_page >= start_page:
   109          Fatal("PT_LOAD segments share a page: %s and %s", segment,
   110                segments[i - 1])
   111  
   112  
   113  # Matches the section name in readelf -SW output.
   114  _SECTION_NAME_RE = re.compile(r"""^\s+\[\ ?\d+\]\s+
   115                                (?P<name>\.\S+)\s+
   116                                (?P<type>\S+)\s+
   117                                (?P<addr>[0-9a-f]+)\s+
   118                                (?P<off>[0-9a-f]+)\s+
   119                                (?P<size>[0-9a-f]+)""", re.VERBOSE)
   120  
   121  
   122  def CheckData(vdso_path):
   123    """Verifies the VDSO contains no .data or .bss sections.
   124  
   125    The readelf line format looks like:
   126  
   127    There are 15 section headers, starting at offset 0x15f0:
   128  
   129    Section Headers:
   130      [Nr] Name         Type      Address          Off    Size   ES Flg Lk Inf Al
   131      [ 0]              NULL      0000000000000000 000000 000000 00      0   0  0
   132      [ 1] .hash        HASH      ffffffffff700120 000120 000040 04   A  2   0  8
   133      [ 2] .dynsym      DYNSYM    ffffffffff700160 000160 000108 18   A  3   1  8
   134      ...
   135      [13] .strtab      STRTAB    0000000000000000 001448 000123 00      0   0  1
   136      [14] .shstrtab    STRTAB    0000000000000000 00156b 000083 00      0   0  1
   137    Key to Flags:
   138      W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
   139      L (link order), O (extra OS processing required), G (group), T (TLS),
   140      C (compressed), x (unknown), o (OS specific), E (exclude),
   141      l (large), p (processor specific)
   142  
   143    Args:
   144      vdso_path: Path to VDSO binary.
   145    """
   146    output = subprocess.check_output(["readelf", "-SW", vdso_path]).decode()
   147    lines = output.split("\n")
   148  
   149    found_text = False
   150    for line in lines:
   151      m = re.search(_SECTION_NAME_RE, line)
   152      if not m:
   153        continue
   154  
   155      if not line.startswith("  ["):
   156        continue
   157  
   158      name = m.group("name")
   159      size = int(m.group("size"), 16)
   160  
   161      if name == ".text" and size != 0:
   162        found_text = True
   163  
   164      # Clang will typically omit these sections entirely; gcc will include them
   165      # but with size 0.
   166      if name.startswith(".data") and size != 0:
   167        Fatal("VDSO contains non-empty .data section:\n%s" % output)
   168  
   169      if name.startswith(".bss") and size != 0:
   170        Fatal("VDSO contains non-empty .bss section:\n%s" % output)
   171  
   172    if not found_text:
   173      Fatal("VDSO contains no/empty .text section? Bad parsing?:\n%s" % output)
   174  
   175  
   176  def CheckRelocs(vdso_path):
   177    """Verifies that the VDSO includes no relocations.
   178  
   179    Args:
   180      vdso_path: Path to VDSO binary.
   181    """
   182    output = subprocess.check_output(["readelf", "-r", vdso_path]).decode()
   183    if output.strip() != "There are no relocations in this file.":
   184      Fatal("VDSO contains relocations: %s", output)
   185  
   186  
   187  def main():
   188    parser = argparse.ArgumentParser(description="Verify VDSO ELF.")
   189    parser.add_argument("--vdso", required=True, help="Path to VDSO ELF")
   190    parser.add_argument(
   191        "--check-data",
   192        action="store_true",
   193        help="Check that the ELF contains no .data or .bss sections")
   194    args = parser.parse_args()
   195  
   196    CheckSegments(args.vdso)
   197    CheckRelocs(args.vdso)
   198  
   199    if args.check_data:
   200      CheckData(args.vdso)
   201  
   202  
   203  if __name__ == "__main__":
   204    main()