1*4b9c6d91SCole Faust#!/usr/bin/env python3 2*4b9c6d91SCole Faust# -*- coding: utf-8 -*- 3*4b9c6d91SCole Faust# 4*4b9c6d91SCole Faust# Copyright (C) 2018 The Android Open Source Project 5*4b9c6d91SCole Faust# 6*4b9c6d91SCole Faust# Licensed under the Apache License, Version 2.0 (the "License"); 7*4b9c6d91SCole Faust# you may not use this file except in compliance with the License. 8*4b9c6d91SCole Faust# You may obtain a copy of the License at 9*4b9c6d91SCole Faust# 10*4b9c6d91SCole Faust# http://www.apache.org/licenses/LICENSE-2.0 11*4b9c6d91SCole Faust# 12*4b9c6d91SCole Faust# Unless required by applicable law or agreed to in writing, software 13*4b9c6d91SCole Faust# distributed under the License is distributed on an "AS IS" BASIS, 14*4b9c6d91SCole Faust# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15*4b9c6d91SCole Faust# See the License for the specific language governing permissions and 16*4b9c6d91SCole Faust# limitations under the License. 17*4b9c6d91SCole Faust"""A BPF compiler for the Minijail policy file.""" 18*4b9c6d91SCole Faust 19*4b9c6d91SCole Faustfrom __future__ import print_function 20*4b9c6d91SCole Faust 21*4b9c6d91SCole Faustimport enum 22*4b9c6d91SCole Faust 23*4b9c6d91SCole Fausttry: 24*4b9c6d91SCole Faust import bpf 25*4b9c6d91SCole Faust import parser # pylint: disable=wrong-import-order 26*4b9c6d91SCole Faustexcept ImportError: 27*4b9c6d91SCole Faust from minijail import bpf 28*4b9c6d91SCole Faust from minijail import parser # pylint: disable=wrong-import-order 29*4b9c6d91SCole Faust 30*4b9c6d91SCole Faust 31*4b9c6d91SCole Faustclass OptimizationStrategy(enum.Enum): 32*4b9c6d91SCole Faust """The available optimization strategies.""" 33*4b9c6d91SCole Faust 34*4b9c6d91SCole Faust # Generate a linear chain of syscall number checks. Works best for policies 35*4b9c6d91SCole Faust # with very few syscalls. 36*4b9c6d91SCole Faust LINEAR = 'linear' 37*4b9c6d91SCole Faust 38*4b9c6d91SCole Faust # Generate a binary search tree for the syscalls. Works best for policies 39*4b9c6d91SCole Faust # with a lot of syscalls, where no one syscall dominates. 40*4b9c6d91SCole Faust BST = 'bst' 41*4b9c6d91SCole Faust 42*4b9c6d91SCole Faust def __str__(self): 43*4b9c6d91SCole Faust return self.value 44*4b9c6d91SCole Faust 45*4b9c6d91SCole Faust 46*4b9c6d91SCole Faustclass SyscallPolicyEntry: 47*4b9c6d91SCole Faust """The parsed version of a seccomp policy line.""" 48*4b9c6d91SCole Faust 49*4b9c6d91SCole Faust def __init__(self, name, number, frequency): 50*4b9c6d91SCole Faust self.name = name 51*4b9c6d91SCole Faust self.number = number 52*4b9c6d91SCole Faust self.frequency = frequency 53*4b9c6d91SCole Faust self.accumulated = 0 54*4b9c6d91SCole Faust self.filter = None 55*4b9c6d91SCole Faust 56*4b9c6d91SCole Faust def __repr__(self): 57*4b9c6d91SCole Faust return ('SyscallPolicyEntry<name: %s, number: %d, ' 58*4b9c6d91SCole Faust 'frequency: %d, filter: %r>') % ( 59*4b9c6d91SCole Faust self.name, self.number, self.frequency, 60*4b9c6d91SCole Faust self.filter.instructions if self.filter else None) 61*4b9c6d91SCole Faust 62*4b9c6d91SCole Faust def simulate(self, arch, syscall_number, *args): 63*4b9c6d91SCole Faust """Simulate the policy with the given arguments.""" 64*4b9c6d91SCole Faust if not self.filter: 65*4b9c6d91SCole Faust return (0, 'ALLOW') 66*4b9c6d91SCole Faust return bpf.simulate(self.filter.instructions, arch, syscall_number, 67*4b9c6d91SCole Faust *args) 68*4b9c6d91SCole Faust 69*4b9c6d91SCole Faust 70*4b9c6d91SCole Faustclass SyscallPolicyRange: 71*4b9c6d91SCole Faust """A contiguous range of SyscallPolicyEntries that have the same action.""" 72*4b9c6d91SCole Faust 73*4b9c6d91SCole Faust def __init__(self, *entries): 74*4b9c6d91SCole Faust self.numbers = (entries[0].number, entries[-1].number + 1) 75*4b9c6d91SCole Faust self.frequency = sum(e.frequency for e in entries) 76*4b9c6d91SCole Faust self.accumulated = 0 77*4b9c6d91SCole Faust self.filter = entries[0].filter 78*4b9c6d91SCole Faust 79*4b9c6d91SCole Faust def __repr__(self): 80*4b9c6d91SCole Faust return 'SyscallPolicyRange<numbers: %r, frequency: %d, filter: %r>' % ( 81*4b9c6d91SCole Faust self.numbers, self.frequency, 82*4b9c6d91SCole Faust self.filter.instructions if self.filter else None) 83*4b9c6d91SCole Faust 84*4b9c6d91SCole Faust def simulate(self, arch, syscall_number, *args): 85*4b9c6d91SCole Faust """Simulate the policy with the given arguments.""" 86*4b9c6d91SCole Faust if not self.filter: 87*4b9c6d91SCole Faust return (0, 'ALLOW') 88*4b9c6d91SCole Faust return self.filter.simulate(arch, syscall_number, *args) 89*4b9c6d91SCole Faust 90*4b9c6d91SCole Faust 91*4b9c6d91SCole Faustdef _convert_to_ranges(entries): 92*4b9c6d91SCole Faust entries = list(sorted(entries, key=lambda r: r.number)) 93*4b9c6d91SCole Faust lower = 0 94*4b9c6d91SCole Faust while lower < len(entries): 95*4b9c6d91SCole Faust upper = lower + 1 96*4b9c6d91SCole Faust while upper < len(entries): 97*4b9c6d91SCole Faust if entries[upper - 1].filter != entries[upper].filter: 98*4b9c6d91SCole Faust break 99*4b9c6d91SCole Faust if entries[upper - 1].number + 1 != entries[upper].number: 100*4b9c6d91SCole Faust break 101*4b9c6d91SCole Faust upper += 1 102*4b9c6d91SCole Faust yield SyscallPolicyRange(*entries[lower:upper]) 103*4b9c6d91SCole Faust lower = upper 104*4b9c6d91SCole Faust 105*4b9c6d91SCole Faust 106*4b9c6d91SCole Faustdef _compile_single_range(entry, 107*4b9c6d91SCole Faust accept_action, 108*4b9c6d91SCole Faust reject_action, 109*4b9c6d91SCole Faust lower_bound=0, 110*4b9c6d91SCole Faust upper_bound=1e99): 111*4b9c6d91SCole Faust action = accept_action 112*4b9c6d91SCole Faust if entry.filter: 113*4b9c6d91SCole Faust action = entry.filter 114*4b9c6d91SCole Faust if entry.numbers[1] - entry.numbers[0] == 1: 115*4b9c6d91SCole Faust # Single syscall. 116*4b9c6d91SCole Faust # Accept if |X == nr|. 117*4b9c6d91SCole Faust return (1, 118*4b9c6d91SCole Faust bpf.SyscallEntry( 119*4b9c6d91SCole Faust entry.numbers[0], action, reject_action, op=bpf.BPF_JEQ)) 120*4b9c6d91SCole Faust elif entry.numbers[0] == lower_bound: 121*4b9c6d91SCole Faust # Syscall range aligned with the lower bound. 122*4b9c6d91SCole Faust # Accept if |X < nr[1]|. 123*4b9c6d91SCole Faust return (1, 124*4b9c6d91SCole Faust bpf.SyscallEntry( 125*4b9c6d91SCole Faust entry.numbers[1], reject_action, action, op=bpf.BPF_JGE)) 126*4b9c6d91SCole Faust elif entry.numbers[1] == upper_bound: 127*4b9c6d91SCole Faust # Syscall range aligned with the upper bound. 128*4b9c6d91SCole Faust # Accept if |X >= nr[0]|. 129*4b9c6d91SCole Faust return (1, 130*4b9c6d91SCole Faust bpf.SyscallEntry( 131*4b9c6d91SCole Faust entry.numbers[0], action, reject_action, op=bpf.BPF_JGE)) 132*4b9c6d91SCole Faust # Syscall range in the middle. 133*4b9c6d91SCole Faust # Accept if |nr[0] <= X < nr[1]|. 134*4b9c6d91SCole Faust upper_entry = bpf.SyscallEntry( 135*4b9c6d91SCole Faust entry.numbers[1], reject_action, action, op=bpf.BPF_JGE) 136*4b9c6d91SCole Faust return (2, 137*4b9c6d91SCole Faust bpf.SyscallEntry( 138*4b9c6d91SCole Faust entry.numbers[0], upper_entry, reject_action, op=bpf.BPF_JGE)) 139*4b9c6d91SCole Faust 140*4b9c6d91SCole Faust 141*4b9c6d91SCole Faustdef _compile_ranges_linear(ranges, accept_action, reject_action): 142*4b9c6d91SCole Faust # Compiles the list of ranges into a simple linear list of comparisons. In 143*4b9c6d91SCole Faust # order to make the generated code a bit more efficient, we sort the 144*4b9c6d91SCole Faust # ranges by frequency, so that the most frequently-called syscalls appear 145*4b9c6d91SCole Faust # earlier in the chain. 146*4b9c6d91SCole Faust cost = 0 147*4b9c6d91SCole Faust accumulated_frequencies = 0 148*4b9c6d91SCole Faust next_action = reject_action 149*4b9c6d91SCole Faust for entry in sorted(ranges, key=lambda r: r.frequency): 150*4b9c6d91SCole Faust current_cost, next_action = _compile_single_range( 151*4b9c6d91SCole Faust entry, accept_action, next_action) 152*4b9c6d91SCole Faust accumulated_frequencies += entry.frequency 153*4b9c6d91SCole Faust cost += accumulated_frequencies * current_cost 154*4b9c6d91SCole Faust return (cost, next_action) 155*4b9c6d91SCole Faust 156*4b9c6d91SCole Faust 157*4b9c6d91SCole Faustdef _compile_entries_linear(entries, accept_action, reject_action): 158*4b9c6d91SCole Faust return _compile_ranges_linear( 159*4b9c6d91SCole Faust _convert_to_ranges(entries), accept_action, reject_action)[1] 160*4b9c6d91SCole Faust 161*4b9c6d91SCole Faust 162*4b9c6d91SCole Faustdef _compile_entries_bst(entries, accept_action, reject_action): 163*4b9c6d91SCole Faust # Instead of generating a linear list of comparisons, this method generates 164*4b9c6d91SCole Faust # a binary search tree, where some of the leaves can be linear chains of 165*4b9c6d91SCole Faust # comparisons. 166*4b9c6d91SCole Faust # 167*4b9c6d91SCole Faust # Even though we are going to perform a binary search over the syscall 168*4b9c6d91SCole Faust # number, we would still like to rotate some of the internal nodes of the 169*4b9c6d91SCole Faust # binary search tree so that more frequently-used syscalls can be accessed 170*4b9c6d91SCole Faust # more cheaply (i.e. fewer internal nodes need to be traversed to reach 171*4b9c6d91SCole Faust # them). 172*4b9c6d91SCole Faust # 173*4b9c6d91SCole Faust # This uses Dynamic Programming to generate all possible BSTs efficiently 174*4b9c6d91SCole Faust # (in O(n^3)) so that we can get the absolute minimum-cost tree that matches 175*4b9c6d91SCole Faust # all syscall entries. It does so by considering all of the O(n^2) possible 176*4b9c6d91SCole Faust # sub-intervals, and for each one of those try all of the O(n) partitions of 177*4b9c6d91SCole Faust # that sub-interval. At each step, it considers putting the remaining 178*4b9c6d91SCole Faust # entries in a linear comparison chain as well as another BST, and chooses 179*4b9c6d91SCole Faust # the option that minimizes the total overall cost. 180*4b9c6d91SCole Faust # 181*4b9c6d91SCole Faust # Between every pair of non-contiguous allowed syscalls, there are two 182*4b9c6d91SCole Faust # locally optimal options as to where to set the partition for the 183*4b9c6d91SCole Faust # subsequent ranges: aligned to the end of the left subrange or to the 184*4b9c6d91SCole Faust # beginning of the right subrange. The fact that these two options have 185*4b9c6d91SCole Faust # slightly different costs, combined with the possibility of a subtree to 186*4b9c6d91SCole Faust # use the linear chain strategy (which has a completely different cost 187*4b9c6d91SCole Faust # model), causes the target cost function that we are trying to optimize to 188*4b9c6d91SCole Faust # not be unimodal / convex. This unfortunately means that more clever 189*4b9c6d91SCole Faust # techniques like using ternary search (which would reduce the overall 190*4b9c6d91SCole Faust # complexity to O(n^2 log n)) do not work in all cases. 191*4b9c6d91SCole Faust ranges = list(_convert_to_ranges(entries)) 192*4b9c6d91SCole Faust 193*4b9c6d91SCole Faust accumulated = 0 194*4b9c6d91SCole Faust for entry in ranges: 195*4b9c6d91SCole Faust accumulated += entry.frequency 196*4b9c6d91SCole Faust entry.accumulated = accumulated 197*4b9c6d91SCole Faust 198*4b9c6d91SCole Faust # Memoization cache to build the DP table top-down, which is easier to 199*4b9c6d91SCole Faust # understand. 200*4b9c6d91SCole Faust memoized_costs = {} 201*4b9c6d91SCole Faust 202*4b9c6d91SCole Faust def _generate_syscall_bst(ranges, indices, bounds=(0, 2**64 - 1)): 203*4b9c6d91SCole Faust assert bounds[0] <= ranges[indices[0]].numbers[0], (indices, bounds) 204*4b9c6d91SCole Faust assert ranges[indices[1] - 1].numbers[1] <= bounds[1], (indices, 205*4b9c6d91SCole Faust bounds) 206*4b9c6d91SCole Faust 207*4b9c6d91SCole Faust if bounds in memoized_costs: 208*4b9c6d91SCole Faust return memoized_costs[bounds] 209*4b9c6d91SCole Faust if indices[1] - indices[0] == 1: 210*4b9c6d91SCole Faust if bounds == ranges[indices[0]].numbers: 211*4b9c6d91SCole Faust # If bounds are tight around the syscall, it costs nothing. 212*4b9c6d91SCole Faust memoized_costs[bounds] = (0, ranges[indices[0]].filter 213*4b9c6d91SCole Faust or accept_action) 214*4b9c6d91SCole Faust return memoized_costs[bounds] 215*4b9c6d91SCole Faust result = _compile_single_range(ranges[indices[0]], accept_action, 216*4b9c6d91SCole Faust reject_action) 217*4b9c6d91SCole Faust memoized_costs[bounds] = (result[0] * ranges[indices[0]].frequency, 218*4b9c6d91SCole Faust result[1]) 219*4b9c6d91SCole Faust return memoized_costs[bounds] 220*4b9c6d91SCole Faust 221*4b9c6d91SCole Faust # Try the linear model first and use that as the best estimate so far. 222*4b9c6d91SCole Faust best_cost = _compile_ranges_linear(ranges[slice(*indices)], 223*4b9c6d91SCole Faust accept_action, reject_action) 224*4b9c6d91SCole Faust 225*4b9c6d91SCole Faust # Now recursively go through all possible partitions of the interval 226*4b9c6d91SCole Faust # currently being considered. 227*4b9c6d91SCole Faust previous_accumulated = ranges[indices[0]].accumulated - ranges[ 228*4b9c6d91SCole Faust indices[0]].frequency 229*4b9c6d91SCole Faust bst_comparison_cost = ( 230*4b9c6d91SCole Faust ranges[indices[1] - 1].accumulated - previous_accumulated) 231*4b9c6d91SCole Faust for i, entry in enumerate(ranges[slice(*indices)]): 232*4b9c6d91SCole Faust candidates = [entry.numbers[0]] 233*4b9c6d91SCole Faust if i: 234*4b9c6d91SCole Faust candidates.append(ranges[i - 1 + indices[0]].numbers[1]) 235*4b9c6d91SCole Faust for cutoff_bound in candidates: 236*4b9c6d91SCole Faust if not bounds[0] < cutoff_bound < bounds[1]: 237*4b9c6d91SCole Faust continue 238*4b9c6d91SCole Faust if not indices[0] < i + indices[0] < indices[1]: 239*4b9c6d91SCole Faust continue 240*4b9c6d91SCole Faust left_subtree = _generate_syscall_bst( 241*4b9c6d91SCole Faust ranges, (indices[0], i + indices[0]), 242*4b9c6d91SCole Faust (bounds[0], cutoff_bound)) 243*4b9c6d91SCole Faust right_subtree = _generate_syscall_bst( 244*4b9c6d91SCole Faust ranges, (i + indices[0], indices[1]), 245*4b9c6d91SCole Faust (cutoff_bound, bounds[1])) 246*4b9c6d91SCole Faust best_cost = min( 247*4b9c6d91SCole Faust best_cost, 248*4b9c6d91SCole Faust (bst_comparison_cost + left_subtree[0] + right_subtree[0], 249*4b9c6d91SCole Faust bpf.SyscallEntry( 250*4b9c6d91SCole Faust cutoff_bound, 251*4b9c6d91SCole Faust right_subtree[1], 252*4b9c6d91SCole Faust left_subtree[1], 253*4b9c6d91SCole Faust op=bpf.BPF_JGE))) 254*4b9c6d91SCole Faust 255*4b9c6d91SCole Faust memoized_costs[bounds] = best_cost 256*4b9c6d91SCole Faust return memoized_costs[bounds] 257*4b9c6d91SCole Faust 258*4b9c6d91SCole Faust return _generate_syscall_bst(ranges, (0, len(ranges)))[1] 259*4b9c6d91SCole Faust 260*4b9c6d91SCole Faust 261*4b9c6d91SCole Faustclass PolicyCompiler: 262*4b9c6d91SCole Faust """A parser for the Minijail seccomp policy file format.""" 263*4b9c6d91SCole Faust 264*4b9c6d91SCole Faust def __init__(self, arch): 265*4b9c6d91SCole Faust self._arch = arch 266*4b9c6d91SCole Faust 267*4b9c6d91SCole Faust def compile_file(self, 268*4b9c6d91SCole Faust policy_filename, 269*4b9c6d91SCole Faust *, 270*4b9c6d91SCole Faust optimization_strategy, 271*4b9c6d91SCole Faust kill_action, 272*4b9c6d91SCole Faust include_depth_limit=10, 273*4b9c6d91SCole Faust override_default_action=None, 274*4b9c6d91SCole Faust denylist=False, 275*4b9c6d91SCole Faust ret_log=False): 276*4b9c6d91SCole Faust """Return a compiled BPF program from the provided policy file.""" 277*4b9c6d91SCole Faust policy_parser = parser.PolicyParser( 278*4b9c6d91SCole Faust self._arch, 279*4b9c6d91SCole Faust kill_action=kill_action, 280*4b9c6d91SCole Faust include_depth_limit=include_depth_limit, 281*4b9c6d91SCole Faust override_default_action=override_default_action, 282*4b9c6d91SCole Faust denylist=denylist, 283*4b9c6d91SCole Faust ret_log=ret_log) 284*4b9c6d91SCole Faust parsed_policy = policy_parser.parse_file(policy_filename) 285*4b9c6d91SCole Faust entries = [ 286*4b9c6d91SCole Faust self.compile_filter_statement( 287*4b9c6d91SCole Faust filter_statement, kill_action=kill_action, denylist=denylist) 288*4b9c6d91SCole Faust for filter_statement in parsed_policy.filter_statements 289*4b9c6d91SCole Faust ] 290*4b9c6d91SCole Faust 291*4b9c6d91SCole Faust visitor = bpf.FlatteningVisitor( 292*4b9c6d91SCole Faust arch=self._arch, kill_action=kill_action) 293*4b9c6d91SCole Faust if denylist: 294*4b9c6d91SCole Faust accept_action = kill_action 295*4b9c6d91SCole Faust reject_action = bpf.Allow() 296*4b9c6d91SCole Faust else: 297*4b9c6d91SCole Faust accept_action = bpf.Allow() 298*4b9c6d91SCole Faust reject_action = parsed_policy.default_action 299*4b9c6d91SCole Faust if entries: 300*4b9c6d91SCole Faust if optimization_strategy == OptimizationStrategy.BST: 301*4b9c6d91SCole Faust next_action = _compile_entries_bst(entries, accept_action, 302*4b9c6d91SCole Faust reject_action) 303*4b9c6d91SCole Faust else: 304*4b9c6d91SCole Faust next_action = _compile_entries_linear(entries, accept_action, 305*4b9c6d91SCole Faust reject_action) 306*4b9c6d91SCole Faust next_action.accept(bpf.ArgFilterForwardingVisitor(visitor)) 307*4b9c6d91SCole Faust reject_action.accept(visitor) 308*4b9c6d91SCole Faust accept_action.accept(visitor) 309*4b9c6d91SCole Faust bpf.ValidateArch(next_action).accept(visitor) 310*4b9c6d91SCole Faust else: 311*4b9c6d91SCole Faust reject_action.accept(visitor) 312*4b9c6d91SCole Faust bpf.ValidateArch(reject_action).accept(visitor) 313*4b9c6d91SCole Faust return visitor.result 314*4b9c6d91SCole Faust 315*4b9c6d91SCole Faust def compile_filter_statement(self, 316*4b9c6d91SCole Faust filter_statement, 317*4b9c6d91SCole Faust *, 318*4b9c6d91SCole Faust kill_action, 319*4b9c6d91SCole Faust denylist=False): 320*4b9c6d91SCole Faust """Compile one parser.FilterStatement into BPF.""" 321*4b9c6d91SCole Faust policy_entry = SyscallPolicyEntry(filter_statement.syscall.name, 322*4b9c6d91SCole Faust filter_statement.syscall.number, 323*4b9c6d91SCole Faust filter_statement.frequency) 324*4b9c6d91SCole Faust # In each step of the way, the false action is the one that is taken if 325*4b9c6d91SCole Faust # the immediate boolean condition does not match. This means that the 326*4b9c6d91SCole Faust # false action taken here is the one that applies if the whole 327*4b9c6d91SCole Faust # expression fails to match. 328*4b9c6d91SCole Faust false_action = filter_statement.filters[-1].action 329*4b9c6d91SCole Faust if not denylist and false_action == bpf.Allow(): 330*4b9c6d91SCole Faust return policy_entry 331*4b9c6d91SCole Faust # We then traverse the list of filters backwards since we want 332*4b9c6d91SCole Faust # the root of the DAG to be the very first boolean operation in 333*4b9c6d91SCole Faust # the filter chain. 334*4b9c6d91SCole Faust for filt in filter_statement.filters[:-1][::-1]: 335*4b9c6d91SCole Faust for disjunction in filt.expression: 336*4b9c6d91SCole Faust # This is the jump target of the very last comparison in the 337*4b9c6d91SCole Faust # conjunction. Given that any conjunction that succeeds should 338*4b9c6d91SCole Faust # make the whole expression succeed, make the very last 339*4b9c6d91SCole Faust # comparison jump to the accept action if it succeeds. 340*4b9c6d91SCole Faust true_action = filt.action 341*4b9c6d91SCole Faust for atom in disjunction: 342*4b9c6d91SCole Faust block = bpf.Atom(atom.argument_index, atom.op, atom.value, 343*4b9c6d91SCole Faust true_action, false_action) 344*4b9c6d91SCole Faust true_action = block 345*4b9c6d91SCole Faust false_action = true_action 346*4b9c6d91SCole Faust policy_filter = false_action 347*4b9c6d91SCole Faust 348*4b9c6d91SCole Faust # Lower all Atoms into WideAtoms. 349*4b9c6d91SCole Faust lowering_visitor = bpf.LoweringVisitor(arch=self._arch) 350*4b9c6d91SCole Faust policy_filter = lowering_visitor.process(policy_filter) 351*4b9c6d91SCole Faust 352*4b9c6d91SCole Faust # Flatten the IR DAG into a single BasicBlock. 353*4b9c6d91SCole Faust flattening_visitor = bpf.FlatteningVisitor( 354*4b9c6d91SCole Faust arch=self._arch, kill_action=kill_action) 355*4b9c6d91SCole Faust policy_filter.accept(flattening_visitor) 356*4b9c6d91SCole Faust policy_entry.filter = flattening_visitor.result 357*4b9c6d91SCole Faust return policy_entry 358