1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//! Bounds for the symbolic representation-item walk (issue #2866).
//!
//! Split out of `items.rs` so the guards are readable on their own: `items.rs`
//! is the per-IFC-type dispatch, and this is the policy that keeps it from
//! walking a hostile file forever. Everything here is about WHAT the walk is
//! allowed to do; nothing here knows about any IFC type.
use extract_symbolic_item_inner;
use ;
use RenderFrameRebase;
use Transform2D;
use ;
use FxHashSet;
use HashMap;
/// Maximum representation-item nesting this walk follows.
///
/// Every id below comes from the FILE, and a malformed or hostile IFC can
/// close a cycle three different ways here: `IfcGeometricSet.Elements` back to
/// itself, the `IfcMappedItem -> IfcRepresentationMap ->
/// IfcShapeRepresentation.Items` chain, and `IfcCompositeCurve.Segments ->
/// IfcCompositeCurveSegment.ParentCurve`. This walk is reachable from
/// `extract_symbolic_data` on raw uploaded bytes
/// (`apps/server/src/services/streaming.rs`), so unbounded each one aborts the
/// process with a stack overflow -- an abort, not a catchable panic (#2866).
///
/// Kept in step with `MAX_MAPPED_ITEM_DEPTH` in `element.rs` and in
/// `geometry/src/router/processing.rs`, which walk the same mapped-item chain.
pub const MAX_ITEM_DEPTH: u32 = 32;
/// Number of times the WHOLE EXTRACTION may re-enter an id it has already
/// visited, shared across every top-level item.
///
/// A depth cap bounds a path's LENGTH and not its BREADTH: `k` items that each
/// lead back into a cycle cost `O(k^depth)`, so a cap alone converts an abort
/// into a hang -- measured at 7.21s for k=3 on the sibling resolver in #2864
/// before its guard landed. This is the breadth bound.
///
/// Charged on REVISITS ONLY, which is what makes it safe to have at all. An
/// earlier version charged every visit, and that silently truncated a
/// WELL-FORMED file: `IfcGeometricSet` recurses per element, so one flat set
/// of 200,050 curves emitted 199,999 and dropped 51 with no error -- plan
/// hatching, a survey drawing or imported DWG geometry reaches that size
/// legitimately. First visits are bounded by the file itself (an entity must
/// exist to be reached), so they cannot be the exponential; only revisits can,
/// and an acyclic DAG reaching one node down 2^levels paths is exactly that.
///
/// The BUDGET this bounds lives on `SymbolicAccumulator`
/// (`output_cap::SymbolicAccumulator::charge_revisit`), not on [`ItemWalk`],
/// and that placement is the fix for #2937: `extract_symbolic_item` builds a
/// fresh `ItemWalk` for every top-level item, so a budget stored there reset
/// on every item and a file of N items got `N x MAX_ITEM_REVISITS` instead of
/// one bound governing the file. The accumulator is threaded through the
/// whole extraction, so charging it once per revisit -- wherever in the file
/// that revisit happens -- is what makes this a FILE bound rather than an
/// ITEM bound.
///
/// THE VALUE HAS NOT BEEN RE-SIZED FOR ITS NEW SCOPE, and that is a deliberate
/// choice rather than an oversight. 200,000 was picked as a per-item number
/// and is now spent across the whole file, so a file whose revisits are spread
/// over many top-level items can truncate where `main` did not: a 12-product
/// nested block import (306 KB) emits 202,400 of its 240,000 curves here and
/// all 240,000 on `main`.
///
/// It is kept because the alternative is worse and the loss is REPORTED. The
/// same 240,000 curves already truncate on `main` at 200,200 when they sit
/// under ONE top-level item -- so the mis-sizing predates this change; what
/// this widens is which arrangements of the same file hit it. Raising the
/// constant trades directly against the hole this bound exists to close (a
/// fan-out spread thinly across items, which nothing bounded before), and that
/// trade wants a corpus measurement rather than a guess. Until then, a file
/// that loses content says so via `truncated`, which is the property that
/// makes the current value tolerable.
pub const MAX_ITEM_REVISITS: u32 = 200_000;
/// State threaded through the walk: the ancestors on the current path, and the
/// remaining emit budget.
pub
pub
pub