Skip to content

Commit e1adb4c

Browse files
committed
vfs: keep open memory handles in sync with file
A MemoryFileHandle snapshotted the file's buffer at open and never checked the entry again. After the file changed through the provider or another handle, an open handle served stale reads and fstat sizes, and its writes went into the old copy, undoing other handles' changes. Record the entry content each handle last saw or wrote, and adopt the entry's content whenever another handle has replaced it, so an open descriptor is a live view of the file as on a real filesystem. Signed-off-by: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Assisted-by: claude:opus-5.5
1 parent a2a064c commit e1adb4c

2 files changed

Lines changed: 116 additions & 0 deletions

File tree

‎lib/internal/vfs/file_handle.js‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,7 @@ class MemoryFileHandle extends VirtualFileHandle {
429429
#size;
430430
#entry;
431431
#getStats;
432+
#published; // The entry content this handle last saw or wrote.
432433

433434
#checkClosed(syscall) {
434435
if (this.closed) {
@@ -450,6 +451,7 @@ class MemoryFileHandle extends VirtualFileHandle {
450451
this.#size = content.length;
451452
this.#entry = entry;
452453
this.#getStats = getStats;
454+
this.#published = content;
453455

454456
// O_TRUNC empties the file at open time. O_APPEND does not move the
455457
// read offset: it only forces writes to the end, so the position stays
@@ -459,10 +461,24 @@ class MemoryFileHandle extends VirtualFileHandle {
459461
this.#size = 0;
460462
if (entry) {
461463
entry.content = this.#content;
464+
this.#published = this.#content;
462465
}
463466
}
464467
}
465468

469+
/**
470+
* Adopts the entry's content if another handle replaced it, since an
471+
* open descriptor is a live view of the file, not a snapshot.
472+
*/
473+
#sync() {
474+
const entry = this.#entry;
475+
if (entry && entry.content !== this.#published) {
476+
this.#content = entry.content;
477+
this.#size = entry.content.length;
478+
this.#published = entry.content;
479+
}
480+
}
481+
466482
/**
467483
* Throws EBADF if the handle was not opened for writing.
468484
*/
@@ -499,6 +515,7 @@ class MemoryFileHandle extends VirtualFileHandle {
499515
if (this.#entry?.isDynamic && this.#entry.isDynamic()) {
500516
return this.#entry.getContentSync();
501517
}
518+
this.#sync();
502519
return this.#content.subarray(0, this.#size);
503520
}
504521

@@ -572,6 +589,7 @@ class MemoryFileHandle extends VirtualFileHandle {
572589
writeSync(buffer, offset, length, position) {
573590
this.#checkClosed('write');
574591
this.#checkWritable();
592+
this.#sync();
575593

576594
// In append mode, always write at the end
577595
const useCurrentPosition = isCurrentPosition(position);
@@ -603,6 +621,7 @@ class MemoryFileHandle extends VirtualFileHandle {
603621
this.#entry.content = this.#content.subarray(0, this.#size);
604622
this.#entry.mtime = now;
605623
this.#entry.ctime = now;
624+
this.#published = this.#entry.content;
606625
}
607626

608627
// Update position if not using explicit position
@@ -693,6 +712,7 @@ class MemoryFileHandle extends VirtualFileHandle {
693712
statSync(options) {
694713
this.#checkClosed('fstat');
695714
if (this.#getStats) {
715+
this.#sync();
696716
return this.#getStats(this.#size);
697717
}
698718
throw new ERR_INVALID_STATE('stats not available');
@@ -741,6 +761,7 @@ class MemoryFileHandle extends VirtualFileHandle {
741761
truncateSync(len = 0) {
742762
this.#checkClosed('ftruncate');
743763
this.#checkWritable();
764+
this.#sync();
744765

745766
if (len < this.#size) {
746767
// Zero out truncated region to avoid stale data
@@ -764,6 +785,7 @@ class MemoryFileHandle extends VirtualFileHandle {
764785
this.#entry.content = this.#content.subarray(0, this.#size);
765786
this.#entry.mtime = now;
766787
this.#entry.ctime = now;
788+
this.#published = this.#entry.content;
767789
}
768790
}
769791

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Flags: --experimental-vfs
2+
'use strict';
3+
// MemoryProvider: an open handle is a live view of the file, not a snapshot
4+
// taken at open time. Writes made through the provider or through another
5+
// handle must be visible to handles that are already open, as they are on a
6+
// real filesystem. Refs: https://github.com/nodejs/node/issues/66355
7+
const common = require('../common');
8+
const assert = require('assert');
9+
const { create } = require('node:vfs');
10+
11+
// Content replaced through the provider is seen by an open read handle.
12+
{
13+
const { provider: p } = create();
14+
p.writeFileSync('/f', 'AAAA');
15+
const h = p.openSync('/f', 'r');
16+
p.writeFileSync('/f', '0123456789');
17+
18+
const b = Buffer.alloc(10);
19+
assert.strictEqual(h.readSync(b, 0, 10, 0), 10);
20+
assert.strictEqual(b.toString(), '0123456789');
21+
assert.strictEqual(h.readFileSync('utf8'), '0123456789');
22+
assert.strictEqual(h.statSync().size, 10);
23+
assert.strictEqual(p.statSync('/f').size, 10);
24+
h.readFile('utf8').then(common.mustCall((s) => {
25+
assert.strictEqual(s, '0123456789');
26+
h.closeSync();
27+
}));
28+
}
29+
30+
// A shrinking rewrite is seen too: reads stop at the new end.
31+
{
32+
const { provider: p } = create();
33+
p.writeFileSync('/f', '0123456789');
34+
const h = p.openSync('/f', 'r');
35+
p.writeFileSync('/f', 'AB');
36+
37+
const b = Buffer.alloc(10);
38+
assert.strictEqual(h.readSync(b, 0, 10, 0), 2);
39+
assert.strictEqual(b.subarray(0, 2).toString(), 'AB');
40+
assert.strictEqual(h.readFileSync('utf8'), 'AB');
41+
assert.strictEqual(h.statSync().size, 2);
42+
h.closeSync();
43+
}
44+
45+
// Two writable handles on the same file do not clobber each other.
46+
{
47+
const { provider: p } = create();
48+
p.writeFileSync('/f', 'AAAA');
49+
const h1 = p.openSync('/f', 'r+');
50+
const h2 = p.openSync('/f', 'r+');
51+
52+
h1.writeSync(Buffer.from('XY'), 0, 2, 0);
53+
h2.writeSync(Buffer.from('Z'), 0, 1, 3);
54+
assert.strictEqual(p.readFileSync('/f', 'utf8'), 'XYAZ');
55+
assert.strictEqual(h1.readFileSync('utf8'), 'XYAZ');
56+
57+
// Growing through one handle is visible to the other.
58+
h1.writeSync(Buffer.from('0123456789'), 0, 10, 4);
59+
assert.strictEqual(h2.statSync().size, 14);
60+
assert.strictEqual(h2.readFileSync('utf8'), 'XYAZ0123456789');
61+
62+
// Truncating through one handle is visible to the other.
63+
h2.truncateSync(3);
64+
assert.strictEqual(h1.statSync().size, 3);
65+
assert.strictEqual(h1.readFileSync('utf8'), 'XYA');
66+
67+
h1.closeSync();
68+
h2.closeSync();
69+
}
70+
71+
// A write through a handle opened before the file was replaced lands in the
72+
// current content instead of resurrecting the old bytes.
73+
{
74+
const { provider: p } = create();
75+
p.writeFileSync('/f', 'AAAA');
76+
const h = p.openSync('/f', 'r+');
77+
p.writeFileSync('/f', 'BB');
78+
79+
h.writeSync(Buffer.from('C'), 0, 1, 3);
80+
assert.deepStrictEqual(p.readFileSync('/f'), Buffer.from('BB\0C', 'latin1'));
81+
h.closeSync();
82+
}
83+
84+
// An append handle opened earlier appends after the current end.
85+
{
86+
const { provider: p } = create();
87+
p.writeFileSync('/f', 'AAAA');
88+
const h = p.openSync('/f', 'a');
89+
p.writeFileSync('/f', 'B');
90+
91+
h.writeSync(Buffer.from('C'), 0, 1, null);
92+
assert.strictEqual(p.readFileSync('/f', 'utf8'), 'BC');
93+
h.closeSync();
94+
}

0 commit comments

Comments
 (0)