Repository navigation
candor-java v0.37.0
[0.37.0] — 2026-09-13
-
R423 —
java.nio.file.Path.toUri()is a filesystem STAT that was classified PURE. Measured on
JDK 21:Path.of("/tmp/x").toUri()isfile:///tmp/x/when x is an existing DIRECTORY and
file:///tmp/xwhen it is not, so the value is a filesystem observation. A probe function reading that
trailing slash was ABSENT fromfunctionsentirely — a ⟨0.21⟩ purity claim — withdeny Fsat exit 0.
The cause is structural and outlives the verb:java.io.Fileis a DENYLIST, so auditing it re-reads
every exemption (which is howFile.toURIwas found);java.nio.file.Pathis an ALLOWLIST, and nothing
re-reads what an allowlist omits. Priced at ADDED 11 / REMOVED 0 over 325 jars, ground-truthed in
javap. Fixed with its over-charge control: purePathalgebra still certifies. -
Spec floor 0.36 → 0.37 (the STAT-LOCATOR rung), and
assert-audit+workflow-checknow run in CI. -
⚠
File.toURI()STATS, and it was carved out as PURE — a silent under-report over a real syscall.
isPureHandleAccessor'sjava.io.Filecase listedtoURIamong the pathname accessors that "touch NO
filesystem". It appends a trailing slash when the pathname is a directory, so it callsisDirectory().
MEASURED on JDK 21, not read off the name:new File("/tmp/d").toURI()isfile:/tmp/d/when the
directory exists andfile:/tmp/dwhen it does not. A verb whose OUTPUT depends on the filesystem has
read the filesystem.The tell had been in the same list all along:
toURLis ABSENT from it (so it correctly returned Fs
whole-owner) andtoURLis implemented astoURI().toURL(). One list gave two answers for one syscall,
decided by which spelling the caller used. A smoke assertion —absent "File.toURI is pure"— was
standing guard over the wrong answer, which is why it survived; it is now awant … 'Fs'.MEASURED,
bin/corpus-ab.pyover the same 122 jars, PRE = the R409 commit: ADDED 14, REMOVED 0,
CHANGED 110 (inferred+14 / ~45). The 14 ADDED are the sin's signature — functions candor OMITTED
entirely, which under ⟨0.21⟩ is a positive purity claim:org.apache.commons.io.FileUtils.toURLs
(files[i].toURI().toURL()in a loop, ground-truthed injavap), ant'sLocator.fileToURLand
Launcher.getJarArray, woodstox'sURLUtil.toURL, jdom'sSAXBuilderEngine.fileToURL, junit's
FileSource.getUri/DirectorySource.getUri, logback'sFileUtil.fileToURL, plexus'sFileUtils.toURLs.
Everycallsmovement in the 110 is an ADDED edge to one of those newly-reported nodes; none was removed. -
⚠ An Fs call NAMES ITS OWN FILE, and a file nobody captured leaves the
pathssurface incomplete —
SOUNDNESS R409 (and the java half of R414). The only Fs masking guard fired at the path-ESTABLISHING
call (Path.of/Paths.get/a path ctor with a single String literal), so a locator that arrived ALREADY
BUILT was never examined. Both spellings certified underallow Fs /tmp/benignbeside one benign sibling
literal, exit 0, nothing disclosed:void f(java.nio.file.Path p) { Files.write(Paths.get("/tmp/benign"), b); Files.exists(p); } void f(java.io.File h) { Files.write(Paths.get("/tmp/benign"), b); h.exists(); }This is the AS-EFF-008 gate evasion
Literals#literalArgsInWindowwas written to kill for Net and Db and
was never migrated to Fs — the Net GENERAL RULE ("a Net call that contributes no visible host leaves the
surface incomplete") now stated for Fs. The locator is read from the DESCRIPTOR: aLjava/nio/file/Path;
orLjava/io/File;operand, at the receiver or at any argument, IS the file; a handle receiver
(FileOutputStream,FileChannel,InputStream) is not, soout.write(b)names nothing and makes no
claim in either direction.A DETERMINED path is still credited, and that control is half the change.
Path p = Paths.get("/lit"); Files.write(p, b)has no literal inFiles.write's own argument window, so a naive port of the per-call
window would have marked a fully visible path incomplete (the live behaviour of two other engines, R416).
Determination is answered by the value's own provenance — a newpathLiton the analyzer'sProvValue,
carried through locals, inline construction,new File("…")andtoPath()/toFile(), and collapsing to
indeterminate at a disagreeing join.MEASURED,
bin/corpus-ab.py, PRE = a worktree at 67fd1b1: 122 third-party jars, 221,864 rows —
ADDED 0, REMOVED 0, CHANGED 8,174, every one of themincompleteGAININGFsand none losing it;
inferredunchanged (0). Of those 8,174, only 36 were certifiable before (a visiblepaths
surface and no incompleteness) and are not now — all 36 audited, all genuine masking:kotlin.io.path .copyToRecursively(source, target, …)was certified by the""/".."literals beside it while
stat-ing andtoRealPath()-ing both caller-supplied trees;org.xnio.Xnio.openFile(File, FileAccess)by
/dev/nullandNULwhile opening the caller's file; 21 rows of junit-platform-launcher by".". A
second corpus (candor-java's own classes + the 5 standard jars, 6,956 rows) moved 44 rows the same way,
includingorg.sqlite.util.OSInfo.isMusl, whose/proc/self/map_filesand/etc/os-releaseliterals
were certifying aPath.toRealPath()on every symlink under them. Reach on the changed branch: 174
marked call sites, 2 credited-determined, 32 no-locator.