@componentor/fs 3.0.17 → 3.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/dist/index.d.mts +16 -1
- package/dist/index.js +46 -3
- package/dist/index.js.map +1 -1
- package/dist/workers/repair.worker.js +38 -1
- package/dist/workers/repair.worker.js.map +1 -1
- package/dist/workers/server.worker.js +38 -1
- package/dist/workers/server.worker.js.map +1 -1
- package/dist/workers/sync-relay.worker.js +109 -3
- package/dist/workers/sync-relay.worker.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,6 +83,13 @@ const fs = new VFSFileSystem({
|
|
|
83
83
|
sabSize: 4194304, // SharedArrayBuffer size in bytes (default: 4MB)
|
|
84
84
|
debug: false, // Enable debug logging (default: false)
|
|
85
85
|
swScope: undefined, // Custom service worker scope (default: auto-scoped per root)
|
|
86
|
+
limits: { // Upper bounds for VFS validation (prevents corrupt data from causing OOM)
|
|
87
|
+
maxInodes: 4_000_000, // Max inode count (default: 4M)
|
|
88
|
+
maxBlocks: 4_000_000, // Max data blocks (default: 4M)
|
|
89
|
+
maxPathTable: 256 * 1024 * 1024, // Max path table bytes (default: 256MB)
|
|
90
|
+
maxVFSSize: 100 * 1024 * 1024 * 1024, // Max .vfs.bin size (default: 100GB)
|
|
91
|
+
maxPayload: 2 * 1024 * 1024 * 1024, // Max single SAB payload (default: 2GB)
|
|
92
|
+
},
|
|
86
93
|
});
|
|
87
94
|
```
|
|
88
95
|
|
|
@@ -570,6 +577,18 @@ Make sure `opfsSync` is enabled (it's `true` by default). Files are mirrored to
|
|
|
570
577
|
|
|
571
578
|
## Changelog
|
|
572
579
|
|
|
580
|
+
### v3.0.18 (2026)
|
|
581
|
+
|
|
582
|
+
**Features:**
|
|
583
|
+
- Configurable VFS limits via `limits` option: `maxInodes`, `maxBlocks`, `maxPathTable`, `maxVFSSize`, `maxPayload`
|
|
584
|
+
|
|
585
|
+
**Fixes:**
|
|
586
|
+
- Pre-validate superblock before `engine.init()` to prevent hangs from corrupt values causing huge allocations
|
|
587
|
+
- Add upper bounds in `mount()`: max 4M inodes, 4M blocks, 256MB path table, 100GB total VFS size
|
|
588
|
+
- Ensure all mount errors are prefixed with `Corrupt VFS:` for consistent corruption fallback
|
|
589
|
+
- Cap `readPayload()` at 2GB (configurable) and validate each chunk length in the multi-chunk loop to prevent OOM/infinite loops from corrupt SAB data
|
|
590
|
+
- Cap `MemoryHandle.grow()` at 4GB to prevent OOM from corrupt VFS offsets on main-thread fallback
|
|
591
|
+
|
|
573
592
|
### v3.0.17 (2026)
|
|
574
593
|
|
|
575
594
|
**Features:**
|
|
@@ -757,7 +776,7 @@ git clone https://github.com/componentor/fs
|
|
|
757
776
|
cd fs
|
|
758
777
|
npm install
|
|
759
778
|
npm run build # Build the library
|
|
760
|
-
npm test # Run unit tests (
|
|
779
|
+
npm test # Run unit tests (107 tests)
|
|
761
780
|
npm run benchmark:open # Run benchmarks in browser
|
|
762
781
|
```
|
|
763
782
|
|
package/dist/index.d.mts
CHANGED
|
@@ -140,6 +140,19 @@ type WatchFileListener = (curr: Stats, prev: Stats) => void;
|
|
|
140
140
|
* Automatically selected as fallback when VFS corruption is detected in hybrid mode.
|
|
141
141
|
*/
|
|
142
142
|
type FSMode = 'hybrid' | 'vfs' | 'opfs';
|
|
143
|
+
/** Upper bounds for VFS validation. Prevents corrupt data from causing OOM/hangs. */
|
|
144
|
+
interface VFSLimits {
|
|
145
|
+
/** Maximum number of inodes (default: 4,000,000) */
|
|
146
|
+
maxInodes?: number;
|
|
147
|
+
/** Maximum number of data blocks (default: 4,000,000) */
|
|
148
|
+
maxBlocks?: number;
|
|
149
|
+
/** Maximum path table size in bytes (default: 256MB) */
|
|
150
|
+
maxPathTable?: number;
|
|
151
|
+
/** Maximum total VFS file size in bytes (default: 100GB) */
|
|
152
|
+
maxVFSSize?: number;
|
|
153
|
+
/** Maximum single SAB payload size in bytes (default: 2GB) */
|
|
154
|
+
maxPayload?: number;
|
|
155
|
+
}
|
|
143
156
|
/** VFS configuration options */
|
|
144
157
|
interface VFSConfig {
|
|
145
158
|
root?: string;
|
|
@@ -158,6 +171,8 @@ interface VFSConfig {
|
|
|
158
171
|
* `'./opfs-fs-sw/'` (relative to the SW script URL) so it won't collide
|
|
159
172
|
* with the host application's service worker. */
|
|
160
173
|
swScope?: string;
|
|
174
|
+
/** Upper bounds for VFS validation (prevents corrupt data from causing OOM/hangs). */
|
|
175
|
+
limits?: VFSLimits;
|
|
161
176
|
}
|
|
162
177
|
|
|
163
178
|
type AsyncRequestFn = (op: number, path: string, flags?: number, data?: Uint8Array | string | null, path2?: string, fdArgs?: Record<string, unknown>) => Promise<{
|
|
@@ -506,4 +521,4 @@ declare function getDefaultFS(): VFSFileSystem;
|
|
|
506
521
|
/** Async init helper — avoids blocking main thread */
|
|
507
522
|
declare function init(): Promise<void>;
|
|
508
523
|
|
|
509
|
-
export { type Dir, type Dirent, type Encoding, FSError, type FSMode, type FSWatcher, type FileHandle, type LoadResult, type MkdirOptions, type PathLike, type ReadOptions, type ReadStreamOptions, type ReaddirOptions, type RepairResult, type RmOptions, type RmdirOptions, type Stats, type UnpackResult, type VFSConfig, VFSFileSystem, type WatchEventType, type WatchFileListener, type WatchListener, type WatchOptions, type WriteOptions, type WriteStreamOptions, constants, createError, createFS, getDefaultFS, init, loadFromOPFS, path, repairVFS, statusToError, unpackToOPFS };
|
|
524
|
+
export { type Dir, type Dirent, type Encoding, FSError, type FSMode, type FSWatcher, type FileHandle, type LoadResult, type MkdirOptions, type PathLike, type ReadOptions, type ReadStreamOptions, type ReaddirOptions, type RepairResult, type RmOptions, type RmdirOptions, type Stats, type UnpackResult, type VFSConfig, VFSFileSystem, type VFSLimits, type WatchEventType, type WatchFileListener, type WatchListener, type WatchOptions, type WriteOptions, type WriteStreamOptions, constants, createError, createFS, getDefaultFS, init, loadFromOPFS, path, repairVFS, statusToError, unpackToOPFS };
|
package/dist/index.js
CHANGED
|
@@ -1351,7 +1351,8 @@ var VFSFileSystem = class {
|
|
|
1351
1351
|
strictPermissions: config.strictPermissions ?? false,
|
|
1352
1352
|
sabSize: config.sabSize ?? DEFAULT_SAB_SIZE,
|
|
1353
1353
|
debug: config.debug ?? false,
|
|
1354
|
-
swScope: config.swScope
|
|
1354
|
+
swScope: config.swScope,
|
|
1355
|
+
limits: config.limits
|
|
1355
1356
|
};
|
|
1356
1357
|
this.tabId = crypto.randomUUID();
|
|
1357
1358
|
this.ns = ns;
|
|
@@ -1473,7 +1474,8 @@ var VFSFileSystem = class {
|
|
|
1473
1474
|
gid: this.config.gid,
|
|
1474
1475
|
umask: this.config.umask,
|
|
1475
1476
|
strictPermissions: this.config.strictPermissions,
|
|
1476
|
-
debug: this.config.debug
|
|
1477
|
+
debug: this.config.debug,
|
|
1478
|
+
limits: this.config.limits
|
|
1477
1479
|
}
|
|
1478
1480
|
});
|
|
1479
1481
|
}
|
|
@@ -2193,6 +2195,13 @@ var VFSEngine = class {
|
|
|
2193
2195
|
superblockDirty = false;
|
|
2194
2196
|
// Free inode hint — skip O(n) scan
|
|
2195
2197
|
freeInodeHint = 0;
|
|
2198
|
+
// Configurable upper bounds
|
|
2199
|
+
maxInodes = 4e6;
|
|
2200
|
+
maxBlocks = 4e6;
|
|
2201
|
+
maxPathTable = 256 * 1024 * 1024;
|
|
2202
|
+
// 256MB
|
|
2203
|
+
maxVFSSize = 100 * 1024 * 1024 * 1024;
|
|
2204
|
+
// 100GB
|
|
2196
2205
|
init(handle, opts) {
|
|
2197
2206
|
this.handle = handle;
|
|
2198
2207
|
this.processUid = opts?.uid ?? 0;
|
|
@@ -2200,11 +2209,23 @@ var VFSEngine = class {
|
|
|
2200
2209
|
this.umask = opts?.umask ?? DEFAULT_UMASK;
|
|
2201
2210
|
this.strictPermissions = opts?.strictPermissions ?? false;
|
|
2202
2211
|
this.debug = opts?.debug ?? false;
|
|
2212
|
+
if (opts?.limits) {
|
|
2213
|
+
if (opts.limits.maxInodes != null) this.maxInodes = opts.limits.maxInodes;
|
|
2214
|
+
if (opts.limits.maxBlocks != null) this.maxBlocks = opts.limits.maxBlocks;
|
|
2215
|
+
if (opts.limits.maxPathTable != null) this.maxPathTable = opts.limits.maxPathTable;
|
|
2216
|
+
if (opts.limits.maxVFSSize != null) this.maxVFSSize = opts.limits.maxVFSSize;
|
|
2217
|
+
}
|
|
2203
2218
|
const size = handle.getSize();
|
|
2204
2219
|
if (size === 0) {
|
|
2205
2220
|
this.format();
|
|
2206
2221
|
} else {
|
|
2207
|
-
|
|
2222
|
+
try {
|
|
2223
|
+
this.mount();
|
|
2224
|
+
} catch (err) {
|
|
2225
|
+
const msg = err.message ?? String(err);
|
|
2226
|
+
if (msg.startsWith("Corrupt VFS:")) throw err;
|
|
2227
|
+
throw new Error(`Corrupt VFS: ${msg}`);
|
|
2228
|
+
}
|
|
2208
2229
|
}
|
|
2209
2230
|
}
|
|
2210
2231
|
/** Release the sync access handle (call on fatal error or shutdown) */
|
|
@@ -2271,6 +2292,18 @@ var VFSEngine = class {
|
|
|
2271
2292
|
if (freeBlocks > totalBlocks) {
|
|
2272
2293
|
throw new Error(`Corrupt VFS: free blocks (${freeBlocks}) exceeds total blocks (${totalBlocks})`);
|
|
2273
2294
|
}
|
|
2295
|
+
if (inodeCount > this.maxInodes) {
|
|
2296
|
+
throw new Error(`Corrupt VFS: inode count ${inodeCount} exceeds maximum ${this.maxInodes}`);
|
|
2297
|
+
}
|
|
2298
|
+
if (totalBlocks > this.maxBlocks) {
|
|
2299
|
+
throw new Error(`Corrupt VFS: total blocks ${totalBlocks} exceeds maximum ${this.maxBlocks}`);
|
|
2300
|
+
}
|
|
2301
|
+
if (fileSize > this.maxVFSSize) {
|
|
2302
|
+
throw new Error(`Corrupt VFS: file size ${fileSize} exceeds maximum ${this.maxVFSSize}`);
|
|
2303
|
+
}
|
|
2304
|
+
if (!Number.isFinite(inodeTableOffset) || inodeTableOffset < 0 || !Number.isFinite(pathTableOffset) || pathTableOffset < 0 || !Number.isFinite(bitmapOffset) || bitmapOffset < 0 || !Number.isFinite(dataOffset) || dataOffset < 0) {
|
|
2305
|
+
throw new Error(`Corrupt VFS: non-finite or negative section offset`);
|
|
2306
|
+
}
|
|
2274
2307
|
if (inodeTableOffset !== SUPERBLOCK.SIZE) {
|
|
2275
2308
|
throw new Error(`Corrupt VFS: inode table offset ${inodeTableOffset} (expected ${SUPERBLOCK.SIZE})`);
|
|
2276
2309
|
}
|
|
@@ -2288,7 +2321,13 @@ var VFSEngine = class {
|
|
|
2288
2321
|
if (pathUsed > pathTableSize) {
|
|
2289
2322
|
throw new Error(`Corrupt VFS: path used (${pathUsed}) exceeds path table size (${pathTableSize})`);
|
|
2290
2323
|
}
|
|
2324
|
+
if (pathTableSize > this.maxPathTable) {
|
|
2325
|
+
throw new Error(`Corrupt VFS: path table size ${pathTableSize} exceeds maximum ${this.maxPathTable}`);
|
|
2326
|
+
}
|
|
2291
2327
|
const expectedMinSize = dataOffset + totalBlocks * blockSize;
|
|
2328
|
+
if (expectedMinSize > this.maxVFSSize) {
|
|
2329
|
+
throw new Error(`Corrupt VFS: computed layout size ${expectedMinSize} exceeds maximum ${this.maxVFSSize}`);
|
|
2330
|
+
}
|
|
2292
2331
|
if (fileSize < expectedMinSize) {
|
|
2293
2332
|
throw new Error(`Corrupt VFS: file size ${fileSize} too small for layout (need ${expectedMinSize})`);
|
|
2294
2333
|
}
|
|
@@ -3483,6 +3522,10 @@ var MemoryHandle = class {
|
|
|
3483
3522
|
return this.buf.buffer.slice(0, this.len);
|
|
3484
3523
|
}
|
|
3485
3524
|
grow(minSize) {
|
|
3525
|
+
const MAX_SIZE = 4 * 1024 * 1024 * 1024;
|
|
3526
|
+
if (minSize > MAX_SIZE) {
|
|
3527
|
+
throw new Error(`MemoryHandle: cannot grow to ${minSize} bytes (max ${MAX_SIZE})`);
|
|
3528
|
+
}
|
|
3486
3529
|
const newSize = Math.max(minSize, this.buf.length * 2);
|
|
3487
3530
|
const newBuf = new Uint8Array(newSize);
|
|
3488
3531
|
newBuf.set(this.buf.subarray(0, this.len));
|