Skip to content

Commit e8ad497

Browse files
fix(asm): fix memory corruption in waf interface [backport #5260 to 1.9] (#5267)
Backport of #5260 to 1.9
1 parent 07222fb commit e8ad497

File tree

5 files changed

+129
-72
lines changed

5 files changed

+129
-72
lines changed

ddtrace/appsec/ddwaf/__init__.py

Lines changed: 47 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,17 @@
1818

1919
try:
2020
from .ddwaf_types import ddwaf_config
21-
from .ddwaf_types import ddwaf_context_destroy
22-
from .ddwaf_types import ddwaf_context_init
23-
from .ddwaf_types import ddwaf_destroy
21+
from .ddwaf_types import ddwaf_context_capsule
2422
from .ddwaf_types import ddwaf_get_version
25-
from .ddwaf_types import ddwaf_init
2623
from .ddwaf_types import ddwaf_object
24+
from .ddwaf_types import ddwaf_object_free
2725
from .ddwaf_types import ddwaf_result
28-
from .ddwaf_types import ddwaf_result_free
2926
from .ddwaf_types import ddwaf_ruleset_info
3027
from .ddwaf_types import ddwaf_run
31-
from .ddwaf_types import ddwaf_update
28+
from .ddwaf_types import py_ddwaf_context_init
29+
from .ddwaf_types import py_ddwaf_init
3230
from .ddwaf_types import py_ddwaf_required_addresses
31+
from .ddwaf_types import py_ddwaf_update
3332

3433
_DDWAF_LOADED = True
3534
except OSError:
@@ -82,16 +81,18 @@ def __init__(self, ruleset_map, obfuscation_parameter_key_regexp, obfuscation_pa
8281
key_regex=obfuscation_parameter_key_regexp, value_regex=obfuscation_parameter_value_regexp
8382
)
8483
self._info = ddwaf_ruleset_info()
85-
self._ruleset_map = ddwaf_object.create_without_limits(ruleset_map)
86-
self._handle = ddwaf_init(self._ruleset_map, ctypes.byref(config), ctypes.byref(self._info))
87-
self._ctx = 0
84+
ruleset_map_object = ddwaf_object.create_without_limits(ruleset_map)
85+
self._handle = py_ddwaf_init(ruleset_map_object, ctypes.byref(config), ctypes.byref(self._info))
8886
if not self._handle or self._info.failed:
87+
# We keep the handle alive in case of errors, as some valid rules can be loaded
88+
# at the same time some invalid ones are rejected
8989
LOGGER.error(
9090
"DDWAF.__init__: invalid rules\n ruleset: %s\nloaded:%s\nerrors:%s\n",
91-
self._ruleset_map.struct,
91+
ruleset_map_object.struct,
9292
self._info.loaded,
9393
self.info.errors,
9494
)
95+
ddwaf_object_free(ctypes.byref(ruleset_map_object))
9596

9697
@property
9798
def required_data(self):
@@ -110,63 +111,52 @@ def update_rules(self, new_rules):
110111
# type: (dict[text_type, DDWafRulesType]) -> bool
111112
"""update the rules of the WAF instance. return True if an error occurs."""
112113
rules = ddwaf_object.create_without_limits(new_rules)
113-
result = ddwaf_update(self._handle, rules, ctypes.byref(self._info))
114-
if result == 0:
115-
LOGGER.error("DDWAF.update_rules: invalid rules")
116-
return True
117-
else:
114+
result = py_ddwaf_update(self._handle, rules, self._info)
115+
ddwaf_object_free(rules)
116+
if result:
118117
LOGGER.debug("DDWAF.update_rules success.\ninfo %s", self.info)
119118
self._handle = result
119+
return True
120+
else:
121+
LOGGER.debug("DDWAF.update_rules: keeping the previous handle.")
120122
return False
121123

122124
def _at_request_start(self):
123-
if self._ctx:
124-
ddwaf_context_destroy(self._ctx)
125-
self._ctx = ddwaf_context_init(self._handle)
126-
if self._ctx == 0:
127-
LOGGER.error("DDWaf failure to create the context")
125+
# type: () -> ddwaf_context_capsule
126+
if self._handle:
127+
ctx = py_ddwaf_context_init(self._handle)
128+
if not ctx:
129+
LOGGER.error("DDWaf._at_request_start: failure to create the context.")
130+
return ctx
128131

129132
def _at_request_end(self):
130-
if self._ctx:
131-
ddwaf_context_destroy(self._ctx)
132-
self._ctx = 0
133+
# () -> None
134+
pass
133135

134136
def run(
135137
self, # type: DDWaf
138+
ctx, # type: ddwaf_context_capsule
136139
data, # type: DDWafRulesType
137140
timeout_ms=DEFAULT_DDWAF_TIMEOUT_MS, # type:int
138141
):
139142
# type: (...) -> DDWaf_result
140143
start = time.time()
141144

142-
if self._ctx == 0:
143-
LOGGER.warning("DDWaf failsafe to create the context")
144-
self._ctx = ddwaf_context_init(self._handle)
145-
146-
if self._ctx == 0:
147-
LOGGER.error("DDWaf failure: no context created")
145+
if not ctx:
146+
LOGGER.error("DDWaf.run: dry run. no context created.")
148147
return DDWaf_result(None, [], 0, (time.time() - start) * 1e6)
149148

150149
result = ddwaf_result()
151150
wrapper = ddwaf_object(data)
152-
error = ddwaf_run(self._ctx, wrapper, ctypes.byref(result), timeout_ms * 1000)
151+
error = ddwaf_run(ctx.ctx, wrapper, ctypes.byref(result), timeout_ms * 1000)
153152
if error < 0:
154153
LOGGER.warning("run DDWAF error: %d\ninput %s\nerror %s", error, wrapper.struct, self.info.errors)
155-
try:
156-
return DDWaf_result(
157-
result.data.decode("UTF-8", errors="ignore") if hasattr(result, "data") and result.data else None,
158-
[result.actions.array[i].decode("UTF-8", errors="ignore") for i in range(result.actions.size)],
159-
result.total_runtime / 1e3,
160-
(time.time() - start) * 1e6,
161-
)
162-
finally:
163-
ddwaf_result_free(ctypes.byref(result))
164-
165-
def __dealloc__(self):
166-
if self._ctx:
167-
ddwaf_context_destroy(self._ctx)
168-
if self._handle:
169-
ddwaf_destroy(self._handle)
154+
return DDWaf_result(
155+
result.data.decode("UTF-8", errors="ignore") if hasattr(result, "data") and result.data else None,
156+
[result.actions.array[i].decode("UTF-8", errors="ignore") for i in range(result.actions.size)],
157+
result.total_runtime / 1e3,
158+
(time.time() - start) * 1e6,
159+
)
170160

171161
def version():
172162
# type: () -> text_type
@@ -185,13 +175,25 @@ def __init__(self, rules, obfuscation_parameter_key_regexp, obfuscation_paramete
185175

186176
def run(
187177
self, # type: DDWaf
178+
ctx, # type: ddwaf_context_capsule
188179
data, # type: Union[None, int, text_type, list[Any], dict[text_type, Any]]
189180
timeout_ms=DEFAULT_DDWAF_TIMEOUT_MS, # type:int
190181
):
191182
# type: (...) -> DDWaf_result
192183
LOGGER.warning("DDWaf features disabled. dry run")
193184
return DDWaf_result(None, [], 0.0, 0.0)
194185

186+
def update_rules(self, _):
187+
# type: (dict[text_type, DDWafRulesType]) -> bool
188+
LOGGER.warning("DDWaf features disabled. dry update")
189+
return False
190+
191+
def _at_request_start(self):
192+
pass
193+
194+
def _at_request_end(self):
195+
pass
196+
195197
def version():
196198
# type: () -> text_type
197199
LOGGER.warning("DDWaf features disabled. null version")

ddtrace/appsec/ddwaf/ddwaf_types.py

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,12 @@ def __repr__(self):
247247
self.actions,
248248
)
249249

250+
def __del__(self):
251+
try:
252+
ddwaf_result_free(self)
253+
except TypeError:
254+
pass
255+
250256

251257
ddwaf_result_p = ctypes.POINTER(ddwaf_result)
252258

@@ -278,7 +284,11 @@ class ddwaf_config_obfuscator(ctypes.Structure):
278284
]
279285

280286

281-
ddwaf_object_free_fn = ctypes.POINTER(ctypes.CFUNCTYPE(None, ddwaf_object_p))
287+
ddwaf_object_free_fn = ctypes.CFUNCTYPE(None, ddwaf_object_p)
288+
ddwaf_object_free = ddwaf_object_free_fn(
289+
("ddwaf_object_free", ddwaf),
290+
((1, "object"),),
291+
)
282292

283293

284294
class ddwaf_config(ctypes.Structure):
@@ -296,7 +306,7 @@ def __init__(
296306
max_string_length=0,
297307
key_regex="",
298308
value_regex="",
299-
free_fn=None,
309+
free_fn=ddwaf_object_free,
300310
):
301311
# type: (ddwaf_config, int, int, int, unicode, unicode, Optional[Any]) -> None
302312
self.limits.max_container_size = max_container_size
@@ -310,10 +320,46 @@ def __init__(
310320
ddwaf_config_p = ctypes.POINTER(ddwaf_config)
311321

312322

313-
# TODO MAYBE LATER
314323
ddwaf_handle = ctypes.c_void_p # may stay as this because it's mainly an abstract type in the interface
315324
ddwaf_context = ctypes.c_void_p # may stay as this because it's mainly an abstract type in the interface
316325

326+
327+
class ddwaf_handle_capsule:
328+
def __init__(self, handle):
329+
# type: (ddwaf_handle) -> None
330+
self.handle = handle
331+
self.free_fn = ddwaf_destroy
332+
333+
def __del__(self):
334+
if self.handle:
335+
try:
336+
self.free_fn(self.handle)
337+
except TypeError:
338+
pass
339+
self.handle = None
340+
341+
def __bool__(self):
342+
return bool(self.handle)
343+
344+
345+
class ddwaf_context_capsule:
346+
def __init__(self, ctx):
347+
# type: (ddwaf_context) -> None
348+
self.ctx = ctx
349+
self.free_fn = ddwaf_context_destroy
350+
351+
def __del__(self):
352+
if self.ctx:
353+
try:
354+
self.free_fn(self.ctx)
355+
except TypeError:
356+
pass
357+
self.ctx = None
358+
359+
def __bool__(self):
360+
return bool(self.ctx)
361+
362+
317363
ddwaf_log_cb = ctypes.POINTER(
318364
ctypes.CFUNCTYPE(
319365
None, ctypes.c_int, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_uint, ctypes.c_char_p, ctypes.c_uint64
@@ -334,6 +380,12 @@ def __init__(
334380
),
335381
)
336382

383+
384+
def py_ddwaf_init(ruleset_map, config, info):
385+
# type: (ddwaf_object, Any, Any) -> ddwaf_handle_capsule
386+
return ddwaf_handle_capsule(ddwaf_init(ruleset_map, config, info))
387+
388+
337389
ddwaf_update = ctypes.CFUNCTYPE(ddwaf_handle, ddwaf_handle, ddwaf_object_p, ddwaf_ruleset_info_p)(
338390
("ddwaf_update", ddwaf),
339391
(
@@ -343,6 +395,12 @@ def __init__(
343395
),
344396
)
345397

398+
399+
def py_ddwaf_update(handle, ruleset_map, info):
400+
# type: (ddwaf_handle_capsule, ddwaf_object, Any) -> ddwaf_handle_capsule
401+
return ddwaf_handle_capsule(ddwaf_update(handle.handle, ruleset_map, ctypes.byref(info)))
402+
403+
346404
ddwaf_destroy = ctypes.CFUNCTYPE(None, ddwaf_handle)(
347405
("ddwaf_destroy", ddwaf),
348406
((1, "handle"),),
@@ -366,9 +424,9 @@ def __init__(
366424

367425

368426
def py_ddwaf_required_addresses(handle):
369-
# type: (ctypes.c_void_p) -> list[unicode]
427+
# type: (ddwaf_handle_capsule) -> list[unicode]
370428
size = ctypes.c_uint32()
371-
obj = ddwaf_required_addresses(handle, ctypes.byref(size))
429+
obj = ddwaf_required_addresses(handle.handle, ctypes.byref(size))
372430
return [obj[i].decode("UTF-8") for i in range(size.value)]
373431

374432

@@ -377,6 +435,12 @@ def py_ddwaf_required_addresses(handle):
377435
((1, "handle"),),
378436
)
379437

438+
439+
def py_ddwaf_context_init(handle):
440+
# type: (ddwaf_handle_capsule) -> ddwaf_context_capsule
441+
return ddwaf_context_capsule(ddwaf_context_init(handle.handle))
442+
443+
380444
ddwaf_run = ctypes.CFUNCTYPE(ctypes.c_int, ddwaf_context, ddwaf_object_p, ddwaf_result_p, ctypes.c_uint64)(
381445
("ddwaf_run", ddwaf), ((1, "context"), (1, "data"), (1, "result"), (1, "timeout"))
382446
)
@@ -470,10 +534,6 @@ def py_ddwaf_required_addresses(handle):
470534
# ddwaf_object_get_index
471535
# ddwaf_object_get_bool https://github.com/DataDog/libddwaf/commit/7dc68dacd972ae2e2a3c03a69116909c98dbd9cb
472536

473-
ddwaf_object_free = ctypes.CFUNCTYPE(None, ddwaf_object_p)(
474-
("ddwaf_object_free", ddwaf),
475-
((1, "object"),),
476-
)
477537

478538
ddwaf_get_version = ctypes.CFUNCTYPE(ctypes.c_char_p)(
479539
("ddwaf_get_version", ddwaf),

ddtrace/appsec/processor.py

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@
4242
from typing import Tuple
4343
from typing import Union
4444

45-
from ddtrace.appsec.ddwaf import DDWaf_result
46-
from ddtrace.appsec.ddwaf.ddwaf_types import DDWafRulesType
45+
from ddtrace.appsec.ddwaf.ddwaf_types import ddwaf_context_capsule
4746
from ddtrace.span import Span
4847

4948

@@ -195,7 +194,7 @@ def on_span_start(self, span):
195194

196195
if span.span_type != SpanTypes.WEB:
197196
return
198-
self._ddwaf._at_request_start()
197+
ctx = self._ddwaf._at_request_start()
199198

200199
peer_ip = _asm_request_context.get_ip()
201200
headers = _asm_request_context.get_headers()
@@ -205,7 +204,7 @@ def on_span_start(self, span):
205204
span.set_tag_str(RUNTIME_FAMILY, "python")
206205

207206
def waf_callable(custom_data=None):
208-
return self._waf_action(span._local_root or span, custom_data)
207+
return self._waf_action(span._local_root or span, ctx, custom_data)
209208

210209
_asm_request_context.set_waf_callback(waf_callable)
211210

@@ -228,8 +227,8 @@ def waf_callable(custom_data=None):
228227
# _asm_request_context.call_callback()
229228
_asm_request_context.call_waf_callback({"REQUEST_HTTP_IP": None})
230229

231-
def _waf_action(self, span, custom_data=None):
232-
# type: (Span, dict[str, Any] | None) -> None
230+
def _waf_action(self, span, ctx, custom_data=None):
231+
# type: (Span, ddwaf_context_capsule, dict[str, Any] | None) -> None
233232
"""
234233
Call the `WAF` with the given parameters. If `custom_data_names` is specified as
235234
a list of `(WAF_NAME, WAF_STR)` tuples specifying what values of the `WAF_DATA_NAMES`
@@ -267,7 +266,7 @@ def _waf_action(self, span, custom_data=None):
267266
log.debug("[action] WAF missing value %s", SPAN_DATA_NAMES[key])
268267

269268
log.debug("[DDAS-001-00] Executing ASM In-App WAF")
270-
waf_results = self._ddwaf.run(data, self._waf_timeout)
269+
waf_results = self._ddwaf.run(ctx, data, self._waf_timeout)
271270
if waf_results and waf_results.data:
272271
log.debug("[DDAS-011-00] ASM In-App WAF returned: %s", waf_results.data)
273272

@@ -342,15 +341,6 @@ def _is_needed(self, address):
342341
# type: (str) -> bool
343342
return address in self._addresses_to_keep
344343

345-
def _run_ddwaf(self, data):
346-
# type: (DDWafRulesType) -> DDWaf_result | None
347-
try:
348-
return self._ddwaf.run(data, self._waf_timeout) # res is a serialized json
349-
except Exception:
350-
log.warning("Error executing ASM In-App WAF: ", exc_info=True)
351-
352-
return None
353-
354344
def on_span_finish(self, span):
355345
# type: (Span) -> None
356346

@@ -359,5 +349,5 @@ def on_span_finish(self, span):
359349
# this call is only necessary for tests or frameworks that are not using blocking
360350
if span.get_tag(APPSEC.JSON) is None:
361351
log.debug("metrics waf call")
362-
self._waf_action(span)
352+
_asm_request_context.call_waf_callback()
363353
self._ddwaf._at_request_end()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
fixes:
3+
- |
4+
ASM: fix memory leaks and memory corruption in the interface between ASM and the WAF library

tests/appsec/test_processor.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,8 @@ def test_ddwaf_run():
473473
"server.request.cookies": {"attack": "1' or '1' = '1'"},
474474
"server.response.headers.no_cookies": {"content-type": "text/html; charset=utf-8", "content-length": "207"},
475475
}
476-
res = _ddwaf.run(data, DEFAULT.WAF_TIMEOUT) # res is a serialized json
476+
ctx = _ddwaf._at_request_start()
477+
res = _ddwaf.run(ctx, data, DEFAULT.WAF_TIMEOUT) # res is a serialized json
477478
assert res.data.startswith('[{"rule":{"id":"crs-942-100"')
478479
assert res.runtime > 0
479480
assert res.total_runtime > 0

0 commit comments

Comments
 (0)