Loading...
#!/usr/bin/env python3
"""
test_fortify_source.py

This verifies that when _FORTIFY_SOURCE is enabled, the appropriate functions
get defined to a _chk or _ptrchk variant (and if necessary, that the size is
queried with the right kind of __builtin_object_size invocation).

IF YOU ARE ADDING A CHECKED VARIANT FOR A FUNCTION:
After adding your variant, run this script with -g to generate the new expected
normal. Replace expected values in the PARAMETERS arrays with what you get.

IF YOU ARE ADDING A NEW CONDITIONAL MODE:
If you conditionally add a checked variant based on a C version or POSIX
standard version, make sure that there is a TestParameters entry that covers the
case where you expect the builtin to be used and at least one case where it is
not. Add entries as needed.

Run the script at desk with:

% python3 fortify_source.py --verbose --check-syntax --include-dir $(DSTROOT)/usr/include fortify_source.c

Don't forget to test with each SDK variant (ie, Darwin userspace, DriverKit, etc).
"""
from argparse import ArgumentParser
from collections import namedtuple
import os
import re
import subprocess
import sys

BADFUNC = "BADFUNC"
UNDEFINED = "UNDEFINED"
BUILTIN_OBSZ = "BUILTIN_OBSZ"
BUILTIN_OBSZ0 = "BUILTIN_OBSZ0"
PTRCHECK_OBSZ = "PTRCHECK_OBSZ"
PTRCHECK_OBSZ0 = "PTRCHECK_OBSZ0"

TestParameters = namedtuple(typename="TestParameters", field_names=["posix_c_source", "darwin_c_level", "fbounds_safety", "expected_driverkit", "expected_userspace"])

PARAMETERS = [
	TestParameters(posix_c_source=None, darwin_c_level=0, fbounds_safety=None,
		expected_driverkit={"bcopy": UNDEFINED, "bzero": UNDEFINED, "memccpy": BUILTIN_OBSZ, "memmove": BUILTIN_OBSZ, "memcpy": BUILTIN_OBSZ, "memset": BUILTIN_OBSZ, "snprintf": UNDEFINED, "sprintf": UNDEFINED, "stpcpy": UNDEFINED, "stpncpy": UNDEFINED, "strcat": UNDEFINED, "strcpy": UNDEFINED, "strlcat": UNDEFINED, "strlcpy": UNDEFINED, "strncat": BUILTIN_OBSZ0, "strncpy": BUILTIN_OBSZ0, "vsnprintf": UNDEFINED, "vsprintf": UNDEFINED, },
		expected_userspace={"bcopy": UNDEFINED, "bzero": UNDEFINED, "memccpy": BUILTIN_OBSZ, "memmove": BUILTIN_OBSZ, "memcpy": BUILTIN_OBSZ, "memset": BUILTIN_OBSZ, "snprintf": UNDEFINED, "sprintf": BUILTIN_OBSZ0, "stpcpy": UNDEFINED, "stpncpy": UNDEFINED, "strcat": BUILTIN_OBSZ0, "strcpy": BUILTIN_OBSZ0, "strlcat": UNDEFINED, "strlcpy": UNDEFINED, "strncat": BUILTIN_OBSZ0, "strncpy": BUILTIN_OBSZ0, "vsnprintf": UNDEFINED, "vsprintf": UNDEFINED, },),

	TestParameters(posix_c_source=None, darwin_c_level="200112", fbounds_safety=None,
		expected_driverkit={"bcopy": UNDEFINED, "bzero": UNDEFINED, "memccpy": BUILTIN_OBSZ, "memmove": BUILTIN_OBSZ, "memcpy": BUILTIN_OBSZ, "memset": BUILTIN_OBSZ, "snprintf": BUILTIN_OBSZ0, "sprintf": UNDEFINED, "stpcpy": UNDEFINED, "stpncpy": UNDEFINED, "strcat": UNDEFINED, "strcpy": UNDEFINED, "strlcat": UNDEFINED, "strlcpy": UNDEFINED, "strncat": BUILTIN_OBSZ0, "strncpy": BUILTIN_OBSZ0, "vsnprintf": BUILTIN_OBSZ0, "vsprintf": UNDEFINED, },
		expected_userspace={"bcopy": UNDEFINED, "bzero": UNDEFINED, "memccpy": BUILTIN_OBSZ, "memmove": BUILTIN_OBSZ, "memcpy": BUILTIN_OBSZ, "memset": BUILTIN_OBSZ, "snprintf": BUILTIN_OBSZ0, "sprintf": BUILTIN_OBSZ0, "stpcpy": UNDEFINED, "stpncpy": UNDEFINED, "strcat": BUILTIN_OBSZ0, "strcpy": BUILTIN_OBSZ0, "strlcat": UNDEFINED, "strlcpy": UNDEFINED, "strncat": BUILTIN_OBSZ0, "strncpy": BUILTIN_OBSZ0, "vsnprintf": BUILTIN_OBSZ0, "vsprintf": BUILTIN_OBSZ0, },),

	TestParameters(posix_c_source=None, darwin_c_level="200809", fbounds_safety=None,
		expected_driverkit={"bcopy": UNDEFINED, "bzero": UNDEFINED, "memccpy": BUILTIN_OBSZ, "memmove": BUILTIN_OBSZ, "memcpy": BUILTIN_OBSZ, "memset": BUILTIN_OBSZ, "snprintf": BUILTIN_OBSZ0, "sprintf": UNDEFINED, "stpcpy": UNDEFINED, "stpncpy": BUILTIN_OBSZ0, "strcat": UNDEFINED, "strcpy": UNDEFINED, "strlcat": UNDEFINED, "strlcpy": UNDEFINED, "strncat": BUILTIN_OBSZ0, "strncpy": BUILTIN_OBSZ0, "vsnprintf": BUILTIN_OBSZ0, "vsprintf": UNDEFINED, },
		expected_userspace={"bcopy": UNDEFINED, "bzero": UNDEFINED, "memccpy": BUILTIN_OBSZ, "memmove": BUILTIN_OBSZ, "memcpy": BUILTIN_OBSZ, "memset": BUILTIN_OBSZ, "snprintf": BUILTIN_OBSZ0, "sprintf": BUILTIN_OBSZ0, "stpcpy": BUILTIN_OBSZ0, "stpncpy": BUILTIN_OBSZ0, "strcat": BUILTIN_OBSZ0, "strcpy": BUILTIN_OBSZ0, "strlcat": UNDEFINED, "strlcpy": UNDEFINED, "strncat": BUILTIN_OBSZ0, "strncpy": BUILTIN_OBSZ0, "vsnprintf": BUILTIN_OBSZ0, "vsprintf": BUILTIN_OBSZ0, },),

	TestParameters(posix_c_source=None, darwin_c_level="__DARWIN_C_FULL", fbounds_safety=None,
		expected_driverkit={"bcopy": BUILTIN_OBSZ, "bzero": BUILTIN_OBSZ, "memccpy": BUILTIN_OBSZ, "memmove": BUILTIN_OBSZ, "memcpy": BUILTIN_OBSZ, "memset": BUILTIN_OBSZ, "snprintf": BUILTIN_OBSZ0, "sprintf": UNDEFINED, "stpcpy": UNDEFINED, "stpncpy": BUILTIN_OBSZ0, "strcat": UNDEFINED, "strcpy": UNDEFINED, "strlcat": BUILTIN_OBSZ0, "strlcpy": BUILTIN_OBSZ0, "strncat": BUILTIN_OBSZ0, "strncpy": BUILTIN_OBSZ0, "vsnprintf": BUILTIN_OBSZ0, "vsprintf": UNDEFINED, },
		expected_userspace={"bcopy": BUILTIN_OBSZ, "bzero": BUILTIN_OBSZ, "memccpy": BUILTIN_OBSZ, "memmove": BUILTIN_OBSZ, "memcpy": BUILTIN_OBSZ, "memset": BUILTIN_OBSZ, "snprintf": BUILTIN_OBSZ0, "sprintf": BUILTIN_OBSZ0, "stpcpy": BUILTIN_OBSZ0, "stpncpy": BUILTIN_OBSZ0, "strcat": BUILTIN_OBSZ0, "strcpy": BUILTIN_OBSZ0, "strlcat": BUILTIN_OBSZ0, "strlcpy": BUILTIN_OBSZ0, "strncat": BUILTIN_OBSZ0, "strncpy": BUILTIN_OBSZ0, "vsnprintf": BUILTIN_OBSZ0, "vsprintf": BUILTIN_OBSZ0, },),

	TestParameters(posix_c_source="0", darwin_c_level="__DARWIN_C_FULL", fbounds_safety=None,
		expected_driverkit={"bcopy": BUILTIN_OBSZ, "bzero": BUILTIN_OBSZ, "memccpy": BUILTIN_OBSZ, "memmove": BUILTIN_OBSZ, "memcpy": BUILTIN_OBSZ, "memset": BUILTIN_OBSZ, "snprintf": BUILTIN_OBSZ0, "sprintf": UNDEFINED, "stpcpy": UNDEFINED, "stpncpy": BUILTIN_OBSZ0, "strcat": UNDEFINED, "strcpy": UNDEFINED, "strlcat": BUILTIN_OBSZ0, "strlcpy": BUILTIN_OBSZ0, "strncat": BUILTIN_OBSZ0, "strncpy": BUILTIN_OBSZ0, "vsnprintf": BUILTIN_OBSZ0, "vsprintf": UNDEFINED, },
		expected_userspace={"bcopy": BUILTIN_OBSZ, "bzero": BUILTIN_OBSZ, "memccpy": BUILTIN_OBSZ, "memmove": BUILTIN_OBSZ, "memcpy": BUILTIN_OBSZ, "memset": BUILTIN_OBSZ, "snprintf": BUILTIN_OBSZ0, "sprintf": BUILTIN_OBSZ0, "stpcpy": BUILTIN_OBSZ0, "stpncpy": BUILTIN_OBSZ0, "strcat": BUILTIN_OBSZ0, "strcpy": BUILTIN_OBSZ0, "strlcat": BUILTIN_OBSZ0, "strlcpy": BUILTIN_OBSZ0, "strncat": BUILTIN_OBSZ0, "strncpy": BUILTIN_OBSZ0, "vsnprintf": BUILTIN_OBSZ0, "vsprintf": BUILTIN_OBSZ0, },),

	TestParameters(posix_c_source=None, darwin_c_level="__DARWIN_C_FULL", fbounds_safety=None,
		expected_driverkit={"bcopy": BUILTIN_OBSZ, "bzero": BUILTIN_OBSZ, "memccpy": BUILTIN_OBSZ, "memmove": BUILTIN_OBSZ, "memcpy": BUILTIN_OBSZ, "memset": BUILTIN_OBSZ, "snprintf": BUILTIN_OBSZ0, "sprintf": UNDEFINED, "stpcpy": UNDEFINED, "stpncpy": BUILTIN_OBSZ0, "strcat": UNDEFINED, "strcpy": UNDEFINED, "strlcat": BUILTIN_OBSZ0, "strlcpy": BUILTIN_OBSZ0, "strncat": BUILTIN_OBSZ0, "strncpy": BUILTIN_OBSZ0, "vsnprintf": BUILTIN_OBSZ0, "vsprintf": UNDEFINED, },
		expected_userspace={"bcopy": BUILTIN_OBSZ, "bzero": BUILTIN_OBSZ, "memccpy": BUILTIN_OBSZ, "memmove": BUILTIN_OBSZ, "memcpy": BUILTIN_OBSZ, "memset": BUILTIN_OBSZ, "snprintf": BUILTIN_OBSZ0, "sprintf": BUILTIN_OBSZ0, "stpcpy": BUILTIN_OBSZ0, "stpncpy": BUILTIN_OBSZ0, "strcat": BUILTIN_OBSZ0, "strcpy": BUILTIN_OBSZ0, "strlcat": BUILTIN_OBSZ0, "strlcpy": BUILTIN_OBSZ0, "strncat": BUILTIN_OBSZ0, "strncpy": BUILTIN_OBSZ0, "vsnprintf": BUILTIN_OBSZ0, "vsprintf": BUILTIN_OBSZ0, },),

	TestParameters(posix_c_source=None, darwin_c_level="__DARWIN_C_FULL", fbounds_safety="on",
		expected_driverkit={"bcopy": BUILTIN_OBSZ, "bzero": BUILTIN_OBSZ, "memccpy": BUILTIN_OBSZ, "memmove": BUILTIN_OBSZ, "memcpy": BUILTIN_OBSZ, "memset": BUILTIN_OBSZ, "snprintf": BUILTIN_OBSZ0, "sprintf": UNDEFINED, "stpcpy": UNDEFINED, "stpncpy": BUILTIN_OBSZ0, "strcat": UNDEFINED, "strcpy": UNDEFINED, "strlcat": BUILTIN_OBSZ0, "strlcpy": BUILTIN_OBSZ0, "strncat": BUILTIN_OBSZ0, "strncpy": BUILTIN_OBSZ0, "vsnprintf": BUILTIN_OBSZ0, "vsprintf": UNDEFINED, },
		expected_userspace={"bcopy": BUILTIN_OBSZ, "bzero": BUILTIN_OBSZ, "memccpy": BUILTIN_OBSZ, "memmove": BUILTIN_OBSZ, "memcpy": BUILTIN_OBSZ, "memset": BUILTIN_OBSZ, "snprintf": BUILTIN_OBSZ0, "sprintf": BUILTIN_OBSZ0, "stpcpy": BUILTIN_OBSZ0, "stpncpy": BUILTIN_OBSZ0, "strcat": BUILTIN_OBSZ0, "strcpy": BUILTIN_OBSZ0, "strlcat": BUILTIN_OBSZ0, "strlcpy": BUILTIN_OBSZ0, "strncat": BUILTIN_OBSZ0, "strncpy": BUILTIN_OBSZ0, "vsnprintf": BUILTIN_OBSZ0, "vsprintf": BUILTIN_OBSZ0, },),

	TestParameters(posix_c_source=None, darwin_c_level="__DARWIN_C_FULL", fbounds_safety="on_attributes",
		expected_driverkit={"bcopy": PTRCHECK_OBSZ, "bzero": PTRCHECK_OBSZ, "memccpy": PTRCHECK_OBSZ, "memmove": PTRCHECK_OBSZ, "memcpy": PTRCHECK_OBSZ, "memset": PTRCHECK_OBSZ, "snprintf": PTRCHECK_OBSZ0, "sprintf": UNDEFINED, "stpcpy": UNDEFINED, "stpncpy": UNDEFINED, "strcat": UNDEFINED, "strcpy": UNDEFINED, "strlcat": PTRCHECK_OBSZ, "strlcpy": PTRCHECK_OBSZ, "strncat": PTRCHECK_OBSZ, "strncpy": UNDEFINED, "vsnprintf": PTRCHECK_OBSZ0, "vsprintf": UNDEFINED, },
		expected_userspace={"bcopy": PTRCHECK_OBSZ, "bzero": PTRCHECK_OBSZ, "memccpy": PTRCHECK_OBSZ, "memmove": PTRCHECK_OBSZ, "memcpy": PTRCHECK_OBSZ, "memset": PTRCHECK_OBSZ, "snprintf": PTRCHECK_OBSZ0, "sprintf": UNDEFINED, "stpcpy": UNDEFINED, "stpncpy": UNDEFINED, "strcat": UNDEFINED, "strcpy": UNDEFINED, "strlcat": PTRCHECK_OBSZ, "strlcpy": PTRCHECK_OBSZ, "strncat": UNDEFINED, "strncpy": UNDEFINED, "vsnprintf": PTRCHECK_OBSZ0, "vsprintf": UNDEFINED, },),
]


def parse_substitution_dictionary(stream):
	test_line_re = re.compile(rb"^test_(\w+):\s*([^(;\s]*).*$")
	result = {}
	for line in stream:
		m = test_line_re.match(line)
		if not m:
			continue
		key = m.group(1).decode("ASCII")
		if m.group(2) == b"undefined":
			result[key] = UNDEFINED
			continue

		# for all substitutions except bcopy/bzero (which boil down to memcpy/memset),
		# we check that the result line contains the right function name
		colon = line.index(b":") + 1
		if line.find(b"_" + m.group(1) + b"_", colon) == -1:
			if key not in ["bcopy", "bzero"]:
				result[key] = BADFUNC
				continue

		if m.group(2) == "":
			if "__libc_ptrchk_strbuf_chk" in line:
				result[key] = PTRCHECK_OBSZ
		else:
			builtin = b"__builtin" in m.group(2)
			obsz = False
			idx = line.find(b"__builtin_object_size")
			if idx != -1:
				open = line.index(b",", idx) + 1
				close = line.find(b")", open)
				obsz = line[open:close].strip() != b"0"

			if (builtin, obsz) == (True, True):
				result[key] = BUILTIN_OBSZ0
			elif (builtin, obsz) == (True, False):
				result[key] = BUILTIN_OBSZ
			elif (builtin, obsz) == (False, True):
				result[key] = PTRCHECK_OBSZ0
			elif (builtin, obsz) == (False, False):
				result[key] = PTRCHECK_OBSZ
	return result


def print_substitution_dictionary(d):
	print("{", end="")
	for k in sorted(d.keys()):
		print('"%s": %s, ' % (k, d[k]), end="")
	print("},")


def main():
	parser = ArgumentParser(description="test that bounds-sensitive functions have _chk substitutions")
	parser.add_argument("test_file", default="./fortify_source.c", help="file to preprocess")
	parser.add_argument("--sdk", default=os.getenv("SDKROOT"), help="SDK to test for (defaults to $SDKROOT)")
	parser.add_argument("-g", "--generate", action="store_true", help="print observed output instead of comparing it with desired output")
	parser.add_argument("-I", "--include-dir", action="append", default=[], help="Additional include paths (you can point this to the usr/include of a libc root)")
	parser.add_argument("-v", "--verbose", action="store_true", help="Print clang commands that are executed")
	parser.add_argument("-s", "--check-syntax", action="store_true", help="Also run clang with -fsyntax-only to check that this actually builds")
	arguments = parser.parse_args()
	if arguments.sdk is None:
		sys.stderr.write("*** $SDKROOT is not set\n")
		sys.exit(1)

	is_driverkit = "driverkit" in arguments.sdk.lower()

	def verbose_print(value):
		if arguments.verbose:
			print(" ".join(value))
		return value

	failed = False
	for (posix_c_source, darwin_c_level, fbounds_safety, expected_dk, expected_us) in PARAMETERS:
		clang_args = [
			"xcrun", "--sdk", arguments.sdk, "clang", "-Werror", "-D_FORTIFY_SOURCE=2", arguments.test_file]
		for inc in arguments.include_dir:
			clang_args.append("-isystem")
			clang_args.append(inc)
		if posix_c_source is not None:
			clang_args.append("-D_TEST_POSIX_C_SOURCE=%s" % posix_c_source)
		if darwin_c_level is not None:
			clang_args.append("-D_TEST_DARWIN_C_LEVEL=%s" % darwin_c_level)
		if fbounds_safety == "on":
			clang_args.append("-fbounds-safety")
			clang_args.append("-DUNDEF_BOUNDS_SAFETY_ATTRIBUTES")
		elif fbounds_safety == "on_attributes":
			clang_args.append("-fbounds-safety")
			clang_args.append("-D__LIBC_STAGED_BOUNDS_SAFETY_ATTRIBUTES")

		if arguments.check_syntax:
			syntax_check = list(clang_args)
			syntax_check.extend(("-fsyntax-only", "-Wno-nullability-completeness"))
			subprocess.check_call(verbose_print(syntax_check))

		clang_args.extend(("-E", "-o", "-"))
		p = subprocess.Popen(verbose_print(clang_args), stdin=subprocess.DEVNULL, stdout=subprocess.PIPE)

		d = parse_substitution_dictionary(p.stdout)
		p.wait()
		if arguments.generate:
			print_substitution_dictionary(d)
			continue

		expected = expected_dk if is_driverkit else expected_us
		if d == expected:
			continue

		failed = True
		params = []
		params.append("_POSIX_C_SOURCE=%r" % posix_c_source)
		params.append("__DARWIN_C_LEVEL=%r" % darwin_c_level)
		params.append("-f%sbounds-safety" % ("" if fbounds_safety else "no"))
		if fbounds_safety:
			params.append("-%s__LIBC_STAGED_BOUNDS_SAFETY_ATTRIBUTES" % ("D" if fbounds_safety == "on_attributes" else "U"))
		print("*** FAILED: " + ", ".join(params))
		for k in sorted(d.keys()):
			if k not in expected:
				print("***  %s is %s (missing in expected dictionary)" % (k, d[k]))
			elif d[k] != expected[k]:
				print("***  %s is %s (expected: %s)" % (k, d[k], expected[k]))
		for k in sorted(expected.keys()):
			if k not in d:
				print("***  %s is missing (expected: %s)" % (k, expected[k]))

	sys.exit(1 if failed else 0)


if __name__ == "__main__":
	main()