Skip to content

Windows: image reads fail with "Local media path is not safe to read" due to dev/ino mismatch in fs-safe openVerifiedLocalFile #25699

@alexmao1024

Description

@alexmao1024

Summary

On Windows, inbound images sometimes fail with Local media path is not safe to read / path changed during read. The root cause appears to be a false-positive in openVerifiedLocalFile where handle.stat() and fs.lstat() return different dev values on Windows, causing the path-mismatch check to throw even though the file is stable and not a symlink.

Environment

  • OS: Windows 11 (NTFS)
  • Node: v22.14.0
  • OpenClaw: 2026.2.19-2
  • Channel: Telegram/WhatsApp inbound images

Repro

  1. Send an image to the bot (Telegram/WhatsApp).
  2. OpenClaw logs show:
    • Local media path is not safe to read: C:\Users\...\.openclaw\media\inbound\...jpg
    • or SafeOpenError path-mismatch path changed during read.
  3. The file exists and can be read normally outside OpenClaw.

Root Cause (suspected)

In src/infra/fs-safe.ts:

const [stat, lstat] = await Promise.all([handle.stat(), fs.lstat(filePath)]);
if (stat.ino !== lstat.ino || stat.dev !== lstat.dev) {
  throw new SafeOpenError("path-mismatch", "path changed during read");
}
const realPath = await fs.realpath(filePath);
const realStat = await fs.stat(realPath);
if (stat.ino !== realStat.ino || stat.dev !== realStat.dev) {
  throw new SafeOpenError("path-mismatch", "path mismatch");
}

On Windows, handle.stat().dev differs from fs.lstat().dev even for the same file. This triggers the error even though the file is stable and not a symlink.

Suggested Fix

Gate dev comparison by platform on Windows, e.g.:

const devMismatch = stat.dev !== lstat.dev && process.platform !== "win32";
if (stat.ino !== lstat.ino || devMismatch) { ... }

const realDevMismatch = stat.dev !== realStat.dev && process.platform !== "win32";
if (stat.ino !== realStat.ino || realDevMismatch) { ... }

This keeps the security intent on POSIX while avoiding a false-positive on Windows.

Notes

After applying the above change locally (patched build output), inbound images load successfully and vision works as expected.

If you want, I can send a PR with the exact change.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions