# Copyright 2018 The gVisor Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Verify VDSO ELF does not contain any relocations and is directly mmappable.
"""

import argparse
import logging
import re
import subprocess

PAGE_SIZE = 4096


def PageRoundDown(addr):
  """Rounds down to the nearest page.

  Args:
    addr: An address.

  Returns:
    The address rounded down to the nearest page.
  """
  return addr & ~(PAGE_SIZE - 1)


def Fatal(*args, **kwargs):
  """Logs a critical message and exits with code 1.

  Args:
    *args: Args to pass to logging.critical.
    **kwargs: Keyword args to pass to logging.critical.
  """
  logging.critical(*args, **kwargs)
  exit(1)


def CheckSegments(vdso_path):
  """Verifies layout of PT_LOAD segments.

  PT_LOAD segments must be laid out such that the ELF is directly mmappable.

  Specifically, check that:
  * PT_LOAD file offsets are equivalent to the memory offset from the first
    segment.
  * No extra zeroed space (memsz) is required.
  * PT_LOAD segments are in order (required for any ELF).
  * No two PT_LOAD segments share part of the same page.

  The readelf line format looks like:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  LOAD           0x000000 0xffffffffff700000 0xffffffffff700000 0x000e68 0x000e68 R E 0x1000

  Args:
    vdso_path: Path to VDSO binary.
  """
  output = subprocess.check_output(["readelf", "-lW", vdso_path]).decode()
  lines = output.split("\n")

  segments = []
  for line in lines:
    if not line.startswith("  LOAD"):
      continue

    components = line.split()

    segments.append({
        "offset": int(components[1], 16),
        "addr": int(components[2], 16),
        "filesz": int(components[4], 16),
        "memsz": int(components[5], 16),
    })

  if not segments:
    Fatal("No PT_LOAD segments in VDSO")

  first = segments[0]
  if first["offset"] != 0:
    Fatal("First PT_LOAD segment has non-zero file offset: %s", first)

  for i, segment in enumerate(segments):
    memoff = segment["addr"] - first["addr"]
    if memoff != segment["offset"]:
      Fatal("PT_LOAD segment has different memory and file offsets: %s",
            segments)

    if segment["memsz"] != segment["filesz"]:
      Fatal("PT_LOAD segment memsz != filesz: %s", segment)

    if i > 0:
      last_end = segments[i-1]["addr"] + segments[i-1]["memsz"]
      if segment["addr"] < last_end:
        Fatal("PT_LOAD segments out of order")

      last_page = PageRoundDown(last_end)
      start_page = PageRoundDown(segment["addr"])
      if last_page >= start_page:
        Fatal("PT_LOAD segments share a page: %s and %s", segment,
              segments[i - 1])


# Matches the section name in readelf -SW output.
_SECTION_NAME_RE = re.compile(r"""^\s+\[\ ?\d+\]\s+
                              (?P<name>\.\S+)\s+
                              (?P<type>\S+)\s+
                              (?P<addr>[0-9a-f]+)\s+
                              (?P<off>[0-9a-f]+)\s+
                              (?P<size>[0-9a-f]+)""", re.VERBOSE)


def CheckData(vdso_path):
  """Verifies the VDSO contains no .data or .bss sections.

  The readelf line format looks like:

  There are 15 section headers, starting at offset 0x15f0:

  Section Headers:
    [Nr] Name         Type      Address          Off    Size   ES Flg Lk Inf Al
    [ 0]              NULL      0000000000000000 000000 000000 00      0   0  0
    [ 1] .hash        HASH      ffffffffff700120 000120 000040 04   A  2   0  8
    [ 2] .dynsym      DYNSYM    ffffffffff700160 000160 000108 18   A  3   1  8
    ...
    [13] .strtab      STRTAB    0000000000000000 001448 000123 00      0   0  1
    [14] .shstrtab    STRTAB    0000000000000000 00156b 000083 00      0   0  1
  Key to Flags:
    W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
    L (link order), O (extra OS processing required), G (group), T (TLS),
    C (compressed), x (unknown), o (OS specific), E (exclude),
    l (large), p (processor specific)

  Args:
    vdso_path: Path to VDSO binary.
  """
  output = subprocess.check_output(["readelf", "-SW", vdso_path]).decode()
  lines = output.split("\n")

  found_text = False
  for line in lines:
    m = re.search(_SECTION_NAME_RE, line)
    if not m:
      continue

    if not line.startswith("  ["):
      continue

    name = m.group("name")
    size = int(m.group("size"), 16)

    if name == ".text" and size != 0:
      found_text = True

    # Clang will typically omit these sections entirely; gcc will include them
    # but with size 0.
    if name.startswith(".data") and size != 0:
      Fatal("VDSO contains non-empty .data section:\n%s" % output)

    if name.startswith(".bss") and size != 0:
      Fatal("VDSO contains non-empty .bss section:\n%s" % output)

  if not found_text:
    Fatal("VDSO contains no/empty .text section? Bad parsing?:\n%s" % output)


def CheckRelocs(vdso_path):
  """Verifies that the VDSO includes no relocations.

  Args:
    vdso_path: Path to VDSO binary.
  """
  output = subprocess.check_output(["readelf", "-r", vdso_path]).decode()
  if output.strip() != "There are no relocations in this file.":
    Fatal("VDSO contains relocations: %s", output)


def main():
  parser = argparse.ArgumentParser(description="Verify VDSO ELF.")
  parser.add_argument("--vdso", required=True, help="Path to VDSO ELF")
  parser.add_argument(
      "--check-data",
      action="store_true",
      help="Check that the ELF contains no .data or .bss sections")
  args = parser.parse_args()

  CheckSegments(args.vdso)
  CheckRelocs(args.vdso)

  if args.check_data:
    CheckData(args.vdso)


if __name__ == "__main__":
  main()
