def reduce_array_ext(expr): """Find a subset of expr that still produces array-ext in models. This is for producing minimal test cases for a specific class of bugs where Z3 exposes the internal array-ext function (e.g., Z3 issue #125). """ subs = expr.children() i = 0 while i < len(subs): nsubs = subs[:i] + subs[i+1:] e2 = z3.And(nsubs) print subs[i].sexpr(), for rep in range(100): check = simsym.check(e2) if check.is_unknown: continue if 'array-ext' in check.z3_model.sexpr(): # Got an array-ext, so subs[i] must not be necessary print "=> dropping" subs = nsubs break else: # Never got an array-ext, so subs[i] is probably necessary print "=> keeping" i += 1 return z3.And(subs)
def xcheck(cond): check = simsym.check(simsym.symand([pc, iso_constraint, cond])) if check.is_unknown: if unknown_count[0] == 0: print ' Idempotence unknown:', check.reason # print ' ' + str(cond) unknown_count[0] += 1 return check
def print_cond(msg, cond): if args.check_conds and simsym.check(cond).is_unsat: return ## If the assumptions (i.e., calls to simsym.assume) imply the condition ## is true, we say that condition always holds, and we can print "always". ## It would be nice to print a clean condition that excludes assumptions, ## even if the assumptions don't directly imply the condition, but that ## would require finding the simplest expression for x such that ## ## x AND simsym.assume_list = cond ## ## which seems hard to do using Z3. In principle, this should be the ## same as simplifying the 'c' expression below, but Z3 isn't good at ## simplifying it. We could keep the two kinds of constraints (i.e., ## explicit assumptions vs. symbolic execution control flow constraints) ## separate in simsym, which will make them easier to disentangle.. #c = simsym.implies(simsym.symand(simsym.assume_list), cond) ## XXX the above doesn't work well -- it causes open*open to say "always". ## One hypothesis is that we should be pairing the assume_list with each ## path condition, instead of taking the assume_list across all paths. c = cond if args.check_conds and simsym.check(simsym.symnot(c)).is_unsat: s = 'always' else: if args.print_conds: scond = simsym.simplify(cond, args.simplify_more) s = '\n ' + str(scond).replace('\n', '\n ') else: if args.check_conds: s = 'sometimes' else: s = 'maybe' print ' %s: %s' % (msg, s)
def print_cond(msg, cond, check_conds, print_conds): if check_conds and simsym.check(cond).is_unsat: return ## If the assumptions (i.e., calls to simsym.assume) imply the condition ## is true, we say that condition always holds, and we can print "always". ## It would be nice to print a clean condition that excludes assumptions, ## even if the assumptions don't directly imply the condition, but that ## would require finding the simplest expression for x such that ## ## x AND simsym.assume_list = cond ## ## which seems hard to do using Z3. In principle, this should be the ## same as simplifying the 'c' expression below, but Z3 isn't good at ## simplifying it. We could keep the two kinds of constraints (i.e., ## explicit assumptions vs. symbolic execution control flow constraints) ## separate in simsym, which will make them easier to disentangle.. # c = simsym.implies(simsym.symand(simsym.assume_list), cond) ## XXX the above doesn't work well -- it causes open*open to say "always". ## One hypothesis is that we should be pairing the assume_list with each ## path condition, instead of taking the assume_list across all paths. c = cond if check_conds and simsym.check(simsym.symnot(c)).is_unsat: s = "always" else: if print_conds: scond = simsym.simplify(cond, print_conds == "simplify") s = "\n " + str(scond).replace("\n", "\n ") else: if check_conds: s = "sometimes" else: s = "maybe" print " %s: %s" % (msg, s)
def on_path(self, result): super(TestWriter, self).on_path(result) pathinfo = collections.OrderedDict([ ('id', '_'.join(self.callset_names) + '_' + result.pathid) ]) self.model_data_callset[result.pathid] = pathinfo if result.type == 'exception': pathinfo['exception'] = '\n'.join( traceback.format_exception_only(*result.exc_info[:2])) self.nerror += 1 return pathinfo['diverge'] = ', '.join(map(str, result.value.diverge)) # Filter out non-commutative results if len(result.value.diverge): return if not self.trace_file and not self.testgen: return if self.trace_file: print >> self.trace_file, "=== Path %s ===" % result.pathid print >> self.trace_file e = result.path_condition ## This can potentially reduce the number of test cases ## by, e.g., eliminating irrelevant variables from e. ## The effect doesn't seem significant: one version of Fs ## produces 3204 test cases without simplify, and 3182 with. e = simsym.simplify(e) if args.verbose_testgen: print "Simplified path condition:" print e if self.trace_file: print >> self.trace_file, e print >> self.trace_file # Find the uninterpreted constants in the path condition. We # omit assumptions because uninterpreted constants that appear # only in assumptions generally don't represent that the model # actually "touched". We use the simplified expression # because the final state comparison in original expression # contains a lot of trivial expressions like x==x for all # state variables x, and we don't care about these # uninterpreted constants. e_vars = expr_vars( simsym.simplify( simsym.symand( result.get_path_condition_list(with_assume=False, with_det=True)))) if self.testgen: self.testgen.begin_path(result) self.model_data_testinfo_list = [] pathinfo['tests'] = self.model_data_testinfo_list self.npathmodel = 0 self.last_assignments = None while not self.stop_call_set() and \ self.npathmodel < args.max_tests_per_path: # XXX Would it be faster to reuse the solver? check = simsym.check(e) if check.is_sat and 'array-ext' in check.z3_model.sexpr(): # Work around some non-deterministic bug that causes # Z3 to occasionally produce models containing # 'array-ext' applications that break evaluation. print 'Warning: Working around array-ext bug' for i in range(10): check = simsym.check(e) if not check.is_sat: continue if 'array-ext' not in check.z3_model.sexpr(): break else: self._testerror('array-ext workaround failed', pathinfo) break if check.is_unsat: break if check.is_unknown: # raise Exception('Cannot enumerate: %s' % str(e)) self._testerror(check.reason, pathinfo) break if args.verbose_testgen: print "Model:" print check.model testid = ('_'.join(self.callset_names) + '_' + result.pathid + '_' + str(self.npathmodel)) testinfo = collections.OrderedDict(id=testid) self.model_data_testinfo_list.append(testinfo) assignments = self.__on_model(result, check.z3_model, e, testid) if assignments is None: break if args.verbose_testgen: print 'Assignments:' pprint.pprint(assignments) if args.diff_testgen: new_assignments = {} if self.last_assignments is not None: for aexpr, val in assignments: hexpr = z3util.HashableAst(aexpr) sval = str(val) last_sval = self.last_assignments.get(hexpr) if last_sval is not None and last_sval != sval: print '%s: %s -> %s' % (aexpr, last_sval, sval) new_assignments[hexpr] = sval self.last_assignments = new_assignments testinfo['assignments'] = {} for aexpr, val in assignments[None]: testinfo['assignments'][str(aexpr)] = str(val) # Construct the isomorphism condition for the assignments # used by testgen. This tells us exactly what values # actually mattered to test case generation. However, # this set isn't perfect: testgen may have queried # assignments that didn't actually matter to the # function's behavior (e.g., flags that didn't matter # because the function will return an error anyway, etc). # To filter out such uninterpreted constants, we only # consider those that were *both* used in an assignment by # testgen and appeared in the path condition expression. # XXX We should revisit this and see how much difference # this makes. same = IsomorphicMatch() for realm, rassigns in assignments.iteritems(): for aexpr, val in rassigns: aexpr_vars = expr_vars(aexpr) if not aexpr_vars.isdisjoint(e_vars): same.add(realm, aexpr, val, result) elif args.verbose_testgen: print 'Ignoring assignment:', (aexpr, val) isocond = same.condition() # Compute idempotent projections for this test if args.idempotent_projs: projs, proj_errors = idempotent_projs(result, isocond) testinfo['idempotent_projs'] = projs if proj_errors: testinfo['idempotence_unknown'] = proj_errors # Construct constraint for next test notsame = simsym.symnot(isocond) if args.verbose_testgen: print 'Negation', self.nmodel, ':', notsame e = simsym.symand([e, notsame]) if self.npathmodel == args.max_tests_per_path: print ' Max tests reached for path %s' % result.pathid if self.testgen: self.testgen.end_path()
def setup_proc(self, pid, fdmap, vamap, pipe_end_fds): emit = self.emit emit('int fd __attribute__((unused));', 'int r __attribute__((unused));') for fd, (symfd, inode) in fdmap.items(): if symfd.ispipe: pipe_setup_fd = self.pipes[symfd.pipeid] if symfd.pipewriter: pipe_setup_fd += 1 emit('r = dup2(%d, %d);' % (pipe_setup_fd, fd), 'if (r < 0) setup_error("dup2");') else: emit('fd = open("%s", O_RDWR);' % self.inums[symfd.inum].fname, 'if (fd < 0) setup_error("open");', 'r = lseek(fd, %d, SEEK_SET);' % (inode.offsets[symfd.off]), 'if (fd >= 0 && r < 0) setup_error("lseek");', 'r = dup2(fd, %d);' % fd, 'if (fd >= 0 && r < 0) setup_error("dup2");', 'close(fd);') # There may be other FDs open to pipes that never came up in the # test, but that matter to keep the pipe open. Check for this # possibility for every pipe end. for (_, pipewriter), (pipeid, pidcounts) in pipe_end_fds.items(): # How many of this end are already set up in this process? knowncount = pidcounts[pid] # Try to find > knowncount distinct instances of this end in # this process. conds = [] distinct = [] sym_fd_map = self.fs.getproc(pid).fd_map for i in range(knowncount + 1): ofdnum = fs_module.SFdNum.var() ofd = sym_fd_map._map[ofdnum] conds.extend([sym_fd_map._valid[ofdnum], ofd.ispipe, ofd.pipeid == pipeid, ofd.pipewriter == pipewriter]) distinct.append(ofdnum) if len(distinct) > 1: conds.append(simsym.distinct(*distinct)) # XXX I've tried to *prove* that the path condition implies # there *must* be more FDs for this pipe end, so the decision # will stand regardless of choices made by model completion, but # I just can't get it to work: # prove(simsym.implies(self.constraint, # simsym.exists(ofdnums, simsym.symand(conds)))) result = simsym.check(simsym.symand([self.constraint] + conds)) if result.is_unknown: print 'Warning: Unable to check pipe FD existence:', result.reason elif result.is_sat: # Open up another pipe end pipe_setup_fd = self.pipes[pipeid] if pipewriter: pipe_setup_fd += 1 emit('r = dup(%d);' % pipe_setup_fd, 'if (r < 0) setup_error("dup");') for va, vainfo in vamap.items(): if vainfo.anon: emit('init_map_anon(%#x, %d, %d);' % (va, vainfo.writable.val, self.datavals[vainfo.anondata].first_byte)) else: inode = self.inums[vainfo.inum] emit('init_map_file(%#x, %d, "%s", %#x);' % (va, vainfo.writable.val, inode.fname, inode.offsets[vainfo.off]))
def on_path(self, result): super(TestWriter, self).on_path(result) pathinfo = collections.OrderedDict([ ('id', '_'.join(self.callset_names) + '_' + result.pathid)]) self.model_data_callset[result.pathid] = pathinfo if result.type == 'exception': pathinfo['exception'] = '\n'.join( traceback.format_exception_only(*result.exc_info[:2])) self.nerror += 1 return pathinfo['diverge'] = ', '.join(map(str, result.value.diverge)) # Filter out non-commutative results if len(result.value.diverge): return if not self.trace_file and not self.testgen: return if self.trace_file: print >> self.trace_file, "=== Path %s ===" % result.pathid print >> self.trace_file e = result.path_condition ## This can potentially reduce the number of test cases ## by, e.g., eliminating irrelevant variables from e. ## The effect doesn't seem significant: one version of Fs ## produces 3204 test cases without simplify, and 3182 with. e = simsym.simplify(e) if args.verbose_testgen: print "Simplified path condition:" print e if self.trace_file: print >> self.trace_file, e print >> self.trace_file # Find the uninterpreted constants in the path condition. We # omit assumptions because uninterpreted constants that appear # only in assumptions generally don't represent that the model # actually "touched". We use the simplified expression # because the final state comparison in original expression # contains a lot of trivial expressions like x==x for all # state variables x, and we don't care about these # uninterpreted constants. e_vars = expr_vars( simsym.simplify( simsym.symand( result.get_path_condition_list( with_assume=False, with_det=True)))) if self.testgen: self.testgen.begin_path(result) self.model_data_testinfo_list = [] pathinfo['tests'] = self.model_data_testinfo_list self.npathmodel = 0 self.last_assignments = None while not self.stop_call_set() and \ self.npathmodel < args.max_tests_per_path: # XXX Would it be faster to reuse the solver? check = simsym.check(e) if check.is_sat and 'array-ext' in check.z3_model.sexpr(): # Work around some non-deterministic bug that causes # Z3 to occasionally produce models containing # 'array-ext' applications that break evaluation. print 'Warning: Working around array-ext bug' for i in range(10): check = simsym.check(e) if not check.is_sat: continue if 'array-ext' not in check.z3_model.sexpr(): break else: self._testerror('array-ext workaround failed', pathinfo) break if check.is_unsat: break if check.is_unknown: # raise Exception('Cannot enumerate: %s' % str(e)) self._testerror(check.reason, pathinfo) break if args.verbose_testgen: print "Model:" print check.model testid = ('_'.join(self.callset_names) + '_' + result.pathid + '_' + str(self.npathmodel)) testinfo = collections.OrderedDict(id=testid) self.model_data_testinfo_list.append(testinfo) assignments = self.__on_model(result, check.z3_model, e, testid) if assignments is None: break if args.verbose_testgen: print 'Assignments:' pprint.pprint(assignments) if args.diff_testgen: new_assignments = {} if self.last_assignments is not None: for aexpr, val in assignments: hexpr = z3util.HashableAst(aexpr) sval = str(val) last_sval = self.last_assignments.get(hexpr) if last_sval is not None and last_sval != sval: print '%s: %s -> %s' % (aexpr, last_sval, sval) new_assignments[hexpr] = sval self.last_assignments = new_assignments testinfo['assignments'] = {} for aexpr, val in assignments[None]: testinfo['assignments'][str(aexpr)] = str(val) # Construct the isomorphism condition for the assignments # used by testgen. This tells us exactly what values # actually mattered to test case generation. However, # this set isn't perfect: testgen may have queried # assignments that didn't actually matter to the # function's behavior (e.g., flags that didn't matter # because the function will return an error anyway, etc). # To filter out such uninterpreted constants, we only # consider those that were *both* used in an assignment by # testgen and appeared in the path condition expression. # XXX We should revisit this and see how much difference # this makes. same = IsomorphicMatch() for realm, rassigns in assignments.iteritems(): for aexpr, val in rassigns: aexpr_vars = expr_vars(aexpr) if not aexpr_vars.isdisjoint(e_vars): same.add(realm, aexpr, val, result) elif args.verbose_testgen: print 'Ignoring assignment:', (aexpr, val) isocond = same.condition() # Compute idempotent projections for this test if args.idempotent_projs: projs, proj_errors = idempotent_projs(result, isocond) testinfo['idempotent_projs'] = projs if proj_errors: testinfo['idempotence_unknown'] = proj_errors # Construct constraint for next test notsame = simsym.symnot(isocond) if args.verbose_testgen: print 'Negation', self.nmodel, ':', notsame e = simsym.symand([e, notsame]) if self.npathmodel == args.max_tests_per_path: print ' Max tests reached for path %s' % result.pathid if self.testgen: self.testgen.end_path()
print "nex_pc", nex_pc ex_pr = simsym.exists(simsym.internals(), pr) nex_pr = simsym.symnot(ex_pr) ps2 = simsym.symand([ps, nex_pc, nex_pr]) ps_ex_pr = simsym.symand([ps, ex_pr]) pr2 = simsym.symand([simsym.symor([pr, ps_ex_pr]), nex_pc]) ps_ex_pc = simsym.symand([ps, ex_pc]) pr_ex_pc = simsym.symand([pr, ex_pc]) pc2 = simsym.symor([pc, ps_ex_pc, pr_ex_pc]) for msg, cond in (('commute', pc2), ('results diverge', pr2), ('states diverge', ps2)): if simsym.check(cond)[0] == z3.unsat: continue if simsym.check(simsym.symnot(cond))[0] == z3.unsat: s = 'any state' else: if print_cond: scond = simsym.simplify(cond) s = '\n ' + str(scond).replace('\n', '\n ') else: s = 'sometimes' print ' %s: %s' % (msg, s) if testfile is not None: for cond, ores in rvs: #print "cond: ", cond #print "pc2: ", pc2
def setup_proc(self, pid, fdmap, vamap, pipe_end_fds): emit = self.emit emit("int fd __attribute__((unused));", "int r __attribute__((unused));") for fd, (symfd, inode) in fdmap.items(): if symfd.ispipe: pipe_setup_fd = self.pipes[symfd.pipeid] if symfd.pipewriter: pipe_setup_fd += 1 emit("r = dup2(%d, %d);" % (pipe_setup_fd, fd), 'if (r < 0) setup_error("dup2");') else: emit( 'fd = open("%s", O_RDWR);' % self.inums[symfd.inum].fname, 'if (fd < 0) setup_error("open");', "r = lseek(fd, %d, SEEK_SET);" % (inode.offsets[symfd.off]), 'if (fd >= 0 && r < 0) setup_error("lseek");', "r = dup2(fd, %d);" % fd, 'if (fd >= 0 && r < 0) setup_error("dup2");', "close(fd);", ) # There may be other FDs open to pipes that never came up in the # test, but that matter to keep the pipe open. Check for this # possibility for every pipe end. for (_, pipewriter), (pipeid, pidcounts) in pipe_end_fds.items(): # How many of this end are already set up in this process? knowncount = pidcounts[pid] # Try to find > knowncount distinct instances of this end in # this process. conds = [] distinct = [] sym_fd_map = self.fs.getproc(pid).fd_map for i in range(knowncount + 1): ofdnum = fs_module.SFdNum.var() ofd = sym_fd_map._map[ofdnum] conds.extend( [sym_fd_map._valid[ofdnum], ofd.ispipe, ofd.pipeid == pipeid, ofd.pipewriter == pipewriter] ) distinct.append(ofdnum) if len(distinct) > 1: conds.append(simsym.distinct(*distinct)) # XXX I've tried to *prove* that the path condition implies # there *must* be more FDs for this pipe end, so the decision # will stand regardless of choices made by model completion, but # I just can't get it to work: # prove(simsym.implies(self.constraint, # simsym.exists(ofdnums, simsym.symand(conds)))) result = simsym.check(simsym.symand([self.constraint] + conds)) if result.is_unknown: print "Warning: Unable to check pipe FD existence:", result.reason elif result.is_sat: # Open up another pipe end pipe_setup_fd = self.pipes[pipeid] if pipewriter: pipe_setup_fd += 1 emit("r = dup(%d);" % pipe_setup_fd, 'if (r < 0) setup_error("dup");') for va, vainfo in vamap.items(): if vainfo.anon: emit( "init_map_anon(%#x, %d, %d);" % (va, vainfo.writable.val, self.datavals[vainfo.anondata].first_byte) ) else: inode = self.inums[vainfo.inum] emit( 'init_map_file(%#x, %d, "%s", %#x);' % (va, vainfo.writable.val, inode.fname, inode.offsets[vainfo.off]) )