Skip to content

Prevent lookups from seeing a hosts file reload in progress - #430

Open
oshoukry wants to merge 2 commits into
dnsjava:masterfrom
oshoukry:429-hosts-file-reload-race
Open

oshoukry wants to merge 2 commits into
dnsjava:masterfrom
oshoukry:429-hosts-file-reload-race

Conversation

@oshoukry

@oshoukry oshoukry commented Nov 2, 2026 •

Copy link
Copy Markdown

Closes #429.

Problem

HostsFileParser reloads a changed hosts file by clearing its maps in place and refilling them, while getAddressForHost / getNameForReverseLookup read those maps without the lock. A lookup between the clear and the refill returns Optional.empty() for a name that is in both versions of the file. The first read has the same window, because the empty maps are published before parsing. Separately, isEntireFileParsed is never reset, so a file that grows past dnsjava.hostsfile.max_size_bytes stops answering uncached names. Line-by-line root cause in #429.

How it was found

DNSControl routes JVM-wide DNS through Lookup and uses HostsFileParser for jdk.net.hosts.file. Its concurrency test runs 32 threads of Lookup while the hosts file is atomically replaced 3000 times. Every answer must be the old or the new address. Against 3.7.0's parser, 8 of 10 runs had lookups with no answer. DNSControl currently works around it by swapping in fully read, non-reloading parsers (0 of 20 runs failed), and would like to drop that.

Fix

The maps, the file size and the "entire file parsed" flag move into one HostsCache object per read of the file:

  • readHostsFile() builds the next HostsCache off to the side and publishes it with a single volatile write. A published cache is never cleared; createOrClearCache() is gone.
  • validateCache() returns the current cache, and each lookup works only on that snapshot, so it sees either the old or the new file.
  • For files over the full-cache limit, entry-by-entry searches still add to the snapshot's ConcurrentHashMaps, as before.
  • A missing file publishes an empty cache, as before, and no longer leaves a stale isEntireFileParsed/size behind.

No public API change and no new locking. The fast path added in #369 stays.

Tests

Three new tests in HostsFileParserTest. The first two pause a read inside the file read with a spied FileSystemProvider (as handleNoModificationTime already does) and coordinate through latches only, with no sleeps or polling:

  • testLookupDuringReloadFindsUnchangedEntry looks up while a reload is paused and must get the entry from the previous read.
  • testLookupDuringFirstReadWaitsForIt holds the first read, checks that no cache has been published and that a concurrent lookup has not answered, then releases the read and expects both lookups to find the entry. A package-private hasCache(), next to the existing cacheSize(), exposes the published state.
Test master parser This PR
testLookupDuringReloadFindsUnchangedEntry fails passes
testLookupDuringFirstReadWaitsForIt fails passes
testFileGrownBeyondFullCacheSizeIsSearched fails passes
the other 24 HostsFileParserTest tests pass pass

Run with no retries on Corretto 11.0.15, 21.0.6 and 25 (macOS); HostsFileParserTest also passed 10 consecutive runs. Raw output with master's parser (plus the equivalent hasCache(), i.e. hostsCache != null):

testLookupDuringReloadFindsUnchangedEntry   Expecting Optional to contain: kubernetes.docker.internal./127.0.0.1 but was empty.
testLookupDuringFirstReadWaitsForIt         [Cache published before it is read] Expecting value to be false but was true
testFileGrownBeyondFullCacheSizeIsSearched  Expecting Optional to contain: localhost-10./127.0.0.10 but was empty.

mvn verify locally: checkstyle (0 violations), spotless and forbiddenapis are clean on 11 and 25. The only failures in the full suite are NioUdpClientTest (2) and ResolverTest (1), which fail the same way on unmodified master on this machine. It's a macOS UDP loopback issue ("Can't assign requested address").

HostsFileParser reloaded a changed file by clearing its maps in place and
refilling them, while lookups read the maps without the lock. A lookup in
that window found nothing, even for an entry present in both versions of
the file. The first read had the same window: the maps were published
before the file was parsed.

Each read of the file now builds its own cache, published with a single
volatile write and never cleared, and each lookup works on one snapshot.

This also resets the "entire file parsed" flag per read, so a file that
grows beyond dnsjava.hostsfile.max_size_bytes is searched again instead of
reporting every uncached name as absent.

Closes dnsjava#429
@codecov

codecov Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.11111% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.44%. Comparing base (9e26eba) to head (396fe73).

Files with missing lines Patch % Lines
...main/java/org/xbill/DNS/hosts/HostsFileParser.java 86.11% 0 Missing and 5 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master     #430      +/-   ##
============================================
+ Coverage     71.42%   71.44%   +0.01%     
  Complexity     3401     3401              
============================================
  Files           198      198              
  Lines         14031    14031              
  Branches       2245     2245              
============================================
+ Hits          10022    10024       +2     
+ Misses         3378     3376       -2     
  Partials        631      631              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…lling thread state

The test paused the first read and slept in a loop until the concurrent
lookup showed as BLOCKED. It now holds the read on a latch, checks that no
cache has been published and that the concurrent lookup has not answered,
then releases the read and expects both lookups to find the entry. A
package-private hasCache(), next to cacheSize(), exposes the published
state.
@sonarqubecloud

sonarqubecloud Bot commented Nov 2, 2026

Copy link
Copy Markdown

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HostsFileParser: lookups during a hosts file reload miss existing entries

1 participant