def iused(self, inum): dir = SInum.var('dir') fn = SFn.var('fn') fd = SFdNum.var('fd') pid = SPid.var('pid') # If we try to simply index into dirmap, its __getitem__ # won't have access to the supposition that it contains the right # key, and throw an exception. Thus, we use _map directly. return simsym.symor([ ## XXX Directories impl: # simsym.exists(dir, # simsym.symand([ # self.i_map[dir].isdir, # simsym.exists(fn, # simsym.symand([self.i_map[dir].dirmap.contains(fn), # self.i_map[dir].dirmap._map[fn] == inum]))])), ## XXX Non-directories impl: simsym.exists(fn, simsym.symand([self.root_dir.contains(fn), self.root_dir._map[fn] == inum])), simsym.exists(fd, simsym.symand([self.proc0.fd_map.contains(fd), simsym.symnot(self.proc0.fd_map._map[fd].ispipe), self.proc0.fd_map._map[fd].inum == inum])), simsym.exists(fd, simsym.symand([self.proc1.fd_map.contains(fd), simsym.symnot(self.proc1.fd_map._map[fd].ispipe), self.proc1.fd_map._map[fd].inum == inum])), ])
def write(self, fd, databyte, pid): self.add_selfpid(pid) if not self.getproc(pid).fd_map.contains(fd): return {'r': -1, 'errno': errno.EBADF} if self.getproc(pid).fd_map[fd].ispipe: if not self.getproc(pid).fd_map[fd].pipewriter: return {'r': -1, 'errno': errno.EBADF} pipeid = self.getproc(pid).fd_map[fd].pipeid pipe = self.pipes[pipeid] otherfd = SFdNum.var('otherfd') if simsym.symnot(simsym.symor([ simsym.exists(otherfd, simsym.symand([self.proc0.fd_map.contains(otherfd), self.proc0.fd_map._map[otherfd].ispipe, simsym.symnot(self.proc0.fd_map._map[otherfd].pipewriter), self.proc0.fd_map._map[otherfd].pipeid == pipeid])), simsym.exists(otherfd, simsym.symand([self.proc1.fd_map.contains(otherfd), self.proc1.fd_map._map[otherfd].ispipe, simsym.symnot(self.proc1.fd_map._map[otherfd].pipewriter), self.proc1.fd_map._map[otherfd].pipeid == pipeid]))])): # XXX This condition has the same problem as the one # in read. return {'r': -1, 'errno': errno.EPIPE} simsym.assume(pipe.data.len() < DATA_MAX_LEN) pipe.data.append(databyte) return {'r': DATAVAL_BYTES} off = self.getproc(pid).fd_map[fd].off self.getproc(pid).fd_map[fd].off = off + 1 return self.iwrite(self.getproc(pid).fd_map[fd].inum, off, databyte)
def open(self, pn, creat, excl, trunc, anyfd, pid): # XXX O_RDONLY, O_WRONLY, O_RDWR self.add_selfpid(pid) internal_time = STime.var('internal_time*') created = False anyfd = False _, pndirmap, pnlast = self.nameiparent(pn) if creat: if not pndirmap.contains(pnlast): internal_alloc_inum = SInum.var('internal_alloc_inum*') simsym.assume(simsym.symnot(self.iused(internal_alloc_inum))) simsym.assume(internal_time >= self.i_map[internal_alloc_inum].atime) simsym.assume(internal_time >= self.i_map[internal_alloc_inum].mtime) simsym.assume(internal_time >= self.i_map[internal_alloc_inum].ctime) inode = self.i_map[internal_alloc_inum] inode.data._len = 0 inode.nlink = 1 inode.atime = inode.mtime = inode.ctime = internal_time pndirmap[pnlast] = internal_alloc_inum created = True else: if excl: return {'r': -1, 'errno': errno.EEXIST} if not pndirmap.contains(pnlast): return {'r': -1, 'errno': errno.ENOENT} inum = pndirmap[pnlast] if trunc: if not created: simsym.assume(internal_time >= self.i_map[inum].mtime) simsym.assume(internal_time >= self.i_map[inum].ctime) self.i_map[inum].mtime = internal_time self.i_map[inum].ctime = internal_time self.i_map[inum].data._len = 0 internal_ret_fd = SFdNum.var('internal_ret_fd*') simsym.assume(internal_ret_fd >= 0) simsym.assume(simsym.symnot(self.getproc(pid).fd_map.contains(internal_ret_fd))) ## Lowest FD otherfd = SFdNum.var('fd') simsym.assume(simsym.symor([anyfd, simsym.symnot(simsym.exists(otherfd, simsym.symand([otherfd >= 0, otherfd < internal_ret_fd, self.getproc(pid).fd_map.contains(otherfd)])))])) fd_data = self.getproc(pid).fd_map.create(internal_ret_fd) fd_data.inum = inum fd_data.off = 0 fd_data.ispipe = False return {'r': internal_ret_fd}
def mmap(self, anon, writable, fixed, va, fd, off, pid): ## TODO: MAP_SHARED/MAP_PRIVATE for files ## -> how to model delayed file read? ## TODO: MAP_SHARED/MAP_PRIVATE for anon (with fork) ## TODO: zeroing anon memory self.add_selfpid(pid) myproc = self.getproc(pid) if not fixed: va = SVa.var('internal_freeva*') simsym.assume(simsym.symnot(myproc.va_map.contains(va))) if not anon: if not myproc.fd_map.contains(fd): return {'r': -1, 'errno': errno.EBADF} if myproc.fd_map[fd].ispipe: # The Linux manpage is misleading, but POSIX is clear # about ENODEV and this is what Linux does. return {'r': -1, 'errno': errno.ENODEV} vma = myproc.va_map.create(va) vma.anon = anon vma.writable = writable if anon: vma.anondata = SDataVal.var() else: simsym.assume(off >= 0) simsym.assume(off % PAGE_DATAVALS == 0) vma.off = off vma.inum = myproc.fd_map[fd].inum # This has to be well-typed, so we use a different variable to # represent VAs. return {'r:va': va}
def pipe(self, pid): self.add_selfpid(pid) internal_pipeid = SPipeId.var('internal_pipeid*') xfd = SFdNum.var('xfd') simsym.assume(simsym.symnot(simsym.symor([ simsym.exists(xfd, simsym.symand([self.proc0.fd_map.contains(xfd), self.proc0.fd_map._map[xfd].ispipe, self.proc0.fd_map._map[xfd].pipeid == internal_pipeid])), simsym.exists(xfd, simsym.symand([self.proc1.fd_map.contains(xfd), self.proc1.fd_map._map[xfd].ispipe, self.proc1.fd_map._map[xfd].pipeid == internal_pipeid]))]))) empty_pipe = self.pipes[internal_pipeid] empty_pipe.data._len = 0 ## lowest FD for read end internal_fd_r = SFdNum.var('internal_fd_r*') simsym.assume(internal_fd_r >= 0) simsym.assume(simsym.symnot(self.getproc(pid).fd_map.contains(internal_fd_r))) simsym.assume(simsym.symnot(simsym.exists(xfd, simsym.symand([xfd >= 0, xfd < internal_fd_r, self.getproc(pid).fd_map.contains(xfd)])))) fd_r_data = self.getproc(pid).fd_map.create(internal_fd_r) fd_r_data.ispipe = True fd_r_data.pipeid = internal_pipeid fd_r_data.pipewriter = False ## lowest FD for write end internal_fd_w = SFdNum.var('internal_fd_w*') simsym.assume(internal_fd_w >= 0) simsym.assume(simsym.symnot(self.getproc(pid).fd_map.contains(internal_fd_w))) simsym.assume(simsym.symnot(simsym.exists(xfd, simsym.symand([xfd >= 0, xfd < internal_fd_w, self.getproc(pid).fd_map.contains(xfd)])))) fd_w_data = self.getproc(pid).fd_map.create(internal_fd_w) fd_w_data.ispipe = True fd_w_data.pipeid = internal_pipeid fd_w_data.pipewriter = True return {'r': 0, 'fds[0]': internal_fd_r, 'fds[1]': internal_fd_w}
def write(self, fd, databyte, pid): self.add_selfpid(pid) if not self.getproc(pid).fd_map.contains(fd): return {'r': -1, 'errno': errno.EBADF} if self.getproc(pid).fd_map[fd].ispipe: if not self.getproc(pid).fd_map[fd].pipewriter: return {'r': -1, 'errno': errno.EBADF} pipeid = self.getproc(pid).fd_map[fd].pipeid pipe = self.pipes[pipeid] otherfd = SFdNum.var('otherfd') if simsym.symnot( simsym.symor([ simsym.exists( otherfd, simsym.symand([ self.proc0.fd_map.contains(otherfd), self.proc0.fd_map._map[otherfd].ispipe, simsym.symnot(self.proc0.fd_map._map[otherfd]. pipewriter), self.proc0.fd_map._map[otherfd].pipeid == pipeid ])), simsym.exists( otherfd, simsym.symand([ self.proc1.fd_map.contains(otherfd), self.proc1.fd_map._map[otherfd].ispipe, simsym.symnot(self.proc1.fd_map._map[otherfd]. pipewriter), self.proc1.fd_map._map[otherfd].pipeid == pipeid ])) ])): # XXX This condition has the same problem as the one # in read. return {'r': -1, 'errno': errno.EPIPE} simsym.assume(pipe.data.len() < DATA_MAX_LEN) pipe.data.append(databyte) return {'r': DATAVAL_BYTES} off = self.getproc(pid).fd_map[fd].off self.getproc(pid).fd_map[fd].off = off + 1 return self.iwrite(self.getproc(pid).fd_map[fd].inum, off, databyte)
def rename(self, src, dst): if simsym.symnot(self.fname_to_inum.contains(src)): #return (-1, errno.ENOENT) return -1 if src == dst: return 0 if self.fname_to_inum.contains(dst): self.inodes[self.fname_to_inum[dst]].nlink -= 1 self.fname_to_inum[dst] = self.fname_to_inum[src] del self.fname_to_inum[src] return 0
def socket(self, domain, type, prot, anyfd, pid): self.add_selfpid(pid) if not ((domain == AF_INET or domain == AF_INET6) and type == SOCK_DGRAM and prot == 0): return {'r': -1, 'errno': errno.EAFNOSUPPORT} internal_ret_fd = SFdNum.var('internal_ret_fd*') simsym.assume(internal_ret_fd >= 0) simsym.assume( simsym.symnot(self.getproc(pid).fd_map.contains(internal_ret_fd))) ## Lowest FD otherfd = SFdNum.var('fd') simsym.assume( simsym.symor([ anyfd, simsym.symnot( simsym.exists( otherfd, simsym.symand([ otherfd >= 0, otherfd < internal_ret_fd, self.getproc(pid).fd_map.contains(otherfd) ]))) ])) sock = self.getproc(pid).fd_map.create(internal_ret_fd) sock.domain = domain sock.type = type sock.prot = prot sock.can_read = True sock.can_write = True sock.is_bound = False sock.is_connected = False sock.local_addr = 0 sock.local_port = 0 sock.remote_addr = 0 sock.remote_port = 0 return {'r': internal_ret_fd}
def iused(self, inum): dir = SInum.var('dir') fn = SFn.var('fn') fd = SFdNum.var('fd') pid = SPid.var('pid') # If we try to simply index into dirmap, its __getitem__ # won't have access to the supposition that it contains the right # key, and throw an exception. Thus, we use _map directly. return simsym.symor([ ## XXX Directories impl: # simsym.exists(dir, # simsym.symand([ # self.i_map[dir].isdir, # simsym.exists(fn, # simsym.symand([self.i_map[dir].dirmap.contains(fn), # self.i_map[dir].dirmap._map[fn] == inum]))])), ## XXX Non-directories impl: simsym.exists( fn, simsym.symand([ self.root_dir.contains(fn), self.root_dir._map[fn] == inum ])), simsym.exists( fd, simsym.symand([ self.proc0.fd_map.contains(fd), simsym.symnot(self.proc0.fd_map._map[fd].ispipe), self.proc0.fd_map._map[fd].inum == inum ])), simsym.exists( fd, simsym.symand([ self.proc1.fd_map.contains(fd), simsym.symnot(self.proc1.fd_map._map[fd].ispipe), self.proc1.fd_map._map[fd].inum == inum ])), ])
def do_callset(base, callset, test_writer): print ' '.join([c.__name__ for c in callset]) test_writer.begin_call_set(callset) reporter = progress.ProgressReporter( ' {0.npath} paths ({0.ncompath} commutative), {0.nmodel} testcases,' + ' {0.nerror} errors', test_writer) condlists = collections.defaultdict(list) terminated = False diverged = set() all_internals = [] for sar in simsym.symbolic_apply(test, base, *callset): if sar.type == 'value': is_commutative = (len(sar.value.diverge) == 0) diverged.update(sar.value.diverge) condlists[is_commutative].append(sar.path_condition) all_internals.extend(sar.internals) test_writer.on_result(sar) if not test_writer.keep_going(): terminated = True break test_writer.end_call_set() reporter.end() if terminated: print ' enumeration incomplete; skipping conditions' return conds = collections.defaultdict(lambda: [simsym.wrap(z3.BoolVal(False))]) for result, condlist in condlists.items(): conds[result] = condlist if True in condlists: commute = simsym.symor(condlists[True]) # Internal variables help deal with situations where, for the # same assignment of initial state + external inputs, two # operations both can commute and can diverge (depending on # internal choice, like the inode number for file creation). cannot_commute = simsym.symnot(simsym.exists(all_internals, commute)) print_cond('can commute', commute) else: cannot_commute = True if False in condlists: diverge = simsym.symor(condlists[False]) print_cond('can not commute; %s' % str_diverge(diverged), simsym.symand([diverge, cannot_commute]))
def open(self, which): fn = simsym.SInt.any('Fs.open[%s].fn' % which) creat = simsym.SBool.any('Fs.open[%s].creat' % which) excl = simsym.SBool.any('Fs.open[%s].excl' % which) trunc = simsym.SBool.any('Fs.open[%s].trunc' % which) if creat: if not self.fn_to_ino.contains(fn): if self.numifree == 0: return ('err', errno.ENOSPC) ino = simsym.SInt.any('Fs.open[%s].ialloc' % which) simsym.add_internal(ino) simsym.assume(simsym.symnot(self.iused(ino))) self.numifree = self.numifree - 1 self.ino_to_data[ino] = 0 self.fn_to_ino[fn] = ino else: if excl: return ('err', errno.EEXIST) if not self.fn_to_ino.contains(fn): return ('err', errno.ENOENT) if trunc: self.ino_to_data[self.fn_to_ino[fn]] = 0 return ('ok',)
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 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 open(self, pn, creat, excl, trunc, anyfd, pid): # XXX O_RDONLY, O_WRONLY, O_RDWR self.add_selfpid(pid) internal_time = STime.var('internal_time*') created = False anyfd = False _, pndirmap, pnlast = self.nameiparent(pn) if creat: if not pndirmap.contains(pnlast): internal_alloc_inum = SInum.var('internal_alloc_inum*') simsym.assume(simsym.symnot(self.iused(internal_alloc_inum))) simsym.assume( internal_time >= self.i_map[internal_alloc_inum].atime) simsym.assume( internal_time >= self.i_map[internal_alloc_inum].mtime) simsym.assume( internal_time >= self.i_map[internal_alloc_inum].ctime) inode = self.i_map[internal_alloc_inum] inode.data._len = 0 inode.nlink = 1 inode.atime = inode.mtime = inode.ctime = internal_time pndirmap[pnlast] = internal_alloc_inum created = True else: if excl: return {'r': -1, 'errno': errno.EEXIST} if not pndirmap.contains(pnlast): return {'r': -1, 'errno': errno.ENOENT} inum = pndirmap[pnlast] if trunc: if not created: simsym.assume(internal_time >= self.i_map[inum].mtime) simsym.assume(internal_time >= self.i_map[inum].ctime) self.i_map[inum].mtime = internal_time self.i_map[inum].ctime = internal_time self.i_map[inum].data._len = 0 internal_ret_fd = SFdNum.var('internal_ret_fd*') simsym.assume(internal_ret_fd >= 0) simsym.assume( simsym.symnot(self.getproc(pid).fd_map.contains(internal_ret_fd))) ## Lowest FD otherfd = SFdNum.var('fd') simsym.assume( simsym.symor([ anyfd, simsym.symnot( simsym.exists( otherfd, simsym.symand([ otherfd >= 0, otherfd < internal_ret_fd, self.getproc(pid).fd_map.contains(otherfd) ]))) ])) fd_data = self.getproc(pid).fd_map.create(internal_ret_fd) fd_data.inum = inum fd_data.off = 0 fd_data.ispipe = False return {'r': internal_ret_fd}
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 __ne__(self, o): r = (self == o) if r is NotImplemented: return NotImplemented return simsym.symnot(r)
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 test_callset(base, callset, monitors, check_conds=False, print_conds=False): """Test the SIM-commutativity of a call set. base must be a class type for the system state. calls must be the unbound methods of base to test for SIM commutativity. As the test proceeds, this will invoke the appropriate methods of the ExecutionMonitorBase instances in monitors. The caller is responsible for calling the 'finish' method of the monitors after all callsets are done. If check_conds is true, check commutativity conditions for sat/unsat and report this. If print_conds is true, print commutativity conditions. If print_conds is "simplify", use ctx-solver-simplify to further simplify conditions. """ monitor = MetaMonitor([StatMonitor()] + monitors) print " ".join([c.__name__ for c in callset]) monitor.begin_call_set(callset) reporter = progress.ProgressReporter(" " + monitor.get_progress_format(), monitor) condlists = collections.defaultdict(list) terminated = False diverged = set() all_internals = [] for sar in simsym.symbolic_apply(test, base, *callset): if sar.type == "value": is_commutative = len(sar.value.diverge) == 0 diverged.update(sar.value.diverge) condlists[is_commutative].append(sar.path_condition) all_internals.extend(sar.internals) monitor.on_path(sar) if monitor.stop_call_set(): terminated = True break monitor.end_call_set() reporter.end() if terminated: print " enumeration incomplete; skipping conditions" return conds = collections.defaultdict(lambda: [simsym.wrap(z3.BoolVal(False))]) for result, condlist in condlists.items(): conds[result] = condlist if True in condlists: commute = simsym.symor(condlists[True]) # Internal variables help deal with situations where, for the # same assignment of initial state + external inputs, two # operations both can commute and can diverge (depending on # internal choice, like the inode number for file creation). cannot_commute = simsym.symnot(simsym.exists(all_internals, commute)) print_cond("can commute", commute, check_conds, print_conds) else: cannot_commute = True if False in condlists: diverge = simsym.symor(condlists[False]) print_cond( "can not commute; %s" % ", ".join(map(str, diverged)), simsym.symand([diverge, cannot_commute]), check_conds, print_conds, )
def _declare_assumptions(self, assume): super(ProcessQueue, self)._declare_assumptions(assume) # 'iseq' restriction i = simsym.SInt.var() j = simsym.SInt.var() assume(simsym.symnot(simsym.exists(i, simsym.exists(j, simsym.symand(i != j, i >= 0, j >= 0, i < self.elts.len(), j < self.elts.len(), self.elts[i] == self.elts[j])))))
def pipe(self, pid): self.add_selfpid(pid) internal_pipeid = SPipeId.var('internal_pipeid*') xfd = SFdNum.var('xfd') simsym.assume( simsym.symnot( simsym.symor([ simsym.exists( xfd, simsym.symand([ self.proc0.fd_map.contains(xfd), self.proc0.fd_map._map[xfd].ispipe, self.proc0.fd_map._map[xfd].pipeid == internal_pipeid ])), simsym.exists( xfd, simsym.symand([ self.proc1.fd_map.contains(xfd), self.proc1.fd_map._map[xfd].ispipe, self.proc1.fd_map._map[xfd].pipeid == internal_pipeid ])) ]))) empty_pipe = self.pipes[internal_pipeid] empty_pipe.data._len = 0 ## lowest FD for read end internal_fd_r = SFdNum.var('internal_fd_r*') simsym.assume(internal_fd_r >= 0) simsym.assume( simsym.symnot(self.getproc(pid).fd_map.contains(internal_fd_r))) simsym.assume( simsym.symnot( simsym.exists( xfd, simsym.symand([ xfd >= 0, xfd < internal_fd_r, self.getproc(pid).fd_map.contains(xfd) ])))) fd_r_data = self.getproc(pid).fd_map.create(internal_fd_r) fd_r_data.ispipe = True fd_r_data.pipeid = internal_pipeid fd_r_data.pipewriter = False ## lowest FD for write end internal_fd_w = SFdNum.var('internal_fd_w*') simsym.assume(internal_fd_w >= 0) simsym.assume( simsym.symnot(self.getproc(pid).fd_map.contains(internal_fd_w))) simsym.assume( simsym.symnot( simsym.exists( xfd, simsym.symand([ xfd >= 0, xfd < internal_fd_w, self.getproc(pid).fd_map.contains(xfd) ])))) fd_w_data = self.getproc(pid).fd_map.create(internal_fd_w) fd_w_data.ispipe = True fd_w_data.pipeid = internal_pipeid fd_w_data.pipewriter = True return {'r': 0, 'fds[0]': internal_fd_r, 'fds[1]': internal_fd_w}
for p in projections: for c in calls: projected_calls.append(projected_call(p, projections[p], c)) for callset in itertools.combinations_with_replacement(projected_calls, ncomb): print ' '.join([c.__name__ for c in callset]) rvs = simsym.symbolic_apply(test, base, *callset) conds = collections.defaultdict(lambda: simsym.wrap(z3.BoolVal(False))) for cond, res in simsym.combine(rvs): conds[res] = cond pc = simsym.simplify(conds['']) pr = simsym.simplify(simsym.symor([conds['r'], conds['rs']])) ps = simsym.simplify(conds['s']) ex_pc = simsym.exists(simsym.internals(), pc) nex_pc = simsym.symnot(ex_pc) 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)):