Skip to content

DNS Server Returns NOERROR for Unresolvable CNAME Chain #1818

@qifan-sailboat

Description

@qifan-sailboat

Title

DNS Server Returns NOERROR for Unresolvable CNAME Chain

Reporter: Qifan Zhang, Palo Alto Networks

Summary

Technitium DNS Server 14.3.0 returns NOERROR (RCODE 0) with a CNAME record in the answer
section when the resolver encounters a CNAME cycle or self-referential CNAME that cannot
be resolved to a terminal record of the requested type. The answer section contains only the
CNAME record; no A record is present.

The resolver terminates quickly (2–7ms) and does not loop or generate excessive upstream
queries. The issue is the RCODE: clients interpret NOERROR as "this name exists and was
successfully resolved," but they receive no usable record. A SERVFAIL response would correctly
signal that the name could not be resolved, allowing clients to retry or surface an error.

RFC 1034 §3.6.2 requires resolvers to detect CNAME loop conditions. Technitium appears to
detect the condition (it terminates and does not recurse indefinitely), but does not translate
that detection into a SERVFAIL response.

This is distinct from CVE-2022-48256, which was a self-referential CNAME causing a loop or
crash. No resource exhaustion or availability impact occurs here.

Technitium Version Affected

Technitium DNS Server 14.3.0
Tested: 2026-04-03
Configuration: recursion=Allow, dnssecValidation=false

Steps to Reproduce

Setup: Run Technitium 14.3.0 with recursive resolution enabled. Configure an authoritative
NS to answer loop.test. A with loop.test. 300 IN CNAME loop.test.

Test:

dig @<technitium-ip> loop.test. A

Expected:

;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL

Observed:

;; ->>HEADER<<- opcode: QUERY, status: NOERROR
;; ANSWER SECTION:
loop.test. 300 IN CNAME loop.test.

The same behavior occurs for two-node same-zone cycles (A CNAME B, B CNAME A) and
cross-zone cycles.

Control — self-referential CNAME that resolves (positive control):

Configure NS to answer alias.test. A with alias.test. CNAME real.test. and
real.test. A 10.0.0.1. Querying alias.test. A should return NOERROR with the A record —
this works correctly.

What Is the Current Bug Behavior?

For any CNAME chain that terminates without a record of the requested type (because the chain
is cyclic, or the final name has no A record), Technitium returns NOERROR with only the CNAME
record(s) in the answer section.

Cycle type Client RCODE Latency Upstream queries
Self-referential (A CNAME A) NOERROR 2ms 2
Same-zone 2-node (A→B→A) NOERROR 3ms 6
Cross-zone 2-node (A→B→A, separate NS) NOERROR 2ms 6
8-hop cross-zone chain NOERROR 7ms 20

Root cause: ProcessCNAMEAsync() in DnsServerCore/Dns/DnsServer.cs (lines 3581–3784)
follows the CNAME chain in a loop. When the loop exits — whether due to cycle detection, the
MAX_CNAME_HOPS = 16 limit, or an empty answer from upstream — the RCODE is taken from the
last upstream response received (line ~3765):

// DnsServer.cs:3764-3765
else
{
    rcode = newResponse.RCODE;   // ← inherits NOERROR from the last CNAME response

The upstream nameserver correctly returns NOERROR for the CNAME record it served. The resolver
never checks whether the chain reached a terminal answer, so it forwards that NOERROR to the
client regardless of whether the chain was resolved. The null branch (line ~3748) is also
explicit NOERROR:

// DnsServer.cs:3746-3749
if (newResponse is null)
{
    rcode = DnsResponseCode.NoError;   // ← explicit NOERROR when recursion unavailable

Every exit path from the loop that does not produce a terminal A record ends up returning
NOERROR. There is no check after the loop for whether newAnswer contains a terminal record.

The CVE-2022-48256 fix added MAX_CNAME_HOPS and a visited-target set, which correctly
prevent infinite following. The current issue is separate: the loop terminates correctly, but
the wrong RCODE is returned.

What Is the Expected Correct Behavior?

When a CNAME chain cannot be resolved to a record of the requested type, the resolver should
return SERVFAIL. NOERROR with only CNAME records and no terminal A record is ambiguous and
misleads clients and downstream caches that treat RCODE=0 as success.

We tested against several other major resolvers (BIND9, Unbound, PowerDNS Recursor, Knot
Resolver) using the same zone setup; all return SERVFAIL for unresolvable CNAME chains.

Suggested Fix

After the CNAME-following loop in ProcessCNAMEAsync(), check whether the accumulated answer
contains a terminal record. If not, return SERVFAIL:

// After the loop, before building the final response:
bool hasTerminalAnswer = newAnswer.Count > 0 &&
    newAnswer[^1].Type != DnsResourceRecordType.CNAME;

if (!hasTerminalAnswer)
{
    rcode = DnsResponseCode.ServerFailure;
    authority = Array.Empty<DnsResourceRecord>();
    additional = Array.Empty<DnsResourceRecord>();
}
else
{
    rcode = newResponse?.RCODE ?? DnsResponseCode.NoError;
    // ... existing logic unchanged
}

This single check covers all unresolvable exit paths (cycle detected, hop limit exceeded,
empty upstream answer, recursion unavailable) uniformly, without affecting the success path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions