Skip to content

fix(guideline): prevent zip file closed race condition in GuidelineIO#153

Merged
ic0ns merged 2 commits intomainfrom
fix/guideline-jar-race-condition
Apr 27, 2026
Merged

fix(guideline): prevent zip file closed race condition in GuidelineIO#153
ic0ns merged 2 commits intomainfrom
fix/guideline-jar-race-condition

Conversation

@juaninf
Copy link
Copy Markdown
Contributor

@juaninf juaninf commented Mar 28, 2026

Fixes #152

Problem

GuidelineIO.listXmlFiles opened the resource jar via JarURLConnection.getJarFile() and wrapped the result in a try-with-resources. With the JVM's default useCaches=true, getJarFile() returns a JVM-wide cached JarFile from JarFileFactory — shared across all callers. Closing it from one thread broke any other thread mid-iteration on the same instance:

  • JDK ≤17: IllegalStateException: zip file closed
  • JDK 21: FileNotFoundException

See JDK-8246714.

Fix

Don't close the cached JarFile. The JVM owns its lifecycle; user code must not call close() on instances obtained from a cached JarURLConnection.

An earlier iteration of this PR set useCaches(false) to give each caller a private JarFile. That also fixed the race, but at the cost of reopening and reparsing the jar's central directory on every readGuidelines call. Under high-throughput scanning (millions of connections in the crawler), that overhead is significant. The current fix keeps the JVM cache and just stops closing it — fast path preserved, race gone.

Tests

GuidelineIOConcurrencyTest (new):

  1. testUnderlyingJdkCachingStillCausesErrorWithoutFix — demonstrates the JDK-level behaviour with a barrier-coordinated reproduction: two threads, one iterating, the other closing the shared cached JarFile. The iterating thread throws. This documents why listXmlFiles must not close the JarFile.
  2. testConcurrentReadGuidelinesIsCorrectAfterFix — 50 threads × 200 iterations of readGuidelines against a freshly-built test jar; asserts no exceptions and that every call returns the correct guideline count.

Both pass. Existing GuidelineIOTest (7 tests) also passes unchanged.

@juaninf juaninf requested a review from face-al March 28, 2026 13:11
@juaninf juaninf marked this pull request as draft March 28, 2026 13:16
@juaninf juaninf marked this pull request as ready for review April 1, 2026 04:28
@juaninf juaninf requested review from ic0ns and removed request for face-al April 1, 2026 04:29
@ic0ns
Copy link
Copy Markdown
Contributor

ic0ns commented Apr 1, 2026

From my side looks good? @XoMEX ?

The previous fix disabled JarURLConnection caching to give each caller
a private JarFile. That avoided the race but reopened and reparsed the
jar on every readGuidelines call — costly under high-throughput scanning
(millions of connections in the crawler).

The cache itself was never the bug. The bug was wrapping the cached
JarFile in try-with-resources and closing it from user code, which broke
other threads still iterating the JVM-shared instance. JarFileFactory
owns the lifecycle of cached JarFiles; callers must not close them.

Drop setUseCaches(false) and the try-with-resources. Update the
concurrency test docstring to reflect the new reasoning. The 50-thread
× 200-iteration test continues to pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ic0ns ic0ns merged commit 334d5aa into main Apr 27, 2026
13 checks passed
@ic0ns ic0ns deleted the fix/guideline-jar-race-condition branch April 27, 2026 20:06
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.

IllegalStateException: zip file closed in GuidelineIO.listXmlFiles

2 participants