{"record":{"id":"2230c3376af51652","repo":"github/copilot-sdk","slug":"runtime-asset-escapes-cache-directory","errorCode":null,"errorMessage":"Runtime asset escapes cache directory: ","messagePattern":"Runtime asset escapes cache directory: ","errorType":"exception","errorClass":"IOException","httpStatus":null,"severity":"error","filePath":"java/sdk/src/main/java/com/github/copilot/ffi/NativeRuntimeLoader.java","lineNumber":465,"sourceCode":"        try (BufferedReader reader = new BufferedReader(\n                new InputStreamReader(inventoryResource.openStream(), StandardCharsets.UTF_8))) {\n            String line;\n            while ((line = reader.readLine()) != null) {\n                if (line.isBlank()) {\n                    continue;\n                }\n                String[] fields = line.split(\"\\\\t\", 2);\n                if (fields.length != 2) {\n                    throw new IOException(\"Invalid runtime asset inventory entry: \" + line);\n                }\n                boolean executable = (Integer.parseInt(fields[0], 8) & 0111) != 0;\n                Path relative = Path.of(fields[1]).normalize();\n                if (relative.isAbsolute() || relative.startsWith(\"..\")) {\n                    throw new IOException(\"Unsafe runtime asset inventory path: \" + fields[1]);\n                }\n                Path cached = cacheDir.resolve(relative).normalize();\n                if (!cached.startsWith(cacheDir)) {\n                    throw new IOException(\"Runtime asset escapes cache directory: \" + fields[1]);\n                }\n                if (isValidCachedFile(cached) && (!executable || isWindows() || Files.isExecutable(cached))) {\n                    continue;\n                }\n\n                String resourcePath = \"native/\" + classifier + \"/\" + fields[1];\n                URL resource = loader.getResource(resourcePath);\n                if (resource == null) {\n                    throw new FileNotFoundException(\"Runtime asset not found on classpath: \" + resourcePath);\n                }\n                Files.createDirectories(cached.getParent());\n                Path temp = Files.createTempFile(cached.getParent(), \"runtime-asset-tmp-\", \"\");\n                try {\n                    copyResourceToTemp(resource, resourcePath, temp);\n                    if (executable) {\n                        makeExecutable(temp);\n                    }\n                    publisher.publish(temp, cached);","sourceCodeStart":447,"sourceCodeEnd":483,"githubUrl":"https://github.com/github/copilot-sdk/blob/cd8cf15dc3f9e762615790aaed0a771a0f392755/java/sdk/src/main/java/com/github/copilot/ffi/NativeRuntimeLoader.java#L447-L483","documentation":"After resolving the inventory entry against the cache directory, the resolved path is normalized and checked with cached.startsWith(cacheDir). If the final absolute path does not remain inside the cache directory, the library throws this error. This is a defense-in-depth check (belt to the suspenders of error 180) preventing symlink or resolution tricks from letting an asset be written outside the cache.","triggerScenarios":"A cacheDir.resolve(relative).normalize() result escapes cacheDir — e.g. via a '..'-heavy relative path that normalizes oddly, or cacheDir itself containing symbolic links / redundant segments (like '/cache/../cache2') making the prefix comparison fail.","commonSituations":"Cache directory configured via a path containing symlinks (e.g. /var → /private/var on macOS, or /tmp symlink); user-supplied cache dir with '..' segments; corrupted inventory entries.","solutions":["Pass a canonical (symlink-free, java.io.File.getCanonicalFile()-normalized) cache directory to the loader.","Check the inventory resource for path entries escaping the cache and replace the artifact.","Verify cacheDir is an absolute normalized path without '..' or symlinked parents."],"exampleFix":"// before\nPath cacheDir = Path.of(prop);\n// after\nPath cacheDir = Path.of(prop).toRealPath();","handlingStrategy":"validation","validationCode":"// Java: give the loader a canonical, symlink-free cache dir up front\nPath cacheDir = Path.of(System.getProperty(\"copilot.cache.dir\", \n    System.getProperty(\"user.home\") + \"/.cache/copilot\")).toRealPath();","typeGuard":null,"tryCatchPattern":"try { loader.extractRuntimeToCache(...); } catch (IOException e) { if (e.getMessage().startsWith(\"Runtime asset escapes cache directory\")) { // reconfigure cache dir with toRealPath(), clear cache, retry once } else throw e; }","preventionTips":["Avoid symlinked paths for the cache directory (notably /tmp and /var on macOS)","Use absolute, normalized paths without '..' when configuring cache locations","Keep the cache directory owned by the running user"],"tags":["path-traversal","security","java","filesystem"],"backgroundTag":"path-traversal-blocked","analyzedSha":"cd8cf15dc3f9e762615790aaed0a771a0f392755","analyzedAt":"2026-09-09T18:32:31.973Z","contentChangedAt":"2026-09-09T18:32:31.973Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}