Skip to content

Commit 2f057f1

Browse files
committed
vfs: add process vfs-mount and vfs-unmount events
Add security monitoring events that are emitted when a VFS is mounted or unmounted. Applications can listen to these events to detect unauthorized VFS usage or enforce security policies. Events include: - mountPoint: The path where the VFS is mounted - overlay: Whether overlay mode is enabled - readonly: Whether the VFS is read-only
1 parent 99c94eb commit 2f057f1

File tree

3 files changed

+184
-0
lines changed

3 files changed

+184
-0
lines changed

doc/api/vfs.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,32 @@ This is particularly dangerous because:
914914
* Only specific targeted files are affected
915915
* Other operations appear to work normally
916916

917+
### Monitoring VFS mounts
918+
919+
To help detect unauthorized VFS usage, the `process` object emits events when
920+
a VFS is mounted or unmounted:
921+
922+
```cjs
923+
process.on('vfs-mount', (info) => {
924+
console.log(`VFS mounted at ${info.mountPoint}`);
925+
console.log(` overlay: ${info.overlay}`);
926+
console.log(` readonly: ${info.readonly}`);
927+
});
928+
929+
process.on('vfs-unmount', (info) => {
930+
console.log(`VFS unmounted from ${info.mountPoint}`);
931+
});
932+
```
933+
934+
The event object contains:
935+
936+
* `mountPoint` {string} The path where the VFS is mounted.
937+
* `overlay` {boolean} Whether overlay mode is enabled.
938+
* `readonly` {boolean} Whether the VFS is read-only.
939+
940+
Applications can use these events to log VFS activity, alert on suspicious
941+
mounts (such as mounting over system paths), or enforce security policies.
942+
917943
### Recommendations
918944

919945
* **Audit dependencies**: Be cautious of third-party modules that use VFS, as

lib/internal/vfs/file_system.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,13 +233,30 @@ class VirtualFileSystem {
233233
if (this[kVirtualCwdEnabled]) {
234234
this._hookProcessCwd();
235235
}
236+
237+
// Emit mount event for security monitoring
238+
process.emit('vfs-mount', {
239+
mountPoint: this[kMountPoint],
240+
overlay: this[kOverlay],
241+
readonly: this[kProvider].readonly,
242+
});
243+
236244
return this;
237245
}
238246

239247
/**
240248
* Unmounts the VFS.
241249
*/
242250
unmount() {
251+
// Emit unmount event before clearing state (only if currently mounted)
252+
if (this[kMounted]) {
253+
process.emit('vfs-unmount', {
254+
mountPoint: this[kMountPoint],
255+
overlay: this[kOverlay],
256+
readonly: this[kProvider].readonly,
257+
});
258+
}
259+
243260
this._unhookProcessCwd();
244261
if (this[kModuleHooks]) {
245262
loadModuleHooks();
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const vfs = require('node:vfs');
6+
7+
// Test vfs-mount event
8+
{
9+
const mountEvents = [];
10+
const mountListener = common.mustCall((info) => {
11+
mountEvents.push(info);
12+
});
13+
14+
process.on('vfs-mount', mountListener);
15+
16+
const myVfs = vfs.create();
17+
myVfs.writeFileSync('/test.txt', 'hello');
18+
myVfs.mount('/virtual');
19+
20+
assert.strictEqual(mountEvents.length, 1);
21+
assert.strictEqual(mountEvents[0].mountPoint, '/virtual');
22+
assert.strictEqual(mountEvents[0].overlay, false);
23+
assert.strictEqual(mountEvents[0].readonly, false);
24+
25+
myVfs.unmount();
26+
process.removeListener('vfs-mount', mountListener);
27+
}
28+
29+
// Test vfs-unmount event
30+
{
31+
const unmountEvents = [];
32+
const unmountListener = common.mustCall((info) => {
33+
unmountEvents.push(info);
34+
});
35+
36+
process.on('vfs-unmount', unmountListener);
37+
38+
const myVfs = vfs.create();
39+
myVfs.writeFileSync('/test.txt', 'hello');
40+
myVfs.mount('/virtual');
41+
myVfs.unmount();
42+
43+
assert.strictEqual(unmountEvents.length, 1);
44+
assert.strictEqual(unmountEvents[0].mountPoint, '/virtual');
45+
assert.strictEqual(unmountEvents[0].overlay, false);
46+
assert.strictEqual(unmountEvents[0].readonly, false);
47+
48+
process.removeListener('vfs-unmount', unmountListener);
49+
}
50+
51+
// Test events with overlay mode
52+
{
53+
const mountEvents = [];
54+
const unmountEvents = [];
55+
56+
const mountListener = common.mustCall((info) => {
57+
mountEvents.push(info);
58+
});
59+
const unmountListener = common.mustCall((info) => {
60+
unmountEvents.push(info);
61+
});
62+
63+
process.on('vfs-mount', mountListener);
64+
process.on('vfs-unmount', unmountListener);
65+
66+
const myVfs = vfs.create({ overlay: true });
67+
myVfs.writeFileSync('/test.txt', 'hello');
68+
myVfs.mount('/');
69+
70+
assert.strictEqual(mountEvents.length, 1);
71+
assert.strictEqual(mountEvents[0].mountPoint, '/');
72+
assert.strictEqual(mountEvents[0].overlay, true);
73+
74+
myVfs.unmount();
75+
76+
assert.strictEqual(unmountEvents.length, 1);
77+
assert.strictEqual(unmountEvents[0].overlay, true);
78+
79+
process.removeListener('vfs-mount', mountListener);
80+
process.removeListener('vfs-unmount', unmountListener);
81+
}
82+
83+
// Test events with readonly mode
84+
{
85+
const mountEvents = [];
86+
87+
const mountListener = common.mustCall((info) => {
88+
mountEvents.push(info);
89+
});
90+
91+
process.on('vfs-mount', mountListener);
92+
93+
const myVfs = vfs.create();
94+
myVfs.writeFileSync('/test.txt', 'hello');
95+
myVfs.provider.setReadOnly();
96+
myVfs.mount('/virtual');
97+
98+
assert.strictEqual(mountEvents.length, 1);
99+
assert.strictEqual(mountEvents[0].readonly, true);
100+
101+
myVfs.unmount();
102+
process.removeListener('vfs-mount', mountListener);
103+
}
104+
105+
// Test that unmount on non-mounted VFS does not emit event
106+
{
107+
const unmountEvents = [];
108+
const unmountListener = (info) => {
109+
unmountEvents.push(info);
110+
};
111+
112+
process.on('vfs-unmount', unmountListener);
113+
114+
const myVfs = vfs.create();
115+
myVfs.unmount(); // Unmount without mounting first
116+
117+
assert.strictEqual(unmountEvents.length, 0);
118+
119+
process.removeListener('vfs-unmount', unmountListener);
120+
}
121+
122+
// Test using declaration triggers unmount event
123+
{
124+
const unmountEvents = [];
125+
const unmountListener = common.mustCall((info) => {
126+
unmountEvents.push(info);
127+
});
128+
129+
process.on('vfs-unmount', unmountListener);
130+
131+
{
132+
using myVfs = vfs.create();
133+
myVfs.writeFileSync('/test.txt', 'hello');
134+
myVfs.mount('/virtual');
135+
} // VFS is automatically unmounted here via Symbol.dispose
136+
137+
assert.strictEqual(unmountEvents.length, 1);
138+
assert.strictEqual(unmountEvents[0].mountPoint, '/virtual');
139+
140+
process.removeListener('vfs-unmount', unmountListener);
141+
}

0 commit comments

Comments
 (0)