[Security] ConnMan DNS proxy get_name() out-of-bounds read

R4mbb <[email protected]> Mon, 1 Jun 2026 10:57:35 +0900
Newsgroups dev.linux.lists.connman
Message-ID <CA+8xMBxLOfL7CutDYEeFDjZPJorTjcxFn7O4zsSVcGT90=4eig@mail.gmail.com>
Hello ConnMan maintainers,

I would like to report an out-of-bounds read in ConnMan's built-in DNS proxy,
reachable when parsing an attacker-controlled upstream DNS response. I have not
publicly disclosed this issue before this report.


Summary
=======

get_name() in src/dnsproxy.c performs an ineffective bounds check while parsing
DNS names. The per-label bounds check compares the packet start pointer (`pkt`)
instead of the current read cursor (`p`) against the buffer end (`max`). As a
result, the check does not protect the following memcpy() from reading past the
end of the received DNS message buffer.

For a syntactically valid DNS label, this can read up to about 63 bytes past the
packet. Because the code uses `*p` directly and only treats `0xc0..0xff` as DNS
compression pointers, reserved label bytes in the `0x40..0xbf` range can be
interpreted as plain lengths as well. In that malformed-label case,
the attempted
copy length can be larger, up to roughly 191 bytes.

The issue is reachable through ConnMan's DNS response parsing path:

    parse_response() -> parse_rr() -> get_name()

when ConnMan's built-in DNS proxy processes a malicious or MITM-controlled
upstream DNS response.


Affected version / commit
=========================

Confirmed on ConnMan git HEAD:

    9e46e4a021cd8566b942d1a72b41b14a0a58f506

from the canonical git.kernel.org repository.

The `pkt + label_len > max` source-side check appears to have been present
for a long time, and this issue is still present at HEAD. It is closely related
to previously fixed ConnMan DNS-proxy bugs, but it does not appear to be covered
by them.

In particular, CVE-2017-12865 affected this same get_name() helper and hardened
its output-side bounds checking for the `name` buffer, but the
source-side cursor
check before this memcpy() remained unchanged. This report is the read-side
instance in get_name(): the parser validates the packet start pointer (`pkt`),
not the current source cursor (`p`), before copying from `p + 1`.

This issue also appears distinct from CVE-2022-23097, which involved a similar
`ptr + 1 + len` over-read pattern in a different DNS-proxy path, and from
CVE-2021-26675 / CVE-2021-33833 (uncompress()), CVE-2022-23096 / CVE-2022-23098
(TCP framing), and CVE-2025-32366 / CVE-2025-32743.


Prerequisites
=============

- ConnMan is running with its built-in DNS proxy enabled.
- The attacker can control a DNS response, for example by operating a malicious
  upstream resolver or by acting as an on-path/MITM attacker on the ConnMan ->
  upstream DNS path.
- No authentication is required.


Root cause
==========

src/dnsproxy.c, get_name(), label branch:

    unsigned label_len = *p;

    if (pkt + label_len > max)            /* BUG: pkt = packet start, not p */
            return -ENOBUFS;
    ...
    name[(*name_len)++] = label_len;
    memcpy(name + *name_len, p + 1, label_len + 1);   /* OOB read source */
    *name_len += label_len;
    ...
    p += label_len + 1;
    ...
    if (p >= max)                         /* checked only AFTER the memcpy */
            return -ENOBUFS;

`pkt` is the constant packet start. Therefore `pkt + label_len > max` does not
validate whether the current cursor `p` has enough remaining bytes for
`label_len + 1` bytes to be copied from `p + 1`.

For a normal DNS label, `label_len` is at most 63, so the existing check is
ineffective for ordinary packet sizes. In addition, the code uses `*p` directly
without masking and only handles the `0xc0..0xff` compression-pointer range
specially. Reserved label bytes in the `0x40..0xbf` range are not
rejected before
the copy, so malformed label bytes can produce a larger attempted copy length.

The cursor `p` is only checked after the memcpy(). If `p` is near `max`, the
memcpy() reads past the DNS message buffer before the later `if (p >=
max)` check
can reject the name.

One reachable construction is to use a compression pointer in an answer RR name
that points near the final bytes of the packet. The compression offset check
allows offsets below the packet length. A label-length byte near the end of the
message then causes get_name() to copy from beyond `max`.

Relevant reachable sites:

- get_name() used for the RR owner name in parse_rr()
- get_name() used for names inside RDATA, such as CNAME/NS/PTR targets

Buffer type differs by transport path:

- UDP path: stack receive buffer
- TCP path: heap receive buffer, which makes the allocation-boundary OOB read
  straightforward to detect with AddressSanitizer


Reproduction
============

I reproduced this in two ways with AddressSanitizer:

1. A standalone harness using the exact get_name() function copied from HEAD,
   driven with a crafted packet buffer ending in a maximum-length label byte.

2. An end-to-end parser harness using the verbatim parse_response(), parse_rr(),
   and get_name() bodies from HEAD, driving:

       parse_response() -> parse_rr() -> get_name()

   with a crafted DNS response whose answer name uses a compression pointer to
   the final bytes of the packet. This is still a standalone harness rather than
   a live connmand integration test, but it exercises the relevant parser chain.

Observed ASan result:

    AddressSanitizer: heap-buffer-overflow
    READ of size 64
        #1 get_name  src/dnsproxy.c (the memcpy)
        #2 parse_rr
        #3 parse_response

I can provide the standalone PoC and the end-to-end reproduction harness on
request.


Impact
======

This is an out-of-bounds read (CWE-125) past the DNS message buffer,
triggered by
an attacker-controlled DNS response.

The realistic impact appears limited to potential denial of service:

- ASan builds reliably detect the OOB read.
- In non-instrumented builds, a crash depends on whether the over-read
crosses an
  inaccessible memory region.
- If the over-read does not fault, the malformed RR is rejected
immediately after
  the copy when the cursor check runs.

I do not believe this leads to information disclosure or RCE on its own. After
the over-reading memcpy(), `p += label_len + 1` makes the cursor reach or exceed
`max`; the next `if (p >= max) return -ENOBUFS;` aborts get_name(), and the
enclosing RR is discarded. The out-of-bounds bytes are therefore not expected to
be cached, reflected, or otherwise used.

Suggested CVSS v3.1:

    AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L = 3.7 Low

If scored specifically for a malicious configured/upstream resolver
with reliable
response delivery, AC:L may be arguable, giving:

    AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L = 5.3 Medium


Suggested fix
=============

Validate the current cursor before copying the label data:

    -   if (pkt + label_len > max)
    +   if (label_len + 1 > max - p)
            return -ENOBUFS;

It would also be safer to reject reserved DNS label encodings in this branch
instead of treating every non-compression byte as a plain label length.

In addition, consider adding a `ptr < buf + buflen` guard at the top of the
answer loop before calling parse_rr().


Disclosure
==========

I have not published the PoC or report elsewhere. Please let me know whether you
would like the standalone PoC, the end-to-end ASan harness, or a proposed patch.

Thank you.