Skip to content

RangeError: start offset of Int32Array should be a multiple of 4 #224

@tbrockman

Description

@tbrockman

Stacktrace:

RangeError: start offset of Int32Array should be a multiple of 4
    at new Int32Array (<anonymous>)
    at new MetadataBlock (file:///home/theo/dev/ezcodelol/node_modules/.pnpm/@zenfs+core@2.2.2/node_modules/@zenfs/core/dist/backends/single_buffer.js:303:13)
    at new MetadataBlock (eval at __decorateStruct (file:///home/theo/dev/ezcodelol/node_modules/.pnpm/memium@0.2.0/node_modules/memium/dist/struct.js:52:24), <anonymous>:6:6)
    at SuperBlock.rotateMetadata (file:///home/theo/dev/ezcodelol/node_modules/.pnpm/@zenfs+core@2.2.2/node_modules/@zenfs/core/dist/backends/single_buffer.js:489:30)
    at SingleBufferStore.set (file:///home/theo/dev/ezcodelol/node_modules/.pnpm/@zenfs+core@2.2.2/node_modules/@zenfs/core/dist/backends/single_buffer.js:674:33)
    at SyncMapTransaction.setSync (file:///home/theo/dev/ezcodelol/node_modules/.pnpm/@zenfs+core@2.2.2/node_modules/@zenfs/core/dist/backends/store/map.js:20:20)
    at SyncMapTransaction.set (file:///home/theo/dev/ezcodelol/node_modules/.pnpm/@zenfs+core@2.2.2/node_modules/@zenfs/core/dist/backends/store/store.js:24:21)
    at WrappedTransaction.set (file:///home/theo/dev/ezcodelol/node_modules/.pnpm/@zenfs+core@2.2.2/node_modules/@zenfs/core/dist/backends/store/store.js:135:24)
    at async StoreFS.commitNew (file:///home/theo/dev/ezcodelol/node_modules/.pnpm/@zenfs+core@2.2.2/node_modules/@zenfs/core/dist/backends/store/fs.js:905:13)
    at async StoreFS.createFile (file:///home/theo/dev/ezcodelol/node_modules/.pnpm/@zenfs+core@2.2.2/node_modules/@zenfs/core/dist/backends/store/fs.js:452:16)

Context:
Ran into this when some code like the following (which seems to fail while copying my node_modules folder -- can work on a reproduction if the issue doesn't seem obvious):

Note

"Why are you not using fs.cp here?", you might ask. It seems to result in an empty filesystem when copying Passthrough -> SingleBuffer, so I just wrote my own dumb version.

export const copyDir = async (fs: typeof _fs, source: string, dest: string, filter: (path: string) => boolean = () => true) => {
    const symlinkQueue: { src: string; dest: string }[] = [];

    async function copyRecursive(src: string, dst: string) {
        try {
            const entries = await fs.readdir(src, { withFileTypes: true });
            await fs.mkdir(dst, { recursive: true });

            for (const entry of entries) {
                const srcPath = path.join(src, entry.name);
                const srcRelPath = path.relative(source, srcPath);
                const dstPath = path.join(dst, entry.name);

                if (!filter(srcRelPath)) {
                    continue;
                }

                if (entry.isDirectory()) {
                    await copyRecursive(srcPath, dstPath);
                } else if (entry.isFile()) {
                    const data = nodeFs.readFileSync(srcRelPath);
                    const stats = nodeFs.statSync(srcRelPath);

                    await fs.writeFile(dstPath, data, { encoding: 'utf-8' });
                    // Copy file metadata
                    // console.log('stats', stats)
                    await fs.chown(dstPath, stats.uid, stats.gid);
                    await fs.chmod(dstPath, stats.mode);
                    await fs.utimes(dstPath, stats.atime, stats.mtime);
                } else if (entry.isSymbolicLink()) {
                    symlinkQueue.push({ src: srcPath, dest: dstPath });
                }
            }
        } catch (e) {
            console.error(`Failed to copy ${src} to ${dest}:`, e);
            throw e;
        }
    }

    async function resolveSymlinks() {
        for (const { src, dest } of symlinkQueue) {
            try {
                const target = await fs.readlink(src);
                const absoluteTarget = path.resolve(path.dirname(src), target);

                try {
                    await fs.stat(absoluteTarget);
                    await fs.symlink(target, dest);
                } catch {
                    await fs.copyFile(absoluteTarget, dest);
                }
            } catch (err) {
                console.error(`Failed to copy symlink ${src}:`, err);
            }
        }
    }

    await copyRecursive(source, dest);
    await resolveSymlinks();
}

export const takeSnapshot = async (props: Partial<TakeSnapshotProps> = {}) => {
    let { root, filter } = { ...snapshotDefaults, ...props };

    const estimateUsed = async (folderPath: string) => {
        let total = 0;
    
        const walk = async (dir: string) => {
            const entries = await nodePromises.readdir(dir, { withFileTypes: true })
            for (const entry of entries) {
                const fullPath = path.join(dir, entry.name);
                const stats = await nodePromises.lstat(fullPath);
    
                if (entry.isDirectory()) {
                    await walk(fullPath);
                } else {
                    total += stats.blocks * 512;
                }
            }
        };
    
        await walk(folderPath);
        return total;
    };
    

    const buffer = new ArrayBuffer((await estimateUsed(root))); // ???
    console.debug('buffer', { size: buffer.byteLength, root });

    const readable = await resolveMountConfig({ backend: Passthrough, fs: nodeFs, prefix: root });
    const writable = await resolveMountConfig({ backend: SingleBuffer, buffer });
    // TODO: there's probably a better way to do this
    const hostPath = '/mnt/host';
    const snapshotPath = '/mnt/snapshot';
    mount(hostPath, readable);
    mount(snapshotPath, writable);
    await readable.ready()
    await writable.ready()
    await copyDir(_fs, hostPath, snapshotPath, filter)

    return buffer;
};

// ...
// await takeSnapshot()

Expectation:

This seems like it should work (and it did somewhere around ~2.0.0 -- or perhaps I broke this), not exactly sure what's going wrong here.

Very brief debugging attempt:

Looks like somehow this.used_bytes for the MetadataBlock isn't divisible by 4 (see second argument).

Image

Not sure if related, but is field alignment correct here?

Image

Example stats of file causing this issue:

failed to write {
  stats: Stats {
    dev: 64513,
    mode: 33204,
    nlink: 2,
    uid: 1000,
    gid: 1000,
    rdev: 0,
    blksize: 4096,
    ino: 86000425,
    size: 47,
    blocks: 8,
    atimeMs: 1746813668894.515,
    mtimeMs: 1746805215458.8567,
    ctimeMs: 1746805217720.872,
    birthtimeMs: 1746805215458.8567
  },
  data: 'import { assoc } from "../fp";\nexport = assoc;\n'
}

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions