{"record":{"id":"5521494841e029c1","repo":"openclaw/openclaw","slug":"non-canonical-file-path","errorCode":null,"errorMessage":"non-canonical file path","messagePattern":"non-canonical file path","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"extensions/browser/src/browser/extension-native-host.ts","lineNumber":83,"sourceCode":"async function validateOwnedFile(filePath: string, executable: boolean): Promise<string> {\n  const resolved = path.resolve(filePath);\n  const info = await fs.lstat(resolved);\n  if (!info.isFile() || info.isSymbolicLink()) {\n    throw new Error(\"unsafe file type\");\n  }\n  if (process.platform !== \"win32\") {\n    const uid = process.getuid?.();\n    if (uid !== undefined && info.uid !== uid) {\n      throw new Error(\"foreign file owner\");\n    }\n    const mode = info.mode & 0o777;\n    if ((mode & 0o077) !== 0 || (executable && (mode & 0o100) === 0)) {\n      throw new Error(\"unsafe file mode\");\n    }\n  }\n  const canonical = await fs.realpath(resolved);\n  if (canonical !== resolved) {\n    throw new Error(\"non-canonical file path\");\n  }\n  return canonical;\n}\n\nasync function validateNativeManifest(params: {\n  manifestPath: string;\n  launcherPath: string;\n  callerOrigin: string;\n  expectedOrigins: string[];\n  stateDir?: string;\n}): Promise<void> {\n  const manifestPath = await validateOwnedFile(params.manifestPath, false);\n  const launcherPath = await validateOwnedFile(params.launcherPath, true);\n  const managedRoot = path.resolve(\n    params.stateDir ?? resolveStateDir(),\n    \"browser\",\n    \"native-messaging\",\n  );","sourceCodeStart":65,"sourceCodeEnd":101,"githubUrl":"https://github.com/openclaw/openclaw/blob/01804a75319da4b69c9ab98ceaa30477e22b8c0b/extensions/browser/src/browser/extension-native-host.ts#L65-L101","documentation":"validateOwnedFile requires path.resolve(filePath) to equal fs.realpath(filePath), meaning no component anywhere along the path is a symbolicic link. If realpath differs (the path resolves through one or more symlinks), the path is non-canonical and throws.","triggerScenarios":"Any directory component of the manifest or launcher path is a symlink, so fs.realpath differs from path.resolve.","commonSituations":"OPENCLAW_STATE_DIR points into a symlinked directory (common on macOS where /var -> /private/var); a symlinked HOME; the native-messaging directory itself is a symlink; deployment on a system with symlinked /home or /tmp.","solutions":["Set OPENCLAW_STATE_DIR to a canonical (realpath-resolved) path with no symlink components.","Resolve symlinks in the state directory before passing it: fs.realpathSync(dir).","On macOS, prefer canonical paths (/private/var, /Users) over their symlinked aliases (/var)."],"exampleFix":"// before: OPENCLAW_STATE_DIR=/var/lib/openclaw (symlink) -> \"non-canonical file path\"\n// after: use the realpath\nprocess.env.OPENCLAW_STATE_DIR = fs.realpathSync(\"/var/lib/openclaw\");","handlingStrategy":"try-catch","validationCode":"import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nasync function isCanonicalPath(p: string): Promise<boolean> {\n  const resolved = path.resolve(p);\n  try {\n    return (await fs.realpath(resolved)) === resolved;\n  } catch {\n    return false;\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  await validateNativeManifest(params);\n} catch (error) {\n  if (error instanceof Error && error.message === \"non-canonical file path\") {\n    // resolve OPENCLAW_STATE_DIR with realpath to remove symlink components\n  }\n}","preventionTips":["Set OPENCLAW_STATE_DIR to a realpath-resolved value with no symlink components.","On macOS, avoid /var aliases; prefer /private/var or /Users.","Resolve symlinks in HOME before relying on it for state paths."],"tags":["browser-extension","native-messaging","security","filesystem"],"backgroundTag":null,"analyzedSha":"01804a75319da4b69c9ab98ceaa30477e22b8c0b","analyzedAt":"2026-08-12T04:37:58.197Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}