def eval_keys(self, ctx): key_pairs = [] if not self.keys: # there is nothing to match with... return z3.BoolVal(False) for index, (key_expr, key_type) in enumerate(self.keys): key_eval = ctx.resolve_expr(key_expr) key_sort = key_eval.sort() key_match = z3.Const(f"{self.name}_table_key_{index}", key_sort) if key_type == "exact": # Just a simple comparison, nothing special key_pairs.append(key_eval == key_match) elif key_type == "lpm": # I think this can be arbitrarily large... # If the shift exceeds the bit width, everything will be zero # but that does not matter # TODO: Test this? mask_var = z3.BitVec(f"{self.name}_table_mask_{index}", key_sort) lpm_mask = z3.BitVecVal(2**key_sort.size() - 1, key_sort) << mask_var match = (key_eval & lpm_mask) == (key_match & lpm_mask) key_pairs.append(match) elif key_type == "ternary": # Just apply a symbolic mask, any zero bit is a wildcard # TODO: Test this? mask = z3.Const(f"{self.name}_table_mask_{index}", key_sort) # this is dumb... if isinstance(key_sort, z3.BoolSortRef): match = z3.And(key_eval, mask) == z3.And(key_match, mask) else: match = (key_eval & mask) == (key_match & mask) key_pairs.append(match) elif key_type == "range": # Pick an arbitrary minimum and maximum within the bit range # the minimum must be strictly lesser than the max # I do not think a match is needed? # TODO: Test this? min_key = z3.Const(f"{self.name}_table_min_{index}", key_sort) max_key = z3.Const(f"{self.name}_table_max_{index}", key_sort) match = z3.And(z3.ULE(min_key, key_eval), z3.UGE(max_key, key_eval)) key_pairs.append(z3.And(match, z3.ULT(min_key, max_key))) elif key_type == "optional": # As far as I understand this is just a wildcard for control # plane purposes. Semantically, there is no point? # TODO: Test this? key_pairs.append(z3.BoolVal(True)) elif key_type == "selector": # Selectors are a deep rabbit hole # This rabbit hole does not yet make sense to me # FIXME: Implement # will intentionally fail if no implementation is present # impl = self.properties["implementation"] # impl_extern = self.prog_state.resolve_reference(impl) key_pairs.append(z3.BoolVal(True)) else: # weird key, might be some specific specification raise RuntimeError(f"Key type {key_type} not supported!") return z3.And(key_pairs)
def build_select_cond(p4_state, case_expr, match_list): select_cond = [] # these casts are kind of silly but simplify the code a lot if isinstance(case_expr, StructInstance): case_expr = case_expr.flatten() elif not isinstance(case_expr, list): case_expr = [case_expr] for idx, case_match in enumerate(case_expr): # default implies don't care, do not add # TODO: Verify that this assumption is right... if isinstance(case_match, DefaultExpression): select_cond.append(z3.BoolVal(True)) elif isinstance(case_match, P4Range): x = case_match.min y = case_match.max match_key = z3.And(z3.ULE(x, match_list[idx]), z3.UGE(y, match_list[idx])) select_cond.append(match_key) elif isinstance(case_match, P4Mask): val = p4_state.resolve_expr(case_match.value) mask = case_match.mask match_key = (val | ~mask) == (match_list[idx] | ~mask) select_cond.append(match_key) else: select_cond.append(case_match == match_list[idx]) if not select_cond: return z3.BoolVal(False) return z3.And(*select_cond)
def eval(self, p4_state): context = p4_state.current_context() cond = z3.simplify(p4_state.resolve_expr(self.cond)) forward_cond_copy = context.tmp_forward_cond then_vars = None if not cond == z3.BoolVal(False): var_store, contexts = p4_state.checkpoint() context.tmp_forward_cond = z3.And(forward_cond_copy, cond) try: self.then_block.eval(p4_state) except ParserException: RejectState().eval(p4_state) if not (context.has_returned or p4_state.has_exited): then_vars = p4_state.get_attrs() p4_state.has_exited = False context.has_returned = False p4_state.restore(var_store, contexts) if not cond == z3.BoolVal(True): var_store, contexts = p4_state.checkpoint() context.tmp_forward_cond = z3.And(forward_cond_copy, z3.Not(cond)) try: self.else_block.eval(p4_state) except ParserException: RejectState().eval(p4_state) if p4_state.has_exited or context.has_returned: p4_state.restore(var_store, contexts) p4_state.has_exited = False context.has_returned = False context.tmp_forward_cond = forward_cond_copy if then_vars: merge_attrs(p4_state, cond, then_vars)
def operator(x, y): # if x and y are ints we might deal with a signed value # we need to use the normal operator in this case if isinstance(x, int) and isinstance(y, int): return op.gt(x, y) # FIXME: Find a better way to model negative comparions # right now we have this hack if isinstance(x, int) and x < 0: return z3.BoolVal(False) if isinstance(y, int) and y < 0: return z3.BoolVal(True) return z3.UGT(x, y)
def extract_hdr(self, ctx, merged_args): hdr = merged_args[self.hdr_param_name].p4_val # apply the local and parent extern type ctxs for type_name, p4_type in self.extern_ctx.items(): ctx.add_type(type_name, ctx.resolve_type(p4_type)) for type_name, p4_type in self.type_ctx.items(): ctx.add_type(type_name, ctx.resolve_type(p4_type)) # advance the header index if a next field has been accessed hdr_stack = detect_hdr_stack_next(ctx, hdr) if hdr_stack: compare = hdr_stack.locals[ "nextIndex"] >= hdr_stack.locals["size"] if z3.simplify(compare) == z3.BoolVal(True): raise ParserException("Index out of bounds!") # grab the hdr value hdr_expr = ctx.resolve_expr(hdr) hdr_expr.activate() bind_const = z3.Const(f"{self.name}_{self.hdr_param_name}", hdr_expr.z3_type) hdr_expr.bind(bind_const) # advance the stack, if it exists if hdr_stack: hdr_stack.locals["lastIndex"] = hdr_stack.locals[ "nextIndex"] hdr_stack.locals["nextIndex"] += 1 self.call_counter += 1
def eval_table(self, p4_state): action_exprs = [] action_matches = [] context = p4_state.current_context() forward_cond_copy = context.tmp_forward_cond # only bother to evaluate if the table can actually hit if not self.locals["hit"] == z3.BoolVal(False): # note: the action lists are pass by reference here # first evaluate all the constant entries self.eval_const_entries(p4_state, action_exprs, action_matches) # then append dynamic table entries to the constant entries self.eval_table_entries(p4_state, action_exprs, action_matches) # finally start evaluating the default entry var_store, contexts = p4_state.checkpoint() # this hits when the table is either missed, or no action matches cond = z3.Or(self.locals["miss"], z3.Not(z3.Or(*action_matches))) context.tmp_forward_cond = z3.And(forward_cond_copy, cond) self.eval_default(p4_state) if p4_state.has_exited: p4_state.restore(var_store, contexts) p4_state.has_exited = False context.tmp_forward_cond = forward_cond_copy # generate a nested set of if expressions per available action for cond, then_vars in action_exprs: merge_attrs(p4_state, cond, then_vars)
def __init__(self, name, **properties): super(P4Table, self).__init__(name, None, {}) self.keys = [] self.const_entries = [] self.actions = OrderedDict() self.default_action = None self.tbl_action = z3.Int(f"{self.name}_action") self.p4_attrs["hit"] = z3.BoolVal(False) self.p4_attrs["miss"] = z3.BoolVal(True) self.p4_attrs["action_run"] = self self.p4_attrs["apply"] = self.apply self.add_keys(properties) self.add_default(properties) self.add_actions(properties) self.add_const_entries(properties)
def eval(self, p4_state): context = p4_state.current_context() if self.expr is None: expr = None else: # resolve the expr before restoring the state expr = p4_state.resolve_expr(self.expr) if isinstance(context.return_type, z3.BitVecSortRef): expr = z3_cast(expr, context.return_type) # we return a complex typed expression list, instantiate if isinstance(expr, list): instance = gen_instance(p4_state, "undefined", context.return_type) instance.set_list(expr) expr = instance cond = z3.simplify( z3.And(z3.Not(z3.Or(*context.forward_conds)), context.tmp_forward_cond)) if not cond == z3.BoolVal(False): context.return_states.append((cond, p4_state.copy_attrs())) if expr is not None: context.return_exprs.append((cond, expr)) context.has_returned = True context.forward_conds.append(context.tmp_forward_cond)
def __init__(self, name, **properties): super(P4Table, self).__init__(name, params={}) self.keys = [] self.const_entries = [] self.actions = OrderedDict() self.default_action = None self.tbl_action = z3.Int(f"{self.name}_action") self.implementation = None self.locals["hit"] = z3.BoolVal(False) self.locals["miss"] = z3.BoolVal(True) self.locals["action_run"] = self self.locals["apply"] = self.apply # some custom logic to resolve properties self.add_keys(properties) self.add_default(properties) self.add_actions(properties) self.add_const_entries(properties) # set the rest self.properties = properties
def eval_keys(self, p4_state): key_pairs = [] if not self.keys: # there is nothing to match with... return z3.BoolVal(False) for index, key in enumerate(self.keys): key_eval = p4_state.resolve_expr(key) key_sort = key_eval.sort() key_match = z3.Const(f"{self.name}_table_key_{index}", key_sort) key_pairs.append(key_eval == key_match) return z3.And(key_pairs)
def eval(self, p4_state): cond = z3.simplify(p4_state.resolve_expr(self.cond)) # handle side effects for function and table calls if cond == z3.BoolVal(False): return p4_state.resolve_expr(self.else_val) if cond == z3.BoolVal(True): return p4_state.resolve_expr(self.then_val) var_store, chain_copy = p4_state.checkpoint() context = p4_state.current_context() forward_cond_copy = context.tmp_forward_cond context.tmp_forward_cond = z3.And(forward_cond_copy, cond) then_expr = p4_state.resolve_expr(self.then_val) then_vars = p4_state.get_attrs() p4_state.restore(var_store, chain_copy) context.tmp_forward_cond = forward_cond_copy else_expr = p4_state.resolve_expr(self.else_val) merge_attrs(p4_state, cond, then_vars) return handle_mux(cond, then_expr, else_expr)
def eval_switch_table_matches(self, ctx, table): cases = OrderedDict() if table.immutable: # if the table is immutable we can only match on const entries for c_keys, (action_name, _) in table.const_entries: const_matches = [] # check if the action of the entry is even present if action_name not in self.case_blocks: continue # compute the match key # FIXME: Deal with side effects here # Maybe just checkpoint and restore? Ugh. So expensive... match_cond = table.get_const_matches(ctx, c_keys) action = table.actions[action_name][0] if action_name in cases: prev_match, _ = cases[action_name] match_cond = z3.Or(match_cond, prev_match) const_matches.append(match_cond) cases[action_name] = ( match_cond, self.case_blocks[action_name]) # we also need to process the default action separately # this basically hits only if no other case matches _, action_name, _ = table.default_action match_cond = z3.Not(z3.Or(*const_matches)) if action_name in cases: prev_match, _ = cases[action_name] match_cond = z3.Or(match_cond, prev_match) const_matches.append(match_cond) cases[action_name] = (match_cond, self.case_blocks[action_name]) else: # otherwise we are dealing with a normal table # just insert the match entries combined with the hit expression add_default = None for case_name, case_block in self.case_blocks.items(): match_var = table.tbl_action action = table.actions[case_name][0] match_cond = z3.And(table.locals["hit"], (action == match_var)) cases[case_name] = (match_cond, case_block) # we need to check if the default is in the cases # this implies that the "default" case can never be executed if case_name == table.default_action[1]: add_default = (case_name, (z3.BoolVal(True), case_block)) if add_default and len(table.actions) == len(cases): cases[add_default[0]] = add_default[1] return cases
def eval(self, p4_state): # FIXME: This checkpointing should not be necessary # Figure out what is going on var_store, contexts = p4_state.checkpoint() forward_conds = [] tmp_forward_conds = [] for context in reversed(p4_state.contexts): context.copy_out(p4_state) forward_conds.extend(context.forward_conds) tmp_forward_conds.append(context.tmp_forward_cond) context = p4_state.current_context() cond = z3.simplify( z3.And(z3.Not(z3.Or(*forward_conds)), z3.And(*tmp_forward_conds))) if not cond == z3.BoolVal(False): p4_state.exit_states.append((cond, p4_state.get_z3_repr())) p4_state.has_exited = True p4_state.restore(var_store, contexts) context.forward_conds.append(context.tmp_forward_cond)
def eval(self, p4_state): switches = [] for case_val, case_name in reversed(self.cases): case_expr = p4_state.resolve_expr(case_val) select_cond = [] if isinstance(case_expr, P4ComplexInstance): case_expr = case_expr.flatten() if isinstance(case_expr, list): for idx, case_match in enumerate(case_expr): # default implies don't care, do not add # TODO: Verify that this assumption is right... if isinstance(case_match, DefaultExpression): continue match = self.match[idx] match_expr = p4_state.resolve_expr(match) cond = match_expr == case_match select_cond.append(cond) else: # default implies don't care, do not add # TODO: Verify that this assumption is right... if isinstance(case_expr, DefaultExpression): continue for match in self.match: match_expr = p4_state.resolve_expr(match) cond = case_expr == match_expr select_cond.append(cond) if not select_cond: select_cond = [z3.BoolVal(False)] var_store, chain_copy = p4_state.checkpoint() parser_state = self.state_list[case_name] state_expr = parser_state.eval(p4_state) p4_state.restore(var_store, chain_copy) switches.append((z3.And(*select_cond), state_expr)) default_parser_state = self.state_list[self.default] expr = default_parser_state.eval(p4_state) for cond, state_expr in switches: expr = z3.If(cond, state_expr, expr) return expr
def operator(x, y): # if x and y are ints we might deal with a signed value # we need to use the normal operator in this case if isinstance(x, int) and isinstance(y, int): return z3.BoolVal(op.gt(x, y)) return z3.UGT(x, y)
def operator(x, y): # this trick ensures that we always return a z3 value return op.eq(x, y) == z3.BoolVal(True)