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.
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
Steps to Reproduce
Setup: Run Technitium 14.3.0 with recursive resolution enabled. Configure an authoritative
NS to answer
loop.test. Awithloop.test. 300 IN CNAME loop.test.Test:
Expected:
Observed:
The same behavior occurs for two-node same-zone cycles (
A CNAME B,B CNAME A) andcross-zone cycles.
Control — self-referential CNAME that resolves (positive control):
Configure NS to answer
alias.test. Awithalias.test. CNAME real.test.andreal.test. A 10.0.0.1. Queryingalias.test. Ashould 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.
A CNAME A)A→B→A)A→B→A, separate NS)Root cause:
ProcessCNAMEAsync()inDnsServerCore/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 = 16limit, or an empty answer from upstream — the RCODE is taken from thelast upstream response received (line ~3765):
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
nullbranch (line ~3748) is alsoexplicit NOERROR:
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
newAnswercontains a terminal record.The CVE-2022-48256 fix added
MAX_CNAME_HOPSand a visited-target set, which correctlyprevent 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 answercontains a terminal record. If not, return SERVFAIL:
This single check covers all unresolvable exit paths (cycle detected, hop limit exceeded,
empty upstream answer, recursion unavailable) uniformly, without affecting the success path.