-
-
Notifications
You must be signed in to change notification settings - Fork 69.1k
Bug: LocalMediaAccessError on Windows due to fstat/lstat dev mismatch in openVerifiedLocalFile #21989
Description
Bug description
On Windows (NTFS), all local media file reads fail with LocalMediaAccessError: Local media path is not safe to read due to a device number (dev) mismatch between handle.stat() (fstat) and fs.lstat() (path-based lstat) in openVerifiedLocalFile.
This breaks TTS audio delivery, image sending, and any feature that relies on readLocalFileSafely / loadWebMedia with local file paths.
Root cause
In src/infra/fs-safe.ts, openVerifiedLocalFile performs a TOCTOU/symlink safety check:
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");On Windows/NTFS with Node.js:
handle.stat()(fstat via file descriptor) returns the actual NTFS volume serial number (e.g.,708139836)fs.lstat()/fs.stat()(path-based) returnsdev: 0
This is a documented Node.js behavior on Windows — path-based stat calls return 0 for dev, while handle-based stat returns the real volume serial number.
The same issue affects the second check:
const realStat = await fs.stat(realPath);
if (stat.ino !== realStat.ino || stat.dev !== realStat.dev)
throw new SafeOpenError("path-mismatch", "path mismatch");Reproduction
const fs = require('fs');
const fsp = require('fs/promises');
const os = require('os');
const path = require('path');
async function reproduce() {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tts-test-'));
const testFile = path.join(tmpDir, 'voice-test.mp3');
fs.writeFileSync(testFile, Buffer.from('test'));
const handle = await fsp.open(testFile, fs.constants.O_RDONLY);
const [stat, lstat] = await Promise.all([handle.stat(), fsp.lstat(testFile)]);
console.log('handle.stat().dev:', stat.dev); // e.g. 708139836
console.log('fs.lstat().dev:', lstat.dev); // 0
console.log('dev mismatch:', stat.dev !== lstat.dev); // true on Windows
await handle.close();
fs.unlinkSync(testFile);
fs.rmdirSync(tmpDir);
}
reproduce();Output on Windows 10 (Node 22.16.0):
handle.stat().dev: 708139836
fs.lstat().dev: 0
dev mismatch: true
Error log
[telegram] telegram tool reply failed: LocalMediaAccessError: Local media path is not safe to read: C:\Users\<user>\AppData\Local\Temp\tts-fCMZWl\voice-1771602803431.mp3
This error repeats for every TTS audio delivery attempt.
Suggested fix
Skip the dev comparison on Windows, where it is known to be unreliable. The ino (file index) comparison remains valid on Windows/NTFS and provides sufficient TOCTOU protection:
const IS_WIN32 = process.platform === "win32";
// In openVerifiedLocalFile:
if (stat.ino !== lstat.ino || (!IS_WIN32 && stat.dev !== lstat.dev))
throw new SafeOpenError("path-mismatch", "path changed during read");
// ...
if (stat.ino !== realStat.ino || (!IS_WIN32 && stat.dev !== realStat.dev))
throw new SafeOpenError("path-mismatch", "path mismatch");This is consistent with the existing Windows-aware pattern already used in the same file:
const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in constants;Environment
- OS: Windows 10 (build 26200)
- Node.js: v22.16.0 (via nvm)
- OpenClaw: 2026.2.19-2 (latest npm release)
- Channel: Telegram (but affects all channels using local media)
- TTS provider: Edge TTS (
pt-BR-AntonioNeural)
Affected files (dist bundles)
All fs-safe-*.js bundles in dist/ contain the same vulnerable check:
dist/fs-safe-BY9kuZ3o.jsdist/fs-safe-EcGbY0tj.jsdist/fs-safe-CWVBGzz3.jsdist/fs-safe-Blsk2yxR.jsdist/plugin-sdk/fs-safe-PV2bjJll.js
Impact
- Severity: High — completely breaks all local media delivery on Windows
- Affected features: TTS audio, local image/video sending, any
loadWebMediawith local paths - Workaround: Manual patch of
fs-safe-*.jsfiles innode_modules(overwritten on update)