1*bbb1b6f9SApple OSS Distributions## 2*bbb1b6f9SApple OSS Distributions# Copyright (c) 2023 Apple Inc. All rights reserved. 3*bbb1b6f9SApple OSS Distributions# 4*bbb1b6f9SApple OSS Distributions# @APPLE_OSREFERENCE_LICENSE_HEADER_START@ 5*bbb1b6f9SApple OSS Distributions# 6*bbb1b6f9SApple OSS Distributions# This file contains Original Code and/or Modifications of Original Code 7*bbb1b6f9SApple OSS Distributions# as defined in and that are subject to the Apple Public Source License 8*bbb1b6f9SApple OSS Distributions# Version 2.0 (the 'License'). You may not use this file except in 9*bbb1b6f9SApple OSS Distributions# compliance with the License. The rights granted to you under the License 10*bbb1b6f9SApple OSS Distributions# may not be used to create, or enable the creation or redistribution of, 11*bbb1b6f9SApple OSS Distributions# unlawful or unlicensed copies of an Apple operating system, or to 12*bbb1b6f9SApple OSS Distributions# circumvent, violate, or enable the circumvention or violation of, any 13*bbb1b6f9SApple OSS Distributions# terms of an Apple operating system software license agreement. 14*bbb1b6f9SApple OSS Distributions# 15*bbb1b6f9SApple OSS Distributions# Please obtain a copy of the License at 16*bbb1b6f9SApple OSS Distributions# http://www.opensource.apple.com/apsl/ and read it before using this file. 17*bbb1b6f9SApple OSS Distributions# 18*bbb1b6f9SApple OSS Distributions# The Original Code and all software distributed under the License are 19*bbb1b6f9SApple OSS Distributions# distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 20*bbb1b6f9SApple OSS Distributions# EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 21*bbb1b6f9SApple OSS Distributions# INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 22*bbb1b6f9SApple OSS Distributions# FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 23*bbb1b6f9SApple OSS Distributions# Please see the License for the specific language governing rights and 24*bbb1b6f9SApple OSS Distributions# limitations under the License. 25*bbb1b6f9SApple OSS Distributions# 26*bbb1b6f9SApple OSS Distributions# @APPLE_OSREFERENCE_LICENSE_HEADER_END@ 27*bbb1b6f9SApple OSS Distributions## 28*bbb1b6f9SApple OSS Distributions 29*bbb1b6f9SApple OSS Distributions""" LLDB Scripted Process designed for unit-testing mock support. """ 30*bbb1b6f9SApple OSS Distributions 31*bbb1b6f9SApple OSS Distributionsimport unittest 32*bbb1b6f9SApple OSS Distributionsimport sys 33*bbb1b6f9SApple OSS Distributionsimport logging 34*bbb1b6f9SApple OSS Distributionsfrom collections import namedtuple 35*bbb1b6f9SApple OSS Distributionsfrom pathlib import Path 36*bbb1b6f9SApple OSS Distributions 37*bbb1b6f9SApple OSS Distributionsimport lldb 38*bbb1b6f9SApple OSS Distributionsimport lldbmock.memorymock 39*bbb1b6f9SApple OSS Distributionsfrom lldb.plugins.scripted_process import ScriptedProcess, ScriptedThread 40*bbb1b6f9SApple OSS Distributionsfrom lldbtest.unittest import LLDBTextTestRunner, LLDBJSONTestRunner 41*bbb1b6f9SApple OSS Distributionsfrom lldbtest.coverage import CoverageContext 42*bbb1b6f9SApple OSS Distributions 43*bbb1b6f9SApple OSS Distributions# location of this script 44*bbb1b6f9SApple OSS DistributionsSCRIPT_PATH = Path(__file__).parent 45*bbb1b6f9SApple OSS Distributions 46*bbb1b6f9SApple OSS Distributions# Configure logging. 47*bbb1b6f9SApple OSS Distributions# This script is loaded first so we can share root logger with other files. 48*bbb1b6f9SApple OSS Distributions 49*bbb1b6f9SApple OSS Distributionslogging.root.setLevel(logging.INFO) 50*bbb1b6f9SApple OSS Distributionslogging.basicConfig(level=logging.INFO) 51*bbb1b6f9SApple OSS Distributionslldb.test_logger = logging.getLogger("UnitTest") 52*bbb1b6f9SApple OSS Distributionslldb.test_logger.getChild("ScriptedProcess").info("Log initialized.") 53*bbb1b6f9SApple OSS Distributions 54*bbb1b6f9SApple OSS Distributions 55*bbb1b6f9SApple OSS Distributionsclass TestThread(ScriptedThread): 56*bbb1b6f9SApple OSS Distributions """ Scripted thread that represents custom thread state. """ 57*bbb1b6f9SApple OSS Distributions 58*bbb1b6f9SApple OSS Distributions 59*bbb1b6f9SApple OSS Distributionsclass TestProcess(ScriptedProcess): 60*bbb1b6f9SApple OSS Distributions """ Scripted process that represents target's memory. """ 61*bbb1b6f9SApple OSS Distributions 62*bbb1b6f9SApple OSS Distributions LOG = lldb.test_logger.getChild("ScriptedProcess") 63*bbb1b6f9SApple OSS Distributions 64*bbb1b6f9SApple OSS Distributions MockElem = namedtuple('MockElem', ['addr', 'mock']) 65*bbb1b6f9SApple OSS Distributions 66*bbb1b6f9SApple OSS Distributions def __init__(self, ctx: lldb.SBExecutionContext, args: lldb.SBStructuredData): 67*bbb1b6f9SApple OSS Distributions super().__init__(ctx, args) 68*bbb1b6f9SApple OSS Distributions 69*bbb1b6f9SApple OSS Distributions self.verbose = args.GetValueForKey("verbose").GetBooleanValue() 70*bbb1b6f9SApple OSS Distributions self.debug = args.GetValueForKey("debug").GetBooleanValue() 71*bbb1b6f9SApple OSS Distributions self.json = args.GetValueForKey("json").GetStringValue(256) 72*bbb1b6f9SApple OSS Distributions print(self.json) 73*bbb1b6f9SApple OSS Distributions self._mocks = [] 74*bbb1b6f9SApple OSS Distributions 75*bbb1b6f9SApple OSS Distributions # 76*bbb1b6f9SApple OSS Distributions # Testing framework API 77*bbb1b6f9SApple OSS Distributions # 78*bbb1b6f9SApple OSS Distributions 79*bbb1b6f9SApple OSS Distributions def add_mock(self, addr: int, mock: lldbmock.memorymock.BaseMock): 80*bbb1b6f9SApple OSS Distributions # Do not allow overlaping mocks to keep logic simple. 81*bbb1b6f9SApple OSS Distributions if any(me for me in self._mocks 82*bbb1b6f9SApple OSS Distributions if me.addr <= addr < (me.addr + me.mock.size)): 83*bbb1b6f9SApple OSS Distributions raise ValueError("Overlaping mock with") 84*bbb1b6f9SApple OSS Distributions 85*bbb1b6f9SApple OSS Distributions self._mocks.append(TestProcess.MockElem(addr, mock)) 86*bbb1b6f9SApple OSS Distributions 87*bbb1b6f9SApple OSS Distributions def remove_mock(self, mock: lldbmock.memorymock.BaseMock): 88*bbb1b6f9SApple OSS Distributions raise NotImplementedError("Mock removal not implemented yet") 89*bbb1b6f9SApple OSS Distributions 90*bbb1b6f9SApple OSS Distributions def reset_mocks(self): 91*bbb1b6f9SApple OSS Distributions """ Remove all mocks. """ 92*bbb1b6f9SApple OSS Distributions self._mocks = [] 93*bbb1b6f9SApple OSS Distributions 94*bbb1b6f9SApple OSS Distributions # 95*bbb1b6f9SApple OSS Distributions # LLDB Scripted Process Implementation 96*bbb1b6f9SApple OSS Distributions # 97*bbb1b6f9SApple OSS Distributions 98*bbb1b6f9SApple OSS Distributions def get_memory_region_containing_address( 99*bbb1b6f9SApple OSS Distributions self, 100*bbb1b6f9SApple OSS Distributions addr: int 101*bbb1b6f9SApple OSS Distributions ) -> lldb.SBMemoryRegionInfo: 102*bbb1b6f9SApple OSS Distributions # A generic answer should work in our case 103*bbb1b6f9SApple OSS Distributions return lldb.SBMemoryRegionInfo() 104*bbb1b6f9SApple OSS Distributions 105*bbb1b6f9SApple OSS Distributions def read_memory_at_address( 106*bbb1b6f9SApple OSS Distributions self, 107*bbb1b6f9SApple OSS Distributions addr: int, 108*bbb1b6f9SApple OSS Distributions size: int, 109*bbb1b6f9SApple OSS Distributions error: lldb.SBError = lldb.SBError() 110*bbb1b6f9SApple OSS Distributions ) -> lldb.SBData: 111*bbb1b6f9SApple OSS Distributions """ Performs I/O read on top of set of mock structures. 112*bbb1b6f9SApple OSS Distributions Undefined regions are set to 0. 113*bbb1b6f9SApple OSS Distributions """ 114*bbb1b6f9SApple OSS Distributions data = lldb.SBData() 115*bbb1b6f9SApple OSS Distributions rawdata = bytearray(size) 116*bbb1b6f9SApple OSS Distributions 117*bbb1b6f9SApple OSS Distributions # Avoid delegating reads back to SBTarget. That leads to infinite 118*bbb1b6f9SApple OSS Distributions # recursion as SBTarget calls to read from SBProcess instance. 119*bbb1b6f9SApple OSS Distributions 120*bbb1b6f9SApple OSS Distributions # Overlay mocks on top of the I/O. 121*bbb1b6f9SApple OSS Distributions for maddr, mock in ( 122*bbb1b6f9SApple OSS Distributions (me.addr, me.mock) for me 123*bbb1b6f9SApple OSS Distributions in self._mocks): 124*bbb1b6f9SApple OSS Distributions 125*bbb1b6f9SApple OSS Distributions # check for overlap 126*bbb1b6f9SApple OSS Distributions start_addr = max(addr, maddr) 127*bbb1b6f9SApple OSS Distributions end_addr = min(addr + size, maddr + mock.size) 128*bbb1b6f9SApple OSS Distributions 129*bbb1b6f9SApple OSS Distributions if end_addr < start_addr: 130*bbb1b6f9SApple OSS Distributions # no intersection of I/O and mock entry 131*bbb1b6f9SApple OSS Distributions continue 132*bbb1b6f9SApple OSS Distributions 133*bbb1b6f9SApple OSS Distributions offs = start_addr - maddr # In the mock space 134*bbb1b6f9SApple OSS Distributions boffs = start_addr - addr # In mbuffer space 135*bbb1b6f9SApple OSS Distributions sz = end_addr - start_addr # size to read 136*bbb1b6f9SApple OSS Distributions 137*bbb1b6f9SApple OSS Distributions self.LOG.debug("overlap: %x +%d", offs, sz) 138*bbb1b6f9SApple OSS Distributions self.LOG.debug("raw read %x +%d", addr, size) 139*bbb1b6f9SApple OSS Distributions self.LOG.debug("final read %x +%d", start_addr - addr, sz) 140*bbb1b6f9SApple OSS Distributions #self.LOG.debug("data: %s", mock.getData()[offs: offs + sz]) 141*bbb1b6f9SApple OSS Distributions 142*bbb1b6f9SApple OSS Distributions # Merge mock data into I/O buffer. 143*bbb1b6f9SApple OSS Distributions rawdata[boffs: boffs + sz] = mock.getData()[offs:offs+sz] 144*bbb1b6f9SApple OSS Distributions 145*bbb1b6f9SApple OSS Distributions data.SetDataWithOwnership( 146*bbb1b6f9SApple OSS Distributions error, 147*bbb1b6f9SApple OSS Distributions rawdata, 148*bbb1b6f9SApple OSS Distributions lldb.eByteOrderLittle, 149*bbb1b6f9SApple OSS Distributions 8 150*bbb1b6f9SApple OSS Distributions ) 151*bbb1b6f9SApple OSS Distributions 152*bbb1b6f9SApple OSS Distributions return data 153*bbb1b6f9SApple OSS Distributions 154*bbb1b6f9SApple OSS Distributions def get_loaded_images(self) -> list: 155*bbb1b6f9SApple OSS Distributions return self.loaded_images 156*bbb1b6f9SApple OSS Distributions 157*bbb1b6f9SApple OSS Distributions def get_process_id(self) -> int: 158*bbb1b6f9SApple OSS Distributions return 0 159*bbb1b6f9SApple OSS Distributions 160*bbb1b6f9SApple OSS Distributions def is_alive(self) -> bool: 161*bbb1b6f9SApple OSS Distributions return True 162*bbb1b6f9SApple OSS Distributions 163*bbb1b6f9SApple OSS Distributions def get_scripted_thread_plugin(self) -> str: 164*bbb1b6f9SApple OSS Distributions return __class__.__module__ + '.' + TestThread.__name__ 165*bbb1b6f9SApple OSS Distributions 166*bbb1b6f9SApple OSS Distributions 167*bbb1b6f9SApple OSS Distributionsdef run_unit_tests(debugger, _command, _exe_ctx, _result, _internal_dict): 168*bbb1b6f9SApple OSS Distributions """ Runs standart Python unit tests inside LLDB. """ 169*bbb1b6f9SApple OSS Distributions 170*bbb1b6f9SApple OSS Distributions # Obtain current plugin instance 171*bbb1b6f9SApple OSS Distributions sp = debugger.GetSelectedTarget().GetProcess().GetScriptedImplementation() 172*bbb1b6f9SApple OSS Distributions 173*bbb1b6f9SApple OSS Distributions # Enable debugging 174*bbb1b6f9SApple OSS Distributions if sp.debug: 175*bbb1b6f9SApple OSS Distributions logging.root.setLevel(logging.DEBUG) 176*bbb1b6f9SApple OSS Distributions logging.basicConfig(level=logging.DEBUG) 177*bbb1b6f9SApple OSS Distributions 178*bbb1b6f9SApple OSS Distributions log = logging.getLogger("ScriptedProcess") 179*bbb1b6f9SApple OSS Distributions log.info("Running tests") 180*bbb1b6f9SApple OSS Distributions log.info("Using path: %s", SCRIPT_PATH / "lldb_tests") 181*bbb1b6f9SApple OSS Distributions tests = unittest.TestLoader().discover(SCRIPT_PATH / "lldb_tests") 182*bbb1b6f9SApple OSS Distributions 183*bbb1b6f9SApple OSS Distributions # Select runner class 184*bbb1b6f9SApple OSS Distributions RunnerClass = LLDBJSONTestRunner if sp.json else LLDBTextTestRunner 185*bbb1b6f9SApple OSS Distributions 186*bbb1b6f9SApple OSS Distributions # Open output file if requested 187*bbb1b6f9SApple OSS Distributions if sp.json: 188*bbb1b6f9SApple OSS Distributions with open(f"{sp.json}-lldb.json", 'wt') as outfile: 189*bbb1b6f9SApple OSS Distributions runner = RunnerClass(verbosity=2 if sp.verbose else 1, debug=sp.debug, 190*bbb1b6f9SApple OSS Distributions stream=outfile) 191*bbb1b6f9SApple OSS Distributions runner.run(tests) 192*bbb1b6f9SApple OSS Distributions else: 193*bbb1b6f9SApple OSS Distributions runner = RunnerClass(stream=sys.stderr, verbosity=2 if sp.verbose else 1, 194*bbb1b6f9SApple OSS Distributions debug=sp.debug) 195*bbb1b6f9SApple OSS Distributions runner.run(tests) 196*bbb1b6f9SApple OSS Distributions 197*bbb1b6f9SApple OSS Distributionsdef __lldb_init_module(_debugger, _internal_dict): 198*bbb1b6f9SApple OSS Distributions """ LLDB entry point """ 199*bbb1b6f9SApple OSS Distributions 200*bbb1b6f9SApple OSS Distributions # XNU has really bad import structure and it is easy to create circular 201*bbb1b6f9SApple OSS Distributions # dependencies. Forcibly import XNU before tests are ran so the final 202*bbb1b6f9SApple OSS Distributions # result is close to what imports from a dSYM would end up with. 203*bbb1b6f9SApple OSS Distributions with CoverageContext(): 204*bbb1b6f9SApple OSS Distributions lldb.debugger.HandleCommand( 205*bbb1b6f9SApple OSS Distributions f"command script import {SCRIPT_PATH / '../xnu.py'}") 206*bbb1b6f9SApple OSS Distributions 207*bbb1b6f9SApple OSS Distributions logging.getLogger("ScriptedProcess").info("Running LLDB module init.") 208*bbb1b6f9SApple OSS Distributions lldb.debugger.HandleCommand(f"command script add " 209*bbb1b6f9SApple OSS Distributions f"-f {__name__}.{run_unit_tests.__name__}" 210*bbb1b6f9SApple OSS Distributions f" run-unit-tests") 211