1*795d594fSAndroid Build Coastguard Worker#!/usr/bin/python3 2*795d594fSAndroid Build Coastguard Worker# 3*795d594fSAndroid Build Coastguard Worker# Copyright (C) 2021 The Android Open Source Project 4*795d594fSAndroid Build Coastguard Worker# 5*795d594fSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License"); 6*795d594fSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License. 7*795d594fSAndroid Build Coastguard Worker# You may obtain a copy of the License at 8*795d594fSAndroid Build Coastguard Worker# 9*795d594fSAndroid Build Coastguard Worker# http://www.apache.org/licenses/LICENSE-2.0 10*795d594fSAndroid Build Coastguard Worker# 11*795d594fSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software 12*795d594fSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS, 13*795d594fSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14*795d594fSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and 15*795d594fSAndroid Build Coastguard Worker# limitations under the License. 16*795d594fSAndroid Build Coastguard Worker 17*795d594fSAndroid Build Coastguard Worker# 18*795d594fSAndroid Build Coastguard Worker# Generates profiles from the set of all methods in a given set of dex/jars and 19*795d594fSAndroid Build Coastguard Worker# bisects to find minimal repro sets. 20*795d594fSAndroid Build Coastguard Worker# 21*795d594fSAndroid Build Coastguard Worker 22*795d594fSAndroid Build Coastguard Workerimport shlex 23*795d594fSAndroid Build Coastguard Workerimport argparse 24*795d594fSAndroid Build Coastguard Workerimport pylibdexfile 25*795d594fSAndroid Build Coastguard Workerimport math 26*795d594fSAndroid Build Coastguard Workerimport subprocess 27*795d594fSAndroid Build Coastguard Workerfrom collections import namedtuple 28*795d594fSAndroid Build Coastguard Workerimport sys 29*795d594fSAndroid Build Coastguard Workerimport random 30*795d594fSAndroid Build Coastguard Workerimport os 31*795d594fSAndroid Build Coastguard Worker 32*795d594fSAndroid Build Coastguard WorkerApkEntry = namedtuple("ApkEntry", ["file", "location"]) 33*795d594fSAndroid Build Coastguard Worker 34*795d594fSAndroid Build Coastguard Worker 35*795d594fSAndroid Build Coastguard Workerdef get_parser(): 36*795d594fSAndroid Build Coastguard Worker parser = argparse.ArgumentParser( 37*795d594fSAndroid Build Coastguard Worker description="Bisect profile contents. We will wait while the user runs test" 38*795d594fSAndroid Build Coastguard Worker ) 39*795d594fSAndroid Build Coastguard Worker 40*795d594fSAndroid Build Coastguard Worker class ApkAction(argparse.Action): 41*795d594fSAndroid Build Coastguard Worker 42*795d594fSAndroid Build Coastguard Worker def __init__(self, option_strings, dest, **kwargs): 43*795d594fSAndroid Build Coastguard Worker super(ApkAction, self).__init__(option_strings, dest, **kwargs) 44*795d594fSAndroid Build Coastguard Worker 45*795d594fSAndroid Build Coastguard Worker def __call__(self, parser, namespace, values, option_string=None): 46*795d594fSAndroid Build Coastguard Worker lst = getattr(namespace, self.dest) 47*795d594fSAndroid Build Coastguard Worker if lst is None: 48*795d594fSAndroid Build Coastguard Worker setattr(namespace, self.dest, []) 49*795d594fSAndroid Build Coastguard Worker lst = getattr(namespace, self.dest) 50*795d594fSAndroid Build Coastguard Worker if len(values) == 1: 51*795d594fSAndroid Build Coastguard Worker values = (values[0], values[0]) 52*795d594fSAndroid Build Coastguard Worker assert len(values) == 2, values 53*795d594fSAndroid Build Coastguard Worker lst.append(ApkEntry(*values)) 54*795d594fSAndroid Build Coastguard Worker 55*795d594fSAndroid Build Coastguard Worker apks = parser.add_argument_group(title="APK selection") 56*795d594fSAndroid Build Coastguard Worker apks.add_argument( 57*795d594fSAndroid Build Coastguard Worker "--apk", 58*795d594fSAndroid Build Coastguard Worker action=ApkAction, 59*795d594fSAndroid Build Coastguard Worker dest="apks", 60*795d594fSAndroid Build Coastguard Worker nargs=1, 61*795d594fSAndroid Build Coastguard Worker default=[], 62*795d594fSAndroid Build Coastguard Worker help="an apk/dex/jar to get methods from. Uses same path as location. " + 63*795d594fSAndroid Build Coastguard Worker "Use --apk-and-location if this isn't desired." 64*795d594fSAndroid Build Coastguard Worker ) 65*795d594fSAndroid Build Coastguard Worker apks.add_argument( 66*795d594fSAndroid Build Coastguard Worker "--apk-and-location", 67*795d594fSAndroid Build Coastguard Worker action=ApkAction, 68*795d594fSAndroid Build Coastguard Worker nargs=2, 69*795d594fSAndroid Build Coastguard Worker dest="apks", 70*795d594fSAndroid Build Coastguard Worker help="an apk/dex/jar + location to get methods from." 71*795d594fSAndroid Build Coastguard Worker ) 72*795d594fSAndroid Build Coastguard Worker profiles = parser.add_argument_group( 73*795d594fSAndroid Build Coastguard Worker title="Profile selection").add_mutually_exclusive_group() 74*795d594fSAndroid Build Coastguard Worker profiles.add_argument( 75*795d594fSAndroid Build Coastguard Worker "--input-text-profile", help="a text profile to use for bisect") 76*795d594fSAndroid Build Coastguard Worker profiles.add_argument("--input-profile", help="a profile to use for bisect") 77*795d594fSAndroid Build Coastguard Worker parser.add_argument( 78*795d594fSAndroid Build Coastguard Worker "--output-source", help="human readable file create the profile from") 79*795d594fSAndroid Build Coastguard Worker parser.add_argument("--test-exec", help="file to exec (without arguments) to test a" + 80*795d594fSAndroid Build Coastguard Worker " candidate. Test should exit 0 if the issue" + 81*795d594fSAndroid Build Coastguard Worker " is not present and non-zero if the issue is" + 82*795d594fSAndroid Build Coastguard Worker " present.") 83*795d594fSAndroid Build Coastguard Worker parser.add_argument("output_file", help="file we will write the profiles to") 84*795d594fSAndroid Build Coastguard Worker return parser 85*795d594fSAndroid Build Coastguard Worker 86*795d594fSAndroid Build Coastguard Worker 87*795d594fSAndroid Build Coastguard Workerdef dump_files(meths, args, output): 88*795d594fSAndroid Build Coastguard Worker for m in meths: 89*795d594fSAndroid Build Coastguard Worker print("HS{}".format(m), file=output) 90*795d594fSAndroid Build Coastguard Worker output.flush() 91*795d594fSAndroid Build Coastguard Worker profman_args = [ 92*795d594fSAndroid Build Coastguard Worker "profmand", "--reference-profile-file={}".format(args.output_file), 93*795d594fSAndroid Build Coastguard Worker "--create-profile-from={}".format(args.output_source) 94*795d594fSAndroid Build Coastguard Worker ] 95*795d594fSAndroid Build Coastguard Worker print(" ".join(map(shlex.quote, profman_args))) 96*795d594fSAndroid Build Coastguard Worker for apk in args.apks: 97*795d594fSAndroid Build Coastguard Worker profman_args += [ 98*795d594fSAndroid Build Coastguard Worker "--apk={}".format(apk.file), "--dex-location={}".format(apk.location) 99*795d594fSAndroid Build Coastguard Worker ] 100*795d594fSAndroid Build Coastguard Worker profman = subprocess.run(profman_args) 101*795d594fSAndroid Build Coastguard Worker profman.check_returncode() 102*795d594fSAndroid Build Coastguard Worker 103*795d594fSAndroid Build Coastguard Worker 104*795d594fSAndroid Build Coastguard Workerdef get_answer(args): 105*795d594fSAndroid Build Coastguard Worker if args.test_exec is None: 106*795d594fSAndroid Build Coastguard Worker while True: 107*795d594fSAndroid Build Coastguard Worker answer = input("Does the file at {} cause the issue (y/n):".format( 108*795d594fSAndroid Build Coastguard Worker args.output_file)) 109*795d594fSAndroid Build Coastguard Worker if len(answer) >= 1 and answer[0].lower() == "y": 110*795d594fSAndroid Build Coastguard Worker return "y" 111*795d594fSAndroid Build Coastguard Worker elif len(answer) >= 1 and answer[0].lower() == "n": 112*795d594fSAndroid Build Coastguard Worker return "n" 113*795d594fSAndroid Build Coastguard Worker else: 114*795d594fSAndroid Build Coastguard Worker print("Please enter 'y' or 'n' only!") 115*795d594fSAndroid Build Coastguard Worker else: 116*795d594fSAndroid Build Coastguard Worker test_args = shlex.split(args.test_exec) 117*795d594fSAndroid Build Coastguard Worker print(" ".join(map(shlex.quote, test_args))) 118*795d594fSAndroid Build Coastguard Worker answer = subprocess.run(test_args) 119*795d594fSAndroid Build Coastguard Worker if answer.returncode == 0: 120*795d594fSAndroid Build Coastguard Worker return "n" 121*795d594fSAndroid Build Coastguard Worker else: 122*795d594fSAndroid Build Coastguard Worker return "y" 123*795d594fSAndroid Build Coastguard Worker 124*795d594fSAndroid Build Coastguard Workerdef run_test(meths, args): 125*795d594fSAndroid Build Coastguard Worker with open(args.output_source, "wt") as output: 126*795d594fSAndroid Build Coastguard Worker dump_files(meths, args, output) 127*795d594fSAndroid Build Coastguard Worker print("Currently testing {} methods. ~{} rounds to go.".format( 128*795d594fSAndroid Build Coastguard Worker len(meths), 1 + math.floor(math.log2(len(meths))))) 129*795d594fSAndroid Build Coastguard Worker return get_answer(args) 130*795d594fSAndroid Build Coastguard Worker 131*795d594fSAndroid Build Coastguard Workerdef main(): 132*795d594fSAndroid Build Coastguard Worker parser = get_parser() 133*795d594fSAndroid Build Coastguard Worker args = parser.parse_args() 134*795d594fSAndroid Build Coastguard Worker if args.output_source is None: 135*795d594fSAndroid Build Coastguard Worker fdnum = os.memfd_create("tempfile_profile") 136*795d594fSAndroid Build Coastguard Worker args.output_source = "/proc/{}/fd/{}".format(os.getpid(), fdnum) 137*795d594fSAndroid Build Coastguard Worker all_dexs = list() 138*795d594fSAndroid Build Coastguard Worker for f in args.apks: 139*795d594fSAndroid Build Coastguard Worker try: 140*795d594fSAndroid Build Coastguard Worker all_dexs.append(pylibdexfile.FileDexFile(f.file, f.location)) 141*795d594fSAndroid Build Coastguard Worker except Exception as e1: 142*795d594fSAndroid Build Coastguard Worker try: 143*795d594fSAndroid Build Coastguard Worker all_dexs += pylibdexfile.OpenJar(f.file) 144*795d594fSAndroid Build Coastguard Worker except Exception as e2: 145*795d594fSAndroid Build Coastguard Worker parser.error("Failed to open file: {}. errors were {} and {}".format( 146*795d594fSAndroid Build Coastguard Worker f.file, e1, e2)) 147*795d594fSAndroid Build Coastguard Worker if args.input_profile is not None: 148*795d594fSAndroid Build Coastguard Worker profman_args = [ 149*795d594fSAndroid Build Coastguard Worker "profmand", "--dump-classes-and-methods", 150*795d594fSAndroid Build Coastguard Worker "--profile-file={}".format(args.input_profile) 151*795d594fSAndroid Build Coastguard Worker ] 152*795d594fSAndroid Build Coastguard Worker for apk in args.apks: 153*795d594fSAndroid Build Coastguard Worker profman_args.append("--apk={}".format(apk.file)) 154*795d594fSAndroid Build Coastguard Worker print(" ".join(map(shlex.quote, profman_args))) 155*795d594fSAndroid Build Coastguard Worker res = subprocess.run( 156*795d594fSAndroid Build Coastguard Worker profman_args, capture_output=True, universal_newlines=True) 157*795d594fSAndroid Build Coastguard Worker res.check_returncode() 158*795d594fSAndroid Build Coastguard Worker meth_list = list(filter(lambda a: a != "", res.stdout.split())) 159*795d594fSAndroid Build Coastguard Worker elif args.input_text_profile is not None: 160*795d594fSAndroid Build Coastguard Worker with open(args.input_text_profile, "rt") as inp: 161*795d594fSAndroid Build Coastguard Worker meth_list = list(filter(lambda a: a != "", inp.readlines())) 162*795d594fSAndroid Build Coastguard Worker else: 163*795d594fSAndroid Build Coastguard Worker all_methods = set() 164*795d594fSAndroid Build Coastguard Worker for d in all_dexs: 165*795d594fSAndroid Build Coastguard Worker for m in d.methods: 166*795d594fSAndroid Build Coastguard Worker all_methods.add(m.descriptor) 167*795d594fSAndroid Build Coastguard Worker meth_list = list(all_methods) 168*795d594fSAndroid Build Coastguard Worker print("Found {} methods. Will take ~{} iterations".format( 169*795d594fSAndroid Build Coastguard Worker len(meth_list), 1 + math.floor(math.log2(len(meth_list))))) 170*795d594fSAndroid Build Coastguard Worker print( 171*795d594fSAndroid Build Coastguard Worker "type 'yes' if the behavior you are looking for is present (i.e. the compiled code crashes " + 172*795d594fSAndroid Build Coastguard Worker "or something)" 173*795d594fSAndroid Build Coastguard Worker ) 174*795d594fSAndroid Build Coastguard Worker print("Performing single check with all methods") 175*795d594fSAndroid Build Coastguard Worker result = run_test(meth_list, args) 176*795d594fSAndroid Build Coastguard Worker if result[0].lower() != "y": 177*795d594fSAndroid Build Coastguard Worker cont = input( 178*795d594fSAndroid Build Coastguard Worker "The behavior you were looking for did not occur when run against all methods. Continue " + 179*795d594fSAndroid Build Coastguard Worker "(yes/no)? " 180*795d594fSAndroid Build Coastguard Worker ) 181*795d594fSAndroid Build Coastguard Worker if cont[0].lower() != "y": 182*795d594fSAndroid Build Coastguard Worker print("Aborting!") 183*795d594fSAndroid Build Coastguard Worker sys.exit(1) 184*795d594fSAndroid Build Coastguard Worker needs_dump = False 185*795d594fSAndroid Build Coastguard Worker while len(meth_list) > 1: 186*795d594fSAndroid Build Coastguard Worker test_methods = list(meth_list[0:len(meth_list) // 2]) 187*795d594fSAndroid Build Coastguard Worker result = run_test(test_methods, args) 188*795d594fSAndroid Build Coastguard Worker if result[0].lower() == "y": 189*795d594fSAndroid Build Coastguard Worker meth_list = test_methods 190*795d594fSAndroid Build Coastguard Worker needs_dump = False 191*795d594fSAndroid Build Coastguard Worker else: 192*795d594fSAndroid Build Coastguard Worker meth_list = meth_list[len(meth_list) // 2:] 193*795d594fSAndroid Build Coastguard Worker needs_dump = True 194*795d594fSAndroid Build Coastguard Worker if needs_dump: 195*795d594fSAndroid Build Coastguard Worker with open(args.output_source, "wt") as output: 196*795d594fSAndroid Build Coastguard Worker dump_files(meth_list, args, output) 197*795d594fSAndroid Build Coastguard Worker print("Found result!") 198*795d594fSAndroid Build Coastguard Worker print("{}".format(meth_list[0])) 199*795d594fSAndroid Build Coastguard Worker print("Leaving profile at {} and text profile at {}".format( 200*795d594fSAndroid Build Coastguard Worker args.output_file, args.output_source)) 201*795d594fSAndroid Build Coastguard Worker 202*795d594fSAndroid Build Coastguard Worker 203*795d594fSAndroid Build Coastguard Workerif __name__ == "__main__": 204*795d594fSAndroid Build Coastguard Worker main() 205