NormB left a comment (kamailio/kamailio#4629)

## Analysis & Fix for #4629 — DTMF Event SIGSEGV in rtpengine Module

I was able to reproduce this crash and have a proposed fix. Sharing the full 
analysis below in case it's helpful.

### Root Cause

`rtpengine_raise_dtmf_event()` unconditionally dereferences all 10 `pv_spec_t*` 
pointers via `->setf()` when processing incoming DTMF event JSON. These 
pointers are only non-NULL if the corresponding `modparam` has been configured. 
Six new DTMF pvars were added in 6.1:

| New in 6.1 | modparam |
|------------|----------|
| `dtmf_event_source_label_pvar` | `dtmf_event_source_label` |
| `dtmf_event_tags_pvar` | `dtmf_event_tags` |
| `dtmf_event_type_pvar` | `dtmf_event_type` |
| `dtmf_event_source_ip_pvar` | `dtmf_event_source_ip` |
| `dtmf_event_duration_pvar` | `dtmf_event_duration` |
| `dtmf_event_volume_pvar` | `dtmf_event_volume` |

When rtpengine sends a DTMF event containing any of these new fields (e.g., 
`source_label`) and the user hasn't configured the corresponding modparam, the 
code dereferences a NULL `pv_spec_t*`:

```c
// rtpengine.c:1762 — dtmf_event_source_label_pvar is NULL
if(dtmf_event_source_label_pvar->setf(0,
        &dtmf_event_source_label_pvar->pvp, (int)EQ_T, &pv_val) < 0) {
```

The `setf` function pointer sits at offset `0x10` (16 bytes) in `pv_spec_t` on 
x86_64, so `NULL->setf` produces the `segfault at 10` seen in the report.

This is a regression from 5.8, where only 4 DTMF pvars existed and the new JSON 
fields were simply ignored by the parser.

### Crash Signature Match

Our reproduction matches the reporter's crash:

| | Reporter | Reproduction |
|--|----------|--------------|
| **Fault address** | `segfault at 10` | `segfault at 10` |
| **Error code** | `error 4` (user-mode read) | SIGSEGV |
| **Module** | `rtpengine.so` | `rtpengine.so` |
| **Version** | `kamailio 6.1.1 (x86_64)` | `kamailio 6.1.1 (x86_64)` |
| **Frames in module** | 2 (`#0` + `#1`) | 2 (`rtpengine_raise_dtmf_event` → 
`rtpengine_dtmf_events_loop`) |
| **Outer chain** | `init_child → main_loop → main` | `child_init → init_child 
→ main_loop → main` |

The `segfault at 10` is distinctive — it's not a generic NULL deref at `0x0`, 
but specifically the `setf` field at offset `0x10` in `pv_spec_t`, which 
uniquely identifies this class of bug.

### Immediate Workaround

While waiting for a fix, adding the 6 new modparams to your configuration will 
prevent the crash:

```cfg
modparam("rtpengine", "dtmf_event_source_label", "$var(dtmf_source_label)")
modparam("rtpengine", "dtmf_event_tags", "$var(dtmf_tags)")
modparam("rtpengine", "dtmf_event_type", "$var(dtmf_type)")
modparam("rtpengine", "dtmf_event_source_ip", "$var(dtmf_source_ip)")
modparam("rtpengine", "dtmf_event_duration", "$var(dtmf_duration)")
modparam("rtpengine", "dtmf_event_volume", "$var(dtmf_volume)")
```

This initializes the pvar pointers so they are non-NULL when the DTMF event 
handler runs. No rebuild required.

### Reproduction

**Environment:** Debian 13 (trixie), x86_64, Kamailio built from source.

**Test config** — mimics a 5.8-era configuration with only the original 4 DTMF 
pvars:

```cfg
#!KAMAILIO

debug=3
log_stderror=yes
fork=yes
children=1
auto_aliases=no

listen=udp:127.0.0.1:5060

mpath="/usr/local/kamailio-6.1/lib64/kamailio/modules/"

loadmodule "rtpengine.so"
loadmodule "pv.so"
loadmodule "xlog.so"
loadmodule "sl.so"
loadmodule "tm.so"

modparam("rtpengine", "rtpengine_sock", "udp:127.0.0.1:22222")
modparam("rtpengine", "dtmf_events_sock", "127.0.0.1:9999")
modparam("rtpengine", "dtmf_event_callid", "$var(dtmf_callid)")
modparam("rtpengine", "dtmf_event_source_tag", "$var(dtmf_source_tag)")
modparam("rtpengine", "dtmf_event_timestamp", "$var(dtmf_timestamp)")
modparam("rtpengine", "dtmf_event", "$var(dtmf_event)")
# The 6 new pvars are intentionally NOT configured

event_route[rtpengine:dtmf-event] {
    xlog("L_INFO", "DTMF event received\n");
}

request_route {
    sl_send_reply(200, "OK");
}
```

**Trigger** — send a DTMF event JSON containing any of the new fields:

```bash
echo -n 
'{"callid":"test-call-123","source_tag":"tag1","timestamp":1709942400,"event":1,"source_label":"caller"}'
 \
    | nc -u -w1 127.0.0.1 9999
```

### Test Results

The same DTMF event JSON is used for tests 1–3 and 5–6 to isolate the variable 
under test (the Kamailio version/patch):

```json
{"callid":"test-call-123","source_tag":"tag1","timestamp":1709942400,"event":1,"source_label":"caller"}
```

Tests 4 and 7 use a JSON payload with all 10 fields populated to exercise every 
pvar path:

```json
{"callid":"test-call-123","source_tag":"tag1","timestamp":1709942400,"event":1,
 "source_label":"caller","tags":["audio","video"],"type":1,
 "source_ip":"10.0.0.1","duration":160,"volume":10}
```

**6.1 branch** (reporter's version):

| Test | Version | Config | Input | Result |
|------|---------|--------|-------|--------|
| 1 | **6.1.1 (unpatched)** | 4 original pvars | JSON with `source_label` | 
**SIGSEGV** — core dump, `segfault at 10` |
| 2 | **5.8.8** | 4 original pvars | same JSON | **No crash** — field ignored 
(no handler in 5.8) |
| 3 | **6.1.1 (patched)** | 4 original pvars | same JSON | **No crash** — field 
skipped by NULL guard |
| 4 | **6.1.1 (patched)** | all 10 pvars | JSON with all 10 fields | **No 
crash** — all fields populated correctly |

**master** (6.2.0-dev0, commit `91a95ec`):

| Test | Version | Config | Input | Result |
|------|---------|--------|-------|--------|
| 5 | **master (unpatched)** | 4 original pvars | JSON with `source_label` | 
**SIGSEGV** — same crash as 6.1 |
| 6 | **master (patched)** | 4 original pvars | same JSON | **No crash** — 
field skipped by NULL guard |
| 7 | **master (patched)** | all 10 pvars | JSON with all 10 fields | **No 
crash** — all fields populated correctly |

### GDB Backtrace (unpatched 6.1.1)

```
Core was generated by `kamailio -f kamailio-dtmf-test.cfg -DD -E'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x00007f5941c2905b in rtpengine_raise_dtmf_event (
    buffer=0x7f593dbc6980 
"{\"callid\":\"test-call-123\",\"source_tag\":\"tag1\",
    \"timestamp\":1709942400,\"event\":1,\"source_label\":\"caller\"}", len=103)
    at rtpengine.c:1762
1762            if(dtmf_event_source_label_pvar->setf(0,
#1  0x00007f5941c2672f in rtpengine_dtmf_events_loop () at rtpengine.c:1646
#2  0x00007f5941c3b345 in child_init (rank=0) at rtpengine.c:3058
#3  0x000055624ee9fbbc in init_mod_child (m=..., rank=0) at core/sr_module.c:934
...
#9  0x000055624ec3f978 in main_loop () at main.c:2034
```

### Proposed Fix

The patch wraps each of the 10 `->setf()` dereferences in 
`rtpengine_raise_dtmf_event()` with a NULL check on the corresponding pvar 
pointer. When a pvar is unconfigured, the JSON field is silently skipped. Shown 
here abbreviated for readability — the full patch applies the same pattern to 
all 10 fields:

```diff
--- a/src/modules/rtpengine/rtpengine.c
+++ b/src/modules/rtpengine/rtpengine.c
@@ -1691,14 +1691,16 @@
                if(strcmp(it->string, "callid") == 0) {
-                       pv_value_t pv_val;
-                       pv_val.rs.s = it->valuestring;
-                       ...
-                       if(dtmf_event_callid_pvar->setf(
-                                          0, &dtmf_event_callid_pvar->pvp, 
...) < 0) {
+                       if(dtmf_event_callid_pvar) {
+                               pv_value_t pv_val;
+                               pv_val.rs.s = it->valuestring;
+                               ...
+                               if(dtmf_event_callid_pvar->setf(0,
+                                                  
&dtmf_event_callid_pvar->pvp, ...) < 0) {
+                               }
                        }
                } else if(strcmp(it->string, "source_label") == 0) {
-                       ...
-                       if(dtmf_event_source_label_pvar->setf(...) < 0) {
+                       if(dtmf_event_source_label_pvar) {
+                               ...
+                               if(dtmf_event_source_label_pvar->setf(...) < 0) 
{
+                               }
                        }
         // ... same pattern for all 10 fields ...
```

This is a minimal fix that addresses the immediate crash. The module 
maintainers, being more familiar with the internals of the rtpengine module, 
may prefer a different approach — for example, restructuring the function to 
use a lookup table, or requiring certain pvars as mandatory when 
`dtmf_events_sock` is configured. Happy to adjust the patch based on feedback.

Note that while the crash is triggered by the 6 new pvars added in 6.1, the 
NULL guards are applied to all 10 for defense-in-depth, since the original 4 
have the same vulnerability if a user configures `dtmf_events_sock` without any 
pvars.

-- 
Reply to this email directly or view it on GitHub:
https://github.com/kamailio/kamailio/issues/4629#issuecomment-4028298442
You are receiving this because you are subscribed to this thread.

Message ID: <kamailio/kamailio/issues/4629/[email protected]>
_______________________________________________
Kamailio - Development Mailing List -- [email protected]
To unsubscribe send an email to [email protected]
Important: keep the mailing list in the recipients, do not reply only to the 
sender!

Reply via email to