HMCL-dev/HMCL · error · IOException

path escapes instance root

Error message

${description} path escapes instance root: ${target}

What it means

DefaultGameRepositoryDraft.validateInstanceFileTarget rejects any candidate file target that is not strictly inside the instance's root directory. The library throws this to prevent instance file operations (jar replacement, manifest application) from writing or reading outside the per-instance directory, i.e. a path-traversal containment check. It fires whenever the target resolves to the instance root itself or lies outside it.

Solutions

  1. Normalize and resolve the target path (toAbsolutePath().normalize(), resolve against instance root) before passing it to the API
  2. Ensure the target is a relative path composed from getInstanceRoot(id), not a user- or manifest-supplied absolute path
  3. Check for symlinks (Files.isSymbolicLink on parents) and use toRealPath() when containment matters
  4. Inspect the instance manifest for suspicious jar/inheritsFrom path values and correct them

Example fix

// before
Path target = Paths.get(manifest.jarPath());
repo.applyManifest(id, manifest, target);
// after
Path target = repo.getInstanceRoot(id).resolve(manifest.jarPath()).normalize();
repo.applyManifest(id, manifest, target);
Defensive patterns

Strategy: validation

Validate before calling

Path root = repo.getInstanceRoot(id).toAbsolutePath().normalize();
Path target = candidate.toAbsolutePath().normalize();
if (target.equals(root) || !target.startsWith(root)) throw new IllegalArgumentException("target escapes instance root");

Type guard

static boolean isInside(Path root, Path candidate) {
    Path r = root.toAbsolutePath().normalize();
    Path t = candidate.toAbsolutePath().normalize();
    return !t.equals(r) && t.startsWith(r);
}

Try / catch

try { repo.applyManifest(id, manifest, target); }
catch (IOException e) { if (e.getMessage().contains("escapes instance root")) { LOG.warning("Blocked path traversal: " + target); } else throw e; }

Prevention

When it happens

Trigger: Calling getPrimaryJarTarget or applyManifest when the constructed target Path is absolute-unnormalized, contains '..' segments, or symlinks to a location outside getInstanceRoot(id); also when target equals the instance root itself.

Common situations: Crafted or corrupted instance manifests referencing ../ paths; symlinked instance directories; manually edited version JSON with an absolute jar path; tests passing fabricated Paths.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of HMCL-dev/HMCL@24702dc5a0 (2026-09-10). Data as JSON: /api/errors/b8ca99fc4ce30928. Report an issue: GitHub.

Appendix: source

Thrown at HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositoryDraft.java:466

        validateInstanceFileTarget(id, target, "Primary JAR");
        return target;
    }

    /// Verifies that a file target is a strict descendant of its instance root.
    ///
    /// @param id          the owning instance id
    /// @param target      the normalized target path
    /// @param description description used in an exception message
    /// @throws IOException if the target is outside the instance root
    private void validateInstanceFileTarget(
            GameInstanceID id,
            Path target,
            String description) throws IOException {
        Path expectedRoot = baseSnapshot.getLayout().getInstanceRoot(id)
                .toAbsolutePath()
                .normalize();
        if (target.equals(expectedRoot) || !target.startsWith(expectedRoot)) {
            throw new IOException(description + " path escapes instance root: " + target);
        }
    }

    /// Creates a directory for rollback data produced by the current commit attempt.
    ///
    /// @return the new rollback directory
    /// @throws IOException if the directory cannot be created
    private Path createRollbackDirectory() throws IOException {
        Path parent = baseSnapshot.getLayout().getBaseDirectory()
                .toAbsolutePath()
                .normalize()
                .resolve(".hmcl")
                .resolve("repository-drafts");
        Files.createDirectories(parent);
        return Files.createTempDirectory(parent, "commit-");
    }

    /// Applies one instance directory rename.

View on GitHub (pinned to 24702dc5a0)