Skip to content

candor-java v0.37.0

Choose a tag to compare

@tombaldwin tombaldwin released this 13 Sep 18:24
· 127 commits to main since this release

[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() is file:///tmp/x/ when x is an existing DIRECTORY and
    file:///tmp/x when it is not, so the value is a filesystem observation. A probe function reading that
    trailing slash was ABSENT from functions entirely — a ⟨0.21⟩ purity claim — with deny Fs at exit 0.
    The cause is structural and outlives the verb: java.io.File is a DENYLIST, so auditing it re-reads
    every exemption (which is how File.toURI was found); java.nio.file.Path is 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: pure Path algebra still certifies.

  • Spec floor 0.36 → 0.37 (the STAT-LOCATOR rung), and assert-audit + workflow-check now run in CI.

  • ⚠ File.toURI() STATS, and it was carved out as PURE — a silent under-report over a real syscall.
    isPureHandleAccessor's java.io.File case listed toURI among the pathname accessors that "touch NO
    filesystem". It appends a trailing slash when the pathname is a directory, so it calls isDirectory().
    MEASURED on JDK 21, not read off the name: new File("/tmp/d").toURI() is file:/tmp/d/ when the
    directory exists and file:/tmp/d when 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: toURL is ABSENT from it (so it correctly returned Fs
    whole-owner) and toURL is implemented as toURI().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 a want … 'Fs'.

    MEASURED, bin/corpus-ab.py over 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 in javap), ant's Locator.fileToURL and
    Launcher.getJarArray, woodstox's URLUtil.toURL, jdom's SAXBuilderEngine.fileToURL, junit's
    FileSource.getUri/DirectorySource.getUri, logback's FileUtil.fileToURL, plexus's FileUtils.toURLs.
    Every calls movement 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 paths surface 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 under allow Fs /tmp/benign beside 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#literalArgsInWindow was 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: a Ljava/nio/file/Path;
    or Ljava/io/File; operand, at the receiver or at any argument, IS the file; a handle receiver
    (FileOutputStream, FileChannel, InputStream) is not, so out.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 in Files.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 new pathLit on the analyzer's ProvValue,
    carried through locals, inline construction, new File("…") and toPath()/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 them incomplete GAINING Fs and none losing it;
    inferred unchanged (0).
    Of those 8,174, only 36 were certifiable before (a visible paths
    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 and toRealPath()-ing both caller-supplied trees; org.xnio.Xnio.openFile(File, FileAccess) by
    /dev/null and NUL while 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,
    including org.sqlite.util.OSInfo.isMusl, whose /proc/self/map_files and /etc/os-release literals
    were certifying a Path.toRealPath() on every symlink under them. Reach on the changed branch: 174
    marked call sites, 2 credited-determined, 32 no-locator.