Skip to content

Commit fd983b7

Browse files
committed
test: add test for mkdir recursive on read-only fs
mkdirSync with { recursive: true } on a read-only filesystem incorrectly throws ENOENT instead of the expected EROFS error. Two test cases are included: - On Linux with passwordless sudo, mounts a real read-only tmpfs to verify the error code against the actual kernel behavior. - Uses internalBinding to mock binding.mkdir and simulate EROFS, ensuring the error propagates correctly without requiring root. Refs: #47098 Refs: #48105 Signed-off-by: Kenny Yeo <kenny@yeoyou.net>
1 parent 43d5058 commit fd983b7

File tree

1 file changed

+60
-0
lines changed

1 file changed

+60
-0
lines changed

test/parallel/test-fs-mkdir.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Flags: --expose-internals
12
// Copyright Joyent, Inc. and other Node contributors.
23
//
34
// Permission is hereby granted, free of charge, to any person obtaining a
@@ -22,6 +23,8 @@
2223
'use strict';
2324
const common = require('../common');
2425
const assert = require('assert');
26+
const { internalBinding } = require('internal/test/binding');
27+
const child_process = require('child_process');
2528
const fs = require('fs');
2629
const path = require('path');
2730
const { isMainThread } = require('worker_threads');
@@ -184,6 +187,63 @@ function nextdir() {
184187
}));
185188
}
186189

190+
// `mkdirp` when folder was readonly (EROFS) - mocked via internalBinding.
191+
// Verifies that EROFS is propagated correctly without requiring root/sudo.
192+
{
193+
const fsBinding = internalBinding('fs');
194+
const originalMkdir = fsBinding.mkdir;
195+
fsBinding.mkdir = function(p) {
196+
const err = new Error(`EROFS: read-only file system, mkdir '${p}'`);
197+
err.code = 'EROFS';
198+
err.syscall = 'mkdir';
199+
err.path = p;
200+
throw err;
201+
};
202+
try {
203+
const pathname = path.join(tmpdir.path, nextdir(), nextdir());
204+
assert.throws(
205+
() => { fs.mkdirSync(pathname, { recursive: true }); },
206+
{ code: 'EROFS', message: /EROFS:.*mkdir/, name: 'Error', syscall: 'mkdir' }
207+
);
208+
} finally {
209+
fsBinding.mkdir = originalMkdir;
210+
}
211+
}
212+
213+
// `mkdirp` when folder was readonly.
214+
if (common.isLinux) {
215+
const roTmpfsPath = path.join(tmpdir.path, 'ro-tmpfs');
216+
fs.mkdirSync(roTmpfsPath);
217+
218+
const { status, stderr } = child_process.spawnSync(
219+
'sudo', ['-n', 'mount', '-t', 'tmpfs', '-o', 'ro', 'tmpfs', roTmpfsPath],
220+
{ stdio: 'pipe', encoding: 'utf8' }
221+
);
222+
223+
if (status !== 0) {
224+
console.warn(
225+
'Cannot test EROFS: passwordless sudo required to mount read-only tmpfs. ' +
226+
`Mount failed with status ${status}: ${stderr}`
227+
);
228+
} else {
229+
try {
230+
const pathname = path.join(roTmpfsPath, nextdir());
231+
assert.throws(
232+
() => { fs.mkdirSync(pathname, { recursive: true }); },
233+
{
234+
code: 'EROFS',
235+
message: /EROFS:.*mkdir/,
236+
name: 'Error',
237+
syscall: 'mkdir',
238+
}
239+
);
240+
} finally {
241+
child_process.spawnSync('sudo', ['-n', 'umount', roTmpfsPath]);
242+
}
243+
}
244+
fs.rmdirSync(roTmpfsPath);
245+
}
246+
187247
// `mkdirp` when path is a file.
188248
{
189249
const pathname = tmpdir.resolve(nextdir(), nextdir());

0 commit comments

Comments
 (0)