{"record":{"id":"62970f6a01dd8c1e","repo":"hashicorp/nomad","slug":"archive-contains-symlink-that-escapes-alloc-dir","errorCode":null,"errorMessage":"archive contains symlink that escapes alloc dir","messagePattern":"archive contains symlink that escapes alloc dir","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"client/allocwatcher/alloc_watcher.go","lineNumber":637,"sourceCode":"\t\t\t// Can't change owner if not root or on Windows.\n\t\t\tif euid == 0 {\n\t\t\t\tif err := os.Chown(name, hdr.Uid, hdr.Gid); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"error chowning directory %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\t// If the header is for a symlink we create the symlink\n\t\tif hdr.Typeflag == tar.TypeSymlink {\n\t\t\tif err = os.Symlink(hdr.Linkname, filepath.Join(dest, hdr.Name)); err != nil {\n\t\t\t\treturn fmt.Errorf(\"error creating symlink: %w\", err)\n\t\t\t}\n\n\t\t\tfor _, path := range []string{hdr.Name, hdr.Linkname} {\n\t\t\t\tif escapes, err := escapingfs.PathEscapesAllocDir(dest, \"\", path); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"error evaluating symlink: %w\", err)\n\t\t\t\t} else if escapes {\n\t\t\t\t\treturn fmt.Errorf(\"archive contains symlink that escapes alloc dir\")\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcontinue\n\t\t}\n\t\t// If the header is a file, we write to a file\n\t\tif hdr.Typeflag == tar.TypeReg {\n\t\t\tfPath := filepath.Join(dest, hdr.Name)\n\t\t\tif _, err := os.Lstat(fPath); err == nil {\n\t\t\t\tif err := os.Remove(fPath); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"error removing existing file: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tf, err := os.Create(fPath)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"error creating file: %w\", err)\n\t\t\t}\n","sourceCodeStart":619,"sourceCodeEnd":655,"githubUrl":"https://github.com/hashicorp/nomad/blob/482b49bf1aec006f089bcfc7e632d8f6ac303e5e/client/allocwatcher/alloc_watcher.go#L619-L655","documentation":"Security guard against path traversal during alloc dir migration: if a symlink entry in the tar archive (its name or its target) would resolve outside the allocation directory, Nomad aborts extraction with this error. It protects against malicious or corrupted archives escaping the sandbox.","triggerScenarios":"escapingfs.PathEscapesAllocDir returns escapes=true for hdr.Name or hdr.Linkname of a TypeSymlink entry — e.g. a link target like ../../outside or an absolute path.","commonSituations":"A task created symlinks pointing outside the alloc dir (shared host paths, /tmp links); adversarial or corrupted archives during cross-node migration; workloads using bind-mount-like relative links.","solutions":["Find the offending symlink in the previous alloc dir and repoint it inside the alloc dir","Fix the task to not create symlinks escaping its alloc directory","Re-snapshot the alloc dir after removing the escaping link, then retry migration","If migration is not essential, disable it (canonicalize/no migration) and let the new alloc start fresh"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Before relying on migration, audit task-created symlinks in the alloc dir:\n// walk the dir and fail the check if filepath.EvalSymlinks resolves outside the alloc dir root","typeGuard":"func symlinkEscapes(dest, link, target string) bool {\n    resolved := filepath.Clean(filepath.Join(dest, link))\n    return !strings.HasPrefix(resolved, filepath.Clean(dest)+string(os.PathSeparator))\n}","tryCatchPattern":"if err := watcher.Wait(ctx); err != nil {\n    if strings.Contains(err.Error(), \"escapes alloc dir\") {\n        // Security violation: do NOT retry with same data; start fresh alloc\n        return startWithoutMigration(ctx, alloc)\n    }\n    return err\n}","preventionTips":["Ensure tasks only create symlinks within their alloc directory","Audit shared volume/task configs that produce absolute or ../ link targets","Keep Nomad clients patched — this is a security control"],"tags":["nomad","security","symlink","path-traversal"],"backgroundTag":"symlink-path-escape","analyzedSha":"482b49bf1aec006f089bcfc7e632d8f6ac303e5e","analyzedAt":"2026-09-04T07:54:14.808Z","contentChangedAt":"2026-09-04T07:54:14.808Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}