nuttx/tools/kasan_global.py

350 lines
11 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
############################################################################
# tools/kasan_global.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you 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.
#
############################################################################
import argparse
import os
from construct import Int32ul, Int64ul, Struct
from elftools.elf.elffile import ELFFile
debug = False
# The maximum gap that two data segments can tolerate
KASAN_MAX_DATA_GAP = 1 << 16
# The section where the global variable descriptor
# generated by the compiler is located
KASAN_SECTION = ".kasan.global"
# Segments stored in the shadow area
KASAN_SHADOW_SECTION = ".kasan.shadows"
# The structure of parsing strings required for 32-bit and 64 bit
KASAN_GLOBAL_STRUCT_32 = Struct(
"beg" / Int32ul,
"size" / Int32ul,
"size_with_redzone" / Int32ul,
"name" / Int32ul,
"module_name" / Int32ul,
"has_dynamic_init" / Int32ul,
"location" / Int32ul,
"odr_indicator" / Int32ul,
)
KASAN_GLOBAL_STRUCT_64 = Struct(
"beg" / Int64ul,
"size" / Int64ul,
"size_with_redzone" / Int64ul,
"name" / Int64ul,
"module_name" / Int64ul,
"has_dynamic_init" / Int64ul,
"location" / Int64ul,
"odr_indicator" / Int64ul,
)
# Global configuration information
class Config:
def __init__(self, outpath, elf, ldscript, align):
self.outpath = outpath
self.elf = elf
self.ldscript = ldscript
self.align = align
if self.elf is None or os.path.exists(self.elf) is False:
self.elf = None
return
with open(self.elf, "rb") as file:
elf_file = ELFFile(file)
elf_header = elf_file.header
# Obtain bit width
bitness = elf_header["e_ident"]["EI_CLASS"]
if bitness == "ELFCLASS32":
self.bitwides = 32
elif bitness == "ELFCLASS64":
self.bitwides = 64
# Big and little end
endianness = elf_header["e_ident"]["EI_DATA"]
if endianness == "ELFDATA2LSB":
self.endian = "little"
elif endianness == "ELFDATA2MSB":
self.endian = "big"
class KASanRegion:
def __init__(self, start, end, align, endian, bitwides) -> None:
self.start = start
self.end = end
self.align = align
self.endian = endian
self.bitwides = bitwides
self.size = int((end - start) // self.align // self.bitwides) + 1
self.__shadow = [0] * self.size
def shadow(self) -> bytearray:
ret = bytearray()
for i in self.__shadow:
for j in range(self.bitwides // 8):
if self.endian == "little":
ret.append((i >> (j * 8)) & 0xFF)
else:
ret.append((i >> ((self.bitwides // 8 - j - 1) * 8)) & 0xFF)
return ret
def mark_bit(self, index, nbit, nbits):
for i in range(nbits):
self.__shadow[index] |= 1 << nbit
nbit += 1
if nbit == self.bitwides:
index += 1
nbit = 0
def poison(self, dict):
dict_size = dict["size"]
if dict_size % self.align:
dict_size = int((dict_size + self.align - 1) // self.align) * self.align
distance = (dict["beg"] + dict_size - self.start) // self.align
# Index of the marked shadow area
index = int(distance // self.bitwides)
# The X-th bit of the specific byte marked
nbit = distance % self.bitwides
# Number of digits to be marked
nbits = (dict["size_with_redzone"] - dict_size) // self.align
if debug:
print(
"regin: %08x addr: %08x size: %d bits: %d || poison index: %d nbit: %d nbits: %d"
% (
self.start,
dict["beg"],
dict["size"],
int(dict["size_with_redzone"] // self.align),
index,
nbit,
nbits,
)
)
# Using 32bytes: with 1bit alignment,
# only one bit of inaccessible area exists for each pair of global variables.
self.mark_bit(index, nbit, nbits)
class KASanInfo:
def __init__(self, align, endian, bitwides) -> None:
self.align = align
self.endian = endian
self.bitwides = bitwides
# Record the starting position of the merged data block
self.data_sections = []
# Record the kasan region corresponding to each data block
self.regions: list[KASanRegion] = []
def merge_ranges(self, dict):
if len(self.data_sections) == 0:
self.data_sections.append(
[dict["beg"], dict["beg"] + dict["size_with_redzone"]]
)
return
start = dict["beg"]
end = dict["beg"] + dict["size_with_redzone"]
if start - self.data_sections[-1][1] <= KASAN_MAX_DATA_GAP:
self.data_sections[-1][1] = end
else:
self.data_sections.append([start, end])
def create_region(self):
for i in self.data_sections:
start = i[0]
end = i[1]
if debug:
print("KAsan Shadow Block: %08x ---- %08x" % (start, end))
self.regions.append(
KASanRegion(start, end, self.align, self.endian, self.bitwides)
)
def mark_shadow(self, dict):
for i in self.regions:
start = i.start
end = i.end
if start <= dict["beg"] and dict["beg"] + dict["size_with_redzone"] <= end:
i.poison(dict)
break
# Global variable descriptor
def get_global_dict(GLOBAL_STRUCT: Struct, bytes: bytes):
dict = GLOBAL_STRUCT.parse(bytes)
return {
"beg": dict.beg,
"size": dict.size,
"size_with_redzone": dict.size_with_redzone,
}
def get_elf_section(elf, section) -> bytes:
with open(elf, "rb") as file:
elf = ELFFile(file)
for i in elf.iter_sections():
if i.name == section:
return i.data()
return None
def long_to_bytestring(bitwides, endian, value: int) -> str:
res = ""
byte_array = value.to_bytes(length=int(bitwides / 8), byteorder=endian)
for i in byte_array:
res += "0x%02x, " % (i)
return res
def create_kasan_file(config: Config, region_list=[]):
region: KASanRegion = None
with open(config.outpath, "w") as file:
file.write("#include <nuttx/nuttx.h>\n")
# Write the kasan region array
for i in range(len(region_list)):
file.write(
'static const unsigned char locate_data("%s")'
"\nglobals_region%d[] = {\n" % (KASAN_SHADOW_SECTION, i)
)
region = region_list[i]
# Fill the array of regions
# The filling order is as follows, from mm/kasan/generic.c
# This type will be used to record the size of the shadow area
# to facilitate the program to traverse the array.
# 2. uintptr_t begin;
# 3. uintptr_t end;
# 4. uintptr_t shadow[1];
file.write(
"%s\n"
% (long_to_bytestring(config.bitwides, config.endian, region.start))
)
file.write(
"%s\n"
% (long_to_bytestring(config.bitwides, config.endian, region.end))
)
shadow = region.shadow()
for j in range(len(shadow)):
if j % 8 == 0:
file.write("\n")
file.write("0x%02x, " % (shadow[j]))
file.write("\n};")
# Record kasan region pointer location
file.write("\nconst unsigned long g_global_region[] = {\n")
for i in range(len(region_list)):
file.write("\n(const unsigned long)&globals_region%d," % (i))
file.write("0x00\n};")
# Error extraction section processing to enable the program to compile successfully
def handle_error(config: Config, string=None):
if string:
print(string)
create_kasan_file(config)
exit(0)
def parse_args() -> Config:
global debug
parser = argparse.ArgumentParser(description="Build kasan global variable region")
parser.add_argument("-o", "--outpath", help="outpath")
parser.add_argument("-a", "--align", default=32, type=int, help="alignment")
parser.add_argument("-d", "--ldscript", help="ld script path(Only support sim)")
parser.add_argument("-e", "--elffile", help="elffile")
parser.add_argument(
"--debug",
action="store_true",
default=False,
help="if enabled, it will show more logs.",
)
args = parser.parse_args()
debug = args.debug
return Config(args.outpath, args.elffile, args.ldscript, args.align)
def main():
config = parse_args()
if config.elf is None:
handle_error(config)
# Identify the segment information that needs to be extracted
section = get_elf_section(config.elf, KASAN_SECTION)
if section is None:
handle_error(
config,
"Please update the link script, section ['%s'] cannot be found"
% (KASAN_SECTION),
)
# List of global variable descriptors
dict_list = []
# Extract all global variable descriptors within the
if config.bitwides == 32:
global_struct = KASAN_GLOBAL_STRUCT_32
elif config.bitwides == 64:
global_struct = KASAN_GLOBAL_STRUCT_64
step = global_struct.sizeof()
for i in range(0, len(section), step):
dict = get_global_dict(global_struct, section[i : i + step])
dict_list.append(dict)
dict_list = sorted(dict_list, key=lambda item: item["beg"])
# Merge all global variables to obtain several segments of the distribution range
kasan = KASanInfo(config.align, config.endian, config.bitwides)
for i in dict_list:
kasan.merge_ranges(i)
# Create empty shadow zone
kasan.create_region()
# Mark on the shadow area
for i in dict_list:
kasan.mark_shadow(i)
create_kasan_file(config, kasan.regions)
if __name__ == "__main__":
main()