|
25 | 25 | # DB specification. It stops a pointer fan-out, where nested pointers to shared |
26 | 26 | # targets would otherwise cost 2**depth decode operations. The largest real |
27 | 27 | # records decode a few hundred values, so the limit leaves a wide margin. |
28 | | -# Pointer cycles and over-deep data are caught separately by Python's own |
29 | | -# recursion limit (see ``decode``). |
| 28 | +# Pointer cycles and over-deep data are caught separately by an explicit, |
| 29 | +# call-local depth limit (see ``decode``). |
30 | 30 | _MAX_VALUES = 1 << 16 |
| 31 | +_MAX_DEPTH = 512 |
31 | 32 | _TOO_MANY_VALUES = ( |
32 | 33 | "The MaxMind DB file's data section exceeds the maximum number of values" |
33 | 34 | ) |
| 35 | +_TOO_DEEP = "The MaxMind DB file's data section exceeds the maximum depth" |
34 | 36 |
|
35 | 37 |
|
36 | 38 | class Decoder: |
@@ -63,10 +65,14 @@ def _decode_array( |
63 | 65 | budget[0] -= size |
64 | 66 | if budget[0] < 0: |
65 | 67 | raise InvalidDatabaseError(_TOO_MANY_VALUES) |
| 68 | + budget[1] += 1 |
| 69 | + if budget[1] > _MAX_DEPTH: |
| 70 | + raise InvalidDatabaseError(_TOO_DEEP) |
66 | 71 | array = [] |
67 | 72 | for _ in range(size): |
68 | 73 | (value, offset) = self._decode(offset, budget) |
69 | 74 | array.append(value) |
| 75 | + budget[1] -= 1 |
70 | 76 | return array, offset |
71 | 77 |
|
72 | 78 | def _decode_boolean( |
@@ -136,11 +142,15 @@ def _decode_map( |
136 | 142 | budget[0] -= size * 2 |
137 | 143 | if budget[0] < 0: |
138 | 144 | raise InvalidDatabaseError(_TOO_MANY_VALUES) |
| 145 | + budget[1] += 1 |
| 146 | + if budget[1] > _MAX_DEPTH: |
| 147 | + raise InvalidDatabaseError(_TOO_DEEP) |
139 | 148 | container: dict[str, Record] = {} |
140 | 149 | for _ in range(size): |
141 | 150 | (key, offset) = self._decode(offset, budget) |
142 | 151 | (value, offset) = self._decode(offset, budget) |
143 | 152 | container[cast("str", key)] = value |
| 153 | + budget[1] -= 1 |
144 | 154 | return container, offset |
145 | 155 |
|
146 | 156 | def _decode_pointer( |
@@ -169,7 +179,11 @@ def _decode_pointer( |
169 | 179 | if self._pointer_test: |
170 | 180 | return pointer, new_offset |
171 | 181 |
|
| 182 | + budget[1] += 1 |
| 183 | + if budget[1] > _MAX_DEPTH: |
| 184 | + raise InvalidDatabaseError(_TOO_DEEP) |
172 | 185 | (value, _) = self._decode(pointer, budget) |
| 186 | + budget[1] -= 1 |
173 | 187 | return value, new_offset |
174 | 188 |
|
175 | 189 | def _decode_uint( |
@@ -215,16 +229,16 @@ def decode(self, offset: int) -> tuple[Record, int]: |
215 | 229 |
|
216 | 230 | """ |
217 | 231 | # Bound the work per lookup so a crafted database cannot exhaust CPU or |
218 | | - # memory. ``budget`` is a single-element list so the running count is |
219 | | - # shared across the recursion. It is call-local, which keeps the |
220 | | - # decoder safe for concurrent reads. There is no separate depth limit: a |
221 | | - # pointer cycle or over-deep data exhausts Python's own recursion limit, |
222 | | - # which is converted into an InvalidDatabaseError. |
| 232 | + # memory. ``budget`` carries the remaining value count and current |
| 233 | + # structural depth so both are shared across the recursion. It is |
| 234 | + # call-local, which keeps the decoder safe for concurrent reads. The |
| 235 | + # explicit depth limit is independent of Python's process-wide recursion |
| 236 | + # limit; RecursionError remains a fallback on interpreters whose stack |
| 237 | + # limit is reached first. |
223 | 238 | try: |
224 | | - return self._decode(offset, [_MAX_VALUES]) |
| 239 | + return self._decode(offset, [_MAX_VALUES, 0]) |
225 | 240 | except RecursionError as ex: |
226 | | - msg = "The MaxMind DB file's data section exceeds the maximum depth" |
227 | | - raise InvalidDatabaseError(msg) from ex |
| 241 | + raise InvalidDatabaseError(_TOO_DEEP) from ex |
228 | 242 |
|
229 | 243 | def _decode(self, offset: int, budget: list[int]) -> tuple[Record, int]: |
230 | 244 | new_offset = offset + 1 |
|
0 commit comments