@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.
@@ -154,6 +154,13 @@ var VFSEngine = class {
154
154
  superblockDirty = false;
155
155
  // Free inode hint — skip O(n) scan
156
156
  freeInodeHint = 0;
157
+ // Configurable upper bounds
158
+ maxInodes = 4e6;
159
+ maxBlocks = 4e6;
160
+ maxPathTable = 256 * 1024 * 1024;
161
+ // 256MB
162
+ maxVFSSize = 100 * 1024 * 1024 * 1024;
163
+ // 100GB
157
164
  init(handle, opts) {
158
165
  this.handle = handle;
159
166
  this.processUid = opts?.uid ?? 0;
@@ -161,11 +168,23 @@ var VFSEngine = class {
161
168
  this.umask = opts?.umask ?? DEFAULT_UMASK;
162
169
  this.strictPermissions = opts?.strictPermissions ?? false;
163
170
  this.debug = opts?.debug ?? false;
171
+ if (opts?.limits) {
172
+ if (opts.limits.maxInodes != null) this.maxInodes = opts.limits.maxInodes;
173
+ if (opts.limits.maxBlocks != null) this.maxBlocks = opts.limits.maxBlocks;
174
+ if (opts.limits.maxPathTable != null) this.maxPathTable = opts.limits.maxPathTable;
175
+ if (opts.limits.maxVFSSize != null) this.maxVFSSize = opts.limits.maxVFSSize;
176
+ }
164
177
  const size = handle.getSize();
165
178
  if (size === 0) {
166
179
  this.format();
167
180
  } else {
168
- this.mount();
181
+ try {
182
+ this.mount();
183
+ } catch (err) {
184
+ const msg = err.message ?? String(err);
185
+ if (msg.startsWith("Corrupt VFS:")) throw err;
186
+ throw new Error(`Corrupt VFS: ${msg}`);
187
+ }
169
188
  }
170
189
  }
171
190
  /** Release the sync access handle (call on fatal error or shutdown) */
@@ -232,6 +251,18 @@ var VFSEngine = class {
232
251
  if (freeBlocks > totalBlocks) {
233
252
  throw new Error(`Corrupt VFS: free blocks (${freeBlocks}) exceeds total blocks (${totalBlocks})`);
234
253
  }
254
+ if (inodeCount > this.maxInodes) {
255
+ throw new Error(`Corrupt VFS: inode count ${inodeCount} exceeds maximum ${this.maxInodes}`);
256
+ }
257
+ if (totalBlocks > this.maxBlocks) {
258
+ throw new Error(`Corrupt VFS: total blocks ${totalBlocks} exceeds maximum ${this.maxBlocks}`);
259
+ }
260
+ if (fileSize > this.maxVFSSize) {
261
+ throw new Error(`Corrupt VFS: file size ${fileSize} exceeds maximum ${this.maxVFSSize}`);
262
+ }
263
+ if (!Number.isFinite(inodeTableOffset) || inodeTableOffset < 0 || !Number.isFinite(pathTableOffset) || pathTableOffset < 0 || !Number.isFinite(bitmapOffset) || bitmapOffset < 0 || !Number.isFinite(dataOffset) || dataOffset < 0) {
264
+ throw new Error(`Corrupt VFS: non-finite or negative section offset`);
265
+ }
235
266
  if (inodeTableOffset !== SUPERBLOCK.SIZE) {
236
267
  throw new Error(`Corrupt VFS: inode table offset ${inodeTableOffset} (expected ${SUPERBLOCK.SIZE})`);
237
268
  }
@@ -249,7 +280,13 @@ var VFSEngine = class {
249
280
  if (pathUsed > pathTableSize) {
250
281
  throw new Error(`Corrupt VFS: path used (${pathUsed}) exceeds path table size (${pathTableSize})`);
251
282
  }
283
+ if (pathTableSize > this.maxPathTable) {
284
+ throw new Error(`Corrupt VFS: path table size ${pathTableSize} exceeds maximum ${this.maxPathTable}`);
285
+ }
252
286
  const expectedMinSize = dataOffset + totalBlocks * blockSize;
287
+ if (expectedMinSize > this.maxVFSSize) {
288
+ throw new Error(`Corrupt VFS: computed layout size ${expectedMinSize} exceeds maximum ${this.maxVFSSize}`);
289
+ }
253
290
  if (fileSize < expectedMinSize) {
254
291
  throw new Error(`Corrupt VFS: file size ${fileSize} too small for layout (need ${expectedMinSize})`);
255
292
  }
@@ -2557,6 +2594,10 @@ function readPayload(targetSab, targetCtrl) {
2557
2594
  if (totalLen <= maxChunk) {
2558
2595
  return new Uint8Array(targetSab, HEADER_SIZE, chunkLen).slice();
2559
2596
  }
2597
+ if (totalLen > activeLimits.maxPayload || totalLen <= 0) {
2598
+ console.error(`[sync-relay] readPayload: totalLen=${totalLen} exceeds limit (${activeLimits.maxPayload}) or invalid`);
2599
+ return new Uint8Array(0);
2600
+ }
2560
2601
  const fullBuffer = new Uint8Array(totalLen);
2561
2602
  let offset = 0;
2562
2603
  fullBuffer.set(new Uint8Array(targetSab, HEADER_SIZE, chunkLen), offset);
@@ -2566,6 +2607,10 @@ function readPayload(targetSab, targetCtrl) {
2566
2607
  Atomics.notify(targetCtrl, 0);
2567
2608
  Atomics.wait(targetCtrl, 0, SIGNAL.CHUNK_ACK);
2568
2609
  const nextLen = Atomics.load(targetCtrl, 3);
2610
+ if (nextLen <= 0 || nextLen > maxChunk) {
2611
+ console.error(`[sync-relay] readPayload: invalid nextLen=${nextLen} at offset=${offset}`);
2612
+ return fullBuffer.slice(0, offset);
2613
+ }
2569
2614
  fullBuffer.set(new Uint8Array(targetSab, HEADER_SIZE, nextLen), offset);
2570
2615
  offset += nextLen;
2571
2616
  }
@@ -2770,8 +2815,57 @@ async function scanOPFSEntries(dir, prefix) {
2770
2815
  }
2771
2816
  return result;
2772
2817
  }
2818
+ var DEFAULT_LIMITS = {
2819
+ maxInodes: 4e6,
2820
+ maxBlocks: 4e6,
2821
+ maxPathTable: 256 * 1024 * 1024,
2822
+ maxVFSSize: 100 * 1024 * 1024 * 1024,
2823
+ maxPayload: 2 * 1024 * 1024 * 1024
2824
+ };
2825
+ function resolveLimits(input) {
2826
+ return { ...DEFAULT_LIMITS, ...input };
2827
+ }
2828
+ var activeLimits = { ...DEFAULT_LIMITS };
2829
+ function quickValidateVFS(handle, fileSize, limits) {
2830
+ if (fileSize < SUPERBLOCK.SIZE) return `file too small (${fileSize} bytes)`;
2831
+ const buf = new Uint8Array(SUPERBLOCK.SIZE);
2832
+ handle.read(buf, { at: 0 });
2833
+ const v = new DataView(buf.buffer);
2834
+ const magic = v.getUint32(SUPERBLOCK.MAGIC, true);
2835
+ if (magic !== VFS_MAGIC) return `bad magic 0x${magic.toString(16)}`;
2836
+ const version = v.getUint32(SUPERBLOCK.VERSION, true);
2837
+ if (version !== VFS_VERSION) return `unsupported version ${version}`;
2838
+ const inodeCount = v.getUint32(SUPERBLOCK.INODE_COUNT, true);
2839
+ const blockSize = v.getUint32(SUPERBLOCK.BLOCK_SIZE, true);
2840
+ const totalBlocks = v.getUint32(SUPERBLOCK.TOTAL_BLOCKS, true);
2841
+ const freeBlocks = v.getUint32(SUPERBLOCK.FREE_BLOCKS, true);
2842
+ const inodeTableOffset = v.getFloat64(SUPERBLOCK.INODE_OFFSET, true);
2843
+ const pathTableOffset = v.getFloat64(SUPERBLOCK.PATH_OFFSET, true);
2844
+ const dataOffset = v.getFloat64(SUPERBLOCK.DATA_OFFSET, true);
2845
+ const bitmapOffset = v.getFloat64(SUPERBLOCK.BITMAP_OFFSET, true);
2846
+ const pathUsed = v.getUint32(SUPERBLOCK.PATH_USED, true);
2847
+ if (blockSize === 0 || (blockSize & blockSize - 1) !== 0) return `invalid block size ${blockSize}`;
2848
+ if (inodeCount === 0) return "inode count is 0";
2849
+ if (inodeCount > limits.maxInodes) return `inode count ${inodeCount} exceeds maximum ${limits.maxInodes}`;
2850
+ if (totalBlocks > limits.maxBlocks) return `total blocks ${totalBlocks} exceeds maximum ${limits.maxBlocks}`;
2851
+ if (freeBlocks > totalBlocks) return `free blocks (${freeBlocks}) exceeds total (${totalBlocks})`;
2852
+ if (!Number.isFinite(inodeTableOffset) || inodeTableOffset < 0 || !Number.isFinite(pathTableOffset) || pathTableOffset < 0 || !Number.isFinite(bitmapOffset) || bitmapOffset < 0 || !Number.isFinite(dataOffset) || dataOffset < 0) return "non-finite or negative section offset";
2853
+ if (inodeTableOffset !== SUPERBLOCK.SIZE) return `inode table offset ${inodeTableOffset} (expected ${SUPERBLOCK.SIZE})`;
2854
+ const expectedPathOffset = inodeTableOffset + inodeCount * INODE_SIZE;
2855
+ if (pathTableOffset !== expectedPathOffset) return `path table offset ${pathTableOffset} (expected ${expectedPathOffset})`;
2856
+ if (bitmapOffset <= pathTableOffset) return "bitmap offset must be after path table";
2857
+ if (dataOffset <= bitmapOffset) return "data offset must be after bitmap";
2858
+ const pathTableSize = bitmapOffset - pathTableOffset;
2859
+ if (pathUsed > pathTableSize) return `path used (${pathUsed}) exceeds path table size (${pathTableSize})`;
2860
+ if (pathTableSize > limits.maxPathTable) return `path table size ${pathTableSize} exceeds maximum ${limits.maxPathTable}`;
2861
+ const expectedMinSize = dataOffset + totalBlocks * blockSize;
2862
+ if (expectedMinSize > limits.maxVFSSize) return `computed layout size ${expectedMinSize} exceeds maximum ${limits.maxVFSSize}`;
2863
+ if (fileSize < expectedMinSize) return `file size ${fileSize} too small for layout (need ${expectedMinSize})`;
2864
+ return null;
2865
+ }
2773
2866
  async function initEngine(config) {
2774
2867
  debug = config.debug ?? false;
2868
+ activeLimits = resolveLimits(config.limits);
2775
2869
  let rootDir = await navigator.storage.getDirectory();
2776
2870
  if (config.root && config.root !== "/") {
2777
2871
  const segments = config.root.split("/").filter(Boolean);
@@ -2781,14 +2875,26 @@ async function initEngine(config) {
2781
2875
  }
2782
2876
  const vfsFileHandle = await rootDir.getFileHandle(".vfs.bin", { create: true });
2783
2877
  const vfsHandle = await vfsFileHandle.createSyncAccessHandle();
2784
- const wasFresh = vfsHandle.getSize() === 0;
2878
+ const vfsSize = vfsHandle.getSize();
2879
+ if (vfsSize > 0) {
2880
+ const validationError = quickValidateVFS(vfsHandle, vfsSize, activeLimits);
2881
+ if (validationError) {
2882
+ try {
2883
+ vfsHandle.close();
2884
+ } catch (_) {
2885
+ }
2886
+ throw new Error(`Corrupt VFS: ${validationError}`);
2887
+ }
2888
+ }
2889
+ const wasFresh = vfsSize === 0;
2785
2890
  try {
2786
2891
  engine.init(vfsHandle, {
2787
2892
  uid: config.uid,
2788
2893
  gid: config.gid,
2789
2894
  umask: config.umask,
2790
2895
  strictPermissions: config.strictPermissions,
2791
- debug: config.debug
2896
+ debug: config.debug,
2897
+ limits: activeLimits
2792
2898
  });
2793
2899
  } catch (err) {
2794
2900
  try {