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
- Normalize and resolve the target path (toAbsolutePath().normalize(), resolve against instance root) before passing it to the API
- Ensure the target is a relative path composed from getInstanceRoot(id), not a user- or manifest-supplied absolute path
- Check for symlinks (Files.isSymbolicLink on parents) and use toRealPath() when containment matters
- 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
- Always build targets with instanceRoot.resolve(relative).normalize()
- Never pass raw manifest/user-supplied paths as absolute targets
- Resolve symlinks with toRealPath() before containment checks
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
- Malformed modpack configuration
- Theme-pack asset entry contains an unsafe segment:
- Theme-pack asset escapes the installed directory:
- Theme-pack entry contains an unsafe segment
- Unsecure path:
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)