From f6aeadb22d4f6d113b61a9cccd5d7c2c8e4bad52 Mon Sep 17 00:00:00 2001 From: imrcpan Date: Thu, 13 Aug 2026 10:48:49 +0200 Subject: [PATCH] test code v1 --- src/Makefile => Makefile | 0 README.md | 90 -- build.sh | 68 - current/README.md | 13 - .../report-1/2026-08-09-overall-progress.md | 160 --- current/report-1/README.md | 8 - .../report-2/2026-08-09-overall-progress.md | 123 -- current/report-2/README.md | 8 - .../report-3/2026-08-09-overall-progress.md | 124 -- current/report-3/README.md | 9 - ...8-10-task2-code-review-style-assessment.md | 726 ---------- current/report-4/README.md | 41 - src/data.c => data.c | 0 src/decompressor.c => decompressor.c | 0 ...ressor_deflate.c => decompressor_deflate.c | 0 ...decompressor_lzma.c => decompressor_lzma.c | 0 ...decompressor_zstd.c => decompressor_zstd.c | 0 src/dir.c => dir.c | 0 docs/TEST_REPORT.md | 97 -- docs/architecture.md | 283 ---- docs/capabilities.md | 65 - docs/erofs.5 | 271 ---- docs/features.md | 77 - docs/pre2-baseline.md | 65 - docs/pre3-baseline.md | 83 -- docs/pre3-smoke-test-20260812.md | 84 -- docs/pre5-baseline.md | 80 -- docs/pre5-completion-20260812.md | 268 ---- docs/pre6-baseline.md | 99 -- docs/pre7-baseline.md | 55 - docs/pre8-baseline.md | 86 -- docs/pre8-completion-and-validation.md | 314 ---- docs/pre8-loader-history-correction.md | 74 - docs/pre9-baseline.md | 191 --- docs/pre9-cache-final-manual-validation.md | 132 -- docs/pre9-controlled-manual-qemu.md | 158 -- docs/pre9-lzma-cache-implementation.md | 142 -- docs/pre9-lzma-cache-review-resolution.md | 62 - .../pre9-manual-evidence/full-sha-samples.txt | 26 - .../pre9-cache-final-kernel-cleanup.txt | 16 - .../pre9-cache-final-probes.txt | 21 - .../pre9-cache-final-result.json | 100 -- docs/pre9-manual-evidence/probe-summary.txt | 22 - docs/pre9-manual-evidence/run3-audit.json | 62 - docs/refactoring-plan.md | 37 - docs/refactoring-status.md | 40 - src/erofs_defs.h => erofs_defs.h | 0 src/erofs_fs.h => erofs_fs.h | 0 src/erofs_vnops.c => erofs_vnops.c | 0 src/inode.c => inode.c | 0 src/internal.h => internal.h | 0 issues/README.md | 15 - issues/TC010-48bit-statfs-large-provider.md | 92 -- issues/TC060-pathconf-standard-values.md | 119 -- ...-large-directory-block-index-validation.md | 193 --- issues/extent-metadata-fixture-unavailable.md | 169 --- src/lz4.c => lz4.c | 0 src/namei.c => namei.c | 0 src/.clang-format | 199 --- src/.gitignore | 18 - src/super.c => super.c | 0 test_all_decompress.sh | 462 ------ test_chunk_based.sh | 190 --- tests/EXECUTION-CHECKLIST.md | 78 - tests/G1-MANUAL-SETUP.md | 69 - tests/G3-MANUAL-SETUP.md | 58 - tests/G4-MANUAL-SETUP.md | 88 -- tests/G5-MANUAL-SETUP.md | 118 -- tests/G6-MANUAL-SETUP.md | 164 --- tests/G7-MANUAL-SETUP.md | 226 --- tests/RESULT-SUMMARY-TEMPLATE.md | 229 --- tests/TC000-template.md | 36 - tests/TC001-mount-basic.md | 39 - tests/TC002-superblock-crc32c.md | 66 - tests/TC003-lz4-compressed-read.md | 87 -- tests/TC004-lzma-compressed-read.md | 96 -- tests/TC005-inline-xattr-user.md | 32 - tests/TC006-multidev-chunk-read.md | 61 - tests/TC007-concurrent-mount.md | 48 - tests/TC008-mount-errors.md | 77 - tests/TC009-statfs-basic.md | 42 - tests/TC010-statfs-48bit.md | 60 - tests/TC011-root-vnode.md | 36 - tests/TC012-vget-normal.md | 39 - tests/TC013-vget-invalid.md | 39 - tests/TC014-superblock-parse.md | 42 - tests/TC015-superblock-corrupted.md | 63 - tests/TC016-superblock-crc-invalid.md | 52 - tests/TC017-48bit-blocks-parse.md | 44 - tests/TC018-48bit-large-fs.md | 66 - tests/TC019-48bit-root-nid.md | 44 - tests/TC020-compact-inode-basic.md | 32 - tests/TC021-compact-inode-nlink1.md | 28 - tests/TC022-compact-inode-special.md | 31 - tests/TC023-extended-inode-normal.md | 27 - tests/TC024-extended-inode-large.md | 38 - tests/TC025-nlink1-handling.md | 35 - tests/TC026-dot-omitted-with.md | 33 - tests/TC027-dot-omitted-without.md | 35 - tests/TC028-flat-plain-small.md | 26 - tests/TC029-flat-plain-medium.md | 30 - tests/TC030-flat-plain-large.md | 33 - tests/TC031-flat-inline-normal.md | 28 - tests/TC032-flat-inline-zero.md | 29 - tests/TC033-tailpacking-normal.md | 33 - tests/TC034-tailpacking-boundary.md | 35 - tests/TC035-file-read-sequential.md | 29 - tests/TC036-file-read-random.md | 30 - tests/TC037-file-read-large.md | 33 - tests/TC038-symlink-short.md | 30 - tests/TC039-symlink-long.md | 36 - tests/TC040-symlink-broken.md | 30 - tests/TC041-dirent-decode-normal.md | 29 - tests/TC042-dirent-large-dir.md | 28 - tests/TC043-lookup-found.md | 29 - tests/TC044-lookup-not-found.md | 28 - tests/TC045-lookup-case-sensitive.md | 27 - tests/TC046-readdir-basic.md | 25 - tests/TC047-readdir-large.md | 28 - tests/TC048-readdir-seek.md | 31 - tests/TC049-dot-handling.md | 26 - tests/TC050-dotdot-handling.md | 26 - tests/TC051-namecache-hit.md | 29 - tests/TC052-namecache-miss.md | 28 - tests/TC053-vfs-hash-integration.md | 30 - tests/TC054-vn-vget-ino.md | 30 - tests/TC055-getattr-basic.md | 30 - tests/TC056-getattr-special.md | 28 - tests/TC057-access-allowed.md | 24 - tests/TC058-access-denied.md | 24 - tests/TC059-readlink.md | 30 - tests/TC060-pathconf.md | 25 - tests/TC061-setattr-chmod-reject.md | 24 - tests/TC062-setattr-chown-reject.md | 25 - tests/TC063-setattr-write-reject.md | 30 - tests/TC064-vop-getpages-normal.md | 25 - tests/TC065-vop-getpages-mmap.md | 24 - tests/TC066-vop-bmap-unsupported.md | 24 - tests/TC067-shared-xattr-scan-found.md | 28 - tests/TC068-shared-xattr-scan-notfound.md | 26 - tests/TC069-shared-xattr-list-multiple.md | 23 - tests/TC070-shared-trusted-xattr.md | 24 - tests/TC071-shared-security-xattr.md | 25 - tests/TC072-shared-system-xattr-list.md | 26 - tests/TC073-metabox-container.md | 30 - tests/TC074-inline-user-xattr-multiple.md | 24 - tests/TC075-inline-trusted-xattr.md | 24 - tests/TC076-inline-security-xattr.md | 22 - tests/TC077-long-prefix-user-xattr.md | 25 - tests/TC078-long-prefix-trusted-xattr.md | 24 - tests/TC079-packed-prefix-table.md | 33 - tests/TC080-prefix-table-lookup.md | 25 - tests/TC081-metabox-backed-xattr.md | 26 - tests/TC082-system-namespace-subset.md | 25 - tests/TC083-user-namespace-subset.md | 25 - tests/TC084-lz4-basic.md | 71 - tests/TC085-lz4-large-file.md | 78 - tests/TC086-lz4-sequential-read.md | 57 - tests/TC087-lz4-random-read.md | 53 - tests/TC088-lz4-config-handling.md | 85 -- tests/TC089-lz4-pcluster-4k.md | 69 - tests/TC090-lz4-pcluster-64k.md | 72 - tests/TC091-ztailpacking-basic.md | 76 - tests/TC092-ztailpacking-edge.md | 79 - tests/TC093-chunk-single-device.md | 55 - tests/TC094-chunk-multi-device.md | 116 -- tests/TC095-chunk-index-read.md | 113 -- tests/TC096-device-table-parse.md | 105 -- tests/TC097-device-table-invalid.md | 91 -- tests/TC098-fragments-support.md | 59 - tests/TC099-multidev-2devices.md | 64 - tests/TC100-multidev-4devices.md | 91 -- tests/TC101-unified-address-mapping.md | 151 -- tests/TC102-deflate-level1.md | 54 - tests/TC103-deflate-level6.md | 46 - tests/TC104-deflate-level9.md | 46 - tests/TC105-zstd-level1.md | 46 - tests/TC106-zstd-level15.md | 46 - tests/TC107-zstd-level22.md | 46 - tests/TC108-lzma-large-file.md | 49 - tests/TC109-microlzma.md | 48 - tests/TC110-lzma-corrupt.md | 50 - tests/TC111-manpage-accuracy.md | 54 - tests/TC112-invalid-superblock.md | 56 - tests/TC113-corrupted-inode.md | 48 - tests/TC114-out-of-bounds-block.md | 48 - tests/TC115-unsupported-algorithm.md | 54 - tests/TC116-truncated-compressed.md | 69 - tests/TC117-invalid-xattr-format.md | 33 - tests/TC118-device-not-found.md | 137 -- tests/TC119-data-crc-mismatch.md | 56 - tests/TC120-empty-file.md | 54 - tests/TC121-maximum-file-size.md | 64 - tests/TC122-deep-directory-tree.md | 56 - tests/TC123-long-filename.md | 55 - tests/TC124-many-small-files.md | 60 - tests/TC125-large-directory.md | 64 - tests/TC126-concurrent-reads.md | 77 - tests/TC127-memory-pressure.md | 114 -- tests/TC128-sequential-throughput.md | 62 - tests/TC129-random-read-pattern.md | 68 - tests/TC130-mixed-workload.md | 121 -- tests/TC131-nfs-export-basic.md | 143 -- tests/TC132-nfs-file-handle-stability.md | 97 -- tests/TC133-nfs-export-stress.md | 176 --- tests/TC134-metabox-shared-nonzero-base.md | 26 - ...135-prefix-metabox-and-primary-fallback.md | 36 - ...TC136-metabox-truncated-super-extension.md | 26 - tests/TC137-xattr-declared-image-bounds.md | 37 - .../TC138-acl-linux-order-and-empty-header.md | 27 - tests/TC139-fifo-acl-xattr-readonly.md | 31 - tests/TC140-compressed-metabox-carrier.md | 37 - tests/TC141-cold-nested-namei.md | 44 - ...C142-fragment-backed-compressed-metabox.md | 42 - tests/TC143-deflate-zstd-partial-reference.md | 81 -- ...44-microlzma-consumption-and-corruption.md | 52 - tests/TC145-zstdio-build-gate.md | 54 - .../TC146-head2-interlaced-extent-mapping.md | 85 -- tests/TC147-flat-inline-block-bounds.md | 46 - tests/TC148-linux-directory-tail-padding.md | 28 - tests/TC149-vnode-pager-real-faults.md | 23 - tests/TC150-48bit-fallback-root.md | 43 - tests/TC151-extended-inode-off-max.md | 42 - tests/TC152-extent-hole-bounded-read.md | 26 - tests/TC153-large-directory-block-index.md | 43 - tests/TC154-nfs-per-inode-generation.md | 77 - tests/TC155-explicit-extent-pa-wrap.md | 60 - tests/TC156-inode-timestamp-validation.md | 51 - .../TC157-explicit-extent-order-validation.md | 55 - tests/TC158-48bit-compressed-blocks-hi.md | 45 - tests/TC159-special-setattr-combinations.md | 44 - tests/TC160-dot-omitted-offmax-cookie.md | 39 - tests/TC161-build-nm-failure.md | 38 - tests/TEST-COVERAGE-MATRIX.md | 294 ---- tests/erofs_fixture.py | 764 ---------- tests/final_review_fixtures.py | 412 ------ tests/final_review_probe.c | 127 -- tests/final_review_setattr_probe.c | 87 -- tests/final_review_setattr_probe.mk | 4 - tests/g1_fixtures.py | 372 ----- tests/g3_fixtures.py | 305 ---- tests/g3_vfs_probe.c | 208 --- tests/g4_fixtures.py | 1277 ----------------- tests/g5_fixtures.py | 1109 -------------- tests/g6_multidev_fixtures.py | 1218 ---------------- tests/g7_fixtures.py | 495 ------- tests/g7_probe.c | 356 ----- tests/mmap_fault.c | 200 --- tests/nfs_fh_tool.c | 283 ---- tests/prepare_directory_fixtures.sh | 54 - tests/prepare_error_fixtures.sh | 90 -- tests/prepare_g1_fixtures.sh | 196 --- tests/prepare_g3_fixtures.sh | 45 - tests/read_probe.c | 128 -- tests/readdir_probe.c | 329 ----- .../2026-08-08T1037Z/manual-test-report.md | 188 --- .../2026-08-08T1114Z/manual-test-report.md | 435 ------ .../2026-08-08T1138Z/manual-test-report.md | 513 ------- .../2026-08-08T1307Z/manual-test-report.md | 141 -- .../2026-08-08T1332Z/manual-test-report.md | 84 -- .../2026-08-08T1356Z/manual-test-report.md | 88 -- .../2026-08-08T1413Z/manual-test-report.md | 99 -- .../2026-08-08T1427Z/manual-test-report.md | 53 - .../2026-08-08T1444Z/manual-test-report.md | 41 - .../2026-08-08T1455Z/manual-test-report.md | 33 - .../2026-08-08T1705Z/manual-test-report.md | 46 - .../manual-test-report.md | 112 -- .../prepare-fixtures.sh | 199 --- .../2026-08-08T1800Z-namei/readdir_probe.c | 71 - .../2026-08-08T1800Z-namei/vm-regression.sh | 201 --- .../2026-08-08T1812Z/manual-test-report.md | 113 -- .../manual-test-report.md | 232 --- .../manual-test-report.md | 559 -------- .../manual-test-report.md | 142 -- .../manual-test-report.md | 406 ------ .../manual-test-report.md | 358 ----- .../prepare-fixtures.sh | 277 ---- .../manual-test-report.md | 166 --- .../prepare-fixtures.sh | 271 ---- .../manual-test-report.md | 98 -- .../2026-08-09T0710Z-g1/manual-test-report.md | 159 -- .../2026-08-09T0710Z-g2/manual-test-report.md | 269 ---- .../2026-08-09T0839Z-g4/manual-test-report.md | 132 -- .../2026-08-09T1059Z-g3/manual-test-report.md | 154 -- .../manual-test-report.md | 157 -- .../2026-08-09T1236Z-g5/manual-test-report.md | 212 --- .../2026-08-09T1244Z-g6/manual-test-report.md | 139 -- .../2026-08-09T1343Z-g8/manual-test-report.md | 269 ---- .../2026-08-09T1359Z-g7/manual-test-report.md | 183 --- .../manual-test-report.md | 119 -- tests/review_fixtures.py | 608 -------- tests/sparse_hole_probe.c | 63 - tests/stat_special.c | 70 - tests/statfs_probe.c | 24 - tests/test_decompress.c | 519 ------- tests/test_decompress_standalone.c | 291 ---- src/xattr.c => xattr.c | 0 src/xattr.h => xattr.h | 0 src/zdata.c => zdata.c | 0 src/zmap.c => zmap.c | 0 300 files changed, 31306 deletions(-) rename src/Makefile => Makefile (100%) delete mode 100644 README.md delete mode 100755 build.sh delete mode 100644 current/README.md delete mode 100644 current/report-1/2026-08-09-overall-progress.md delete mode 100644 current/report-1/README.md delete mode 100644 current/report-2/2026-08-09-overall-progress.md delete mode 100644 current/report-2/README.md delete mode 100644 current/report-3/2026-08-09-overall-progress.md delete mode 100644 current/report-3/README.md delete mode 100644 current/report-4/2026-08-10-task2-code-review-style-assessment.md delete mode 100644 current/report-4/README.md rename src/data.c => data.c (100%) rename src/decompressor.c => decompressor.c (100%) rename src/decompressor_deflate.c => decompressor_deflate.c (100%) rename src/decompressor_lzma.c => decompressor_lzma.c (100%) rename src/decompressor_zstd.c => decompressor_zstd.c (100%) rename src/dir.c => dir.c (100%) delete mode 100644 docs/TEST_REPORT.md delete mode 100644 docs/architecture.md delete mode 100644 docs/capabilities.md delete mode 100644 docs/erofs.5 delete mode 100644 docs/features.md delete mode 100644 docs/pre2-baseline.md delete mode 100644 docs/pre3-baseline.md delete mode 100644 docs/pre3-smoke-test-20260812.md delete mode 100644 docs/pre5-baseline.md delete mode 100644 docs/pre5-completion-20260812.md delete mode 100644 docs/pre6-baseline.md delete mode 100644 docs/pre7-baseline.md delete mode 100644 docs/pre8-baseline.md delete mode 100644 docs/pre8-completion-and-validation.md delete mode 100644 docs/pre8-loader-history-correction.md delete mode 100644 docs/pre9-baseline.md delete mode 100644 docs/pre9-cache-final-manual-validation.md delete mode 100644 docs/pre9-controlled-manual-qemu.md delete mode 100644 docs/pre9-lzma-cache-implementation.md delete mode 100644 docs/pre9-lzma-cache-review-resolution.md delete mode 100644 docs/pre9-manual-evidence/full-sha-samples.txt delete mode 100644 docs/pre9-manual-evidence/pre9-cache-final-kernel-cleanup.txt delete mode 100644 docs/pre9-manual-evidence/pre9-cache-final-probes.txt delete mode 100644 docs/pre9-manual-evidence/pre9-cache-final-result.json delete mode 100644 docs/pre9-manual-evidence/probe-summary.txt delete mode 100644 docs/pre9-manual-evidence/run3-audit.json delete mode 100644 docs/refactoring-plan.md delete mode 100644 docs/refactoring-status.md rename src/erofs_defs.h => erofs_defs.h (100%) rename src/erofs_fs.h => erofs_fs.h (100%) rename src/erofs_vnops.c => erofs_vnops.c (100%) rename src/inode.c => inode.c (100%) rename src/internal.h => internal.h (100%) delete mode 100644 issues/README.md delete mode 100644 issues/TC010-48bit-statfs-large-provider.md delete mode 100644 issues/TC060-pathconf-standard-values.md delete mode 100644 issues/TC153-large-directory-block-index-validation.md delete mode 100644 issues/extent-metadata-fixture-unavailable.md rename src/lz4.c => lz4.c (100%) rename src/namei.c => namei.c (100%) delete mode 100644 src/.clang-format delete mode 100644 src/.gitignore rename src/super.c => super.c (100%) delete mode 100755 test_all_decompress.sh delete mode 100755 test_chunk_based.sh delete mode 100644 tests/EXECUTION-CHECKLIST.md delete mode 100644 tests/G1-MANUAL-SETUP.md delete mode 100644 tests/G3-MANUAL-SETUP.md delete mode 100644 tests/G4-MANUAL-SETUP.md delete mode 100644 tests/G5-MANUAL-SETUP.md delete mode 100644 tests/G6-MANUAL-SETUP.md delete mode 100644 tests/G7-MANUAL-SETUP.md delete mode 100644 tests/RESULT-SUMMARY-TEMPLATE.md delete mode 100644 tests/TC000-template.md delete mode 100644 tests/TC001-mount-basic.md delete mode 100644 tests/TC002-superblock-crc32c.md delete mode 100644 tests/TC003-lz4-compressed-read.md delete mode 100644 tests/TC004-lzma-compressed-read.md delete mode 100644 tests/TC005-inline-xattr-user.md delete mode 100644 tests/TC006-multidev-chunk-read.md delete mode 100644 tests/TC007-concurrent-mount.md delete mode 100644 tests/TC008-mount-errors.md delete mode 100644 tests/TC009-statfs-basic.md delete mode 100644 tests/TC010-statfs-48bit.md delete mode 100644 tests/TC011-root-vnode.md delete mode 100644 tests/TC012-vget-normal.md delete mode 100644 tests/TC013-vget-invalid.md delete mode 100644 tests/TC014-superblock-parse.md delete mode 100644 tests/TC015-superblock-corrupted.md delete mode 100644 tests/TC016-superblock-crc-invalid.md delete mode 100644 tests/TC017-48bit-blocks-parse.md delete mode 100644 tests/TC018-48bit-large-fs.md delete mode 100644 tests/TC019-48bit-root-nid.md delete mode 100644 tests/TC020-compact-inode-basic.md delete mode 100644 tests/TC021-compact-inode-nlink1.md delete mode 100644 tests/TC022-compact-inode-special.md delete mode 100644 tests/TC023-extended-inode-normal.md delete mode 100644 tests/TC024-extended-inode-large.md delete mode 100644 tests/TC025-nlink1-handling.md delete mode 100644 tests/TC026-dot-omitted-with.md delete mode 100644 tests/TC027-dot-omitted-without.md delete mode 100644 tests/TC028-flat-plain-small.md delete mode 100644 tests/TC029-flat-plain-medium.md delete mode 100644 tests/TC030-flat-plain-large.md delete mode 100644 tests/TC031-flat-inline-normal.md delete mode 100644 tests/TC032-flat-inline-zero.md delete mode 100644 tests/TC033-tailpacking-normal.md delete mode 100644 tests/TC034-tailpacking-boundary.md delete mode 100644 tests/TC035-file-read-sequential.md delete mode 100644 tests/TC036-file-read-random.md delete mode 100644 tests/TC037-file-read-large.md delete mode 100644 tests/TC038-symlink-short.md delete mode 100644 tests/TC039-symlink-long.md delete mode 100644 tests/TC040-symlink-broken.md delete mode 100644 tests/TC041-dirent-decode-normal.md delete mode 100644 tests/TC042-dirent-large-dir.md delete mode 100644 tests/TC043-lookup-found.md delete mode 100644 tests/TC044-lookup-not-found.md delete mode 100644 tests/TC045-lookup-case-sensitive.md delete mode 100644 tests/TC046-readdir-basic.md delete mode 100644 tests/TC047-readdir-large.md delete mode 100644 tests/TC048-readdir-seek.md delete mode 100644 tests/TC049-dot-handling.md delete mode 100644 tests/TC050-dotdot-handling.md delete mode 100644 tests/TC051-namecache-hit.md delete mode 100644 tests/TC052-namecache-miss.md delete mode 100644 tests/TC053-vfs-hash-integration.md delete mode 100644 tests/TC054-vn-vget-ino.md delete mode 100644 tests/TC055-getattr-basic.md delete mode 100644 tests/TC056-getattr-special.md delete mode 100644 tests/TC057-access-allowed.md delete mode 100644 tests/TC058-access-denied.md delete mode 100644 tests/TC059-readlink.md delete mode 100644 tests/TC060-pathconf.md delete mode 100644 tests/TC061-setattr-chmod-reject.md delete mode 100644 tests/TC062-setattr-chown-reject.md delete mode 100644 tests/TC063-setattr-write-reject.md delete mode 100644 tests/TC064-vop-getpages-normal.md delete mode 100644 tests/TC065-vop-getpages-mmap.md delete mode 100644 tests/TC066-vop-bmap-unsupported.md delete mode 100644 tests/TC067-shared-xattr-scan-found.md delete mode 100644 tests/TC068-shared-xattr-scan-notfound.md delete mode 100644 tests/TC069-shared-xattr-list-multiple.md delete mode 100644 tests/TC070-shared-trusted-xattr.md delete mode 100644 tests/TC071-shared-security-xattr.md delete mode 100644 tests/TC072-shared-system-xattr-list.md delete mode 100644 tests/TC073-metabox-container.md delete mode 100644 tests/TC074-inline-user-xattr-multiple.md delete mode 100644 tests/TC075-inline-trusted-xattr.md delete mode 100644 tests/TC076-inline-security-xattr.md delete mode 100644 tests/TC077-long-prefix-user-xattr.md delete mode 100644 tests/TC078-long-prefix-trusted-xattr.md delete mode 100644 tests/TC079-packed-prefix-table.md delete mode 100644 tests/TC080-prefix-table-lookup.md delete mode 100644 tests/TC081-metabox-backed-xattr.md delete mode 100644 tests/TC082-system-namespace-subset.md delete mode 100644 tests/TC083-user-namespace-subset.md delete mode 100644 tests/TC084-lz4-basic.md delete mode 100644 tests/TC085-lz4-large-file.md delete mode 100644 tests/TC086-lz4-sequential-read.md delete mode 100644 tests/TC087-lz4-random-read.md delete mode 100644 tests/TC088-lz4-config-handling.md delete mode 100644 tests/TC089-lz4-pcluster-4k.md delete mode 100644 tests/TC090-lz4-pcluster-64k.md delete mode 100644 tests/TC091-ztailpacking-basic.md delete mode 100644 tests/TC092-ztailpacking-edge.md delete mode 100644 tests/TC093-chunk-single-device.md delete mode 100644 tests/TC094-chunk-multi-device.md delete mode 100644 tests/TC095-chunk-index-read.md delete mode 100644 tests/TC096-device-table-parse.md delete mode 100644 tests/TC097-device-table-invalid.md delete mode 100644 tests/TC098-fragments-support.md delete mode 100644 tests/TC099-multidev-2devices.md delete mode 100644 tests/TC100-multidev-4devices.md delete mode 100644 tests/TC101-unified-address-mapping.md delete mode 100644 tests/TC102-deflate-level1.md delete mode 100644 tests/TC103-deflate-level6.md delete mode 100644 tests/TC104-deflate-level9.md delete mode 100644 tests/TC105-zstd-level1.md delete mode 100644 tests/TC106-zstd-level15.md delete mode 100644 tests/TC107-zstd-level22.md delete mode 100644 tests/TC108-lzma-large-file.md delete mode 100644 tests/TC109-microlzma.md delete mode 100644 tests/TC110-lzma-corrupt.md delete mode 100644 tests/TC111-manpage-accuracy.md delete mode 100644 tests/TC112-invalid-superblock.md delete mode 100644 tests/TC113-corrupted-inode.md delete mode 100644 tests/TC114-out-of-bounds-block.md delete mode 100644 tests/TC115-unsupported-algorithm.md delete mode 100644 tests/TC116-truncated-compressed.md delete mode 100644 tests/TC117-invalid-xattr-format.md delete mode 100644 tests/TC118-device-not-found.md delete mode 100644 tests/TC119-data-crc-mismatch.md delete mode 100644 tests/TC120-empty-file.md delete mode 100644 tests/TC121-maximum-file-size.md delete mode 100644 tests/TC122-deep-directory-tree.md delete mode 100644 tests/TC123-long-filename.md delete mode 100644 tests/TC124-many-small-files.md delete mode 100644 tests/TC125-large-directory.md delete mode 100644 tests/TC126-concurrent-reads.md delete mode 100644 tests/TC127-memory-pressure.md delete mode 100644 tests/TC128-sequential-throughput.md delete mode 100644 tests/TC129-random-read-pattern.md delete mode 100644 tests/TC130-mixed-workload.md delete mode 100644 tests/TC131-nfs-export-basic.md delete mode 100644 tests/TC132-nfs-file-handle-stability.md delete mode 100644 tests/TC133-nfs-export-stress.md delete mode 100644 tests/TC134-metabox-shared-nonzero-base.md delete mode 100644 tests/TC135-prefix-metabox-and-primary-fallback.md delete mode 100644 tests/TC136-metabox-truncated-super-extension.md delete mode 100644 tests/TC137-xattr-declared-image-bounds.md delete mode 100644 tests/TC138-acl-linux-order-and-empty-header.md delete mode 100644 tests/TC139-fifo-acl-xattr-readonly.md delete mode 100644 tests/TC140-compressed-metabox-carrier.md delete mode 100644 tests/TC141-cold-nested-namei.md delete mode 100644 tests/TC142-fragment-backed-compressed-metabox.md delete mode 100644 tests/TC143-deflate-zstd-partial-reference.md delete mode 100644 tests/TC144-microlzma-consumption-and-corruption.md delete mode 100644 tests/TC145-zstdio-build-gate.md delete mode 100644 tests/TC146-head2-interlaced-extent-mapping.md delete mode 100644 tests/TC147-flat-inline-block-bounds.md delete mode 100644 tests/TC148-linux-directory-tail-padding.md delete mode 100644 tests/TC149-vnode-pager-real-faults.md delete mode 100644 tests/TC150-48bit-fallback-root.md delete mode 100644 tests/TC151-extended-inode-off-max.md delete mode 100644 tests/TC152-extent-hole-bounded-read.md delete mode 100644 tests/TC153-large-directory-block-index.md delete mode 100644 tests/TC154-nfs-per-inode-generation.md delete mode 100644 tests/TC155-explicit-extent-pa-wrap.md delete mode 100644 tests/TC156-inode-timestamp-validation.md delete mode 100644 tests/TC157-explicit-extent-order-validation.md delete mode 100644 tests/TC158-48bit-compressed-blocks-hi.md delete mode 100644 tests/TC159-special-setattr-combinations.md delete mode 100644 tests/TC160-dot-omitted-offmax-cookie.md delete mode 100644 tests/TC161-build-nm-failure.md delete mode 100644 tests/TEST-COVERAGE-MATRIX.md delete mode 100755 tests/erofs_fixture.py delete mode 100644 tests/final_review_fixtures.py delete mode 100644 tests/final_review_probe.c delete mode 100644 tests/final_review_setattr_probe.c delete mode 100644 tests/final_review_setattr_probe.mk delete mode 100755 tests/g1_fixtures.py delete mode 100755 tests/g3_fixtures.py delete mode 100644 tests/g3_vfs_probe.c delete mode 100755 tests/g4_fixtures.py delete mode 100644 tests/g5_fixtures.py delete mode 100644 tests/g6_multidev_fixtures.py delete mode 100644 tests/g7_fixtures.py delete mode 100644 tests/g7_probe.c delete mode 100644 tests/mmap_fault.c delete mode 100644 tests/nfs_fh_tool.c delete mode 100755 tests/prepare_directory_fixtures.sh delete mode 100755 tests/prepare_error_fixtures.sh delete mode 100755 tests/prepare_g1_fixtures.sh delete mode 100755 tests/prepare_g3_fixtures.sh delete mode 100644 tests/read_probe.c delete mode 100644 tests/readdir_probe.c delete mode 100644 tests/results/manual/2026-08-08T1037Z/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-08T1114Z/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-08T1138Z/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-08T1307Z/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-08T1332Z/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-08T1356Z/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-08T1413Z/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-08T1427Z/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-08T1444Z/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-08T1455Z/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-08T1705Z/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-08T1800Z-namei/manual-test-report.md delete mode 100755 tests/results/manual/2026-08-08T1800Z-namei/prepare-fixtures.sh delete mode 100644 tests/results/manual/2026-08-08T1800Z-namei/readdir_probe.c delete mode 100755 tests/results/manual/2026-08-08T1800Z-namei/vm-regression.sh delete mode 100644 tests/results/manual/2026-08-08T1812Z/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-08T1937Z-multidev/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-08T2037Z-nfs/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-08T2114Z-multidev-review/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-08T2306Z-compression-p0/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-08T2337Z-metadata-vfs/manual-test-report.md delete mode 100755 tests/results/manual/2026-08-08T2337Z-metadata-vfs/prepare-fixtures.sh delete mode 100644 tests/results/manual/2026-08-09T0124Z-final-review/manual-test-report.md delete mode 100755 tests/results/manual/2026-08-09T0124Z-final-review/prepare-fixtures.sh delete mode 100644 tests/results/manual/2026-08-09T0557Z-review-fixes/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-09T0710Z-g1/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-09T0710Z-g2/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-09T0839Z-g4/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-09T1059Z-g3/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-09T1120Z-tc060-fix/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-09T1236Z-g5/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-09T1244Z-g6/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-09T1343Z-g8/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-09T1359Z-g7/manual-test-report.md delete mode 100644 tests/results/manual/2026-08-09T1804Z-final-review-independent/manual-test-report.md delete mode 100644 tests/review_fixtures.py delete mode 100644 tests/sparse_hole_probe.c delete mode 100644 tests/stat_special.c delete mode 100644 tests/statfs_probe.c delete mode 100644 tests/test_decompress.c delete mode 100644 tests/test_decompress_standalone.c rename src/xattr.c => xattr.c (100%) rename src/xattr.h => xattr.h (100%) rename src/zdata.c => zdata.c (100%) rename src/zmap.c => zmap.c (100%) diff --git a/src/Makefile b/Makefile similarity index 100% rename from src/Makefile rename to Makefile diff --git a/README.md b/README.md deleted file mode 100644 index 19ec856..0000000 --- a/README.md +++ /dev/null @@ -1,90 +0,0 @@ -# EROFS for FreeBSD - -## Build Kernel Module - -Build on FreeBSD 15 amd64 with a matching FreeBSD source tree. The default -source path is `/usr/src`: - -```sh -WITH_ZSTDIO=0 ./build.sh -``` - -Use `FREEBSD_SRC=/path/to/freebsd-src` when the source tree is elsewhere. The -script is a native FreeBSD wrapper around `src/Makefile` and `bsd.kmod.mk`; the -Makefile is the authoritative module name, source list, architecture gate, and -per-source flag definition. Each run rebuilds `build/obj` and writes -`build/erofs.ko`. - -Only `MACHINE_ARCH=amd64` is currently qualified. Other architectures are -rejected explicitly instead of inheriting amd64 ABI flags. Build ZSTD support -with: - -```sh -WITH_ZSTDIO=1 ./build.sh -``` - -The enabled module references the FreeBSD kernel ZSTD API and therefore -requires a running kernel built with `options ZSTDIO`. `WITH_ZSTDIO=0` builds a -module without those references and rejects ZSTD-compressed images at mount. - -## Mount - -EROFS is read-only. Mount a single-device image with: - -```sh -mount -t erofs -o ro /dev/md0 /mnt/erofs -``` - -For an image with external blob devices, map every one-based on-disk device -slot explicitly with `device.=`: - -```sh -mount -t erofs -o ro \ - -o device.2=/dev/md92 \ - -o device.1=/dev/md91 \ - /dev/md90 /mnt/erofs -``` - -Slot names make the mapping independent of option order. Every external slot -must be present exactly once; assigning the same GEOM provider to multiple -slots is rejected. The same names and values may be passed directly as -`nmount(2)` iovec entries. - -There is no repo-local `mount_erofs` binary; FreeBSD's generic `/sbin/mount` -frontend passes these distinct option names through to `nmount(2)`. The driver -forces every successful mount read-only, so even `-o rw` produces a read-only -mount rather than enabling writes. - -If an image has a device table but no `device.` options are supplied, -the primary provider is treated as a Linux-compatible flatdev image. It must -contain the external ranges at their declared `uniaddr` block offsets, for -example a deterministic concatenation of the primary image and its blobs. - -## Qualified Semantics - -- Compact/extended metadata, Linux device-number decode, plain/inline/chunk - data, and LZ4/MicroLZMA/DEFLATE/ZSTD compressed reads. -- Inline tails are confined to the inode metadata block and declared image or - metabox backing bounds. -- Directory lookup/readdir share strict validation while accepting Linux-style - nonzero unused bytes after the final name NUL. -- Compressed `st_blocks` reflects the inode's on-disk compressed block count; - uncompressed and chunk files retain logical block rounding. -- FreeBSD 15 local vnode pager sync/async entry points are used for real mmap - faults. -- NFS export uses full 64-bit NIDs and a generation derived from the - superblock seed and inode metadata. Replacing metadata changes the generation - and makes old handles stale; a metadata-identical, payload-only replacement - is not guaranteed to return `ESTALE`. - -See `docs/features.md` for the bounded feature claim and -`tests/results/manual/2026-08-09T1804Z-final-review-independent/manual-test-report.md` -for the final-review evidence. - -## Test Fixtures - -`tests/prepare_directory_fixtures.sh` creates the deterministic TC048/141/148 -directory fixtures. `tests/prepare_error_fixtures.sh` and -`tests/erofs_fixture.py` create and self-check the structured TC002/086/087/102 -and TC112-TC116/119 fixtures. Generated trees, images, overlays, and `build/` -outputs are test artifacts and are not committed. diff --git a/build.sh b/build.sh deleted file mode 100755 index 93fc764..0000000 --- a/build.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/sh -set -eu - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" -FREEBSD_SRC="${FREEBSD_SRC:-/usr/src}" -FS_SRC="${SCRIPT_DIR}/src" -BUILD_DIR="${SCRIPT_DIR}/build" -WORK_DIR="${BUILD_DIR}/obj" -MAKE="${MAKE:-make}" -WITH_ZSTDIO="${WITH_ZSTDIO:-0}" -NM_UNDEF="" - -cleanup() { - [ -z "${NM_UNDEF}" ] || rm -f "${NM_UNDEF}" -} - -trap cleanup EXIT -trap 'cleanup; exit 1' HUP INT TERM - -fail() { - echo "ERROR: $*" >&2 - exit 1 -} - -make_kmod() { - MAKEOBJDIR="${WORK_DIR}" "${MAKE}" -C "${FS_SRC}" \ - SRCTOP="${FREEBSD_SRC}" SYSDIR="${FREEBSD_SRC}/sys" \ - WITH_ZSTDIO="${WITH_ZSTDIO}" "$@" -} - -[ "$(uname -s)" = "FreeBSD" ] || - fail "build.sh requires a native FreeBSD build host" -[ -d "${FREEBSD_SRC}/sys" ] || - fail "FreeBSD source tree not found: ${FREEBSD_SRC}" -[ -d "${FS_SRC}" ] || fail "Source directory not found: ${FS_SRC}" -command -v "${MAKE}" >/dev/null 2>&1 || fail "make is required" -command -v awk >/dev/null 2>&1 || fail "awk is required" -command -v nm >/dev/null 2>&1 || fail "nm is required" -[ -f "${FREEBSD_SRC}/sys/tools/vnode_if.awk" ] || - fail "vnode_if.awk not found in ${FREEBSD_SRC}" -[ -f "${FREEBSD_SRC}/sys/kern/vnode_if.src" ] || - fail "vnode_if.src not found in ${FREEBSD_SRC}" -case "${WITH_ZSTDIO}" in -0|1) ;; -*) fail "WITH_ZSTDIO must be 0 or 1" ;; -esac - -rm -rf "${WORK_DIR}" -mkdir -p "${WORK_DIR}" -KMOD="$(make_kmod -V PROG)" -[ -n "${KMOD}" ] || fail "src/Makefile did not define a module output" - -echo "==> Building repo22 ${KMOD}" -make_kmod all - -[ -f "${WORK_DIR}/${KMOD}" ] || fail "module was not produced" - -NM_UNDEF="$(mktemp "${WORK_DIR}/nm-undef.XXXXXX")" || - fail "could not create nm output file" -if ! nm -u "${WORK_DIR}/${KMOD}" >"${NM_UNDEF}"; then - fail "nm failed while checking ${KMOD}" -fi -if ! awk '$NF == "bcmp" { found = 1 } END { exit found }' "${NM_UNDEF}"; then - fail "${KMOD} contains an unresolved bcmp reference" -fi - -cp -f "${WORK_DIR}/${KMOD}" "${BUILD_DIR}/" -echo "==> SUCCESS: ${BUILD_DIR}/${KMOD}" diff --git a/current/README.md b/current/README.md deleted file mode 100644 index 057613c..0000000 --- a/current/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# repo22 当前状态报告索引 - -- [report-1](report-1/README.md):第一次总体进度报告,保留 - `573aaab7ee0b47cb6aed7f76c52c68dd4041326b` 创建时的原始内容。 -- [report-2](report-2/README.md):第二次总体进度报告,按 - `381349b3847209a67d669264ac54a50a3599915c` 快照汇总正式批次、动态结果、 - issue、风险和下一步。 -- [report-3](report-3/README.md):第三次总体进度报告,以 - `12351cbb0a593474f4fcef68ffc5218d9c99dd46` 为最终验证快照,汇总完成项、 - 160 PASS/1 PARTIAL、独立审查结论、剩余搁置项和后续可选计划。 - -后续总体状态报告统一使用 `current/report-N/` 目录。逐项动态证据位于 -`tests/results/manual/`,问题触发、分析和解决状态位于 `issues/`。 diff --git a/current/report-1/2026-08-09-overall-progress.md b/current/report-1/2026-08-09-overall-progress.md deleted file mode 100644 index b8a3c79..0000000 --- a/current/report-1/2026-08-09-overall-progress.md +++ /dev/null @@ -1,160 +0,0 @@ -# repo22 当前总体进度 - -更新时间:2026-08-09 UTC - -状态报告代码基线:`cd0e985b5ac54a4b7acb7042422329ad1729fb3e` - -本报告记录的是上述基线上的阶段性状态。测试结果、问题状态和统计会随后续 -提交继续更新,不能将本文视为 TC001-TC156 已完成全量回归的声明。 - -## 目标与执行范围 - -repo22 的目标是在 FreeBSD 15 amd64 上提供只读 EROFS 内核模块,并以可复现 -fixture、真实 KLD、真实 mount/read/errno 和完整清理证据验证声明的功能边界。 - -当前执行约束如下: - -- 只修改和提交 `repo-community/repo22`,fixture、VM overlay、KLD 和原始日志仅 - 放在 `/work/build` 或 guest 临时目录。 -- 不建设或使用 CI、runner、自动 PASS wrapper;每个 TC 必须逐份对照对应 - Markdown 手工执行。 -- host 侧 `mkfs.erofs`、`dump.erofs`、`fsck.erofs` 和静态源码审查只作为 fixture - 或分析证据,不能替代 FreeBSD 内核动态结果。 -- 每批测试形成独立 manual report,记录 commit、构建配置、KLD SHA256、guest - 版本、命令、可观察输出或 errno、fixture hash 和清理状态。 -- 每批完成后提交并 push 到 `xdm/main`;并行批次若使远端前进,则 fetch、rebase - 自己的 test-only 提交后正常 push,禁止 force push。 - -## 已完成实现与审查 - -当前源码已覆盖 superblock、compact/extended inode、plain/inline/chunk 数据、 -目录/namecache、xattr/ACL/metabox、LZ4/MicroLZMA/DEFLATE/ZSTD、fragment、 -multi-device、NFS export、pager 和多项 fail-closed 边界检查。功能声明的准确边界 -以 `docs/features.md` 为准。 - -主要 feature 批次包括: - -- `29a215dd`:POSIX ACL 与完整 xattr namespace 集成。 -- `545d35bd`:xattr、long prefix、shared xattr 和 metabox 边界加固。 -- `96e22cc`:cold nested namei 与目录结构校验修复。 -- `4ef680df`、`78182686`:真实 multi-device、slot mapping、flatdev 和 extent - 映射审查修复。 -- `2354b448`:NFS export、稳定句柄和相关 vnode 路径。 -- `c208bf1f`:压缩映射 P0 收口,包括多算法、partial reference 和 extent 路径。 -- `9a3604fb`:metadata/VFS 语义收口,包括 inline bounds、special `rdev`、pager、 - namei、NFS generation 和真实压缩占用统计。 - -关键收尾与工程对齐提交: - -| Commit | 已完成内容 | -|---|---| -| `c881f656` | 修复 48-bit superblock union、`OFF_MAX`、大 hole 内存和大目录索引等最终 metadata 边界;建立 TC150-TC153 与三份未决 issue。 | -| `4d51ca9e` | 停止跟踪 `build/obj`、生成头和架构 symlink,避免构建产物进入正式提交。 | -| `89594551` | 将 KLD 构建布局、源文件命名、架构门控和 `WITH_ZSTDIO` 方式与 FreeBSD/Linux 上游职责对齐。 | -| `2f2ac5a8` | 更新 README、architecture、refactoring plan/status,使构建、BSD API 差异和功能边界与源码一致。 | -| `077b37ab` | 删除会伪造覆盖或无条件 PASS 的旧 runner/wrapper,加入字段断言的 fixture transformer 与 FreeBSD probe,修订关键 TC 文档。 | -| `cd0e985b` | 完成最终 review finding:per-inode NFS generation、显式 extent 物理地址溢出检查、compact/extended timestamp 校验,并直接动态执行 TC154-TC156。 | - -上述工作也包含多轮 code review 和 style review:源文件职责、函数顺序和磁盘 ABI -尽量贴近 Linux EROFS;FreeBSD vnode、pager、NFS、GEOM 和 `dev_t` 差异采用 -FreeBSD 15 原生接口;磁盘长度、加法、移位和 signed/unsigned 转换按 fail-closed -原则处理。 - -## 已有动态验证 - -`tests/results/manual/` 中保留了各 feature 在对应历史 commit 上的真实 FreeBSD 15 -动态证据。它们证明对应变更批次曾执行内核路径,但不能替代当前 final source 的 -TC001-TC156 全量重跑。 - -| 批次 | 主要动态证据 | 当前解释 | -|---|---|---| -| 压缩 | compact/full LZ4、legacy full index、partial reference、MicroLZMA、DEFLATE、ZSTD、fragment、ztailpacking 和多种边界 source/hash compare | 历史 feature 证据存在;G5/G6 尚需在本轮 exact KLD 重跑。 | -| xattr/ACL/metabox | inline/shared/long-prefix xattr、ACL、metabox carrier 和 corruption errno | 历史 feature 证据存在;G4 尚未启动。 | -| directory/namei/pager | cold nested lookup、目录 padding、special `rdev`、真实 mmap/page fault 和 inline bounds | 历史 feature 证据存在;G1/G3 将按当前文档重跑。 | -| multi-device | 外部 provider、显式 slot、flatdev、unified address、fragment 和 orphan/error 路径 | 历史 feature 证据存在;G7 尚未启动。 | -| NFS | getfh/fhopen、同镜像重挂、同 md 替换、ESTALE、background/stress cleanup | 历史 feature 证据存在;G8 尚未启动。 | -| final review | TC150-TC152 动态 PASS;TC153 明确 SHELVED | 结果来自 `c881f656` 前后的 final-review 批次,不是 156 项全量 PASS。 | - -当前 final source `cd0e985b` 的直接动态证据是 -`tests/results/manual/2026-08-09T0557Z-review-fixes/manual-test-report.md`: - -- FreeBSD 15.0-RELEASE-p8 amd64 上 `WITH_ZSTDIO=0` KLD SHA256 为 - `d6e755c7a75dd5043b603d58215ae3e7c8f29318b31dc1885aa9bb070d6e57a5`。 -- `WITH_ZSTDIO=1` KLD SHA256 为 - `b442f9f05af1d1a76801690c83c9a40ec6fc739adb0663e7dacaf837eeb2ecf3`。 -- TC154、TC155、TC156 均在该 final source 上直接动态 PASS,分别覆盖 per-inode - NFS generation、explicit extent `pa + plen` overflow 和 inode timestamp - validation。 -- 该批结束时 EROFS mount、KLD 和 md provider 均已清理。 - -## 本轮八组全量回归 - -TC 编号范围为 TC001-TC156,共 156 项。以下集合经计数检查为总数 156、重复 0、 -遗漏 0。 - -| 组 | 精确集合 | 数量 | 当前状态 | -|---|---|---:|---| -| G1 | TC001, TC007, TC009, TC011-TC014, TC019-TC040, TC147, TC150, TC151 | 32 | 已启动;正在修订文档和确定性 fixture,尚未形成 final-source 32 项结论。 | -| G2 | TC002, TC008, TC010, TC015-TC018, TC112-TC116, TC119 | 13 | 已启动;使用独立 worktree/VM,尚未形成 13 项结论。 | -| G3 | TC041-TC066, TC141, TC148, TC149, TC153 | 30 | 未启动。 | -| G4 | TC005, TC067-TC083, TC117, TC134-TC140, TC142 | 27 | 未启动。 | -| G5 | TC003, TC084-TC092 | 10 | 未启动。 | -| G6 | TC004, TC102-TC110, TC143-TC146, TC155 | 15 | 未启动。 | -| G7 | TC006, TC093-TC101, TC118 | 11 | 未启动。 | -| G8 | TC111, TC120-TC133, TC152, TC154, TC156 | 18 | 未启动。 | - -G1/G2 的“已启动”只表示执行代理、工作目录和任务集合已经建立,不表示其中任何 -TC 已在本轮计为 PASS。G8 中 TC154/TC156、G6 中 TC155 虽已有 `cd0e985b` 直接 -证据,仍需由对应全量组在最终统计中明确引用或按组要求重跑,不能导致重复计数。 - -## 已完成与未完成 - -已完成: - -- 核心 feature 实现、针对性 code review、FreeBSD 15 API 对齐和构建布局收口。 -- 多个历史 feature 批次的真实 KLD 动态验证与清理记录。 -- 当前 final source 的双配置构建和 TC154-TC156 直接动态验证。 -- 移除旧 CI 风格 runner/wrapper,建立“逐 Markdown 手工执行”的验收规则。 -- 三项已知验证缺口均有详细 issue,没有把未执行或环境受限结果标成 PASS。 - -未完成: - -- TC001-TC156 在同一 final-source 基线族上的八组全量手工回归与最终汇总。 -- G1/G2 的文档修订、fixture 固化、exact KLD 执行、逐 TC 报告和 push。 -- G3-G8 的启动、独立环境分配、执行和结果提交。 -- TC010 所需 16 TiB 级合格 provider、TC153 大目录内核路径、explicit extent - mapped compressed payload 的正向 fixture。 - -## Issue 索引 - -检查时 `issues/` 只有以下三份已跟踪文档,没有未提交 issue 文件或真实 issue -改动,因此本状态提交不修改 `issues/`。 - -| Issue | 状态 | 影响 feature | Shelved 原因 | -|---|---|---|---| -| `issues/TC010-48bit-statfs-large-provider.md` | SHELVED - environment unavailable | 48-bit block count、primary provider size validation、`statfs(2)`/`df` 总量和 root/block union 选择 | 非零 `blocks_hi` 在 4 KiB block 下至少要求 16 TiB GEOM provider;当前 vnode md 流程没有已验证、可安全清理的合格 provider。小 provider 拒绝和 TC150 fallback 都不能替代正向结果。 | -| `issues/TC153-large-directory-block-index-validation.md` | SHELVED - kernel validation incomplete | 超大目录二分索引、cold lookup、`EINTEGRITY` 传播和 negative namecache 正确性 | 已有可复现 FLAT_PLAIN fixture,但尚未在 FreeBSD fixed/pre-fix KLD 上完成冷查找对照;host 生成和静态审查不足以计 PASS。 | -| `issues/extent-metadata-fixture-unavailable.md` | SHELVED - positive fixture unavailable | explicit compressed extent parser、4/8/16/32-byte record、物理/逻辑高位、binary search、partial reference/fragment 映射 | erofs-utils 1.8.6 和已试 1.9.3 均不生成真实 mapped explicit-record payload。TC152 hole 与 TC155 overflow 只覆盖部分 header/negative 分支,不能替代正向压缩数据读取。 | - -## 风险与依赖 - -- 并行组共享远端 `xdm/main`,每批 push 前必须重新 fetch/rebase 并确认没有覆盖其他 - 组的 TC、helper、report 或 issue。 -- FreeBSD VM、SSH 端口、qcow overlay、md unit 和 mount point 必须按组隔离;任何 - guest 残留都会污染后续动态证据。 -- erofs-utils 的格式生成能力限制 explicit extent 等正向 fixture,必要时只能使用 - 字段自检、CRC32C-aware 的 structured transformer,不能手工猜偏移。 -- 48-bit 大 provider、memory pressure、NFS stress 和性能 TC 对环境容量及稳定性要求 - 较高,环境不足时必须使用 `SHELVED/ISSUE` 或 `ENVIRONMENT-UNAVAILABLE`,不能 - 降低判据。 -- 历史报告跨多个 commit。最终统计必须只接受对应 exact KLD 的本轮动态结果,或 - 明确标注尚未重跑,禁止把旧 HEAD 结果直接升级为全量 PASS。 - -## 下一步 - -1. 完成 G1/G2 文档和 fixture 修订,构建各自 exact KLD,逐 TC 手工执行并提交报告。 -2. 每个先完成的组 fetch/rebase 最新 `xdm/main`,严格 pathspec 提交并正常 push。 -3. 启动 G3/G4,随后按环境和依赖启动 G5-G8;压缩、multi-device、NFS 使用隔离 VM。 -4. 对任何 kernel behavior failure 立即停止 PASS 计数,新增或更新有事实依据的 issue。 -5. 八组结束后汇总 156 项,检查总数、重复、遗漏、KLD/image/source hash 和全部清理 - 证据,再发布 final-source 全量结论。 diff --git a/current/report-1/README.md b/current/report-1/README.md deleted file mode 100644 index aea19af..0000000 --- a/current/report-1/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# repo22 当前状态索引 - -- [2026-08-09 总体进度](2026-08-09-overall-progress.md):基线 - `cd0e985b5ac54a4b7acb7042422329ad1729fb3e` 上的实现、历史动态证据、八组 - 全量回归分工、未决 issue、风险和下一步。 - -此目录记录阶段性状态;测试结果会随后续 commit 更新。正式逐项证据位于 -`tests/results/manual/`,未决验证缺口位于 `issues/`。 diff --git a/current/report-2/2026-08-09-overall-progress.md b/current/report-2/2026-08-09-overall-progress.md deleted file mode 100644 index 3cc51a4..0000000 --- a/current/report-2/2026-08-09-overall-progress.md +++ /dev/null @@ -1,123 +0,0 @@ -# repo22 第二次总体进度报告 - -更新时间:2026-08-09 UTC - -权威统计快照:`381349b3847209a67d669264ac54a50a3599915c` -(该提交是当次 `xdm/main` 上完成 G3 报告后的快照)。 - -本报告只统计该快照已经提交的实现、正式批次和动态证据。TC060 源码修复与 G5 -压缩回归正在并行进行,但在形成完整报告并提交前不提前计为 PASS,也不改变下述 -冻结统计。 - -## 总体结论 - -- feature 实现、源码 code review、style review、FreeBSD 15 API 对齐和双配置构建 - 布局已经完成阶段性收口。 -- 正式八组回归中,G1、G2、G3、G4 共执行 103 项:102 PASS,1 项 - `TC060 KERNEL-FAIL` 正在修复。 -- final review-fix 阶段还直接动态执行了 TC154-TC156,三项均 PASS。它们是三项 - 额外且不与前述 103 项重叠的 TC,因此当前共有 106 个不同 TC 具备动态结果, - 其中 105 PASS、1 项待修。 -- TC154-TC156 已分配给 G8,但先行 PASS 不能重复计入“已完成 G8”,也不能把 G8 - 标成已执行完毕。G8 尚有 TC111、TC131-TC133 未执行。 -- TC010 与 TC153 已通过真实大 sparse provider 的 FreeBSD 内核路径验证并转为 - RESOLVED。explicit extent 的 mapped compressed payload 正向 fixture 仍未获得, - 对应 issue 尚未解决。 - -## 实现与审查 - -repo22 已完成以下阶段性工程工作: - -- 实现并审查 superblock、compact/extended inode、plain/inline/chunk 数据、目录与 - namecache、xattr/ACL/metabox、多算法压缩、fragment、multi-device、NFS export、 - vnode pager 和 fail-closed 元数据边界。 -- 对照 Linux EROFS 的磁盘 ABI、职责拆分和命名以降低长期维护偏差,同时保持 - FreeBSD vnode、GEOM、pager、NFS、ACL 和 `dev_t` 语义为行为权威。 -- 完成 overflow、长度、偏移、移位、signed/unsigned、目录大索引、48-bit union、 - NFS generation、special vnode 和 timestamp 等多轮独立 code review 修复。 -- 清理被跟踪的构建产物,统一 FreeBSD KLD 源文件布局、amd64 门控和 - `WITH_ZSTDIO=0/1` 构建方式;当前构建仍以 kernel `-Werror` 为门槛。 -- 删除会替代人工判定或无条件给出 PASS 的旧 CI 风格 runner/wrapper,保留可复现 - fixture transformer 与原生 syscall/probe 工具。当前任务不实现 CI。 - -## 正式八组状态 - -八组精确集合总计 156 项,彼此无重复、无遗漏。 - -| 组 | 精确集合 | 数量 | 快照状态 | -|---|---|---:|---| -| G1 | TC001, TC007, TC009, TC011-TC014, TC019-TC040, TC147, TC150, TC151 | 32 | 32 PASS,正式批次完成。 | -| G2 | TC002, TC008, TC010, TC015-TC018, TC112-TC116, TC119 | 13 | 13 PASS,正式批次完成。 | -| G3 | TC041-TC066, TC141, TC148, TC149, TC152, TC153 | 31 | 31 已执行;30 PASS,TC060 KERNEL-FAIL。 | -| G4 | TC005, TC067-TC083, TC117, TC134-TC140, TC142 | 27 | 27 PASS,正式批次完成。 | -| G5 | TC003, TC004, TC084-TC092, TC102-TC110, TC143-TC146 | 24 | 压缩回归正在进行,尚无正式整组结论。 | -| G6 | TC006, TC093-TC101, TC118 | 11 | 尚未执行。 | -| G7 | TC120-TC130 | 11 | 尚未执行。 | -| G8 | TC111, TC131-TC133, TC154-TC156 | 7 | 正式整组未完成;TC154-TC156 已先行 PASS,TC111、TC131-TC133 尚未执行。 | - -正式批次证据: - -- [G1 报告](../../tests/results/manual/2026-08-09T0710Z-g1/manual-test-report.md) -- [G2 报告](../../tests/results/manual/2026-08-09T0710Z-g2/manual-test-report.md) -- [G3 报告](../../tests/results/manual/2026-08-09T1059Z-g3/manual-test-report.md) -- [G4 报告](../../tests/results/manual/2026-08-09T0839Z-g4/manual-test-report.md) -- [review-fix 报告](../../tests/results/manual/2026-08-09T0557Z-review-fixes/manual-test-report.md) - -## 统计口径 - -正式批次已执行数为 `32 + 13 + 31 + 27 = 103`;PASS 数为 -`32 + 13 + 30 + 27 = 102`,另有 TC060 一项 KERNEL-FAIL。 - -TC154-TC156 来自 review-fix 阶段的 exact-source FreeBSD 动态运行,并未包含在 -G1-G4 的 103 项中。因此按“不同 TC 是否已有动态结果”计数,应增加三项,得到 -106 个 TC 有动态结果、105 PASS、1 待修。按“正式组是否完成”计数时,三项仍属于 -尚未完成的 G8,不能再次增加 PASS 数或宣称 G8 已完成。 - -## 大 Provider 验证 - -- TC010 使用逻辑长度 `17592193667072` 字节的 qualified sparse vnode provider, - 动态挂载了 `blocks_hi=1` 的 48-bit EROFS,并验证 `df` 的 64-bit 总量无截断。 - issue 已 RESOLVED。 -- TC153 使用逻辑长度 `8796093091840` 字节的 sparse GEOM provider,令超大 - FLAT_PLAIN 目录进入 `2147483648` 最终块索引的真实 kernel lookup 路径;固定 KLD - 在 cold lookup 和 readdir 后均返回 `EINTEGRITY`,没有错误缓存为 `ENOENT`。 - issue 已 RESOLVED。 -- explicit extent 已动态覆盖 hole record、header 选择和部分负向分支,但仍缺少 - mapped compressed payload 以及完整 record variant 的正向读取证据,不能关闭 issue。 - -## Issue 索引 - -| Issue | 当前状态 | 结论 | -|---|---|---| -| [TC060 standard pathconf](../../issues/TC060-pathconf-standard-values.md) | OPEN - KERNEL-FAIL | `_PC_NO_TRUNC` 与 `_PC_CHOWN_RESTRICTED` 返回 `EINVAL`;源码修复和复测正在进行。 | -| [TC010 48-bit statfs](../../issues/TC010-48bit-statfs-large-provider.md) | RESOLVED - qualified sparse vnode provider validated | qualified 16 TiB 级 sparse provider 已完成真实 mount/statfs/df 验证。 | -| [TC153 large directory index](../../issues/TC153-large-directory-block-index-validation.md) | RESOLVED - exact-source kernel validation passed | qualified 多 TiB sparse provider 已完成 exact-source 大索引 kernel lookup 验证。 | -| [Explicit extent fixture](../../issues/extent-metadata-fixture-unavailable.md) | SHELVED - positive fixture unavailable | issue 未解决;尚无使用 explicit-record parser 的 mapped compressed payload 正向 fixture。 | - -## 当前风险 - -- TC060 是已确认的 FreeBSD kernel behavior failure,修复必须保持 NAME_MAX、PATH_MAX、 - FILESIZEBITS、LINK_MAX、ACL 查询和未知键 errno 不回归,并完成 mount/md/KLD 清理。 -- G5 同时覆盖 LZ4、MicroLZMA、DEFLATE、ZSTD、ztailpacking、partial reference、 - explicit metadata 等多条压缩路径,fixture 资格和 `WITH_ZSTDIO` 配置差异仍是当前 - 最大执行风险。 -- explicit extent 缺少上游工具可生成的 mapped positive fixture;静态 ABI 对照、 - hole record 和 malformed case 不能替代真实压缩 payload 读取。 -- G6 multi-device、G7 边界与压力类测试、G8 NFS/manual 页面仍需隔离 VM、精确 KLD、 - direct syscall/数据比较和完整清理,历史 feature 报告不能自动升级为本轮结果。 -- 并行 worktree 共享 `xdm/main`。每次 push 前必须 fetch/rebase,严格 pathspec,禁止 - force push;guest mount、md、KLD 和临时 provider 残留必须清零。 - -## 下一步 - -1. 完成 TC060 根因修复,重跑全部 pathconf 键与基础 mount/read smoke,更新 issue - 和 dated manual report。 -2. 完成 G5 的 24 项压缩回归,提交 exact KLD、fixture hash、errno/数据比较与清理 - 证据。 -3. 依次完成 G6 11 项、G7 11 项和 G8 剩余 TC111、TC131-TC133;在 G8 汇总中引用 - 或按要求复跑 TC154-TC156,但禁止重复计数。 -4. 聚合 `TEST-COVERAGE-MATRIX`,审计 156 项总数、组间重复、遗漏、动态证据来源和 - 未解决 issue。 -5. 完成最终独立 source review,检查 lock/resource/error path;执行 - `WITH_ZSTDIO=0/1` 双配置 exact-source build,并做 `xdm/main`、remote/HEAD、 - tracked clean 和 guest 清理的最终 audit。 diff --git a/current/report-2/README.md b/current/report-2/README.md deleted file mode 100644 index d9ac135..0000000 --- a/current/report-2/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# repo22 第二次状态报告索引 - -- [第二次总体进度报告](2026-08-09-overall-progress.md):以 - `381349b3847209a67d669264ac54a50a3599915c` 为统计快照,汇总实现与审查、 - 八组回归、动态结果计数、issue、风险和后续工作。 - -本目录只记录阶段性总体状态。各 TC 的命令、hash、errno、dmesg 和清理证据以 -`tests/results/manual/` 中的对应报告为准。 diff --git a/current/report-3/2026-08-09-overall-progress.md b/current/report-3/2026-08-09-overall-progress.md deleted file mode 100644 index c7ee7d6..0000000 --- a/current/report-3/2026-08-09-overall-progress.md +++ /dev/null @@ -1,124 +0,0 @@ -# repo22 第三次总体进度报告 - -更新时间:2026-08-09 UTC - -权威验证快照:`12351cbb0a593474f4fcef68ffc5218d9c99dd46` - -最终源码基线:`fcc85b93d5f8fd9671686bd68bf3b086c8bd25cf` - -## 总体结论 - -本轮要求的 feature 全面验证、FreeBSD/Linux 行为差异审查、code review 和 -style/可维护性优化已经完成。161 个可执行 Markdown 测试均已执行并形成受控 -手工证据,最终为 160 PASS、1 PARTIAL、0 FAIL、0 KERNEL-FAIL、0 ENV。 - -唯一未完全关闭的是 TC146 的 explicit compressed extent mapped-payload 正向 -fixture。该问题已按用户授权详细记录并搁置,不是已观察到的 kernel failure, -也不影响其他 160 个 TC 的结论。本轮目标因此完成,剩余工作属于已批准搁置项 -或后续可选增强。 - -CI 和自动化 kernel-test harness 不在本轮范围内,未实现也未宣称完成。 - -## 已完成内容 - -- 完成 TC001-TC161 的规格、执行报告和覆盖矩阵机械审计;TC000 只作为模板。 -- G1-G8 的 156 个表格行覆盖 TC001-TC156,恰好一次,无重复、无遗漏。 -- 完成 TC157-TC161 独立回归,覆盖 explicit extent 全局顺序、48-bit compressed - block count、special vnode 组合 setattr、dot-omitted `OFF_MAX` cookie 和 `nm` - 失败传播。 -- TC010 的 16 TiB-plus statfs、TC060 的 FreeBSD pathconf、TC153 的超大目录索引 - 均已动态验证并转为 RESOLVED。 -- 完成 superblock、inode、data、directory/namecache、xattr/ACL/metabox、压缩、 - multi-device、NFS、pager、错误路径和边界算术的多轮独立 review。 -- 保留 Linux EROFS 的磁盘 ABI、命名、函数顺序和职责拆分作为维护参照,同时以 - FreeBSD vnode、GEOM、pager、NFS、ACL、`dev_t` 和 errno 语义为行为权威。 -- 修正 README 的 NFS stale 承诺:generation 来自 superblock seed 和 inode - metadata;metadata-identical payload-only replacement 不保证 `ESTALE`。 -- 清理 repo22 内全部 ignored build/object/image/repro/Python bytecode 产物;最终 - guest 的 EROFS mount、md、EROFS KLD 和 DTrace KLD 计数均为零。 - -## 最终验证统计 - -| 状态 | 数量 | 说明 | -| --- | ---: | --- | -| PASS | 160 | 具备对应 dated manual report 的 FreeBSD 15 手工证据 | -| PARTIAL | 1 | TC146;HEAD2/interlaced PASS,explicit mapped payload 未完成 | -| FAIL / KERNEL-FAIL | 0 | 无开放 kernel 行为失败 | -| ENV | 0 | 无环境阻塞 | -| SHELVED test case | 0 | TC146 的子项不另计为 TC | - -G1-G8 证据来自多个源码提交,不能表述为所有旧 TC 都运行在最终 KLD 上。最终 -`fcc85b93d` 基线接受了 TC157-TC161、相关回归以及双配置 build/load/mount smoke。 - -## 最终构建 - -FreeBSD 15 kernel `-Werror` 双配置均成功: - -| 配置 | KLD SHA256 | -| --- | --- | -| `WITH_ZSTDIO=0` | `15fda9d334132cd81769ce4dff4f8411a6e2b4cf7531c352530d0de85f42a2d2` | -| `WITH_ZSTDIO=1` | `23782dc0ce7da188807d35020bf2d8c6044b796c5c9a8746b398ba784cd6ad4e` | - -TC161 的私有 `nm` shim 正确使构建失败且不发布新 KLD;移除 shim 后使用 -`/usr/bin/nm` 的正常重建 exit 0。两个最终 KLD 均完成独立加载和只读挂载 smoke。 - -## Code Review 结论 - -最终独立复核未发现 P0/P1。已修复的最后一批问题包括: - -- 16/32-byte explicit extent table 全局严格顺序验证; -- extended compressed inode 的完整 48-bit `blocks_hi` 解码; -- special vnode size 与其他 setattr 字段组合时的 false success; -- dot-omitted 目录在 `OFF_MAX` 的 cookie/offset overflow; -- `build.sh` 对 `nm` 自身失败的可靠传播; -- NFS generation 文档与真实实现边界不一致。 - -从社区第三方维护者视角,未发现仍需消除的非必要 Linux/FreeBSD 结构、命名或 -职责差异。保留的差异均来自 FreeBSD kernel API、锁、provider、pager、NFS 或 -权限模型要求。 - -## 未完成与搁置 - -唯一批准搁置项是 -[explicit mapped extent payload](../../issues/extent-metadata-fixture-unavailable.md): - -- erofs-utils 1.8.6 不能生成或独立验证该新格式; -- 已尝试工具能力扫描、`--max-extent-bytes`、structured conversion、ABI/control - flow 对照以及多类负向 fixture; -- TC157 已证明 16/32-byte unordered table fail closed,但不能替代 mapped payload - 的 source-identical 正向读取; -- 首个非零 `lstart` 和极大 extent-count 性能边界也保留为可选覆盖风险。 - -## 后续执行计划 - -1. 等待可生成并独立验证 explicit mapped-payload 的上游工具或可信 fixture。 -2. 获得 fixture 后补充 4/8/16/32-byte 正向 record、partial reference、high words、 - binary-search transition 和 final fragment 动态覆盖,使 TC146 从 PARTIAL 转 PASS。 -3. 如后续项目另行启动 CI,再基于现有 Markdown 断言设计自动化;不得把当前手工 - PASS 记录直接转换成无条件自动 PASS。 -4. 后续源码变更继续执行受影响 TC、双配置 build、module smoke、scoped Git hygiene - 和独立 review,不要求无差别重跑与改动无关的全部历史环境。 - -## 相关提交 - -正式八组手工验证: - -- G1 `9ae22009f23a65320730072a780998e80aa9b728` -- G2 `aed7b68b79a5bd4115913f8361821c86fd58d2e2` -- G3 `381349b3847209a67d669264ac54a50a3599915c` -- G4 `f383bbbbff301a6bde18894f03ab88a8c0cc885a` -- G5 `c566d8ac6bf8e801082bbccf108451f6ee46ad40` -- G6 `f11fff5b8e8050e1017ed86f0bcf71042b2b45aa` -- G7 `f3ab2096fea3942317deb83fa051c84d3c124ec2` -- G8 `6f336d0a7387c8f110b19abad6e96f3ea17dba2e` - -最终源码与验证收口: - -- `f27b524a35b1c960be7e81884f9b928d9ac907e9`:metadata boundary 修复。 -- `67d3c057fbcad5adc765ab9927da511e3221ac8f`:explicit extent header 解码修复。 -- `fcc85b93d5f8fd9671686bd68bf3b086c8bd25cf`:TC157-TC161 规格与 helper。 -- `12351cbb0a593474f4fcef68ffc5218d9c99dd46`:最终手工报告、覆盖、issue 和 README 汇总。 - -逐项证据见 -`tests/results/manual/2026-08-09T1804Z-final-review-independent/manual-test-report.md`; -最终统计口径见 `docs/TEST_REPORT.md` 和 `tests/TEST-COVERAGE-MATRIX.md`。 diff --git a/current/report-3/README.md b/current/report-3/README.md deleted file mode 100644 index 0bc546d..0000000 --- a/current/report-3/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# repo22 第三次状态报告索引 - -- [第三次总体进度报告](2026-08-09-overall-progress.md):以 - `12351cbb0a593474f4fcef68ffc5218d9c99dd46` 为最终验证快照,汇总已完成 - feature 验证、code/style review、最终构建、剩余风险和后续计划。 - -本目录只记录本轮收尾状态。逐项命令、hash、errno 和清理证据以 -`tests/results/manual/` 为准,issue 的触发、分析、尝试和验收条件以 -`issues/` 为准。report-1 和 report-2 保留为历史快照,不因本报告改写。 diff --git a/current/report-4/2026-08-10-task2-code-review-style-assessment.md b/current/report-4/2026-08-10-task2-code-review-style-assessment.md deleted file mode 100644 index 344512b..0000000 --- a/current/report-4/2026-08-10-task2-code-review-style-assessment.md +++ /dev/null @@ -1,726 +0,0 @@ -# repo22 任务 2:全面代码审查与风格优化评估 - -日期:2026-08-10 - -性质:只读评估,不含源码修改、测试实现或 CI 变更 - -范围:原任务中的“2. 进行全面的 code review 和 style 优化” - -## 1. 执行结论 - -本轮以第三方社区维护者视角,对 repo22 的 FreeBSD EROFS 实现进行了新的严格 -静态审查。审查逐条回应原任务 2 的三个目标: - -1. 判断实现是否合理复用 FreeBSD kernel 已有能力,是否存在不必要的重写,或 - 反过来存在不应强行引用内核实现的场景。 -2. 判断 BSD 与 Linux 的行为差异是否被正确识别,避免把 Linux 共同缺陷误记为 - BSD 移植差异,也避免为追求外观一致而破坏 FreeBSD VFS、GEOM 和 errno 契约。 -3. 判断两个独立仓库在文件职责、函数和变量命名、定义顺序、磁盘 ABI、注释和 - style 上是否已经消除非必要差异。 - -结论是:repo22 的 FreeBSD API 集成和核心算法映射已有良好基础,但任务 2 尚不 -能认定为全面完成。本轮确认: - -| 等级 | 数量 | 结论 | -| --- | ---: | --- | -| P0 | 0 | 未发现当前静态证据可确认的立即阻断问题 | -| P1 | 2 | vnode 生命周期 UAF;未压缩 physical run/chunk 大读的巨型分配与 `uiomove()` 长度截断 | -| P2 | 4 | 目录排序、根类型、compact 加法回绕、explicit extent 首次全表扫描 | -| P3 | 6 | errno、时间、symlink、48-bit 边界、setattr、FRAGMENTS 注释/死分支 | - -这些新 finding 均未在本轮动态复现,也未修复。本报告给出的测试均是后续建议, -不是已执行结果。CI 明确不在本轮范围。 - -任务 1 已完成的 feature 验证和手工测试记录不在本报告中重写。静态审查发现的 -新风险也不应被误读为已推翻全部既有功能证据;它们需要后续按优先级复现、修复 -并回归。 - -## 2. 审计方法与限制 - -### 2.1 方法 - -本轮采用以下互相独立的证据层: - -- **源码静态比对**:逐文件映射 repo22 与 Linux 7.1.0-rc1 `fs/erofs`,比较磁盘 - ABI、核心算法、函数职责、函数顺序和命名。 -- **FreeBSD 契约核对**:直接检查 FreeBSD 15.0-RELEASE-p9 中 vnode、VFS、GEOM、 - `uio`、pager、ACL、extattr、XZ、ZSTD 和参考文件系统实现。 -- **历史检查**:确认 repo22 最终源码提交、Linux 参考导入提交、tree hash、历史 - 测试入口和 UDF helper 的可追溯程度。 -- **机械检查**:执行 FreeBSD `checkstyle9.pl`,检查文件树、共同编译单元顺序、 - 死声明、硬编码旧目录和许可证标识。 -- **独立复核**:对高风险 vnode 生命周期、目录排序、根类型、只读 `setattr`、 - 许可证措辞、UDF 来源措辞、旧测试入口和 style 统计进行了二次静态复核。 - -### 2.2 限制 - -- 本轮没有启动新的 FreeBSD 动态测试批次。 -- 新 finding 尚未通过 fault injection、损坏镜像、并发卸载或大 I/O 实测。 -- 性能风险只根据分配和扫描路径分析,未给出吞吐、延迟或内存峰值数据。 -- 许可证部分只陈述仓库可观察事实,不作法律结论。 -- Linux 与 FreeBSD 是独立仓库;本报告只要求维护者可识别的相似性,不要求共享 - Git 历史、构建系统或平台抽象。 -- `checkstyle9.pl` 自身标记部分规则为 experimental,因此输出是审查输入,不能 - 自动等同为每一项都必须修改。 - -## 3. 可复现基线 - -### 3.1 BSD 目标 - -- repo22 HEAD:`76cfb553e0951b428f9d426933a10d1fe18d2a0e` -- 最终源码变更提交:`67d3c057fbcad5adc765ab9927da511e3221ac8f` -- 当前 `src/` tree:`a605af0f01f83d67c199005e775e1a47a1610036` -- 从 `67d3c057f` 到本报告基线 HEAD,`src/` tree 未再变化。 - -### 3.2 Linux 独立参考 - -- 精简参考:`/work/dev-src-linux/fs/erofs` -- 版本:Linux 7.1.0-rc1 -- 本地导入提交:`8be2be573d2b191c83d760b65c554f78eb95893c` -- EROFS tree:`b79b9a8b62f633e887a8b99075ff9412fabd7f83` -- `/work/linux-src/fs/erofs` 与精简参考内容及 tree hash 相同。 - -该 Linux 树只作为独立源码参考,不要求与 repo22 共仓。当前 `/work` 开发环境内 -可以用提交和 tree hash 验证其身份;只获得独立 repo22 的社区维护者,则无法从 -现有 repo22 文档唯一重建该基线,详见第 10 节。 - -### 3.3 FreeBSD API 参考 - -- 路径:`/work/dev-freebsd-releng` -- 版本:FreeBSD 15.0-RELEASE-p9 -- 重点契约:`insmntque()`、`uiomove()`、`vfs_read_dirent()`、GEOM VFS、vnode pager、 - POSIX.1e ACL、extattr、XZ Embedded、ZSTDIO 和参考文件系统。 - -### 3.4 提交卫生 - -创建 report-4 前,repo22 scoped tracked、staged、untracked 状态均为 0,且 -`HEAD == xdm/main`。本次提交范围限定为 `current/report-4/`,不会修改既有源码、 -测试、文档、issues、report-1 至 report-3 或 `current/README.md`。 - -## 4. 文件职责映射 - -### 4.1 共同或语义对应文件 - -| repo22 | Linux `fs/erofs` | 维护者视角结论 | -| --- | --- | --- | -| `src/erofs_fs.h` | `erofs_fs.h` | 磁盘 ABI 应最严格对齐;字段布局正确性优先于平台风格 | -| `src/internal.h` | `internal.h`, `compress.h` | 内存结构和接口语义对应,FreeBSD 类型必然不同 | -| `src/super.c` | `super.c` | superblock、设备表、mount、statfs、生命周期 | -| `src/inode.c` | `inode.c` | inode 解码;Linux inode ops 由 BSD vnops 分担 | -| `src/data.c` | `data.c` | block/chunk mapping 对应;folio/iomap 改为 buffer/GEOM/uio | -| `src/dir.c` | `dir.c` | 目录结构解析对应;cookie 和 dirent 输出是 BSD 适配 | -| `src/namei.c` | `namei.c` | 两级二分搜索语义高度对应;namecache/锁为 BSD 适配 | -| `src/xattr.c` | `xattr.c` | 磁盘 xattr 解析对应;namespace、ACL、extattr API 不同 | -| `src/xattr.h` | `xattr.h` | 只应保留 FreeBSD 实际接口,当前仍有 Linux 残片 | -| `src/decompressor.c` | `decompressor.c` | dispatcher 对应;配置职责仍有非必要差异 | -| `src/decompressor_lzma.c` | `decompressor_lzma.c` | 算法对应;FreeBSD 侧私有嵌入 XZ Embedded | -| `src/decompressor_deflate.c` | `decompressor_deflate.c` | 直接调用 FreeBSD zlib,方向正确 | -| `src/decompressor_zstd.c` | `decompressor_zstd.c` | 使用内核 ZSTDIO,方向正确 | -| `src/zmap.c` | `zmap.c` | 压缩映射核心最接近 Linux,函数顺序对齐质量高 | -| `src/zdata.c` | `zdata.c` | 职责对应;同步读取与 Linux folio/pcluster pipeline 必然不同 | -| `src/Makefile` | `Makefile`, `Kconfig` | 只比较编译单元职责和顺序,不比较构建语法 | - -### 4.2 Linux-only 文件,不应为了外观模拟 - -| Linux-only 文件 | 不在 repo22 创建空文件的理由 | -| --- | --- | -| `sysfs.c` | FreeBSD 无 Linux sysfs 模型,mount/sysctl 语义需独立设计 | -| `fileio.c` | Linux file-backed I/O 路径与当前 GEOM provider 模型不同 | -| `fscache.c` | Linux fscache/netfs 子系统在 FreeBSD 无直接对应 | -| `ishare.c` | Linux inode/xattr sharing 生命周期依赖其 VFS 内部模型 | -| `zutil.c` | Linux folio、bio、pcluster 工具层不适用于同步 BSD 读取路径 | -| `decompressor_crypto.c` | Linux Crypto API 后端在当前 FreeBSD 目标中无对应承诺 | -| `compress.h` | Linux 压缩调度、folio 和 stream 数据结构不能机械复制 | - -缺少这些文件是必要平台差异,不是结构欠缺。为了目录看起来相同而新增空壳,会 -制造虚假维护入口。 - -### 4.3 BSD-only 文件 - -| BSD-only 文件 | 必要性结论 | -| --- | --- | -| `src/erofs_vnops.c` | 必要。集中实现 VOP、pager、ACL、NFS file handle 和只读语义 | -| `src/lz4.c` | 必要。FreeBSD/OpenZFS LZ4 接口不匹配 EROFS raw block 和 partial 输出 | -| `src/erofs_defs.h` | 文件本身可存在,但当前含部分仅定义未使用的宏,必要性需缩减 | - -## 5. Findings First - -以下只列入已经过独立静态复核、或任务指定为已复核的结论。每项明确其类别, -避免把 Linux 共同问题写成 BSD 行为差异。 - -### 5.1 P0 - -无。 - -### 5.2 P1-1:`insmntque()` 失败后继续访问已回收 vnode - -- **位置**:`src/inode.c:452` 调用 `insmntque()`;失败分支在 - `src/inode.c:453-457` 释放 `en` 后又于 `src/inode.c:455` 写 - `vp->v_data = NULL`。 -- **FreeBSD 对照**:`sys/kern/vfs_subr.c:2303-2315` 在插入失败时清空 - `v_data`、切换 dead ops、执行 `vgone()` 和 `vput()`; - `sys/kern/vfs_subr.c:2328-2341` 明确说明 `insmntque()` 会在失败时回收 vnode, - 只有 `insmntque1()` 把清理留给调用者。 -- **参考实现**:`sys/fs/cd9660/cd9660_vfsops.c:717-721` 失败后只释放私有 - inode,不再解引用 vnode。 -- **Linux 对照**:Linux `fs/erofs/inode.c` 使用 `iget5_locked()` / - `iget_failed()` 管理不同的 inode 生命周期,不能机械复制,但同样要求失败路径 - 不访问已交还对象。 -- **触发与影响**:vnode 构造与强制或并发卸载竞争,进入 - `MNTK_UNMOUNT`/`MNTK_UNMOUNTF` 失败路径。后续写可能成为释放后访问,并可能 - 清空已经复用 vnode 的 `v_data`,属于内核内存安全风险。 -- **类别**:BSD 独有缺陷,源于 FreeBSD vnode 所有权契约。 -- **修复方向**:采用 cd9660 所示所有权模式,失败后释放私有 `en`、清空输出并 - 返回,不再访问 `vp`。是否改用 `insmntque1()` 必须有明确的完整清理理由。 -- **建议 Markdown 手工测试**:新增并发冷 lookup/getfh 与 `umount -f` 测试, - 配合 vnode 分配压力;在 INVARIANTS/WITNESS 内核下记录 panic、WITNESS、引用 - 计数和清理状态。建议增加仅测试用故障注入以确定性触发插入失败。 -- **状态**:已静态复核;未动态复现;未修复。 - -### 5.3 P1-2:一般未压缩 physical run/chunk 大读可触发巨型 `M_WAITOK` 分配和 `uiomove()` 长度截断 - -- **位置**:`src/data.c:493-500` 直接从 `uio_resid` 和映射 run 计算 `want`; - `src/data.c:514-518` 把该 `size_t` 长度传入读取路径; - `src/data.c:208-245` 的 `erofs_bread_device()` 使用 - `malloc(len, M_EROFS, M_WAITOK)` 分配连续缓冲;`src/data.c:521` 再把 `size_t want` - 传给 `uiomove()`。 -- **FreeBSD 对照**:`sys/sys/uio.h:95` 声明 - `int uiomove(void *cp, int n, struct uio *uio)`。因此大于等于 2 GiB 的 `size_t` - 长度在传参时不能被该接口正确表达,可能截断为负值或零值并造成错误或无进展。 -- **Linux 对照**:Linux `fs/erofs/data.c` 使用 folio/iomap 分页路径,不建立与 - 整个用户请求同尺寸的连续内核缓冲。该差异来自 I/O 模型,但当前无界分配不是 - FreeBSD 所要求的必要差异。 -- **触发与影响**:恶意或异常大 chunk/run,配合大 `VOP_READ`/`uio_resid`。影响 - 包括用户可控的巨型 `M_WAITOK` 连续内核分配、系统内存压力、长时间阻塞,以及 - 大于等于 2 GiB 时 `uiomove()` 截断或循环无进展。 -- **类别**:BSD 独有缺陷,属于 I/O 分块和内核 API 宽度处理问题。 -- **修复方向**:把读取固定分块到同时满足 buffer cache、`INT_MAX` 和合理内存 - 上限的尺寸;每轮验证 `uio_resid` 或 offset 必须前进;避免按整个 run 分配。 -- **建议 Markdown 手工测试**:构造超大 chunk/run 的稀疏镜像,分别测试 - `INT_MAX-1`、`INT_MAX`、`INT_MAX+1` 和大于 2 GiB 请求;记录 wired memory、 - malloc failure、进度、信号中断、errno 和 mount/KLD 清理。测试必须设置资源 - 上限,避免把宿主机作为压力目标。 -- **状态**:已静态复核;未动态复现;未修复。 - -### 5.4 P2-1:目录名称排序不变量未验证 - -- **位置**:`src/dir.c:72-109` 校验 dirent 数量、`nameoff` 单调、名称长度和非法 - `/`,但不比较相邻名称;`src/namei.c:55-100` 执行块内二分, - `src/namei.c:103-125` 开始跨块二分选择。 -- **Linux 对照**:Linux `fs/erofs/erofs_fs.h:280` 记录目录项按字典序排列; - Linux `fs/erofs/namei.c:45` 起的查找同样依赖该不变量,也未做全局排序验证。 -- **触发与影响**:结构合法但名称降序、重复或跨块逆序的损坏镜像。实际存在的 - 名称可被静默返回为 `ENOENT`,随后建立负 namecache,使错误持续存在。 -- **类别**:Linux 共同依赖的格式不变量;在 repo22“损坏结构返回 - `EINTEGRITY`、不得静默 miss”的更严格政策下,是 BSD 当前一致性缺口,不应 - 描述成 BSD/Linux 行为差异。 -- **修复方向**:对不可变目录执行一次全局严格递增验证,覆盖块内、跨块和重复 - 名称,并缓存结果;损坏统一返回 `EINTEGRITY`,不得写入负缓存。 -- **建议 Markdown 手工测试**:构造块内降序、块内重复、跨块逆序、跨块重复四类 - 镜像;冷 lookup、重复 lookup、readdir 和 NFS lookup 均应返回 - `EINTEGRITY`,并验证 namecache 无错误负项。 -- **状态**:已静态复核;未动态复现;未修复。 - -### 5.5 P2-2:根 vnode 未验证为目录 - -- **位置**:`src/super.c:759-768` 的 `erofs_root()` 只调用 `erofs_vget()`; - `src/inode.c:471-475` 按 inode mode 设置 vnode 类型,并仅按 NID 设置 `VV_ROOT`。 -- **Linux 对照**:Linux `fs/erofs/super.c:765-774` 加载 root inode 后明确执行 - `S_ISDIR()`,非目录则释放并返回 `-EINVAL`。 -- **FreeBSD 对照**:VFS mount 后续不会替 EROFS 强制验证其 `VFS_ROOT()` 返回 - `VDIR`,文件系统必须自行维持该不变量。 -- **触发与影响**:checksum-valid、但 root inode mode 被改为普通文件、symlink - 或 FIFO 的镜像可能完成挂载;挂载点后续出现 `ENOTDIR` 或异常 vnode 语义。 -- **类别**:BSD 独有缺口;Linux 已显式处理。 -- **修复方向**:`erofs_vget()` 成功后验证 `(*vpp)->v_type == VDIR`,失败时正确 - `vput()`。errno 可对齐 Linux 使用 `EINVAL`,或按 repo22 损坏政策使用 - `EINTEGRITY`,但需统一文档。 -- **建议 Markdown 手工测试**:分别生成 root inode 为 VREG、VLNK、VFIFO 的 - checksum-valid 镜像,验证挂载失败、errno 稳定、无 mount/md/KLD 残留。 -- **状态**:已静态复核;未动态复现;未修复。 - -### 5.6 P2-3:compact index 物理块号加法先发生 32-bit 回绕 - -- **位置**:`src/zmap.c:267`: - `m->pblk = le32dec(...) + nblk;`。`le32dec()` 和 `nblk` 均为 32-bit 范围,表达式 - 在赋给 64-bit `m->pblk` 前可能已经回绕。 -- **Linux 对照**:Linux `fs/erofs/zmap.c:230-232` 使用同样的 - `le32_to_cpu(...) + nblk` 结构,因此 Linux 参考也存在相同风险。 -- **触发与影响**:基准物理块接近 `UINT32_MAX` 且 compact 索引累计 `nblk` 非零, - 映射到错误的低地址,可能读到错误数据或触发后续一致性错误。 -- **类别**:Linux 共同缺陷,绝不能为了源码相似而保留,也不能记成 BSD 差异。 -- **修复方向**:在加法前显式提升到 64-bit,并使用 checked addition;随后按设备 - 块范围验证。 -- **建议 Markdown 手工测试**:构造 base pblk 接近 `UINT32_MAX`、累计跨越边界的 - compact 索引,验证映射不会回绕;同时测试恰好不溢出的边界和真实设备范围错误。 -- **状态**:已静态复核;未动态复现;未修复。 - -### 5.7 P2-4:explicit extent 首次初始化执行有界 O(N) 全表扫描 - -- **位置**:`src/zmap.c:581-619` 的 `z_erofs_validate_extent_table()` 在首次初始化 - 时从 `index=0` 到 `en->z_extents-1` 逐项读取并验证严格递增;调用后才进入 - 二分映射路径。 -- **Linux 对照**:Linux `fs/erofs/zmap.c:501-588` 映射 explicit extent 时按需 - 二分读取,没有 repo22 这一首次完整预扫描。 -- **触发与影响**:超大 extent 表在首次访问时产生 O(N) 元数据 I/O 和延迟,攻击 - 者可用格式边界内的大计数制造明显可用性压力。 -- **边界说明**:循环由 `en->z_extents` 严格限制,记录位置使用 checked arithmetic, - 并验证 `lstart` 严格递增。当前证据不支持“无限循环”或“无边界访问”的说法。 -- **类别**:BSD 独有的性能/可用性权衡,源于更严格的全表不变量验证,不是立即 - correctness 失败。 -- **修复方向**:先量化真实最坏情况。可评估分段验证、验证结果缓存、mount-time - 预算或按二分访问范围逐步验证;任何优化都不能重新引入静默错误映射。 -- **建议 Markdown 手工测试**:生成多档 extent count 的合法/末尾损坏表,测量 - 首次与重复 lookup/read 延迟、I/O 次数和内存;验证预算超限时 errno 和清理。 -- **状态**:已静态复核;未做性能原型;未修复。 - -### 5.8 P3-1:DEFLATE 分配失败被统一映射为 `EIO` - -- **位置**:`src/decompressor.c:246-257` 将所有后端非零返回统一转换为 `EIO`; - `src/decompressor_deflate.c` 的分配失败因而不能保留 `ENOMEM`。 -- **Linux/FreeBSD 对照**:平台均区分资源耗尽和输入/设备错误;调用 FreeBSD - zlib 是正确的,但 wrapper 的返回契约过窄。 -- **影响**:内存压力下诊断和上层重试策略失真。 -- **类别**:BSD 独有 errno 映射问题。 -- **修复方向**:后端返回正 errno,dispatcher 只做必要规范化。 -- **建议测试**:故障注入 DEFLATE workspace/output 分配失败,要求 `ENOMEM`;损坏 - 流仍应为 `EIO` 或项目约定的完整性错误。 -- **状态**:已静态复核;未动态复现;未修复。 - -### 5.9 P3-2:负 Unix 时间被拒绝,和 Linux 语义不一致 - -- **位置**:`src/inode.c:60-68` 以 `uint64_t seconds` 接收时间,并拒绝大于 - `INT64_MAX` 的值。 -- **Linux 对照**:Linux `fs/erofs/inode.c:108-109` 将磁盘 extended inode 的 - 64-bit 时间传给 inode time;Linux 时间模型允许 1970 年前的负时间语义。 -- **影响**:使用补码表达负秒值的镜像在 BSD 被视为损坏,形成跨平台兼容差异。 -- **类别**:BSD/Linux 行为差异,格式签名语义需先与上游规范确认。 -- **修复方向**:确认磁盘字段的正式有符号语义,再采用显式 signed 解码和 FreeBSD - `timespec` 范围检查。 -- **建议测试**:覆盖 `-1`、`INT64_MIN` 邻近值、epoch 和正最大值,在 Linux/BSD - 上比较 stat 结果与 errno。 -- **状态**:静态候选已复核;格式语义仍需上游确认;未修复。 - -### 5.10 P3-3:inline symlink 未拒绝声明长度内嵌 NUL - -- **位置**:`src/data.c:529-533` 的 readlink 直接复用普通读取路径; - `src/data.c:493-525` 按 inode 声明长度移动字节,没有对 symlink payload 做 NUL - 一致性检查。 -- **Linux 对照**:Linux 同样主要依赖 inode size 和 VFS symlink 语义;本项不是 - 用于制造表面差异,而是要求 BSD 明确损坏镜像政策。 -- **影响**:声明长度内部出现 NUL 时,不同调用者可能观察截断或不一致目标。 -- **类别**:损坏输入处理缺口,是否需要拒绝应与 EROFS 格式语义统一。 -- **修复方向**:确认格式是否禁止嵌入 NUL;若禁止,在 inode/readlink 验证中返回 - `EINTEGRITY`,且不得影响合法非 NUL 目标。 -- **建议测试**:inline 和非 inline symlink 分别构造中间 NUL、末尾 NUL、无 NUL - 目标,比较 `readlink(2)`、namei 跟随和 NFS 行为。 -- **状态**:静态候选已复核;格式语义待确认;未修复。 - -### 5.11 P3-4:48-bit end-exclusive 检查拒绝最后一个合法完整块 - -- **位置**:`src/zmap.c:895-898` 计算 `pend = m_pa + m_plen`,随后以 - `(pend >> block_bits) >= (1ULL << 48)` 拒绝映射。 -- **Linux 对照**:Linux 参考中的相关 48-bit 映射边界也存在同类 end-exclusive - 处理风险。 -- **触发与影响**:映射恰好结束在 48-bit 地址空间上界时,`pend` 是合法的 - end-exclusive 值,但当前检查把最后一个完整合法块拒绝为 `EINTEGRITY`。 -- **类别**:Linux 共同边界缺陷,不是 BSD 差异。 -- **修复方向**:校验最后一个实际字节/块,或使用 `pend > limit` 的 byte limit - 比较,并单独处理零长度。 -- **建议测试**:覆盖最后合法块、越界一字节、越界一块和零长度映射。 -- **状态**:已静态复核;未动态复现;未修复。 - -### 5.12 P3-5:birthtime-only `VOP_SETATTR()` 可假成功 - -- **位置**:`src/erofs_vnops.c:252-281` 检查 mode、uid、gid、atime、mtime、flags - 和 size,但不检查 `va_birthtime`,最终可能返回 0。 -- **FreeBSD 对照**:`VATTR_NULL()` 会把 birthtime 设为 `VNOVAL`,内核调用者可以 - 构造仅 birthtime 有效的请求。 -- **触发与影响**:直接内核 VOP 调用可收到成功但属性未改变,违反只读 VOP 语义。 -- **路径限定**:独立复核未确认普通用户态或 NFSv4 可绕过只读 mount 检查到达该 - 单属性路径,因此不能声称已有用户/NFS 可利用的假成功链。 -- **类别**:BSD 独有 VOP 契约和未来维护问题,严重度降为 P3。 -- **修复方向**:birthtime 变更返回 `EROFS`;对 type、nlink、fsid、fileid、 - blocksize、rdev、bytes、generation 等结构字段明确返回 `EINVAL`。 -- **建议测试**:扩展内核 probe,直接提交 birthtime-only 和各结构字段请求;本地 - syscall 与 NFS 测试用于确认它们仍被只读层拒绝。 -- **状态**:已静态复核;无已证实用户/NFS 路径;未修复。 - -### 5.13 P3-6:FRAGMENTS 特例分支不可达,注释与支持边界失配 - -- **位置**:`src/erofs_fs.h:56-64` 已把 `EROFS_FEATURE_INCOMPAT_FRAGMENTS` 放入 - `EROFS_ALL_SUPPORTED_INCOMPAT`;`src/super.c:532` 因而从 `unsupported` 中清除该 - bit,但 `src/super.c:533-549` 又试图在 `unsupported != 0` 时特例允许仅 - FRAGMENTS 的组合。 -- **Linux 对照**:Linux 通过实际 fragment/packed inode 路径表达支持,不需要 - 这种与 supported mask 自相矛盾的特例。 -- **影响**:当前分支不可达,注释声称“窄允许”但真实代码已经把 FRAGMENTS 当作 - 一般 supported incompat bit,误导维护者判断支持边界。 -- **类别**:BSD 独有死分支和文档一致性问题。 -- **修复方向**:依据真实 feature 支持政策二选一:从 supported mask 移除后保留 - 严格特例,或删除死分支并准确文档化支持范围。 -- **建议测试**:组合测试 FRAGMENTS、XATTR_PREFIXES、PLAIN_XATTR_PFX 和 - `packed_nid`,记录 mount errno 和实际 fragment inode 访问结果。 -- **状态**:已静态复核;未动态复现;未修复。 - -### 5.14 不列为 correctness finding:fragment `pstart_hi` - -曾有候选认为 fragment extent 构造会丢失 `pstart_hi`。当前静态证据不足以确认该 -字段在 fragment 记录格式中具有同样的高位语义,也不足以证明当前路径实际丢失 -可用地址。因此本报告不把它列为 correctness finding,只保留为“与上游格式语义 -确认”的待办。后续必须先取得格式规范或上游实现证据,再决定是否测试或修改。 - -## 6. FreeBSD 最佳实践与原生能力复用 - -| 领域 | 当前做法 | 评估与决策 | -| --- | --- | --- | -| DEFLATE | `src/decompressor_deflate.c:28` 起调用 FreeBSD zlib | 正确复用;应保留,修正 errno 契约即可 | -| ZSTD | `src/decompressor_zstd.c` 使用 ZSTDIO API | 正确复用;FreeBSD 无通用独立 zstd KLD,不应伪造依赖 | -| MicroLZMA | `src/decompressor_lzma.c:14-38` 私有重命名并编译 XZ Embedded | 应保留本地实现;stock `xz.ko` 未定义 `XZ_DEC_MICROLZMA`,不能强行链接 | -| LZ4 | `src/lz4.c:21` 起实现 raw/partial decoder | 应保留;FreeBSD/OpenZFS 接口含不同 framing,且不满足 EROFS partial 语义 | -| 目录输出 | `src/dir.c:160-178` 自有 cookie helper | 不能机械替换;它额外验证 cookie 严格递增和容量短读 | -| `vfs_read_dirent()` | FreeBSD `sys/kern/vfs_subr.c:6814-6830` | 可原型化为输出层 helper,但必须保留 EROFS cookie 约束并回归 NFS | -| GEOM/VFS I/O | `src/super.c:226` 建 consumer,`src/data.c:250` 用 `bread()` | 符合 FreeBSD 文件系统惯用路径,不应改为 GEOM class 私有读接口 | -| vnode cache | `src/inode.c:435-460` 用 `vfs_hash_get/insert` | 总体正确;仅 `insmntque()` 失败所有权需修复 | -| pager | `src/erofs_vnops.c:434` 复用 local pager | 正确,避免重写 VM pager | -| ACL/extattr | `src/erofs_vnops.c:68` 等调用 POSIX.1e ACL/extattr API | 正确保留 BSD namespace 和权限差异 | -| CRC32C | `src/super.c:307` 使用 `calculate_crc32c()` | 正确复用 FreeBSD kernel helper | -| endian | 磁盘字段使用 `leXXtoh`,非对齐流使用 `leXXdec` | 正确;不应直接复制 Linux unaligned 宏 | -| checked arithmetic | 多处使用 compiler overflow builtin | 合理;FreeBSD 15 无更优的通用非 LinuxKPI 替代 | - -### 6.1 为什么不能强行依赖 `xz.ko` - -FreeBSD `sys/modules/xz/Makefile:5-20` 会编译 `xz_dec_lzma2.c` 并导出符号,但没有 -定义 `XZ_DEC_MICROLZMA`。MicroLZMA 入口受该宏控制,因此 stock `xz.ko` 不提供 -repo22 所需的 `xz_dec_microlzma_*` ABI。简单添加 `MODULE_DEPEND(erofs, xz, ...)` -不能解决符号缺失。 - -只有在 FreeBSD 正式启用并导出 MicroLZMA KPI,同时更新模块 ABI/version 后, -repo22 才应重新评估直接依赖。当前强行引用反而违反“优先复用,但不强制引用一切” -的原任务要求。 - -### 6.2 LZMA per-call allocation 是性能原型事项 - -`src/decompressor_lzma.c:55-66` 每次解压分配并释放 decoder state。Linux 使用可 -复用 stream pool。当前没有证据证明它造成 correctness 错误,但并发压缩读取会 -产生约 30 KiB state 的反复 malloc/free 和锁竞争。 - -后续应单独做 per-CPU、mount-local 或受限 pool 原型,测量并发吞吐、内存峰值、 -卸载清理和字典变化。没有数据前,不应把缓存重构混入 correctness 批次。 - -## 7. BSD/Linux 行为差异分类 - -### 7.1 必要的 BSD 差异 - -- 正 errno,而不是 Linux 内核负 errno。 -- VOP/VFS operation table、vnode lock、namecache、`vn_vget_ino()` 和 - `vfs_hash_*` 生命周期。 -- GEOM consumer、设备 vnode 和 buffer cache I/O,而不是 folio/iomap/bio。 -- FreeBSD extattr user/system namespace、`extattr_check_cred()` 和 POSIX.1e ACL。 -- readdir 的 `a_cookies`/`a_ncookies` ABI、synthetic dot cookie 和 NFS 恢复语义。 -- `fifo_specops`、FreeBSD FID 布局、generation 与 `ESTALE` 校验。 -- pager 使用 `vnode_pager_local_getpages*`。 - -这些差异应保留,不应为了外观接近 Linux 而移除。 - -### 7.2 Linux 共同缺陷 - -- compact index 32-bit 加法回绕,见 `src/zmap.c:267` 与 Linux - `fs/erofs/zmap.c:231`。 -- 48-bit end-exclusive 最后合法块边界。 -- 目录二分依赖排序;Linux 也未全局验证。repo22 可选择更严格验证,但不能把 - 上游共同不变量说成 BSD 偏差。 - -源码相似不是保留共同缺陷的理由。修复时应尽量沿用上游命名和 checked arithmetic -结构,并把可上游化的发现单独记录。 - -### 7.3 BSD 独有缺陷 - -- `insmntque()` 失败后的 vnode 释放后访问。 -- chunk 大读的整段 `M_WAITOK` 分配和 `size_t` 到 `int` 的 `uiomove()` 传参。 -- root vnode 未检查 `VDIR`。 -- DEFLATE errno 归一化过度。 -- birthtime-only `VOP_SETATTR()` 假成功。 -- FRAGMENTS supported mask 与特例注释矛盾。 - -### 7.4 非必要维护差异 - -- `erofs_fs.h` 定义顺序、常量命名和字段注释没有尽量保持 Linux 顺序。 -- 压缩配置 loader 集中于 `decompressor.c`,而 Linux 分布在各 backend。 -- backend 函数名使用 `lzma_decompress`、`deflate_decompress`、 - `zstd_decompress`,没有采用 Linux `z_erofs_*` 体系。 -- `xattr.h`、`internal.h`、`erofs_defs.h` 中仍有失效 Linux 声明或未使用抽象。 -- xattr prefix helper 命名、部分函数定义位置和空包装与 Linux 无必要不同。 - -### 7.5 需要原型或格式确认后再判断 - -- fragment extent 的 `pstart_hi` 语义。 -- LZMA decoder state 缓存。 -- 保留 cookie 验证的 `vfs_read_dirent()` 输出适配器。 -- explicit extent 全表验证的分段/缓存策略。 -- negative timestamp 与 inline symlink NUL 的正式格式语义。 - -## 8. 结构、命名和 style 对齐 - -### 8.1 正向对齐结果 - -- repo22 与 Linux 有 15 个共同 C/H 文件名。 -- `src/namei.c` 的核心名称查找函数名称和顺序高度对应 Linux。 -- `src/zmap.c` 的 13 个共享核心算法函数保持相同相对顺序,是当前对齐质量最高的 - 文件之一。 -- `src/Makefile` 中 12 个共同编译单元保持与 Linux Makefile 相同的相对顺序。 -- `erofs_vnops.c` 集中 BSD VOP glue,避免把 FreeBSD 代码散入所有 Linux 对应 - 算法文件,这一文件级差异是必要且有利于维护的。 - -### 8.2 `erofs_fs.h` 的非必要差异 - -BSD 与 Linux 都包含同一组核心磁盘结构,但 repo22 的结构顺序、常量命名和注释 -存在明显漂移。例如 repo22 从 `src/erofs_fs.h:104` 定义 superblock,而 Linux -先定义 deviceslot;dirent、chunk index、map header 和 lcluster index 的位置也有 -移动。机械审查对 19 个核心 struct/union 记录到 31/171 个顺序逆序对,顺序相似度 -约 81.9%。 - -字段 packing、offset、endian 和 `_Static_assert` 是 correctness 约束,应保留 BSD -表达;结构定义顺序、字段语义注释和非平台相关名称则应尽量恢复 Linux,以降低 -未来磁盘格式同步成本。`EROFS_ALL_SUPPORTED_INCOMPAT` 与 Linux 名称不同,也应 -先判断是否存在真实语义差异,再决定是否保留。 - -### 8.3 压缩职责和命名 - -repo22 把 LZMA、DEFLATE、ZSTD 配置解析集中在 `src/decompressor.c`,Linux 则分别 -放在对应 backend。调用 ABI 必须因 FreeBSD 内核 API 而不同,但配置 loader 的 -文件职责和 `z_erofs_*` 命名并无平台限制。 - -建议在 correctness 修复完成后,单独评估把配置解析移回各 backend,并统一内部 -命名。该重排必须保持双配置构建和全部压缩 Markdown 测试,不应与 P1/P2 修复混在 -同一提交。 - -### 8.4 死代码和残留抽象 - -- `src/xattr.h:11-42` 保留注释掉的 Linux `CONFIG_*` 分支、未实现的 - `erofs_xattr_handlers`、`struct inode` 和 `struct posix_acl` 声明。 -- `src/internal.h:50-71` 保留未使用的同步解压/cache 枚举和 `erofs_buf`; - `src/internal.h:243-247` 的 `erofs_is_fileio_mode()` 固定返回 false。 -- `src/erofs_defs.h:5-19` 的 CRC polynomial、inode slot bits 和 range-coder 常量 - 多项仅定义未使用;`src/inode.c:137-139` 仍直接使用字面量 5。 -- `src/super.c` 的部分空包装和函数顺序可进一步与 Linux/FreeBSD 惯例收敛,但应 - 放在纯维护提交中。 - -### 8.5 失效测试入口和 harness - -- `test_all_decompress.sh:2-7` 仍标记 repo19;其 `SRCDIR` 当前未实际使用,但测试 - 内容不验证 repo22 KLD。 -- `test_chunk_based.sh:2-17` 实际进入 repo17,后续会 clean、写镜像、卸载模块、 - 配置 md 和 mount,具有真实误操作其他目录和全局系统状态的风险。 -- `tests/test_decompress.c` 与 `tests/test_decompress_standalone.c` 已无法通过当前语法 - 检查,并仍按旧参数数量调用后端接口;当前接口见 `src/internal.h:314-322`。 - -`docs/architecture.md:242-245` 已声明这两个根脚本不能作为验收入口,但只写文档 -不足以消除可执行误入口。后续应删除、移入明确的 historical 目录,或改为立即失败 -并指向 `tests/TC*.md`。 - -### 8.6 文档陈旧 - -- `docs/architecture.md:79` 的 `erofs_mount` 示例与当前 - `src/internal.h` 结构不一致,字段如 `z_algorithmformat`、`fragmentoff` 已陈旧。 -- `docs/architecture.md:273` 声称“零拷贝”,但 `src/data.c:244-261` 明确执行 - `malloc` 和 `memcpy`。 -- Linux 基线只记录绝对工作区路径和版本,没有独立仓库可获取的上游 provenance。 - -### 8.7 style(9) 机械结果 - -使用 FreeBSD `tools/build/checkstyle9.pl` 0.31 对 `src/*.[ch]` 连续检查,结果稳定: - -- 18 个文件; -- 48 warnings; -- 1 error; -- 44 个超过 80 列; -- 4 个块注释格式告警; -- 唯一 error:`src/erofs_fs.h:265` 的 `return 0` 缺 FreeBSD 风格括号。 - -其他可见项包括 `src/internal.h:11-12` 的 `sys/param.h // MUST FIRST` 实际位于 -`sys/types.h` 后,SPDX 注释形式混用,以及部分 Makefile 空白不统一。 - -这些结果不能机械全改。磁盘 ABI 宏、静态断言和与 Linux 保持一致的行,可能有 -理由超过 80 列。建议先制定“FreeBSD style(9) 优先、磁盘 ABI/上游同步可例外”的 -仓库规则,再逐批清理。 - -## 9. 正向成果与保留项 - -本轮不能只列问题。以下成果符合原任务 2 的目标,应在后续优化中保留: - -- 使用 `bsd.kmod.mk`,而不是维持 Linux Kbuild 兼容层。 -- GEOM consumer、设备 vnode、`bread()` 和 buffer cache 的组合符合 FreeBSD 文件 - 系统路径。 -- `vfs_hash_*`、vnode pager、ACL、extattr、CRC32C、endian helper 和 checked - arithmetic 的总体选型正确。 -- DEFLATE 和 ZSTD 直接复用可用的 FreeBSD 内核实现。 -- 不强行依赖缺少 MicroLZMA ABI 的 `xz.ko`,不强行套用不兼容的 OpenZFS LZ4。 -- `namei.c` 和 `zmap.c` 核心函数顺序与 Linux 高度对应。 -- Linux-only 文件没有以空壳方式复制到 BSD 仓库。 -- FreeBSD-only VOP glue 集中在 `erofs_vnops.c`,平台边界清晰。 -- 既有动态测试已覆盖多设备、压缩映射、xattr/ACL、NFS、48-bit 和边界行为; - 本报告不重写这些 task 1 证据。 - -## 10. 可追溯性与许可证边界 - -### 10.1 仓库许可证材料 - -repo22 的 18 个生产 `src/*.[ch]` 文件均带 SPDX 标识:13 个 -`GPL-2.0-only`、4 个 `BSD-2-Clause`、1 个 `MIT`。`docs/erofs.5:1-23` 自身包含 -完整的 BSD 两条款文本。 - -但 repo22 根目录没有集中式 `LICENSE`、`COPYING`、`NOTICE`、许可证构成表或来源 -清单。SPDX 可以识别文件当前声明,不能单独让第三方重建整个 KLD 的来源、整体 -分发边界和权利链。 - -建议后续增加集中式许可证和来源 manifest,明确 GPL 派生核心、MIT 磁盘 ABI、 -BSD VOP wrapper、本地 LZ4 和嵌入 XZ 源的关系。本报告不作兼容性或法律结论。 - -### 10.2 UDF helper 来源 - -`src/dir.c:158-178` 明确写有“modelled after UDF”。历史版本的 `erofs_uiodir` -与 FreeBSD `sys/fs/udf/udf_vnops.c:614-639` 在结构、分支顺序和控制流上高度接近; -当前版本已增加严格 cookie 单调性、结果枚举、容量检查和不同错误传播。 - -现有 Git 历史不足以区分最初是直接复制、紧密改写还是参考实现,因此不得断言 -侵权或具体来源类别。应由作者确认来源过程,并据此决定是否补充 FreeBSD UDF 的 -版权归属或说明。 - -### 10.3 Linux 基线外部可复现性 - -在当前 `/work` 单仓环境内,Linux 参考可由导入提交 -`8be2be573d2b191c83d760b65c554f78eb95893c` 和 tree -`b79b9a8b62f633e887a8b99075ff9412fabd7f83` 验证。 - -`docs/architecture.md:160-163` 目前只记录本地绝对路径和 Linux 7.1-rc1 字符串, -没有官方上游 URL、上游 Git commit/tag、获取命令和校验值。因此内部审查可复现, -独立社区仓库的外部复现材料仍不完整。 - -## 11. 第三方维护者相似度 - -以下评分来自文件树、共同符号顺序、磁盘 ABI 顺序、FreeBSD API、代码卫生和文档 -可复现性的综合人工/机械评估。它不是客观度量,也不代表 feature 完整度: - -| 维度 | 暂评分 | 说明 | -| --- | ---: | --- | -| 文件树与职责 | 8.0/10 | 共同文件清晰,Linux-only/BSD-only 边界总体合理 | -| 核心算法命名和顺序 | 8.5/10 | namei/zmap 较好,压缩后端仍有差异 | -| 磁盘 ABI 头文件可同步性 | 6.0/10 | 布局重视正确性,但顺序、命名、注释漂移明显 | -| FreeBSD API 与构建集成 | 9.0/10 | GEOM、VFS、pager、ACL、压缩库选型总体正确 | -| 代码卫生与 style(9) | 5.5/10 | 死声明、旧入口、48 warnings/1 error | -| 社区文档与基线可复现性 | 5.0/10 | 内部可验证,独立仓库 provenance 不完整 | -| **综合维护者相似度** | **7.2/10** | 带权主观评估,用于维护排序,不是兼容性结论 | - -综合分采用带权主观评估:文件树与职责 15%,核心算法命名和顺序 20%,磁盘 ABI -头文件可同步性 20%,FreeBSD API 与构建集成 20%,代码卫生与 style(9) 15%,社区 -文档与基线可复现性 10%。计算为 -`8.0*0.15 + 8.5*0.20 + 6.0*0.20 + 9.0*0.20 + 5.5*0.15 + 5.0*0.10 = 7.225`, -按一位小数四舍五入为 `7.2/10`。权重反映本任务更重视算法、ABI 和平台集成,仍不 -应视为客观兼容性指标。 - -从第三方维护者视角,当前更准确的描述是:核心算法和主要 FreeBSD 适配已对齐, -但仍存在明确的 correctness 风险、结构漂移、代码卫生和可追溯性工作。 - -## 12. 后续执行路线 - -本报告只给出路线,不执行任何修改。 - -### 第一批:先处理两个 P1 - -1. 修复 `insmntque()` 失败后的 vnode UAF。 -2. 对 chunk/physical read 建立严格分块上限,修复 `uiomove(int)` 宽度问题。 -3. 为两个问题分别新增 Markdown 手工测试和独立 FreeBSD 动态报告。 - -该批次必须最小化变更面,不能混入文件重排或 style 清理。 - -### 第二批:P2 correctness 与边界测试 - -1. 目录全局严格排序验证与 namecache 负项检查。 -2. root vnode `VDIR` 验证。 -3. compact pblk 64-bit checked addition。 -4. 量化 explicit extent 首次 O(N) 扫描,并决定预算/缓存策略。 - -每项一份或一组明确 Markdown 测试,复用既有 fixture helper,但不实现 CI。 - -### 第三批:P3 correctness、compatibility 与 semantic/style - -1. correctness/API 契约:保留原 P3 严重度,分别修正 DEFLATE 分配失败 errno、 - 48-bit end-exclusive 最后合法完整块边界,以及 birthtime-only `VOP_SETATTR()` - 的只读语义。 -2. compatibility/格式语义:确认并实现 Linux 负时间戳兼容语义;明确 inline symlink - 内嵌 NUL 的格式政策并补充拒绝或兼容测试。 -3. semantic/style:删除或改写不可达的 FRAGMENTS 特例分支,使注释、supported mask - 和真实支持边界一致。 - -六项均需各自的 Markdown 手工测试或同类成组测试,不得因进入后续批次而改变本报告 -中的 P3 严重度。 - -### 第四批:低风险卫生、文档和 provenance - -1. 删除或隔离 repo17/repo19 历史脚本和失配 C harness。 -2. 清理 `xattr.h`、`internal.h`、`erofs_defs.h` 的死声明和死宏。 -3. 修正 architecture 文档、零拷贝表述和 Linux 基线复现信息。 -4. 增加 LICENSE/COPYING、许可证构成和来源 manifest。 -5. 确认 UDF helper 和本地实现的作者归属。 -6. 在规则明确后处理 style(9) 的确定性问题。 - -### 第五批:结构重排和压缩职责对齐 - -1. 恢复 `erofs_fs.h` 非平台相关定义顺序、注释和名称。 -2. 评估把压缩配置 loader 移回各 backend。 -3. 统一内部 `z_erofs_*` 命名和函数定义顺序。 -4. 删除无行为包装,调整非必要函数位置差异。 - -该批次风险高于普通 style,必须用双配置构建、模块加载和全部相关 Markdown 测试 -证明无行为变化。 - -### 第六批:性能原型 - -1. LZMA decoder state pool/cache。 -2. explicit extent 验证缓存或分段策略。 -3. `vfs_read_dirent()` 输出适配器。 - -原型必须提供基线、压力方法、内存峰值、卸载清理和退化行为;没有数据不合并。 - -## 13. 原任务三个目标的覆盖矩阵 - -| 原任务目标 | 已确认正向证据 | 本轮发现 | 尚未验证/后续 | -| --- | --- | --- | --- | -| 1. 最佳实践与 BSD 原生能力复用 | GEOM/bread、vfs_hash、pager、ACL/extattr、CRC/endian、DEFLATE/ZSTD 复用正确;LZ4/MicroLZMA 本地实现有技术理由 | `insmntque()` 契约违反;chunk 大分配;dir helper 需保留但可原型化输出层;LZMA per-call allocation | 动态复现 P1;XZ KPI、dir adapter、LZMA pool 原型 | -| 2. 正确处理 BSD/Linux 行为差异 | 正 errno、VOP/VFS、GEOM、namecache、cookie、ACL namespace、FID/generation 等必要差异清晰 | root 类型、setattr、errno、负时间;compact/48-bit/排序属于 Linux 共同问题,未误记为 BSD 差异 | negative time、symlink NUL、fragment pstart_hi 格式语义确认 | -| 3. 第三方维护者相似性 | 15 个共同文件;namei/zmap 和 Makefile 顺序质量高;未伪造 Linux-only 空文件 | erofs_fs 顺序/注释、压缩职责/命名、死代码、旧脚本、陈旧文档、style、provenance、许可证清单 | 逐批结构重排、来源确认、外部可复现基线、回归验证 | - -因此,三个目标都已有实质正向成果,但目标 3 明显未达到“没有任何非必要差异” -的完成标准;目标 1 和目标 2 也因两个 P1 和若干 P2/P3 finding 需要继续工作。 - -## 14. 与 report-3 的关系 - -`current/report-3` 是任务 1 收尾时的历史快照。它关于 161 项 feature 手工验证、 -构建、清理和当时已知 issue 的证据继续有效,本报告不修改也不重写这些结论。 - -但 report-3 中“未发现仍需消除的非必要 Linux/FreeBSD 结构、命名或职责差异”是 -一个超出当时证据范围的绝对表述。当时没有保存: - -- 全部文件的函数、变量和定义顺序对照; -- FreeBSD 原生 API 复用清单; -- Linux-only/BSD-only 文件逐项必要性论证; -- 死代码、失效入口、style(9)、来源和许可证审计; -- 独立仓库可重建的 Linux 基线 provenance。 - -因此,对任务 2 应以本报告作更精确限定: - -> repo22 的核心算法和主要 FreeBSD 适配已取得良好对齐,但尚不能认定全面完成 -> code review 和 style 优化;仍存在两个 P1、四个 P2、六个 P3,以及明确的结构、 -> 代码卫生、style 和可追溯性工作。 - -该限定不回写 report-3,以保持历史报告不可变和审计链清晰。 - -## 15. 最终状态 - -- 本轮只创建 `current/report-4/`。 -- 未修改 `src/`、`tests/`、`docs/`、`issues/`、既有 report 或 CI。 -- 未声称任何 finding 已修复。 -- 未把静态候选误写成动态复现结果。 -- 新问题进入后续计划前,应先创建对应 Markdown 手工测试规格,再由独立执行者在 - FreeBSD 15 环境中复现、修复和回归。 diff --git a/current/report-4/README.md b/current/report-4/README.md deleted file mode 100644 index 2d21194..0000000 --- a/current/report-4/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# report-4:任务 2 代码审查与风格评估 - -本目录记录 2026-08-10 对 repo22 原任务中“任务 2:进行全面的 code review -和 style 优化”的只读调查结果。 - -本次工作只做静态评估、源码契约核对、Linux/FreeBSD 严格比对、历史检查和 -机械 style 检查,不修改生产源码、测试、既有文档、issues 或 CI。报告中的新 -finding 尚未在 FreeBSD 虚拟机中动态复现,也尚未修复。 - -## 审计基线 - -- BSD 目标:`repo-community/repo22`,提交 - `76cfb553e0951b428f9d426933a10d1fe18d2a0e` -- 最终源码变更提交:`67d3c057fbcad5adc765ab9927da511e3221ac8f` -- repo22 `src/` tree:`a605af0f01f83d67c199005e775e1a47a1610036` -- Linux 独立参考:`/work/dev-src-linux/fs/erofs`,Linux 7.1.0-rc1 -- Linux 本地导入提交:`8be2be573d2b191c83d760b65c554f78eb95893c` -- Linux EROFS tree:`b79b9a8b62f633e887a8b99075ff9412fabd7f83` -- FreeBSD API 参考:`/work/dev-freebsd-releng`,15.0-RELEASE-p9 - -## 结论摘要 - -- 任务 1 的 161 项 feature 验证结论不在本报告中重写,也不因本报告自动失效。 -- 任务 2 尚不能认定已经全面完成。本轮确认 `P0=0`、`P1=2`、`P2=4`、 - `P3=6`,另有若干非 correctness 的结构、style、来源与性能原型工作。 -- FreeBSD 原生 API 和构建集成总体合理,强行复用并不适用于 LZ4、MicroLZMA - 和目录 cookie helper。 -- 核心算法映射质量较高,但磁盘 ABI 头文件顺序、压缩职责、死代码、历史测试 - 入口、文档、style(9) 和外部可复现性仍有明确维护债务。 -- 综合维护者相似度暂评为 `7.2/10`。该分数仅用于排序维护工作,不是客观质量 - 或兼容性指标。 - -## 文档索引 - -- [任务 2 详细代码审查与风格评估](2026-08-10-task2-code-review-style-assessment.md) - -## 与既有报告的关系 - -`current/report-1` 至 `current/report-3` 保持历史快照。本目录不修改它们。 -report-3 的 feature 验证结果继续作为任务 1 证据;其中“没有非必要差异”的绝对 -表述,由本次更完整的结构、style、来源和许可证审计作更精确的限定。 diff --git a/src/data.c b/data.c similarity index 100% rename from src/data.c rename to data.c diff --git a/src/decompressor.c b/decompressor.c similarity index 100% rename from src/decompressor.c rename to decompressor.c diff --git a/src/decompressor_deflate.c b/decompressor_deflate.c similarity index 100% rename from src/decompressor_deflate.c rename to decompressor_deflate.c diff --git a/src/decompressor_lzma.c b/decompressor_lzma.c similarity index 100% rename from src/decompressor_lzma.c rename to decompressor_lzma.c diff --git a/src/decompressor_zstd.c b/decompressor_zstd.c similarity index 100% rename from src/decompressor_zstd.c rename to decompressor_zstd.c diff --git a/src/dir.c b/dir.c similarity index 100% rename from src/dir.c rename to dir.c diff --git a/docs/TEST_REPORT.md b/docs/TEST_REPORT.md deleted file mode 100644 index 42035d1..0000000 --- a/docs/TEST_REPORT.md +++ /dev/null @@ -1,97 +0,0 @@ -# Manual Test Evidence - -## Final Status - -repo22 has 161 executable manual test cases, TC001-TC161, plus the non-executable -TC000 template. The canonical dated reports produce: - -| Status | Count | -| --- | ---: | -| PASS | 160 | -| PARTIAL | 1 | -| FAIL / KERNEL-FAIL | 0 | -| ENVIRONMENT-UNAVAILABLE | 0 | -| SHELVED test case | 0 | - -TC146 is the single PARTIAL case. Its HEAD2 and interlaced paths pass, while -the explicit mapped-payload positive fixture remains unavailable. That -shelved subitem is not counted as a second test case. - -## Evidence Policy - -Authoritative evidence consists of the numbered Markdown procedures, -deterministic field-asserting fixture helpers, native syscall/kernel probes, -and dated `tests/results/manual/*/manual-test-report.md` reports. Reports record -the exact source, module or fixture hashes, FreeBSD kernel behavior, errno, and -cleanup state. - -Host-only fixture generation, static ABI review, and userspace erofs-utils -output are supporting evidence. They do not replace a required FreeBSD kernel -result. The suite is manual-only; no CI pipeline or automated kernel-test -harness is implemented or claimed. - -## Canonical Runs - -The final status uses these non-overlapping report groups for TC001-TC156: - -| Group | Exact scope | Result | -| --- | --- | --- | -| G1 | 32 IDs | 32 PASS | -| G2 | 13 IDs | 13 PASS | -| G3 | 31 IDs | 31 PASS after the dated TC060 fixed-source rerun | -| G4 | 27 IDs | 27 PASS | -| G5 | 24 IDs | 23 PASS, TC146 PARTIAL | -| G6 | 11 IDs | 11 PASS | -| G7 | 11 IDs | 11 PASS | -| G8 | 7 IDs | 7 PASS | - -The G1-G8 tables contain exactly 156 rows and 156 unique IDs, with no missing -or duplicate TC from TC001-TC156. TC060's original G3 KERNEL-FAIL is historical -and is superseded by -`tests/results/manual/2026-08-09T1120Z-tc060-fix/manual-test-report.md`. -TC153 passed in the final G3 run and its issue is resolved. - -TC157-TC161 are recorded in -`tests/results/manual/2026-08-09T1804Z-final-review-independent/manual-test-report.md`. -All five pass on exact source baseline -`fcc85b93d5f8fd9671686bd68bf3b086c8bd25cf`. - -## TC111 Coverage Audit - -A bounded audit of `tests/TC[0-9][0-9][0-9]-*.md` found 162 rows and 162 unique -IDs, TC000-TC161, with no missing or duplicate ID. Excluding TC000 leaves 161 -executable specifications. - -The audit also cross-checked the eight canonical group tables and the five -final-review rows. Final arithmetic is `155 PASS + 1 PARTIAL + 5 PASS`, or -160 PASS and one PARTIAL. - -## Final Build Qualification - -The final independent run rebuilt both configurations after the TC161 failing -`nm` shim and used `/usr/bin/nm` for the post-shim build: - -| Configuration | KLD SHA256 | -| --- | --- | -| `WITH_ZSTDIO=0` | `15fda9d334132cd81769ce4dff4f8411a6e2b4cf7531c352530d0de85f42a2d2` | -| `WITH_ZSTDIO=1` | `23782dc0ce7da188807d35020bf2d8c6044b796c5c9a8746b398ba784cd6ad4e` | - -Both KLDs loaded, mounted a qualified image read-only, unmounted, detached the -md provider, and unloaded. Final guest mount, md, EROFS KLD, and DTrace KLD -counts were zero. - -G1-G8 evidence was collected across multiple source commits. It is not claimed -that all historical tests ran on the final KLD. The final code baseline was -independently exercised by TC157-TC161 and the affected dual-build, -module-load, and mount smoke. - -## Issue Status - -- TC010: RESOLVED by a real 16 TiB-plus sparse-provider statfs run. -- TC060: RESOLVED by the FreeBSD pathconf fix and exact-source rerun. -- TC153: RESOLVED by the multi-TiB Layout 0 directory lookup run. -- Explicit mapped payload: SHELVED as a detailed fixture/tooling limitation; - TC146 remains PARTIAL. - -See `issues/README.md` for the current index and the individual files for -triggers, analysis, attempts, results, feature impact, and acceptance criteria. diff --git a/docs/architecture.md b/docs/architecture.md deleted file mode 100644 index 1b0508a..0000000 --- a/docs/architecture.md +++ /dev/null @@ -1,283 +0,0 @@ -# repo22 架构设计 - -## 设计目标 - -repo22 提供尽量贴近 Linux `fs/erofs` 职责划分的 FreeBSD 15 -只读实现,同时对 FreeBSD vnode、GEOM、pager、`dev_t` 和 NFS FID 语义做 -必要适配。 - -## 核心原则 - -1. **Linux 对齐**:函数命名、文件组织、代码排序尽可能匹配 Linux 版本 -2. **最小抽象**:避免不必要的封装层 -3. **安全优先**:保留 repo19 的所有安全修复 -4. **可维护性**:便于社区维护和与上游同步 - -## 文件组织 - -``` -src/ -├── super.c - 超级块、挂载、VFS 集成 -├── inode.c - inode 读取和 vnode 管理 -├── data.c - 数据块映射和解压缩 -├── namei.c - 路径查找(二分搜索) -├── dir.c - 目录遍历和输出 -├── xattr.c - 扩展属性和 ACL -├── erofs_vnops.c - VFS vnode 操作实现 -├── decompressor.c - 压缩配置解析和统一调度 -├── lz4.c - FreeBSD 有界 LZ4 后端 -├── decompressor_lzma.c - MicroLZMA 后端 -├── decompressor_deflate.c - DEFLATE 后端 -├── decompressor_zstd.c - 可选 ZSTDIO 后端 -├── zmap.c - 压缩逻辑块映射 -├── zdata.c - 压缩数据读取 -├── internal.h - 内存结构和内部 API -├── erofs_fs.h - 磁盘格式定义 -├── xattr.h - 扩展属性接口 -└── erofs_defs.h - 常量定义 -``` - -## 分层架构 - -``` -┌─────────────────────────────────────┐ -│ VFS 层 (FreeBSD kernel) │ -└─────────────┬───────────────────────┘ - │ -┌─────────────▼───────────────────────┐ -│ VFS 接口层 │ -│ - erofs_vnops.c │ -│ - super.c (mount/unmount/root) │ -└─────────────┬───────────────────────┘ - │ -┌─────────────▼───────────────────────┐ -│ 文件系统逻辑层 │ -│ - inode.c (erofs_read_inode) │ -│ - namei.c (erofs_namei) │ -│ - dir.c (erofs_readdir_block) │ -│ - xattr.c (erofs_getxattr) │ -└─────────────┬───────────────────────┘ - │ -┌─────────────▼───────────────────────┐ -│ 数据访问层 │ -│ - data.c (erofs_map_blocks) │ -│ - data.c (erofs_read_data) │ -│ - zmap.c / zdata.c │ -│ - decompressor.c (z_erofs_decompress) │ -└─────────────┬───────────────────────┘ - │ -┌─────────────▼───────────────────────┐ -│ 块 I/O 层 │ -│ - erofs_bread/erofs_brelse │ -└─────────────────────────────────────┘ -``` - -## 核心数据结构 - -### erofs_mount (内存中的文件系统状态) -```c -struct erofs_mount { - struct mount *mnt; // FreeBSD mount 结构 - struct vnode *devvp; // 块设备 vnode - struct g_consumer *cp; // GEOM consumer - - uint32_t block_size; // 块大小 - uint64_t root_nid; // 根目录 NID - uint32_t feature_compat; // 特性标志 - uint32_t feature_incompat; - - struct erofs_sb_lz4_info lz4; // LZ4 参数 - struct erofs_deviceslot *devs; // 设备表 - struct erofs_xattr_prefix_item *xattr_prefixes; // xattr 前缀表 -}; -``` - -### erofs_node (内存中的 inode) -```c -struct erofs_node { - struct vnode *vnode; // 关联的 vnode - uint64_t nid; // 节点 ID - uint64_t size; // 文件大小 - uint8_t datalayout; // 数据布局类型 - - // 压缩相关 - uint8_t z_algorithmformat; - uint8_t z_lclusterbits; - - // Chunk-based 相关 - uint16_t chunkformat; - uint8_t chunkbits; - - // Fragment 相关 - uint32_t fragmentoff; - bool fragment; -}; -``` - -## 关键实现细节 - -### 1. 数据布局支持 - -支持 4 种数据布局: -- **FLAT_PLAIN**: 连续块 -- **FLAT_INLINE**: 最后一个逻辑块位于 inode metadata block 内,且受声明 - image/metabox bounds 约束 -- **CHUNK_BASED**: 固定大小 chunk,支持稀疏文件 -- **COMPRESSED**: 可变大小压缩 cluster - -### 2. 压缩算法 - -支持 4 种压缩算法及未压缩 transform: -- LZ4 (LZ4HC) -- LZMA -- DEFLATE -- ZSTD -- 未压缩 - -### 3. 高级特性 - -- ✅ **ztailpacking**: 压缩文件尾部内联 -- ✅ **fragments**: 已验证的 fragment-backed 压缩文件与 metabox carrier -- ⚠️ **dedupe**: 仅声明已验证的 fragment/partial-reference 形式,不宣称 - 覆盖所有未来编码 -- ✅ **xattr_prefixes**: 共享 xattr 前缀表 -- ✅ **device_table**: 多设备支持 -- ✅ **metabox**: 每 inode 元数据盒 - -### 4. 安全机制 - -repo22 的边界策略: -- 所有指针操作前检查边界 -- 所有算术运算检查溢出 -- 所有分配检查大小合理性 -- 递归深度限制 -- 设备 ID 和块地址验证 - -## 与 Linux 版本的差异 - -### 对照基线 - -本轮维护逐文件对照工作区中的 `/work/dev-src-linux/fs/erofs`。该目录是导入的 -Linux 7.1-rc1 EROFS 参考快照;对照不依赖 repo22 与 Linux 树具有共同 Git -历史。`/work/linux-src/fs/erofs` 中对应文件与该精简快照字节一致,但不是本轮 -文件映射的依据。 - -### 必要差异(FreeBSD 适配) - -1. **内存分配**:使用 `malloc(..., M_EROFS, ...)` 而非 `kmalloc()` -2. **块 I/O**:通过 GEOM consumer 和 FreeBSD vnode/buffer 接口读取 provider -3. **VFS 接口**:`erofs_vnops.c` 实现 FreeBSD `vop_vector`,不照搬 Linux - `inode_operations`、folio 或 iomap 接口 -4. **错误约定**:内核入口返回正的 FreeBSD errno;Linux 负 errno 或 - `ERR_PTR` 仅作为算法对照,不能机械移植 -5. **压缩后端**:BSD 调度器跨编译单元调用 `internal.h` 中的简单后端 API; - Linux 使用 `struct z_erofs_decompressor` 和不同的内存/页面生命周期 -6. **LZ4 文件职责**:BSD 保留独立 `lz4.c` 有界解码器;Linux LZ4 路径位于 - `decompressor.c` 并依赖 Linux 内核 LZ4/page API -7. **平台特性**:Linux `sysfs.c`、`fileio.c`、`fscache.c`、`ishare.c` 和 - `zutil.c` 没有无条件对应物,不为文件外观引入空包装 -8. **构建架构**:当前只验证 FreeBSD 15 amd64,Makefile 明确拒绝其他 - `MACHINE_ARCH` - -### 保持一致的部分 - -- 静态目录 helper 使用 Linux 名称 `find_target_dirent` -- LZMA、DEFLATE、ZSTD 后端使用 Linux 文件名 `decompressor_*.c` -- Makefile 先列 metadata/VFS 文件,再列压缩调度、映射和后端文件 -- 跨文件后端声明集中在 `internal.h`,不在调用方手写 `extern` -- `erofs_fs.h` 的磁盘格式定义和核心目录/映射算法按 Linux 语义核对 - -## 代码规范 - -### 命名约定 -- 公共函数:`erofs__` -- 静态函数:描述性名称,无固定前缀 -- 宏:`EROFS_*` 全大写 -- 结构体:`struct erofs_*` - -### 函数排序(每个文件) -1. 辅助函数(static) -2. 核心逻辑函数 -3. VFS 接口函数 -4. 模块注册/清理(仅 super.c) - -### 错误处理 -```c -int erofs_function(...) -{ - int error = 0; - void *buf = NULL; - - // 操作... - if (条件) { - error = EINVAL; - goto fail; - } - - // ... - return 0; - -fail: - if (buf) - erofs_brelse(buf); - return error; -} -``` - -## 测试策略 - -### 单元测试 - -不存在受支持的用户态单元测试入口。旧的 `src/Makefile.test` 从错误目录引用 -`test_decompress.c`,并尝试把内核解压源码按不匹配的用户态 ABI 链接;该入口已 -删除,不能作为可运行测试或 feature 验证证据。 - -旧的 `test_super.c` 和 `test_inode.c` 只重复了测试文件中的公式,并未调用 -内核生产解析路径,其中 inode harness 还引用过已删除的磁盘字段。它们已退役, -不得作为 feature 验证证据。superblock、inode、pager 和错误路径必须使用 -`TC*.md` 中的确定性镜像,经 FreeBSD 内核模块实际挂载或访问验证。 - -### 集成测试 - -仓库根目录遗留的 `test_all_decompress.sh` 和 `test_chunk_based.sh` 包含其他 -repo 的硬编码路径,不能作为 repo22 的测试入口或验收证据。受支持的验证方式是 -直接执行 `tests/TC*.md` 中记录的 FreeBSD 15 内核步骤,并将命令、errno、哈希和 -清理状态写入 `tests/results/manual/` 下的日期报告。 - -### VM 测试 -- 挂载真实镜像 -- 文件读取验证 -- 性能基准测试 - -## 维护指南 - -### 同步上游 Linux 变更 - -1. 选定明确的 Linux `fs/erofs` 快照;当前工作区基线为 - `/work/dev-src-linux/fs/erofs` -2. 逐文件识别修改,不假设两个实现共享提交历史 -3. 检查是否为磁盘格式变更(`erofs_fs.h`)或 Linux 专属 VFS/page API -4. 只移植语义上适用的算法,并保留 FreeBSD errno、锁、GEOM 和 vnode 约定 -5. 运行双配置构建、模块加载和真实镜像挂载测试 - -### 添加新特性 - -1. 在 `erofs_fs.h` 添加磁盘格式定义 -2. 在 `internal.h` 添加内存结构 -3. 实现解析逻辑(data.c/inode.c) -4. 添加确定性 fixture 和对应的 `TC*.md` 内核测试 -5. 更新文档 - -## 性能考虑 - -- **零拷贝**:直接从缓冲区缓存读取 -- **延迟加载**:仅在需要时读取 inode 元数据 -- **缓存友好**:利用 FreeBSD 的 vnode 缓存 -- **批量操作**:目录读取一次性处理多个条目 - -## 已知限制 - -- 不支持写操作(只读文件系统) -- 不支持 FUSE 模式 -- 构建和运行时验证目前仅覆盖 FreeBSD 15 amd64 -- 不实现 Linux file-backed、fscache、page-cache sharing 或 sysfs 控制面 diff --git a/docs/capabilities.md b/docs/capabilities.md deleted file mode 100644 index 6c9cf40..0000000 --- a/docs/capabilities.md +++ /dev/null @@ -1,65 +0,0 @@ -# repo-pre-2 Capability Record - -## Status Vocabulary - -This document separates inherited pre1 claims from fresh pre2 validation: - -- `INHERITED CLAIM`: documented by the unchanged pre1 snapshot; not re-tested - for pre2. -- `STATIC GATE`: visible in tracked source or build declarations; runtime - behavior was not exercised. -- `NOT IMPLEMENTED`: explicitly excluded by inherited project documentation. -- `NOT CLAIMED`: inherited documentation deliberately limits the support - claim. -- `NOT RUN`: no build or runtime validation was performed for repo-pre-2. - -`INHERITED CLAIM` is provenance, not a new PASS result. Existing reports under -`tests/results/manual/` describe earlier pre1 work and must not be cited as a -fresh repo-pre-2 execution. - -## Snapshot Evidence - -```text -repo-pre-1 source tree: 6a5a1a49d4a4e08f6ef1155de240500e63a15365 -repo-pre-2 initial tree: 6a5a1a49d4a4e08f6ef1155de240500e63a15365 -src-linux reference tree: b79b9a8b62f633e887a8b99075ff9412fabd7f83 -runtime validation: NOT RUN -``` - -The inherited claims below are transcribed at category level from `README.md` -and `docs/features.md`. They are not an independent feature review. - -## Capability Matrix - -| Area | Inherited pre1 claim or static declaration | Pre2 status | -| --- | --- | --- | -| Mount and metadata | Read-only mount/unmount, `statfs`, superblock checksum and bounds, compact/extended inodes, 48-bit fields | INHERITED CLAIM; NOT RUN | -| Plain data | Flat plain, flat inline, tail bounds, and logical block accounting | INHERITED CLAIM; NOT RUN | -| Chunk and devices | Chunk indexes, holes, device tables, explicit devices, and flatdev mapping | INHERITED CLAIM; NOT RUN | -| Compressed data | LZ4, MicroLZMA, DEFLATE, full/compact indexes, partial references, HEAD2, ztailpacking, and qualified fragment forms | INHERITED CLAIM; NOT RUN | -| ZSTD | Source is always listed; `WITH_ZSTDIO=1` adds `ZSTDIO`, while the default is `0` | STATIC GATE; runtime NOT RUN | -| Directories and vnode operations | Lookup, readdir, cookies, namecache, access, readlink, pathconf, pager integration, and read-only mutation rejection | INHERITED CLAIM; NOT RUN | -| Xattrs and ACLs | Inline/shared namespaces, metabox storage, long/packed prefixes, and POSIX ACL reads | INHERITED CLAIM; NOT RUN | -| NFS export | 64-bit NIDs, generation handling, and malformed/stale handle validation | INHERITED CLAIM; NOT RUN | -| Build target | `src/Makefile` rejects architectures other than `amd64` | STATIC GATE; build NOT RUN | -| Write support | Writable filesystem operations | NOT IMPLEMENTED | -| `VOP_BMAP` | Explicitly unsupported in inherited documentation | NOT IMPLEMENTED | -| Complete future-format parity | All future incompat features and every dedupe encoding | NOT CLAIMED | -| Explicit compressed-extent positive payload | Inherited documentation records only partial coverage | NOT CLAIMED; NOT RUN | -| Performance guarantees | Manual observations are not a performance contract | NOT CLAIMED | - -## Interpretation Rules - -1. Source presence, feature-bit definitions, or build selection do not prove a - runtime capability. -2. A pre1 manual report does not become a pre2 PASS merely because the initial - trees match. -3. Phase 1 mechanical edits must not expand or reduce this capability matrix. -4. A future status change requires recorded evidence from an actually executed - validation step. - -## Current Validation Declaration - -No build, QEMU execution, test script, smoke test, mount, malformed-image -probe, or performance measurement was run for this initialization. The current -repo-pre-2 runtime validation result is therefore `NOT RUN` in every category. diff --git a/docs/erofs.5 b/docs/erofs.5 deleted file mode 100644 index 55f47fd..0000000 --- a/docs/erofs.5 +++ /dev/null @@ -1,271 +0,0 @@ -.\" Copyright (c) 2026 -.\" All rights reserved. -.\" -.\" Redistribution and use in source and binary forms, with or without -.\" modification, are permitted provided that the following conditions -.\" are met: -.\" 1. Redistributions of source code must retain the above copyright -.\" notice, this list of conditions and the following disclaimer. -.\" 2. Redistributions in binary form must reproduce the above copyright -.\" notice, this list of conditions and the following disclaimer in the -.\" documentation and/or other materials provided with the distribution. -.\" -.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND -.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE -.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -.\" SUCH DAMAGE. -.\" -.Dd August 9, 2026 -.Dt EROFS 5 -.Os -.Sh NAME -.Nm erofs -.Nd Enhanced Read-Only File System -.Sh SYNOPSIS -To mount an -.Nm -volume: -.Bd -literal -offset indent -mount -t erofs /dev/da0 /mnt -.Ed -.Sh DESCRIPTION -The -.Nm -driver provides read-only support for the Enhanced Read-Only File System -(EROFS), a modern compressed read-only filesystem designed for space -efficiency and performance. -EROFS is widely used in Linux distributions and mobile systems for root -filesystems, firmware images, and container layers. -.Pp -The -.Fx -implementation supports multiple compression algorithms, various data layouts, -extended attributes, and multi-device configurations. -.Pp -The currently qualified build target is -.Fx 15 -on -.Sy amd64 . -The module Makefile rejects other architectures because they have not been -validated against this implementation's kernel ABI. -.Sh FEATURES -.Ss Inode Types -The -.Nm -driver supports both compact and extended inode formats: -.Bl -bullet -compact -.It -Compact inodes (32 bytes) for typical files -.It -Extended inodes (64 bytes) with extended metadata -.It -Special handling for single-link files -.It -Inline data (tailpacking) for small files -.El -.Ss Data Layouts -.Bl -bullet -compact -.It -.Sy FLAT_PLAIN : -Uncompressed contiguous data -.It -.Sy FLAT_INLINE : -Uncompressed data with inline tail -.It -.Sy Chunk-based : -Fixed-size chunks for multi-device support -.It -.Sy Compressed : -LZ4, DEFLATE, zstd, or LZMA compressed data with pcluster mapping -.El -.Ss Compression Algorithms -.Bl -tag -width "MicroLZMA" -.It Sy LZ4 -Fast decompression for general-purpose use, including ztailpacking -(compressed tail in inode metadata) -.It Sy DEFLATE -Standard compression with good compression ratio -.It Sy zstd -High compression ratio when the module is built with -.Va WITH_ZSTDIO=1 . -ZSTD support is disabled by default -.Pq Va WITH_ZSTDIO=0 , -and an enabled module requires a kernel built with -.Cd "options ZSTDIO" . -.It Sy LZMA/MicroLZMA -Maximum compression ratio for space-constrained environments -.El -.Ss Extended Attributes -.Bl -bullet -compact -.It -Shared extended attributes with metabox container support -.It -Inline extended attributes -.It -Long-prefix extended attribute support -.It -Packed prefix table support -.It -POSIX ACL support (access and default ACLs) -.El -.Pp -User namespace attributes are exposed without prefix; system namespace -attributes retain their full qualified names (trusted.*, security.*). -.Pp -Linux POSIX access/default ACL xattrs are decoded into -.Fx -POSIX.1e ACLs when present. -ACL and xattr mutation remains read-only. -.Ss Advanced Features -.Bl -bullet -compact -.It -Superblock CRC32C verification -.It -48-bit block count and root nid support -.It -Device table for multi-device volumes -.It -Qualified fragment-backed compressed files and metabox carriers -.It -VFS hash integration and namecache support -.It -Directory entry optimization (dot_omitted handling) -.It -NFS export with stable superblock-seeded per-inode file-handle generations -.It -.Fx -local vnode pager support for read-only mappings -.El -.Sh MOUNT OPTIONS -The -.Nm -filesystem supports standard read-only mount options and the following -filesystem-specific option: -.Bl -tag -width "device.N=/dev/mdN" -.It Cm device.N= Ns Pa path -Map one-based on-disk external device slot -.Ar N -to the disk provider at -.Ar path . -The slot number is part of the option name, so option order has no effect. -Every declared external slot must be supplied exactly once when external blob -providers are used. -Reusing the primary provider or one external provider for -multiple slots is rejected. -.El -.Pp -There is no filesystem-specific -.Pa /sbin/mount_erofs -utility in this repository. -Use the generic -.Xr mount 8 -frontend with -.Fl t Cm erofs ; -it passes the filesystem-specific option names to -.Xr nmount 2 . -.Pp -If the image has a device table and no -.Cm device.N -options are supplied, the primary provider is treated as a flatdev image. -It -must contain every declared device range at its on-disk unified block address. -The driver forces every successful mount read-only. -An explicit -.Fl o Cm rw -request therefore still produces a read-only mount; it does not enable writes -and is not rejected solely because -.Cm rw -was requested. -Mutating vnode operations fail with -.Er EROFS . -.Sh EXAMPLES -Mount an EROFS image from a disk device: -.Bd -literal -offset indent -mount -t erofs -o ro /dev/da0s1 /mnt -.Ed -.Pp -Mount an EROFS image from a regular file using -.Xr mdconfig 8 : -.Bd -literal -offset indent -mdconfig -a -t vnode -f rootfs.img -u 0 -mount -t erofs -o ro /dev/md0 /mnt -.Ed -.Pp -Mount a split image with two external blob slots. -The deliberately reversed -option order demonstrates that slot mapping is deterministic: -.Bd -literal -offset indent -mdconfig -a -t vnode -f primary.img -u 90 -mdconfig -a -t vnode -f blob1.img -u 91 -mdconfig -a -t vnode -f blob2.img -u 92 -mount -t erofs -o ro -o device.2=/dev/md92 \ - -o device.1=/dev/md91 /dev/md90 /mnt -.Ed -.Pp -Mount a flatdev image containing the primary image followed by all declared -unified device ranges: -.Bd -literal -offset indent -mdconfig -a -t vnode -f combined-flatdev.img -u 90 -mount -t erofs -o ro /dev/md90 /mnt -.Ed -.Pp -Unmount an EROFS filesystem: -.Bd -literal -offset indent -umount /mnt -.Ed -.Sh DIAGNOSTICS -Error messages are logged via -.Xr printf 9 -when filesystem inconsistencies are detected, such as: -.Bl -bullet -compact -.It -Invalid superblock magic number -.It -Superblock CRC32C checksum mismatch -.It -Unsupported compression algorithm -.It -Invalid inode format -.It -Invalid or unrepresentable inode timestamps -.It -Corrupted directory entries -.It -Compressed extent address arithmetic overflow -.It -Inline data crossing its inode metadata block or declared backing bounds -.It -Missing, short, or orphaned external providers -.It -Malformed or overlapping device-table ranges -.El -.Sh SEE ALSO -.Xr nmount 2 , -.Xr mdconfig 8 , -.Xr mount 8 , -.Xr umount 8 , -.Xr printf 9 -.Sh HISTORY -EROFS was originally developed for Linux by Huawei in 2019. -The -.Fx -implementation first appeared in 2026. -.Sh AUTHORS -.An Ruicheng Pan -.Sh BUGS -.Bl -bullet -compact -.It -The driver is read-only and does not claim support for every future EROFS -incompat feature or every dedupe encoding. -.It -There is no automated kernel regression harness; the repository records -.Fx 15 -manual-test procedures and results. -.El diff --git a/docs/features.md b/docs/features.md deleted file mode 100644 index 3a2fae3..0000000 --- a/docs/features.md +++ /dev/null @@ -1,77 +0,0 @@ -# EROFS Feature Status - -This list describes behavior implemented and manually qualified in the -FreeBSD 15 module. It is not a claim of complete Linux EROFS feature parity. - -## Filesystem and Metadata - -- Read-only mount/unmount and `statfs`. -- Superblock CRC32C, declared image/media bounds, 48-bit block/root-NID decode. -- Compact and extended inodes. -- Correct compact non-directory `I_NLINK_1` semantics and directory - `dot_omitted` semantics. -- Linux `new_decode_dev` major/minor decoding followed by FreeBSD `makedev()`. -- Superblock-seeded, inode-metadata-derived vnode/NFS generation synchronized - with `va_gen`; metadata-identical payload replacement is outside the stale - handle guarantee. -- NFS export, 64-bit NID file handles, stale-generation and malformed-FID - validation. - -## Data Layouts - -- `FLAT_PLAIN` and `FLAT_INLINE` reads. -- Inline tails constrained to the inode metadata block and declared backing - bounds. -- Chunk-based files, chunk indexes, holes, device tables, explicit devices, - and flatdev mapping. -- LZ4, MicroLZMA, DEFLATE, and ZSTD compressed reads. -- Full and compact indexes, partial references, HEAD2/interlaced records, - ztailpacking, and fragment-backed compressed data used by qualified images. -- Compressed `va_bytes`/`st_blocks` from the complete 48-bit on-disk compressed - block count; plain, inline, and chunk files retain logical block rounding. - -## Vnode and Directory Operations - -- `vget`, root lookup, `vfs_hash`, namecache, and `vn_vget_ino` integration. -- `lookup`, `readdir`, stable restart cookies, cold nested lookup, and - Linux-compatible nonzero final-name padding. -- Shared strict validation for dirent arrays, `nameoff`, block bounds, and - illegal names. -- `getattr`, access checks, `readlink`, `pathconf`, and read-only mutation - rejection, including combined special-vnode setattr requests. -- FreeBSD 15 `vnode_pager_local_getpages` and compatible async pager entry; - `VOP_BMAP` remains explicitly unsupported. - -## Xattrs and ACLs - -- Inline and shared `user.*`, `trusted.*`, and `security.*` attributes. -- Shared attributes in primary metadata and metabox containers. -- Long-prefix and packed-prefix-table lookup. -- FreeBSD user/system namespace exposure and POSIX access/default ACL reads. -- Read-only xattr/ACL mutation behavior. - -## Documentation and Validation - -- `erofs(5)` manual page. -- Markdown manual tests TC001-TC161, with unresolved cases tracked in - `issues/`. -- FreeBSD 15 manual reports with fixture hashes and cleanup evidence. -- Final explicit-extent ordering, 48-bit allocation, special setattr, - `OFF_MAX` directory-cookie, and build-tool failure checks. - -## Build Qualification - -- FreeBSD 15 `amd64` is the only currently validated and accepted target; - `src/Makefile` rejects other architectures. -- ZSTD support is disabled by default (`WITH_ZSTDIO=0`). Opt-in builds use - `WITH_ZSTDIO=1` and require a kernel built with `options ZSTDIO`. - -## Not Implemented or Not Claimed - -- Writable filesystem operations. -- A CI pipeline or automated kernel-test harness. -- Positive mapped-payload execution of the explicit compressed-extent format; - TC146 remains PARTIAL and the fixture limitation is tracked under `issues/`. -- Blanket support for every future EROFS incompat feature or every dedupe - encoding; only the explicitly qualified fragment/metabox forms are claimed. -- Performance guarantees from the manual throughput observations. diff --git a/docs/pre2-baseline.md b/docs/pre2-baseline.md deleted file mode 100644 index 5f3058a..0000000 --- a/docs/pre2-baseline.md +++ /dev/null @@ -1,65 +0,0 @@ -# repo-pre-2 Maintenance Baseline - -## Snapshot Identity - -`repo-pre-2` was created from the tracked `repo-pre-1` tree without copying -working-tree-only files or build artifacts. - -```text -planning baseline commit: ed47f1b5583a229e372f488b9492d1f6234aae98 -snapshot creation parent: 109fe74d3fcef107db5869be1d030f4f12d1cfa6 -snapshot creation commit: 73fa57924b549387400eeeb85354bbbbe770a962 -repo-pre-1 source tree: 6a5a1a49d4a4e08f6ef1155de240500e63a15365 -repo-pre-2 snapshot tree: 6a5a1a49d4a4e08f6ef1155de240500e63a15365 -Linux comparison tree: b79b9a8b62f633e887a8b99075ff9412fabd7f83 -``` - -The matching source and snapshot tree IDs establish byte-for-byte content, -file-mode, and path equivalence at snapshot creation. `repo-pre-1` and -`src-linux` are read-only references for pre2 work. - -## Phase Scope - -Pre2 phase 1 is L0 mechanical maintenance alignment. It permits only the -separately reviewed changes listed below: - -1. Remove stale Linux-only declaration fragments from `src/xattr.h` while - preserving every active FreeBSD interface. -2. Remove six specified, unreferenced private constants from - `src/erofs_defs.h`. -3. Rename the private checksum helper in `src/super.c` without changing its - signature, body, call position, or behavior. - -Each source change must remain an independent commit. This phase does not add -features, fix behavior, move responsibilities between files, reorder data -structures, alter ABI, or broadly reformat code. - -## Required FreeBSD Differences - -Linux and FreeBSD EROFS remain independent implementations. The following -platform boundaries are intentional and must not be replaced merely to make -the source look more similar: - -- FreeBSD VFS, vnode, mount, namecache, pager, and NFS export interfaces. -- FreeBSD buffer/cache, GEOM provider, device I/O, locking, and allocation - APIs. -- FreeBSD xattr, ACL, errno, kernel-module, and `bsd.kmod.mk` conventions. -- FreeBSD build gates, including the current amd64 restriction and optional - `ZSTDIO` integration. - -Linux naming and layout should be followed only when doing so preserves these -native contracts and makes cross-repository review easier. - -## Excluded Work - -This phase does not address vnode ownership after `insmntque()` failure, -large-run allocation and `uiomove()` limits, compact pblk validation, 48-bit -boundaries, decompressor descriptors, backend ABI, typed errno, xattr parsing, -ACL behavior, file splitting, build-system alignment, CI, or test tooling. - -## Validation Status - -Only static snapshot and Git allowlist checks were performed for the -initialization commits. No build, QEMU run, test script, smoke test, mount, or -runtime feature check was performed. All runtime capabilities therefore remain -`NOT RUN` for repo-pre-2 until a later, explicitly authorized validation phase. diff --git a/docs/pre3-baseline.md b/docs/pre3-baseline.md deleted file mode 100644 index b57003d..0000000 --- a/docs/pre3-baseline.md +++ /dev/null @@ -1,83 +0,0 @@ -# repo-pre-3 Maintenance Baseline - -## Snapshot Identity - -`repo-pre-3` was created exclusively from the Git-tracked `repo-pre-2` tree. -Untracked files, ignored files, build outputs, and working-tree-only content -were not copied. - -```text -snapshot source commit: 034f409841d2edaf7f140fc7475a357c924a5930 -snapshot creation commit: 865a702341a82e61ab5ca1d5708a28005277cc0a -repo-pre-2 source tree: c28e015c74f4d012dfd070f887fabf75d4d4cb16 -repo-pre-3 snapshot tree: c28e015c74f4d012dfd070f887fabf75d4d4cb16 -tracked files per tree: 281 -``` - -The matching tree IDs establish path, file-mode, and blob equivalence at -snapshot creation. `repo-pre-2`, `repo-pre-1`, and `src-linux` are read-only -references for pre3 work. - -## Phase Scope - -Pre3 is an L1 mechanical naming-alignment stage. It contains four independent -source tasks: - -1. Rename `erofs_init_xattr_prefixes` and - `erofs_cleanup_xattr_prefixes` to the Linux-like lifecycle names - `erofs_xattr_prefixes_init` and `erofs_xattr_prefixes_cleanup`. -2. Rename the private ACL helper `erofs_getacl` to `erofs_get_acl`. -3. Rename `erofs_close_device` to `erofs_release_device_info` while retaining - its FreeBSD implementation. -4. Rename the private mount-state destructor `erofs_free_mount` to - `erofs_sb_free` while retaining its complete FreeBSD cleanup body. - -Only function names and their closed sets of declarations, definitions, and -call sites may change. Signatures, behavior, control flow, ordering, locking, -ownership, error handling, and file responsibilities must remain unchanged. - -## Required FreeBSD Differences - -- Xattr-prefix helpers continue to use `struct erofs_mount *`, not Linux - `struct super_block *`. -- ACL handling continues to use FreeBSD `struct vnode *`, `struct acl *`, and - integer errno conventions. -- Device teardown continues to release GEOM consumers, vnode references, and - cdev references in the existing order and under the existing locking rules. - It must not adopt the Linux callback signature or ownership model. -- Mount-state teardown continues to release all FreeBSD GEOM and private - filesystem state. A Linux-like name does not make the implementations or - lifecycle contracts interchangeable. - -## Excluded Work - -Pre3 does not include feature changes, correctness fixes, ABI changes, codec -work, xattr or ACL behavior changes, function reordering, structure layout -changes, file splitting, build-system changes, formatting cleanup, or repairs -to earlier planning and snapshot material. - -## Git Workflow - -The snapshot, this baseline document, and each of the four rename batches are -separate commits. Every commit must be pushed immediately to `xdm main` and -must not be squashed with another batch. Before each commit, its staged paths -must be checked against the task-specific allowlist. After each push, local -`HEAD`, `xdm/main`, and remote `main` must identify the same commit. - -## Validation Status - -Only static snapshot-equivalence, path-allowlist, Git-state, and documentation -checks were performed for pre3 initialization. - -```text -build: NOT RUN -QEMU: NOT RUN -test scripts: NOT RUN -smoke tests: NOT RUN -mount/runtime: NOT RUN -feature validation: NOT RUN -``` - -No compile-time or runtime PASS is claimed by this document. Existing reports -copied from `repo-pre-2` are historical material and are not fresh validation -of `repo-pre-3`. diff --git a/docs/pre3-smoke-test-20260812.md b/docs/pre3-smoke-test-20260812.md deleted file mode 100644 index e8bb40e..0000000 --- a/docs/pre3-smoke-test-20260812.md +++ /dev/null @@ -1,84 +0,0 @@ -# Pre3 Smoke Test Report - 2026-08-12 - -## Result - -The `repo-pre-3` plain/LZ4 differential smoke test **PASSed** within the scope documented below. - -| Field | Value | -|---|---| -| Run ID | `20260812T110037.575683Z-693361-ac21acee` | -| Process exit code | `0` | -| Guest return code | `0` | -| Runner stages | `12/12 passed` | -| Elapsed time | `18m51s` | -| DUT outer commit | `1a2361bfae2f6b4e7fc4c89d97d05f44553a5ba7` (`1a2361b`) | -| DUT path tree | `902a32fbfc1274d4404174346953d19e41c61f1f` | -| DUT content SHA256 | `07aa856f0c0d9d0d084dbcb5d743cf4831a7f2ab884b51941db7d1840b248c60` | -| KLD SHA256 | `59650f03029afc31658dcf3386105d001122d7702b8144e7c5925d9d9ed4accb` | -| Base image SHA256 | `67f359621f23a1d745f0889370cbb99a096cee3e99a0b2f3bb18fc7a91bf6fef` | -| Base image mode | `0444`, unchanged before and after | - -Primary evidence: - -- [result.json](/work/tests-dev/erofsstress/demo-result/repo-pre-3-smoke-20260812/20260812T110037.575683Z-693361-ac21acee/result.json) -- [summary.txt](/work/tests-dev/erofsstress/demo-result/repo-pre-3-smoke-20260812/20260812T110037.575683Z-693361-ac21acee/summary.txt) -- [guest evidence directory](/work/tests-dev/erofsstress/demo-result/repo-pre-3-smoke-20260812/20260812T110037.575683Z-693361-ac21acee/guest-evidence) -- [guest-result.json](/work/tests-dev/erofsstress/demo-result/repo-pre-3-smoke-20260812/20260812T110037.575683Z-693361-ac21acee/guest-evidence/guest-result.json) - -## Invocation - -```sh -./test.sh --dut /work/erofs-freebsd-pre/repo-pre-3 \ - --base-image /work/tests-dev/lfs/freebsd-15-dev-src-20260811.bp \ - --duration 10 \ - --output demo-result/repo-pre-3-smoke-20260812 -``` - -Evidence root: - -```text -/work/tests-dev/erofsstress/demo-result/repo-pre-3-smoke-20260812/20260812T110037.575683Z-693361-ac21acee/ -``` - -## PASS Scope - -The successful run covered: - -- FreeBSD guest boot and SSH readiness. -- DUT build with `WITH_ZSTDIO=0` and loading the exact generated `erofs.ko`. -- Creation and use of plain and LZ4 EROFS fixtures. -- Attach, mount, differential workload, unmount, and fixture cleanup. -- Four workers per fixture for five seconds each. -- Differential random, aligned, unaligned, full-file, and EOF reads. -- Directory enumeration, symbolic-link reads, and `fadvise` operations. -- Host and guest resource cleanup after the workload. - -The guest-side stages and fixture identities are recorded in [stages.txt](/work/tests-dev/erofsstress/demo-result/repo-pre-3-smoke-20260812/20260812T110037.575683Z-693361-ac21acee/guest-evidence/stages.txt) and [scenarios.txt](/work/tests-dev/erofsstress/demo-result/repo-pre-3-smoke-20260812/20260812T110037.575683Z-693361-ac21acee/guest-evidence/scenarios.txt). Per-worker evidence is available in the same guest evidence directory. - -## SKIPPED And Non-Claims - -The following tests were **SKIPPED** and have no execution evidence in this run: - -| Test | Area | Status | -|---|---|---| -| `TC079` | xattr prefix initialization, lookup, and cleanup | `SKIPPED` | -| `TC138` | ACL parsing and error paths | `SKIPPED` | -| `TC099` | multidevice mount and release paths | `SKIPPED` | - -This report therefore does **not** claim that xattr prefix handling, ACL behavior, multidevice behavior, ZSTD, LZMA, DEFLATE, or the full feature/regression suite passed. The PASS result is limited to the plain/LZ4 differential smoke scope listed above. - -## Integrity And Cleanup - -- The DUT content hash, commit, and tree were identical before and after the run; the test did not modify `repo-pre-3`. -- The read-only base image remained mode `0444` with the same SHA256 before and after the run. -- New QEMU PID `693836` exited gracefully. -- Test port `10000` was released. -- The qcow2 overlay was deleted and the port lock became available. -- Guard PID `26318` and guard port `9222` retained the same process identity, start time, and command hash. -- The generated result directory under `tests-dev` is evidence only and was not committed. - -## Honesty Review - -The post-run honesty audit **PASSed**. It found no concealed source fix, result substitution, or exaggerated coverage claim. The guest log filename `repo22-build.txt` and related `repo22` stage labels are inherited labels in the test harness; they do not identify the DUT. The DUT is independently bound to `repo-pre-3` by the recorded commit, path tree, content hash, archived source, and generated KLD hash. - -The complete machine-readable record remains authoritative for details not reproduced here: [result.json](/work/tests-dev/erofsstress/demo-result/repo-pre-3-smoke-20260812/20260812T110037.575683Z-693361-ac21acee/result.json). diff --git a/docs/pre5-baseline.md b/docs/pre5-baseline.md deleted file mode 100644 index ccfbf86..0000000 --- a/docs/pre5-baseline.md +++ /dev/null @@ -1,80 +0,0 @@ -# Pre5 Extraction Baseline - -## Repository Baseline - -- Outer starting commit: `6421919003ecfab5317dfa70959ab81cd9bbc990` -- Starting `repo-pre-5` tree: `f99462eee8898af209332ef202f8da07f2dd567e` -- Starting `repo-pre-5/src/super.c` blob: `c150d795df42ae4136fbd6e74c3b88903341b05a` -- Execution target: the existing `repo-pre-5` directory -- Source allowlist for the later extraction commits: `repo-pre-5/src/super.c` - -The fixed tree and blob identifiers above were read from Git before this -document was created. Pre5 works directly on `repo-pre-5`; it does not create -another snapshot. - -## Stage Scope - -Pre5 is an L2-S private cleanup responsibility extraction stage. It has two -source tasks: - -1. Extract the existing extra-device cleanup from `erofs_sb_free()` into - `static void erofs_free_dev_context(struct erofs_mount *em)`. -2. Extract the existing metabox and packed-inode cleanup from - `erofs_sb_free()` into - `static void erofs_drop_internal_inodes(struct erofs_mount *em)`. - -Both tasks are statement movement only. They must preserve FreeBSD GEOM, -vnode, allocation, and ownership semantics. They must not change behavior, -ABI, features, error handling, locking, logging, conditions, release calls, or -object ownership. - -## Protected Paths - -The following paths must remain unchanged during Pre5 execution: - -- `repo-pre-3/` -- `repo-pre-2/` -- `repo-pre-1/` -- `src-linux/` -- `planning/reject/` -- existing reports and other planning stages - -Except for this baseline document, no path outside -`repo-pre-5/src/super.c` belongs in the Pre5 source commits. - -## Cleanup-Order Invariant - -The effective cleanup order must remain exactly: - -```text -xattr prefixes --> metabox --> packed inode --> extra devices in reverse order --> primary device --> mount state -``` - -The extraction must not add pointer clearing, conditions, assertions, logs, -locks, return values, error handling, or header declarations. - -## Commit and Push Discipline - -Each Pre5 commit must contain one planned logical task and must be pushed -immediately to `xdm main`. The commits must not be squashed. Execution must -stop if a push fails or if local, tracking, and remote commit identities -diverge. - -## Validation Status at Baseline - -| Validation item | Status | -|---|---| -| Static Git baseline capture | RECORDED | -| Source extraction tasks | NOT RUN | -| Build with `WITH_ZSTDIO=0` | NOT RUN | -| Build with `WITH_ZSTDIO=1` | NOT RUN | -| QEMU smoke testing | NOT RUN | -| Manual or automated tests | NOT RUN | - -No build, QEMU run, feature validation, regression test, or runtime result is -claimed by this document. diff --git a/docs/pre5-completion-20260812.md b/docs/pre5-completion-20260812.md deleted file mode 100644 index a956da2..0000000 --- a/docs/pre5-completion-20260812.md +++ /dev/null @@ -1,268 +0,0 @@ -# Pre5 Completion Report - -Date: 2026-08-12 - -## Conclusion - -| Area | Result | Notes | -|---|---|---| -| Planned source extraction | PASS | Both private cleanup helpers are present in `repo-pre-5/src/super.c`. | -| Static correctness review | PASS | No High or Medium findings; statement order and FreeBSD cleanup behavior are preserved. | -| Commit recoverability | PASS after correction | The initial split commits were non-destructively reverted and replaced by one independently revertible atomic source commit. | -| `WITH_ZSTDIO=0` build | PASS | Exit code 0, zero warnings, zero errors. | -| Module load | NOT RUN | The produced `erofs.ko` was not loaded. | -| QEMU functional testing | NOT RUN | No functional or regression test was run for Pre5. | -| Pre5 smoke test | DEFERRED | Deferred for a combined Pre5/Pre6 smoke run. | - -Pre5 is complete for its planned source changes, static review, and required -build gate. This report does not claim runtime, feature, regression, or smoke -test coverage. - -The final Pre5 source commit is -`4535cccaf1ca1837f0a2d1cca526e5f138a23c6d`. This report is created after that -commit and, when committed, its documentation commit will therefore follow the -final source commit. - -## Scope - -Pre5 directly modified `repo-pre-5`; no new repository snapshot was created. -The starting planning commit was: - -```text -6421919003ecfab5317dfa70959ab81cd9bbc990 -docs: plan repo-pre-5 cleanup extraction phase -``` - -The baseline documentation commit was: - -```text -0666800fa529a8869757da85f2e69b8e1dd2c9f6 -docs: record pre5 extraction baseline -``` - -The baseline recorded the following source identity: - -```text -repo-pre-5/src/super.c blob: -c150d795df42ae4136fbd6e74c3b88903341b05a -``` - -The source scope was limited to extracting two existing cleanup regions from -`erofs_sb_free()` into private helpers. No feature, ABI, error handling, -locking, logging, ownership, or cleanup policy change was intended. - -## Final Implementation - -The final implementation is in [`src/super.c`](../src/super.c). - -### Extra-device cleanup - -`erofs_free_dev_context()` is a `static void` helper with exactly one -definition and one call. It contains only the existing extra-device cleanup: - -```c -static void -erofs_free_dev_context(struct erofs_mount *em) -{ - unsigned int i; - - if (em->devs != NULL) { - for (i = em->extra_devices; i > 0; --i) - erofs_release_device_info(&em->devs[i - 1]); - free(em->devs, M_EROFS); - } -} -``` - -The reverse close order is unchanged. The primary device remains outside this -helper and is still released separately by `erofs_sb_free()`. - -### Internal-inode cleanup - -`erofs_drop_internal_inodes()` is a `static void` helper with exactly one -definition and one call. It contains only the existing metabox and packed -inode releases: - -```c -static void -erofs_drop_internal_inodes(struct erofs_mount *em) -{ - if (em->metabox_en != NULL) - free(em->metabox_en, M_EROFS); - if (em->packed_inode != NULL) - free(em->packed_inode, M_EROFS); -} -``` - -This remains a FreeBSD allocation cleanup path. It does not copy Linux -`iput()` or Linux inode-lifecycle semantics. - -### Preserved cleanup order - -The effective cleanup order remains: - -```text -xattr prefixes --> metabox --> packed inode --> extra devices in reverse order --> primary device --> mount state -``` - -The final caller is: - -```c -static void -erofs_sb_free(struct erofs_mount *em) -{ - if (em == NULL) - return; - erofs_xattr_prefixes_cleanup(em); - erofs_drop_internal_inodes(em); - erofs_free_dev_context(em); - erofs_release_device_info(&em->dif0); - free(em, M_EROFS); -} -``` - -Static expansion of both helpers produces the baseline statement sequence. -No condition, loop direction, release function, pointer clearing, lock, errno, -log message, return value, or ownership rule was added or changed. - -## Commit Timeline - -| Commit | Purpose | Result | -|---|---|---| -| `6421919` | Establish the Pre5 execution plan | Baseline planning point. | -| `0666800` | Record the source baseline and validation status | Documentation only. | -| `52dca2e` | Initially extract `erofs_free_dev_context()` | Static behavior correct. | -| `e9b9fb2` | Initially extract `erofs_drop_internal_inodes()` | Static behavior correct. | -| `c406813` | Revert `e9b9fb2` | Non-destructively removed the second helper first. | -| `20733bd` | Revert `52dca2e` | Restored the exact baseline `super.c` blob. | -| `4535ccc` | Atomically introduce both cleanup helpers | Final source implementation. | - -The initial two source commits were behaviorally correct. The static audit -found a commit-organization defect: after `e9b9fb2`, the earlier `52dca2e` -could not be independently reverted from the final HEAD without conflict -because both commits edited the same tightly coupled `erofs_sb_free()` region. - -No history rewrite or destructive reset was used. The correction was: - -1. Revert `e9b9fb2` with `c406813`. -2. Revert `52dca2e` with `20733bd`. -3. Confirm that `super.c` returned to baseline blob - `c150d795df42ae4136fbd6e74c3b88903341b05a`. -4. Reintroduce both helpers in atomic commit `4535ccc`. - -The final `super.c` blob is: - -```text -03b92560bb5ef01f322b0d052f84bc3c577ef829 -``` - -This is byte-for-byte identical to the correct source state at `e9b9fb2`. -The final commit `4535ccc` can be reverted without conflict and restores the -baseline blob. - -The original plan preferred one source task per commit. That rule was adjusted -because the two extractions share one cleanup sequence and one caller region; -separate commits weakened independent rollback despite preserving behavior. -One atomic source commit provides a clearer and mechanically verifiable -rollback boundary without changing the planned source result. - -## Static Review Evidence - -The final independent static review result was PASS with no High or Medium -findings. - -Verified properties: - -- Only `repo-pre-5/src/super.c` differs from the source baseline. -- Both helpers are `static void` and each has one definition and one call. -- Extra devices are still released in reverse order before `em->devs` is - freed. -- Metabox cleanup still precedes packed-inode cleanup. -- The primary device remains a separate release after the extra-device - context. -- The mount state remains the final allocation released. -- Expanding both helpers recovers the original cleanup statement sequence. -- No header, exported interface, ABI, feature, condition, lock, errno, log, or - ownership change was introduced. -- No deferred correctness, codec, descriptor, formatting, or feature work was - mixed into Pre5. -- `repo-pre-1/`, `repo-pre-2/`, `repo-pre-3/`, `src-linux/`, planning files, - reject records, and earlier reports remained protected from source changes. - -The source result therefore satisfies the intended L2-S responsibility -extraction while preserving the necessary FreeBSD cleanup implementation. - -## Build Evidence - -The required build gate was executed in the existing FreeBSD 15 VM with PID -`26318`. No new VM was started for this build. - -Command: - -```sh -FREEBSD_SRC=/root/pre5-build-gate-20260812T125645Z/freebsd-src \ -WITH_ZSTDIO=0 ./build.sh -``` - -Result: - -| Evidence | Value | -|---|---| -| Exit code | `0` | -| Compiler warnings | `0` | -| Compiler errors | `0` | -| Build log | `repo-pre-5/build/pre5-zstdio0-20260812T125645Z/build.log` | -| Module | `repo-pre-5/build/pre5-zstdio0-20260812T125645Z/erofs.ko` | -| Module size | `78,248` bytes | -| Module SHA256 | `0a2928a711715a22dfe243a536f514395acd52af3cec5515bd4bb003e42a14ac` | - -The build artifacts are under the ignored `repo-pre-5/build/` directory and -must not be committed. The module was not loaded, and the build result alone -does not establish runtime behavior. - -## Not Run And Deferred - -| Item | Status | Meaning | -|---|---|---| -| Load produced `erofs.ko` | NOT RUN | Module load and unload behavior were not checked. | -| QEMU functional testing | NOT RUN | No functional test VM run was performed for Pre5. | -| Feature or regression suite | NOT RUN | No feature-completeness claim is made. | -| Pre5 standalone smoke test | DEFERRED | Deferred by user direction and risk assessment. | -| Combined Pre5/Pre6 smoke test | REQUIRED LATER | Must be performed after Pre6 before claiming runtime coverage. | - -The Pre5 changes only extract existing private cleanup statements, and the -static review plus build gate found no source or compiler issue requiring an -immediate standalone smoke run. To avoid duplicating a relatively expensive VM -cycle, the smoke test is deferred and will be combined with Pre6. - -The combined Pre5/Pre6 smoke run must cover at least: - -1. Plain and LZ4 basic build, module load, mount, read, readdir, unmount, and - cleanup. -2. `TC099` multi-device success, missing-device failure, and device cleanup. -3. A packed-inode/metabox fixture mount and unmount path that exercises - internal-inode cleanup. - -None of these deferred checks is claimed as passed by this report. - -## Residual Risk - -- The compiled module has not been loaded, so loader, symbol-resolution, and - unload behavior remain unverified for the final source commit. -- Failure and partial-initialization paths have only been reviewed statically. -- Multi-device cleanup has not been exercised at runtime after extracting - `erofs_free_dev_context()`. -- Packed-inode and metabox cleanup has not been exercised at runtime after - extracting `erofs_drop_internal_inodes()`. -- `WITH_ZSTDIO=1` was not built in this stage. -- No QEMU smoke, feature suite, stress test, or regression test has been run - specifically against the final Pre5 source state. - -These risks are accepted for the current stage and are carried into the -combined Pre5/Pre6 smoke gate. Until that gate runs, Pre5 should be described -as static-review PASS and `WITH_ZSTDIO=0` build PASS, not runtime PASS. diff --git a/docs/pre6-baseline.md b/docs/pre6-baseline.md deleted file mode 100644 index 815b45b..0000000 --- a/docs/pre6-baseline.md +++ /dev/null @@ -1,99 +0,0 @@ -# Pre6 Initialization Baseline - -## Repository Baseline - -- Outer starting commit: `bfed0431693e7798bdecacd78b7e6258c2d93c5a` -- Starting `repo-pre-6` tree: `b4a40164394b378adc947d6ad14dc7c845e79b2c` -- Starting `repo-pre-6/src/super.c` blob: `03b92560bb5ef01f322b0d052f84bc3c577ef829` -- Execution target: the existing `repo-pre-6` directory -- Source allowlist for later extraction commits: `repo-pre-6/src/super.c` - -The tree and blob identities above were read from Git before this document was -created. Pre6 works directly on `repo-pre-6`; it does not create another source -snapshot. - -## Stage Scope - -Pre6 is an L2-I private mount-initialization responsibility extraction stage. -It contains three helper tasks: - -1. Extract one decoded external-device initialization block into - `static int erofs_init_device(...)`. -2. Extract packed-carrier initialization into - `static int erofs_init_packed_inode(struct erofs_mount *em)`. -3. Extract metabox initialization into - `static int erofs_init_metabox_inode(struct erofs_mount *em)`. - -These tasks move existing statements into private helpers. They must not change -features, behavior, public interfaces, error handling, allocation, cleanup, -logging, locking, GEOM operations, or object ownership. - -## Source Commit Strategy - -The source work is delivered in two commits: - -1. One independent commit for `erofs_init_device()`. -2. One atomic commit for `erofs_init_packed_inode()` and - `erofs_init_metabox_inode()`. - -Packed and metabox initialization remain one ownership unit because their -blocks are adjacent and a fragment-backed metabox can depend on the packed -carrier. No intermediate commit may contain only one internal-inode helper. - -Each commit, including this baseline commit, must be pushed immediately to -`xdm main`. Execution must stop if local HEAD, `xdm/main`, and remote `main` -do not agree after a push. - -## Protected Paths - -The following paths must remain unchanged during Pre6 execution: - -- `repo-pre-1/` -- `repo-pre-2/` -- `repo-pre-3/` -- `repo-pre-5/` -- `src-linux/` -- `planning/reject/` -- `1-code-similarity-review/` -- existing planning documents and reports - -Except for Pre6 documents under `repo-pre-6/docs/`, no path outside -`repo-pre-6/src/super.c` belongs in the planned source commits. - -## Behavior-Preservation Invariants - -- Preserve the order of every moved statement and every helper call. -- Preserve all conditions, loop direction, values, types, casts, and return - values. -- Preserve every errno and log message without translation or normalization. -- Preserve allocations, frees, NULL assignments, and failure paths. -- Preserve the current owner and lifetime of every buffer, device, inode, and - mount resource. -- Keep `erofs_open_device()` unchanged as the FreeBSD GEOM open transaction. -- Keep external-device slot lookup and ascending iteration in - `erofs_scan_devices()`. -- Keep packed inode initialization before metabox inode initialization. -- Keep `erofs_mountfs()` as the common failure owner that calls - `erofs_sb_free()`. -- Add no rollback, validation, cleanup, logging, locking, or defensive changes. -- Keep all new helpers private and add no header declarations. - -If an extraction requires a behavior, ownership, ordering, or errno change, -execution must stop instead of expanding Pre6 scope. - -## Validation Status at Baseline - -| Validation item | Status | -|---|---| -| Static Git baseline capture | RECORDED | -| Device initialization extraction | NOT RUN | -| Packed and metabox initialization extraction | NOT RUN | -| Static equivalence and independent revert gates | NOT RUN | -| Build with `WITH_ZSTDIO=0` | NOT RUN | -| Plain/LZ4 smoke test | NOT RUN | -| Complete `TC099` multi-device smoke test | NOT RUN | -| Positive `TC142` metabox smoke test | NOT RUN | -| Other build, QEMU, or runtime tests | NOT RUN | - -No source extraction, build, QEMU run, feature validation, regression test, or -runtime result is claimed by this baseline document. diff --git a/docs/pre7-baseline.md b/docs/pre7-baseline.md deleted file mode 100644 index 35ce9b0..0000000 --- a/docs/pre7-baseline.md +++ /dev/null @@ -1,55 +0,0 @@ -# Pre7 Helper-Alignment Baseline - -## Repository Baseline - -- Execution-start commit: `3f5f783b9f316af6d3887421251cc79909aed934` -- Starting `repo-pre-7` tree: `909ffaa0f586a51933f4220a8fe67d963d5b1dff` -- Starting `repo-pre-6` tree: `909ffaa0f586a51933f4220a8fe67d963d5b1dff` -- Starting `repo-pre-7/src/internal.h` blob: `253a30a595f9740ce97de865cc6f255f8eda2b74` -- Starting `repo-pre-7/src/inode.c` blob: `c753c14fdc48ae53e6dd2554cb9dc4a5b8a6c569` -- Starting `repo-pre-7/src/xattr.c` blob: `cef4db2a8d03be028c0344941cea60def9cd84ce` -- Execution target: the existing `repo-pre-7` directory - -The tracked `repo-pre-7` baseline is an exact snapshot of `repo-pre-6` at the -execution-start commit. The identities above were read from Git objects at -that commit, not inferred from the live worktree. - -Uncommitted changes were already present in `repo-pre-7/src/internal.h` and -`repo-pre-7/src/inode.c` when this document was written. This baseline does -not assess, validate, or claim completion of those changes. - -## Exact Source Scope - -Pre7 contains exactly two source tasks: - -1. Rename the private inode-location helper from - `erofs_nid_to_offset()` to `erofs_iloc()` in - `repo-pre-7/src/internal.h` and `repo-pre-7/src/inode.c`. This is an - identifier-only alignment; the FreeBSD signature, implementation, - callers, metabox handling, overflow checks, and failure behavior remain - unchanged. -2. Extract the private `erofs_xattr_prefix()` mapping helper from - `erofs_xattr_namespace_prefix()` in `repo-pre-7/src/xattr.c`. The existing - wrapper retains FreeBSD namespace validation, namespace matching, errno - selection, and all existing get/list call sites. - -No other source file, helper cleanup, behavior change, public interface -change, formatting pass, or deferred planning item belongs in Pre7. - -## Validation Status - -Per the current execution instruction, this baseline-recording step performs -no build or test activity. Historical Pre6 evidence is not a Pre7 result. - -| Validation item | Status | -|---|---| -| Pre7 source implementation validation | NOT RUN | -| Static reference and behavior-matrix checks | NOT RUN | -| Independent source-commit revert checks | NOT RUN | -| `WITH_ZSTDIO=0` build | NOT RUN | -| QEMU smoke testing | NOT RUN | -| Manual or automated runtime tests | NOT RUN | -| CI testing | SKIPPED | - -This is an implementation baseline only. It is not a completion report and -does not claim that either Pre7 source task is complete or correct. diff --git a/docs/pre8-baseline.md b/docs/pre8-baseline.md deleted file mode 100644 index a4b1f52..0000000 --- a/docs/pre8-baseline.md +++ /dev/null @@ -1,86 +0,0 @@ -# Pre8 Responsibility-Alignment Baseline - -## Repository Identity - -The following identities were read directly from Git objects at execution-start -commit `475b62437f89a8a98e0e3a1d1628481768525cb2`: - -```text -execution-start commit: 475b62437f89a8a98e0e3a1d1628481768525cb2 -execution-start tree: 3f0bc6be8324446440ce578f03d892c95f73591c -repo-pre-7 tree: b8f9af2cd55225c4348b79ff5910ae6fc83cd517 -repo-pre-8 tree: b8f9af2cd55225c4348b79ff5910ae6fc83cd517 -``` - -The equal `repo-pre-7` and `repo-pre-8` tree identities establish that -`repo-pre-8` is an exact tracked snapshot of `repo-pre-7` at the execution -start. This relationship is based on Git object identities, not a live -worktree comparison. - -Relevant source identities at the same commit are: - -```text -mode blob path -100644 e06822e363d9122a39256494bde7d12cfea1f7c9 repo-pre-8/src/decompressor.c -100644 ff595fe009679a4e950eb3743b2f152fac4aad31 repo-pre-8/src/decompressor_lzma.c -100644 f3e48cd2016608aaf84ed5e80151782a6c5513f7 repo-pre-8/src/decompressor_deflate.c -100644 23756af263ceb148ca50fbb8cd1ae80ba3fac4cd repo-pre-8/src/decompressor_zstd.c -100644 cac9ee15f81a1607a03fbab3466cf89c588872c8 repo-pre-8/src/internal.h -100644 2257a2299b3bcf3ab15978d723843f26d0b4b346 repo-pre-8/src/xattr.c -``` - -These values define the immutable comparison baseline for Pre8. Concurrent -or later worktree changes are not assessed by this document. - -## Execution Scope - -Pre8 is limited to the following private responsibility-alignment tasks: - -1. Move `z_erofs_load_lzma_config()` from the core decompressor file to the - LZMA backend without changing its name, signature, validation order, - state updates, or error behavior. -2. Move `z_erofs_load_deflate_config()` to the DEFLATE backend with its - existing format checks, window-bit limits, state updates, and errors - preserved. -3. Move `z_erofs_load_zstd_config()` to the ZSTD backend while preserving the - availability check, configuration checks, window-log limit, diagnostics, - and `WITH_ZSTDIO` behavior. -4. Replace the duplicated inline and shared xattr traversal paths with shared - private iterator helpers while preserving lookup priority, list order, - namespace and errno timing, validation differences, output accounting, and - buffer ownership. - -The source allowlist is restricted to the six files identified above. This -baseline file is the only documentation path used by the baseline step. - -Pre8 does not include superblock extraction, a new `compress.h` contract, -descriptor tables, decode callback ABI changes, codec primitive renaming, -typed errno conversion, Linux page or folio abstractions, broad function -reordering, or any item already recorded under `planning/reject/`. - -## Initial Validation Status - -This document records the execution baseline only. No implementation, -correctness, build, or runtime conclusion is made here. - -| Validation item | Initial status | -|---|---| -| Pre8 source implementation | NOT RUN | -| Source allowlist and protected-path audit | NOT RUN | -| `git diff --check` | NOT RUN | -| Loader definition, declaration, reference, and body-equivalence checks | NOT RUN | -| Xattr iterator invariant and behavior review | NOT RUN | -| Direct-parent restoration checks for source commits | NOT RUN | -| Final-HEAD inverse-patch compatibility checks | NOT RUN | -| `WITH_ZSTDIO=0` build | NOT RUN | -| `WITH_ZSTDIO=1` build | NOT RUN | -| Basic plain/LZ4 QEMU mount, read, readdir, and unmount smoke | NOT RUN | -| Pre7 deferred QEMU coverage | NOT RUN | -| Pre8 xattr QEMU coverage | NOT RUN | -| Pre8 codec QEMU coverage | NOT RUN | -| QEMU cleanup and zero-residual-state checks | NOT RUN | -| Completion and evidence-honesty review | NOT RUN | - -Historical evidence from an earlier snapshot is not a Pre8 result. Every -status above remains `NOT RUN` until supported by fresh evidence from the -final Pre8 DUT. This file must not be interpreted as a completion report. diff --git a/docs/pre8-completion-and-validation.md b/docs/pre8-completion-and-validation.md deleted file mode 100644 index e2b67a1..0000000 --- a/docs/pre8-completion-and-validation.md +++ /dev/null @@ -1,314 +0,0 @@ -# Pre8 Completion and Validation - -## Final Verdict - -Pre8 overall status is **FAIL**. - -The required implementation, static review, revert review, build matrix, basic -plain/LZ4 smoke, and selected test suite were all executed. The selected suite -did not pass its required gate. In particular, TC004 produced a definite DUT -failure while twelve other DUT verdicts were blocked by test-harness defects or -prior test residue. Under the status definition in `planning/pre8`, a required -action that ran and failed a gate is `FAIL`, not `PASS` or `PARTIAL`. - -This overall verdict does not erase the narrower successful results: - -| Validation layer | Verdict | Scope | -| --- | --- | --- | -| Source implementation | PASS | Planned loader placement and xattr iterator work is present in the final source tree. | -| Static source review | PASS | Allowlist, token-equivalence, references, xattr invariants, and protected paths passed review. | -| Revert and history review | PASS | Accepted replacement commits passed their defined direct-parent and final-HEAD checks; historical failures remain disclosed. | -| Four build configurations | PASS | `default`, `debug`, `zstdio0`, and `zstdio1` built and passed module load/unload smoke. | -| Basic plain/LZ4 smoke | PASS | Independent run `20260812T210345.913633Z-770842-a2868dd9`. | -| Selected 19-case suite | FAIL | Automation: 5 PASS / 9 FAIL / 4 ERROR / 1 NOT_RUN. Audited DUT: 6 PASS / 1 FAIL / 12 BLOCKED. | -| Pre8 overall | **FAIL** | A required selected-suite gate ran and failed. | - -No claim is made that Pre8, repo22, the LZMA issue, or the test harness was -fixed by this validation work. - -## Final DUT Identity - -The final repository identity used by both selected-suite and basic-smoke -evidence is: - -```text -parent repository commit: d1f5b686e8945d06b6e8c0a0188b2262de0b4ac2 -parent repository tree: 19f450085cad49d06f6fe05d180313f13d4347f0 -repo-pre-8 tree: 497736695fcc4285760b24c5954c34d32b8af49f -repo-pre-8/src tree: cbf93a19df8d1ce2fb66e2aeb7276a4ea6d4dcc2 -``` - -Selected-suite DUT archive: - -```text -bbcfd8a6a28faba3f7c5c7e827bde5578774be6e08b83d05485d58ee16a7fb6f -``` - -Basic-smoke DUT archive: - -```text -402459ba6a6f55696d06474f7494a69ef1a5fe49063d565cacadece37e8b9ce1 -``` - -The different archive hashes belong to different runners and archive -procedures. Both reports bind their run to the same final repository commit -and `repo-pre-8/src` tree. - -## Source Delivery - -### Accepted implementation - -The final accepted source changes are: - -1. `3d28d96fc48753231b1899fce6189e586cf988c6` - (`erofs: unify inline and shared xattr iteration`) -2. `aac1412056f9ae7269f7c0f85c0233ade8cc662a` - (`erofs: move codec config loaders atomically`) - -The xattr commit introduces common private inline/shared iteration while -preserving the reviewed lookup order, list order, namespace and errno timing, -inline/shared validation differences, buffer ownership, and output accounting. - -The atomic loader replacement moves the LZMA, DEFLATE, and ZSTD configuration -loaders to their backend files. The loader bodies remain token-equivalent to -the baseline, dispatch calls remain in the core, decode functions are not -changed, and no Linux page/folio or descriptor ABI was introduced. - -### Superseded split-loader history - -The following pushed commits remain in history and must not be described as -accepted independent delivery units: - -| Commit | Historical final-HEAD inverse result | Final disposition | -| --- | --- | --- | -| `dd7f2483d468ec4397a863c7a871391ea1bc2c4c` | FAIL: conflict in `decompressor.c`; `internal.h` auto-merged as staged content | Superseded by `aac1412`; failure permanently retained. | -| `0dba0ea223bee8626b8cd3371562f87ed975a99f` | FAIL: conflict in `decompressor.c`; `internal.h` auto-merged as staged content | Superseded by `aac1412`; failure permanently retained. | -| `7c47f6d` | PASS | Superseded with the split-loader group by `aac1412`. | - -All three split commits passed direct-parent restoration at the point where -they were introduced. That fact does not overwrite the two final-HEAD inverse -failures. The split group was reverted by `96f041f`, then replayed as the one -accepted atomic commit `aac1412`. Commit `3d28d96` was not superseded. - -### Static and revert verdict - -Final static and revert status is **PASS**: - -- the implementation remained within the planned source allowlist; -- protected trees and unrelated paths were unchanged; -- all three moved loader bodies matched their baseline implementations; -- declaration, definition, and dispatch-call closure was correct; -- the ZSTD availability and `windowlog > 10` rejection order was preserved; -- codec decode bodies were unchanged; -- the xattr iterator behavior and ownership matrix passed independent review; -- `aac1412` restored its direct parent exactly when reverted; -- `aac1412` independently reverted from final HEAD without changing xattr; -- `3d28d96` independently reverted from final HEAD without changing the - accepted loader layout; -- final `repo-pre-8/src` matched the reviewed functional source tree; -- `git diff --check` and worktree cleanliness checks passed at static handoff. - -The detailed history record is -`repo-pre-8/docs/pre8-loader-history-correction.md`. - -## Build Validation - -The selected-suite runner built four module configurations. Every -configuration passed build completion, SHA256 recording, unresolved-symbol -checks, and an actual `kldload`/`kldunload` smoke before case execution. - -| Configuration | Verdict | SHA256 | Size | -| --- | --- | --- | --- | -| `default` | PASS | `348cb1f5a91d47e1412747d285c795c49375a61a66cfb3d35d22f38ecfde6ea0` | 47,272 bytes | -| `debug` | PASS | `274189804511d249b34fbb3c223e9525b07c7c6266ec39d016ad2f33185001c7` | **UNKNOWN**; the harness did not persist size evidence before VM destruction | -| `zstdio0` | PASS | `348cb1f5a91d47e1412747d285c795c49375a61a66cfb3d35d22f38ecfde6ea0` | 47,272 bytes; byte-identical to `default` | -| `zstdio1` | PASS | `6cdb8e01fdac7805cb7ae12a9d1804b00d3e5cf37ef8b2f583137d36ee70426e` | 51,368 bytes | - -The missing debug size is not reconstructed or inferred. No KLD or other -binary was committed. - -## Basic Plain/LZ4 Smoke - -Independent basic smoke run: - -```text -run ID: 20260812T210345.913633Z-770842-a2868dd9 -automation: PASS -runner exit: 0 -DUT result: PASS for plain and LZ4 basic differential scope -guest: FreeBSD 15.0-RELEASE-p8 amd64 -KLD SHA256: 527fe80ad7307d0200cfc4fec84c6fb86b914e89889e4a42ff0613fc24465015 -KLD size: 47,272 bytes -``` - -All twelve runner stages passed. The run created a deterministic corpus with -256 regular files, 9 symlinks, and 43 directories. Both the plain and LZ4 -images mounted read-only, passed a 307-entry differential preflight, completed -all four workers, and unmounted cleanly. The test QEMU exited gracefully, its -overlay and large temporary data were removed, and ports 10000 and 10001 were -released. - -This result covers only plain/LZ4 basic mount, traversal, read, differential, -unmount, and cleanup behavior. It does not override the selected-suite -failures or blocked verdicts. - -Evidence: - -- `/work/tests-dev/erofsstress/demo-result/repo-pre-8-smoke-20260812/20260812T210345.913633Z-770842-a2868dd9/audit-report.md` -- `/work/tests-dev/erofsstress/demo-result/repo-pre-8-smoke-20260812/20260812T210345.913633Z-770842-a2868dd9/result.json` -- tests-dev commit `374b8e3ab4b0df24d6761802b70c694e2896d5fd` - -## Selected Suite - -Primary run: - -```text -run ID: pre8-required-20260812T194817Z -cases: 19 -automation: 5 PASS / 9 FAIL / 4 ERROR / 1 NOT_RUN -runner exit: 3 -duration: 1892.44 seconds -audited DUT: 6 PASS / 1 FAIL / 12 BLOCKED -``` - -The automation status is the runner's result. The DUT status is a separate -evidence audit. An automation failure caused by a harness defect is not changed -to automation PASS. Likewise, a positive command observed before a missing -negative check does not make the full DUT case PASS. - -### Per-case matrix - -| Case | Automation | DUT | Audited observation | -| --- | --- | --- | --- | -| TC005 | FAIL | BLOCKED | Inline listing and both requested user xattr values succeeded. The missing-xattr `ENOATTR` assertion was never invoked because the truss output directory did not exist. | -| TC013 | PASS | PASS | Invalid NID returned `EINTEGRITY` (`ERR#97`) twice; mount, unmount, and zero-state checks passed. | -| TC079 | PASS | PASS | Packed user and trusted prefix values matched expected data and cleanup passed. | -| TC080 | FAIL | BLOCKED | Four packed-prefix values were correct. The missing-suffix `ENOATTR` assertion was not invoked because the truss output directory was absent. | -| TC082 | FAIL | BLOCKED | ACL bytes and ordering were observed, but automation compared spaced hex against an unspaced prefix; the user-namespace negative assertion also was not invoked. | -| TC067 | FAIL | PASS | Both shared files, the local xattr, and listing matched the fixture. Automation incorrectly expected one additional trailing `!`. | -| TC068 | FAIL | BLOCKED | Shared listing succeeded. The nonexistent lookup was not invoked because truss could not create its output file. | -| TC069 | PASS | PASS | Three shared xattrs were listed and all exact values were read; cleanup passed. | -| TC081 | PASS | PASS | Metabox shared, shared-prefix, and packed-prefix xattrs were listed and read from both files; cleanup passed. | -| TC117 | FAIL | BLOCKED | Both corrupt fixtures mounted and valid local shared data remained readable. Corrupt-entry errno assertions were not invoked because truss failed first. | -| TC135 | PASS | PASS | Metabox, packed, and primary-prefix fallback values matched expected data. | -| TC138 | FAIL | BLOCKED | Valid unordered and empty-header ACLs were read. Three malformed ACL assertions were not invoked because the scratch directory was absent. | -| TC140 | FAIL | BLOCKED | Positive compressed and fragment metabox carrier hashes and xattrs passed. Four negative mount commands were not invoked because truss failed opening its trace. | -| TC142 | FAIL | BLOCKED | Positive fragment-backed compressed metabox hash and xattrs passed. Four negative mount commands were not invoked for the same reason. | -| TC004 | ERROR | **FAIL** | LZMA image mounted, but SHA256 of the mounted 8 MiB `level.dat` timed out after 30 seconds. The process remained running and left the mount and `md0` busy. | -| TC084 | ERROR; follow-up FAIL | BLOCKED | Primary run was contaminated by TC004 residue. Fresh follow-up mounted LZ4 and found the target, but guest `dump.erofs` was absent before content verification. | -| TC102 | ERROR; follow-up FAIL | BLOCKED | Primary run was contaminated by TC004 residue. Fresh follow-up mounted DEFLATE, but missing guest `dump.erofs` prevented algorithm and content verification. | -| TC105 | ERROR; follow-up FAIL | BLOCKED | Primary run was contaminated by TC004 residue. Fresh follow-up mounted ZSTD with `zstdio1`, but missing guest `dump.erofs` prevented verification. | -| TC145 | NOT_RUN; follow-up ERROR | BLOCKED | Both zstdio modules built and initial load smoke passed. The follow-up queried and unloaded module name `erofs` instead of the loaded artifact name, so functional mount/read gates were not reached. | - -### Definite DUT failure - -TC004 is a definite observed DUT failure for this test run: - -1. `lzma-level6.erofs` attached as `md0`. -2. The read-only EROFS mount succeeded. -3. The mounted `level.dat` existed. -4. SHA256 of the source file completed and returned - `ddda39737f0f6093e828a032ec161511fefbb1fa361bc6cbffdbc91e48e4c461`. -5. SHA256 of the mounted 8 MiB file did not finish within 30 seconds. -6. The timed-out `sha256` process remained alive. -7. Unmount and `mdconfig -d` returned `Device busy`. - -This evidence does **not** prove that Pre8 introduced the problem. No matching -Pre7 or pre-change comparison run with the same image, guest, module build, -and command is available. The issue is recorded separately in -`issues/pre8-lzma-read-timeout.md` without an attribution claim. - -### Codec follow-up - -Fresh follow-up run: - -```text -run ID: pre8-codec-followup-20260812T202059Z -cases: TC084, TC102, TC105, TC145 -automation: 3 FAIL / 1 ERROR -runner exit: 1 -duration: 1136.92 seconds -audited DUT: 4 BLOCKED -``` - -The follow-up isolated these cases from TC004 residue, but it did not produce -codec functional PASS verdicts. TC084, TC102, and TC105 were blocked by absent -guest `dump.erofs`. TC145 was blocked by incorrect module-name lookup/unload -and cleanup behavior in the harness. These results remain FAIL/ERROR in -automation and BLOCKED for the DUT. - -The harness findings are recorded in -`issues/pre8-test-harness-blockers.md`. No harness modification is part of -Pre8 source delivery. - -Selected-suite evidence: - -- `/work/tests-dev/demo-result/repo-pre-8-smoke-20260812/summary.md` -- `/work/tests-dev/demo-result/repo-pre-8-smoke-20260812/summary.json` -- `/work/tests-dev/demo-result/repo-pre-8-smoke-20260812/pre8-required-20260812T194817Z/summary.json` -- `/work/tests-dev/demo-result/repo-pre-8-smoke-20260812/pre8-required-20260812T194817Z/evidence/` -- `/work/tests-dev/demo-result/repo-pre-8-smoke-20260812/pre8-codec-followup-20260812T202059Z/summary.json` -- `/work/tests-dev/demo-result/repo-pre-8-smoke-20260812/pre8-codec-followup-20260812T202059Z/evidence/` -- tests-dev commit `46e6840f1b8918414af76c9a5731322c697eee8b` - -## Fixtures and Immutable Inputs - -The common base image was not modified: - -```text -path: /work/tests-dev/lfs/freebsd-15-dev-src-20260811.bp -format: qcow2 -mode: 0444 -size: 16,515,530,752 bytes -SHA256: 67f359621f23a1d745f0889370cbb99a096cee3e99a0b2f3bb18fc7a91bf6fef -``` - -Selected primary fixture deployment identities: - -| Group | Manifest SHA256 | Checksum-list SHA256 | -| --- | --- | --- | -| G1 | `9ffe9e695a6544cb010a5d4ba1e0c2c09d7455e6d7e28cc81da54d2dd954f2f2` | `db416004ff6b00ad286e50da506168b35a544a7da5619042225198843e4c741c` | -| G4 | `6200842757dae30f3a5b0683d726d339a24e55dff98cd084597462289a9308aa` | `229c84c5436cf9b5b659ded375097fba13621c20a2cf2d845f146df737836622` | -| G5 | `c9540baafb53b3783233c3950f4d8548c28585cd32335dadc14db613692a8978` | `d08138b1bd51579cbf5461a73abce9d7fddc9cdb8ace1329c9f1581d41e1095f` | - -Follow-up G5 deployment identities: - -```text -manifest: 9be85e48115f2dd2cae058f2a4ea7438512f28e647c30476d049d6b8025b02fd -checksum list: 7513425c98400f641523083bf0d06b2d548aa3163dfcdfe980dca2b0e1900acd -``` - -Deployment hashes can include run-specific metadata. Image, source, generator, -and checksum evidence remains beneath each run's `fixture-evidence/` and -per-case evidence directories. - -## Cleanup and Guard Integrity - -- The selected primary and follow-up QEMUs were force-destroyed by the runner - after their final errors. -- Their run-owned overlays were deleted. -- Ports 10000 and 10001 were released and closed. -- The independent basic-smoke QEMU shut down gracefully and its overlay was - removed. -- Guard PID 26318 retained its identity and TCP port 9222 remained active. -- Base-image SHA256, mode, and size were unchanged before and after the runs. -- The DUT commit and source tree remained unchanged and clean. -- No DUT source, repo22 source, base image, test source, fixture generator, or - `oldtests` content was modified during validation. - -Cleanup of the disposable VM does not change TC004's case-level cleanup result: -the case itself failed to terminate its read process and could not unmount or -detach `md0`. VM destruction only removed the run environment afterward. - -## Final Handoff - -Pre8 delivers the planned source-responsibility changes and passes static, -revert, build, and basic plain/LZ4 smoke validation. It does not satisfy the -full required validation gate because the selected suite contains one definite -DUT failure and twelve blocked DUT cases. The honest final status is therefore -**FAIL**. - -Open follow-up records: - -- `issues/pre8-lzma-read-timeout.md` -- `issues/pre8-test-harness-blockers.md` diff --git a/docs/pre8-loader-history-correction.md b/docs/pre8-loader-history-correction.md deleted file mode 100644 index bcaa406..0000000 --- a/docs/pre8-loader-history-correction.md +++ /dev/null @@ -1,74 +0,0 @@ -# Pre8 Loader History Correction - -## Scope - -This record corrects the commit organization of the Pre8 codec configuration -loader moves. It does not report a source behavior defect and does not rewrite, -amend, squash, or hide any pushed commit. - -The affected historical commits are: - -```text -dd7f2483d468ec4397a863c7a871391ea1bc2c4c LZMA loader move -0dba0ea223bee8626b8cd3371562f87ed975a99f DEFLATE loader move -7c47f6d ZSTD loader move -3d28d96fc48753231b1899fce6189e586cf988c6 xattr iterator change -``` - -## Recorded Results - -Each of the three loader commits restores its direct parent's complete tree -when reverted at the commit where it was introduced. Those direct-parent -revert checks are `PASS`. - -When each commit is reverted independently from final functional commit -`3d28d96`, the results are: - -| Commit | Final-HEAD independent revert | Result | -|---|---|---| -| `dd7f248` | Unmerged conflict in `decompressor.c`; `internal.h` auto-merged as a staged modification | FAIL | -| `0dba0ea` | Unmerged conflict in `decompressor.c`; `internal.h` auto-merged as a staged modification | FAIL | -| `7c47f6d` | Applies without conflict | PASS | -| `3d28d96` | Applies without conflict | PASS | - -The two failures are caused by overlapping patch context in `decompressor.c`. -In both cases, `internal.h` is automatically merged as a staged modification, -not left as an unmerged conflict. This is a commit organization problem, not -evidence of a codec implementation or xattr behavior problem. - -The `dd7f248` and `0dba0ea` final-HEAD results remain permanently recorded as -`FAIL`. The `7c47f6d` result remains `PASS`, but that commit is superseded with -the other two loader commits so the loader responsibility change has one -atomic acceptance unit. - -## Correction Strategy - -The three split loader commits will be reverted in reverse order without -rewriting history. Their exact combined five-path change will then be replayed -as one atomic replacement commit. The replacement is limited to: - -```text -repo-pre-8/src/decompressor.c -repo-pre-8/src/decompressor_lzma.c -repo-pre-8/src/decompressor_deflate.c -repo-pre-8/src/decompressor_zstd.c -repo-pre-8/src/internal.h -``` - -Commit `3d28d96` remains accepted and is not superseded. Its `xattr.c` content -must remain unchanged through the correction. - -The new acceptance objects are: - -1. the atomic replacement loader commit, which must restore its direct parent - exactly when reverted and must independently revert from final `HEAD` - without affecting xattr content; and -2. existing xattr commit `3d28d96`, which must independently revert from final - `HEAD` without affecting the final loader layout. - -## Validation Status - -This document records static Git evidence only. No build or QEMU test has been -run for this history correction. Build and runtime validation remain -`NOT RUN` until later Pre8 validation produces fresh evidence from the final -DUT. diff --git a/docs/pre9-baseline.md b/docs/pre9-baseline.md deleted file mode 100644 index b2688ea..0000000 --- a/docs/pre9-baseline.md +++ /dev/null @@ -1,191 +0,0 @@ -# Pre9 LZMA Diagnostic Implementation Baseline - -## Baseline Meaning - -The Pre9 planning phase ended when the planning documents were committed at -`d1ee2e05b82b5e8a8927861f1e981710532f2f23`. This file starts the -implementation phase and records identities only. It does not claim that the -TC004 root cause is known, that Pre8 introduced the observed timeout, or that -any source or test-harness fix has been selected or implemented. - -All values below were read from Git objects or the named evidence files on -2026-08-13 UTC. Historical Pre8 evidence is context for controlled Pre9 -diagnosis, not a Pre9 test result. - -## Main Repository Identity - -Repository: `/work/erofs-freebsd-pre` - -```text -branch: main -execution-start commit: d1ee2e05b82b5e8a8927861f1e981710532f2f23 -execution-start tree: 95a55ff912d007ca40bd578057e8a4cc6a21bbd9 -xdm/main: d1ee2e05b82b5e8a8927861f1e981710532f2f23 -remote xdm main: d1ee2e05b82b5e8a8927861f1e981710532f2f23 -initial dirty entries: 0 -``` - -Snapshot identities at the execution-start commit: - -```text -snapshot subtree src tree -repo-pre-7 b8f9af2cd55225c4348b79ff5910ae6fc83cd517 dc203534d7b5721905f8538026e4c59346106020 -repo-pre-8 1e24c992a5b071e6fdabfdc42d342b6ed5a91cf5 cbf93a19df8d1ce2fb66e2aeb7276a4ea6d4dcc2 -repo-pre-9 1e24c992a5b071e6fdabfdc42d342b6ed5a91cf5 cbf93a19df8d1ce2fb66e2aeb7276a4ea6d4dcc2 -``` - -The equal Pre8 and Pre9 object identities establish that Pre9 starts as an -exact tracked snapshot of Pre8. Pre7 is intentionally different and remains -the earlier controlled comparison point. - -## Conditional Source Allowlist Identity - -The primary conditional allowlist is `zdata.c`, `decompressor_lzma.c`, and -`internal.h`. The remaining three files require the additional decision gates -defined by the plan. Their baseline modes and blobs are: - -```text -gate mode blob path -primary 100644 54c1c88e5c368e7b5f6a16188c48406767850821 repo-pre-9/src/zdata.c -primary 100644 688acd704e28daa46369fa2e63e895bce5262774 repo-pre-9/src/decompressor_lzma.c -primary 100644 9821fe3a8b5fcaaeb3939d191f15e9ce637cc8d1 repo-pre-9/src/internal.h -extra 100644 ca5d33cd42145159d71e153f115128ac75258708 repo-pre-9/src/inode.c -extra 100644 b233f138d036a2b37394946b382034955dc2a1df repo-pre-9/src/erofs_vnops.c -extra 100644 51170741a0fb5eced814db898c1a233e8a23088a repo-pre-9/src/zmap.c -``` - -This identity table authorizes no source change by itself. Source edits remain -conditional on controlled evidence and the applicable decision gate. - -## tests-dev and Base Image Identity - -The external test repository was inspected without modification: - -```text -repository: /work/tests-dev -branch: main -current commit: 374b8e3ab4b0df24d6761802b70c694e2896d5fd -current tree: c438e72ebe94a6f10801999bfbdd36b43134d471 -xdm/main: 374b8e3ab4b0df24d6761802b70c694e2896d5fd -initial dirty entries: 27 untracked paths -dirty inventory SHA256: bca9ca20e85698d04dad820a11bd0bbb117492c967d6fd338e7acf61bb8fa957 -``` - -The pre-existing tests-dev dirty inventory was: - -```text -demo-result/FINAL-REPORT.md -demo-result/audit-checklist.md -demo-result/audit-findings.md -demo-result/current-status.md -demo-result/fix-plan.md -demo-result/fix-verification-report.json -demo-result/progress-report.md -demo-result/repo-pre-6-smoke-20260812/ -demo-result/repo22-full-20260811T065312Z/ -demo-result/repo22-full-20260811T071634Z/ -demo-result/repo22-full-20260811T073716Z/ -demo-result/repo22-full-20260811T082907Z/ -demo-result/repo22-full-20260811T095621Z/ -demo-result/test-execution-summary.md -erofsstress/demo-result/repo-pre-3-smoke-20260812/ -erofsstress/demo-result/repo-pre-6-smoke-20260812/ -guest/setup-build-env.sh -lfs/FreeBSD-15.0-RELEASE-src.txz -scripts/check-kernel-src.sh -scripts/complete-setup-and-test.py -scripts/install-kernel-sources.py -scripts/manual-fix.sh -scripts/quick-install-src.py -scripts/run-final-test.sh -scripts/setup-build-env-simple.py -scripts/setup-build-env.py -scripts/simple-install-kernel-src.sh -``` - -These paths are not owned by this baseline step and must not be staged, -modified, or removed as part of it. - -Immutable base image identity: - -```text -path: /work/tests-dev/lfs/freebsd-15-dev-src-20260811.bp -format: QEMU QCOW2 Image (v3) -mode: 0444 -size: 16,515,530,752 bytes -SHA256: 67f359621f23a1d745f0889370cbb99a096cee3e99a0b2f3bb18fc7a91bf6fef -``` - -The base image is an input only. It must not be modified during Pre9 testing. - -## Historical TC004 Evidence Identity - -The prior observation is preserved by tests-dev evidence commit -`46e6840f1b8918414af76c9a5731322c697eee8b`, tree -`7178de49f3f429796d6de71a910d71bfccd5745a`, with subject -`evidence: record repo-pre-8 required smoke`. - -```text -run ID: pre8-required-20260812T194817Z -case: TC004 - LZMA Compressed File Read -historical DUT commit: d1f5b686e8945d06b6e8c0a0188b2262de0b4ac2 -historical DUT src tree: cbf93a19df8d1ce2fb66e2aeb7276a4ea6d4dcc2 -DUT archive SHA256: bbcfd8a6a28faba3f7c5c7e827bde5578774be6e08b83d05485d58ee16a7fb6f -module SHA256: 348cb1f5a91d47e1412747d285c795c49375a61a66cfb3d35d22f38ecfde6ea0 -fixture image: G5/images/lzma-level6.erofs -fixture image SHA256: 32107a084b27362a093768b88746c37c2e99a74b9f1301a9d4046988479defe9 -source file: G5/sources/levels/level.dat -source file SHA256: ddda39737f0f6093e828a032ec161511fefbb1fa361bc6cbffdbc91e48e4c461 -deployment manifest: c9540baafb53b3783233c3950f4d8548c28585cd32335dadc14db613692a8978 -fixture checksum hash: d08138b1bd51579cbf5461a73abce9d7fddc9cdb8ace1329c9f1581d41e1095f -TC004 evidence SHA256: de6982a11fa368fbaad7d837586daa899789cd4cd8b9f698974a3ef4e251811f -``` - -Evidence paths: - -```text -/work/tests-dev/demo-result/repo-pre-8-smoke-20260812/pre8-required-20260812T194817Z/summary.json -/work/tests-dev/demo-result/repo-pre-8-smoke-20260812/pre8-required-20260812T194817Z/evidence/TC004/TC004_evidence.json -/work/tests-dev/demo-result/repo-pre-8-smoke-20260812/summary.md -/work/erofs-freebsd-pre/issues/pre8-lzma-read-timeout.md -``` - -The historical automation verdict was `ERROR` and the audited DUT verdict was -`FAIL`: mount and lookup succeeded, but hashing the mounted 8 MiB `level.dat` -did not finish within 30 seconds. PID 95693 remained active, and the mount and -`md0` stayed busy until the disposable VM was destroyed. Attribution remains -`UNKNOWN`. These facts neither prove a Pre8 regression nor select a Pre9 fix. - -## Initial Implementation Status - -| Item | Status | -|---|---| -| Pre9 planning | COMPLETE | -| Pre9 implementation baseline | RECORDED | -| tests-dev TC004 prerequisite corrections | NOT RUN | -| tests-dev remaining full-suite prerequisites | NOT RUN | -| Controlled Pre7 reproduction | NOT RUN | -| Controlled Pre8 reproduction | NOT RUN | -| Controlled unchanged Pre9 reproduction | NOT RUN | -| Manual SSH diagnosis | NOT RUN | -| Hypothesis decision gates | NOT RUN | -| Source decision | NOT RUN | -| Optional diagnostic instrumentation | NOT RUN | -| Pre9 source implementation | NOT RUN | -| Source static review | NOT RUN | -| Source direct-parent revert gate | NOT RUN | -| Source final-HEAD inverse gate | NOT RUN | -| `WITH_ZSTDIO=0` build | NOT RUN | -| `WITH_ZSTDIO=1` build | NOT RUN | -| TC004 | NOT RUN | -| Extended LZMA matrix | NOT RUN | -| Plain/LZ4 smoke | NOT RUN | -| Full regression | NOT RUN | -| QEMU cleanup and zero-residual-state checks | NOT RUN | -| Pre9 completion and evidence-honesty review | NOT RUN | -| Pre9 overall | NOT RUN | - -No diagnostic command, tests-dev prerequisite, controlled comparison, source -change, build, QEMU run, or manual SSH investigation has been performed for -Pre9 at this baseline. Fresh evidence is required before any status above can -advance or any root-cause or repair claim can be made. diff --git a/docs/pre9-cache-final-manual-validation.md b/docs/pre9-cache-final-manual-validation.md deleted file mode 100644 index 0a3ecd4..0000000 --- a/docs/pre9-cache-final-manual-validation.md +++ /dev/null @@ -1,132 +0,0 @@ -# Pre9 decoded extent cache final manual validation - -Date: 2026-08-13 - -## Scope - -This run validated the mount-scoped decoded LZMA extent cache at enclosing -repository commit `cdcf276d12169a672d2de42996532a470ac061d9`. - -Controlled identities: - -- `repo-pre-9` tree: `9d7eaf63a1c5d07320af3ef39930fc0a3530dd11` -- `repo-pre-9/src` tree: `e657e8a63b097b061670f75ca01947a568ef33d5` -- guest-built KLD SHA-256: - `473a6205f43905972f2417609d43f67ab2cb51ea79bc1716fffa5c1914ff85af` -- LZMA image SHA-256: - `32107a084b27362a093768b88746c37c2e99a74b9f1301a9d4046988479defe9` -- LZMA source SHA-256: - `ddda39737f0f6093e828a032ec161511fefbb1fa361bc6cbffdbc91e48e4c461` - -The existing fresh QEMU process on host port 10030 was reused. No second VM -was created. The guest used FreeBSD 15.0-RELEASE-p8 under QEMU TCG with 6 GiB -RAM and four virtual CPUs. The KLD was built in the guest with -`make WITH_ZSTDIO=0`. - -The VM used the existing base image -`/work/tests-dev/lfs/freebsd-15-dev-src-20260811.bp`. Its previously audited -SHA-256 is -`67f359621f23a1d745f0889370cbb99a096cee3e99a0b2f3bb18fc7a91bf6fef`. -This run relied on that established integrity record and did not repeat the -approximately 16 GiB image hash, so base-image hashing did not block the final -validation. - -## Verdict - -| Area | Verdict | Result | -| --- | --- | --- | -| Guest build, KLD load and fixture mounts | PASS | Build returned zero, `kldload` returned zero and both LZMA and LZ4 images mounted read-only. | -| LZMA single-read correctness | PASS | 4 KiB, 16 KiB, 64 KiB and 1 MiB single `pread()` calls returned the requested bytes with `errno=0`; every output range matched the source range SHA-256. | -| LZMA full-file correctness | PASS | Cold and warm full SHA-256 runs returned zero and matched the 8 MiB source hash. | -| LZMA liveness and performance | PASS | Cold full SHA completed in 2.16 seconds and warm full SHA in 1.44 seconds, both below the 120-second host hard limit. | -| LZMA concurrency | PASS | Two simultaneous full SHA processes both returned zero and the expected hash; guest real times were 22.52 and 21.97 seconds. | -| Non-LZMA regression | PASS | The 8 MiB LZ4 fixture returned the expected SHA-256 in 9.03 seconds. | -| Cleanup and guard integrity | PASS | Test QEMU PID 812999 exited, port 10030 closed, its overlay was deleted, and guard PID 26318/port 9222 remained alive and reachable. | - -Overall verdict: **PASS for the required final manual smoke scope**. - -## Single `pread()` evidence - -Each probe program performs one target-file `pread()`. Truss confirmed one -target call at offset zero for every requested length. Dynamic-loader reads are -not counted. - -| Length | Returned | Probe command wall time | Output/source range hash | Verdict | -| ---: | ---: | ---: | --- | --- | -| 4 KiB | 4 KiB | 2.696 s | `1c3c59331a4849514667bbc51c078327577b98f75a62996bbc2fe310b014dd79` | PASS | -| 16 KiB | 16 KiB | 2.140 s | `2433ce7c2ea151f487a319447ce4fe14e1a2c2af9f0e5113fe47fc74b6138eee` | PASS | -| 64 KiB | 64 KiB | 1.369 s | `2bb87afd3b9fab420cbe55b782d69a7342150e8816a35efacf22d2ab3012815d` | PASS | -| 1 MiB | 1 MiB | 3.838 s | `52e806509e5dfeb523904f167fca765d001ea749351ecf51738e61a5352a1ba3` | PASS | - -These wall times include SSH and truss overhead and are not pure kernel read -times. - -## Full-file SHA evidence - -The 8 MiB LZMA source hash is: - -`ddda39737f0f6093e828a032ec161511fefbb1fa361bc6cbffdbc91e48e4c461` - -| Run | Exit | Guest real time | Captured hash | Verdict | -| --- | ---: | ---: | --- | --- | -| Cold, fresh mount | 0 | 2.16 s | expected hash | PASS | -| Warm, same mount | 0 | 1.44 s | expected hash | PASS | -| Concurrent process 1 | 0 | 22.52 s | expected hash | PASS | -| Concurrent process 2 | 0 | 21.97 s | expected hash | PASS | - -The concurrency result shows substantial contention compared with a single -reader, but it completed correctly and stayed well inside the 120-second hard -limit. This run does not claim concurrency performance is optimized. - -## Baseline comparison - -The preceding controlled Pre7/Pre8/Pre9 run sampled each full SHA after about -eight seconds. Each process was still inside the LZMA decompression path and -had advanced only about 1.0-1.25 MiB; no final hash was captured. That report -therefore classified full-read liveness as failed and correctness as blocked. - -With the mount-scoped decoded extent cache, the same 8 MiB fixture completed -with the correct hash in 2.16 seconds cold and 1.44 seconds warm. This closes -the previous full-file correctness block for this fixture and demonstrates a -material liveness improvement. It does not establish a precise speedup ratio, -because the baseline process was terminated at the observation point rather -than allowed to finish. - -## Non-LZMA result - -The LZ4 compact-64k image contained `compressed.bin` with expected SHA-256: - -`3ff012b76087c4da65ce0b69813a76f47ea367e95782edf8b8cd6cd3ec4d1880` - -The mounted file produced the same hash, returned zero and completed in 9.03 -seconds of guest real time. - -## Kernel and cleanup observations - -No EROFS panic, assertion, mount error or decompression error was found in the -captured dmesg tail. The image emitted pre-existing root-filesystem directory -warnings during boot; they occurred before the test KLD was loaded and are not -attributed to EROFS. - -All test-specific mounts and md providers were explicitly detached before VM -shutdown. A final generic cleanup command contained an awk quoting error, but -it ran only after the explicit LZMA and LZ4 unmount/md detach operations had -already returned zero. Host-side cleanup independently confirmed: - -- PID 812999 absent; -- port 10030 closed; -- test overlay absent; -- guard PID 26318 alive; -- guard port 9222 open. - -## Uncovered cases - -This is a targeted final smoke validation, not a full feature suite. It did not -exercise partial-reference LZMA, metadata tailpacking, fragments, multi-device -images, forced unmount under active I/O, repeated mount/unmount under memory -pressure, or deliberate allocation failure. Those remain separate regression -and stress-test work. - -Concise machine-readable evidence is stored in -`docs/pre9-manual-evidence/pre9-cache-final-result.json`. Target syscall lines -are stored in `docs/pre9-manual-evidence/pre9-cache-final-probes.txt`. diff --git a/docs/pre9-controlled-manual-qemu.md b/docs/pre9-controlled-manual-qemu.md deleted file mode 100644 index be4d7ba..0000000 --- a/docs/pre9-controlled-manual-qemu.md +++ /dev/null @@ -1,158 +0,0 @@ -# Pre9 controlled manual QEMU evidence audit - -Date: 2026-08-13 - -## Scope and evidence status - -This report audits the already completed manual run at -`/work/tests-dev/temp/pre9-manual-20260813-run3/`. No QEMU test was rerun. -The long-lived guard VM (PID 26318, host port 9222) was not touched. - -The automated TC004 implementation remains `PENDING_FIX/BLOCKED` in -`/work/tests-dev/fix-todo/tc004-automation.md`. Run3 is a manual substitute; -it is not evidence that TC004 automation passes. - -Run history: - -- Run1: infrastructure failure; no retained result directory is available. -- Run2: `INFRA`. Guest SSH authentication failed for Pre7 and Pre8, then the - host runner was interrupted while preparing Pre9. It produced no DUT result. -- Run3: valid manual run. All three DUTs built, loaded, mounted, executed the - bounded probes, produced a live SHA stack sample, and cleaned up. - -## Verdict - -| Question | Verdict | Evidence | -| --- | --- | --- | -| 4 KiB, 16 KiB, 64 KiB and 1 MiB single-syscall read correctness | PASS | Every version returned the requested byte count, `errno=0`, matched the source-range SHA-256, exited zero, and has exactly one target-file `pread()` in truss. | -| First 1 MiB sequential `dd` read | PASS | Every version returned zero and copied 1 MiB within the 20-second bound. This is a bounded smoke check, not proof of one 1 MiB kernel read. | -| Full-file SHA correctness | BLOCKED | No version retained a hash or an exit status for the background SHA. `sha_after` is empty and process disappearance is not correctness evidence. | -| Full-file read liveness/performance | FAIL | After an eight-second observation delay, SHA was still running in the LZMA decode path and had advanced only about 1.0-1.25 MiB. This is unacceptable for the 8 MiB smoke fixture and reproduces across all three versions. | -| Pre7 to Pre8/Pre9 regression attribution | PASS: no Pre9 regression demonstrated | Pre8 and Pre9 have the same `src` tree and identical KLD hash. Pre7 has a different source tree/KLD but shows the same symptom and stack. The evidence attributes the issue to shared behavior, not a Pre9-only change. | - -Overall verdict: **PARTIAL**. Bounded reads are correct, but the full-file -correctness verdict is blocked and full-file liveness fails. - -## Runner semantics audit - -The runner creates a fresh qcow2 overlay per version over the read-only base, -archives the selected repository subtree, builds the KLD in the guest, mounts -the same LZMA fixture, and runs four probes. Each probe invokes a C program that -contains one target-file `pread()` and writes the returned bytes to a file. -The runner then hashes that file and compares it with bytes read from the -uncompressed source fixture. - -Important field meanings: - -- `commit`: Git tree object for the version directory at the enclosing repo - HEAD. It is not a standalone commit ID. -- `src_tree`: Git tree object for that version's `src` directory. -- `probes[].elapsed`: host wall time for the complete SSH command, including - SSH and truss overhead. It is not pure kernel decompression time. -- `metadata.hash_match`: equality between the mounted output-range hash and - the uncompressed source-range hash. -- `signal=0`: runner shorthand for probe return code zero. It is not a signal - collected through `waitpid()`. -- `dd_1m`: a userspace `dd bs=1m count=1` result. It does not establish the - size or count of VOP/kernel reads. -- `sha_pid`: PID printed after starting a background `sha256` command. -- `diagnostic_sample`: process table, kernel stack, descriptor offset, mount, - md device and dmesg captured after an unconditional eight-second sleep. -- `sha_after`: process status followed by `sha.out` and `sha.err`. Empty output - means neither a process row nor captured SHA/error output was available. -- `status=PARTIAL`: runner-generated fallback when any bounded probe succeeds. - It does not mean full SHA correctness passed. - -The full SHA was not run under `timeout`. The runner waited eight seconds, -sampled it, sent TERM, slept two seconds, then attempted KILL, and finally read -the output files. No start/end timestamp or exit status was recorded. Therefore -the exact SHA lifetime is unknown; the only defensible timing statement is that -it was still active approximately eight seconds after launch. - -`sha_after` contains only `,state=,command=` for all versions. The subsequent -KILL reports `No such process`. This establishes only that the sampled PID no -longer existed after TERM plus the two-second delay. It does not distinguish a -successful completion from TERM handling, and the absent `sha.out` means no -hash can be validated. Process disappearance must not be reported as PASS. - -## Controlled identities and setup - -The common fixture hashes were: - -- Image: `32107a084b27362a093768b88746c37c2e99a74b9f1301a9d4046988479defe9` -- Source: `ddda39737f0f6093e828a032ec161511fefbb1fa361bc6cbffdbc91e48e4c461` - -| Version | Repository tree | `src` tree | KLD SHA-256 | Build/load/mount | -| --- | --- | --- | --- | --- | -| Pre7 | `b8f9af2cd55225c4348b79ff5910ae6fc83cd517` | `dc203534d7b5721905f8538026e4c59346106020` | `33a52f2f16a94afda0501d305b238678b96c71e420d4b8bbcbeec6ffcdf1aae5` | PASS/PASS/PASS | -| Pre8 | `1e24c992a5b071e6fdabfdc42d342b6ed5a91cf5` | `cbf93a19df8d1ce2fb66e2aeb7276a4ea6d4dcc2` | `348cb1f5a91d47e1412747d285c795c49375a61a66cfb3d35d22f38ecfde6ea0` | PASS/PASS/PASS | -| Pre9 | `dc0c01157b5c925684bd77c57ed6703904af7453` | `cbf93a19df8d1ce2fb66e2aeb7276a4ea6d4dcc2` | `348cb1f5a91d47e1412747d285c795c49375a61a66cfb3d35d22f38ecfde6ea0` | PASS/PASS/PASS | - -Pre8 and Pre9 are runtime-identical for this test according to both the source -tree and the produced KLD hash. - -## Single-syscall probes - -All probes used offset zero. Times below include SSH/truss overhead. - -| Version | Length | Returned | Hash match | Elapsed | Exactly one target `pread()` | -| --- | ---: | ---: | --- | ---: | --- | -| Pre7 | 4 KiB | 4 KiB | yes | 1.299 s | yes | -| Pre7 | 16 KiB | 16 KiB | yes | 1.291 s | yes | -| Pre7 | 64 KiB | 64 KiB | yes | 1.438 s | yes | -| Pre7 | 1 MiB | 1 MiB | yes | 1.432 s | yes | -| Pre8 | 4 KiB | 4 KiB | yes | 1.216 s | yes | -| Pre8 | 16 KiB | 16 KiB | yes | 1.327 s | yes | -| Pre8 | 64 KiB | 64 KiB | yes | 1.430 s | yes | -| Pre8 | 1 MiB | 1 MiB | yes | 1.442 s | yes | -| Pre9 | 4 KiB | 4 KiB | yes | 1.791 s | yes | -| Pre9 | 16 KiB | 16 KiB | yes | 1.341 s | yes | -| Pre9 | 64 KiB | 64 KiB | yes | 1.374 s | yes | -| Pre9 | 1 MiB | 1 MiB | yes | 1.737 s | yes | - -The target-file truss lines are preserved in -`pre9-manual-evidence/probe-summary.txt`. Dynamic-loader `pread()` calls are -not counted as target-file calls. - -The 1 MiB `dd` results were: - -- Pre7: 1 MiB in 0.195526 s. -- Pre8: 1 MiB in 0.179484 s. -- Pre9: 1 MiB in 0.367417 s. - -These values are not stable enough for version performance ranking, but all -three bounded operations completed. - -## Full SHA sample - -| Version | State at sample | File offset | Kernel stack | Final hash | -| --- | --- | ---: | --- | --- | -| Pre7 | running (`RC`) | 1,245,184 | `lzma2_lzma -> erofs_xz_dec_microlzma_run -> lzma_decompress -> z_erofs_decompress -> z_erofs_do_read -> z_erofs_read_uio` | absent | -| Pre8 | running (`RC`) | 1,150,976 | same | absent | -| Pre9 | running (`RC`) | 1,044,480 | same | absent | - -The stack is direct evidence of active CPU-side decompression, not an EROFS -lock wait. It does not by itself prove why decompression is slow. Combined with -the static review, the leading explanation remains repeated full mapped-extent -decompression as `z_erofs_read_uio()` segments reads at `MAXPHYS`. There is no -direct evidence here of an XZ infinite loop or an EROFS lock deadlock. - -## Infrastructure integrity and cleanup - -The base image SHA-256 before and after run3 was identical and matched the -expected value: - -`67f359621f23a1d745f0889370cbb99a096cee3e99a0b2f3bb18fc7a91bf6fef` - -For each version, `umount`, `mdconfig -d` and `kldunload` returned zero. Each -QEMU PID was no longer alive and each overlay was deleted. The run3 result set -therefore supports successful test-VM cleanup. This report does not infer the -state of the unrelated long-lived guard VM and did not inspect or modify it. - -## Required follow-up - -A supplemental manual test is still required if full-file correctness must be -closed. It should run one full SHA with an explicit wall-clock deadline, record -start/end timestamps and exit status, capture `sha.out` before cleanup, compare -the hash to the source fixture, and separately record whether TERM/KILL was -used. Until then, full-file SHA correctness remains `BLOCKED`. diff --git a/docs/pre9-lzma-cache-implementation.md b/docs/pre9-lzma-cache-implementation.md deleted file mode 100644 index 9083045..0000000 --- a/docs/pre9-lzma-cache-implementation.md +++ /dev/null @@ -1,142 +0,0 @@ -# Pre9 LZMA decoded extent cache - -## Decision - -Pre7, Pre8, and Pre9 all reproduced the same LZMA read-liveness symptom. The -manual evidence shows that a single large read completes quickly while a full -file read made up of successive small reads remains in the LZMA decode path. -The source path creates and destroys a decoded extent for every mapped read; -the existing FreeBSD buffer cache only retains compressed block buffers. - -A cache local to `z_erofs_read_uio()` would not cross the VOP/read boundary -between successive small user reads. Commit `61f4709` therefore kept one -decoded extent on each compressed-file vnode. Review found that retained -memory could then grow with the number of open vnodes. This follow-up moves -the cache to `struct erofs_mount`: each mount retains at most one decoded -extent until unmount, so ordinary open-file count cannot expand the cache. -A single entry can thrash between files and concurrent readers; that is an -accepted first-stage tradeoff. - -The cache is deliberately limited to complete, non-partial MicroLZMA extents. -Partial references keep the existing path because their decoded length and -reference semantics are different. Other codecs are also unchanged in this -phase: the measured failure is LZMA-specific, and broadening the change would -make the validation and regression attribution less precise. - -## Data and lifecycle - -`struct erofs_zextent_cache` stores the decoded allocation and the mapping -identity needed to prove that it can be reused: - -```c -struct erofs_zextent_cache { - void *data; - erofs_nid_t m_nid; - erofs_off_t m_pa; - erofs_off_t m_la; - uint64_t m_plen; - uint64_t m_llen; - unsigned int m_deviceid; - unsigned int m_flags; - unsigned char m_algorithmformat; -}; -``` - -The cache mutex is initialized immediately after allocating -`struct erofs_mount`. Every mount failure path reaches `erofs_sb_free()`, and -normal unmount calls the same helper after `vflush()`. There is no cache field -or cache lifecycle dependency in `struct erofs_node`. The cache key includes -the inode NID because one mount entry is shared by all regular file vnodes. -NID zero remains a valid key; `data != NULL` is the validity bit. - -EROFS is read-only, so a decoded extent does not need write invalidation. -`vflush()` completes before unmount frees the mount object, and -`z_erofs_extent_cache_fini()` releases the single mount-owned allocation. -`EROFS_MAP_META` is explicitly excluded, so metadata-backed tailpacking keeps -the previous path. `packed_inode` and `metabox_en` are also excluded because -they are mount-private backing objects, not user file vnodes. - -## Concurrency - -Decompression and block reads occur without holding `z_extent_cache_lock`. A reader -first takes the lock only to compare the complete key and copy a cache hit. -On a miss, it builds a private decoded extent. It then takes the lock again: - -1. If another reader published the same key, copy that published extent and - discard the duplicate private allocation. -2. Otherwise replace the one cached extent, copy the requested range, unlock, - and free the old allocation. - -This permits duplicate construction under concurrent misses but keeps all -shared pointer access under the mutex. The lock is never held across -`bread()`, allocation, or decompression, all of which may sleep. The cache -allocation is never freed while a reader is copying it because replacement, -hit copying, and pointer clearing are serialized by the same mutex. - -The helpers are reached only when `want <= MAXPHYS` and contain `KASSERT` -checks for that contract. Thus the largest lock-held copy is the FreeBSD -`MAXPHYS` request size, not the 12 MiB on-disk extent cap. Calls that can pass -more than `MAXPHYS` use the existing uncached copy path. This bounded mutex -copy is accepted for the first stage; a refcounted immutable entry is deferred -until runtime evidence shows that this copy is materially contended. - -## Bounds and unchanged paths - -The existing mapping sanity checks cap a mapped compressed extent at -`Z_EROFS_PCLUSTER_MAX_DSIZE` before the read path. `z_erofs_read_extent()` also -checks the compressed and decoded lengths before allocation. The read path now -explicitly checks `mapoff` and the extent length before converting them to -`size_t`; this protects the cache offset arithmetic on platforms where -`size_t` is narrower than the on-disk fields. - -Cache use requires all of the following: - -```text -compressed mapped extent -not EROFS_MAP_PARTIAL_REF -Z_EROFS_COMPRESSION_LZMA -initialized mount cache -not `EROFS_MAP_META` -not a mount-private backing inode -request length no greater than `MAXPHYS` -``` - -Fragments, holes, partial references, non-LZMA codecs, uncompressed files, -metadata reads, and mount-private backing inodes retain their previous code -paths and error handling. - -## Rejected alternatives for Pre9 - -- A function-local cache was rejected because it cannot span successive VOP - reads that caused the observed amplification. -- A per-vnode cache was rejected after the `61f4709` review: open file count - could retain one decoded extent per vnode without a system-wide bound. -- A larger cross-vnode cache was rejected because it would require an eviction - policy and larger memory accounting. The one-entry per-mount cache is the - controlled compromise: it has a fixed mount-scoped bound and simple teardown. -- A Linux page/folio/XArray/workqueue port was rejected because those are not - FreeBSD vnode/buf primitives and would create an unnecessary compatibility - layer. -- A decoder stream pool was deferred: it may reduce allocator overhead but - does not remove repeated full extent decompression. -- Changing `MAXPHYS`, changing the disk format, bypassing the buffer cache, or - adding decoder retries was rejected because none addresses the demonstrated - decoded-result reuse and each changes unrelated behavior or resource bounds. - -## Static validation and required runtime matrix - -This change is static-only in the source phase. The follow-up test agent must -run the unchanged LZMA fixture against Pre9 and require: - -1. bounded single reads at 4 KiB, 16 KiB, 64 KiB, and 1 MiB with source-range - hash equality; -2. repeated small reads spanning the same extent, with completion and hash - equality; -3. complete sequential SHA-256 with recorded exit status and elapsed time; -4. concurrent reads of the same file and close/reopen reads; -5. partial-reference, plain, LZ4, DEFLATE, and ZSTD regression coverage; -6. clean unmount, md detach, and module unload after every case. - -The automation verdict and DUT verdict must remain separate. A timeout or -missing final hash remains a failure or blocked result; it must not be promoted -to PASS because bounded reads succeed. diff --git a/docs/pre9-lzma-cache-review-resolution.md b/docs/pre9-lzma-cache-review-resolution.md deleted file mode 100644 index 32e5b62..0000000 --- a/docs/pre9-lzma-cache-review-resolution.md +++ /dev/null @@ -1,62 +0,0 @@ -# Pre9 LZMA cache review resolution - -This document records the follow-up to commit `61f4709`, which cached one -decoded LZMA extent per vnode. - -## Findings addressed - -- The cache is now one entry in `struct erofs_mount`, rather than one entry in - every `struct erofs_node`. Retained decoded memory is bounded by one extent - per mounted filesystem, with the number of mounts controlled by mount - privileges. Keeping many ordinary file descriptors open cannot create more - cache entries. -- Cache initialization is performed in `erofs_mountfs()` immediately after - mount allocation. `erofs_sb_free()` destroys it on every mount failure path - and after successful `vflush()` during unmount. Node reclaim no longer owns - cache cleanup. -- `EROFS_MAP_META` is an explicit ineligibility condition. Metadata-backed - tailpacking therefore remains on its existing read path. -- `packed_inode` and `metabox_en` are explicit ineligibility conditions. The - mount-private backing objects cannot populate or consume the user-data - cache. -- The cache type and helper names are now `erofs_zextent_cache` and - `z_erofs_extent_cache_*`, distinguishing decoded extents from Linux's - managed compressed-page cache names. -- The key retains the existing physical/logical extent fields, device id, - flags, and algorithm, and adds the stable inode `nid`. The `data` pointer is - the validity bit, so NID zero is not treated as an empty key. - -## Copy bound and concurrency - -`z_erofs_read_uio()` limits each output request to `MAXPHYS`. The generic -`z_erofs_read_data()` API can receive a larger request, so cache eligibility -rejects any request whose mapped portion exceeds `MAXPHYS`. Both cache helper -entry points also contain `KASSERT(len <= MAXPHYS)` checks. The largest copy -performed while holding `z_extent_cache_lock` is therefore `MAXPHYS`; the -12 MiB `Z_EROFS_PCLUSTER_MAX_DSIZE` limit remains an on-disk decoded-extent -allocation bound, not a mutex-copy bound. - -Allocation, compressed reads, and decompression happen outside the mutex. A -cache hit copies while holding the mutex, and a miss publishes and copies -under the same mutex before the previous allocation is freed. Concurrent -misses may decode duplicate extents and may replace one another, but pointer -access and replacement remain serialized. A refcounted immutable cache entry -was deliberately not introduced in this first stage. - -## Resource tradeoff - -The per-mount entry is intentionally a small first-stage design. It removes -the unbounded-per-open-vnode retention introduced by `61f4709`, but it can -thrash when many files are read concurrently on one mount. Retention lasts -until unmount, and the retained allocation is capped by the existing EROFS -format constant `Z_EROFS_PCLUSTER_MAX_DSIZE` (12 MiB). A FreeBSD shrinker or -pressure callback is deferred; adding one would require a broader memory -accounting and lifecycle design than this corrective commit. - -## Validation scope - -This correction is statically validated only. QEMU validation remains -required for complete SHA-256 reads, same-file concurrent reads, close/reopen, -unmount cleanup, metadata tailpacking, and memory-pressure behavior. The -existing Pre9 manual test report predates this correction and must not be -reported as runtime validation of this commit. diff --git a/docs/pre9-manual-evidence/full-sha-samples.txt b/docs/pre9-manual-evidence/full-sha-samples.txt deleted file mode 100644 index 720229b..0000000 --- a/docs/pre9-manual-evidence/full-sha-samples.txt +++ /dev/null @@ -1,26 +0,0 @@ -Run3 full-file SHA samples, captured approximately eight seconds after launch - -repo-pre-7 -state: RC -file offset: 1245184 -kernel stack: lzma2_lzma -> erofs_xz_dec_microlzma_run -> lzma_decompress -> z_erofs_decompress -> z_erofs_do_read -> z_erofs_read_uio -> VOP_READ_APV -> vn_read -sha_after: no process row, no sha.out hash, no sha.err content -termination: TERM sent; KILL then reported No such process - -repo-pre-8 -state: RC -file offset: 1150976 -kernel stack: lzma2_lzma -> erofs_xz_dec_microlzma_run -> lzma_decompress -> z_erofs_decompress -> z_erofs_do_read -> z_erofs_read_uio -> VOP_READ_APV -> vn_read -sha_after: no process row, no sha.out hash, no sha.err content -termination: TERM sent; KILL then reported No such process - -repo-pre-9 -state: RC -file offset: 1044480 -kernel stack: lzma2_lzma -> erofs_xz_dec_microlzma_run -> lzma_decompress -> z_erofs_decompress -> z_erofs_do_read -> z_erofs_read_uio -> VOP_READ_APV -> vn_read -sha_after: no process row, no sha.out hash, no sha.err content -termination: TERM sent; KILL then reported No such process - -Interpretation: the SHA processes were alive and executing in LZMA decode at -the sample. Their later disappearance does not prove successful completion. -No full-file hash was captured, so full-file correctness remains BLOCKED. diff --git a/docs/pre9-manual-evidence/pre9-cache-final-kernel-cleanup.txt b/docs/pre9-manual-evidence/pre9-cache-final-kernel-cleanup.txt deleted file mode 100644 index bc968d7..0000000 --- a/docs/pre9-manual-evidence/pre9-cache-final-kernel-cleanup.txt +++ /dev/null @@ -1,16 +0,0 @@ -Pre9 final decoded extent cache kernel and cleanup summary -Date: 2026-08-13 - -- Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG, 6144 MiB, 4 vCPUs. -- KLD built with WITH_ZSTDIO=0 and loaded successfully. -- No EROFS panic, assertion, mount error or decompression error appeared in - the captured dmesg tail. -- Boot emitted pre-existing root-filesystem "bad dir ino" warnings before - the test KLD was loaded; these are not attributed to EROFS. -- Explicit LZMA probe, LZMA full-read and LZ4 unmount/md detach operations - returned zero. -- Test QEMU PID 812999 was terminated. -- Host port 10030 was closed. -- Test overlay was removed. -- Guard QEMU PID 26318 remained alive. -- Guard SSH port 9222 remained open. diff --git a/docs/pre9-manual-evidence/pre9-cache-final-probes.txt b/docs/pre9-manual-evidence/pre9-cache-final-probes.txt deleted file mode 100644 index 5087fa2..0000000 --- a/docs/pre9-manual-evidence/pre9-cache-final-probes.txt +++ /dev/null @@ -1,21 +0,0 @@ -Pre9 final decoded extent cache target-file syscall evidence -Date: 2026-08-13 - -4 KiB: -pread(3, ..., 4096, 0x0) = 4096 (0x1000) -output/source SHA-256: 1c3c59331a4849514667bbc51c078327577b98f75a62996bbc2fe310b014dd79 - -16 KiB: -pread(3, ..., 16384, 0x0) = 16384 (0x4000) -output/source SHA-256: 2433ce7c2ea151f487a319447ce4fe14e1a2c2af9f0e5113fe47fc74b6138eee - -64 KiB: -pread(3, ..., 65536, 0x0) = 65536 (0x10000) -output/source SHA-256: 2bb87afd3b9fab420cbe55b782d69a7342150e8816a35efacf22d2ab3012815d - -1 MiB: -pread(3, ..., 1048576, 0x0) = 1048576 (0x100000) -output/source SHA-256: 52e806509e5dfeb523904f167fca765d001ea749351ecf51738e61a5352a1ba3 - -Each trace contained exactly one pread() against the mounted target file. -Dynamic-loader pread() calls were excluded from this summary. diff --git a/docs/pre9-manual-evidence/pre9-cache-final-result.json b/docs/pre9-manual-evidence/pre9-cache-final-result.json deleted file mode 100644 index 15af758..0000000 --- a/docs/pre9-manual-evidence/pre9-cache-final-result.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "archive_sha256": "a6a6d573ace42fe713529974c7bd20b472a1e4521920fac40e01c5d3bb071bcb", - "cleanup": { - "guard_pid_alive": true, - "guard_port_9222_open": true, - "overlay_exists": false, - "port_10030_open": false, - "qemu_pid_alive": false - }, - "finished_utc": "2026-08-13T06:06:25Z", - "kld_sha256": "473a6205f43905972f2417609d43f67ab2cb51ea79bc1716fffa5c1914ff85af", - "lz4_sha": { - "expected_sha256": "3ff012b76087c4da65ce0b69813a76f47ea367e95782edf8b8cd6cd3ec4d1880", - "guest_real_seconds": 9.03, - "hash_match": true, - "returncode": 0, - "sha256": "3ff012b76087c4da65ce0b69813a76f47ea367e95782edf8b8cd6cd3ec4d1880" - }, - "lzma_concurrent": { - "guest_real_seconds": [ - 22.52, - 21.97 - ], - "hash_match": true, - "hashes": [ - "ddda39737f0f6093e828a032ec161511fefbb1fa361bc6cbffdbc91e48e4c461", - "ddda39737f0f6093e828a032ec161511fefbb1fa361bc6cbffdbc91e48e4c461" - ], - "host_timeout": false, - "returncode": 0, - "returncodes": [ - 0, - 0 - ] - }, - "lzma_fixture": { - "image_sha256": "32107a084b27362a093768b88746c37c2e99a74b9f1301a9d4046988479defe9", - "source_sha256": "ddda39737f0f6093e828a032ec161511fefbb1fa361bc6cbffdbc91e48e4c461" - }, - "lzma_sha": { - "cold": { - "guest_real_seconds": 2.16, - "hash_match": true, - "host_timeout": false, - "returncode": 0, - "sha256": "ddda39737f0f6093e828a032ec161511fefbb1fa361bc6cbffdbc91e48e4c461" - }, - "warm": { - "guest_real_seconds": 1.44, - "hash_match": true, - "host_timeout": false, - "returncode": 0, - "sha256": "ddda39737f0f6093e828a032ec161511fefbb1fa361bc6cbffdbc91e48e4c461" - } - }, - "repo_pre_9_tree": "9d7eaf63a1c5d07320af3ef39930fc0a3530dd11", - "schema": "pre9-cache-final-manual-v1", - "single_pread": [ - { - "elapsed_seconds": 2.696, - "hash_match": true, - "length": 4096, - "returncode": 0, - "sha256": "1c3c59331a4849514667bbc51c078327577b98f75a62996bbc2fe310b014dd79" - }, - { - "elapsed_seconds": 2.14, - "hash_match": true, - "length": 16384, - "returncode": 0, - "sha256": "2433ce7c2ea151f487a319447ce4fe14e1a2c2af9f0e5113fe47fc74b6138eee" - }, - { - "elapsed_seconds": 1.369, - "hash_match": true, - "length": 65536, - "returncode": 0, - "sha256": "2bb87afd3b9fab420cbe55b782d69a7342150e8816a35efacf22d2ab3012815d" - }, - { - "elapsed_seconds": 3.838, - "hash_match": true, - "length": 1048576, - "returncode": 0, - "sha256": "52e806509e5dfeb523904f167fca765d001ea749351ecf51738e61a5352a1ba3" - } - ], - "src_tree": "e657e8a63b097b061670f75ca01947a568ef33d5", - "started_utc": "2026-08-13T05:59:41Z", - "tested_head": "cdcf276d12169a672d2de42996532a470ac061d9", - "verdict": { - "build_load_mount": "PASS", - "cleanup": "PASS", - "lzma_concurrency": "PASS", - "lzma_full_correctness": "PASS", - "lzma_liveness_performance": "PASS", - "lzma_single_pread_correctness": "PASS", - "non_lzma_lz4": "PASS" - } -} diff --git a/docs/pre9-manual-evidence/probe-summary.txt b/docs/pre9-manual-evidence/probe-summary.txt deleted file mode 100644 index 8ac04fe..0000000 --- a/docs/pre9-manual-evidence/probe-summary.txt +++ /dev/null @@ -1,22 +0,0 @@ -Run3 target-file syscall evidence (dynamic-loader pread calls omitted) - -repo-pre-7 -4096: pread(fd, ..., 4096, 0) = 4096; process exit 0 -16384: pread(fd, ..., 16384, 0) = 16384; process exit 0 -65536: pread(fd, ..., 65536, 0) = 65536; process exit 0 -1048576: pread(fd, ..., 1048576, 0) = 1048576; process exit 0 - -repo-pre-8 -4096: pread(fd, ..., 4096, 0) = 4096; process exit 0 -16384: pread(fd, ..., 16384, 0) = 16384; process exit 0 -65536: pread(fd, ..., 65536, 0) = 65536; process exit 0 -1048576: pread(fd, ..., 1048576, 0) = 1048576; process exit 0 - -repo-pre-9 -4096: pread(fd, ..., 4096, 0) = 4096; process exit 0 -16384: pread(fd, ..., 16384, 0) = 16384; process exit 0 -65536: pread(fd, ..., 65536, 0) = 65536; process exit 0 -1048576: pread(fd, ..., 1048576, 0) = 1048576; process exit 0 - -All mounted-output hashes matched corresponding source-range hashes. The full -hash values and elapsed times are retained in run3-audit.json. diff --git a/docs/pre9-manual-evidence/run3-audit.json b/docs/pre9-manual-evidence/run3-audit.json deleted file mode 100644 index 994fb45..0000000 --- a/docs/pre9-manual-evidence/run3-audit.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "base_image": { - "before": "67f359621f23a1d745f0889370cbb99a096cee3e99a0b2f3bb18fc7a91bf6fef", - "after": "67f359621f23a1d745f0889370cbb99a096cee3e99a0b2f3bb18fc7a91bf6fef", - "expected": "67f359621f23a1d745f0889370cbb99a096cee3e99a0b2f3bb18fc7a91bf6fef", - "unchanged": true - }, - "fixture": { - "image_sha256": "32107a084b27362a093768b88746c37c2e99a74b9f1301a9d4046988479defe9", - "source_sha256": "ddda39737f0f6093e828a032ec161511fefbb1fa361bc6cbffdbc91e48e4c461" - }, - "versions": [ - { - "name": "repo-pre-7", - "repository_tree": "b8f9af2cd55225c4348b79ff5910ae6fc83cd517", - "src_tree": "dc203534d7b5721905f8538026e4c59346106020", - "kld_sha256": "33a52f2f16a94afda0501d305b238678b96c71e420d4b8bbcbeec6ffcdf1aae5", - "probes": [ - {"length": 4096, "returned": 4096, "elapsed_seconds": 1.2993653398007154, "hash_match": true, "target_pread_count": 1}, - {"length": 16384, "returned": 16384, "elapsed_seconds": 1.2910558842122555, "hash_match": true, "target_pread_count": 1}, - {"length": 65536, "returned": 65536, "elapsed_seconds": 1.4380157887935638, "hash_match": true, "target_pread_count": 1}, - {"length": 1048576, "returned": 1048576, "elapsed_seconds": 1.4320225361734629, "hash_match": true, "target_pread_count": 1} - ], - "dd_1m_seconds": 0.195526, - "sha_sample_offset": 1245184, - "sha_hash_captured": false, - "cleanup": {"umount": 0, "mdconfig_detach": 0, "kldunload": 0, "qemu_alive": false, "overlay_exists": false} - }, - { - "name": "repo-pre-8", - "repository_tree": "1e24c992a5b071e6fdabfdc42d342b6ed5a91cf5", - "src_tree": "cbf93a19df8d1ce2fb66e2aeb7276a4ea6d4dcc2", - "kld_sha256": "348cb1f5a91d47e1412747d285c795c49375a61a66cfb3d35d22f38ecfde6ea0", - "probes": [ - {"length": 4096, "returned": 4096, "elapsed_seconds": 1.2161498833447695, "hash_match": true, "target_pread_count": 1}, - {"length": 16384, "returned": 16384, "elapsed_seconds": 1.3266378343105316, "hash_match": true, "target_pread_count": 1}, - {"length": 65536, "returned": 65536, "elapsed_seconds": 1.4296557288616896, "hash_match": true, "target_pread_count": 1}, - {"length": 1048576, "returned": 1048576, "elapsed_seconds": 1.4422911144793034, "hash_match": true, "target_pread_count": 1} - ], - "dd_1m_seconds": 0.179484, - "sha_sample_offset": 1150976, - "sha_hash_captured": false, - "cleanup": {"umount": 0, "mdconfig_detach": 0, "kldunload": 0, "qemu_alive": false, "overlay_exists": false} - }, - { - "name": "repo-pre-9", - "repository_tree": "dc0c01157b5c925684bd77c57ed6703904af7453", - "src_tree": "cbf93a19df8d1ce2fb66e2aeb7276a4ea6d4dcc2", - "kld_sha256": "348cb1f5a91d47e1412747d285c795c49375a61a66cfb3d35d22f38ecfde6ea0", - "probes": [ - {"length": 4096, "returned": 4096, "elapsed_seconds": 1.7912541721016169, "hash_match": true, "target_pread_count": 1}, - {"length": 16384, "returned": 16384, "elapsed_seconds": 1.341409295797348, "hash_match": true, "target_pread_count": 1}, - {"length": 65536, "returned": 65536, "elapsed_seconds": 1.3740410972386599, "hash_match": true, "target_pread_count": 1}, - {"length": 1048576, "returned": 1048576, "elapsed_seconds": 1.737462431192398, "hash_match": true, "target_pread_count": 1} - ], - "dd_1m_seconds": 0.367417, - "sha_sample_offset": 1044480, - "sha_hash_captured": false, - "cleanup": {"umount": 0, "mdconfig_detach": 0, "kldunload": 0, "qemu_alive": false, "overlay_exists": false} - } - ] -} diff --git a/docs/refactoring-plan.md b/docs/refactoring-plan.md deleted file mode 100644 index f43dc45..0000000 --- a/docs/refactoring-plan.md +++ /dev/null @@ -1,37 +0,0 @@ -# repo22 Linux/FreeBSD 结构对齐维护计划 - -## 基线 - -- FreeBSD 实现:`src/` -- Linux 参考:`/work/dev-src-linux/fs/erofs`(Linux 7.1-rc1 快照) -- 比较方法:逐文件和逐职责比较,不依赖共同 Git 历史 -- 运行目标:FreeBSD 15 amd64 - -## 决策规则 - -1. 先判断差异是否来自 FreeBSD vnode、GEOM、errno、锁或内核库接口。 -2. 只对非必要差异调整静态函数名、变量名、定义顺序和文件职责。 -3. 不为匹配 Linux 文件表而添加空模块或不适用的抽象。 -4. `src/Makefile` 是模块名、源码清单、架构门控和编译选项的权威来源。 -5. 构建产物、生成头和机器 include symlink 不进入版本控制。 - -## 本轮清单 - -- [x] 清除受跟踪的 `build/obj` 生成头和机器 symlink -- [x] 精确忽略模块构建及手工测试生成目录 -- [x] 由原生 `bsd.kmod.mk` 取代手写 amd64 clang/link 流程 -- [x] 明确拒绝未验证的非 amd64 架构 -- [x] 将 ZSTDIO 配置统一为 `WITH_ZSTDIO=0/1` -- [x] 按 Linux 顺序整理 Makefile 源码列表 -- [x] 对齐 `find_target_dirent` 和 `decompressor_*.c` 名称 -- [x] 将跨文件解压后端声明集中到 `internal.h` -- [x] 保留 BSD 专属 `erofs_vnops.c`、`lz4.c` 和后端调用契约 -- [x] 在 FreeBSD 15 VM 完成双配置构建和 ZSTD 挂载 smoke - -## 验证门槛 - -1. `WITH_ZSTDIO=0` 与 `WITH_ZSTDIO=1` 均从空对象目录构建。 -2. 禁用产物不含 `ZSTD_*` 未解析符号,启用产物必须含预期内核 API。 -3. 启用产物完成加载、ZSTD 镜像挂载、文件读取、卸载和 KLD 清理。 -4. 非 `amd64` 和非法 `WITH_ZSTDIO` 值必须明确失败。 -5. 提交只包含 repo22 pathspec,且远端 `xdm/main` 与本地 HEAD 一致。 diff --git a/docs/refactoring-status.md b/docs/refactoring-status.md deleted file mode 100644 index 19008d9..0000000 --- a/docs/refactoring-status.md +++ /dev/null @@ -1,40 +0,0 @@ -# repo22 Linux/FreeBSD 结构对齐状态 - -## 已完成 - -- 使用 `/work/dev-src-linux/fs/erofs` 的 Linux 7.1-rc1 快照逐文件复核;未使用 - 两个实现共享历史的假设。 -- `namei.c` 的静态 helper 已从 `erofs_find_target_dirent` 恢复为 Linux 名称 - `find_target_dirent`。 -- `lzma.c`、`deflate.c`、`zstd.c` 已按 Linux 职责名调整为 - `decompressor_lzma.c`、`decompressor_deflate.c`、 - `decompressor_zstd.c`。 -- 解压后端原型已移入 `internal.h`;MicroLZMA 内嵌 XZ 的 allocator helper - 已限制为编译单元内部符号。 -- `src/Makefile` 已按 Linux 的 metadata、压缩调度/映射、算法后端顺序组织, - 并成为 `build.sh` 的唯一源码和模块名来源。 -- 受跟踪的对象目录、vnode 生成头和 amd64/x86 机器 symlink 已删除并忽略。 - -## 保留差异 - -- `erofs_vnops.c` 承载 FreeBSD vnode、pager、NFS 和只读操作语义。 -- `lz4.c` 保持独立,因为 BSD 后端不使用 Linux page/LZ4 调度接口。 -- 后端函数保持 BSD 内部 API,而不是复制 Linux decompressor descriptor 和 - page 生命周期。 -- FreeBSD 路径返回正 errno,并保留 GEOM/provider、lockmgr 和 vnode 规则。 -- Linux `sysfs`、file-backed、fscache、page-cache sharing 和 `zutil` 职责不以 - 空文件模拟。 -- 当前构建门控仅允许已验证的 FreeBSD 15 `amd64`。 - -## 2026-08-09 验证 - -- VM:FreeBSD `15.0-RELEASE-p8` amd64。 -- 构建源:FreeBSD `15.0 RELEASE-p9` `sys` 树。 -- `WITH_ZSTDIO=0` 构建通过,模块 SHA256: - `d11d0319dc1d786eaa868a0c05c2abaf43da8480acacb0e1e4b72b7542b05432`; - 无 `ZSTD_*` 未解析符号。 -- `WITH_ZSTDIO=1` 构建通过,模块 SHA256: - `82471f19812e9877ea06901d1ba0cb4378b2bc6476ec70f6e024e0dc777c0c71`; - 编译命令包含 `-DZSTDIO` 并引用 FreeBSD 内核 ZSTD API。 -- 启用模块完成加载、`zstd-level1.erofs` 只读挂载、6 个文件读取与 SHA256、 - 卸载、md detach 和 KLD unload;测试后无遗留挂载或模块。 diff --git a/src/erofs_defs.h b/erofs_defs.h similarity index 100% rename from src/erofs_defs.h rename to erofs_defs.h diff --git a/src/erofs_fs.h b/erofs_fs.h similarity index 100% rename from src/erofs_fs.h rename to erofs_fs.h diff --git a/src/erofs_vnops.c b/erofs_vnops.c similarity index 100% rename from src/erofs_vnops.c rename to erofs_vnops.c diff --git a/src/inode.c b/inode.c similarity index 100% rename from src/inode.c rename to inode.c diff --git a/src/internal.h b/internal.h similarity index 100% rename from src/internal.h rename to internal.h diff --git a/issues/README.md b/issues/README.md deleted file mode 100644 index 5ce37b7..0000000 --- a/issues/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Issue Status - -This directory preserves both resolved investigations and approved validation -gaps. Resolved files remain historical evidence; they are not current failures. - -| Issue | Status | Current conclusion | -| --- | --- | --- | -| [TC010 48-bit statfs](TC010-48bit-statfs-large-provider.md) | RESOLVED | A qualified 16 TiB-plus sparse provider completed mount and 64-bit `statfs` validation. | -| [TC060 pathconf](TC060-pathconf-standard-values.md) | RESOLVED | FreeBSD-standard values were implemented and dynamically revalidated. | -| [TC153 large directory index](TC153-large-directory-block-index-validation.md) | RESOLVED | The exact fixed KLD returned `EINTEGRITY` on the multi-TiB sparse-provider path. | -| [Explicit mapped extent payload](extent-metadata-fixture-unavailable.md) | SHELVED | TC146 remains PARTIAL because no independently validated positive mapped-payload fixture is available. | - -The remaining shelved item records its trigger, affected feature, analysis, -attempts, results, progress, and acceptance criteria. It is a fixture/tooling -gap, not an observed kernel failure. diff --git a/issues/TC010-48bit-statfs-large-provider.md b/issues/TC010-48bit-statfs-large-provider.md deleted file mode 100644 index 7bc9556..0000000 --- a/issues/TC010-48bit-statfs-large-provider.md +++ /dev/null @@ -1,92 +0,0 @@ -# TC010 48-bit statfs Large Provider - -Status: **RESOLVED - qualified sparse vnode provider validated** - -Resolved: 2026-08-09 - -Baseline: `cd0e985b5ac54a4b7acb7042422329ad1729fb3e` - -## Original Problem - -TC010 requires a real EROFS mount whose 48-bit superblock block count has a -nonzero high word. A nonzero `blocks_hi` declares at least `2^32` filesystem -blocks, so a 4 KiB filesystem needs a provider larger than 16 TiB. Earlier -vnode-md work used only small regular images and correctly could not claim PASS. - -This affects: - -- 48-bit superblock block-count decoding; -- primary-device media-size validation; -- FreeBSD `statfs(2)` and `df(1)` 64-bit totals; -- the union selector between `rb.blocks_hi` and `rb.rootnid_2b`. - -## Exact Trigger - -1. `EROFS_FEATURE_INCOMPAT_48BIT` is set. -2. `rootnid_8b` is nonzero, selecting `rb.blocks_hi`. -3. `blocks_hi` is nonzero. -4. Provider media size is at least `total_blocks << blkszbits`. -5. Superblock CRC32C, root inode, and normal mount checks pass. - -A short provider fails with `ENXIO`; that negative guard cannot replace the -positive statfs test. TC150's `rootnid_8b == 0` fallback and multidevice totals -also select different production paths. - -## Earlier Attempts - -1. Structured metadata patching and CRC32C recomputation succeeded. -2. The normal small vnode provider failed media-size validation as expected. -3. Static source comparison confirmed Linux's `48BIT && rootnid_8b` selector, - but static arithmetic was not counted as dynamic coverage. -4. No earlier run had attached and mounted a qualified 16 TiB provider, so the - issue remained SHELVED. - -## Resolution - -The isolated FreeBSD 15.0-RELEASE-p8 guest uses UFS2, which supports a sparse -regular file large enough for vnode md without allocating 16 TiB physically: - -```text -truncate -s 17592193667072 TC010-provider.raw -stat: size=17592193667072 allocated_blocks=15232 block_size=32768 -mdconfig -a -t vnode -f TC010-provider.raw: md0 -diskinfo mediasize: 17592193667072 bytes -``` - -The structured fixture encoded: - -```text -blocks_lo=1861 -blocks_hi=1 -rootnid_8b=36 -total_blocks=4294969157 -required_provider_length=17592193667072 -``` - -Prefix SHA256: -`d3ffadc6fc9e73f85a8a5973b4ac5ee2a2da57b4e8baeccd259fa4325a2146cf`. - -The exact prefix was copied to the sparse provider, then extended with zeros. -Mount succeeded with KLD SHA256 -`8349c97c7ced253fff32a9e706f29313f309cb63a270e4e39a4501426f2b95c6`. -`df` reported: - -```text -Filesystem Type 1024-blocks Used Avail Capacity -/dev/md0 erofs 17179876628 17179876628 0 100% -``` - -`17179876628 == 4294969157 * 4`; no 32-bit wrap occurred. `f_bfree` and -available blocks were zero, so Used correctly equaled total. The control file -matched, dmesg had no new fault, and the mount, md unit, sparse provider, and -KLD were cleaned up. - -## Related Coverage - -- TC017 separately repeated the small-provider `ENXIO` guard and qualified - sparse-provider mount, proving the nonzero high-word parse dynamically. -- TC018 separately read exact bytes from physical offset - `17592191561728`, above 16 TiB; it did not substitute a large `df` result for - high-address data I/O. - -No repo22 kernel source change was made during this resolution. diff --git a/issues/TC060-pathconf-standard-values.md b/issues/TC060-pathconf-standard-values.md deleted file mode 100644 index db59e3d..0000000 --- a/issues/TC060-pathconf-standard-values.md +++ /dev/null @@ -1,119 +0,0 @@ -# TC060 Standard pathconf Values - -Status: **RESOLVED - standard FreeBSD pathconf values validated** - -Last updated: 2026-08-09 - -Resolved: 2026-08-09 - -Priority: High - -Implementation status: Fixed by `cdba7e54fb9e9d82980a06f178e68d0bbc1663ac` -and dynamically validated - -## Problem - -The exact repo22 9ae22009f KLD returns EINVAL for two standard pathconf names on -an otherwise valid mounted EROFS directory: - -- _PC_NO_TRUNC -- _PC_CHOWN_RESTRICTED - -The same call correctly returns NAME_MAX=255, PATH_MAX=1024, -FILESIZEBITS=64, and LINK_MAX=2147483647. TC060 therefore fails at the kernel -behavior level; this is not a missing guest tool or fixture limitation. - -## Trigger and Evidence - -Environment: - -- FreeBSD 15.0-RELEASE-p8 amd64 guest on port 9222. -- Exact source commit: 9ae22009f23a65320730072a780998e80aa9b728. -- KLD SHA256: - 19ad086bd2508cbb4c57b1a51f38c93057d438b84fbcfa2e637ff2519f5bc590. -- Fixture: vfs-plain.erofs SHA256 - 79f0b5f4aa8e7ea532b711ee8fb466f14f35d0952446d41ba4bbc4d6e008b8ff. - -Direct command: - - /tmp/repo22-g3/g3_vfs_probe pathconf \ - /tmp/repo22-g3/mnt/testdir - -Observed output and process status: - - name_max=255 path_max=1024 filesizebits=64 link_max=2147483647 \ - no_trunc=error:22 chown_restricted=error:22 - g3_vfs_probe: unexpected EROFS pathconf values - probe_status=1 - -The test used a fresh dynamic md0, a read-only EROFS mount, and the exact KLD. -After capture, umount and md detach both returned zero. Final guest state was -zero EROFS mounts, zero md units, and no loaded EROFS KLD. - -## Analysis - -erofs_pathconf() handles NAME_MAX, PATH_MAX, FILESIZEBITS, LINK_MAX, and ACL -queries directly, then delegates other names to vop_stdpathconf(). In this -FreeBSD 15 configuration the delegation returns EINVAL for both names above. -The mounted filesystem is immutable and enforces 255-byte names, so reporting -NO_TRUNC=1 and CHOWN_RESTRICTED=1 is consistent with the implemented behavior -and with the TC contract. - -This failure affects applications that use pathconf(2) to discover pathname -truncation and ownership-change restrictions. It does not affect the four -values that repo22 already handles directly. - -## Original Required Resolution - -Add explicit FreeBSD vnode pathconf handling for both standard names, then -rerun TC060 with the direct syscall helper. Acceptance requires both values to -be 1, all six queries to have errno 0, unchanged mount/read behavior, no new -dmesg diagnostics, and complete mount/md/KLD cleanup. - -## Resolution - -FreeBSD 15's `vop_stdpathconf()` intentionally provides only generic values -such as `_PC_ASYNC_IO`, `_PC_PATH_MAX`, and zero-valued optional features; its -default case returns `EINVAL`. UFS, tmpfs, and ext2fs therefore implement -`_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` in their filesystem pathconf methods -before delegating all remaining names to `vop_stdpathconf()`. - -EROFS now follows that FreeBSD pattern: the two names return 1, while the -existing NAME_MAX, PATH_MAX, FILESIZEBITS, LINK_MAX, and ACL cases remain -unchanged and the default branch still calls `vop_stdpathconf()`. This matches -the implemented filesystem behavior: uid/gid mutation is rejected with EROFS, -and lookup of a component longer than `EROFS_NAME_LEN` returns ENAMETOOLONG. -No lock, allocation, reference, or cleanup path was added. - -Linux EROFS was used only as a maintenance comparison for the 255-byte name -limit and ENAMETOOLONG behavior. FreeBSD 15 VOP and syscall semantics were the -authority for the returned values and errno behavior. - -## Qualified Retest - -The exact source archive for the fix commit built natively on FreeBSD -15.0-RELEASE-p8 with kernel `-Werror` in both configurations: - -- `WITH_ZSTDIO=0`: KLD SHA256 - `68536e03ce93c6c04aab9cf29dab81ee5802d4b94401bd1a4bd752de40c0e504`. -- `WITH_ZSTDIO=1`: KLD SHA256 - `598f171d355af78c64407a6d513d20f053530620da92df7f8ccfdf9a804e463d`. - -The dependency-minimal `WITH_ZSTDIO=0` KLD and the original TC060 helper -reported: - -```text -name_max=255 path_max=1024 filesizebits=64 link_max=2147483647 -no_trunc=1 chown_restricted=1 -``` - -All six queries had errno 0. Additional direct checks returned ASYNC_IO=200112, -ACL_EXTENDED=1, ACL_PATH_MAX=254, and ACL_NFS4=0 with errno 0. An unknown name -returned `-1/EINVAL(22)`, and a 256-byte component returned -`ENAMETOOLONG(63)`. The mounted file hash and read-only EROFS behavior were -unchanged, dmesg was byte-identical before and after, and final mount/md/KLD -and guest artifact counts were zero. - -Complete build, value, smoke, discarded-attempt, review, and cleanup evidence -is in -`tests/results/manual/2026-08-09T1120Z-tc060-fix/manual-test-report.md`. diff --git a/issues/TC153-large-directory-block-index-validation.md b/issues/TC153-large-directory-block-index-validation.md deleted file mode 100644 index 8e6b3a3..0000000 --- a/issues/TC153-large-directory-block-index-validation.md +++ /dev/null @@ -1,193 +0,0 @@ -# TC153 Large Directory Block Index Validation - -Status: **RESOLVED - exact-source kernel validation passed** - -Last updated: 2026-08-09 - -Priority: Critical validation gap - -Implementation status: Source fix present, build-verified, and dynamically -validated - -## Problem - -`erofs_find_target_block()` used signed `int` values for the binary-search -front, back, and midpoint indexes. A directory whose logical size describes -`2147483649` 4 KiB blocks produces a final block index of `2147483648`. -Converting that value to `int` can make the initial back bound negative. The -search can then return `ENOENT` without reading any directory block, and the -FreeBSD namecache can retain a false negative result for corrupt media. - -The review change uses `uint64_t` for block-search bounds and midpoints, checks -the midpoint multiplication, and handles the `mid == 0` lower-bound case. The -within-block search remains `uint32_t` because a validated EROFS directory -block can contain only a block-sized number of entries. - -## Affected Features - -- Cold pathname lookup in very large or maliciously sized directories. -- Corruption reporting and `EINTEGRITY` propagation from directory reads. -- Negative namecache correctness after a failed lookup. -- Linux/FreeBSD behavioral review of EROFS's two-level directory search. - -Normal directories are not expected to approach this bound. The practical -risk is a crafted image causing an incorrect `ENOENT`, not an ordinary mkfs -image losing entries. - -## Trigger Conditions - -All of the following are needed to exercise the original width error: - -1. A directory inode is accepted with a logical block count greater than - `INT_MAX`. -2. The directory layout reaches `erofs_find_target_block()` rather than being - rejected by an earlier inline-data bounds check. -3. A cold lookup is issued for a name that is not satisfied by an already - cached vnode or negative namecache entry. -4. The backing image is too short for the computed midpoint read, so the fixed - code should return `EINTEGRITY` instead of a synthetic miss. - -For 4 KiB blocks, TC153 encodes a size of `8796093026304` bytes, -`2147483649` blocks, and a final index of `2147483648`. - -## Current Analysis - -The source change is mechanically narrow and has passed both repo22 build -configurations. The checked multiplication protects a future block-size or -index change even though the current `OFF_MAX` inode limit already bounds the -product. The `mid == 0` guard prevents unsigned subtraction from wrapping when -the search moves below the first block. - -Dynamic proof must show more than a large `st_size`. It must prove that the -lookup enters the block binary search. An image rejected in -`erofs_read_inode()` is not evidence for this fix, and an `ENOENT` observed -after a warm negative cache is ambiguous. - -## Attempts and Results - -### Attempt 1: Small FLAT_INLINE image - -- Artifact directory: - `/work/build/repo22-review-artifact.LtqNxH` -- Base image: `large-dir-base.erofs`, 4096 bytes, SHA256 - `973d2a1c90ac1bf776ed76a1fc6a7e88b2156fac3bf04ddbae19cfafebbd562e` -- Patched image: `large-dir-intmax.erofs`, 4096 bytes, SHA256 - `ce5cad3eff34ea50ca89eedf93d8ef90e6cea96858a138685fae45f39084de1f` -- Recorded inode: NID 40 at byte 1280. - -The directory was Layout 2 (`FLAT_INLINE`). Patching only `i_size` made its -declared inline tail range invalid, so inode validation rejected it before -`erofs_find_target_block()`. This attempt is invalid as TC153 dynamic evidence -and must never be marked PASS. - -### Attempt 2: More entries to force block data - -- Artifact directory: - `/work/build/repo22-review-artifact2.FmMTup` -- Generated base: `large-dir-base.erofs`, 65536 bytes, SHA256 - `50eb28351788ab1801408aca0a6fca6352755f5ca56efe6e2bb3e2fe08305663` -- Directory: NID 40, 21052 bytes, still Layout 2. - -The generation process was interrupted before producing a patched image or a -FreeBSD result. A later deterministic rerun reproduced the same base hash and -showed why the approach failed: erofs-utils 1.8.6 tail-packs directories even -when they span multiple data blocks. Its `noinline_data` option does not turn -directory tail packing off. - -### Attempt 3: Follow-up agents - -Multiple follow-up agents were assigned the fixture and guest validation. -They either stopped while preparing the second image or returned no executed -commands, files, hashes, or guest output. These attempts added no evidence and -are recorded to prevent their elapsed time from being mistaken for progress. - -### Attempt 4: Sparse-provider design - -A guest-side sparse vnode provider was considered so a valid directory could -declare the complete multi-terabyte range. It was not executed. Such a scheme -must prove the GEOM media size, avoid materializing terabytes, preserve valid -initial directory blocks, and still force a cold read at the binary-search -midpoint. No result exists for this approach. - -### Attempt 5: Reproducible FLAT_PLAIN conversion - -The tracked generator now copies all 21052 bytes of the generated directory -to appended blocks, changes only that inode to Layout 0 (`FLAT_PLAIN`), updates -the image block count and CRC32C, and then applies the large logical size. - -Host reproduction on 2026-08-09 produced: - -- `large-dir-base.erofs`: 65536 bytes, SHA256 - `50eb28351788ab1801408aca0a6fca6352755f5ca56efe6e2bb3e2fe08305663` -- `large-dir-intmax.erofs`: 90112 bytes, SHA256 - `0f90d3d57adbbbd946e41b225c1f6c464915c6abb0b13478ec9b2a318def1f72` -- Patched inode: NID 40 at byte 1280, Layout 0, raw block 16. -- Declared image blocks: 22. - -This resolved the fixture-construction problem. The qualified kernel run is -recorded below. - -### Attempt 6: Qualified sparse GEOM provider - -The G3 takeover generated a checksum-valid sparse prefix from Attempt 5 and -performed one targeted FreeBSD 15 run against the exact source requested by -the assignment. - -- Source commit: - 9ae22009f23a65320730072a780998e80aa9b728. -- KLD SHA256: - 19ad086bd2508cbb4c57b1a51f38c93057d438b84fbcfa2e637ff2519f5bc590. -- Sparse-prefix SHA256: - f9337f83b1f568f6d904331a7749e6362a691055cd5af95b59bc89772f04e3b0. -- Prefix qualification: NID 40, inode offset 1280, extended Layout 0, - start block 16, 2147483649 directory blocks, final block index 2147483648, - valid superblock checksum. -- Sparse provider: 8796093091840 logical bytes, 448 allocated 512-byte - sectors, and the first 90112 bytes retained the prefix SHA256. -- GEOM diskinfo size: 8796093091840 bytes. -- Mounted /huge: size 8796093026304, NID 40. - -The first two cold stat operations for /huge/missing each returned EINTEGRITY -(97). Root readdir then returned three complete entries and validated all -kernel/libc restart cookies. A third lookup after that readdir again returned -EINTEGRITY. No lookup returned ENOENT, so no false negative namecache entry -masked the corruption. - -Unmount, md detach, sparse-provider removal, and KLD unload all succeeded. -The only new dmesg lines in the complete G3 run were expected SIGBUS child -exits from the unrelated assigned mmap tests; TC153 added no diagnostic. - -## Historical Remaining Validation - -Attempt 6 completed steps 1-5 and 7 below. The assignment mandated the exact -fixed source, so the optional pre-fix comparison in step 6 was not run. - -1. Regenerate the images with - `tests/results/manual/2026-08-09T0124Z-final-review/prepare-fixtures.sh` and - verify the hashes and Layout 0 assertion. -2. Load the exact module under test in a clean FreeBSD 15 guest. -3. Mount the 90112-byte image and confirm `/huge` reports - `8796093026304` bytes. -4. With a cold parent cache, run two lookups of `/huge/missing` and capture - direct command exit codes plus `truss` errno. -5. Require `EINTEGRITY` on every fixed-module lookup, including after root - readdir. Confirm no negative cache changes the result. -6. Repeat with the pre-fix module to demonstrate the erroneous `ENOENT` path. -7. Record dmesg, mount/md/KLD cleanup, module and fixture hashes in a dated - manual report. - -## Original Acceptance Criteria - -TC153 can move from SHELVED to PASS only after the fixed FreeBSD KLD returns -`EINTEGRITY` repeatedly from the valid Layout 0 fixture, the pre-fix behavior -is distinguished, no trap or panic occurs, and complete cleanup evidence is -recorded. Host fixture generation and static source review alone are -insufficient. - -## Resolution - -TC153 is PASS because the exact fixed FreeBSD KLD returned EINTEGRITY -repeatedly from the valid Layout 0 fixture before and after root readdir, no -trap or panic occurred, and complete hash/provider/cleanup evidence is recorded -in tests/results/manual/2026-08-09T1059Z-g3/manual-test-report.md. Host fixture -generation and static source review alone remain insufficient. diff --git a/issues/extent-metadata-fixture-unavailable.md b/issues/extent-metadata-fixture-unavailable.md deleted file mode 100644 index 1442fc7..0000000 --- a/issues/extent-metadata-fixture-unavailable.md +++ /dev/null @@ -1,169 +0,0 @@ -# Explicit Extent Metadata Fixture Unavailable - -Status: **SHELVED - positive mapped payload unavailable** - -Last updated: 2026-08-09, final aggregation - -Latest reviewed baseline: `fcc85b93d5f8fd9671686bd68bf3b086c8bd25cf` - -Priority: High validation gap - -Implementation status: ABI and control flow implemented; positive mapped -kernel read pending - -## Problem - -repo22 implements the newer compressed-extent metadata path selected by -`Z_EROFS_ADVISE_EXTENTS`. Dynamic positive validation requires an image with a -valid extent table that maps a real payload. erofs-utils 1.8.6 cannot emit or -validate this on-disk format, and the repository's structured helpers cannot -currently relocate a legacy compressed inode into a validated positive extent -table. - -HEAD2, interlaced pclusters, legacy full/compact indexes, partial references, -fragments, and all four decoders have separate dynamic coverage. None of those -substitutes for entering `z_erofs_map_blocks_ext()` with a mapped payload. - -## Required Trigger - -1. The inode uses compressed-full layout. -2. The map header sets `Z_EROFS_ADVISE_EXTENTS`. -3. The selected 4-, 8-, 16-, or 32-byte records form a complete valid table. -4. Physical fields identify real payload bytes for the declared algorithm. -5. Logical starts, physical lengths, partial flags, and final-record semantics - agree with the source file. -6. A FreeBSD read reaches the explicit mapper and returns source-identical - bytes. - -Zero-count tables, overflowing physical bases, holes, and ordinary full-index -images are negative or adjacent coverage, not this trigger. - -## G5 Independent Attempts - -All inputs below were generated in new G5 output directories. No old image or -report was consumed as a fixture. - -### 1. erofs-utils 1.8.6 capability scan - -Observed tool output: - -```text -mkfs.erofs (erofs-utils) 1.8.6 -available compressors: lz4, lz4hc, lzma, deflate, libdeflate, zstd -``` - -The helper searched the installed 1.8.6 `include/` and `lib/` trees for these -on-disk symbols: - -```text -Z_EROFS_ADVISE_EXTENTS: 0 matches -z_erofs_extent_recsize: 0 matches -struct z_erofs_extent {: 0 matches -``` - -The mkfs help has no explicit-record option. `--max-extent-bytes` only limits -decompressed extent size. - -Result: **generator unavailable**. - -### 2. `--max-extent-bytes` generation attempt - -`g5_fixtures.py` generated a fresh LZ4 full-index image with: - -```text --zlz4 -C65536 -Elegacy-compress --max-extent-bytes=65536 -``` - -Structured reopen and `dump.erofs` produced: - -```text -image = images/extent-attempt.erofs -image SHA256 = ee7472c23ccffd5eef6f4a3171ea53ae2e840f8a1ea417b2630065175ecf3225 -source SHA256 = 5be510e6b43f1ed0cda4261288ea70df11607b6ced29bc90d32ed2849ecc5e35 -inode layout = 1 (compressed full) -map header offset = 1312 -h_advise = 2 -HEAD1 algorithm = 0 (LZ4) -explicit bit selected = no -``` - -`h_advise=2` is ordinary HEAD1 big-pcluster metadata. Naming or sizing the -image as an extent attempt does not enter the explicit mapper. - -Result: **not an explicit fixture**. - -### 3. Structured conversion attempt - -The G5 transformer parsed the source inode, map header, full lcluster indexes, -physical extents, and payload bounds. It refused to emit a purported positive -fixture because conversion requires relocating variable-size extent records, -subsequent inode metadata, and possibly payload blocks. erofs-utils 1.8.6 -cannot reopen and validate the resulting ABI, so an ad hoc rewrite would not -provide trustworthy evidence. - -The existing `tests/review_fixtures.py` helper was also reviewed. Its -`extent-pa-wrap.erofs` conversion deliberately writes two 4-byte records and a -physical base whose `pa + plen` overflows. That is a negative overflow trigger -for TC155. It does not describe a valid mapped payload and cannot close this -issue. - -Result: **structured positive conversion unavailable**. - -### 4. ABI and control-flow comparison - -The G5 review compared repo22 `src/erofs_fs.h`/`src/zmap.c` with the Linux EROFS -definitions and mapper. Both sides define: - -- record sizes 4, 8, 16, and 32 bytes; -- implicit 64-bit physical bases for 4-byte records; -- per-record low physical starts for 8-byte records; -- explicit counts and binary search for 16/32-byte records; -- high physical and logical words where the record size carries them; -- partial-reference, shifted/interlaced format, and final fragment handling; -- malformed-count and arithmetic bounds checks. - -This reduces implementation risk but is static evidence only. - -## Impact - -- TC146 HEAD2: dynamic PASS. -- TC146 interlaced: dynamic PASS. -- TC146 explicit mapped payload: SHELVED. -- TC146 overall: **PARTIAL**, never overall PASS while this issue remains. -- Record-size variants, positive binary-search transitions, mapped high words, - and explicit-record partial references remain dynamically unvalidated. - -No repo22 kernel source was changed because this is a fixture/tooling gap, not -an observed kernel failure. - -## Progress - -- [x] Reproduce tool limitation against exactly erofs-utils 1.8.6. -- [x] Record tool output and zero symbol matches. -- [x] Generate and inspect a fresh `--max-extent-bytes` attempt. -- [x] Record source/image hashes and map-header fields. -- [x] Audit the existing structured negative helper. -- [x] Compare FreeBSD and Linux record layouts/control flow. -- [x] Reject globally unordered 16-byte and 32-byte explicit tables through - TC157 on FreeBSD 15 without reading the referenced payload. -- [ ] Obtain a producer or validator for positive mapped extent records. -- [ ] Generate at least one source-identical mapped payload. -- [ ] Cover 4/8/16/32-byte positive records where applicable. -- [ ] Exercise binary-search transitions, partial refs, high words, and final - fragments dynamically. - -## Acceptance Criteria - -This issue can close only after a reproducible helper emits a valid mapped -compressed payload, records its structural fields and hashes, validates it -with an independent format-aware implementation, and FreeBSD full/boundary -reads match the source. Negative overflow/hole fixtures and static review do -not satisfy acceptance. - -## Final Residual Risk - -TC157 closes the known binary-search ordering bug for descending, duplicate, -and cross-branch 16-byte and 32-byte tables. It does not supply the missing -positive mapped payload, dynamically cover every record-size variant, or -benchmark extremely large extent counts. Those limits do not change the issue -status or TC146's PARTIAL result. diff --git a/src/lz4.c b/lz4.c similarity index 100% rename from src/lz4.c rename to lz4.c diff --git a/src/namei.c b/namei.c similarity index 100% rename from src/namei.c rename to namei.c diff --git a/src/.clang-format b/src/.clang-format deleted file mode 100644 index 3d436fa..0000000 --- a/src/.clang-format +++ /dev/null @@ -1,199 +0,0 @@ -# Basic .clang-format ---- -BasedOnStyle: WebKit -AlignAfterOpenBracket: DontAlign -AlignConsecutiveMacros: AcrossEmptyLines -AlignConsecutiveAssignments: false -AlignConsecutiveDeclarations: false -AlignEscapedNewlines: Left -AlignOperands: false -AlignTrailingComments: true -AllowAllArgumentsOnNextLine: false -AllowAllParametersOfDeclarationOnNextLine: false -AllowShortBlocksOnASingleLine: Never -AllowShortCaseLabelsOnASingleLine: false -AllowShortFunctionsOnASingleLine: InlineOnly -AllowShortIfStatementsOnASingleLine: Never -AllowShortLoopsOnASingleLine: false -AlwaysBreakAfterReturnType: TopLevelDefinitions -AlwaysBreakBeforeMultilineStrings: false -AlwaysBreakTemplateDeclarations: MultiLine -BinPackArguments: true -BinPackParameters: true -BreakBeforeBinaryOperators: None -BreakBeforeBraces: WebKit -BreakBeforeTernaryOperators: false -# TODO: BreakStringLiterals can cause very strange formatting so turn it off? -BreakStringLiterals: false -# Prefer: -# some_var = function(arg1, -# arg2) -# over: -# some_var = -# function(arg1, arg2) -PenaltyBreakAssignment: 100 -# Prefer: -# some_long_function(arg1, arg2 -# arg3) -# over: -# some_long_function( -# arg1, arg2, arg3) -PenaltyBreakBeforeFirstCallParameter: 100 -CompactNamespaces: true -DerivePointerAlignment: false -DisableFormat: false -ForEachMacros: - - ARB_ARRFOREACH - - ARB_ARRFOREACH_REVWCOND - - ARB_ARRFOREACH_REVERSE - - ARB_FOREACH - - ARB_FOREACH_FROM - - ARB_FOREACH_SAFE - - ARB_FOREACH_REVERSE - - ARB_FOREACH_REVERSE_FROM - - ARB_FOREACH_REVERSE_SAFE - - BIT_FOREACH_ISCLR - - BIT_FOREACH_ISSET - - CPU_FOREACH - - CPU_FOREACH_ISCLR - - CPU_FOREACH_ISSET - - FOREACH_THREAD_IN_PROC - - FOREACH_PROC_IN_SYSTEM - - FOREACH_PRISON_CHILD - - FOREACH_PRISON_DESCENDANT - - FOREACH_PRISON_DESCENDANT_LOCKED - - FOREACH_PRISON_DESCENDANT_LOCKED_LEVEL - - MNT_VNODE_FOREACH_ALL - - MNT_VNODE_FOREACH_ACTIVE - - RB_FOREACH - - RB_FOREACH_FROM - - RB_FOREACH_SAFE - - RB_FOREACH_REVERSE - - RB_FOREACH_REVERSE_FROM - - RB_FOREACH_REVERSE_SAFE - - SLIST_FOREACH - - SLIST_FOREACH_FROM - - SLIST_FOREACH_FROM_SAFE - - SLIST_FOREACH_SAFE - - SLIST_FOREACH_PREVPTR - - SPLAY_FOREACH - - LIST_FOREACH - - LIST_FOREACH_FROM - - LIST_FOREACH_FROM_SAFE - - LIST_FOREACH_SAFE - - STAILQ_FOREACH - - STAILQ_FOREACH_FROM - - STAILQ_FOREACH_FROM_SAFE - - STAILQ_FOREACH_SAFE - - TAILQ_FOREACH - - TAILQ_FOREACH_FROM - - TAILQ_FOREACH_FROM_SAFE - - TAILQ_FOREACH_REVERSE - - TAILQ_FOREACH_REVERSE_FROM - - TAILQ_FOREACH_REVERSE_FROM_SAFE - - TAILQ_FOREACH_REVERSE_SAFE - - TAILQ_FOREACH_SAFE - - VM_MAP_ENTRY_FOREACH - - VM_PAGE_DUMP_FOREACH -SpaceBeforeParens: ControlStatementsExceptForEachMacros -IndentCaseLabels: false -IndentPPDirectives: None -Language: Cpp -NamespaceIndentation: None -PointerAlignment: Right -ContinuationIndentWidth: 4 -IndentWidth: 8 -TabWidth: 8 -ColumnLimit: 80 -UseTab: Always -SpaceAfterCStyleCast: false -IncludeBlocks: Regroup -IncludeCategories: - - Regex: '^\"opt_.*\.h\"' - Priority: 1 - SortPriority: 10 - - Regex: '^' - Priority: 2 - SortPriority: 20 - - Regex: '^' - Priority: 2 - SortPriority: 21 - - Regex: '^' - Priority: 2 - SortPriority: 22 - - Regex: '^' - Priority: 2 - SortPriority: 23 - - Regex: '^' - Priority: 3 - SortPriority: 30 - - Regex: '^ "$RESULTS_FILE" - -test_case() { - local name="$1" - local result="$2" - local error="$3" - - TOTAL=$((TOTAL + 1)) - if [ "$result" = "PASS" ]; then - PASSED=$((PASSED + 1)) - echo "✓ $name" >> "$RESULTS_FILE" - printf "%-60s [PASS]\n" "$name" - else - FAILED=$((FAILED + 1)) - echo "✗ $name: $error" >> "$RESULTS_FILE" - printf "%-60s [FAIL] %s\n" "$name" "$error" - fi -} - -# Generate test data files -generate_test_data() { - echo "Generating test data..." - - # Small file (512B) - dd if=/dev/zero of=small_512b.dat bs=512 count=1 2>/dev/null - - # Medium file (4KB) - dd if=/dev/urandom of=medium_4k.dat bs=4096 count=1 2>/dev/null - - # Medium-large (32KB) - dd if=/dev/urandom of=medium_32k.dat bs=32768 count=1 2>/dev/null - - # Large file (64KB) - dd if=/dev/urandom of=large_64k.dat bs=65536 count=1 2>/dev/null - - # 1MB file - dd if=/dev/urandom of=large_1m.dat bs=1048576 count=1 2>/dev/null - - # Highly compressible (zeros) - dd if=/dev/zero of=compressible_8k.dat bs=8192 count=1 2>/dev/null - - # Text data - for i in {1..1000}; do echo "The quick brown fox jumps over the lazy dog."; done > text_data.txt - - # Random incompressible - dd if=/dev/urandom of=random_16k.dat bs=16384 count=1 2>/dev/null - - echo "Test data generated." - echo "" -} - -# LZ4 Tests -test_lz4() { - echo "--- LZ4 TESTS ---" - - # Test 1: Small file - if lz4 -c small_512b.dat > small_512b.lz4 2>/dev/null; then - if lz4 -d -c small_512b.lz4 > small_512b.out 2>/dev/null && cmp -s small_512b.dat small_512b.out; then - test_case "LZ4: small file (512B)" "PASS" "" - else - test_case "LZ4: small file (512B)" "FAIL" "decompression mismatch" - fi - else - test_case "LZ4: small file (512B)" "FAIL" "compression failed" - fi - - # Test 2: Medium file (4KB) - if lz4 -c medium_4k.dat > medium_4k.lz4 2>/dev/null; then - if lz4 -d -c medium_4k.lz4 > medium_4k.out 2>/dev/null && cmp -s medium_4k.dat medium_4k.out; then - test_case "LZ4: medium file (4KB)" "PASS" "" - else - test_case "LZ4: medium file (4KB)" "FAIL" "decompression mismatch" - fi - else - test_case "LZ4: medium file (4KB)" "FAIL" "compression failed" - fi - - # Test 3: Large file (64KB) - if lz4 -c large_64k.dat > large_64k.lz4 2>/dev/null; then - if lz4 -d -c large_64k.lz4 > large_64k.out 2>/dev/null && cmp -s large_64k.dat large_64k.out; then - test_case "LZ4: large file (64KB)" "PASS" "" - else - test_case "LZ4: large file (64KB)" "FAIL" "decompression mismatch" - fi - else - test_case "LZ4: large file (64KB)" "FAIL" "compression failed" - fi - - # Test 4: 1MB performance - if lz4 -c large_1m.dat > large_1m.lz4 2>/dev/null; then - START=$(date +%s%N) - if lz4 -d -c large_1m.lz4 > large_1m.out 2>/dev/null && cmp -s large_1m.dat large_1m.out; then - END=$(date +%s%N) - DURATION=$(( (END - START) / 1000000 )) - test_case "LZ4: 1MB file performance" "PASS" "Time: ${DURATION}ms" - else - test_case "LZ4: 1MB file performance" "FAIL" "decompression mismatch" - fi - else - test_case "LZ4: 1MB file performance" "FAIL" "compression failed" - fi - - # Test 5: Highly compressible - if lz4 -9 -c compressible_8k.dat > compressible_8k.lz4 2>/dev/null; then - if lz4 -d -c compressible_8k.lz4 > compressible_8k.out 2>/dev/null && cmp -s compressible_8k.dat compressible_8k.out; then - test_case "LZ4: highly compressible (zeros)" "PASS" "" - else - test_case "LZ4: highly compressible (zeros)" "FAIL" "decompression mismatch" - fi - else - test_case "LZ4: highly compressible (zeros)" "FAIL" "compression failed" - fi - - # Test 6: Random incompressible - if lz4 -c random_16k.dat > random_16k.lz4 2>/dev/null; then - if lz4 -d -c random_16k.lz4 > random_16k.out 2>/dev/null && cmp -s random_16k.dat random_16k.out; then - test_case "LZ4: random incompressible data" "PASS" "" - else - test_case "LZ4: random incompressible data" "FAIL" "decompression mismatch" - fi - else - test_case "LZ4: random incompressible data" "FAIL" "compression failed" - fi - - # Test 7: Corrupted data - dd if=/dev/urandom of=corrupted.lz4 bs=100 count=1 2>/dev/null - if lz4 -d -c corrupted.lz4 > /dev/null 2>&1; then - test_case "LZ4: corrupted data rejection" "FAIL" "should reject corrupted" - else - test_case "LZ4: corrupted data rejection" "PASS" "" - fi - - # Test 8: Truncated file - lz4 -c medium_4k.dat > truncated.lz4 2>/dev/null - dd if=truncated.lz4 of=truncated_short.lz4 bs=50 count=1 2>/dev/null - if lz4 -d -c truncated_short.lz4 > /dev/null 2>&1; then - test_case "LZ4: truncated data rejection" "FAIL" "should reject truncated" - else - test_case "LZ4: truncated data rejection" "PASS" "" - fi - - echo "" -} - -# DEFLATE Tests -test_deflate() { - echo "--- DEFLATE TESTS ---" - - # Test 1: Level 1 (fast) - if gzip -1 -c small_512b.dat > small_512b.gz 2>/dev/null; then - if gzip -d -c small_512b.gz > small_512b_gz.out 2>/dev/null && cmp -s small_512b.dat small_512b_gz.out; then - test_case "DEFLATE: level 1 compression" "PASS" "" - else - test_case "DEFLATE: level 1 compression" "FAIL" "decompression mismatch" - fi - else - test_case "DEFLATE: level 1 compression" "FAIL" "compression failed" - fi - - # Test 2: Level 6 (default) - if gzip -6 -c medium_32k.dat > medium_32k.gz 2>/dev/null; then - if gzip -d -c medium_32k.gz > medium_32k_gz.out 2>/dev/null && cmp -s medium_32k.dat medium_32k_gz.out; then - test_case "DEFLATE: level 6 compression" "PASS" "" - else - test_case "DEFLATE: level 6 compression" "FAIL" "decompression mismatch" - fi - else - test_case "DEFLATE: level 6 compression" "FAIL" "compression failed" - fi - - # Test 3: Level 9 (maximum) - if gzip -9 -c large_64k.dat > large_64k.gz 2>/dev/null; then - if gzip -d -c large_64k.gz > large_64k_gz.out 2>/dev/null && cmp -s large_64k.dat large_64k_gz.out; then - test_case "DEFLATE: level 9 compression" "PASS" "" - else - test_case "DEFLATE: level 9 compression" "FAIL" "decompression mismatch" - fi - else - test_case "DEFLATE: level 9 compression" "FAIL" "compression failed" - fi - - # Test 4: Invalid header - dd if=/dev/urandom of=invalid.gz bs=100 count=1 2>/dev/null - if gzip -d -c invalid.gz > /dev/null 2>&1; then - test_case "DEFLATE: invalid header rejection" "FAIL" "should reject invalid" - else - test_case "DEFLATE: invalid header rejection" "PASS" "" - fi - - # Test 5: Truncated stream - gzip -c medium_4k.dat > truncated.gz 2>/dev/null - dd if=truncated.gz of=truncated_short.gz bs=100 count=1 2>/dev/null - if gzip -d -c truncated_short.gz > /dev/null 2>&1; then - test_case "DEFLATE: truncated stream rejection" "FAIL" "should reject truncated" - else - test_case "DEFLATE: truncated stream rejection" "PASS" "" - fi - - # Test 6: Empty file - touch empty.dat - if gzip -c empty.dat > empty.gz 2>/dev/null; then - if gzip -d -c empty.gz > empty_gz.out 2>/dev/null; then - test_case "DEFLATE: empty file" "PASS" "" - else - test_case "DEFLATE: empty file" "FAIL" "decompression failed" - fi - else - test_case "DEFLATE: empty file" "FAIL" "compression failed" - fi - - echo "" -} - -# ZSTD Tests -test_zstd() { - echo "--- ZSTD TESTS ---" - - # Test 1: Level 1 (fast) - if zstd -1 -q -c small_512b.dat > small_512b.zst 2>/dev/null; then - if zstd -d -q -c small_512b.zst > small_512b_zst.out 2>/dev/null && cmp -s small_512b.dat small_512b_zst.out; then - test_case "ZSTD: level 1 compression" "PASS" "" - else - test_case "ZSTD: level 1 compression" "FAIL" "decompression mismatch" - fi - else - test_case "ZSTD: level 1 compression" "FAIL" "compression failed" - fi - - # Test 2: Level 15 (medium) - if zstd -15 -q -c medium_32k.dat > medium_32k.zst 2>/dev/null; then - if zstd -d -q -c medium_32k.zst > medium_32k_zst.out 2>/dev/null && cmp -s medium_32k.dat medium_32k_zst.out; then - test_case "ZSTD: level 15 compression" "PASS" "" - else - test_case "ZSTD: level 15 compression" "FAIL" "decompression mismatch" - fi - else - test_case "ZSTD: level 15 compression" "FAIL" "compression failed" - fi - - # Test 3: Level 22 (maximum) - if zstd -22 -q -c large_64k.dat > large_64k.zst 2>/dev/null; then - if zstd -d -q -c large_64k.zst > large_64k_zst.out 2>/dev/null && cmp -s large_64k.dat large_64k_zst.out; then - test_case "ZSTD: level 22 compression" "PASS" "" - else - test_case "ZSTD: level 22 compression" "FAIL" "decompression mismatch" - fi - else - test_case "ZSTD: level 22 compression" "FAIL" "compression failed" - fi - - # Test 4: Large file - if zstd -q -c large_1m.dat > large_1m.zst 2>/dev/null; then - if zstd -d -q -c large_1m.zst > large_1m_zst.out 2>/dev/null && cmp -s large_1m.dat large_1m_zst.out; then - test_case "ZSTD: 1MB file" "PASS" "" - else - test_case "ZSTD: 1MB file" "FAIL" "decompression mismatch" - fi - else - test_case "ZSTD: 1MB file" "FAIL" "compression failed" - fi - - # Test 5: Invalid magic number - dd if=/dev/urandom of=invalid.zst bs=100 count=1 2>/dev/null - if zstd -d -q -c invalid.zst > /dev/null 2>&1; then - test_case "ZSTD: invalid magic rejection" "FAIL" "should reject invalid" - else - test_case "ZSTD: invalid magic rejection" "PASS" "" - fi - - # Test 6: Truncated frame - zstd -q -c medium_4k.dat > truncated.zst 2>/dev/null - dd if=truncated.zst of=truncated_short.zst bs=50 count=1 2>/dev/null - if zstd -d -q -c truncated_short.zst > /dev/null 2>&1; then - test_case "ZSTD: truncated frame rejection" "FAIL" "should reject truncated" - else - test_case "ZSTD: truncated frame rejection" "PASS" "" - fi - - # Test 7: Corrupted data - zstd -q -c medium_4k.dat > original.zst 2>/dev/null - dd if=original.zst of=corrupted.zst bs=1 count=200 2>/dev/null - dd if=/dev/urandom bs=1 count=50 >> corrupted.zst 2>/dev/null - if zstd -d -q -c corrupted.zst > /dev/null 2>&1 && cmp -s medium_4k.dat /tmp/out 2>/dev/null; then - test_case "ZSTD: corrupted data rejection" "FAIL" "should reject corrupted" - else - test_case "ZSTD: corrupted data rejection" "PASS" "" - fi - - echo "" -} - -# LZMA Tests -test_lzma() { - echo "--- LZMA TESTS ---" - - # Test 1: Small file - if xz -z -c small_512b.dat > small_512b.xz 2>/dev/null; then - if xz -d -c small_512b.xz > small_512b_xz.out 2>/dev/null && cmp -s small_512b.dat small_512b_xz.out; then - test_case "LZMA: small file (512B)" "PASS" "" - else - test_case "LZMA: small file (512B)" "FAIL" "decompression mismatch" - fi - else - test_case "LZMA: small file (512B)" "FAIL" "compression failed" - fi - - # Test 2: Dict size 4KB - if xz -z -c --lzma2=dict=4k medium_4k.dat > dict_4k.xz 2>/dev/null; then - if xz -d -c dict_4k.xz > dict_4k.out 2>/dev/null && cmp -s medium_4k.dat dict_4k.out; then - test_case "LZMA: dict size 4KB" "PASS" "" - else - test_case "LZMA: dict size 4KB" "FAIL" "decompression mismatch" - fi - else - test_case "LZMA: dict size 4KB" "FAIL" "compression failed" - fi - - # Test 3: Dict size 16KB - if xz -z -c --lzma2=dict=16k medium_32k.dat > dict_16k.xz 2>/dev/null; then - if xz -d -c dict_16k.xz > dict_16k.out 2>/dev/null && cmp -s medium_32k.dat dict_16k.out; then - test_case "LZMA: dict size 16KB" "PASS" "" - else - test_case "LZMA: dict size 16KB" "FAIL" "decompression mismatch" - fi - else - test_case "LZMA: dict size 16KB" "FAIL" "compression failed" - fi - - # Test 4: Dict size 64KB - if xz -z -c --lzma2=dict=64k large_64k.dat > dict_64k.xz 2>/dev/null; then - if xz -d -c dict_64k.xz > dict_64k.out 2>/dev/null && cmp -s large_64k.dat dict_64k.out; then - test_case "LZMA: dict size 64KB" "PASS" "" - else - test_case "LZMA: dict size 64KB" "FAIL" "decompression mismatch" - fi - else - test_case "LZMA: dict size 64KB" "FAIL" "compression failed" - fi - - # Test 5: Large file - if xz -z -c large_1m.dat > large_1m.xz 2>/dev/null; then - if xz -d -c large_1m.xz > large_1m_xz.out 2>/dev/null && cmp -s large_1m.dat large_1m_xz.out; then - test_case "LZMA: 1MB file" "PASS" "" - else - test_case "LZMA: 1MB file" "FAIL" "decompression mismatch" - fi - else - test_case "LZMA: 1MB file" "FAIL" "compression failed" - fi - - # Test 6: Invalid header - dd if=/dev/urandom of=invalid.xz bs=100 count=1 2>/dev/null - if xz -d -c invalid.xz > /dev/null 2>&1; then - test_case "LZMA: invalid header rejection" "FAIL" "should reject invalid" - else - test_case "LZMA: invalid header rejection" "PASS" "" - fi - - # Test 7: Truncated stream - xz -z -c medium_4k.dat > truncated.xz 2>/dev/null - dd if=truncated.xz of=truncated_short.xz bs=50 count=1 2>/dev/null - if xz -d -c truncated_short.xz > /dev/null 2>&1; then - test_case "LZMA: truncated stream rejection" "FAIL" "should reject truncated" - else - test_case "LZMA: truncated stream rejection" "PASS" "" - fi - - # Test 8: MicroLZMA variant (EROFS-specific) - if xz -z --format=lzma -c small_512b.dat > microlzma.lzma 2>/dev/null; then - if xz -d --format=lzma -c microlzma.lzma > microlzma.out 2>/dev/null && cmp -s small_512b.dat microlzma.out; then - test_case "LZMA: MicroLZMA variant" "PASS" "" - else - test_case "LZMA: MicroLZMA variant" "FAIL" "decompression mismatch" - fi - else - test_case "LZMA: MicroLZMA variant" "FAIL" "compression failed" - fi - - echo "" -} - -# Run all tests -echo "Step 1: Generating test data..." -generate_test_data - -echo "" -echo "Step 2: Executing decompression tests..." -echo "" - -test_lz4 -test_deflate -test_zstd -test_lzma - -# Print final report -echo "===================================================================" -echo " FINAL REPORT" -echo "===================================================================" -echo "" -echo "Total Tests: $TOTAL" -echo "Passed: $PASSED ($(( PASSED * 100 / TOTAL ))%)" -echo "Failed: $FAILED ($(( FAILED * 100 / TOTAL ))%)" -echo "" -echo "===================================================================" -echo " COVERAGE SUMMARY" -echo "===================================================================" -echo "✓ LZ4: Normal paths, error paths, boundary conditions" -echo " - Small/medium/large files (512B - 1MB)" -echo " - High/low compressibility" -echo " - Corrupted and truncated data rejection" -echo "" -echo "✓ LZMA: P0-10 dict validation fix verified" -echo " - Dict sizes: 4KB, 16KB, 64KB" -echo " - MicroLZMA variant support" -echo " - Error path handling" -echo "" -echo "✓ DEFLATE: Multiple compression levels" -echo " - Levels 1, 6, 9" -echo " - Error detection and rejection" -echo " - Empty file handling" -echo "" -echo "✓ ZSTD: Comprehensive level testing" -echo " - Levels 1, 15, 22" -echo " - Large file support" -echo " - Corruption detection" -echo "===================================================================" -echo "" - -# Detailed results -echo "Detailed results saved to: $RESULTS_FILE" -echo "" -cat "$RESULTS_FILE" - -# Cleanup -echo "" -echo "Cleaning up test directory..." -rm -rf "$TESTDIR" - -exit $([ "$FAILED" -eq 0 ] && echo 0 || echo 1) diff --git a/test_chunk_based.sh b/test_chunk_based.sh deleted file mode 100755 index 7a07fca..0000000 --- a/test_chunk_based.sh +++ /dev/null @@ -1,190 +0,0 @@ -#!/bin/sh -# Test script for repo17 chunk-based implementation in FreeBSD VM - -set -e - -WORK_DIR="/work/repo-community/repo17" -TEST_DIR="${WORK_DIR}/test_data" -MOUNT_POINT="${WORK_DIR}/mnt" -IMAGE_FILE="${WORK_DIR}/test_chunk.erofs" - -echo "=== repo17 Chunk-Based Implementation Test ===" -echo "" - -# Step 1: Compile the module -echo "[1/6] Compiling repo17 kernel module..." -cd "${WORK_DIR}/src" -make clean > /dev/null 2>&1 || true -if ! make; then - echo "❌ Module compilation failed" - exit 1 -fi -echo "✅ Module compiled successfully" -echo "" - -# Step 2: Prepare test data -echo "[2/6] Preparing test data..." -rm -rf "${TEST_DIR}" -mkdir -p "${TEST_DIR}" - -# Create regular.txt (small file for basic read test) -echo "Hello, this is a regular file for chunk-based testing." > "${TEST_DIR}/regular.txt" -echo "Line 2 of the regular file." >> "${TEST_DIR}/regular.txt" -echo "Line 3 of the regular file." >> "${TEST_DIR}/regular.txt" - -# Create large.bin (16KB file spanning multiple chunks) -dd if=/dev/urandom of="${TEST_DIR}/large.bin" bs=1024 count=16 2>/dev/null - -# Create medium.dat (cross chunk boundary - 6KB) -dd if=/dev/urandom of="${TEST_DIR}/medium.dat" bs=1024 count=6 2>/dev/null - -# Create small.txt (smaller than chunk size) -echo "Small file content" > "${TEST_DIR}/small.txt" - -echo "✅ Test data created:" -ls -lh "${TEST_DIR}" -echo "" - -# Calculate MD5 checksums before creating image -echo "[3/6] Calculating MD5 checksums of original files..." -cd "${TEST_DIR}" -md5 regular.txt > "${WORK_DIR}/md5sums_original.txt" -md5 large.bin >> "${WORK_DIR}/md5sums_original.txt" -md5 medium.dat >> "${WORK_DIR}/md5sums_original.txt" -md5 small.txt >> "${WORK_DIR}/md5sums_original.txt" -cat "${WORK_DIR}/md5sums_original.txt" -echo "" - -# Step 3: Create chunk-based test image -echo "[4/6] Creating chunk-based EROFS image with 4KB chunk size..." -rm -f "${IMAGE_FILE}" -if ! mkfs.erofs --chunksize=4096 "${IMAGE_FILE}" "${TEST_DIR}"; then - echo "❌ Failed to create chunk-based image" - echo "Note: Ensure mkfs.erofs supports --chunksize option" - exit 1 -fi -echo "✅ Chunk-based image created: ${IMAGE_FILE}" -ls -lh "${IMAGE_FILE}" -echo "" - -# Step 4: Load module and mount -echo "[5/6] Loading module and mounting image..." -mkdir -p "${MOUNT_POINT}" - -# Unload if already loaded -kldunload erofs 2>/dev/null || true - -# Load the module -if ! kldload "${WORK_DIR}/src/erofs.ko"; then - echo "❌ Failed to load kernel module" - exit 1 -fi -echo "✅ Kernel module loaded" - -# Create memory disk -MD_DEV=$(mdconfig -a -t vnode -f "${IMAGE_FILE}") -if [ -z "${MD_DEV}" ]; then - echo "❌ Failed to create memory disk" - kldunload erofs - exit 1 -fi -echo "✅ Memory disk created: /dev/${MD_DEV}" - -# Mount the image -if ! mount -t erofs "/dev/${MD_DEV}" "${MOUNT_POINT}"; then - echo "❌ Mount failed" - mdconfig -d -u "${MD_DEV}" - kldunload erofs - exit 1 -fi -echo "✅ Image mounted at ${MOUNT_POINT}" -echo "" - -# Step 5: Verify file reading -echo "[6/6] Verifying chunk-based file reading..." -echo "" - -# List mounted files -echo "Files in mounted image:" -ls -lh "${MOUNT_POINT}" -echo "" - -# Test reading regular.txt -echo "--- Testing regular.txt (small file) ---" -if cat "${MOUNT_POINT}/regular.txt" > /dev/null 2>&1; then - echo "✅ Read successful" - cat "${MOUNT_POINT}/regular.txt" -else - echo "❌ Read failed" -fi -echo "" - -# Test reading large.bin (spans multiple chunks) -echo "--- Testing large.bin (multiple chunks) ---" -if dd if="${MOUNT_POINT}/large.bin" of=/dev/null bs=1024 2>&1 | grep -q "16+0"; then - echo "✅ Read successful (16KB across chunks)" -else - echo "❌ Read failed" -fi -echo "" - -# Test reading medium.dat (crosses chunk boundary) -echo "--- Testing medium.dat (chunk boundary) ---" -if dd if="${MOUNT_POINT}/medium.dat" of=/dev/null bs=1024 2>&1 | grep -q "6+0"; then - echo "✅ Read successful (6KB crossing boundary)" -else - echo "❌ Read failed" -fi -echo "" - -# Test reading small.txt -echo "--- Testing small.txt (< chunk size) ---" -if cat "${MOUNT_POINT}/small.txt" > /dev/null 2>&1; then - echo "✅ Read successful" - cat "${MOUNT_POINT}/small.txt" -else - echo "❌ Read failed" -fi -echo "" - -# MD5 verification -echo "=== MD5 Checksum Verification ===" -cd "${MOUNT_POINT}" -md5 regular.txt > "${WORK_DIR}/md5sums_mounted.txt" -md5 large.bin >> "${WORK_DIR}/md5sums_mounted.txt" -md5 medium.dat >> "${WORK_DIR}/md5sums_mounted.txt" -md5 small.txt >> "${WORK_DIR}/md5sums_mounted.txt" - -echo "Original checksums:" -cat "${WORK_DIR}/md5sums_original.txt" -echo "" -echo "Mounted checksums:" -cat "${WORK_DIR}/md5sums_mounted.txt" -echo "" - -if diff "${WORK_DIR}/md5sums_original.txt" "${WORK_DIR}/md5sums_mounted.txt" > /dev/null 2>&1; then - echo "✅ All MD5 checksums match!" -else - echo "❌ MD5 checksums do not match" - echo "Differences:" - diff "${WORK_DIR}/md5sums_original.txt" "${WORK_DIR}/md5sums_mounted.txt" || true -fi -echo "" - -# Cleanup -echo "=== Cleanup ===" -umount "${MOUNT_POINT}" -mdconfig -d -u "${MD_DEV}" -kldunload erofs -echo "✅ Cleanup complete" -echo "" - -echo "=== Test Summary ===" -echo "✅ Module compilation: PASSED" -echo "✅ Image creation: PASSED" -echo "✅ Module load: PASSED" -echo "✅ Mount: PASSED" -echo "✅ Chunk-based file reading: PASSED" -echo "✅ MD5 verification: PASSED" -echo "" -echo "All tests completed successfully!" diff --git a/tests/EXECUTION-CHECKLIST.md b/tests/EXECUTION-CHECKLIST.md deleted file mode 100644 index ee298c9..0000000 --- a/tests/EXECUTION-CHECKLIST.md +++ /dev/null @@ -1,78 +0,0 @@ -# Test Execution Checklist - -## Acceptance Rules - -- [x] Execute each TC from its Markdown procedure and record direct command - status, fixture/module hashes, kernel behavior, and cleanup state. -- [x] Treat fixture generation and static review as supporting evidence only. -- [x] Do not use retired wrappers, result collectors, or unconditional PASS - output as acceptance evidence. -- [x] Keep CI and automated kernel-test infrastructure outside this task. - -## Specification Audit - -- [x] TC000 is present as the non-executable template. -- [x] TC001-TC161 are present exactly once. -- [x] Bounded filename audit: 162 rows, 162 unique IDs, no duplicate or gap. -- [x] G1-G8 table audit: 156 rows, 156 unique IDs, no duplicate or omission. -- [x] TC157-TC161 add five unique final-review cases. - -## Canonical Execution Groups - -| Group | Scope | Final result | Evidence | -| --- | --- | --- | --- | -| G1 | TC001, TC007, TC009, TC011-TC014, TC019-TC040, TC147, TC150, TC151 | 32 PASS | `results/manual/2026-08-09T0710Z-g1/` | -| G2 | TC002, TC008, TC010, TC015-TC018, TC112-TC116, TC119 | 13 PASS | `results/manual/2026-08-09T0710Z-g2/` | -| G3 | TC041-TC066, TC141, TC148, TC149, TC152, TC153 | 31 PASS | `results/manual/2026-08-09T1059Z-g3/` plus TC060 fixed-source rerun | -| G4 | TC005, TC067-TC083, TC117, TC134-TC140, TC142 | 27 PASS | `results/manual/2026-08-09T0839Z-g4/` | -| G5 | TC003, TC004, TC084-TC092, TC102-TC110, TC143-TC146 | 23 PASS, 1 PARTIAL | `results/manual/2026-08-09T1236Z-g5/` | -| G6 | TC006, TC093-TC101, TC118 | 11 PASS | `results/manual/2026-08-09T1244Z-g6/` | -| G7 | TC120-TC130 | 11 PASS | `results/manual/2026-08-09T1359Z-g7/` | -| G8 | TC111, TC131-TC133, TC154-TC156 | 7 PASS | `results/manual/2026-08-09T1343Z-g8/` | -| Final | TC157-TC161 | 5 PASS | `results/manual/2026-08-09T1804Z-final-review-independent/` | - -## Final Review Cases - -- [x] TC157: global 16/32-byte explicit-extent order validation. -- [x] TC158: extended compressed inode 48-bit block-count accounting. -- [x] TC159: special-vnode combined setattr rejection. -- [x] TC160: dot-omitted `OFF_MAX` cookie rejection. -- [x] TC161: fatal `nm` failure and uncontaminated post-shim rebuild. - -## Build and Runtime Sign-Off - -- [x] Exact final source baseline: - `fcc85b93d5f8fd9671686bd68bf3b086c8bd25cf`. -- [x] `WITH_ZSTDIO=0` built with kernel `-Werror`; KLD SHA256 - `15fda9d334132cd81769ce4dff4f8411a6e2b4cf7531c352530d0de85f42a2d2`. -- [x] `WITH_ZSTDIO=1` built with kernel `-Werror`; KLD SHA256 - `23782dc0ce7da188807d35020bf2d8c6044b796c5c9a8746b398ba784cd6ad4e`. -- [x] Both final KLDs loaded and completed a read-only mount smoke. -- [x] Final guest EROFS mounts, md units, EROFS KLDs, and DTrace KLDs: zero. -- [x] repo22 generated build/object/image/bytecode artifacts removed. - -## Final Statistics - -```text -Executable test cases: 161 -Executed: 161 -PASS: 160 -PARTIAL: 1 (TC146) -FAIL: 0 -KERNEL-FAIL: 0 -ENVIRONMENT-UNAVAILABLE: 0 -SHELVED test case: 0 -``` - -TC146 is PARTIAL because a positive explicit mapped-payload fixture is not -available. Its shelved subitem is not a separate TC. TC010, TC060, and TC153 -are resolved historical issues. - -## Residual Work - -- [ ] Obtain an independently validated positive explicit mapped-payload - fixture and complete the remaining TC146 subfeature. -- [ ] Optionally extend TC157 with first-nonzero-`lstart` and extreme - extent-count performance coverage if the format contract and a qualified - fixture require it. -- [ ] CI remains intentionally unimplemented and outside this test effort. diff --git a/tests/G1-MANUAL-SETUP.md b/tests/G1-MANUAL-SETUP.md deleted file mode 100644 index a097898..0000000 --- a/tests/G1-MANUAL-SETUP.md +++ /dev/null @@ -1,69 +0,0 @@ -# G1 Manual Test Setup - -This setup supports only the G1 test set. It prepares fixtures and probes; it -does not execute tests, assign results, or append PASS output. - -## Host Preparation - -Run from the repo22 root with an absent output directory: - -```sh -git rev-parse HEAD -tests/prepare_g1_fixtures.sh /work/build/repo22-g1 -(cd /work/build/repo22-g1/images && sha256sum -c IMAGE-SHA256SUMS) -(cd /work/build/repo22-g1 && sha256sum -c SOURCE-SHA256SUMS) -(cd /work/build/repo22-g1 && sha256sum -c SOURCE-METADATA.sha256) -``` - -Record `fixture-evidence.txt`, all three checksum manifests, the erofs-utils -version, and the KLD SHA256. Production erofs-utils 1.8.6 does not recognize -the 48-bit incompat bit, so `root8-48bit.erofs`, -`fallback-48bit-root2.erofs`, and `compact-dot-omitted.erofs` are qualified by -the structured transformer's field/CRC assertions plus the required FreeBSD -mount, not by a false `fsck.erofs` PASS. - -Transfer `images/`, `source/`, `expected/`, the checksum/evidence files, the -exact KLD, and the C probes used by a TC to an empty guest directory. Compile -the probes natively on FreeBSD 15: - -```sh -cc -O2 -Wall -Wextra -Werror -o statfs_probe statfs_probe.c -cc -O2 -Wall -Wextra -Werror -o stat_special stat_special.c -cc -O2 -Wall -Wextra -Werror -o read_probe read_probe.c -cc -O2 -Wall -Wextra -Werror -o mmap_fault mmap_fault.c -cc -O2 -Wall -Wextra -Werror -o nfs_fh_tool nfs_fh_tool.c -``` - -## Per-Test Lifecycle - -Use a fresh dynamic md unit and a TC-specific mount point. Do not assume -`md0`, and detach the unit returned by `mdconfig`: - -```sh -image=/tmp/repo22-g1/images/IMAGE.erofs -mnt=/mnt/repo22-g1-TC -md=$(mdconfig -a -t vnode -f "$image") -mkdir -p "$mnt" -mount -t erofs -o ro "/dev/$md" "$mnt" -mount -p | awk -v p="$mnt" '$2 == p' -``` - -Cleanup after every TC, including a failing assertion: - -```sh -umount "$mnt" 2>/dev/null || true -mdconfig -d -u "${md#md}" 2>/dev/null || true -rmdir "$mnt" 2>/dev/null || true -``` - -At the start and end of the complete G1 run, record `uname -a`, -`freebsd-version -ku`, `kldstat`, `mount`, `mdconfig -l`, the KLD SHA256, and -dmesg line count. The final state must contain no EROFS mount, EROFS KLD, or G1 -md provider. - -## Result Rules - -The only accepted results are `PASS`, `KERNEL-FAIL`, `SHELVED/ISSUE`, and -`ENVIRONMENT-UNAVAILABLE`. Host inspection alone cannot produce PASS. A -kernel behavior failure must retain the observed errno/output and be recorded -in `issues/`; it must not be relabeled PASS. diff --git a/tests/G3-MANUAL-SETUP.md b/tests/G3-MANUAL-SETUP.md deleted file mode 100644 index 0fbe6a8..0000000 --- a/tests/G3-MANUAL-SETUP.md +++ /dev/null @@ -1,58 +0,0 @@ -# G3 Manual Test Setup - -This setup supports only TC041-TC066, TC141, TC148, TC149, TC152, and -TC153. It prepares fixtures and native probes; it does not run tests or assign -results. - -## Host Preparation - -From the repo22 root, use an absent output directory: - - git rev-parse HEAD - tests/prepare_g3_fixtures.sh /work/build/repo22-g3 - (cd /work/build/repo22-g3/vfs && sha256sum -c IMAGE-SHA256SUMS) - (cd /work/build/repo22-g3/vfs/source && - sha256sum -c ../SOURCE-SHA256SUMS) - (cd /work/build/repo22-g3/metadata && sha256sum -c SHA256SUMS) - (cd /work/build/repo22-g3/final && sha256sum -c SHA256SUMS) - -Record fixture-evidence.txt, all checksum manifests, the erofs-utils version, -the exact source commit, and the KLD SHA256. Transfer the fixtures, sources, -and only these native probes to the FreeBSD 15 guest: - - readdir_probe.c g3_vfs_probe.c stat_special.c nfs_fh_tool.c - mmap_fault.c sparse_hole_probe.c - -## Guest Preparation - -Build the KLD natively from an archive exported from the recorded commit. -Build probes directly; no runner or result wrapper is permitted: - - cc -O2 -Wall -Wextra -Werror -std=c17 readdir_probe.c -o readdir_probe - cc -O2 -Wall -Wextra -Werror -std=c17 g3_vfs_probe.c -o g3_vfs_probe - cc -O2 -Wall -Wextra -Werror -std=c17 stat_special.c -o stat_special - cc -O2 -Wall -Wextra -Werror -std=c17 nfs_fh_tool.c -o nfs_fh_tool - cc -O2 -Wall -Wextra -Werror -std=c17 mmap_fault.c -o mmap_fault - cc -O2 -Wall -Wextra -Werror -std=c17 sparse_hole_probe.c \ - -o sparse_hole_probe - -Before the first test, require no EROFS mount, no md provider, and no stale -EROFS KLD. Load only the exact KLD under test. For each test, attach a fresh -dynamic vnode md, mount read-only, execute that numbered Markdown procedure, -then unmount and detach before continuing. - -The report records the literal command, exit status or syscall errno, relevant -fixture/data/KLD hashes, dmesg delta, and cleanup state for every test. - -## Common Mount Pattern - - mkdir -p /tmp/repo22-g3/mnt - unit=$(mdconfig -a -t vnode -f IMAGE) - mount -t erofs -o ro "/dev/$unit" /tmp/repo22-g3/mnt - # Execute exactly one numbered test. - umount /tmp/repo22-g3/mnt - mdconfig -d -u "$unit" - -TC153 additionally creates and removes its documented multi-terabyte sparse -provider. Final cleanup requires zero EROFS mounts, zero md providers, the test -KLD unloaded, and all guest-generated sparse files removed. diff --git a/tests/G4-MANUAL-SETUP.md b/tests/G4-MANUAL-SETUP.md deleted file mode 100644 index 549567f..0000000 --- a/tests/G4-MANUAL-SETUP.md +++ /dev/null @@ -1,88 +0,0 @@ -# G4 xattr, ACL, and metabox manual setup - -This setup is shared only by TC005, TC067-TC083, TC117, TC134-TC140, and -TC142. It does not consume an existing image or report from `/work/build`. - -## Host fixture generation - -The host must provide erofs-utils 1.8.6 and Linux `user.*` xattrs. The helper -creates the source trees, invokes `mkfs.erofs`, performs structured on-disk -transformations, and then reopens every output to verify its fields and hashes. - -```sh -mkfs.erofs -V -run=/work/build/repo22-g4-$(date -u +%Y%m%dT%H%M%SZ) -python3 tests/g4_fixtures.py make --output "$run" -python3 tests/g4_fixtures.py verify --output "$run" -sha256sum -c "$run/IMAGE-SHA256SUMS" -``` - -`mkfs.erofs -V` must report 1.8.6. `SOURCE-SHA256`, -`IMAGE-SHA256SUMS`, and `fixture-manifest.json` are required evidence. The -manifest records the complete source inventory, each mkfs command, image and -provider sizes, and every transformed field as offset/size/before/after bytes. - -The generated images are: - -| Shape | Images | -|---|---| -| inline, shared, packed-prefix, ACL | `basic.erofs` | -| primary prefix fallback | `prefix-primary.erofs` | -| plain, compressed, fragment metabox | `metabox-plain.erofs`, `metabox-compressed.erofs`, `metabox-fragment.erofs` | -| malformed xattr | `bad-inline-entry.erofs`, `bad-shared-entry.erofs` | -| declared bounds | `bad-shared-declared-bounds.erofs`, `bad-prefix-declared-bounds.erofs` | -| superblock validation | `bad-metabox-truncated-extension.erofs`, `bad-ishare-prefix-id.erofs` | -| fragment safety | `bad-fragment-self-loop.erofs`, `bad-fragment-range.erofs`, `bad-metabox-recursive-nid.erofs`, `bad-packed-recursive-nid.erofs` | - -## Module and guest - -Build `src/` at the exact test commit with FreeBSD 15 kernel headers. Record -the commit, header revision/branch, compiler target, `WITH_ZSTDIO`, and KLD -SHA256. Copy only the new KLD and generated images to an isolated FreeBSD 15 -guest. Load the KLD and verify guest hashes before the first case. - -```sh -freebsd-version -uname -a -sha256 /tmp/repo22-g4/erofs.ko /tmp/repo22-g4/images/*.erofs -kldload /tmp/repo22-g4/erofs.ko -mkdir -p /mnt/repo22-g4 /tmp/repo22-g4/logs -``` - -Each Markdown case is run separately. Attach its stated image, mount read-only, -run only the stated observations, then unmount and detach before the next case. - -```sh -unit=$(mdconfig -a -t vnode -f /tmp/repo22-g4/images/IMAGE.erofs) -mount -t erofs -o ro /dev/${unit} /mnt/repo22-g4 -# case-specific commands -umount /mnt/repo22-g4 -mdconfig -d -u "${unit#md}" -``` - -## FreeBSD xattr and errno rules - -Linux `user.*` appears in FreeBSD namespace `user` without `user.` in the -attribute name. Linux `trusted.*`, `security.*`, and POSIX ACL xattrs appear in -FreeBSD namespace `system`; trusted/security retain their full names, while ACL -names are `posix_acl_access` and `posix_acl_default`. Use `lsextattr` and -`getextattr`, not Linux `getfattr` syntax. - -Use `getextattr -qq -x` for byte-exact output. The utility's exit status is not -authoritative for all failures, so capture the kernel result with `truss` and -require errno 87 (`ENOATTR`), 97 (`EINTEGRITY`), 5 (`EIO`), or 30 (`EROFS`) as -specified by the case. Negative mounts must show `nmount(...)=ERR#97` or an -equivalent normalized `EIO` at a boundary where the VFS maps integrity errors. - -## Required cleanup evidence - -After every case, record zero matching mounts and zero matching md units. At -the end, require no active EROFS allocation, unload the KLD, and stop the -dedicated VM. - -```sh -mount | grep repo22-g4 || true -mdconfig -l -vmstat -m | grep erofs -kldunload erofs -``` diff --git a/tests/G5-MANUAL-SETUP.md b/tests/G5-MANUAL-SETUP.md deleted file mode 100644 index 7aa6aa1..0000000 --- a/tests/G5-MANUAL-SETUP.md +++ /dev/null @@ -1,118 +0,0 @@ -# G5 Compression Manual Setup - -This is the canonical fixture and execution contract for exactly these 24 -tests: - -`TC003`, `TC004`, `TC084`-`TC092`, `TC102`-`TC110`, and `TC143`-`TC146`. - -The commands and paths here replace placeholder image names, blind corruption -offsets, and non-source-compared reads in the individual test descriptions. -Do not use old images or reports as fixture inputs. - -## Host fixture generation - -Requirements are `mkfs.erofs`, `dump.erofs`, and `fsck.erofs` 1.8.6. Generate -into a new path; the helper rejects an existing output directory. - -```sh -out=/work/build/repo22-g5-fixtures-a -python3 tests/g5_fixtures.py create \ - --output "$out" \ - --erofs-utils-source /path/to/erofs-utils-1.8.6 -python3 tests/g5_fixtures.py verify --output "$out" -``` - -Generate a second fresh directory and require both checksum inventories to be -identical: - -```sh -cmp "$out/SHA256SUMS" "$out2/SHA256SUMS" -cmp "$out/SOURCE-SHA256SUMS" "$out2/SOURCE-SHA256SUMS" -``` - -`fixture-manifest.json` records every mkfs option, source and image hash, -inode/NID/size/layout, compressed map-header offset, advise bits, algorithm -nibbles, extent summaries, partial-reference indexes and physical blocks, and -corruption pcluster/patch ranges. Creation reopens every transformed image and -the separate `verify` command repeats the structured checks. - -The partial-reference transformer changes the HEAD pblk, sets -`Z_EROFS_LI_PARTIAL_REF`, and copies the complete source pcluster's -`D0_CBLKCNT`. Corruption is applied only after `dump.erofs` and the byte parser -agree on the target algorithm and physical extent. - -## Build matrix - -Build on the FreeBSD 15 guest from the exact repo22 baseline source: - -```sh -FREEBSD_SRC=/root/repo22-g5/freebsd-src WITH_ZSTDIO=2 ./build.sh -# Must fail with: WITH_ZSTDIO must be 0 or 1 - -FREEBSD_SRC=/root/repo22-g5/freebsd-src WITH_ZSTDIO=0 ./build.sh -cp build/erofs.ko /root/repo22-g5/erofs-nozstd.ko -nm -u /root/repo22-g5/erofs-nozstd.ko - -FREEBSD_SRC=/root/repo22-g5/freebsd-src WITH_ZSTDIO=1 ./build.sh -cp build/erofs.ko /root/repo22-g5/erofs-zstdio.ko -nm -u /root/repo22-g5/erofs-zstdio.ko -``` - -The disabled module must have no `ZSTD_*` or `bcmp` reference. The enabled -module may reference only the formal FreeBSD ZSTD API names. Load by full path, -obtain the file ID from the matching `kldstat` path row, and unload that ID. - -## Manual read pattern - -For every image, attach a fresh md provider, mount read-only, compare the full -hash and full bytes where required, and compare every range against the same -offset in the source file. `tests/read_probe.c` provides deterministic `pread` -and errno checks; it is a probe, not a runner. - -```sh -unit=$(mdconfig -a -t vnode -f "$image") -mount -t erofs -o ro "/dev/$unit" "$mnt" -sha256 -q "$source" -sha256 -q "$mnt/$name" -cmp "$source" "$mnt/$name" -read_probe pread "$mnt/$name" "$offset" "$length" guest.bin -read_probe pread "$source" "$offset" "$length" source.bin -cmp source.bin guest.bin -umount "$mnt" -mdconfig -d -u "${unit#md}" -``` - -Use `timeout 20 read_probe expect-error FILE 5` for compressed-stream -corruption. Also compare `control.bin` from the same corrupted image, then -require no mount/md remains and EROFS active allocations return to zero. - -## Fixture mapping - -| Tests | Image and source contract | -|---|---| -| TC003, TC084, TC089 | `lz4-compact-4k.erofs` or `lz4-full-4k.erofs`; `sources/lz4/compressed.bin` | -| TC085 | `lz4-large.erofs`; 268435456-byte `sources/large/large.bin` | -| TC086, TC087, TC090 | `lz4-compact-64k.erofs`; fixed source offsets | -| TC088 | 4K, 64K, and 256K compact LZ4 images; same source bytes | -| TC091, TC092 | `lz4-ztail.erofs`; inline target plus four edge controls | -| TC004 | `lzma-level6.erofs`; full, middle, and EOF reads | -| TC102-TC104 | distinct DEFLATE level 1, 6, and 9 images | -| TC105-TC107 | distinct ZSTD level 1, 15, and 22 images | -| TC108 | `lzma-large.erofs`; 104857601-byte LZMA level 6 source | -| TC109 | `microlzma-edge.erofs`; actual 1B, 4K, and compressed 16K files | -| TC110, TC144 | LZMA partial/corrupt pair plus same-image `control.bin` | -| TC143 | DEFLATE and ZSTD partial/corrupt pairs plus controls | -| TC145 | both KLDs, LZ4 control, and `zstd-level1.erofs` gate/read | -| TC146 HEAD2 | `head2.erofs` and targeted corrupt copy; boundary from manifest | -| TC146 interlaced | `interlaced.erofs`; first compressed/plain transition from manifest | -| TC146 explicit extent | `extent-attempt.erofs` is negative evidence only; mapped payload remains SHELVED | - -TC146 must report HEAD2, interlaced, and explicit extent separately. A normal -full-index image produced with `--max-extent-bytes` is not explicit-extent -coverage. - -## Final cleanup - -Require zero matching mounts, zero md providers, zero EROFS active allocation, -and no loaded EROFS KLD. Compare pre/post dmesg, verify guest responsiveness, -power off the dedicated VM, and remove Python bytecode caches before commit. diff --git a/tests/G6-MANUAL-SETUP.md b/tests/G6-MANUAL-SETUP.md deleted file mode 100644 index 781a30f..0000000 --- a/tests/G6-MANUAL-SETUP.md +++ /dev/null @@ -1,164 +0,0 @@ -# G6 Chunk and Multi-Device Manual Setup - -This setup applies only to `TC006`, `TC093` through `TC101`, and `TC118`. -There are exactly 11 test cases. The commands below generate new inputs; no -fixture or result from an earlier run is an input. - -## Host prerequisites - -- Linux host with erofs-utils 1.8.6 (`mkfs.erofs` and `fsck.erofs`). -- Python 3.11 or newer. -- QEMU with qcow2 support. -- A clean FreeBSD 15 amd64 base disk used only as the backing file for a new - per-run overlay. - -Set a new run directory and generate the fixtures twice: - -```sh -REPO=/path/to/worktree/repo-community/repo22 -RUN=/work/build/repo22-g6-$(date -u +%Y%m%dT%H%M%SZ) -mkdir -p "$RUN" -cd "$REPO" -python3 -B tests/g6_multidev_fixtures.py generate \ - --output "$RUN/fixtures-a" -python3 -B tests/g6_multidev_fixtures.py generate \ - --output "$RUN/fixtures-b" -cmp "$RUN/fixtures-a/manifest.json" "$RUN/fixtures-b/manifest.json" -cmp "$RUN/fixtures-a/SHA256SUMS" "$RUN/fixtures-b/SHA256SUMS" -python3 -B tests/g6_multidev_fixtures.py verify "$RUN/fixtures-a" -``` - -`generate` refuses an existing output directory, fixes source bytes, UUIDs, -timestamps, worker count, and every binary transformation, and asserts the -old field before each patch. `verify` checks every artifact size and SHA256, -then reparses superblock, device-table, and chunk-index fields from disk. - -The manifest records two erofs-utils 1.8.6 limitations. Its fsck qualifies -the mkfs split image, single-index image, explicit 2/3-slot images, table-at-0, -`uniaddr=0`, fragment image, and original two-block LZ4 pcluster. Flatdev and -device-ID-0 unified relocation are qualified by the FreeBSD kernel reads in -TC094 and TC101 because this fsck release does not implement those mappings. - -## Dedicated FreeBSD 15 VM - -Create a new overlay and use only SSH port 9226 for this run: - -```sh -qemu-img create -f qcow2 -F qcow2 \ - -b /work/build/vm-freebsd-dev-base.qcow2 \ - "$RUN/freebsd15-overlay.qcow2" -qemu-system-x86_64 -accel tcg,thread=multi -cpu qemu64 \ - -m 6144 -smp 4 \ - -drive file="$RUN/freebsd15-overlay.qcow2",if=virtio,format=qcow2 \ - -netdev user,id=net0,hostfwd=tcp:127.0.0.1:9226-:22 \ - -device virtio-net-pci,netdev=net0 -display none \ - -serial file:"$RUN/freebsd15-serial.log" -monitor none \ - -pidfile "$RUN/freebsd15-qemu.pid" \ - -D "$RUN/freebsd15-qemu.log" -daemonize -``` - -Record the guest identity before installing test artifacts: - -```sh -uname -a -freebsd-version -ku -sysctl -n kern.osreldate -sha256 /boot/kernel/kernel -mdconfig -l -mount -p | awk '$3 == "erofs"' -``` - -The initial `mdconfig` and EROFS mount outputs must be empty. - -## Exact-source KLD - -On the host, record and archive the exact worktree source: - -```sh -git rev-parse HEAD | tee "$RUN/source.commit" -git status --short -git archive --format=tar HEAD repo-community/repo22 | \ - gzip -n > "$RUN/repo22-source.tar.gz" -git archive --format=tar HEAD dev-freebsd-releng/sys | \ - gzip -n > "$RUN/freebsd-sys-source.tar.gz" -tar -C "$RUN/fixtures-a" -czf "$RUN/g6-fixtures.tar.gz" \ - SHA256SUMS manifest.json images sources -``` - -Transfer both archives to the new guest. Authentication details remain -outside the repository: - -```sh -scp -O -P 9226 "$RUN/repo22-source.tar.gz" \ - "$RUN/freebsd-sys-source.tar.gz" \ - "$RUN/g6-fixtures.tar.gz" root@127.0.0.1:/root/ -``` - -Build natively in the guest, with no source edits: - -```sh -mkdir -p /root/freebsd-src /root/repo22-g6-src /root/repo22-g6 -tar -xzf /root/freebsd-sys-source.tar.gz -C /root/freebsd-src \ - --strip-components 1 -tar -xzf /root/repo22-source.tar.gz -C /root/repo22-g6-src \ - --strip-components 2 -tar -xzf /root/g6-fixtures.tar.gz -C /root/repo22-g6 -cd /root/repo22-g6-src -grep -E '^(REVISION|BRANCH)=' /root/freebsd-src/sys/conf/newvers.sh -env WITH_ZSTDIO=1 FREEBSD_SRC=/root/freebsd-src ./build.sh -sha256 build/erofs.ko -file build/erofs.ko -cp build/erofs.ko /root/repo22-g6/erofs.ko -``` - -At baseline `6b33b4afb490be7d6fec70e499469c306a58435d`, the tracked sys tree is -15.0-RELEASE-p9 and the clean guest is p8; both report OSREL 1500068. Record -this source/guest distinction rather than claiming they are the same patch -level. Also record `source.commit`, `WITH_ZSTDIO=1`, FreeBSD source archive -SHA256, KLD SHA256, kernel SHA256, and all guest values in the report. - -## Manual evidence rules - -Run the commands in each TC Markdown directly. Do not use a runner, CI job, -or test wrapper. Before each test: - -```sh -mkdir -p /mnt/g6 -kldload /root/repo22-g6/erofs.ko -dmesg | tail -40 > /tmp/g6-dmesg-before -``` - -For a negative mount or read, capture the syscall result with `truss` and -record the named errno, not only command exit status: - -```sh -truss -f -o /tmp/operation.truss command arguments -tail -20 /tmp/operation.truss -``` - -After every TC, unmount first, detach external providers in descending slot -order, detach the primary, and unload the module. All four checks must report -zero: - -```sh -mount -p | awk '$3 == "erofs" { print }' -mdconfig -l -kldstat -n erofs 2>/dev/null || true -sysctl -n kern.geom.conftxt | \ - awk '/Geom name: md9[0-3]$|Consumers:|Providers:|erofs/ { print }' -``` - -Also compare the new dmesg suffix and reject any panic, trap, assertion, -watchdog, or EROFS error not expected by the current negative operation. - -## FreeBSD and Linux behavior - -Linux EROFS accepts a device table at byte offset zero and excludes a slot -whose `uniaddr` is zero from device-ID-0 unified lookup; a nonzero device ID -still selects that slot. FreeBSD uses explicit `device.=/dev/` -mount options because GEOM providers are not discovered from Linux block -device tags. FreeBSD also holds one read-only GEOM consumer per provider, so -normal `mdconfig -d` returns `EBUSY` while mounted. A forced GEOM orphan makes -later cold I/O return `ENXIO`; unmount must still release vnode, cdev, and GEOM -references. These lifecycle details have no direct Linux loop-device -equivalent and are checked in TC006 and TC118. diff --git a/tests/G7-MANUAL-SETUP.md b/tests/G7-MANUAL-SETUP.md deleted file mode 100644 index 1a1ff2e..0000000 --- a/tests/G7-MANUAL-SETUP.md +++ /dev/null @@ -1,226 +0,0 @@ -# G7 Boundary and Stress Manual Setup - -This setup applies to exactly `TC120` through `TC130`: 11 tests, with no -additional test IDs. Run each test's Markdown commands directly. Do not use a -CI job, runner, or test wrapper. - -## Deterministic fixtures - -The host requires Python 3, erofs-utils 1.8.6, GNU tar, QEMU, and at least -2 GiB of free working space. Generate two fresh fixture directories; the -helper rejects an existing output path. - -```sh -REPO=/path/to/worktree/repo-community/repo22 -RUN=/work/build/repo22-g7-$(date -u +%Y%m%dT%H%M%SZ) -mkdir -p "$RUN" -cd "$REPO" -python3 -B tests/g7_fixtures.py create --output "$RUN/fixtures-a" -python3 -B tests/g7_fixtures.py create --output "$RUN/fixtures-b" -python3 -B tests/g7_fixtures.py verify --output "$RUN/fixtures-a" -cmp "$RUN/fixtures-a/SOURCE-INVENTORY.tsv" \ - "$RUN/fixtures-b/SOURCE-INVENTORY.tsv" -cmp "$RUN/fixtures-a/SOURCE-SHA256SUMS" \ - "$RUN/fixtures-b/SOURCE-SHA256SUMS" -cmp "$RUN/fixtures-a/SHA256SUMS" "$RUN/fixtures-b/SHA256SUMS" -sha256sum "$RUN/fixtures-a/SOURCE-INVENTORY.tsv" \ - "$RUN/fixtures-a/SOURCE-SHA256SUMS" \ - "$RUN/fixtures-a/SHA256SUMS" \ - "$RUN/fixtures-a/fixture-manifest.json" -``` - -`SOURCE-INVENTORY.tsv` records the exact relative path, size, and SHA256 of -every source file. The helper re-hashes the complete inventory, checks exact -counts and names, checks all sparse markers and allocated-block usage, runs -`fsck.erofs`, reopens the 4 GiB boundary inode with `dump.erofs`, and verifies -the image checksums. `boundaries.erofs` covers TC120-TC125; -`workloads.erofs` covers TC126-TC130. - -The practical sparse boundary is 4 GiB + 4097 bytes. It crosses signed 32-bit, -2 GiB, unsigned 32-bit, 4 GiB, block, hole, and EOF boundaries without -claiming that a 16 TiB image is practical in this VM. The source remains -sparse and every selected range is compared to that exact source in TC121. - -## Dedicated FreeBSD 15 VM - -Create a new qcow2 overlay and use only SSH port 9227: - -```sh -qemu-img create -f qcow2 -F qcow2 \ - -b /work/build/vm-freebsd-dev-base.qcow2 \ - "$RUN/freebsd15-overlay.qcow2" -qemu-system-x86_64 -accel tcg,thread=multi -cpu qemu64 \ - -m 6144 -smp 4 \ - -drive file="$RUN/freebsd15-overlay.qcow2",if=virtio,format=qcow2 \ - -netdev user,id=net0,hostfwd=tcp:127.0.0.1:9227-:22 \ - -device virtio-net-pci,netdev=net0 -display none \ - -serial file:"$RUN/freebsd15-serial.log" -monitor none \ - -pidfile "$RUN/freebsd15-qemu.pid" \ - -D "$RUN/freebsd15-qemu.log" -daemonize -``` - -Record the initial guest identity and resource-control state: - -```sh -uname -a -freebsd-version -ku -sysctl -n kern.osreldate -sha256 /boot/kernel/kernel -sysctl kern.racct.enable -rctl -vmstat -H 1 3 -mdconfig -l -mount -p | awk '$3 == "erofs"' -``` - -If RACCT/RCTL is disabled, enable the FreeBSD loader tunable in this overlay -and reboot it. Do not use a jail as a memory-limit command. - -```sh -grep -q '^kern.racct.enable=' /boot/loader.conf || \ - printf 'kern.racct.enable="1"\n' >> /boot/loader.conf -grep '^kern.racct.enable=' /boot/loader.conf -shutdown -r now -``` - -After reconnecting, require `sysctl -n kern.racct.enable` to print `1` and -record `rctl` plus `vmstat -H 1 3` again. - -## Exact-source KLD and guest inputs - -Commit the helper and corrected Markdown before building. Then archive the -exact commit and the tracked FreeBSD 15 sys tree. The final report may be a -later documentation-only commit, but its `repo22/src` tree hash must equal the -build input tree hash. - -```sh -cd /path/to/worktree -BUILD_COMMIT=$(git rev-parse HEAD) -git status --short -printf '%s\n' "$BUILD_COMMIT" > "$RUN/source.commit" -git rev-parse "$BUILD_COMMIT:repo-community/repo22/src" \ - > "$RUN/repo22-src.tree" -git archive --format=tar "$BUILD_COMMIT" repo-community/repo22 | \ - gzip -n > "$RUN/repo22-source.tar.gz" -git archive --format=tar "$BUILD_COMMIT" dev-freebsd-releng/sys | \ - gzip -n > "$RUN/freebsd-sys-source.tar.gz" -tar --sparse --format=gnu -C "$RUN/fixtures-a" \ - -cf "$RUN/boundaries-source.tar" sources/boundaries -tar --sparse --format=gnu -C "$RUN/fixtures-a" \ - -cf "$RUN/workloads-source.tar" sources/workloads -gzip -n "$RUN/boundaries-source.tar" -gzip -n "$RUN/workloads-source.tar" -sha256sum "$RUN/repo22-source.tar.gz" \ - "$RUN/freebsd-sys-source.tar.gz" \ - "$RUN/boundaries-source.tar.gz" \ - "$RUN/workloads-source.tar.gz" > "$RUN/source-archives.sha256" -``` - -Transfer the archives, images, and fixture metadata to the dedicated guest. -Authentication configuration stays outside the repository. - -```sh -ssh -p 9227 root@127.0.0.1 'mkdir -p /root/repo22-g7-transfer' -scp -O -P 9227 "$RUN/repo22-source.tar.gz" \ - "$RUN/freebsd-sys-source.tar.gz" \ - "$RUN/boundaries-source.tar.gz" "$RUN/workloads-source.tar.gz" \ - "$RUN/fixtures-a/images/boundaries.erofs" \ - "$RUN/fixtures-a/images/workloads.erofs" \ - "$RUN/fixtures-a/SOURCE-INVENTORY.tsv" \ - "$RUN/fixtures-a/SOURCE-SHA256SUMS" \ - "$RUN/fixtures-a/SHA256SUMS" \ - "$RUN/fixtures-a/fixture-manifest.json" \ - "$RUN/fixtures-a/DEEP-PATH" "$RUN/fixtures-a/LONG-NAME" \ - "$RUN/fixtures-a/SPARSE-RANGES.tsv" \ - "$RUN/source.commit" "$RUN/repo22-src.tree" \ - "$RUN/source-archives.sha256" \ - root@127.0.0.1:/root/repo22-g7-transfer/ -``` - -Build and verify natively in the guest: - -```sh -mkdir -p /root/freebsd-src /root/repo22-g7-src \ - /root/repo22-g7/fixtures/images /root/repo22-g7/fixtures/sources \ - /root/repo22-g7/evidence -tar -xzf /root/repo22-g7-transfer/freebsd-sys-source.tar.gz \ - -C /root/freebsd-src --strip-components 1 -tar -xzf /root/repo22-g7-transfer/repo22-source.tar.gz \ - -C /root/repo22-g7-src --strip-components 2 -tar -xzf /root/repo22-g7-transfer/boundaries-source.tar.gz \ - -C /root/repo22-g7/fixtures -tar -xzf /root/repo22-g7-transfer/workloads-source.tar.gz \ - -C /root/repo22-g7/fixtures -cp /root/repo22-g7-transfer/*.erofs /root/repo22-g7/fixtures/images/ -cp /root/repo22-g7-transfer/SOURCE-INVENTORY.tsv \ - /root/repo22-g7-transfer/SOURCE-SHA256SUMS \ - /root/repo22-g7-transfer/SHA256SUMS \ - /root/repo22-g7-transfer/fixture-manifest.json \ - /root/repo22-g7-transfer/DEEP-PATH \ - /root/repo22-g7-transfer/LONG-NAME \ - /root/repo22-g7-transfer/SPARSE-RANGES.tsv \ - /root/repo22-g7/fixtures/ -cd /root/repo22-g7/fixtures -sha256sum -c SHA256SUMS -sha256sum -c SOURCE-SHA256SUMS -stat -f 'size=%z blocks=%b blocksize=%k name=%N' \ - sources/boundaries/maximum/sparse-boundary.bin -cd /root/repo22-g7-src -grep -E '^(REVISION|BRANCH)=' /root/freebsd-src/sys/conf/newvers.sh -env WITH_ZSTDIO=1 FREEBSD_SRC=/root/freebsd-src ./build.sh -cp build/erofs.ko /root/repo22-g7/erofs.ko -cc -std=c11 -O2 -Wall -Wextra -Werror \ - -o /root/repo22-g7/g7_probe tests/g7_probe.c -sha256 /root/repo22-g7/erofs.ko /root/repo22-g7/g7_probe -file /root/repo22-g7/erofs.ko -``` - -Record the build commit, source tree hash, `WITH_ZSTDIO=1`, archive hashes, -FreeBSD source `REVISION`/`BRANCH`, KLD SHA256, probe SHA256, guest identity, -kernel SHA256, and TCG/QEMU configuration in the report. - -## Manual lifecycle - -Load the exact module by full path and record its file ID and pathname: - -```sh -kldload /root/repo22-g7/erofs.ko -kldstat -v | tee /root/repo22-g7/evidence/kldstat-loaded.txt -KLD_ID=$(kldstat -v | awk '$NF == "(/root/repo22-g7/erofs.ko)" { print $1 }') -test -n "$KLD_ID" -printf '%s\n' "$KLD_ID" > /root/repo22-g7/evidence/kld.id -dmesg > /root/repo22-g7/evidence/dmesg-before.txt -``` - -Each TC attaches one fresh vnode-backed md provider, mounts read-only, records -all correctness and metrics evidence, unmounts, detaches that exact provider, -and verifies both are gone. Never use `killall`; concurrent tests persist -every child PID, wait every child, and record every exit code. - -TC128 and TC129 metrics are recording-only under QEMU TCG. Their PASS gate is -read completion and exact source hash/byte comparison, not a fixed MB/s, -IOPS, or latency threshold. TC127 uses FreeBSD RCTL `vmemoryuse:deny` with -cooperating allocation probes, records allocation failure and reclamation, -and verifies reads while pressure is held. - -## Final cleanup - -After TC130, require zero child processes, EROFS mounts, md providers, and -RCTL rules created by G7. Unload the exact KLD file ID, compare dmesg, and -power off the dedicated guest. - -```sh -mount -p | awk '$3 == "erofs" { print }' -mdconfig -l -rctl -kldstat -v | grep -A2 -B2 '/root/repo22-g7/erofs.ko' -KLD_ID=$(cat /root/repo22-g7/evidence/kld.id) -kldunload -i "$KLD_ID" -test -z "$(kldstat -v | grep '/root/repo22-g7/erofs.ko')" -dmesg > /root/repo22-g7/evidence/dmesg-after.txt -shutdown -p now -``` - -Remove the host overlay, QEMU logs/PID file, source tars, temporary sparse -probe data, and Python bytecode caches only after evidence has been copied -into the committed report. Confirm port 9227 is released. diff --git a/tests/RESULT-SUMMARY-TEMPLATE.md b/tests/RESULT-SUMMARY-TEMPLATE.md deleted file mode 100644 index cee0745..0000000 --- a/tests/RESULT-SUMMARY-TEMPLATE.md +++ /dev/null @@ -1,229 +0,0 @@ -# Test Result Summary Template - -## Test Execution Information -- **Date**: YYYY-MM-DD -- **Tester**: Name -- **FreeBSD Version**: 13.x / 14.x -- **Kernel**: uname -a output -- **erofs Module Version**: kldstat output -- **Test Duration**: X hours Y minutes - -## Overall Statistics -``` -Total Test Cases: 153 executable cases (plus TC000 template) -Executed: ___ -Passed: ___ -Failed: ___ -Skipped: ___ -Blocked: ___ - -Pass Rate: ___% -``` - -## Phase Results - -### Phase 1: Basic Functionality (15 tests) -``` -Passed: ___/15 -Failed: ___ -Critical Issues: ___ -``` -Failed Tests: -- TC###: Description - Reason - -### Phase 2: Inode & Data Layouts (21 tests) -``` -Passed: ___/21 -Failed: ___ -Critical Issues: ___ -``` -Failed Tests: -- TC###: Description - Reason - -### Phase 3: Directory & VFS (26 tests) -``` -Passed: ___/26 -Failed: ___ -Critical Issues: ___ -``` -Failed Tests: -- TC###: Description - Reason - -### Phase 4: Extended Attributes (17 tests) -``` -Passed: ___/17 -Failed: ___ -Critical Issues: ___ -``` -Failed Tests: -- TC###: Description - Reason - -### Phase 5: Compression (24 tests) -``` -Passed: ___/24 -Failed: ___ -Critical Issues: ___ -``` - -**LZMA Tests** (repo18 NEW): -- TC004: [PASS/FAIL] -- TC108: [PASS/FAIL] -- TC109: [PASS/FAIL] -- TC110: [PASS/FAIL] - -Failed Tests: -- TC###: Description - Reason - -### Phase 6: Multi-Device (9 tests) -``` -Passed: ___/9 -Failed: ___ -Critical Issues: ___ -``` -Failed Tests: -- TC###: Description - Reason - -### Phase 7: Error Handling (8 tests) -``` -Passed: ___/8 -Failed: ___ -Critical Issues: ___ -``` -Failed Tests: -- TC###: Description - Reason - -### Phase 8: Boundary & Stress (11 tests) -``` -Passed: ___/11 -Failed: ___ -Critical Issues: ___ -``` - -**Performance Results**: -- TC128 (Sequential): ___ MB/s -- TC129 (Random): ___ IOPS -- TC130 (Mixed): ___ req/s - -Failed Tests: -- TC###: Description - Reason - -### Phase 9: Documentation (1 test) -``` -Passed: ___/1 -Failed: ___ -``` - -## Critical Failures -Priority: Critical failures that prevent basic functionality - -| Test ID | Description | Root Cause | Impact | Status | -|---------|-------------|------------|--------|--------| -| TC### | Brief | Reason | High/Medium/Low | Open/Fixed | - -## Known Issues -Non-critical issues or limitations - -| Test ID | Description | Workaround | Severity | Tracked | -|---------|-------------|------------|----------|---------| -| TC### | Brief | Workaround if any | Medium/Low | Issue #X | - -## Performance Summary -| Metric | Result | Baseline | Delta | Status | -|--------|--------|----------|-------|--------| -| Sequential Read | ___ MB/s | 500 MB/s | ___% | PASS/FAIL | -| Random Read | ___ IOPS | 5000 IOPS | ___% | PASS/FAIL | -| Large File (10GB) | ___ s | 20 s | ___% | PASS/FAIL | -| Many Files (10K) | ___ s | 30 s | ___% | PASS/FAIL | - -## Feature Coverage -``` -Total Features: 65 -Tested: ___ -Passed: ___ -Failed: ___ - -Coverage: ___% -``` - -### Feature Status -- [x] Mount operations: PASS -- [x] Inode formats: PASS/FAIL -- [x] Data layouts: PASS/FAIL -- [x] LZ4 compression: PASS/FAIL -- [x] DEFLATE compression: PASS/FAIL -- [x] zstd compression: PASS/FAIL -- [x] **LZMA compression (NEW)**: PASS/FAIL -- [x] Extended attributes: PASS/FAIL -- [x] Multi-device: PASS/FAIL -- [x] Directory operations: PASS/FAIL -- [x] VFS integration: PASS/FAIL - -## Regression Check -Comparison with repo17 (64 features, 91% complete) - -| Feature | repo17 | repo18 | Status | -|---------|--------|--------|--------| -| Basic mount | PASS | PASS | No regression | -| LZ4 | PASS | PASS | No regression | -| DEFLATE | PASS | PASS | No regression | -| zstd | PASS | PASS | No regression | -| **LZMA** | NOT IMPL | PASS | **NEW** | -| ... | ... | ... | ... | - -## repo18 Iteration 1 Validation -**Goal**: LZMA compression support - -### LZMA-Specific Results -- TC004 (Basic LZMA): [PASS/FAIL] -- TC108 (Large file): [PASS/FAIL] -- TC109 (MicroLZMA): [PASS/FAIL] -- TC110 (Corrupted): [PASS/FAIL] - -**Data Integrity**: [PASS/FAIL] -- SHA256 checksums match: [YES/NO] -- Random access works: [YES/NO] -- Large file (100MB+) decompressed correctly: [YES/NO] - -**Implementation Validation**: -- Self-contained decoder (232 lines): [YES/NO] -- No external dependencies: [YES/NO] -- Unified interface: [YES/NO] - -**Iteration 1 Status**: [COMPLETE/INCOMPLETE] - -## System Stability -- Kernel panics: [YES/NO] - Count: ___ -- Memory leaks: [DETECTED/NONE] -- File descriptor leaks: [DETECTED/NONE] -- dmesg errors: [COUNT] - -## Test Environment -``` -CPU: ___ -Memory: ___ GB -Disk: ___ (type) -Load during tests: ___ -``` - -## Recommendations -1. [Action item 1] -2. [Action item 2] -3. ... - -## Sign-Off -- [ ] All critical tests passed -- [ ] No regressions from repo17 -- [ ] LZMA implementation validated (repo18 iter 1) -- [ ] Performance acceptable -- [ ] Known issues documented - -**Tested by**: _______________ -**Reviewed by**: _______________ -**Date**: _______________ -**Approved**: [YES/NO] - -## Attachments -- [ ] Full test logs -- [ ] dmesg output -- [ ] Performance graphs -- [ ] Failure screenshots/dumps diff --git a/tests/TC000-template.md b/tests/TC000-template.md deleted file mode 100644 index ebacc1e..0000000 --- a/tests/TC000-template.md +++ /dev/null @@ -1,36 +0,0 @@ -# Test Case Template - -**Test ID**: TC###-brief-name -**Category**: [Mount Operations | Filesystem Metadata | Inode Operations | Data Layouts | Compression | Directory Operations | Symlink Operations | Extended Attributes | Multi-Device | VFS Integration | Read-Only Enforcement | VM Integration | Error Handling | Performance & Stress] -**Priority**: [Critical | High | Medium | Low] -**Regression**: [None | Issue #XXX] - -## Objective -Brief description of what this test validates. - -## Preconditions -- EROFS image requirements (e.g., "Image with LZ4-compressed files") -- System requirements (e.g., "FreeBSD 13.0+") -- Test data setup - -## Test Steps -1. Step one with specific commands -2. Step two with expected intermediate state -3. ... - -## Expected Results -- Specific expected outcomes -- Expected output values -- Expected file contents - -## Verification Method -How to verify the test passed: -- Command outputs to check -- Files to inspect -- Performance metrics (if applicable) - -## Cleanup -Steps to reset the environment after the test. - -## Notes -Any additional considerations or known limitations. diff --git a/tests/TC001-mount-basic.md b/tests/TC001-mount-basic.md deleted file mode 100644 index 3400ca0..0000000 --- a/tests/TC001-mount-basic.md +++ /dev/null @@ -1,39 +0,0 @@ -# Test Case: Basic Mount and Unmount - -**Test ID**: TC001-mount-basic - -## Objective - -Verify that the exact G1 KLD mounts a valid image read-only, exposes source -data exactly, and unmounts cleanly. - -## Fixture - -Use `images/compact.erofs` and `source/compact/root.txt` generated by -`tests/prepare_g1_fixtures.sh`. Follow `tests/G1-MANUAL-SETUP.md` and record the -image, source, and KLD SHA256 values. - -## Procedure - -```sh -image=/tmp/repo22-g1/images/compact.erofs -source=/tmp/repo22-g1/source/compact/root.txt -mnt=/mnt/repo22-g1-001 -md=$(mdconfig -a -t vnode -f "$image") -mkdir -p "$mnt" -kldstat | grep -i erofs -mount -t erofs -o ro "/dev/$md" "$mnt" -mount -p | awk -v p="$mnt" '$2 == p' -cmp "$source" "$mnt/root.txt" -sha256 "$source" "$mnt/root.txt" -./statfs_probe "$mnt" -umount "$mnt" -mdconfig -d -u "${md#md}" -rmdir "$mnt" -``` - -## Expected Results - -- Mount, `cmp`, `statfs_probe`, unmount, and detach all return zero. -- The mount entry is `erofs` and read-only; both file hashes are identical. -- Cleanup leaves neither this mount point nor its md provider. diff --git a/tests/TC002-superblock-crc32c.md b/tests/TC002-superblock-crc32c.md deleted file mode 100644 index 8352760..0000000 --- a/tests/TC002-superblock-crc32c.md +++ /dev/null @@ -1,66 +0,0 @@ -# Test Case: Superblock CRC32C Validation - -**Test ID**: TC002-superblock-crc32c -**Category**: Filesystem Metadata -**Priority**: Critical - -## Objective - -Verify the real EROFS superblock checksum field and both equivalent checksum -ranges, then prove that a valid image mounts while an equal-length image with -one covered byte changed and no checksum update is rejected. - -## Fixture Qualification - -Run from the repo22 root with erofs-utils 1.8.6: - -```sh -work=$(mktemp -d /tmp/repo22-tc002.XXXXXX) -tests/prepare_error_fixtures.sh "$work/fixtures" -python3 tests/erofs_fixture.py inspect "$work/fixtures/valid-plain.erofs" -python3 tests/erofs_fixture.py inspect "$work/fixtures/bad-super-crc.erofs" -grep '^bad-super-crc ' "$work/fixtures/fixture-evidence.txt" -(cd "$work/fixtures" && sha256 -c SHA256SUMS) -``` - -For a 4096-byte block, the canonical calculation clears the little-endian -checksum at absolute byte 1028 and calculates CRC32C over bytes `[1024,4096)` -with initial value `0xffffffff`. The production verifier uses seed -`0x5045b54a` over `[1032,4096)`. The helper must print identical canonical and -kernel values, `equivalent=True`, and `valid=True` for the control. The bad -image changes byte 1088, preserves provider length, does not recompute CRC, and -must print `valid=False`. - -## FreeBSD Procedure - -```sh -mkdir "$work/mnt" -unit=$(mdconfig -a -t vnode -f "$work/fixtures/valid-plain.erofs") -mount -t erofs -o ro "/dev/$unit" "$work/mnt" -cmp "$work/fixtures/source/control.txt" "$work/mnt/control.txt" -umount "$work/mnt" -mdconfig -d -u "${unit#md}" - -unit=$(mdconfig -a -t vnode -f "$work/fixtures/bad-super-crc.erofs") -set +e -mount -t erofs -o ro "/dev/$unit" "$work/mnt" \ - >"$work/bad-mount.out" 2>"$work/bad-mount.err" -mount_status=$? -set -e -printf 'mount_status=%s\n' "$mount_status" -test "$mount_status" -ne 0 -set +e -truss -o "$work/bad-mount.truss" mount -t erofs -o ro "/dev/$unit" \ - "$work/mnt" >/dev/null 2>&1 -set -e -grep -E 'nmount.*ERR#97' "$work/bad-mount.truss" -! mount -p | awk -v path="$work/mnt" '$2 == path { found=1 } - END { exit found ? 0 : 1 }' -mdconfig -d -u "${unit#md}" -``` - -## Expected Results - -The control mounts and reads exactly. The bad image fails with FreeBSD errno -97 (`EINTEGRITY`) and no mount remains. Record both image hashes, provider -lengths, checksum values/ranges, command status, new dmesg lines, and cleanup. diff --git a/tests/TC003-lz4-compressed-read.md b/tests/TC003-lz4-compressed-read.md deleted file mode 100644 index 95b5ae2..0000000 --- a/tests/TC003-lz4-compressed-read.md +++ /dev/null @@ -1,87 +0,0 @@ -# Test Case: LZ4 Compressed File Read - -**Test ID**: TC003-lz4-compressed-read - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression -**Priority**: Critical -**Regression**: None - -## Objective -Verify correct decompression and reading of LZ4-compressed files. - -## Preconditions -- EROFS image with LZ4-compressed files: `test-lz4.erofs` -- Known reference file: `original.txt` (uncompressed source) -- Compressed file in image: `/compressed/test.txt` -- Mount point: `/mnt/test` - -## Test Steps -1. Mount the LZ4 test image: - ``` - mount -t erofs /dev/md0 /mnt/test - ``` - -2. Verify file exists: - ``` - ls -lh /mnt/test/compressed/test.txt - ``` - -3. Read the entire file: - ``` - cat /mnt/test/compressed/test.txt > /tmp/decompressed.txt - ``` - -4. Calculate checksum: - ``` - sha256 /tmp/decompressed.txt - sha256 original.txt - ``` - -5. Compare byte-for-byte: - ``` - cmp /tmp/decompressed.txt original.txt - ``` - -6. Test partial read: - ``` - dd if=/mnt/test/compressed/test.txt of=/tmp/partial.txt bs=1024 count=1 - ``` - -7. Verify partial read matches: - ``` - cmp -n 1024 /tmp/partial.txt original.txt - ``` - -## Expected Results -- Step 1: Mount succeeds -- Step 2: File size matches original uncompressed size -- Step 4: SHA256 checksums are identical -- Step 5: `cmp` returns 0 (files identical) -- Step 7: First 1024 bytes match - -## Verification Method -- Exact byte-for-byte match with original file -- No corruption in decompressed data -- File attributes (size, timestamps) preserved correctly -- Check inode compression format: - ``` - # Using custom debug tool if available - erofs_inspect /dev/md0 /compressed/test.txt - # Should show: z_algorithmformat[0:2] = Z_EROFS_COMPRESSION_LZ4 - ``` - -## Cleanup -``` -umount /mnt/test -mdconfig -d -u md0 -rm /tmp/decompressed.txt /tmp/partial.txt -``` - -## Notes -- LZ4 is the most common compression algorithm in EROFS -- Test should cover files of various sizes: small (<4KB), medium (4KB-1MB), large (>1MB) -- LZ4 pcluster size is typically 4KB or 64KB -- Related: TC004 (ztailpacking), TC005 (pcluster mapping) diff --git a/tests/TC004-lzma-compressed-read.md b/tests/TC004-lzma-compressed-read.md deleted file mode 100644 index d076211..0000000 --- a/tests/TC004-lzma-compressed-read.md +++ /dev/null @@ -1,96 +0,0 @@ -# Test Case: LZMA Compressed File Read - -**Test ID**: TC004-lzma-compressed-read - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression -**Priority**: High -**Regression**: None - -## Objective -Verify correct decompression and reading of LZMA/MicroLZMA-compressed files (newly implemented in repo18 iteration 1). - -## Preconditions -- EROFS image with LZMA-compressed files: `test-lzma.erofs` -- Known reference file: `original-large.txt` (uncompressed source) -- Compressed file in image: `/compressed/large.txt` -- Mount point: `/mnt/test` - -## Test Steps -1. Mount the LZMA test image: - ```sh - unit=$(mdconfig -a -t vnode -f test-lzma.erofs) - mount -t erofs -o ro /dev/${unit} /mnt/test - ``` - -2. Verify file exists and check size: - ``` - ls -lh /mnt/test/compressed/large.txt - stat -f "Size: %z bytes" /mnt/test/compressed/large.txt - ``` - -3. Read entire compressed file: - ``` - cat /mnt/test/compressed/large.txt > /tmp/lzma-decompressed.txt - ``` - -4. Verify decompression correctness: - ``` - sha256 /tmp/lzma-decompressed.txt - sha256 original-large.txt - ``` - -5. Compare byte-for-byte: - ``` - cmp /tmp/lzma-decompressed.txt original-large.txt - echo $? - ``` - -6. Test random access read: - ``` - dd if=/mnt/test/compressed/large.txt of=/tmp/lzma-middle.txt bs=1k skip=100 count=10 - dd if=original-large.txt of=/tmp/orig-middle.txt bs=1k skip=100 count=10 - cmp /tmp/lzma-middle.txt /tmp/orig-middle.txt - ``` - -7. Test end-of-file read: - ``` - tail -c 1000 /mnt/test/compressed/large.txt > /tmp/lzma-tail.txt - tail -c 1000 original-large.txt > /tmp/orig-tail.txt - cmp /tmp/lzma-tail.txt /tmp/orig-tail.txt - ``` - -## Expected Results -- Step 1: Mount succeeds -- Step 2: File size matches original uncompressed size exactly -- Step 4: SHA256 checksums are identical -- Step 5: Exit code 0 (files identical) -- Step 6: Middle section matches (random access works) -- Step 7: Tail matches (EOF handling correct) - -## Verification Method -- Complete data integrity check via SHA256 -- Partial read correctness (random access) -- No memory corruption or crashes during decompression -- Verify LZMA decoder internal state: - - 1846 probability models initialized - - Range decoder normalization correct - - Dictionary buffer within bounds - -## Cleanup -``` -umount /mnt/test -mdconfig -d -u "${unit}" -rm /tmp/lzma-*.txt /tmp/orig-*.txt -``` - -## Notes -- **NEW in repo18**: This is the first iteration with LZMA support -- LZMA provides maximum compression ratio (~30-50% smaller than LZ4) -- MicroLZMA is a variant without header, commonly used in EROFS -- Self-contained implementation in `src/decompressor_lzma.c` -- Reference implementation: XZ Embedded minimal decoder -- Critical test for repo18 iteration 1 validation -- Regression marker: First LZMA implementation, high priority for validation diff --git a/tests/TC005-inline-xattr-user.md b/tests/TC005-inline-xattr-user.md deleted file mode 100644 index adac5c4..0000000 --- a/tests/TC005-inline-xattr-user.md +++ /dev/null @@ -1,32 +0,0 @@ -# Test Case: Inline user xattrs - -**Test ID**: TC005-inline-xattr-user -**Fixture**: `basic.erofs`, `/inline-user` - -## Objective - -Verify inline `user` namespace list/get and binary-value handling on FreeBSD. -Generate and qualify the image as described in `G4-MANUAL-SETUP.md`; the -manifest must classify `comment` and `special.chars` as inline entries. - -## Manual steps - -```sh -unit=$(mdconfig -a -t vnode -f /tmp/repo22-g4/images/basic.erofs) -mount -t erofs -o ro /dev/${unit} /mnt/repo22-g4 -stat -f 'mode=%Sp size=%z inode=%i' /mnt/repo22-g4/inline-user -lsextattr user /mnt/repo22-g4/inline-user -getextattr -qq -x user comment /mnt/repo22-g4/inline-user -getextattr -qq -x user special.chars /mnt/repo22-g4/inline-user -truss -o /tmp/tc005.truss getextattr -qq user missing /mnt/repo22-g4/inline-user -grep extattr_get_file /tmp/tc005.truss -umount /mnt/repo22-g4 -mdconfig -d -u "${unit#md}" -``` - -## Expected results - -The names appear once. `comment` is hex -`696e6c696e652d757365722d76616c7565007461696c`; `special.chars` is -`7370656369616c2d76616c7565`. The missing name returns `ENOATTR` (87), and -cleanup leaves no mount or md provider. diff --git a/tests/TC006-multidev-chunk-read.md b/tests/TC006-multidev-chunk-read.md deleted file mode 100644 index e619016..0000000 --- a/tests/TC006-multidev-chunk-read.md +++ /dev/null @@ -1,61 +0,0 @@ -# Test Case: Real Multi-Device Chunk Read - -**Test ID**: TC006-multidev-chunk-read -**Category**: Multi-Device / Chunk-Based -**Priority**: Critical - -## Objective - -Prove that 8-byte chunk indexes read file data from a live external GEOM -provider, including a range crossing a chunk boundary. - -## Fixture - -Generate and verify the fresh G6 set as described in `G6-MANUAL-SETUP.md`. -Use `mkfs-blob-primary.erofs` and `mkfs-blob-slot1.blob`. The manifest must -show primary blocks `1`, slot-1 blocks `224`, and every `/tc006.bin` index as -device ID `1`. The 4096-byte primary cannot contain the 98304-byte source. - -## Manual procedure - -```sh -I=/root/repo22-g6/images -S=/root/repo22-g6/sources/chunk -mkdir -p /mnt/g6 -kldload /root/repo22-g6/erofs.ko -mdconfig -a -t vnode -f "$I/mkfs-blob-primary.erofs" -u 90 -mdconfig -a -t vnode -f "$I/mkfs-blob-slot1.blob" -u 91 -mount -t erofs -o ro -o device.1=/dev/md91 /dev/md90 /mnt/g6 -sha256 "$S/tc006.bin" /mnt/g6/tc006.bin -cmp "$S/tc006.bin" /mnt/g6/tc006.bin -dd if="$S/tc006.bin" of=/tmp/tc006.expected bs=1 skip=28672 count=8192 2>/dev/null -dd if=/mnt/g6/tc006.bin of=/tmp/tc006.actual bs=1 skip=28672 count=8192 2>/dev/null -cmp /tmp/tc006.expected /tmp/tc006.actual -truss -f -o /tmp/tc006-ebusy.truss mdconfig -d -u 91 -tail -20 /tmp/tc006-ebusy.truss -sysctl -n kern.geom.conftxt | grep -A8 -B2 'Geom name: md91' -``` - -The detach must fail with `EBUSY`, proving a live external consumer. - -## Expected results - -- Mount, full-file `cmp`, SHA256, and the 8192-byte cross-boundary range pass. -- The external detach returns exactly `EBUSY` while mounted. -- No panic, trap, hang, or unexpected EROFS dmesg message occurs. - -## Cleanup and zero-state check - -```sh -umount /mnt/g6 -mdconfig -d -u 91 -mdconfig -d -u 90 -kldunload erofs -rm -f /tmp/tc006.expected /tmp/tc006.actual -mount -p | awk '$3 == "erofs" { print }' -mdconfig -l -kldstat -n erofs 2>/dev/null || true -sysctl -n kern.geom.conftxt | grep -E 'md9[01]|erofs' || true -``` - -All four final outputs must be empty. diff --git a/tests/TC007-concurrent-mount.md b/tests/TC007-concurrent-mount.md deleted file mode 100644 index 4697b3a..0000000 --- a/tests/TC007-concurrent-mount.md +++ /dev/null @@ -1,48 +0,0 @@ -# Test Case: Concurrent Mount Operations - -**Test ID**: TC007-concurrent-mount - -## Objective - -Verify two simultaneous mounts of the same deterministic EROFS image through -independent md providers, including source-exact reads from both mounts. - -## Fixture - -Use `images/compact.erofs` and `source/compact/testfile.txt` from the G1 -fixture set. Follow `tests/G1-MANUAL-SETUP.md`. - -## Procedure - -```sh -image=/tmp/repo22-g1/images/compact.erofs -source=/tmp/repo22-g1/source/compact/testfile.txt -mnt1=/mnt/repo22-g1-007a -mnt2=/mnt/repo22-g1-007b -md1=$(mdconfig -a -t vnode -f "$image") -md2=$(mdconfig -a -t vnode -f "$image") -mkdir -p "$mnt1" "$mnt2" -set +e -mount -t erofs -o ro "/dev/$md1" "$mnt1" & p1=$! -mount -t erofs -o ro "/dev/$md2" "$mnt2" & p2=$! -wait "$p1"; rc1=$? -wait "$p2"; rc2=$? -set -e -printf 'mount_rc=%d,%d providers=%s,%s\n' "$rc1" "$rc2" "$md1" "$md2" -test "$rc1" -eq 0 -a "$rc2" -eq 0 -cmp "$source" "$mnt1/testfile.txt" -cmp "$source" "$mnt2/testfile.txt" -cmp "$mnt1/testfile.txt" "$mnt2/testfile.txt" -sha256 "$source" "$mnt1/testfile.txt" "$mnt2/testfile.txt" -umount "$mnt1" -umount "$mnt2" -mdconfig -d -u "${md1#md}" -mdconfig -d -u "${md2#md}" -rmdir "$mnt1" "$mnt2" -``` - -## Expected Results - -- Both independently waited mount processes return zero. -- All three comparisons and SHA256 values match. -- No panic, trap, EROFS diagnostic, mount, or md provider remains. diff --git a/tests/TC008-mount-errors.md b/tests/TC008-mount-errors.md deleted file mode 100644 index 9febfae..0000000 --- a/tests/TC008-mount-errors.md +++ /dev/null @@ -1,77 +0,0 @@ -# Test Case: Mount Error Conditions - -**Test ID**: TC008-mount-errors -**Category**: Mount Operations -**Priority**: High - -## Objective - -Verify graceful rejection of three distinct provider conditions: a -checksum-valid bad magic at the correct field, a 1023-byte provider ending -before the superblock, and a zero-length provider. - -## Fixture Qualification - -```sh -work=$(mktemp -d /tmp/repo22-tc008.XXXXXX) -tests/prepare_error_fixtures.sh "$work/fixtures" -grep -E '^(bad-magic|truncated-before-super|empty-provider) ' \ - "$work/fixtures/fixture-evidence.txt" -python3 tests/erofs_fixture.py inspect "$work/fixtures/bad-magic.erofs" -wc -c "$work/fixtures/bad-magic.erofs" \ - "$work/fixtures/truncated-before-super.erofs" \ - "$work/fixtures/empty-provider.erofs" -(cd "$work/fixtures" && sha256 -c SHA256SUMS) -``` - -The bad-magic variant changes only absolute bytes 1024 through 1027 and -preserves media size. It intentionally retains the original checksum: the -production verifier's fixed-magic suffix calculation remains valid, while the -canonical checksum over the now-invalid magic does not. Magic is rejected -before checksum verification. The short and empty fixtures are intentional -provider-size cases, not same-size corruption variants. - -## FreeBSD Procedure - -Create a fresh mount directory. Execute each case separately and record whether -failure occurs at `mdconfig` or `nmount`; never assume a fixed md number. - -```sh -mkdir "$work/mnt" -for image in bad-magic.erofs truncated-before-super.erofs empty-provider.erofs -do - set +e - unit=$(mdconfig -a -t vnode -f "$work/fixtures/$image" \ - 2>"$work/$image.md.err") - md_status=$? - set -e - printf '%s md_status=%s unit=%s\n' "$image" "$md_status" "$unit" - if test "$md_status" -eq 0; then - set +e - mount -t erofs -o ro "/dev/$unit" "$work/mnt" \ - >"$work/$image.out" 2>"$work/$image.mount.err" - mount_status=$? - set -e - printf '%s mount_status=%s\n' "$image" "$mount_status" - test "$mount_status" -ne 0 - set +e - truss -o "$work/$image.mount.truss" mount -t erofs -o ro \ - "/dev/$unit" "$work/mnt" >/dev/null 2>&1 - set -e - grep -E 'nmount.*ERR#' "$work/$image.mount.truss" - mdconfig -d -u "${unit#md}" - else - cat "$work/$image.md.err" - fi - ! mount -p | awk -v path="$work/mnt" '$2 == path { found=1 } - END { exit found ? 0 : 1 }' -done -``` - -## Expected Results - -All three cases fail without panic or hang. Bad magic reaches `nmount` and -returns `EINVAL`. A 1023-byte vnode provider reaches `nmount` and returns -`ENXIO`; a zero-byte provider is rejected by `mdconfig` with `EINVAL`. Record -the observed stage and errno rather than fabricating a mount result. No md unit -or mount may remain after each case. diff --git a/tests/TC009-statfs-basic.md b/tests/TC009-statfs-basic.md deleted file mode 100644 index a4219e9..0000000 --- a/tests/TC009-statfs-basic.md +++ /dev/null @@ -1,42 +0,0 @@ -# Test Case: Basic statfs Information - -**Test ID**: TC009-statfs-basic - -## Objective - -Verify FreeBSD `statfs(2)` fields against the qualified image superblock. - -## Fixture - -Use `images/compact.erofs`. Before transfer, record `dump.erofs -s` and the -`compact` superblock row in `fixture-evidence.txt`. Follow -`tests/G1-MANUAL-SETUP.md` and compile `tests/statfs_probe.c` in the guest. - -## Procedure - -Host qualification: - -```sh -dump.erofs -s /work/build/repo22-g1/images/compact.erofs -python3 tests/erofs_fixture.py inspect /work/build/repo22-g1/images/compact.erofs -``` - -Guest dynamic check after mounting the image on `$mnt`: - -```sh -./statfs_probe "$mnt" -df -kT "$mnt" -df -iT "$mnt" -mount -p | awk -v p="$mnt" '$2 == p' -``` - -Unmount and detach the exact dynamic md unit. - -## Expected Results - -- `fstype=erofs`, `bsize=4096`, `iosize=4096`, and `readonly=1`. -- `blocks` and `files` equal the image's declared block and inode counts. -- `bfree`, `bavail`, and `ffree` are zero; `df` conversions agree. -- `mount -p` contains `ro`; cleanup succeeds. - -`stat -f` is not a `statfs(2)` probe and must not be substituted. diff --git a/tests/TC010-statfs-48bit.md b/tests/TC010-statfs-48bit.md deleted file mode 100644 index d2e5875..0000000 --- a/tests/TC010-statfs-48bit.md +++ /dev/null @@ -1,60 +0,0 @@ -# Test Case: statfs with 48-bit Block Count - -**Test ID**: TC010-statfs-48bit -**Category**: Filesystem Metadata -**Priority**: Medium - -## Objective - -Verify a nonzero 48-bit block-count high word survives mount validation and is -exported without truncation through FreeBSD `statfs(2)` and `df(1)`. - -## Fixture Qualification - -```sh -work=$(mktemp -d /tmp/repo22-tc010.XXXXXX) -tests/prepare_error_fixtures.sh "$work/fixtures" -python3 tests/erofs_fixture.py inspect \ - "$work/fixtures/48bit-statfs-prefix.erofs" -grep '^48bit-statfs-prefix ' "$work/fixtures/fixture-evidence.txt" -sha256 -q "$work/fixtures/48bit-statfs-prefix.erofs" -``` - -Read `total_blocks` and `required_provider_length` from the evidence. The -fixture sets incompat bit `0x80`, `rb.blocks_hi=1`, and a nonzero -`rootnid_8b`, then recomputes CRC32C. Its small prefix hash identifies the -metadata; the qualified provider is that exact prefix followed by sparse zeros -to the required media size. - -## FreeBSD Procedure - -```sh -provider="$work/48bit-statfs-provider.raw" -cp "$work/fixtures/48bit-statfs-prefix.erofs" "$provider" -truncate -s "$required_provider_length" "$provider" -stat -f 'size=%z blocks=%b' "$provider" -unit=$(mdconfig -a -t vnode -f "$provider") -diskinfo -v "/dev/$unit" -mkdir "$work/mnt" -mount -t erofs -o ro "/dev/$unit" "$work/mnt" -df -kT "$work/mnt" -df -iT "$work/mnt" -actual_1k=$(df -k "$work/mnt" | awk 'NR == 2 { print $2 }') -used_1k=$(df -k "$work/mnt" | awk 'NR == 2 { print $3 }') -avail_1k=$(df -k "$work/mnt" | awk 'NR == 2 { print $4 }') -expected_1k=$((total_blocks * 4)) -test "$actual_1k" -eq "$expected_1k" -test "$used_1k" -eq "$expected_1k" -test "$avail_1k" -eq 0 -umount "$work/mnt" -mdconfig -d -u "${unit#md}" -rm -f "$provider" -``` - -## Expected Results - -`diskinfo` reports at least the declared 16 TiB-plus media size. Mount succeeds, -the `df -k` total equals `total_blocks * 4`, Used equals total because -`f_bfree=0`, and Avail is zero. -If the filesystem cannot create or attach the sparse provider, record SHELVED -with all failed commands; a short-provider `ENXIO` is not PASS. diff --git a/tests/TC011-root-vnode.md b/tests/TC011-root-vnode.md deleted file mode 100644 index 7517eec..0000000 --- a/tests/TC011-root-vnode.md +++ /dev/null @@ -1,36 +0,0 @@ -# Test Case: Root Vnode Lookup - -**Test ID**: TC011-root-vnode - -## Objective - -Verify the mounted root vnode has the encoded root NID, directory attributes, -and the same top-level names as the fixture source. - -## Fixture - -Use `images/compact.erofs` and `source/compact`. The structured evidence records -the root NID and compact inode size. Follow `tests/G1-MANUAL-SETUP.md`. - -## Procedure - -After mounting on `$mnt`: - -```sh -stat -f 'ino=%i type=%HT mode=%Lp nlink=%l' "$mnt" -stat -f '%i' "$mnt" > /tmp/tc011-root-nid -ls -la "$mnt" -(cd /tmp/repo22-g1/source/compact && ls -1A | sort) > /tmp/tc011-source-names -(cd "$mnt" && ls -1A | sort) > /tmp/tc011-mount-names -diff -u /tmp/tc011-source-names /tmp/tc011-mount-names -cat "$mnt/root.txt" -``` - -Compare `/tmp/tc011-root-nid` with `root_nid` from `fixture-evidence.txt`, then -unmount, detach, and remove the two temporary name lists. - -## Expected Results - -- Root inode equals the encoded NID and reports a traversable directory. -- The complete top-level name lists match and `root.txt` is readable. -- All operations and cleanup return zero. diff --git a/tests/TC012-vget-normal.md b/tests/TC012-vget-normal.md deleted file mode 100644 index 1c5962a..0000000 --- a/tests/TC012-vget-normal.md +++ /dev/null @@ -1,39 +0,0 @@ -# Test Case: VFS Vget Normal Operation - -**Test ID**: TC012-vget-normal - -## Objective - -Exercise `VFS_VGET` through a real FreeBSD file handle and verify the returned -vnode identifies and reads the same EROFS inode as pathname lookup. - -## Fixture - -Use `images/compact.erofs`, `source/compact/testfile.txt`, and -`tests/nfs_fh_tool.c`. Compile the helper as described in -`tests/G1-MANUAL-SETUP.md`; run as root. - -## Procedure - -After mounting on `$mnt`: - -```sh -stat -f 'path_ino=%i gen=%v size=%z' "$mnt/testfile.txt" -./nfs_fh_tool capture "$mnt/testfile.txt" /tmp/tc012-a.fh -./nfs_fh_tool capture "$mnt/testfile.txt" /tmp/tc012-b.fh -./nfs_fh_tool compare /tmp/tc012-a.fh /tmp/tc012-b.fh -./nfs_fh_tool describe /tmp/tc012-a.fh -./nfs_fh_tool stat /tmp/tc012-a.fh -./nfs_fh_tool cat /tmp/tc012-a.fh /tmp/tc012-fh.out -cmp /tmp/repo22-g1/source/compact/testfile.txt /tmp/tc012-fh.out -sha256 /tmp/repo22-g1/source/compact/testfile.txt /tmp/tc012-fh.out -``` - -Remove the handles/output, then unmount and detach. - -## Expected Results - -- Both captures produce the same handle. -- Handle NID, `fhstat` inode, and pathname inode agree. -- `fhopen` output matches the deterministic source exactly. -- No stale/duplicate vnode error or cleanup failure occurs. diff --git a/tests/TC013-vget-invalid.md b/tests/TC013-vget-invalid.md deleted file mode 100644 index 313ad38..0000000 --- a/tests/TC013-vget-invalid.md +++ /dev/null @@ -1,39 +0,0 @@ -# Test Case: Vget with Invalid NID - -**Test ID**: TC013-vget-invalid - -## Objective - -Verify a checksum-valid directory entry that points outside the image reaches -the cold lookup/vget path and fails consistently with `EINTEGRITY`. - -## Fixture - -Use `images/invalid-dirent-nid.erofs`. `fixture-evidence.txt` records the exact -dirent offset, old NID, out-of-range NID, and valid recomputed CRC. Compile -`tests/read_probe.c` in the guest. - -## Procedure - -Mount the image without first listing the root, then run: - -```sh -./read_probe expect-error "$mnt/invalid-target.txt" 97 -./read_probe expect-error "$mnt/invalid-target.txt" 97 -set +e -truss -o /tmp/tc013.truss stat "$mnt/invalid-target.txt" -rc=$? -set -e -printf 'stat_rc=%d\n' "$rc" -tail -20 /tmp/tc013.truss -``` - -Record dmesg before/after, remove the trace, then unmount and detach. - -## Expected Results - -- Mount succeeds because only the child dirent is malformed. -- Both cold/repeated opens fail at pathname acquisition with FreeBSD errno 97 - (`EINTEGRITY`); `truss` shows the same syscall errno. -- The second failure is not converted to `ENOENT`; no panic or stale vnode - appears, and cleanup succeeds. diff --git a/tests/TC014-superblock-parse.md b/tests/TC014-superblock-parse.md deleted file mode 100644 index 34b0437..0000000 --- a/tests/TC014-superblock-parse.md +++ /dev/null @@ -1,42 +0,0 @@ -# Test Case: Superblock Parsing - -**Test ID**: TC014-superblock-parse - -## Objective - -Verify mandatory superblock fields at their real on-disk offsets and compare -the mounted values exposed by FreeBSD. - -## Fixture - -Use `images/compact.erofs`, its image hash, `fixture-evidence.txt`, and -`tests/statfs_probe.c`. - -## Procedure - -Host inspection: - -```sh -od -An -tx4 -j 1024 -N 4 /work/build/repo22-g1/images/compact.erofs -od -An -tu1 -j 1036 -N 1 /work/build/repo22-g1/images/compact.erofs -od -An -tu2 -j 1038 -N 2 /work/build/repo22-g1/images/compact.erofs -od -An -tu8 -j 1040 -N 8 /work/build/repo22-g1/images/compact.erofs -od -An -tu4 -j 1060 -N 8 /work/build/repo22-g1/images/compact.erofs -dump.erofs -s /work/build/repo22-g1/images/compact.erofs -python3 tests/erofs_fixture.py inspect /work/build/repo22-g1/images/compact.erofs -``` - -Mount the same hashed image in FreeBSD, then run: - -```sh -./statfs_probe "$mnt" -stat -f 'root_ino=%i type=%HT' "$mnt" -mount -p | awk -v p="$mnt" '$2 == p' -``` - -## Expected Results - -- Magic is `0xe0f5e1e2`, `blkszbits=12`, and checksum validation is true. -- Root NID, block count, and inode count agree with root `stat` and - `statfs_probe`; filesystem type is `erofs` and read-only. -- No parser error appears in the dmesg delta; cleanup succeeds. diff --git a/tests/TC015-superblock-corrupted.md b/tests/TC015-superblock-corrupted.md deleted file mode 100644 index 82f11c5..0000000 --- a/tests/TC015-superblock-corrupted.md +++ /dev/null @@ -1,63 +0,0 @@ -# Test Case: Corrupted Superblock Fields - -**Test ID**: TC015-superblock-corrupted -**Category**: Error Handling -**Priority**: High - -## Objective - -Verify ordered rejection of an unsupported block size, an image-bounds-invalid -48-bit root NID, and an unknown incompat feature. Every variant preserves -provider length and carries a checksum valid for its encoded fields. - -## Fixture Qualification - -```sh -work=$(mktemp -d /tmp/repo22-tc015.XXXXXX) -tests/prepare_error_fixtures.sh "$work/fixtures" -grep -E '^(bad-block-size|bad-root-nid|unsupported-feature) ' \ - "$work/fixtures/fixture-evidence.txt" -python3 tests/erofs_fixture.py inspect "$work/fixtures/bad-block-size.erofs" -python3 tests/erofs_fixture.py inspect "$work/fixtures/bad-root-nid.erofs" -python3 tests/erofs_fixture.py inspect "$work/fixtures/unsupported-feature.erofs" -(cd "$work/fixtures" && sha256 -c SHA256SUMS) -``` - -`bad-block-size` changes `blkszbits` at 1036 from 12 to 13 and recomputes the -checksum over `[1024,8192)`. `bad-root-nid` selects the 48-bit union form and -sets `rootnid_8b` beyond the declared inode area. `unsupported-feature` sets -unknown incompat bit `0x80000000`. - -## FreeBSD Procedure - -For each image, attach a dynamic vnode md unit, run mount directly and under -`truss`, assert nonzero status and no mount entry, then detach that exact unit. - -```sh -mkdir "$work/mnt" -for image in bad-block-size.erofs bad-root-nid.erofs unsupported-feature.erofs -do - unit=$(mdconfig -a -t vnode -f "$work/fixtures/$image") - set +e - mount -t erofs -o ro "/dev/$unit" "$work/mnt" \ - >"$work/$image.out" 2>"$work/$image.err" - status=$? - set -e - printf '%s status=%s\n' "$image" "$status" - test "$status" -ne 0 - set +e - truss -o "$work/$image.truss" mount -t erofs -o ro "/dev/$unit" \ - "$work/mnt" >/dev/null 2>&1 - set -e - grep -E 'nmount.*ERR#' "$work/$image.truss" - ! mount -p | awk -v path="$work/mnt" '$2 == path { found=1 } - END { exit found ? 0 : 1 }' - mdconfig -d -u "${unit#md}" -done -``` - -## Expected Results - -Bad block size returns `EINVAL`, bad root NID returns `EINTEGRITY`, and the -unknown incompat bit returns `EOPNOTSUPP`. Record syscall errno, image hash, -provider length, new dmesg lines, and cleanup for all three subcases. diff --git a/tests/TC016-superblock-crc-invalid.md b/tests/TC016-superblock-crc-invalid.md deleted file mode 100644 index a8120e8..0000000 --- a/tests/TC016-superblock-crc-invalid.md +++ /dev/null @@ -1,52 +0,0 @@ -# Test Case: Superblock CRC Field Mismatch - -**Test ID**: TC016-superblock-crc-invalid -**Category**: Error Handling -**Priority**: High - -## Objective - -Verify that changing only the stored superblock checksum field causes -`EINTEGRITY`, with all protected bytes and provider length unchanged. - -## Fixture Qualification - -```sh -work=$(mktemp -d /tmp/repo22-tc016.XXXXXX) -tests/prepare_error_fixtures.sh "$work/fixtures" -grep '^bad-checksum-field ' "$work/fixtures/fixture-evidence.txt" -python3 tests/erofs_fixture.py inspect \ - "$work/fixtures/bad-checksum-field.erofs" -wc -c "$work/fixtures/valid-plain.erofs" \ - "$work/fixtures/bad-checksum-field.erofs" -sha256 -q "$work/fixtures/bad-checksum-field.erofs" -``` - -The mutator flips bit 0 of the little-endian checksum at absolute byte 1028, -does not recompute it, asserts checksum invalidity, and changes no other byte. - -## FreeBSD Procedure - -```sh -mkdir "$work/mnt" -unit=$(mdconfig -a -t vnode -f "$work/fixtures/bad-checksum-field.erofs") -set +e -mount -t erofs -o ro "/dev/$unit" "$work/mnt" \ - >"$work/mount.out" 2>"$work/mount.err" -status=$? -set -e -test "$status" -ne 0 -set +e -truss -o "$work/mount.truss" mount -t erofs -o ro "/dev/$unit" \ - "$work/mnt" >/dev/null 2>&1 -set -e -grep -E 'nmount.*ERR#97' "$work/mount.truss" -! mount -p | awk -v path="$work/mnt" '$2 == path { found=1 } - END { exit found ? 0 : 1 }' -mdconfig -d -u "${unit#md}" -``` - -## Expected Results - -Mount returns errno 97 (`EINTEGRITY`) with a checksum diagnostic. No mount or -md consumer remains. diff --git a/tests/TC017-48bit-blocks-parse.md b/tests/TC017-48bit-blocks-parse.md deleted file mode 100644 index a1ec6f4..0000000 --- a/tests/TC017-48bit-blocks-parse.md +++ /dev/null @@ -1,44 +0,0 @@ -# Test Case: 48-bit Block Count Parsing - -**Test ID**: TC017-48bit-blocks-parse -**Category**: Filesystem Metadata -**Priority**: Medium - -## Objective - -Verify the Linux EROFS union rule dynamically: with incompat bit `0x80` and a -nonzero `rootnid_8b`, combine `blocks_lo` with `rb.blocks_hi << 32` and preserve -the complete count through mount. - -## Fixture Qualification - -```sh -work=$(mktemp -d /tmp/repo22-tc017.XXXXXX) -tests/prepare_error_fixtures.sh "$work/fixtures" -python3 tests/erofs_fixture.py inspect \ - "$work/fixtures/48bit-statfs-prefix.erofs" -od -An -tx2 -j 1038 -N 2 "$work/fixtures/48bit-statfs-prefix.erofs" -od -An -tx4 -j 1060 -N 4 "$work/fixtures/48bit-statfs-prefix.erofs" -od -An -tx8 -j 1136 -N 8 "$work/fixtures/48bit-statfs-prefix.erofs" -grep '^48bit-statfs-prefix ' "$work/fixtures/fixture-evidence.txt" -``` - -Require `blocks_hi=1`, `rootnid_8b != 0`, a valid recomputed CRC, and a recorded -prefix SHA256. The expected count is `blocks_lo | (1 << 32)`. - -## FreeBSD Procedure - -Create and attach the sparse provider exactly as in TC010. Record `diskinfo`, -mount it read-only, compare `df -k` total with `total_blocks * 4`, read -`control.txt`, then unmount and detach the dynamic md unit. - -Also attach the unextended small prefix once and require mount failure with -`ENXIO`; record this only as the negative media-size guard, not the positive -parse result. - -## Expected Results - -The sparse provider mount succeeds and reports the full nonzero-high-word count. -The small prefix fails `ENXIO`. If no qualifying provider can be attached, -record the metadata parsing evidence separately and mark the dynamic portion -SHELVED, never PASS. diff --git a/tests/TC018-48bit-large-fs.md b/tests/TC018-48bit-large-fs.md deleted file mode 100644 index 5128c75..0000000 --- a/tests/TC018-48bit-large-fs.md +++ /dev/null @@ -1,66 +0,0 @@ -# Test Case: Large Filesystem with 48-bit Addressing - -**Test ID**: TC018-48bit-large-fs -**Category**: Filesystem Metadata -**Priority**: Low - -## Objective - -Verify a real FLAT_PLAIN read whose extended inode has `startblk_hi=1`, so the -provider I/O occurs above 16 TiB rather than only reporting a large `statfs` -total. - -## Fixture Qualification - -```sh -work=$(mktemp -d /tmp/repo22-tc018.XXXXXX) -tests/prepare_error_fixtures.sh "$work/fixtures" -python3 tests/erofs_fixture.py inspect \ - "$work/fixtures/48bit-high-file-prefix.erofs" --path /high-offset.txt -grep '^48bit-high-file-prefix ' "$work/fixtures/fixture-evidence.txt" -sha256 -q "$work/fixtures/48bit-high-file-prefix.erofs" -``` - -Require an extended nonempty FLAT_PLAIN inode, `startblk_hi=1`, a physical -`data_offset >= 17592186044416`, a valid CRC, and an end offset within -`required_provider_length`. The mutator zeros the original low-block payload, -so a reader that ignores `startblk_hi` cannot return the expected source bytes. -Read `required_provider_length`, `start_block`, `data_offset`, -`low_decoy_offset`, and `file_size` from the evidence line. - -## FreeBSD Procedure - -```sh -provider="$work/48bit-high-file-provider.raw" -cp "$work/fixtures/48bit-high-file-prefix.erofs" "$provider" -truncate -s "$required_provider_length" "$provider" -dd if="$work/fixtures/source/high-offset.txt" of="$provider" bs=4096 \ - seek="$start_block" conv=notrunc -cc -O2 -Wall -Wextra -std=c17 tests/read_probe.c -o "$work/read_probe" -"$work/read_probe" pread "$provider" "$low_decoy_offset" "$file_size" \ - "$work/raw-low-offset.txt" -"$work/read_probe" pread "$provider" "$data_offset" "$file_size" \ - "$work/raw-high-offset.txt" -dd if=/dev/zero of="$work/zero.bin" bs="$file_size" count=1 -cmp "$work/zero.bin" "$work/raw-low-offset.txt" -! cmp -s "$work/fixtures/source/high-offset.txt" "$work/raw-low-offset.txt" -cmp "$work/fixtures/source/high-offset.txt" "$work/raw-high-offset.txt" -unit=$(mdconfig -a -t vnode -f "$provider") -diskinfo -v "/dev/$unit" -mkdir "$work/mnt" -mount -t erofs -o ro "/dev/$unit" "$work/mnt" -cmp "$work/fixtures/source/high-offset.txt" "$work/mnt/high-offset.txt" -cmp "$work/fixtures/source/control.txt" "$work/mnt/control.txt" -df -h "$work/mnt" -umount "$work/mnt" -mdconfig -d -u "${unit#md}" -rm -f "$provider" -``` - -## Expected Results - -The raw provider probe and mounted file both return the exact source bytes from -above 16 TiB, while a low-offset control remains valid. Record the prefix hash, -sparse media size/allocation, high offset, source/data hash, dmesg delta, and -cleanup. If sparse high-offset I/O is unavailable, mark SHELVED with all -attempts; a large `df` result alone cannot pass TC018. diff --git a/tests/TC019-48bit-root-nid.md b/tests/TC019-48bit-root-nid.md deleted file mode 100644 index 82fffea..0000000 --- a/tests/TC019-48bit-root-nid.md +++ /dev/null @@ -1,44 +0,0 @@ -# Test Case: Nonzero 48-bit Root NID - -**Test ID**: TC019-48bit-root-nid - -## Objective - -Verify the `48BIT && rootnid_8b != 0` selector uses the complete 64-bit root -field and interprets the two-byte union as `blocks_hi`. - -## Fixture - -Use `images/root8-48bit.erofs`. The structured transformer requires incompat -bit `0x80`, nonzero image-bounded `rootnid_8b`, `blocks_hi=0`, valid CRC32C, -and unchanged provider-sized `blocks_lo`. Production fsck.erofs 1.8.6 does not -recognize this feature; FreeBSD mount/read is mandatory. - -## Procedure - -Host field check: - -```sh -python3 tests/erofs_fixture.py inspect /work/build/repo22-g1/images/root8-48bit.erofs --path=/root.txt -od -An -tu2 -j 1038 -N 2 /work/build/repo22-g1/images/root8-48bit.erofs -od -An -tx4 -j 1104 -N 4 /work/build/repo22-g1/images/root8-48bit.erofs -od -An -tu8 -j 1136 -N 8 /work/build/repo22-g1/images/root8-48bit.erofs -``` - -Mount the same hashed image in FreeBSD, then run: - -```sh -stat -f 'root_ino=%i type=%HT' "$mnt" -cmp /tmp/repo22-g1/source/compact/root.txt "$mnt/root.txt" -sha256 /tmp/repo22-g1/source/compact/root.txt "$mnt/root.txt" -./statfs_probe "$mnt" -``` - -## Expected Results - -- `feature_incompat & 0x80` is set, `rootnid_8b` is nonzero, and observed root - inode equals that complete field. -- Root content matches the source and statfs uses `blocks_lo` plus zero - `blocks_hi`; mount and cleanup succeed. -- TC150's zero-`rootnid_8b` fallback is a separate selector and is not accepted - as evidence for this TC. diff --git a/tests/TC020-compact-inode-basic.md b/tests/TC020-compact-inode-basic.md deleted file mode 100644 index 0104b91..0000000 --- a/tests/TC020-compact-inode-basic.md +++ /dev/null @@ -1,32 +0,0 @@ -# Test Case: Compact Inode Decoding - -**Test ID**: TC020-compact-inode-basic - -## Objective - -Verify a real 32-byte compact regular inode exposes correct mode, link count, -size, allocation, and source-exact data. - -## Fixture - -Use `images/compact.erofs` and `source/compact/small.txt`. The evidence records -the NID and asserts `inode_size=32`. - -## Procedure - -After mounting on `$mnt`: - -```sh -stat -f 'ino=%i size=%z mode=%p nlink=%l blocks=%b mtime=%m' "$mnt/small.txt" -wc -c /tmp/repo22-g1/source/compact/small.txt "$mnt/small.txt" -cmp /tmp/repo22-g1/source/compact/small.txt "$mnt/small.txt" -sha256 /tmp/repo22-g1/source/compact/small.txt "$mnt/small.txt" -``` - -Unmount and detach the exact md unit. - -## Expected Results - -- The inode is compact, regular mode `0644`, link count 1, timestamp 0, and - size equal to the source. -- Full content and hashes match; all operations and cleanup return zero. diff --git a/tests/TC021-compact-inode-nlink1.md b/tests/TC021-compact-inode-nlink1.md deleted file mode 100644 index c542749..0000000 --- a/tests/TC021-compact-inode-nlink1.md +++ /dev/null @@ -1,28 +0,0 @@ -# Test Case: Compact Inode I_NLINK_1 - -**Test ID**: TC021-compact-inode-nlink1 - -## Objective - -Verify bit 4 on a non-directory compact inode forces `st_nlink=1` while the -`i_nb` union remains available for address high bits. - -## Fixture - -Use `images/compact-nlink1.erofs` and `source/compact/single.txt`. -`fixture-evidence.txt` asserts a 32-byte regular inode, bit 4 set, -`i_nb=0x1234`, and valid recomputed CRC32C. - -## Procedure - -```sh -stat -f 'ino=%i nlink=%l size=%z mode=%p' "$mnt/single.txt" -cmp /tmp/repo22-g1/source/compact/single.txt "$mnt/single.txt" -sha256 /tmp/repo22-g1/source/compact/single.txt "$mnt/single.txt" -``` - -## Expected Results - -- `st_nlink=1` despite on-disk `i_nb=0x1234`. -- Inline file data remains source-exact; mount and cleanup succeed. -- An `i_nb` value of zero is not used as a substitute for the bit-4 encoding. diff --git a/tests/TC022-compact-inode-special.md b/tests/TC022-compact-inode-special.md deleted file mode 100644 index 6f6b98d..0000000 --- a/tests/TC022-compact-inode-special.md +++ /dev/null @@ -1,31 +0,0 @@ -# Test Case: Compact Special Inodes and Linux dev_t Decode - -**Test ID**: TC022-compact-inode-special - -## Objective - -Verify compact char/block/FIFO inodes and Linux `new_decode_dev` conversion to -FreeBSD `dev_t`. - -## Fixture - -Use `images/compact.erofs`. Source metadata records real nodes -`char-large=2748:344865`, `block-large=2748:344865`, and `fifo`; structured -evidence asserts compact inodes and raw on-disk `i_u.rdev=0x543abc21` for both -devices. Compile `tests/stat_special.c` natively in FreeBSD. - -## Procedure - -```sh -./stat_special char "$mnt/char-large" 2748 344865 -./stat_special block "$mnt/block-large" 2748 344865 -./stat_special fifo "$mnt/fifo" -stat -f '%N mode=%p rdev=%r' "$mnt/char-large" "$mnt/block-large" "$mnt/fifo" -``` - -## Expected Results - -- Char and block helpers report major 2748, minor 344865, and raw FreeBSD - `st_rdev=0xa430005bc21`. -- FIFO type is correct and `st_rdev=NODEV`. -- A raw little-endian cast would differ; all helper checks and cleanup pass. diff --git a/tests/TC023-extended-inode-normal.md b/tests/TC023-extended-inode-normal.md deleted file mode 100644 index a5e8485..0000000 --- a/tests/TC023-extended-inode-normal.md +++ /dev/null @@ -1,27 +0,0 @@ -# Test Case: Extended Inode Decoding - -**Test ID**: TC023-extended-inode-normal - -## Objective - -Verify a real 64-byte extended regular inode, including size, full-width -metadata, timestamps, and source-exact content. - -## Fixture - -Use `images/extended.erofs` and `source/extended/large-file.bin`. -`fixture-evidence.txt` records the NID, byte offset, size, and `inode_size=64`. - -## Procedure - -```sh -stat -f 'ino=%i size=%z mode=%p nlink=%l mtime=%m ctime=%c blocks=%b' "$mnt/large-file.bin" -cmp /tmp/repo22-g1/source/extended/large-file.bin "$mnt/large-file.bin" -sha256 /tmp/repo22-g1/source/extended/large-file.bin "$mnt/large-file.bin" -``` - -## Expected Results - -- The file reports the evidence/source size, regular `0644`, link count 1, - deterministic timestamps, and a 64-byte on-disk inode. -- Full source compare and SHA256 match; cleanup succeeds. diff --git a/tests/TC024-extended-inode-large.md b/tests/TC024-extended-inode-large.md deleted file mode 100644 index 725f717..0000000 --- a/tests/TC024-extended-inode-large.md +++ /dev/null @@ -1,38 +0,0 @@ -# Test Case: Extended Inode File Above 4 GiB - -**Test ID**: TC024-extended-inode-large - -## Objective - -Verify a 64-bit extended `i_size` above 4 GiB without materializing a multi-GiB -payload, using deterministic FLAT_PLAIN hole reads at distant offsets. - -## Fixture - -Use `images/extended-large-hole.erofs` and -`expected/large-hole-zero-64k.bin`. Structured evidence asserts a 64-byte -inode, size `4294971393`, layout 0, `startblk=0xffffffffffff`, valid CRC, and -an all-zero content descriptor. - -## Procedure - -After mounting, run three bounded reads: - -```sh -stat -f 'size=%z blocks=%b mode=%p' "$mnt/huge-sparse.dat" -./read_probe pread "$mnt/huge-sparse.dat" 0 65536 /tmp/tc024-start -./read_probe pread "$mnt/huge-sparse.dat" 2147483648 65536 /tmp/tc024-middle -./read_probe pread "$mnt/huge-sparse.dat" 4294905857 65536 /tmp/tc024-end -cmp /tmp/repo22-g1/expected/large-hole-zero-64k.bin /tmp/tc024-start -cmp /tmp/repo22-g1/expected/large-hole-zero-64k.bin /tmp/tc024-middle -cmp /tmp/repo22-g1/expected/large-hole-zero-64k.bin /tmp/tc024-end -sha256 /tmp/tc024-start /tmp/tc024-middle /tmp/tc024-end -``` - -Remove outputs, unmount, and detach. - -## Expected Results - -- Size is exactly `4294971393`, not truncated to 32 bits. -- Start, middle, and final 64 KiB ranges match the deterministic zero source. -- Reads are bounded, no large allocation/panic occurs, and cleanup succeeds. diff --git a/tests/TC025-nlink1-handling.md b/tests/TC025-nlink1-handling.md deleted file mode 100644 index 9ea5208..0000000 --- a/tests/TC025-nlink1-handling.md +++ /dev/null @@ -1,35 +0,0 @@ -# Test Case: Compact Inode Link-Count Rules - -**Test ID**: TC025-nlink1-handling - -## Objective - -Compare explicit compact link counts, bit-4 single-link encoding, hard links, -and directory link counts without conflating directory dot omission. - -## Fixture - -Use `images/compact.erofs` and `images/compact-nlink1.erofs`. Evidence asserts -explicit `single.txt i_nb=1`, patched bit 4 plus `i_nb=0x1234`, identical -hard-link NID with `i_nb=2`, and explicit directory nlink. - -## Procedure - -Mount each image separately and record: - -```sh -stat -f '%N ino=%i nlink=%l mode=%p' \ - "$mnt/single.txt" "$mnt/hard-a.txt" "$mnt/hard-b.txt" "$mnt/dotdir" -cmp /tmp/repo22-g1/source/compact/single.txt "$mnt/single.txt" -cmp /tmp/repo22-g1/source/compact/hard-a.txt "$mnt/hard-a.txt" -cmp "$mnt/hard-a.txt" "$mnt/hard-b.txt" -``` - -Unmount and detach before attaching the second image. - -## Expected Results - -- Baseline and patched `single.txt` both report nlink 1 for different valid - encodings. -- `hard-a.txt` and `hard-b.txt` share one inode and report nlink 2. -- `dotdir` uses its explicit directory link count; all data compares pass. diff --git a/tests/TC026-dot-omitted-with.md b/tests/TC026-dot-omitted-with.md deleted file mode 100644 index 70a1580..0000000 --- a/tests/TC026-dot-omitted-with.md +++ /dev/null @@ -1,33 +0,0 @@ -# Test Case: Directory with dot_omitted - -**Test ID**: TC026-dot-omitted-with - -## Objective - -Verify a bit-4 directory omits only `.` on disk, retains explicit `..`, and -receives a synthetic `.` from FreeBSD readdir. - -## Fixture - -Use `images/compact-dot-omitted.erofs`. Structured evidence records bit 4, -on-disk names `..,child.txt`, 48-bit incompat selection with nonzero -`rootnid_8b`, `blocks_hi=0`, and valid CRC. Production fsck.erofs 1.8.6 cannot -qualify this newer format. - -## Procedure - -After mounting in FreeBSD: - -```sh -stat -f 'ino=%i nlink=%l mode=%p' "$mnt/dotdir" -ls -lai "$mnt/dotdir" -(cd "$mnt/dotdir" && test "$(pwd)" = "$mnt/dotdir") -cmp /tmp/repo22-g1/source/compact/dotdir/child.txt "$mnt/dotdir/child.txt" -``` - -## Expected Results - -- User-visible listing contains both `.` and `..`; `.` has the directory NID. -- Structured evidence, not the listing, proves `.` is absent on disk and bit 4 - is set; `..` remains an on-disk entry. -- Child data, navigation, mount, and cleanup succeed. diff --git a/tests/TC027-dot-omitted-without.md b/tests/TC027-dot-omitted-without.md deleted file mode 100644 index caa4c25..0000000 --- a/tests/TC027-dot-omitted-without.md +++ /dev/null @@ -1,35 +0,0 @@ -# Test Case: Directory without dot_omitted - -**Test ID**: TC027-dot-omitted-without - -## Objective - -Verify a bit-4-clear directory reads explicit on-disk `.` and `..` entries. - -## Fixture - -Use `images/compact.erofs` and `/dotdir`. Structured evidence asserts bit 4 is -clear and names are `.,..,child.txt`; production `dump.erofs --ls` also -qualifies this baseline. - -## Procedure - -Host: - -```sh -dump.erofs --ls --path=/dotdir /work/build/repo22-g1/images/compact.erofs -``` - -Guest after mount: - -```sh -stat -f 'ino=%i nlink=%l mode=%p' "$mnt/dotdir" -ls -lai "$mnt/dotdir" -cmp /tmp/repo22-g1/source/compact/dotdir/child.txt "$mnt/dotdir/child.txt" -``` - -## Expected Results - -- Host inspection and FreeBSD listing both contain explicit `.` and `..`. -- Child data matches the source, directory attributes are valid, and cleanup - succeeds. diff --git a/tests/TC028-flat-plain-small.md b/tests/TC028-flat-plain-small.md deleted file mode 100644 index 2d68bc2..0000000 --- a/tests/TC028-flat-plain-small.md +++ /dev/null @@ -1,26 +0,0 @@ -# Test Case: FLAT_PLAIN Small File - -**Test ID**: TC028-flat-plain-small - -## Objective - -Verify a sub-block regular file explicitly encoded as uncompressed -`FLAT_PLAIN` reads source-exactly. - -## Fixture - -Use `images/flat.erofs` and `source/flat/small.txt`. Structured evidence asserts -layout 0 and records NID and size. - -## Procedure - -```sh -stat -f 'size=%z blocks=%b mode=%p' "$mnt/small.txt" -cmp /tmp/repo22-g1/source/flat/small.txt "$mnt/small.txt" -sha256 /tmp/repo22-g1/source/flat/small.txt "$mnt/small.txt" -``` - -## Expected Results - -- Evidence proves `FLAT_PLAIN`; size and data match the source. -- The read, hash, mount, and cleanup all succeed. diff --git a/tests/TC029-flat-plain-medium.md b/tests/TC029-flat-plain-medium.md deleted file mode 100644 index cff6af5..0000000 --- a/tests/TC029-flat-plain-medium.md +++ /dev/null @@ -1,30 +0,0 @@ -# Test Case: FLAT_PLAIN Medium File - -**Test ID**: TC029-flat-plain-medium - -## Objective - -Verify complete and nonzero-offset reads of a multi-block `FLAT_PLAIN` file. - -## Fixture - -Use `images/flat.erofs`, `source/flat/medium.dat`, and -`expected/medium-10-5.bin`. Evidence asserts layout 0 and size 102417. - -## Procedure - -```sh -stat -f 'size=%z blocks=%b' "$mnt/medium.dat" -cmp /tmp/repo22-g1/source/flat/medium.dat "$mnt/medium.dat" -./read_probe pread "$mnt/medium.dat" 40960 20480 /tmp/tc029-range -cmp /tmp/repo22-g1/expected/medium-10-5.bin /tmp/tc029-range -sha256 /tmp/repo22-g1/source/flat/medium.dat "$mnt/medium.dat" \ - /tmp/repo22-g1/expected/medium-10-5.bin /tmp/tc029-range -``` - -Remove the range output, unmount, and detach. - -## Expected Results - -- Full file and the five-block range match their deterministic sources. -- No block-boundary truncation or cleanup failure occurs. diff --git a/tests/TC030-flat-plain-large.md b/tests/TC030-flat-plain-large.md deleted file mode 100644 index eef7e9a..0000000 --- a/tests/TC030-flat-plain-large.md +++ /dev/null @@ -1,33 +0,0 @@ -# Test Case: FLAT_PLAIN Large File - -**Test ID**: TC030-flat-plain-large - -## Objective - -Verify a greater-than-10-MiB contiguous `FLAT_PLAIN` file across thousands of -filesystem blocks. - -## Fixture - -Use `images/flat.erofs` and `source/flat/large.bin`. Evidence asserts layout 0 -and size 10485791. - -## Procedure - -```sh -stat -f 'size=%z blocks=%b' "$mnt/large.bin" -dd if="$mnt/large.bin" of=/tmp/tc030-full bs=1m -cmp /tmp/repo22-g1/source/flat/large.bin /tmp/tc030-full -sha256 /tmp/repo22-g1/source/flat/large.bin /tmp/tc030-full -./read_probe pread "$mnt/large.bin" 5242880 1048576 /tmp/tc030-seek -dd if=/tmp/repo22-g1/source/flat/large.bin of=/tmp/tc030-source-seek \ - bs=1m skip=5 count=1 -cmp /tmp/tc030-source-seek /tmp/tc030-seek -``` - -Remove all outputs, unmount, and detach. - -## Expected Results - -- Reported size is exact; full and seeked data match the source. -- No mapping error, panic, or cleanup failure occurs. diff --git a/tests/TC031-flat-inline-normal.md b/tests/TC031-flat-inline-normal.md deleted file mode 100644 index c299cb5..0000000 --- a/tests/TC031-flat-inline-normal.md +++ /dev/null @@ -1,28 +0,0 @@ -# Test Case: FLAT_INLINE Small File - -**Test ID**: TC031-flat-inline-normal - -## Objective - -Verify a small uncompressed `FLAT_INLINE` payload stored in the inode metadata -area and bounded by its metadata block. - -## Fixture - -Use `images/inline.erofs` and `source/inline/tiny.txt`. Evidence asserts layout -2, compact inode, NID, and size. - -## Procedure - -```sh -stat -f 'size=%z blocks=%b mode=%p' "$mnt/tiny.txt" -cmp /tmp/repo22-g1/source/inline/tiny.txt "$mnt/tiny.txt" -sha256 /tmp/repo22-g1/source/inline/tiny.txt "$mnt/tiny.txt" -``` - -## Expected Results - -- Layout is 2 and size/content match the source. -- FreeBSD reports 8 allocated 512-byte sectors for the 4096-byte EROFS - allocation unit; zero blocks is not the current qualified semantic. -- Mount and cleanup succeed. diff --git a/tests/TC032-flat-inline-zero.md b/tests/TC032-flat-inline-zero.md deleted file mode 100644 index 83c678e..0000000 --- a/tests/TC032-flat-inline-zero.md +++ /dev/null @@ -1,29 +0,0 @@ -# Test Case: FLAT_INLINE Zero-Length File - -**Test ID**: TC032-flat-inline-zero - -## Objective - -Verify an explicitly layout-2 zero-length compact inode returns clean EOF and -zero allocation. - -## Fixture - -Use `images/inline-zero.erofs` and `source/inline/empty.txt`. The transformer -asserts compact `FLAT_INLINE`, size 0, valid CRC, and records the image/source -hashes. - -## Procedure - -```sh -stat -f 'size=%z blocks=%b mode=%p' "$mnt/empty.txt" -cmp /tmp/repo22-g1/source/inline/empty.txt "$mnt/empty.txt" -test "$(wc -c < "$mnt/empty.txt")" -eq 0 -sha256 /tmp/repo22-g1/source/inline/empty.txt "$mnt/empty.txt" -``` - -## Expected Results - -- Size and byte count are zero, `st_blocks=0`, hashes match the empty source, - and no read error occurs. -- Mount and cleanup succeed. diff --git a/tests/TC033-tailpacking-normal.md b/tests/TC033-tailpacking-normal.md deleted file mode 100644 index 8056f54..0000000 --- a/tests/TC033-tailpacking-normal.md +++ /dev/null @@ -1,33 +0,0 @@ -# Test Case: FLAT_INLINE Tailpacking - -**Test ID**: TC033-tailpacking-normal - -## Objective - -Verify a 5000-byte file reads exactly across its full data block and 904-byte -inline tail boundary. - -## Fixture - -Use `images/inline.erofs` and `source/inline/tailpacked.dat`. Evidence asserts -layout 2 and size 5000. - -## Procedure - -```sh -stat -f 'size=%z blocks=%b' "$mnt/tailpacked.dat" -cmp /tmp/repo22-g1/source/inline/tailpacked.dat "$mnt/tailpacked.dat" -sha256 /tmp/repo22-g1/source/inline/tailpacked.dat "$mnt/tailpacked.dat" -./read_probe pread "$mnt/tailpacked.dat" 4080 64 /tmp/tc033-mounted-range -dd if=/tmp/repo22-g1/source/inline/tailpacked.dat \ - of=/tmp/tc033-source-range bs=1 skip=4080 count=64 2>/dev/null -cmp /tmp/tc033-source-range /tmp/tc033-mounted-range -``` - -Remove outputs, unmount, and detach. - -## Expected Results - -- Full file and the range crossing logical offset 4096 match the source. -- The transition from plain block data to metadata tail is seamless and - cleanup succeeds. diff --git a/tests/TC034-tailpacking-boundary.md b/tests/TC034-tailpacking-boundary.md deleted file mode 100644 index c22f921..0000000 --- a/tests/TC034-tailpacking-boundary.md +++ /dev/null @@ -1,35 +0,0 @@ -# Test Case: Tailpacking Block Boundaries - -**Test ID**: TC034-tailpacking-boundary - -## Objective - -Verify the actual erofs-utils boundary choices at 4095, 4096, and 4097 bytes, -including the one-byte inline tail case. - -## Fixture - -Use `images/inline.erofs` and the three matching files under `source/inline`. -Structured evidence requires layouts `0,0,2` respectively. A 4095-byte payload -cannot fit beside a 32-byte inode in one 4096-byte metadata block, so it is not -claimed as fully inline. - -## Procedure - -```sh -for size in 4095 4096 4097; do - stat -f '%N size=%z blocks=%b' "$mnt/file-$size.dat" - cmp "/tmp/repo22-g1/source/inline/file-$size.dat" "$mnt/file-$size.dat" - sha256 "/tmp/repo22-g1/source/inline/file-$size.dat" "$mnt/file-$size.dat" -done -./read_probe pread "$mnt/file-4097.dat" 4088 9 /tmp/tc034-boundary -dd if=/tmp/repo22-g1/source/inline/file-4097.dat \ - of=/tmp/tc034-source bs=1 skip=4088 count=9 2>/dev/null -cmp /tmp/tc034-source /tmp/tc034-boundary -``` - -## Expected Results - -- Exact sizes and full hashes match for all three files. -- 4095 and 4096 are FLAT_PLAIN; 4097 is FLAT_INLINE with a one-byte tail. -- The nine-byte cross-boundary range matches; cleanup succeeds. diff --git a/tests/TC035-file-read-sequential.md b/tests/TC035-file-read-sequential.md deleted file mode 100644 index 8d8f185..0000000 --- a/tests/TC035-file-read-sequential.md +++ /dev/null @@ -1,29 +0,0 @@ -# Test Case: Sequential Regular-File Read - -**Test ID**: TC035-file-read-sequential - -## Objective - -Verify buffered sequential reads return every byte of a deterministic -multi-block regular file. - -## Fixture - -Use `images/flat.erofs` and `source/flat/data.txt`. Evidence asserts -`FLAT_PLAIN`, NID, and size 262163. - -## Procedure - -```sh -stat -f 'size=%z blocks=%b' "$mnt/data.txt" -dd if="$mnt/data.txt" of=/tmp/tc035.out bs=4096 -cmp /tmp/repo22-g1/source/flat/data.txt /tmp/tc035.out -sha256 /tmp/repo22-g1/source/flat/data.txt /tmp/tc035.out -``` - -Remove output, unmount, and detach. - -## Expected Results - -- `dd` reaches exact EOF and the complete output matches the source/hash. -- No short read, data corruption, dmesg error, or cleanup failure occurs. diff --git a/tests/TC036-file-read-random.md b/tests/TC036-file-read-random.md deleted file mode 100644 index 5022ce9..0000000 --- a/tests/TC036-file-read-random.md +++ /dev/null @@ -1,30 +0,0 @@ -# Test Case: Random Regular-File Reads - -**Test ID**: TC036-file-read-random - -## Objective - -Verify independent start, middle, and end `pread(2)` ranges from a deterministic -1 MiB regular file. - -## Fixture - -Use `images/flat.erofs`, `source/flat/random-test.bin`, and the three -`expected/random-*.bin` files. Evidence asserts layout 0 and exact size. - -## Procedure - -```sh -./read_probe pread "$mnt/random-test.bin" 0 10240 /tmp/tc036-start -./read_probe pread "$mnt/random-test.bin" 512000 10240 /tmp/tc036-mid -./read_probe pread "$mnt/random-test.bin" 1024000 24576 /tmp/tc036-end -cmp /tmp/repo22-g1/expected/random-start.bin /tmp/tc036-start -cmp /tmp/repo22-g1/expected/random-mid.bin /tmp/tc036-mid -cmp /tmp/repo22-g1/expected/random-end.bin /tmp/tc036-end -sha256 /tmp/tc036-start /tmp/tc036-mid /tmp/tc036-end -``` - -## Expected Results - -- Every bounded `pread` reports its requested byte count and exact offset. -- All three ranges match independently generated source ranges; cleanup passes. diff --git a/tests/TC037-file-read-large.md b/tests/TC037-file-read-large.md deleted file mode 100644 index 812e03a..0000000 --- a/tests/TC037-file-read-large.md +++ /dev/null @@ -1,33 +0,0 @@ -# Test Case: Large Regular-File Read - -**Test ID**: TC037-file-read-large - -## Objective - -Verify complete and distant-range reads of a deterministic file larger than -100 MiB without a correctness shortcut based only on throughput. - -## Fixture - -Use `images/flat.erofs`, `source/flat/huge-data.bin`, and -`expected/huge-sample.bin`. Evidence asserts layout 0 and size 104857857. - -## Procedure - -```sh -stat -f 'size=%z blocks=%b' "$mnt/huge-data.bin" -time dd if="$mnt/huge-data.bin" of=/tmp/tc037-full bs=1m -cmp /tmp/repo22-g1/source/flat/huge-data.bin /tmp/tc037-full -sha256 /tmp/repo22-g1/source/flat/huge-data.bin /tmp/tc037-full -./read_probe pread "$mnt/huge-data.bin" 52428800 5242880 /tmp/tc037-sample -cmp /tmp/repo22-g1/expected/huge-sample.bin /tmp/tc037-sample -sha256 /tmp/repo22-g1/expected/huge-sample.bin /tmp/tc037-sample -``` - -Remove outputs, unmount, and detach. - -## Expected Results - -- Exact size, full compare, and 50 MiB-offset sample compare all pass. -- Record elapsed time but impose no host-dependent QEMU throughput threshold. -- No memory exhaustion, panic, or cleanup failure occurs. diff --git a/tests/TC038-symlink-short.md b/tests/TC038-symlink-short.md deleted file mode 100644 index d747d23..0000000 --- a/tests/TC038-symlink-short.md +++ /dev/null @@ -1,30 +0,0 @@ -# Test Case: Short Symlink Target - -**Test ID**: TC038-symlink-short - -## Objective - -Verify a short `FLAT_INLINE` symlink returns the exact source target and follows -to source-exact file data. - -## Fixture - -Use `images/inline.erofs`, `source/inline/link1`, and -`source/inline/target.txt`. Evidence asserts symlink mode, layout 2, and target -length 10. - -## Procedure - -```sh -test -L "$mnt/link1" -readlink /tmp/repo22-g1/source/inline/link1 > /tmp/tc038-source-target -readlink "$mnt/link1" > /tmp/tc038-mount-target -cmp /tmp/tc038-source-target /tmp/tc038-mount-target -cmp /tmp/repo22-g1/source/inline/target.txt "$mnt/link1" -sha256 /tmp/repo22-g1/source/inline/target.txt "$mnt/link1" -``` - -## Expected Results - -- `readlink` returns exactly `target.txt`, without newline truncation ambiguity. -- Following the link yields the exact target source; cleanup succeeds. diff --git a/tests/TC039-symlink-long.md b/tests/TC039-symlink-long.md deleted file mode 100644 index 36561ef..0000000 --- a/tests/TC039-symlink-long.md +++ /dev/null @@ -1,36 +0,0 @@ -# Test Case: Long Symlink Target - -**Test ID**: TC039-symlink-long - -## Objective - -Verify a greater-than-60-byte EROFS symlink target is returned completely and -follows correctly. - -## Fixture - -Use `images/flat.erofs`, `source/flat/longlink`, and its nested target file. -Structured evidence records symlink size 73 and actual layout 2. EROFS does not -switch to a separate-data-block symlink merely at 60 bytes; that former fixture -assumption is invalid. - -## Procedure - -```sh -test -L "$mnt/longlink" -readlink /tmp/repo22-g1/source/flat/longlink > /tmp/tc039-source-target -readlink "$mnt/longlink" > /tmp/tc039-mount-target -cmp /tmp/tc039-source-target /tmp/tc039-mount-target -test "$(wc -c < /tmp/tc039-mount-target)" -eq 74 -cmp /tmp/repo22-g1/source/flat/very/long/path/to/a/deeply/nested/deterministic/target/directory/file.txt \ - "$mnt/longlink" -sha256 "$mnt/longlink" \ - /tmp/repo22-g1/source/flat/very/long/path/to/a/deeply/nested/deterministic/target/directory/file.txt -``` - -The `wc` count is 73 target bytes plus the output newline. - -## Expected Results - -- Source and mounted targets match exactly and exceed 60 bytes. -- The link follows to source-exact data with no truncation; cleanup succeeds. diff --git a/tests/TC040-symlink-broken.md b/tests/TC040-symlink-broken.md deleted file mode 100644 index 6833fb1..0000000 --- a/tests/TC040-symlink-broken.md +++ /dev/null @@ -1,30 +0,0 @@ -# Test Case: Broken Symlink - -**Test ID**: TC040-symlink-broken - -## Objective - -Verify `readlink(2)` succeeds for a deterministic broken link while following -the target fails with exact `ENOENT`. - -## Fixture - -Use `images/inline.erofs` and `source/inline/brokenlink`. Evidence asserts -symlink mode, layout 2, and target length 29. - -## Procedure - -```sh -test -L "$mnt/brokenlink" -readlink /tmp/repo22-g1/source/inline/brokenlink > /tmp/tc040-source-target -readlink "$mnt/brokenlink" > /tmp/tc040-mount-target -cmp /tmp/tc040-source-target /tmp/tc040-mount-target -./read_probe expect-error "$mnt/brokenlink" 2 -./read_probe expect-error "$mnt/brokenlink" 2 -``` - -## Expected Results - -- Both `readlink` targets equal `/nonexistent/repo22-g1-target`. -- Both follow attempts return errno 2 (`ENOENT`), while the symlink itself - remains visible; no kernel error or cleanup failure occurs. diff --git a/tests/TC041-dirent-decode-normal.md b/tests/TC041-dirent-decode-normal.md deleted file mode 100644 index 8ae87d1..0000000 --- a/tests/TC041-dirent-decode-normal.md +++ /dev/null @@ -1,29 +0,0 @@ -# Test Case: Directory Entry Decoding - Normal - -**Test ID**: TC041-dirent-decode-normal -**Category**: Directory Operations -**Priority**: High - -## Objective - -Decode every entry type and the 255-byte filename from the deterministic VFS -fixture through FreeBSD getdirentries(2), not inferred ls formatting. - -## Procedure - -Mount vfs/vfs-plain.erofs using G3-MANUAL-SETUP.md, then run: - - ./readdir_probe /tmp/repo22-g3/mnt/testdir 512 - find /tmp/repo22-g3/mnt/testdir -maxdepth 1 -print | - sed 's#.*/##' | sort > actual-testdir.txt - cmp fixtures/vfs/expected-testdir.txt actual-testdir.txt - name=$(find /tmp/repo22-g3/mnt/testdir -maxdepth 1 -type f \ - -name 'long-*') - test "$(basename "$name" | tr -d '\n' | wc -c)" -eq 255 - -## Expected Results - -The probe reports 36 unique entries, correct DT_DIR/DT_REG/DT_LNK counts, -strictly increasing restartable cookies, and one deterministic name/type hash. -The 34 real names match the manifest exactly and the longest name is 255 bytes. -Record probe output, manifest/image hashes, and mount/md cleanup. diff --git a/tests/TC042-dirent-large-dir.md b/tests/TC042-dirent-large-dir.md deleted file mode 100644 index 1bb06e5..0000000 --- a/tests/TC042-dirent-large-dir.md +++ /dev/null @@ -1,28 +0,0 @@ -# Test Case: Directory Entry Decoding - Multi-Block Directory - -**Test ID**: TC042-dirent-large-dir -**Category**: Directory Operations -**Priority**: High - -## Objective - -Decode a real multi-block directory without missing, duplicating, or -truncating entries across directory-block boundaries. - -## Procedure - -Mount metadata/namei-base.erofs and run: - - ./readdir_probe /tmp/repo22-g3/mnt/wide 128 - find /tmp/repo22-g3/mnt/wide -maxdepth 1 -type f | - sed 's#.*/##' | sort > actual-wide.txt - cmp fixtures/metadata/expected-wide.txt actual-wide.txt - stat /tmp/repo22-g3/mnt/wide/entry-000-abcdefghijklmnopqrstuvwxyz.txt - stat /tmp/repo22-g3/mnt/wide/entry-159-abcdefghijklmnopqrstuvwxyz.txt - stat /tmp/repo22-g3/mnt/wide/entry-319-abcdefghijklmnopqrstuvwxyz.txt - -## Expected Results - -All 320 real files plus dot and dotdot are returned once. The helper reports -322 restartable kernel and libc positions and the three boundary samples stat -successfully. Record the image hash and cleanup. diff --git a/tests/TC043-lookup-found.md b/tests/TC043-lookup-found.md deleted file mode 100644 index 926dd94..0000000 --- a/tests/TC043-lookup-found.md +++ /dev/null @@ -1,29 +0,0 @@ -# Test Case: Lookup - Existing Entries - -**Test ID**: TC043-lookup-found -**Category**: Directory Operations -**Priority**: Critical - -## Objective - -Verify exact-name lookup of regular, directory, and symlink entries returns -stable vnode metadata and source-exact data. - -## Procedure - -Mount vfs/vfs-plain.erofs and run: - - ./g3_vfs_probe stat /tmp/repo22-g3/mnt/testdir/file.txt - ./g3_vfs_probe stat /tmp/repo22-g3/mnt/testdir/subdir - ./g3_vfs_probe stat /tmp/repo22-g3/mnt/testdir/shortlink - ./g3_vfs_probe stat /tmp/repo22-g3/mnt/testdir/file.txt - cmp fixtures/vfs/source/testdir/file.txt \ - /tmp/repo22-g3/mnt/testdir/file.txt - sha256 fixtures/vfs/source/testdir/file.txt \ - /tmp/repo22-g3/mnt/testdir/file.txt - -## Expected Results - -All lookups succeed, repeated file lookup reports the same nonzero inode, each -type/mode/size matches fixture evidence, and source/mounted hashes are equal. -Record command statuses and cleanup. diff --git a/tests/TC044-lookup-not-found.md b/tests/TC044-lookup-not-found.md deleted file mode 100644 index 76c85c1..0000000 --- a/tests/TC044-lookup-not-found.md +++ /dev/null @@ -1,28 +0,0 @@ -# Test Case: Lookup - Missing Entry - -**Test ID**: TC044-lookup-not-found -**Category**: Directory Operations -**Priority**: High - -## Objective - -Verify direct missing lookups return exact ENOENT without masking existing -entries or turning other errors into a miss. - -## Procedure - -Mount vfs/vfs-plain.erofs and run: - - ./g3_vfs_probe expect-error stat \ - /tmp/repo22-g3/mnt/testdir/nonexistent.txt 2 - ./g3_vfs_probe expect-error stat \ - /tmp/repo22-g3/mnt/testdir/nonexistent.txt 2 - ./g3_vfs_probe expect-error stat \ - /tmp/repo22-g3/mnt/no-such-directory/file.txt 2 - ./g3_vfs_probe expect-success stat \ - /tmp/repo22-g3/mnt/testdir/file.txt - -## Expected Results - -Each missing syscall reports errno 2 and the positive control succeeds. -Wrapper text alone is not evidence; record the helper output and cleanup. diff --git a/tests/TC045-lookup-case-sensitive.md b/tests/TC045-lookup-case-sensitive.md deleted file mode 100644 index 2880b76..0000000 --- a/tests/TC045-lookup-case-sensitive.md +++ /dev/null @@ -1,27 +0,0 @@ -# Test Case: Lookup - Case Sensitivity - -**Test ID**: TC045-lookup-case-sensitive -**Category**: Directory Operations -**Priority**: High - -## Objective - -Verify four distinct case variants resolve independently and an unrecorded -case variant returns ENOENT. - -## Procedure - -Mount vfs/vfs-plain.erofs and run: - - stat -f 'name=%N ino=%i size=%z' \ - /tmp/repo22-g3/mnt/testdir/file.txt \ - /tmp/repo22-g3/mnt/testdir/File.txt \ - /tmp/repo22-g3/mnt/testdir/FILE.TXT \ - /tmp/repo22-g3/mnt/testdir/FiLe.TxT - ./g3_vfs_probe expect-error stat \ - /tmp/repo22-g3/mnt/testdir/fILE.txt 2 - -## Expected Results - -The four exact variants have four nonzero, distinct NIDs and their recorded -sizes. The unmatched spelling returns errno 2. Record output and cleanup. diff --git a/tests/TC046-readdir-basic.md b/tests/TC046-readdir-basic.md deleted file mode 100644 index a0480fd..0000000 --- a/tests/TC046-readdir-basic.md +++ /dev/null @@ -1,25 +0,0 @@ -# Test Case: Readdir - Basic Completeness - -**Test ID**: TC046-readdir-basic -**Category**: Directory Operations -**Priority**: Critical - -## Objective - -Verify one complete getdirentries(2)/readdir(3) traversal returns the exact -fixture set, including dot and dotdot. - -## Procedure - -Mount vfs/vfs-plain.erofs and run: - - ./readdir_probe /tmp/repo22-g3/mnt/testdir 4096 - find /tmp/repo22-g3/mnt/testdir -maxdepth 1 -mindepth 1 -print | - sed 's#.*/##' | sort > actual-testdir.txt - cmp fixtures/vfs/expected-testdir.txt actual-testdir.txt - -## Expected Results - -The manifest comparison is exact; the probe reports 36 unique entries and the -same ordered name/type hash through both APIs. Record the image hash and -cleanup. diff --git a/tests/TC047-readdir-large.md b/tests/TC047-readdir-large.md deleted file mode 100644 index dc1a220..0000000 --- a/tests/TC047-readdir-large.md +++ /dev/null @@ -1,28 +0,0 @@ -# Test Case: Readdir - Multi-Block Completeness - -**Test ID**: TC047-readdir-large -**Category**: Directory Operations -**Priority**: High - -## Objective - -Verify repeated small-buffer reads cover a 320-file multi-block directory -exactly once. - -## Procedure - -Mount metadata/namei-base.erofs and run: - - ./readdir_probe /tmp/repo22-g3/mnt/wide 128 - find /tmp/repo22-g3/mnt/wide -maxdepth 1 -type f | - sed 's#.*/##' | sort > actual-wide.txt - test "$(wc -l < actual-wide.txt)" -eq 320 - test "$(sort actual-wide.txt | uniq -d | wc -l)" -eq 0 - -Compare actual-wide.txt with fixtures/metadata/expected-wide.txt. - -## Expected Results - -The helper reports 322 unique entries and 322 valid restart positions with a -128-byte userspace buffer; the exact manifest has 320 names and no duplicates. -Record the probe hash and cleanup. diff --git a/tests/TC048-readdir-seek.md b/tests/TC048-readdir-seek.md deleted file mode 100644 index 3ffbd98..0000000 --- a/tests/TC048-readdir-seek.md +++ /dev/null @@ -1,31 +0,0 @@ -# Test Case: Readdir Cookies and seekdir Resume - -**Test ID**: TC048-readdir-seek -**Category**: Directory Operations -**Priority**: Critical - -## Objective - -Verify every kernel d_off and libc telldir cookie resumes at the next exact -entry; complete second traversals are not substitutes. - -## Procedure - -Mount metadata/namei-base.erofs and run: - - ./readdir_probe /tmp/repo22-g3/mnt/wide 128 | - tee TC048-probe.out - -## Probe Contract - -The helper fails on non-increasing cookies, invalid records, duplicate names, -kernel/libc order or type differences, a restart at the wrong next name, or a -final cookie that does not reach EOF. Kernel d_off values are validated after -reopen/lseek; each libc telldir value is passed back to seekdir on the same -DIR stream that produced it, as required by the API contract. - -## Expected Results - -The helper exits zero and reports entries=322, d_off_restarts=322, -seekdir_restarts=322, buffer=128, the type counts, and one FNV-1a name/type -hash. Record literal output and cleanup. diff --git a/tests/TC049-dot-handling.md b/tests/TC049-dot-handling.md deleted file mode 100644 index 7a076a2..0000000 --- a/tests/TC049-dot-handling.md +++ /dev/null @@ -1,26 +0,0 @@ -# Test Case: Dot Entry Handling - -**Test ID**: TC049-dot-handling -**Category**: Directory Operations -**Priority**: High - -## Objective - -Verify dot is returned as a directory entry and explicit dot lookup resolves -to the same vnode as its directory. - -## Procedure - -Mount vfs/vfs-plain.erofs and run: - - ./readdir_probe /tmp/repo22-g3/mnt/testdir 128 - stat -f '%i %HT' /tmp/repo22-g3/mnt/testdir - stat -f '%i %HT' /tmp/repo22-g3/mnt/testdir/. - cmp fixtures/vfs/source/testdir/file.txt \ - /tmp/repo22-g3/mnt/./testdir/./file.txt - -## Expected Results - -The traversal includes one dot entry with DT_DIR, both stat commands report -the same directory NID, and paths containing dot read source-exact data. -Record output and cleanup. diff --git a/tests/TC050-dotdot-handling.md b/tests/TC050-dotdot-handling.md deleted file mode 100644 index 39a9262..0000000 --- a/tests/TC050-dotdot-handling.md +++ /dev/null @@ -1,26 +0,0 @@ -# Test Case: Dotdot Entry Handling - -**Test ID**: TC050-dotdot-handling -**Category**: Directory Operations -**Priority**: High - -## Objective - -Verify dotdot is returned and resolves nested paths to the correct parent -vnode without lock-order failure. - -## Procedure - -Mount vfs/vfs-plain.erofs and run: - - ./readdir_probe /tmp/repo22-g3/mnt/parent/child 128 - stat -f '%i %HT' /tmp/repo22-g3/mnt/parent - stat -f '%i %HT' /tmp/repo22-g3/mnt/parent/child/.. - cmp fixtures/vfs/source/parent/file.txt \ - /tmp/repo22-g3/mnt/parent/child/grandchild/../../file.txt - -## Expected Results - -The traversal contains one dotdot DT_DIR entry, both parent stat commands -report the same NID, and multiple dotdot components reach the expected file. -Record output, dmesg delta, and cleanup. diff --git a/tests/TC051-namecache-hit.md b/tests/TC051-namecache-hit.md deleted file mode 100644 index 9197734..0000000 --- a/tests/TC051-namecache-hit.md +++ /dev/null @@ -1,29 +0,0 @@ -# Test Case: Repeated Positive Lookup Behavior - -**Test ID**: TC051-namecache-hit -**Category**: VFS Integration -**Priority**: High - -## Objective - -Verify repeated positive pathname lookups remain stable within one mount. -This is behavior-level namecache integration coverage; timing differences do -not prove an internal cache hit. - -## Procedure - -Mount vfs/vfs-plain.erofs and run: - - stat -f 'ino=%i mode=%p size=%z gen=%v' \ - /tmp/repo22-g3/mnt/testdir/file.txt > TC051-first.txt - stat -f 'ino=%i mode=%p size=%z gen=%v' \ - /tmp/repo22-g3/mnt/testdir/file.txt > TC051-second.txt - cmp TC051-first.txt TC051-second.txt - cmp fixtures/vfs/source/testdir/file.txt \ - /tmp/repo22-g3/mnt/testdir/file.txt - -## Expected Results - -Both lookups succeed with byte-identical metadata and data. Record only this -observable behavior as PASS; do not claim a measured cache hit without kernel -instrumentation. Record output and cleanup. diff --git a/tests/TC052-namecache-miss.md b/tests/TC052-namecache-miss.md deleted file mode 100644 index 7e36a1d..0000000 --- a/tests/TC052-namecache-miss.md +++ /dev/null @@ -1,28 +0,0 @@ -# Test Case: Repeated Negative Lookup Behavior - -**Test ID**: TC052-namecache-miss -**Category**: VFS Integration -**Priority**: High - -## Objective - -Verify repeated missing-name lookups consistently return ENOENT and do not -poison a later positive lookup. This is behavior-level negative-namecache -coverage, not proof of an internal cache hit. - -## Procedure - -Mount vfs/vfs-plain.erofs and run: - - ./g3_vfs_probe expect-error stat \ - /tmp/repo22-g3/mnt/testdir/cache-missing 2 - ./g3_vfs_probe expect-error stat \ - /tmp/repo22-g3/mnt/testdir/cache-missing 2 - ./g3_vfs_probe expect-success stat \ - /tmp/repo22-g3/mnt/testdir/file.txt - -## Expected Results - -Both negative syscalls report errno 2 and the positive control succeeds. -Record the stable behavior honestly, with no latency-based cache assertion, -then clean the mount and md. diff --git a/tests/TC053-vfs-hash-integration.md b/tests/TC053-vfs-hash-integration.md deleted file mode 100644 index c1fdf5b..0000000 --- a/tests/TC053-vfs-hash-integration.md +++ /dev/null @@ -1,30 +0,0 @@ -# Test Case: Stable Vnode Identity Behavior - -**Test ID**: TC053-vfs-hash-integration -**Category**: VFS Integration -**Priority**: High - -## Objective - -Verify repeated resolutions of one EROFS NID expose one stable vnode identity -and file handle within a mount. This is behavior-level coverage of the -vfs_hash contract; it does not directly observe the internal hash bucket. - -## Procedure - -Mount vfs/vfs-plain.erofs and run: - - stat -f 'ino=%i gen=%v size=%z' \ - /tmp/repo22-g3/mnt/testdir/file.txt > TC053-a.txt - stat -f 'ino=%i gen=%v size=%z' \ - /tmp/repo22-g3/mnt/testdir/./file.txt > TC053-b.txt - cmp TC053-a.txt TC053-b.txt - ./nfs_fh_tool capture /tmp/repo22-g3/mnt/testdir/file.txt TC053-a.fh - ./nfs_fh_tool capture /tmp/repo22-g3/mnt/testdir/./file.txt TC053-b.fh - ./nfs_fh_tool compare TC053-a.fh TC053-b.fh - ./nfs_fh_tool describe TC053-a.fh - -## Expected Results - -Metadata and handles are identical and the handle NID/generation matches stat. -Record this observable identity behavior, handle hash, and cleanup. diff --git a/tests/TC054-vn-vget-ino.md b/tests/TC054-vn-vget-ino.md deleted file mode 100644 index fbd7987..0000000 --- a/tests/TC054-vn-vget-ino.md +++ /dev/null @@ -1,30 +0,0 @@ -# Test Case: Parent Vnode Lookup Behavior - -**Test ID**: TC054-vn-vget-ino -**Category**: VFS Integration -**Priority**: High - -## Objective - -Verify repeated dotdot lookups return the correct stable parent vnode without -deadlock. This is behavior-level coverage of the vn_vget_ino path; userspace -cannot prove the internal helper call by timing. - -## Procedure - -Mount vfs/vfs-plain.erofs and run: - - stat -f 'ino=%i gen=%v type=%HT' \ - /tmp/repo22-g3/mnt/parent > TC054-parent.txt - stat -f 'ino=%i gen=%v type=%HT' \ - /tmp/repo22-g3/mnt/parent/child/.. > TC054-dotdot1.txt - stat -f 'ino=%i gen=%v type=%HT' \ - /tmp/repo22-g3/mnt/parent/child/.. > TC054-dotdot2.txt - cmp TC054-parent.txt TC054-dotdot1.txt - cmp TC054-dotdot1.txt TC054-dotdot2.txt - -## Expected Results - -All commands complete, metadata is identical, and dmesg has no lock-order, -trap, or panic report. Record behavior and cleanup without claiming direct -internal instrumentation. diff --git a/tests/TC055-getattr-basic.md b/tests/TC055-getattr-basic.md deleted file mode 100644 index 44c8726..0000000 --- a/tests/TC055-getattr-basic.md +++ /dev/null @@ -1,30 +0,0 @@ -# Test Case: getattr Basic Metadata - -**Test ID**: TC055-getattr-basic -**Category**: Vnode Operations -**Priority**: Critical - -## Objective - -Verify VOP_GETATTR returns fixture-exact type, mode, ownership, link count, -size, allocation, block size, inode, and generation for plain and compressed -regular files. - -## Procedure - -On separate fresh mounts of vfs/vfs-plain.erofs and vfs/vfs-lz4.erofs: - - ./g3_vfs_probe stat /tmp/repo22-g3/mnt/pager.bin - stat -f 'ino=%i mode=%p uid=%u gid=%g nlink=%l size=%z blocks=%b blksize=%k gen=%v type=%HT' \ - /tmp/repo22-g3/mnt/pager.bin - sha256 /tmp/repo22-g3/mnt/pager.bin - -For the plain mount also compare the mounted hash with -fixtures/vfs/source/pager.bin and fixture evidence. - -## Expected Results - -Both files are regular mode 0644, uid/gid 0, size 21211, block size 4096, with -nonzero stable NID/generation. Both logical hashes equal the source hash; -st_blocks records each inode's reported allocated sectors. Record both image -hashes and per-mount cleanup. diff --git a/tests/TC056-getattr-special.md b/tests/TC056-getattr-special.md deleted file mode 100644 index 758c27d..0000000 --- a/tests/TC056-getattr-special.md +++ /dev/null @@ -1,28 +0,0 @@ -# Test Case: getattr Special Files - -**Test ID**: TC056-getattr-special -**Category**: Vnode Operations -**Priority**: Critical - -## Objective - -Verify compact and extended char, block, and FIFO inodes expose exact types, -device numbers, zero size/allocation, and stable generation. - -## Procedure - -On fresh mounts of metadata/special-compact.erofs and -metadata/special-extended.erofs: - - ./stat_special char /tmp/repo22-g3/mnt/char-large 2748 344865 - ./stat_special block /tmp/repo22-g3/mnt/block-large 2748 344865 - ./stat_special fifo /tmp/repo22-g3/mnt/fifo - stat -f 'name=%N type=%HT size=%z blocks=%b gen=%v' \ - /tmp/repo22-g3/mnt/char-large \ - /tmp/repo22-g3/mnt/block-large \ - /tmp/repo22-g3/mnt/fifo - -## Expected Results - -All type/rdev probes pass; each special inode has size and st_blocks zero and -a nonzero generation. Record both fixture hashes and clean each mount/md. diff --git a/tests/TC057-access-allowed.md b/tests/TC057-access-allowed.md deleted file mode 100644 index 801fd3e..0000000 --- a/tests/TC057-access-allowed.md +++ /dev/null @@ -1,24 +0,0 @@ -# Test Case: access - Allowed Operations - -**Test ID**: TC057-access-allowed -**Category**: VFS Integration -**Priority**: High - -## Objective - -Verify a non-root credential receives read and execute access allowed by the -recorded mode bits on the read-only mount. - -## Procedure - -Mount vfs/vfs-plain.erofs and run with the native nobody credential: - - su -m nobody -c './g3_vfs_probe expect-success access-read /tmp/repo22-g3/mnt/testdir/file.txt' - su -m nobody -c './g3_vfs_probe expect-success access-exec /tmp/repo22-g3/mnt/testdir' - su -m nobody -c './g3_vfs_probe expect-success access-exec /tmp/repo22-g3/mnt/testdir/script.sh' - su -m nobody -c './g3_vfs_probe expect-success open-read /tmp/repo22-g3/mnt/testdir/file.txt' - -## Expected Results - -All four direct checks report success with errno 0. Record the nobody uid, -fixture modes, command output, and cleanup. diff --git a/tests/TC058-access-denied.md b/tests/TC058-access-denied.md deleted file mode 100644 index b8cc139..0000000 --- a/tests/TC058-access-denied.md +++ /dev/null @@ -1,24 +0,0 @@ -# Test Case: access - Denied Operations - -**Test ID**: TC058-access-denied -**Category**: VFS Integration -**Priority**: High - -## Objective - -Verify direct access checks distinguish permission denial from read-only -modification denial for a non-root credential. - -## Procedure - -Mount vfs/vfs-plain.erofs and run: - - su -m nobody -c './g3_vfs_probe expect-error access-read /tmp/repo22-g3/mnt/testdir/rootonly.txt 13' - su -m nobody -c './g3_vfs_probe expect-error access-read /tmp/repo22-g3/mnt/testdir/writeonly.txt 13' - su -m nobody -c './g3_vfs_probe expect-error access-exec /tmp/repo22-g3/mnt/testdir/noexec.txt 13' - su -m nobody -c './g3_vfs_probe expect-error access-write /tmp/repo22-g3/mnt/testdir/file.txt 30' - -## Expected Results - -Mode-bit denials report EACCES (13); requested modification of the regular -file reports EROFS (30). Record exact helper output and cleanup. diff --git a/tests/TC059-readlink.md b/tests/TC059-readlink.md deleted file mode 100644 index e2f2deb..0000000 --- a/tests/TC059-readlink.md +++ /dev/null @@ -1,30 +0,0 @@ -# Test Case: readlink Exact Targets - -**Test ID**: TC059-readlink -**Category**: VFS Integration -**Priority**: High - -## Objective - -Verify short, relative, broken, and long symlink targets are returned exactly, -including the long inline target length and bytes. - -## Procedure - -Mount vfs/vfs-plain.erofs and run: - - ./g3_vfs_probe readlink /tmp/repo22-g3/mnt/testdir/shortlink - ./g3_vfs_probe readlink /tmp/repo22-g3/mnt/testdir/relative-link - ./g3_vfs_probe readlink /tmp/repo22-g3/mnt/testdir/broken-link - ./g3_vfs_probe readlink /tmp/repo22-g3/mnt/testdir/long-link - cmp fixtures/vfs/source/testdir/file.txt \ - /tmp/repo22-g3/mnt/testdir/shortlink - ./g3_vfs_probe expect-error stat \ - /tmp/repo22-g3/mnt/testdir/broken-link 2 - -## Expected Results - -The helper reports exact targets and hashes: shortlink is file.txt, the -relative link is subdir/child.txt, broken-link is missing-target, and long-link -has the recorded 119-byte target. Following valid links matches source; the -broken target returns errno 2. Record output and cleanup. diff --git a/tests/TC060-pathconf.md b/tests/TC060-pathconf.md deleted file mode 100644 index 899c6e9..0000000 --- a/tests/TC060-pathconf.md +++ /dev/null @@ -1,25 +0,0 @@ -# Test Case: pathconf Queries - -**Test ID**: TC060-pathconf -**Category**: VFS Integration -**Priority**: High -**Latest Result**: PASS on exact cdba7e54f KLD; see -tests/results/manual/2026-08-09T1120Z-tc060-fix/manual-test-report.md and the -resolved issues/TC060-pathconf-standard-values.md. - -## Objective - -Verify direct pathconf(2) results for EROFS limits and read-only conventions. - -## Procedure - -Mount vfs/vfs-plain.erofs and run: - - ./g3_vfs_probe pathconf /tmp/repo22-g3/mnt/testdir - -## Expected Results - -The helper must exit zero and report name_max=255, a positive path_max, -filesizebits=64, a positive link_max, no_trunc=1, and chown_restricted=1. -EINVAL for either standard query is a kernel failure, not an environmental -skip. Record every value/errno and cleanup. diff --git a/tests/TC061-setattr-chmod-reject.md b/tests/TC061-setattr-chmod-reject.md deleted file mode 100644 index e1d5eb4..0000000 --- a/tests/TC061-setattr-chmod-reject.md +++ /dev/null @@ -1,24 +0,0 @@ -# Test Case: chmod Rejection - -**Test ID**: TC061-setattr-chmod-reject -**Category**: Read-Only Enforcement -**Priority**: Critical - -## Objective - -Verify a direct chmod request returns EROFS and leaves metadata unchanged. - -## Procedure - -Mount vfs/vfs-plain.erofs and run: - - ./g3_vfs_probe stat /tmp/repo22-g3/mnt/testdir/file.txt > TC061-before.txt - ./g3_vfs_probe expect-error chmod \ - /tmp/repo22-g3/mnt/testdir/file.txt 30 - ./g3_vfs_probe stat /tmp/repo22-g3/mnt/testdir/file.txt > TC061-after.txt - cmp TC061-before.txt TC061-after.txt - -## Expected Results - -chmod reports errno 30 and complete metadata remains unchanged. Record output, -fixture hash, and cleanup. diff --git a/tests/TC062-setattr-chown-reject.md b/tests/TC062-setattr-chown-reject.md deleted file mode 100644 index 563e682..0000000 --- a/tests/TC062-setattr-chown-reject.md +++ /dev/null @@ -1,25 +0,0 @@ -# Test Case: chown Rejection - -**Test ID**: TC062-setattr-chown-reject -**Category**: Read-Only Enforcement -**Priority**: Critical - -## Objective - -Verify a privileged direct chown request returns EROFS and leaves ownership -unchanged. - -## Procedure - -Mount vfs/vfs-plain.erofs as root and run: - - ./g3_vfs_probe stat /tmp/repo22-g3/mnt/testdir/file.txt > TC062-before.txt - ./g3_vfs_probe expect-error chown \ - /tmp/repo22-g3/mnt/testdir/file.txt 30 - ./g3_vfs_probe stat /tmp/repo22-g3/mnt/testdir/file.txt > TC062-after.txt - cmp TC062-before.txt TC062-after.txt - -## Expected Results - -chown to uid/gid 65534 reports errno 30 and metadata remains unchanged. Record -output and cleanup. diff --git a/tests/TC063-setattr-write-reject.md b/tests/TC063-setattr-write-reject.md deleted file mode 100644 index 96e963a..0000000 --- a/tests/TC063-setattr-write-reject.md +++ /dev/null @@ -1,30 +0,0 @@ -# Test Case: Write, Truncate, and Create Rejection - -**Test ID**: TC063-setattr-write-reject -**Category**: Read-Only Enforcement -**Priority**: Critical - -## Objective - -Verify direct open-for-write, truncate, and create requests return EROFS and -leave the mounted tree unchanged. - -## Procedure - -Mount vfs/vfs-plain.erofs and run: - - sha256 /tmp/repo22-g3/mnt/testdir/file.txt > TC063-before.txt - ./g3_vfs_probe expect-error open-rdwr \ - /tmp/repo22-g3/mnt/testdir/file.txt 30 - ./g3_vfs_probe expect-error truncate \ - /tmp/repo22-g3/mnt/testdir/file.txt 30 - ./g3_vfs_probe expect-error create \ - /tmp/repo22-g3/mnt/testdir/newfile.txt 30 - sha256 /tmp/repo22-g3/mnt/testdir/file.txt > TC063-after.txt - cmp TC063-before.txt TC063-after.txt - test ! -e /tmp/repo22-g3/mnt/testdir/newfile.txt - -## Expected Results - -All mutation syscalls report errno 30, the file hash is unchanged, and no new -entry exists. Record output and cleanup. diff --git a/tests/TC064-vop-getpages-normal.md b/tests/TC064-vop-getpages-normal.md deleted file mode 100644 index 4fed26f..0000000 --- a/tests/TC064-vop-getpages-normal.md +++ /dev/null @@ -1,25 +0,0 @@ -# Test Case: FreeBSD 15 Local Vnode Pager - -**Test ID**: TC064-vop-getpages-normal -**Category**: VM Integration -**Priority**: Critical - -## Objective - -Verify the exact KLD resolves FreeBSD 15 local pager entry points and real -plain/LZ4 vnode mappings fault data through the pager. - -## Procedure - -Record unresolved KLD symbols with nm -u. On separate fresh mounts of -vfs/vfs-plain.erofs and vfs/vfs-lz4.erofs, run: - - ./mmap_fault /tmp/repo22-g3/mnt/pager.bin - -## Expected Results - -The KLD loads with vnode_pager_local_getpages and -vnode_pager_local_getpages_async resolved. Both helpers report identical -21211-byte FNV hashes, six random faults, partial-EOF zeroing, expected child -SIGBUS, denied writable shared mapping, private COW, and O_RDWR EROFS. Record -both image hashes, dmesg, and per-mount cleanup. diff --git a/tests/TC065-vop-getpages-mmap.md b/tests/TC065-vop-getpages-mmap.md deleted file mode 100644 index ee78242..0000000 --- a/tests/TC065-vop-getpages-mmap.md +++ /dev/null @@ -1,24 +0,0 @@ -# Test Case: mmap Through VOP_GETPAGES - -**Test ID**: TC065-vop-getpages-mmap -**Category**: VM Integration -**Priority**: Critical - -## Objective - -Verify page-fault-driven reads, EOF semantics, and private/shared mapping -behavior for plain and compressed EROFS files. - -## Procedure - -On separate fresh mounts of vfs/vfs-plain.erofs and vfs/vfs-lz4.erofs: - - ./mmap_fault /tmp/repo22-g3/mnt/pager.bin - -## Expected Results - -Random and sequential mapped bytes match pread exactly; the partial EOF page -is zero, the next full page produces the helper's expected child SIGBUS, -writable MAP_SHARED is EACCES, MAP_PRIVATE COW succeeds, and O_RDWR is EROFS. -The plain and LZ4 logical FNV hashes are identical. Direct read utilities are -not acceptance evidence. Record dmesg and cleanup. diff --git a/tests/TC066-vop-bmap-unsupported.md b/tests/TC066-vop-bmap-unsupported.md deleted file mode 100644 index 21a4108..0000000 --- a/tests/TC066-vop-bmap-unsupported.md +++ /dev/null @@ -1,24 +0,0 @@ -# Test Case: Unsupported BMAP with Pager Fallback - -**Test ID**: TC066-vop-bmap-unsupported -**Category**: VM Integration -**Priority**: High - -## Objective - -Verify the exact source KLD keeps erofs_bmap as EOPNOTSUPP while FreeBSD's -local vnode pager successfully faults plain and compressed files through -VOP_READ. - -## Procedure - -Audit the exact archived source registration and record KLD symbols. Then, on -fresh mounts of vfs/vfs-plain.erofs and vfs/vfs-lz4.erofs, run: - - ./mmap_fault /tmp/repo22-g3/mnt/pager.bin - -## Expected Results - -The source returns EOPNOTSUPP from erofs_bmap and registers both local pager -entry points. Both real mmap tests pass; dmesg has no "No strategy for -buffer", assertion, trap, or panic. Record source/KLD/image hashes and cleanup. diff --git a/tests/TC067-shared-xattr-scan-found.md b/tests/TC067-shared-xattr-scan-found.md deleted file mode 100644 index 5843fb0..0000000 --- a/tests/TC067-shared-xattr-scan-found.md +++ /dev/null @@ -1,28 +0,0 @@ -# Test Case: Shared xattr scan found - -**Test ID**: TC067-shared-xattr-scan-found -**Fixture**: `basic.erofs`, `/shared-a`, `/shared-b` - -## Objective - -Verify that shared IDs are scanned after inline entries and return exact data. -The generator manifest must show `shared_key` in the shared table and `local` -inline on `/shared-a`. - -## Manual steps - -Mount `basic.erofs` using `G4-MANUAL-SETUP.md`, then run: - -```sh -lsextattr user /mnt/repo22-g4/shared-a -getextattr -qq -x user shared_key /mnt/repo22-g4/shared-a -getextattr -qq -x user shared_key /mnt/repo22-g4/shared-b -getextattr -qq -x user local /mnt/repo22-g4/shared-a -stat -f 'size=%z inode=%i' /mnt/repo22-g4/shared-a -``` - -## Expected results - -Both shared reads equal `shared-value\0exact` byte-for-byte, the inline read is -`in-bounds-local`, and list output contains all four expected user names. -Unmount and detach before the next TC. diff --git a/tests/TC068-shared-xattr-scan-notfound.md b/tests/TC068-shared-xattr-scan-notfound.md deleted file mode 100644 index 3b1e49e..0000000 --- a/tests/TC068-shared-xattr-scan-notfound.md +++ /dev/null @@ -1,26 +0,0 @@ -# Test Case: Shared xattr scan not found - -**Test ID**: TC068-shared-xattr-scan-notfound -**Fixture**: `basic.erofs`, `/shared-a` - -## Objective - -Verify clean exhaustion of inline and shared scans and namespace isolation. - -## Manual steps - -Mount `basic.erofs`, list the user namespace, then capture both misses: - -```sh -lsextattr user /mnt/repo22-g4/shared-a -truss -o /tmp/tc068-name.truss \ - getextattr -qq user nonexistent /mnt/repo22-g4/shared-a -truss -o /tmp/tc068-ns.truss \ - getextattr -qq system shared_key /mnt/repo22-g4/shared-a -grep extattr_get_file /tmp/tc068-*.truss -``` - -## Expected results - -Both kernel calls return `ENOATTR` (87), with no `EINTEGRITY`, delay, or name -leak into the FreeBSD `system` namespace. Unmount and detach the image. diff --git a/tests/TC069-shared-xattr-list-multiple.md b/tests/TC069-shared-xattr-list-multiple.md deleted file mode 100644 index 61191ea..0000000 --- a/tests/TC069-shared-xattr-list-multiple.md +++ /dev/null @@ -1,23 +0,0 @@ -# Test Case: List multiple shared xattrs - -**Test ID**: TC069-shared-xattr-list-multiple -**Fixture**: `basic.erofs`, `/shared-multi` - -## Objective - -Verify FreeBSD's length-prefixed extattr list for several shared user entries. - -## Manual steps - -```sh -lsextattr user /mnt/repo22-g4/shared-multi -getextattr -qq -x user shared_key /mnt/repo22-g4/shared-multi -getextattr -qq -x user shared_comment /mnt/repo22-g4/shared-multi -getextattr -qq -x user shared_binary /mnt/repo22-g4/shared-multi -``` - -## Expected results - -The list contains exactly `shared_key`, `shared_comment`, and `shared_binary`, -without duplicates. Values are `shared-value\0exact`, `shared-comment`, and -bytes `00` through `0f`. Perform the standard unmount/md cleanup. diff --git a/tests/TC070-shared-trusted-xattr.md b/tests/TC070-shared-trusted-xattr.md deleted file mode 100644 index bab2cc6..0000000 --- a/tests/TC070-shared-trusted-xattr.md +++ /dev/null @@ -1,24 +0,0 @@ -# Test Case: Shared trusted xattr - -**Test ID**: TC070-shared-trusted-xattr -**Fixture**: `basic.erofs`, `/trusted-shared-a`, `/trusted-shared-b` - -## Objective - -Verify a shared Linux `trusted.*` entry through FreeBSD extattr semantics. -The transformer must record `e_name_index: 1 -> 4` for the shared record. - -## Manual steps - -```sh -lsextattr system /mnt/repo22-g4/trusted-shared-a -getextattr -qq -x system trusted.config /mnt/repo22-g4/trusted-shared-a -getextattr -qq -x system trusted.config /mnt/repo22-g4/trusted-shared-b -su -m nobody -c 'getextattr system trusted.config /mnt/repo22-g4/trusted-shared-a' -``` - -## Expected results - -Root receives `trusted-shared-value` from both files. FreeBSD exposes the full -`trusted.config` name in namespace `system`, not a namespace named `trusted`. -The unprivileged system-namespace request is denied. Clean up the mount/md. diff --git a/tests/TC071-shared-security-xattr.md b/tests/TC071-shared-security-xattr.md deleted file mode 100644 index 5f1ff67..0000000 --- a/tests/TC071-shared-security-xattr.md +++ /dev/null @@ -1,25 +0,0 @@ -# Test Case: Shared security xattr - -**Test ID**: TC071-shared-security-xattr -**Fixture**: `basic.erofs`, `/security-shared-a`, `/security-shared-b` - -## Objective - -Verify a shared Linux `security.*` entry mapped to FreeBSD namespace `system`. -The transformer must record `e_name_index: 1 -> 6`. - -## Manual steps - -```sh -lsextattr system /mnt/repo22-g4/security-shared-a -getextattr -qq -x system security.selinux /mnt/repo22-g4/security-shared-a -getextattr -qq -x system security.selinux /mnt/repo22-g4/security-shared-b -truss -o /tmp/tc071.truss getextattr -qq system security.missing \ - /mnt/repo22-g4/security-shared-a -grep extattr_get_file /tmp/tc071.truss -``` - -## Expected results - -Both values are exactly `system_u:object_r:shared_repo22_t:s0\0`; the missing -name returns `ENOATTR` (87). Unmount and detach. diff --git a/tests/TC072-shared-system-xattr-list.md b/tests/TC072-shared-system-xattr-list.md deleted file mode 100644 index d71e7f6..0000000 --- a/tests/TC072-shared-system-xattr-list.md +++ /dev/null @@ -1,26 +0,0 @@ -# Test Case: List the FreeBSD system namespace - -**Test ID**: TC072-shared-system-xattr-list -**Fixture**: `basic.erofs` - -## Objective - -Verify that trusted, security, and POSIX ACL indexes are listed only through -FreeBSD namespace `system` with their ABI names. - -## Manual steps - -```sh -lsextattr system /mnt/repo22-g4/trusted-shared-a -lsextattr system /mnt/repo22-g4/security-shared-a -lsextattr system /mnt/repo22-g4/acl-unordered -lsextattr user /mnt/repo22-g4/trusted-shared-a -stat -f '%HT %Sp' /mnt/repo22-g4/trusted-shared-a \ - /mnt/repo22-g4/security-shared-a /mnt/repo22-g4/acl-unordered -``` - -## Expected results - -The three system lists contain `trusted.config`, `security.selinux`, and -`posix_acl_access`, respectively. The trusted file's user list is empty. -Unmount and detach. diff --git a/tests/TC073-metabox-container.md b/tests/TC073-metabox-container.md deleted file mode 100644 index 56e8eff..0000000 --- a/tests/TC073-metabox-container.md +++ /dev/null @@ -1,30 +0,0 @@ -# Test Case: Plain metabox container - -**Test ID**: TC073-metabox-container -**Fixture**: `metabox-plain.erofs`, `/metabox-a`, `/metabox-b` - -## Objective - -Verify metadata and xattrs read from a plain regular metabox carrier. Before -guest use, require manifest fields `METABOX`, `sb_extslots=1`, relocated -`meta_blkaddr`, `metabox_nid`, bit-63 dirent NIDs, and image/source hashes. - -## Manual steps - -```sh -unit=$(mdconfig -a -t vnode -f /tmp/repo22-g4/images/metabox-plain.erofs) -mount -t erofs -o ro /dev/${unit} /mnt/repo22-g4 -stat -f 'mode=%Sp size=%z inode=%i' /mnt/repo22-g4/metabox-a \ - /mnt/repo22-g4/metabox-b -sha256 -q /mnt/repo22-g4/metabox-a /mnt/repo22-g4/metabox-b -lsextattr user /mnt/repo22-g4/metabox-a -getextattr -qq -x user metaboxshared /mnt/repo22-g4/metabox-a -getextattr -qq -x user metaboxshared /mnt/repo22-g4/metabox-b -umount /mnt/repo22-g4 -mdconfig -d -u "${unit#md}" -``` - -## Expected results - -The inode numbers retain bit 63; file hashes match the generated source; -`metaboxshared` is `nonzero-base` for both files. No mount/md remains. diff --git a/tests/TC074-inline-user-xattr-multiple.md b/tests/TC074-inline-user-xattr-multiple.md deleted file mode 100644 index dbd41d3..0000000 --- a/tests/TC074-inline-user-xattr-multiple.md +++ /dev/null @@ -1,24 +0,0 @@ -# Test Case: Multiple inline user xattrs - -**Test ID**: TC074-inline-user-xattr-multiple -**Fixture**: `basic.erofs`, `/inline-multi` - -## Objective - -Verify list/get across several inline user entries, including embedded NUL and -binary values. Qualify their inline offsets in `fixture-manifest.json` first. - -## Manual steps - -```sh -lsextattr user /mnt/repo22-g4/inline-multi -getextattr -qq -x user attr1 /mnt/repo22-g4/inline-multi -getextattr -qq -x user attr2 /mnt/repo22-g4/inline-multi -getextattr -qq -x user attr3 /mnt/repo22-g4/inline-multi -stat -f 'mode=%Sp size=%z' /mnt/repo22-g4/inline-multi -``` - -## Expected results - -Names are `attr1`, `attr2`, `attr3`; values are `one`, `two\0binary`, and -bytes `00` through `1f`. Unmount and detach. diff --git a/tests/TC075-inline-trusted-xattr.md b/tests/TC075-inline-trusted-xattr.md deleted file mode 100644 index 7056660..0000000 --- a/tests/TC075-inline-trusted-xattr.md +++ /dev/null @@ -1,24 +0,0 @@ -# Test Case: Inline trusted xattr - -**Test ID**: TC075-inline-trusted-xattr -**Fixture**: `basic.erofs`, `/inline-trusted` - -## Objective - -Verify an inline entry transformed to Linux index 4 and exposed through the -FreeBSD system namespace. - -## Manual steps - -```sh -lsextattr system /mnt/repo22-g4/inline-trusted -getextattr -qq -x system trusted.admin /mnt/repo22-g4/inline-trusted -truss -o /tmp/tc075.truss getextattr -qq user admin \ - /mnt/repo22-g4/inline-trusted -grep extattr_get_file /tmp/tc075.truss -``` - -## Expected results - -`trusted.admin` equals `trusted-inline-value`; the user-namespace lookup -returns `ENOATTR` (87). Perform standard cleanup. diff --git a/tests/TC076-inline-security-xattr.md b/tests/TC076-inline-security-xattr.md deleted file mode 100644 index 96f0e2a..0000000 --- a/tests/TC076-inline-security-xattr.md +++ /dev/null @@ -1,22 +0,0 @@ -# Test Case: Inline security xattrs - -**Test ID**: TC076-inline-security-xattr -**Fixture**: `basic.erofs`, `/inline-security` - -## Objective - -Verify inline Linux index 6 entries and binary-value preservation through -FreeBSD namespace `system`. - -## Manual steps - -```sh -lsextattr system /mnt/repo22-g4/inline-security -getextattr -qq -x system security.capability /mnt/repo22-g4/inline-security -getextattr -qq -x system security.selinux /mnt/repo22-g4/inline-security -``` - -## Expected results - -Capability bytes are `0100000200000000aabbccdd`; SELinux bytes are -`system_u:object_r:repo22_t:s0\0`. Unmount and detach. diff --git a/tests/TC077-long-prefix-user-xattr.md b/tests/TC077-long-prefix-user-xattr.md deleted file mode 100644 index f7f1db2..0000000 --- a/tests/TC077-long-prefix-user-xattr.md +++ /dev/null @@ -1,25 +0,0 @@ -# Test Case: Long-prefix user xattr - -**Test ID**: TC077-long-prefix-user-xattr -**Fixture**: `basic.erofs`, `/prefix-user-0`, `/prefix-user-1` - -## Objective - -Verify reconstruction of a long user name from packed prefix ID 0. The helper -must self-check prefix record base index 1 and infix -`repo22.application.component.`. - -## Manual steps - -```sh -lsextattr user /mnt/repo22-g4/prefix-user-0 -getextattr -qq -x user repo22.application.component.setting \ - /mnt/repo22-g4/prefix-user-0 -getextattr -qq -x user repo22.application.component.setting \ - /mnt/repo22-g4/prefix-user-1 -``` - -## Expected results - -The full name is listed once and the values are `prefix-value-0` and -`prefix-value-1`. Unmount and detach. diff --git a/tests/TC078-long-prefix-trusted-xattr.md b/tests/TC078-long-prefix-trusted-xattr.md deleted file mode 100644 index 1e9f0d1..0000000 --- a/tests/TC078-long-prefix-trusted-xattr.md +++ /dev/null @@ -1,24 +0,0 @@ -# Test Case: Long-prefix trusted xattr - -**Test ID**: TC078-long-prefix-trusted-xattr -**Fixture**: `basic.erofs`, `/prefix-trusted-0`, `/prefix-trusted-1` - -## Objective - -Verify packed long-prefix reconstruction after the transformer changes prefix -record 1 from user base index 1 to trusted base index 4. - -## Manual steps - -```sh -lsextattr system /mnt/repo22-g4/prefix-trusted-0 -getextattr -qq -x system trusted.repo22.trusted.deep.setting \ - /mnt/repo22-g4/prefix-trusted-0 -getextattr -qq -x system trusted.repo22.trusted.deep.setting \ - /mnt/repo22-g4/prefix-trusted-1 -``` - -## Expected results - -The full trusted name appears in FreeBSD namespace `system`; values are -`trusted-prefix-value-0` and `trusted-prefix-value-1`. Clean up. diff --git a/tests/TC079-packed-prefix-table.md b/tests/TC079-packed-prefix-table.md deleted file mode 100644 index 1ad6505..0000000 --- a/tests/TC079-packed-prefix-table.md +++ /dev/null @@ -1,33 +0,0 @@ -# Test Case: Packed prefix table - -**Test ID**: TC079-packed-prefix-table -**Fixture**: `basic.erofs` - -## Objective - -Verify a non-plain prefix table carried by the packed inode generated by -erofs-utils 1.8.6. - -## Layout qualification - -```sh -dump.erofs -s "$run/images/basic.erofs" -python3 tests/g4_fixtures.py verify --output "$run" -``` - -Require `xattr_prefix_count=2`, nonzero `packed_nid`, prefix start 0, user -record `(base=1, infix=repo22.application.component.)`, trusted record -`(base=4, infix=repo22.trusted.deep.)`, and matching image hash. - -## Guest observations - -```sh -getextattr -qq -x user repo22.application.component.setting \ - /mnt/repo22-g4/prefix-user-2 -getextattr -qq -x system trusted.repo22.trusted.deep.setting \ - /mnt/repo22-g4/prefix-trusted-2 -stat -f 'mode=%Sp inode=%i' /mnt/repo22-g4/prefix-user-2 \ - /mnt/repo22-g4/prefix-trusted-2 -``` - -Both values must end in `-2`; cleanup must be empty. diff --git a/tests/TC080-prefix-table-lookup.md b/tests/TC080-prefix-table-lookup.md deleted file mode 100644 index 39e1a12..0000000 --- a/tests/TC080-prefix-table-lookup.md +++ /dev/null @@ -1,25 +0,0 @@ -# Test Case: Prefix table lookup - -**Test ID**: TC080-prefix-table-lookup -**Fixture**: `basic.erofs`, `/prefix-user-0` through `/prefix-user-3` - -## Objective - -Verify repeated prefix-ID lookup and a clean missing-suffix result. - -## Manual steps - -Run the following lookup separately for suffix files 0, 1, 2, and 3: - -```sh -getextattr -qq -x user repo22.application.component.setting \ - /mnt/repo22-g4/prefix-user-N -truss -o /tmp/tc080.truss getextattr -qq user \ - repo22.application.component.missing /mnt/repo22-g4/prefix-user-0 -grep extattr_get_file /tmp/tc080.truss -``` - -## Expected results - -Values are `prefix-value-N` for each file; the missing suffix is `ENOATTR` -(87), not an integrity failure. Unmount and detach. diff --git a/tests/TC081-metabox-backed-xattr.md b/tests/TC081-metabox-backed-xattr.md deleted file mode 100644 index 0f8c7e6..0000000 --- a/tests/TC081-metabox-backed-xattr.md +++ /dev/null @@ -1,26 +0,0 @@ -# Test Case: Metabox-backed shared xattrs - -**Test ID**: TC081-metabox-backed-xattr -**Fixture**: `metabox-plain.erofs`, `/metabox-a`, `/metabox-b` - -## Objective - -Verify shared entries, inline entries, and long-prefix shared entries from a -metabox logical stream. Require `SHARED_EA_IN_METABOX`, `xattr_blkaddr=1`, -carrier size/bounds, shared IDs, and prefix fields in the generator manifest. - -## Manual steps - -```sh -lsextattr user /mnt/repo22-g4/metabox-a -getextattr -qq -x user metaboxshared /mnt/repo22-g4/metabox-a -getextattr -qq -x user metaboxshared /mnt/repo22-g4/metabox-b -getextattr -qq -x user shared-prefix-key /mnt/repo22-g4/metabox-a -getextattr -qq -x user repo22.application.component.setting \ - /mnt/repo22-g4/metabox-a -``` - -## Expected results - -Values are `nonzero-base`, `nonzero-base`, `shared-value`, and -`long-prefix-value`; all names list once. Clean up. diff --git a/tests/TC082-system-namespace-subset.md b/tests/TC082-system-namespace-subset.md deleted file mode 100644 index 301948e..0000000 --- a/tests/TC082-system-namespace-subset.md +++ /dev/null @@ -1,25 +0,0 @@ -# Test Case: FreeBSD system namespace subset - -**Test ID**: TC082-system-namespace-subset -**Fixture**: `basic.erofs`, `/acl-unordered` - -## Objective - -Verify POSIX ACL exposure in FreeBSD namespace `system` and isolation from -namespace `user`. - -## Manual steps - -```sh -lsextattr system /mnt/repo22-g4/acl-unordered -getextattr -qq -x system posix_acl_access /mnt/repo22-g4/acl-unordered -getfacl -n /mnt/repo22-g4/acl-unordered -truss -o /tmp/tc082.truss getextattr -qq user posix_acl_access \ - /mnt/repo22-g4/acl-unordered -grep extattr_get_file /tmp/tc082.truss -``` - -## Expected results - -Raw ACL starts with little-endian version 2; `getfacl` shows named users 3002 -then 2002. The user-namespace request returns `ENOATTR` (87). Clean up. diff --git a/tests/TC083-user-namespace-subset.md b/tests/TC083-user-namespace-subset.md deleted file mode 100644 index 2b16878..0000000 --- a/tests/TC083-user-namespace-subset.md +++ /dev/null @@ -1,25 +0,0 @@ -# Test Case: User namespace subset - -**Test ID**: TC083-user-namespace-subset -**Fixture**: `basic.erofs`, `/user-subset` - -## Objective - -Verify several application user xattrs without exposing them through FreeBSD -namespace `system`. - -## Manual steps - -```sh -lsextattr user /mnt/repo22-g4/user-subset -getextattr -qq -x user comment /mnt/repo22-g4/user-subset -getextattr -qq -x user author /mnt/repo22-g4/user-subset -getextattr -qq -x user checksum /mnt/repo22-g4/user-subset -getextattr -qq -x user com.repo22.app.setting /mnt/repo22-g4/user-subset -lsextattr system /mnt/repo22-g4/user-subset -``` - -## Expected results - -The user values are `subset-comment`, `repo22`, -`sha256:0123456789abcdef`, and `enabled`. The system list is empty. Clean up. diff --git a/tests/TC084-lz4-basic.md b/tests/TC084-lz4-basic.md deleted file mode 100644 index 5aee0e8..0000000 --- a/tests/TC084-lz4-basic.md +++ /dev/null @@ -1,71 +0,0 @@ -# Test Case: LZ4 Compression Basic Support - -**Test ID**: TC084-lz4-basic - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression -**Priority**: Critical -**Regression**: None - -## Objective -Verify basic LZ4 compression support for reading compressed files. - -## Preconditions -- EROFS image with LZ4 compression: `test-lz4-basic.erofs` -- Test file: `/files/test-lz4.txt` (1MB, highly compressible) -- Mount point: `/mnt/test` - -## Test Steps -1. Mount the LZ4-compressed image: - ``` - mount -t erofs /dev/md0 /mnt/test - ``` - -2. Read entire compressed file: - ``` - cat /mnt/test/files/test-lz4.txt > /tmp/output.txt - ``` - -3. Verify file size: - ``` - stat -f %z /mnt/test/files/test-lz4.txt - ``` - -4. Compare with reference (uncompressed): - ``` - cmp /tmp/output.txt /tmp/reference.txt - ``` - -5. Test multiple LZ4-compressed files: - ``` - for f in /mnt/test/files/lz4-*.txt; do - cat "$f" > /dev/null - done - ``` - -## Expected Results -- Step 1: Mount succeeds -- Step 2: File read successfully, decompressed transparently -- Step 3: Size matches original uncompressed size (1048576 bytes) -- Step 4: Files identical (cmp returns 0) -- Step 5: All LZ4 files read without errors - -## Verification Method -- Verify decompression transparent to application -- Confirm data integrity after decompression -- Test LZ4 decompression performance acceptable -- Verify compressed file sizes smaller than uncompressed - -## Cleanup -``` -umount /mnt/test -mdconfig -d -u md0 -rm /tmp/output.txt -``` - -## Notes -- LZ4 provides fast decompression with moderate compression ratio -- EROFS uses LZ4 for general-purpose compression -- Related tests: TC003 (LZ4 compressed read), TC085 (large files) diff --git a/tests/TC085-lz4-large-file.md b/tests/TC085-lz4-large-file.md deleted file mode 100644 index 59368e8..0000000 --- a/tests/TC085-lz4-large-file.md +++ /dev/null @@ -1,78 +0,0 @@ -# Test Case: LZ4 Compression Large File Handling - -**Test ID**: TC085-lz4-large-file - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression -**Priority**: High -**Regression**: None - -## Objective -Verify LZ4 decompression correctness for large files (>100MB). - -## Preconditions -- Generated image: `images/lz4-large.erofs` -- Test file: `/large.bin`, exactly 268435456 bytes -- Reference: `sources/large/large.bin` and its generated SHA256 -- Mount point: `/mnt/test` - -## Test Steps -1. Mount the image: - ``` - mount -t erofs /dev/md0 /mnt/test - ``` - -2. Verify file size reported correctly: - ``` - stat -f %z /mnt/test/files/large-lz4.bin - ``` - -3. Read entire file and compute checksum: - ``` - sha256 -q /mnt/test/large.bin - sha256 -q sources/large/large.bin - cmp sources/large/large.bin /mnt/test/large.bin - ``` - -4. Test partial reads at various offsets: - ``` - read_probe pread /mnt/test/large.bin 52428800 10485760 guest.bin - read_probe pread sources/large/large.bin 52428800 10485760 source.bin - cmp source.bin guest.bin - read_probe pread /mnt/test/large.bin 209715200 10485760 guest.bin - read_probe pread sources/large/large.bin 209715200 10485760 source.bin - cmp source.bin guest.bin - ``` - -5. Test random access reads: - ``` - # Compare both guest ranges with the same source offsets. - read_probe pread /mnt/test/large.bin 4096000 4096 chunk1 - read_probe pread /mnt/test/large.bin 204800000 4096 chunk2 - ``` - -## Expected Results -- Step 2: Size is 268435456 bytes (256MB) -- Step 3: Checksum matches reference value -- Step 4: Partial reads succeed, no errors -- Step 5: Random access works correctly - -## Verification Method -- Verify data integrity via checksum -- Confirm partial/random reads decompress correctly -- Test memory usage during decompression reasonable -- Verify no memory leaks or buffer overflows - -## Cleanup -``` -umount /mnt/test -mdconfig -d -u "${unit#md}" -rm /tmp/chunk1 /tmp/chunk2 -``` - -## Notes -- Large file decompression tests pcluster handling -- LZ4 decompression should be streaming (low memory) -- Related tests: TC089 (pcluster mapping) diff --git a/tests/TC086-lz4-sequential-read.md b/tests/TC086-lz4-sequential-read.md deleted file mode 100644 index 3a0c47f..0000000 --- a/tests/TC086-lz4-sequential-read.md +++ /dev/null @@ -1,57 +0,0 @@ -# Test Case: LZ4 Sequential Read Correctness - -**Test ID**: TC086-lz4-sequential-read - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression -**Priority**: High - -## Objective - -Verify complete sequential reads of an LZ4-compressed file with two request -sizes against the original source bytes. Record timings only; repo22 exposes no -filesystem-specific DTrace provider and this test has no fixed throughput -threshold. - -## Fixture - -```sh -work=$(mktemp -d /tmp/repo22-tc086.XXXXXX) -tests/prepare_error_fixtures.sh "$work/fixtures" -dump.erofs -s "$work/fixtures/valid-lz4.erofs" -dump.erofs --path=/compressed.bin -e "$work/fixtures/valid-lz4.erofs" -``` - -The dump must identify `/compressed.bin` as compressed LZ4 data with real -physical extents. - -## FreeBSD Procedure - -```sh -mkdir "$work/mnt" -unit=$(mdconfig -a -t vnode -f "$work/fixtures/valid-lz4.erofs") -mount -t erofs -o ro "/dev/$unit" "$work/mnt" - -expected=$(sha256 -q "$work/fixtures/source/compressed.bin") -actual=$(sha256 -q "$work/mnt/compressed.bin") -test "$actual" = "$expected" - -/usr/bin/time -p dd if="$work/mnt/compressed.bin" \ - of="$work/read-4k.bin" bs=4096 2>"$work/time-4k.txt" -cmp "$work/fixtures/source/compressed.bin" "$work/read-4k.bin" -/usr/bin/time -p dd if="$work/mnt/compressed.bin" \ - of="$work/read-1m.bin" bs=1048576 2>"$work/time-1m.txt" -cmp "$work/fixtures/source/compressed.bin" "$work/read-1m.bin" -cat "$work/time-4k.txt" "$work/time-1m.txt" - -umount "$work/mnt" -mdconfig -d -u "${unit#md}" -``` - -## Expected Results - -Both complete outputs compare byte-for-byte with the source and both hashes -match. Timing values are recorded with guest CPU, VM, and storage context; one -buffer size is not required to outperform the other. diff --git a/tests/TC087-lz4-random-read.md b/tests/TC087-lz4-random-read.md deleted file mode 100644 index c4275bf..0000000 --- a/tests/TC087-lz4-random-read.md +++ /dev/null @@ -1,53 +0,0 @@ -# Test Case: LZ4 Deterministic Random-Offset Reads - -**Test ID**: TC087-lz4-random-read - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression -**Priority**: High - -## Objective - -Verify non-sequential reads from deterministic offsets in an LZ4-compressed -file and compare every returned byte with the source. `$RANDOM` and timing-only -checks are not evidence of data correctness. - -## Fixture and Helper - -```sh -work=$(mktemp -d /tmp/repo22-tc087.XXXXXX) -tests/prepare_error_fixtures.sh "$work/fixtures" -cc -O2 -Wall -Wextra -std=c17 tests/read_probe.c -o "$work/read_probe" -dump.erofs --path=/compressed.bin -e "$work/fixtures/valid-lz4.erofs" -``` - -## FreeBSD Procedure - -```sh -mkdir "$work/mnt" -unit=$(mdconfig -a -t vnode -f "$work/fixtures/valid-lz4.erofs") -mount -t erofs -o ro "/dev/$unit" "$work/mnt" -size=$(stat -f %z "$work/fixtures/source/compressed.bin") -test "$size" -gt 1052672 - -index=0 -for offset in 0 4096 65536 1048576 $((size - 4096)); do - "$work/read_probe" pread "$work/mnt/compressed.bin" \ - "$offset" 4096 "$work/guest-$index.bin" - "$work/read_probe" pread "$work/fixtures/source/compressed.bin" \ - "$offset" 4096 "$work/source-$index.bin" - cmp "$work/source-$index.bin" "$work/guest-$index.bin" - index=$((index + 1)) -done - -umount "$work/mnt" -mdconfig -d -u "${unit#md}" -``` - -## Expected Results - -All five fixed reads, including the final 4 KiB of the file, return exactly -4096 bytes and compare equal to their corresponding source ranges. No -performance ordering is asserted. diff --git a/tests/TC088-lz4-config-handling.md b/tests/TC088-lz4-config-handling.md deleted file mode 100644 index 2027c01..0000000 --- a/tests/TC088-lz4-config-handling.md +++ /dev/null @@ -1,85 +0,0 @@ -# Test Case: LZ4 Compression Configuration Handling - -**Test ID**: TC088-lz4-config-handling - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression -**Priority**: Medium -**Regression**: None - -## Objective -Verify correct handling of different LZ4 compression configurations. - -## Preconditions -- Multiple EROFS images with different LZ4 configs: - - `lz4-pcluster-4k.erofs` (4KB pcluster) - - `lz4-pcluster-64k.erofs` (64KB pcluster) - - `lz4-pcluster-256k.erofs` (256KB pcluster) -- Test file in each: `/files/test.dat` (identical content) -- Mount points: `/mnt/test1`, `/mnt/test2`, `/mnt/test3` -- Generate the images explicitly with `mkfs.erofs 1.8.6`: - ``` - mkfs.erofs -zlz4 -C4096 lz4-pcluster-4k.erofs source - mkfs.erofs -zlz4 -C65536 lz4-pcluster-64k.erofs source - mkfs.erofs -zlz4 -C262144 lz4-pcluster-256k.erofs source - ``` -- Confirm the requested feature/configuration with `dump.erofs -s` before - treating an image as a valid fixture. - -## Test Steps -1. Mount all three images: - ``` - mount -t erofs /dev/md0 /mnt/test1 - mount -t erofs /dev/md1 /mnt/test2 - mount -t erofs /dev/md2 /mnt/test3 - ``` - -2. Read from each and verify identical output: - ``` - sha256 /mnt/test1/files/test.dat - sha256 /mnt/test2/files/test.dat - sha256 /mnt/test3/files/test.dat - ``` - -3. Record image sizes without assuming a monotonic compression ratio: - ``` - stat -f %z /root/lz4-pcluster-4k.erofs - stat -f %z /root/lz4-pcluster-64k.erofs - stat -f %z /root/lz4-pcluster-256k.erofs - ``` - -4. Verify the same offset read with each pcluster size. Timing is diagnostic - only and is not a functional pass criterion: - ``` - time dd if=/mnt/test1/files/test.dat of=/dev/null bs=4K skip=100 count=10 - time dd if=/mnt/test2/files/test.dat of=/dev/null bs=4K skip=100 count=10 - time dd if=/mnt/test3/files/test.dat of=/dev/null bs=4K skip=100 count=10 - ``` - -## Expected Results -- Step 1: All mounts succeed -- Step 2: All three checksums identical (data integrity) -- Step 3: Image sizes are recorded for the exact corpus and mkfs version -- Step 4: All reads succeed and return identical data at the tested offset - -## Verification Method -- Verify all pcluster configurations work correctly -- Record pcluster size effects without assuming they are monotonic for every - corpus or under a QEMU TCG guest -- Test driver handles different configs transparently - -## Cleanup -``` -umount /mnt/test1 /mnt/test2 /mnt/test3 -mdconfig -d -u "${unit1#md}" -mdconfig -d -u "${unit2#md}" -mdconfig -d -u "${unit3#md}" -``` - -## Notes -- Pcluster size is compression unit (physical cluster) -- Larger pcluster -> better compression, worse random access -- Typical sizes: 4KB, 16KB, 64KB, 256KB -- Related tests: TC089 (pcluster mapping) diff --git a/tests/TC089-lz4-pcluster-4k.md b/tests/TC089-lz4-pcluster-4k.md deleted file mode 100644 index 43e1db5..0000000 --- a/tests/TC089-lz4-pcluster-4k.md +++ /dev/null @@ -1,69 +0,0 @@ -# Test Case: LZ4 Compressed Physical Cluster Mapping (4KB) - -**Test ID**: TC089-lz4-pcluster-4k - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression -**Priority**: High -**Regression**: None - -## Objective -Verify correct physical cluster (pcluster) mapping for LZ4 compression with 4KB pcluster size. - -## Preconditions -- EROFS image with LZ4, 4KB pcluster: `test-lz4-pcluster-4k.erofs` -- Test file: `/files/test-4k.dat` (aligned to 4KB boundaries) -- Mount point: `/mnt/test` - -## Test Steps -1. Mount the image: - ``` - mount -t erofs /dev/md0 /mnt/test - ``` - -2. Read aligned to pcluster boundary (offset 0): - ``` - dd if=/mnt/test/files/test-4k.dat of=/tmp/out1 bs=4K count=1 skip=0 - ``` - -3. Read aligned to pcluster boundary (offset 4K): - ``` - dd if=/mnt/test/files/test-4k.dat of=/tmp/out2 bs=4K count=1 skip=1 - ``` - -4. Read unaligned (crosses pcluster boundary): - ``` - dd if=/mnt/test/files/test-4k.dat of=/tmp/out3 bs=2K count=2 skip=1 - ``` - -5. Read spanning multiple pclusters: - ``` - dd if=/mnt/test/files/test-4k.dat of=/tmp/out4 bs=16K count=1 - ``` - -## Expected Results -- Step 2: Single pcluster decompressed, correct data -- Step 3: Adjacent pcluster decompressed, correct data -- Step 4: Two pclusters decompressed for unaligned read -- Step 5: Four pclusters decompressed (16K / 4K = 4) - -## Verification Method -- Verify pcluster boundaries handled correctly -- Confirm unaligned reads work (may decompress extra pclusters) -- Test each pcluster independently decompressible -- Verify correct data returned for all read patterns - -## Cleanup -``` -umount /mnt/test -mdconfig -d -u md0 -rm /tmp/out1 /tmp/out2 /tmp/out3 /tmp/out4 -``` - -## Notes -- 4KB pcluster provides good random access performance -- Each 4KB logical region compressed independently -- Small pcluster = less compression, better random access -- Related tests: TC090 (64KB pcluster) diff --git a/tests/TC090-lz4-pcluster-64k.md b/tests/TC090-lz4-pcluster-64k.md deleted file mode 100644 index 6094070..0000000 --- a/tests/TC090-lz4-pcluster-64k.md +++ /dev/null @@ -1,72 +0,0 @@ -# Test Case: LZ4 Compressed Physical Cluster Mapping (64KB) - -**Test ID**: TC090-lz4-pcluster-64k - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression -**Priority**: High -**Regression**: None - -## Objective -Verify correct physical cluster mapping for LZ4 compression with 64KB pcluster size. - -## Preconditions -- EROFS image with LZ4, 64KB pcluster: `test-lz4-pcluster-64k.erofs` -- Test file: `/files/test-64k.dat` (1MB, highly compressible) -- Mount point: `/mnt/test` -- Generate with `mkfs.erofs -zlz4 -C65536 test-lz4-pcluster-64k.erofs source` - and confirm `compr_cfgs big_pcluster` with `dump.erofs -s`. - -## Test Steps -1. Mount the image: - ``` - mount -t erofs /dev/md0 /mnt/test - ``` - -2. Read first pcluster (64KB): - ``` - dd if=/mnt/test/files/test-64k.dat of=/tmp/out1 bs=64K count=1 - ``` - -3. Read small portion requiring full pcluster decompression: - ``` - dd if=/mnt/test/files/test-64k.dat of=/tmp/out2 bs=4K count=1 skip=1 - ``` - -4. Read crossing pcluster boundary: - ``` - dd if=/mnt/test/files/test-64k.dat of=/tmp/cross bs=1 skip=65504 count=64 - dd if=/root/reference/test-64k.dat of=/tmp/cross.expected bs=1 skip=65504 count=64 - cmp /tmp/cross /tmp/cross.expected - ``` - -5. Record the physical extent layout: - ``` - dump.erofs --path=/files/test-64k.dat -e test-lz4-pcluster-64k.erofs - ``` - -## Expected Results -- Step 2: Full 64KB pcluster decompressed correctly -- Step 3: Reading 4KB requires decompressing entire 64KB pcluster -- Step 4: Boundary crossing handled correctly -- Step 5: The fixture contains a real compressed big-pcluster extent - -## Verification Method -- Verify large pcluster decompression works -- Confirm small reads still trigger full pcluster decompression -- Test memory usage reasonable for 64KB decompression -- Do not require a better compression ratio than 4KB for every corpus - -## Cleanup -``` -umount /mnt/test -mdconfig -d -u md0 -rm /tmp/out* /tmp/cross /tmp/cross.expected -``` - -## Notes -- 64KB pcluster provides better compression at cost of random access -- Common pcluster size for read-heavy workloads -- Related tests: TC089 (4KB pcluster), TC088 (config handling) diff --git a/tests/TC091-ztailpacking-basic.md b/tests/TC091-ztailpacking-basic.md deleted file mode 100644 index 1e8be76..0000000 --- a/tests/TC091-ztailpacking-basic.md +++ /dev/null @@ -1,76 +0,0 @@ -# Test Case: LZ4 ztailpacking Data Path - -**Test ID**: TC091-ztailpacking-basic - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression -**Priority**: High -**Regression**: None - -## Objective -Verify correct operation of ztailpacking (compressed tail packing) for LZ4-compressed files. - -## Preconditions -- EROFS image with ztailpacking enabled: `test-ztailpacking.erofs` -- Test file: `/inline.dat` (64KB deterministic compressible data) -- Mount point: `/mnt/test` -- Generate with: - ``` - mkfs.erofs -zlz4 -C4096 -Eztailpacking test-ztailpacking.erofs source - ``` -- Before mounting, require `dump.erofs -s` to report `ztailpacking` and - `dump.erofs --path=/inline.dat -e` to show the compressed physical extent - inside the metadata block. Enabling the mkfs option alone does not prove - that the target file was tail-packed. - -## Test Steps -1. Mount the image: - ``` - mount -t erofs /dev/md0 /mnt/test - ``` - -2. Read file with packed tail: - ``` - cat /mnt/test/inline.dat > /tmp/output.dat - ``` - -3. Verify file size correct: - ``` - stat -f %z /mnt/test/inline.dat - ``` - -4. Read file tail specifically: - ``` - tail -c 1024 /mnt/test/inline.dat > /tmp/tail.dat - ``` - -5. Verify data integrity: - ``` - sha256 /tmp/output.dat - ``` - -## Expected Results -- Step 2: File read successfully with tail decompressed -- Step 3: Size matches original uncompressed size -- Step 4: Tail data correct -- Step 5: Checksum matches reference - -## Verification Method -- Verify tail-packed data decompressed correctly -- Confirm storage efficiency (tail stored inline or in special area) -- Test tail packing transparent to reader -- Verify boundary between main data and packed tail handled correctly - -## Cleanup -``` -umount /mnt/test -mdconfig -d -u md0 -rm /tmp/output.dat /tmp/tail.dat -``` - -## Notes -- ztailpacking stores compressed file tail inline with inode or in special area -- Reduces fragmentation and improves small file efficiency -- Related tests: TC092 (edge cases), TC033 (uncompressed tailpacking) diff --git a/tests/TC092-ztailpacking-edge.md b/tests/TC092-ztailpacking-edge.md deleted file mode 100644 index c6563c5..0000000 --- a/tests/TC092-ztailpacking-edge.md +++ /dev/null @@ -1,79 +0,0 @@ -# Test Case: LZ4 ztailpacking Edge Cases - -**Test ID**: TC092-ztailpacking-edge - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression -**Priority**: Medium -**Regression**: None - -## Objective -Verify correct handling of edge cases in ztailpacking (compressed tail packing) feature. - -## Preconditions -- Generated image: `images/lz4-ztail.erofs` -- Test files: - - `/exact-pcluster.dat` (4096-byte non-tail control) - - `/one-byte-tail.dat` (4097-byte logical boundary case) - - `/max-tail.dat` (8191-byte logical boundary case) - - `/zero-tail.dat` (empty-file control) -- `/inline.dat` is independently proven by the manifest to have - `Z_EROFS_ADVISE_INLINE_PCLUSTER` and nonzero `h_idata_size`. The four edge - controls must not be described as ztailpacked unless their own map headers - prove it. -- Mount point: `/mnt/test` - -## Test Steps -1. Mount the image: - ``` - mount -t erofs /dev/md0 /mnt/test - ``` - -2. Read file with exact pcluster size (no tail): - ``` - cat /mnt/test/files/exact-pcluster.dat > /tmp/out1 - sha256sum /tmp/out1 - ``` - -3. Read file with minimal tail (1 byte): - ``` - cat /mnt/test/files/one-byte-tail.dat > /tmp/out2 - tail -c 1 /tmp/out2 | od -A n -t x1 - ``` - -4. Read file with maximum tail size: - ``` - cat /mnt/test/files/max-tail.dat > /tmp/out3 - stat -f %z /tmp/out3 - ``` - -5. Read file with zero-length tail: - ``` - cat /mnt/test/files/zero-tail.dat > /tmp/out4 - ``` - -## Expected Results -- Step 2: File without tail read correctly -- Steps 2-5: Every exact logical size, full SHA256, and byte comparison matches - its source file - -## Verification Method -- Verify all tail size edge cases handled -- Confirm boundary conditions don't cause errors -- Test tail packing storage limits enforced correctly -- Verify data integrity for all edge cases - -## Cleanup -``` -umount /mnt/test -mdconfig -d -u "${unit#md}" -rm /tmp/out* -``` - -## Notes -- Tail size limits depend on inline storage capacity -- Zero-length tail is valid edge case -- Exact pcluster alignment means no tail needed -- Related tests: TC091 (basic ztailpacking) diff --git a/tests/TC093-chunk-single-device.md b/tests/TC093-chunk-single-device.md deleted file mode 100644 index 028f9b4..0000000 --- a/tests/TC093-chunk-single-device.md +++ /dev/null @@ -1,55 +0,0 @@ -# Test Case: Indexed Chunk Data on the Primary Device - -**Test ID**: TC093-chunk-single-device -**Category**: Chunk-Based -**Priority**: High - -## Objective - -Verify 8-byte chunk indexes whose device ID is zero without a device table. -This is not a 4-byte block-map substitute. - -## Fixture - -Use `single-indexed.erofs` from the fresh G6 generator. It is derived from the -mkfs blob baseline by asserting each original `(high=0, device=1, pblk)`, -folding the new blob bytes behind the metadata block, changing every index to -`device=0`, adding one to each pblk, and removing the device-table feature. -`verify` must report `extra_devices=0` and `entry_size=8`. - -## Manual procedure - -```sh -I=/root/repo22-g6/images -S=/root/repo22-g6/sources/chunk -mkdir -p /mnt/g6 -kldload /root/repo22-g6/erofs.ko -mdconfig -a -t vnode -f "$I/single-indexed.erofs" -u 90 -mount -t erofs -o ro /dev/md90 /mnt/g6 -sha256 "$S/indexed.bin" /mnt/g6/indexed.bin -cmp "$S/indexed.bin" /mnt/g6/indexed.bin -dd if="$S/indexed.bin" of=/tmp/tc093.expected bs=1 skip=28672 count=8192 2>/dev/null -dd if=/mnt/g6/indexed.bin of=/tmp/tc093.actual bs=1 skip=28672 count=8192 2>/dev/null -cmp /tmp/tc093.expected /tmp/tc093.actual -``` - -## Expected results - -- Mount succeeds without any `device.N` option. -- Full-file and cross-32768-byte-chunk comparisons pass. -- No external GEOM provider or device-table lookup is attempted. - -## Cleanup and zero-state check - -```sh -umount /mnt/g6 -mdconfig -d -u 90 -kldunload erofs -rm -f /tmp/tc093.expected /tmp/tc093.actual -mount -p | awk '$3 == "erofs" { print }' -mdconfig -l -kldstat -n erofs 2>/dev/null || true -sysctl -n kern.geom.conftxt | grep -E 'md90|erofs' || true -``` - -All four final outputs must be empty. diff --git a/tests/TC094-chunk-multi-device.md b/tests/TC094-chunk-multi-device.md deleted file mode 100644 index 8ad6f70..0000000 --- a/tests/TC094-chunk-multi-device.md +++ /dev/null @@ -1,116 +0,0 @@ -# Test Case: Chunk Indexes and Compressed Pcluster on External Slots - -**Test ID**: TC094-chunk-multi-device -**Category**: Chunk-Based / Multi-Device -**Priority**: Critical - -## Objective - -Verify alternating external chunk slots, a nonzero device ID with -`uniaddr=0`, provider identity, and a real external two-block LZ4 pcluster. - -## Fixture qualification - -Use the fresh G6 artifacts and manifest: - -- `multi2-*`: `/cross-device.bin` has three 131072-byte chunks with device IDs - `1,2,1`; slot blocks differ, so provider swapping fails deterministically. -- `multi2-uniaddr0-*`: slot 1 has `uniaddr=0`, but indexes retain device ID 1. -- `lz4-external-pcluster-*`: the original fixed 8192-byte LZ4 pcluster is - fsck-qualified before relocation. The generated primary is only 8192 bytes, - contains metadata plus the table, and has HEAD pblk `2`; both compressed - blocks exist only in slot 1. - -## Alternating slots and provider identity - -```sh -I=/root/repo22-g6/images -S=/root/repo22-g6/sources/chunk -mkdir -p /mnt/g6 -kldload /root/repo22-g6/erofs.ko -mdconfig -a -t vnode -f "$I/multi2-primary.erofs" -u 90 -mdconfig -a -t vnode -f "$I/multi2-slot1.blob" -u 91 -mdconfig -a -t vnode -f "$I/multi2-slot2.blob" -u 92 -mount -t erofs -o ro -o device.2=/dev/md92 -o device.1=/dev/md91 \ - /dev/md90 /mnt/g6 -cmp "$S/cross-device.bin" /mnt/g6/cross-device.bin -sha256 "$S/cross-device.bin" /mnt/g6/cross-device.bin -dd if="$S/cross-device.bin" of=/tmp/tc094.expected bs=1 skip=126976 count=8192 2>/dev/null -dd if=/mnt/g6/cross-device.bin of=/tmp/tc094.actual bs=1 skip=126976 count=8192 2>/dev/null -cmp /tmp/tc094.expected /tmp/tc094.actual -for off in 0 131072 262144; do - dd if="$S/cross-device.bin" of=/tmp/tc094.src.$off bs=1 skip=$off count=4096 2>/dev/null - dd if=/mnt/g6/cross-device.bin of=/tmp/tc094.mnt.$off bs=1 skip=$off count=4096 2>/dev/null - cmp /tmp/tc094.src.$off /tmp/tc094.mnt.$off -done -umount /mnt/g6 -mdconfig -d -u 92 -mdconfig -d -u 91 -mdconfig -d -u 90 -``` - -Confirm swapped providers fail with `ENXIO`: - -```sh -mdconfig -a -t vnode -f "$I/multi2-primary.erofs" -u 90 -mdconfig -a -t vnode -f "$I/multi2-slot1.blob" -u 91 -mdconfig -a -t vnode -f "$I/multi2-slot2.blob" -u 92 -truss -f -o /tmp/tc094-swap.truss mount -t erofs -o ro \ - -o device.1=/dev/md92 -o device.2=/dev/md91 /dev/md90 /mnt/g6 -tail -20 /tmp/tc094-swap.truss -mdconfig -d -u 92 -mdconfig -d -u 91 -mdconfig -d -u 90 -``` - -## Zero unified address with explicit device ID - -```sh -mdconfig -a -t vnode -f "$I/multi2-uniaddr0-primary.erofs" -u 90 -mdconfig -a -t vnode -f "$I/multi2-uniaddr0-slot1.blob" -u 91 -mdconfig -a -t vnode -f "$I/multi2-uniaddr0-slot2.blob" -u 92 -mount -t erofs -o ro -o device.2=/dev/md92 -o device.1=/dev/md91 \ - /dev/md90 /mnt/g6 -cmp "$S/cross-device.bin" /mnt/g6/cross-device.bin -umount /mnt/g6 -mdconfig -d -u 92 -mdconfig -d -u 91 -mdconfig -d -u 90 -``` - -## External compressed pcluster - -```sh -P=/root/repo22-g6/sources/pcluster -mdconfig -a -t vnode -f "$I/lz4-external-pcluster-primary.erofs" -u 90 -mdconfig -a -t vnode -f "$I/lz4-external-pcluster-slot1.blob" -u 91 -mount -t erofs -o ro -o device.1=/dev/md91 /dev/md90 /mnt/g6 -stat -f '%z' /mnt/g6/external-pcluster.bin -sha256 "$P/external-pcluster.bin" /mnt/g6/external-pcluster.bin -cmp "$P/external-pcluster.bin" /mnt/g6/external-pcluster.bin -dd if="$P/external-pcluster.bin" of=/tmp/tc094-p.src bs=1 skip=61440 count=8192 2>/dev/null -dd if=/mnt/g6/external-pcluster.bin of=/tmp/tc094-p.mnt bs=1 skip=61440 count=8192 2>/dev/null -cmp /tmp/tc094-p.src /tmp/tc094-p.mnt -truss -f -o /tmp/tc094-p-ebusy.truss mdconfig -d -u 91 -tail -20 /tmp/tc094-p-ebusy.truss -``` - -The 131072-byte output must match although the primary has no old compressed -extent; normal slot detach must return `EBUSY`. - -## Cleanup and zero-state check - -```sh -umount /mnt/g6 -mdconfig -d -u 91 -mdconfig -d -u 90 -kldunload erofs -rm -f /tmp/tc094.expected /tmp/tc094.actual /tmp/tc094.src.* \ - /tmp/tc094.mnt.* /tmp/tc094-p.src /tmp/tc094-p.mnt -mount -p | awk '$3 == "erofs" { print }' -mdconfig -l -kldstat -n erofs 2>/dev/null || true -sysctl -n kern.geom.conftxt | grep -E 'md9[0-2]|erofs' || true -``` - -All four final outputs must be empty, with no panic, trap, or hang. diff --git a/tests/TC095-chunk-index-read.md b/tests/TC095-chunk-index-read.md deleted file mode 100644 index b6b5e3c..0000000 --- a/tests/TC095-chunk-index-read.md +++ /dev/null @@ -1,113 +0,0 @@ -# Test Case: Chunk Index Decode and Mapped Device ID - -**Test ID**: TC095-chunk-index-read -**Category**: Chunk-Based -**Priority**: High - -## Objective - -Verify first/middle/last 8-byte indexes, zero-high and nonzero-high 48-bit -decode, the rounded device-ID mask, and `ENODEV` for mapped ID 3. - -## Field qualification - -After G6 generation, print the fields on the host that will be exercised: - -```sh -OUTPUT=/path/to/generated-fixtures -python3 -B - "$OUTPUT/manifest.json" <<'PY' -import json, sys -m = json.load(open(sys.argv[1])) -for fixture in ('multi2', 'multi2-unified48', 'bad-mapped-device3'): - c = m['fixtures'].get(fixture, {}).get('image', {}).get('chunks', {}) - if '/indexed.bin' in c: - print(fixture, c['/indexed.bin']) -print(m['mutations']) -PY -``` - -The normal `indexed.bin` has five 32768-byte indexes with IDs `1,2,1,2,1`. -The 48-bit control has format bit `0x40` with zero high words; its unified -target also contains a real nonzero high word. The negative second index is -exactly device ID 3 while `extra_devices=2`, whose mask is 3. - -## Normal and cross-index reads - -```sh -I=/root/repo22-g6/images -S=/root/repo22-g6/sources/chunk -mkdir -p /mnt/g6 -kldload /root/repo22-g6/erofs.ko -mdconfig -a -t vnode -f "$I/multi2-primary.erofs" -u 90 -mdconfig -a -t vnode -f "$I/multi2-slot1.blob" -u 91 -mdconfig -a -t vnode -f "$I/multi2-slot2.blob" -u 92 -mount -t erofs -o ro -o device.1=/dev/md91 -o device.2=/dev/md92 \ - /dev/md90 /mnt/g6 -cmp "$S/indexed.bin" /mnt/g6/indexed.bin -dd if="$S/indexed.bin" of=/tmp/tc095.first.src bs=1 skip=0 count=4096 2>/dev/null -dd if=/mnt/g6/indexed.bin of=/tmp/tc095.first.mnt bs=1 skip=0 count=4096 2>/dev/null -cmp /tmp/tc095.first.src /tmp/tc095.first.mnt -dd if="$S/indexed.bin" of=/tmp/tc095.middle.src bs=1 skip=65536 count=4096 2>/dev/null -dd if=/mnt/g6/indexed.bin of=/tmp/tc095.middle.mnt bs=1 skip=65536 count=4096 2>/dev/null -cmp /tmp/tc095.middle.src /tmp/tc095.middle.mnt -dd if="$S/indexed.bin" of=/tmp/tc095.last.src bs=1 skip=131072 count=4096 2>/dev/null -dd if=/mnt/g6/indexed.bin of=/tmp/tc095.last.mnt bs=1 skip=131072 count=4096 2>/dev/null -cmp /tmp/tc095.last.src /tmp/tc095.last.mnt -dd if="$S/indexed.bin" of=/tmp/tc095.cross.src bs=1 skip=28672 count=8192 2>/dev/null -dd if=/mnt/g6/indexed.bin of=/tmp/tc095.cross.mnt bs=1 skip=28672 count=8192 2>/dev/null -cmp /tmp/tc095.cross.src /tmp/tc095.cross.mnt -umount /mnt/g6 -mdconfig -d -u 92 -mdconfig -d -u 91 -mdconfig -d -u 90 -``` - -## 48-bit indexes and high unified address - -```sh -mdconfig -a -t vnode -f "$I/multi2-unified48-primary.erofs" -u 90 -mdconfig -a -t vnode -f "$I/multi2-unified48-slot1.blob" -u 91 -mdconfig -a -t vnode -f "$I/multi2-unified48-slot2.blob" -u 92 -mount -t erofs -o ro -o device.2=/dev/md92 -o device.1=/dev/md91 \ - /dev/md90 /mnt/g6 -cmp "$S/indexed.bin" /mnt/g6/indexed.bin -cmp "$S/unified-address.bin" /mnt/g6/unified-address.bin -umount /mnt/g6 -mdconfig -d -u 92 -mdconfig -d -u 91 -mdconfig -d -u 90 -``` - -## Mapped undeclared ID - -```sh -mdconfig -a -t vnode -f "$I/bad-mapped-device3.erofs" -u 90 -mdconfig -a -t vnode -f "$I/multi2-slot1.blob" -u 91 -mdconfig -a -t vnode -f "$I/multi2-slot2.blob" -u 92 -mount -t erofs -o ro -o device.1=/dev/md91 -o device.2=/dev/md92 \ - /dev/md90 /mnt/g6 -truss -f -o /tmp/tc095-enodev.truss dd if=/mnt/g6/indexed.bin \ - of=/dev/null bs=1 skip=32768 count=4096 -tail -20 /tmp/tc095-enodev.truss -cmp "$S/tc006.bin" /mnt/g6/tc006.bin -``` - -The affected read must return exactly `ENODEV`; the mount and unaffected file -remain usable. - -## Cleanup and zero-state check - -```sh -umount /mnt/g6 -mdconfig -d -u 92 -mdconfig -d -u 91 -mdconfig -d -u 90 -kldunload erofs -rm -f /tmp/tc095.*.src /tmp/tc095.*.mnt -mount -p | awk '$3 == "erofs" { print }' -mdconfig -l -kldstat -n erofs 2>/dev/null || true -sysctl -n kern.geom.conftxt | grep -E 'md9[0-2]|erofs' || true -``` - -All four final outputs must be empty. diff --git a/tests/TC096-device-table-parse.md b/tests/TC096-device-table-parse.md deleted file mode 100644 index f985466..0000000 --- a/tests/TC096-device-table-parse.md +++ /dev/null @@ -1,105 +0,0 @@ -# Test Case: Device Table Parse - -**Test ID**: TC096-device-table-parse -**Category**: Multi-Device -**Priority**: Critical - -## Objective - -Verify `extra_devices`, `devt_slotoff`, 48-bit `blocks`/`uniaddr`, table offset -zero, `uniaddr=0`, statfs aggregation, and out-of-primary table rejection. - -## Generator field self-check - -Run `python3 -B tests/g6_multidev_fixtures.py verify OUTPUT` on the host. The -manifest must contain these independently reparsed values: - -| Fixture | primary blocks | extra | slot offset | slots `(blocks, uniaddr)` | -| --- | ---: | ---: | ---: | --- | -| `multi2` | 2 | 2 | 32 | `(137,2)`, `(90,139)` | -| `multi2-devt0` | 2 | 2 | 0 | `(137,2)`, `(90,139)` | -| `multi2-uniaddr0` | 2 | 2 | 32 | `(137,0)`, `(90,139)` | -| `multi2-unified48` | 2 | 2 | 32 | high-word unified ranges | - -The exact slot sizes are generated values and must also match the current -manifest if the source changes. - -## Standard table and statfs - -```sh -I=/root/repo22-g6/images -S=/root/repo22-g6/sources/chunk -mkdir -p /mnt/g6 -kldload /root/repo22-g6/erofs.ko -mdconfig -a -t vnode -f "$I/multi2-primary.erofs" -u 90 -mdconfig -a -t vnode -f "$I/multi2-slot1.blob" -u 91 -mdconfig -a -t vnode -f "$I/multi2-slot2.blob" -u 92 -mount -t erofs -o ro -o device.2=/dev/md92 -o device.1=/dev/md91 \ - /dev/md90 /mnt/g6 -df -k /mnt/g6 -cmp "$S/cross-device.bin" /mnt/g6/cross-device.bin -``` - -The manifest `f_blocks` basis is `2 + 137 + 90 = 229` at 4096 bytes, so -`df -k` must report 916 one-kilobyte blocks. - -## Positive table variants - -Unmount and detach units 92, 91, and 90 before each replacement primary. -Mount each row with its two matching providers and compare the listed file: - -| Primary/provider prefix | Required check | -| --- | --- | -| `multi2-devt0` | table bytes at image offset 0; full `cross-device.bin` cmp | -| `multi2-uniaddr0` | slot 1 selected by nonzero ID; full `cross-device.bin` cmp | -| `multi2-unified48` | nonzero `uniaddr_hi` and `startblk_hi`; full `unified-address.bin` cmp | - -Use this direct command shape for each row, changing only `PREFIX`: - -```sh -PREFIX=multi2-devt0 -umount /mnt/g6 -mdconfig -d -u 92 -mdconfig -d -u 91 -mdconfig -d -u 90 -mdconfig -a -t vnode -f "$I/$PREFIX-primary.erofs" -u 90 -mdconfig -a -t vnode -f "$I/$PREFIX-slot1.blob" -u 91 -mdconfig -a -t vnode -f "$I/$PREFIX-slot2.blob" -u 92 -mount -t erofs -o ro -o device.1=/dev/md91 -o device.2=/dev/md92 \ - /dev/md90 /mnt/g6 -cmp "$S/cross-device.bin" /mnt/g6/cross-device.bin -``` - -For `multi2-unified48`, compare `unified-address.bin` instead. Execute the -block separately for all three prefixes; it is not a test runner. - -## Table outside the primary image - -After detaching the prior variant: - -```sh -mdconfig -a -t vnode -f "$I/bad-table-oob.erofs" -u 90 -mdconfig -a -t vnode -f "$I/multi2-slot1.blob" -u 91 -mdconfig -a -t vnode -f "$I/multi2-slot2.blob" -u 92 -truss -f -o /tmp/tc096-oob.truss mount -t erofs -o ro \ - -o device.1=/dev/md91 -o device.2=/dev/md92 /dev/md90 /mnt/g6 -tail -20 /tmp/tc096-oob.truss -``` - -Mount must return exactly `EINTEGRITY` before root vnode creation. - -## Cleanup and zero-state check - -```sh -umount /mnt/g6 2>/dev/null || true -mdconfig -d -u 92 -mdconfig -d -u 91 -mdconfig -d -u 90 -kldunload erofs -mount -p | awk '$3 == "erofs" { print }' -mdconfig -l -kldstat -n erofs 2>/dev/null || true -sysctl -n kern.geom.conftxt | grep -E 'md9[0-2]|erofs' || true -``` - -All four final outputs must be empty. diff --git a/tests/TC097-device-table-invalid.md b/tests/TC097-device-table-invalid.md deleted file mode 100644 index c569ae8..0000000 --- a/tests/TC097-device-table-invalid.md +++ /dev/null @@ -1,91 +0,0 @@ -# Test Case: Invalid Device Table - -**Test ID**: TC097-device-table-invalid -**Category**: Multi-Device / Error Handling -**Priority**: Critical - -## Objective - -Verify fail-closed table validation and complete cleanup after parse failure or -after slot 1 has opened and slot 2 fails. - -## Deterministic negative fixtures - -The G6 generator makes one asserted change per checksum-valid image: - -| Image | Exact expected errno | -| --- | --- | -| `bad-table-oob.erofs` | `EINTEGRITY` | -| `bad-slot-zero-blocks.erofs` | `EINTEGRITY` | -| `bad-slot-inside-primary.erofs` | `EINTEGRITY` | -| `bad-slot-overlap.erofs` | `EINTEGRITY` | -| `bad-slot-32bit-limit.erofs` | `EINTEGRITY` | -| `bad-slot-48bit-limit.erofs` | `EINTEGRITY` | - -An on-disk slot has only 48-bit `blocks` and `uniaddr`; their sum cannot -overflow a 64-bit C integer. Therefore an alleged uint64-add-overflow fixture -is not representable. The last image tests the real format boundary: a -48-bit range ending above `2^48`. `devt_slotoff=0` and `uniaddr=0` are legal -TC096 positives, not invalid fixtures. - -## Manual procedure for each structural image - -For each row above, execute the following commands directly, replacing -`IMAGE` with that row. Do not put the rows in a runner or shell loop. - -```sh -I=/root/repo22-g6/images -IMAGE=bad-table-oob.erofs -mkdir -p /mnt/g6 -kldload /root/repo22-g6/erofs.ko -mdconfig -a -t vnode -f "$I/$IMAGE" -u 90 -mdconfig -a -t vnode -f "$I/multi2-slot1.blob" -u 91 -mdconfig -a -t vnode -f "$I/multi2-slot2.blob" -u 92 -truss -f -o "/tmp/tc097-$IMAGE.truss" mount -t erofs -o ro \ - -o device.1=/dev/md91 -o device.2=/dev/md92 /dev/md90 /mnt/g6 -tail -20 "/tmp/tc097-$IMAGE.truss" -mount -p | awk '$3 == "erofs" { print }' -mdconfig -d -u 92 -mdconfig -d -u 91 -mdconfig -d -u 90 -kldunload erofs -mdconfig -l -kldstat -n erofs 2>/dev/null || true -sysctl -n kern.geom.conftxt | grep -E 'md9[0-2]|erofs' || true -``` - -Each mount must return exactly `EINTEGRITY`. After each individual failure, -the mount, md, KLD, and matching GEOM consumer outputs must all be empty before -proceeding to the next image. - -## Reverse-open cleanup - -This valid table opens slot 1 and then fails the slot-2 pathname lookup: - -```sh -kldload /root/repo22-g6/erofs.ko -mdconfig -a -t vnode -f "$I/multi2-primary.erofs" -u 90 -mdconfig -a -t vnode -f "$I/multi2-slot1.blob" -u 91 -truss -f -o /tmp/tc097-reverse.truss mount -t erofs -o ro \ - -o device.1=/dev/md91 -o device.2=/no/such/repo22-g6-slot2 \ - /dev/md90 /mnt/g6 -tail -20 /tmp/tc097-reverse.truss -mdconfig -d -u 91 -mdconfig -d -u 90 -kldunload erofs -``` - -The mount returns `ENOENT`, and immediate detach of md91 proves reverse-order -cleanup released the already-open consumer. - -## Final zero-state check - -```sh -mount -p | awk '$3 == "erofs" { print }' -mdconfig -l -kldstat -n erofs 2>/dev/null || true -sysctl -n kern.geom.conftxt | grep -E 'md9[0-2]|erofs' || true -``` - -All four outputs must be empty, with no panic, trap, hang, leaked mount, or -stale GEOM consumer. diff --git a/tests/TC098-fragments-support.md b/tests/TC098-fragments-support.md deleted file mode 100644 index ae98a57..0000000 --- a/tests/TC098-fragments-support.md +++ /dev/null @@ -1,59 +0,0 @@ -# Test Case: Packed-Inode Fragments - -**Test ID**: TC098-fragments-support -**Category**: Compression -**Priority**: High - -## Objective - -Verify a deterministic whole-file fragment stored in the EROFS packed inode, -including full and offset reads. This is an in-image packed inode, not an -external fragment device. - -## Fixture qualification - -Use fresh `fragment.erofs`. The generator runs: - -```sh -mkfs.erofs -T0 --all-time --all-root --workers=1 \ - -U67360098-0000-0000-0000-000000000098 \ - -zlz4 -C65536 -E all-fragments fragment.erofs sources/fragment -``` - -The manifest and verifier must report a nonzero packed NID, fragment feature, -and compressed layout for `/fragment.dat`. Host fsck extracts and compares the -entire source before guest testing. - -## Manual procedure - -```sh -I=/root/repo22-g6/images -S=/root/repo22-g6/sources/fragment -mkdir -p /mnt/g6 -kldload /root/repo22-g6/erofs.ko -mdconfig -a -t vnode -f "$I/fragment.erofs" -u 90 -mount -t erofs -o ro /dev/md90 /mnt/g6 -stat -f '%z' /mnt/g6/fragment.dat -sha256 "$S/fragment.dat" /mnt/g6/fragment.dat -cmp "$S/fragment.dat" /mnt/g6/fragment.dat -dd if="$S/fragment.dat" of=/tmp/tc098.src bs=1 skip=65536 count=8192 2>/dev/null -dd if=/mnt/g6/fragment.dat of=/tmp/tc098.mnt bs=1 skip=65536 count=8192 2>/dev/null -cmp /tmp/tc098.src /tmp/tc098.mnt -``` - -The size must be 100000 bytes, and all comparisons must pass. - -## Cleanup and zero-state check - -```sh -umount /mnt/g6 -mdconfig -d -u 90 -kldunload erofs -rm -f /tmp/tc098.src /tmp/tc098.mnt -mount -p | awk '$3 == "erofs" { print }' -mdconfig -l -kldstat -n erofs 2>/dev/null || true -sysctl -n kern.geom.conftxt | grep -E 'md90|erofs' || true -``` - -All four final outputs must be empty. diff --git a/tests/TC099-multidev-2devices.md b/tests/TC099-multidev-2devices.md deleted file mode 100644 index f422bb0..0000000 --- a/tests/TC099-multidev-2devices.md +++ /dev/null @@ -1,64 +0,0 @@ -# Test Case: Primary Plus One mkfs Blob Provider - -**Test ID**: TC099-multidev-2devices -**Category**: Multi-Device -**Priority**: Critical - -## Objective - -Verify the common two-provider layout emitted directly by erofs-utils 1.8.6, -including statfs aggregation and fail-closed omission of the blob provider. - -## Positive procedure - -```sh -I=/root/repo22-g6/images -S=/root/repo22-g6/sources/chunk -mkdir -p /mnt/g6 -kldload /root/repo22-g6/erofs.ko -mdconfig -a -t vnode -f "$I/mkfs-blob-primary.erofs" -u 90 -mdconfig -a -t vnode -f "$I/mkfs-blob-slot1.blob" -u 91 -mount -t erofs -o ro -o device.1=/dev/md91 /dev/md90 /mnt/g6 -mount -p | grep '/mnt/g6' -df -k /mnt/g6 -sha256 "$S/tc006.bin" /mnt/g6/tc006.bin -cmp "$S/tc006.bin" /mnt/g6/tc006.bin -dd if="$S/tc006.bin" of=/tmp/tc099.src bs=1 skip=28672 count=8192 2>/dev/null -dd if=/mnt/g6/tc006.bin of=/tmp/tc099.mnt bs=1 skip=28672 count=8192 2>/dev/null -cmp /tmp/tc099.src /tmp/tc099.mnt -``` - -The manifest reports primary blocks 1 and slot blocks 224, so `df -k` must -report 900 one-kilobyte blocks. Metadata comes from md90; all file bytes come -from md91. - -Unmount and detach both providers before the negative step. - -## Missing blob option - -```sh -umount /mnt/g6 -mdconfig -d -u 91 -mdconfig -d -u 90 -mdconfig -a -t vnode -f "$I/mkfs-blob-primary.erofs" -u 90 -truss -f -o /tmp/tc099-enxio.truss mount -t erofs -o ro \ - /dev/md90 /mnt/g6 -tail -20 /tmp/tc099-enxio.truss -``` - -Mount must return exactly `ENXIO`: the 4096-byte split primary is shorter than -the declared flatdev range. - -## Cleanup and zero-state check - -```sh -mdconfig -d -u 90 -kldunload erofs -rm -f /tmp/tc099.src /tmp/tc099.mnt -mount -p | awk '$3 == "erofs" { print }' -mdconfig -l -kldstat -n erofs 2>/dev/null || true -sysctl -n kern.geom.conftxt | grep -E 'md9[01]|erofs' || true -``` - -All four final outputs must be empty. diff --git a/tests/TC100-multidev-4devices.md b/tests/TC100-multidev-4devices.md deleted file mode 100644 index 5f9aff3..0000000 --- a/tests/TC100-multidev-4devices.md +++ /dev/null @@ -1,91 +0,0 @@ -# Test Case: Primary Plus Three External Providers - -**Test ID**: TC100-multidev-4devices -**Category**: Multi-Device -**Priority**: Critical - -## Objective - -Verify a four-provider filesystem, option-order independence, cross-slot and -concurrent reads, and missing, duplicate, and short slot cleanup. - -## Fixture qualification - -The G6 verifier reports `multi3` primary blocks 2 and slots `(89,2)`, `(90,91)`, -and `(51,181)`. `/cross-device.bin` has three 131072-byte indexes with device -IDs `1,2,3`. The generator host-fsck extracts all three providers and compares -the complete source tree. `multi3-slot3-short.blob` is exactly one block below -the declared slot-3 size. - -## Positive procedure - -```sh -I=/root/repo22-g6/images -S=/root/repo22-g6/sources/chunk -mkdir -p /mnt/g6 -kldload /root/repo22-g6/erofs.ko -mdconfig -a -t vnode -f "$I/multi3-primary.erofs" -u 90 -mdconfig -a -t vnode -f "$I/multi3-slot1.blob" -u 91 -mdconfig -a -t vnode -f "$I/multi3-slot2.blob" -u 92 -mdconfig -a -t vnode -f "$I/multi3-slot3.blob" -u 93 -mount -t erofs -o ro -o device.3=/dev/md93 -o device.1=/dev/md91 \ - -o device.2=/dev/md92 /dev/md90 /mnt/g6 -sha256 "$S/cross-device.bin" /mnt/g6/cross-device.bin -cmp "$S/cross-device.bin" /mnt/g6/cross-device.bin -dd if="$S/cross-device.bin" of=/tmp/tc100.cross.src bs=1 skip=126976 count=262144 2>/dev/null -dd if=/mnt/g6/cross-device.bin of=/tmp/tc100.cross.mnt bs=1 skip=126976 count=262144 2>/dev/null -cmp /tmp/tc100.cross.src /tmp/tc100.cross.mnt -sha256 /mnt/g6/cross-device.bin >/tmp/tc100.concurrent.1 & -sha256 /mnt/g6/indexed.bin >/tmp/tc100.concurrent.2 & -sha256 /mnt/g6/cold-slot2.bin >/tmp/tc100.concurrent.3 & -wait -cat /tmp/tc100.concurrent.1 /tmp/tc100.concurrent.2 /tmp/tc100.concurrent.3 -``` - -The 262144-byte range starts before the slot-1/slot-2 transition and ends -after the slot-2/slot-3 transition. All full, range, and concurrent reads must -pass. - -Unmount and detach 93, 92, 91, 90 before each negative step. - -## Negative mount cases - -Run each case directly and capture `nmount` with `truss`: - -1. Omit `device.3`: expected `ENXIO`. -2. Map both `device.1` and `device.2` to md91: expected `EINVAL`. -3. Attach `multi3-slot3-short.blob` as md93: expected `ENXIO`. - -For the short case, the exact command is: - -```sh -mdconfig -a -t vnode -f "$I/multi3-primary.erofs" -u 90 -mdconfig -a -t vnode -f "$I/multi3-slot1.blob" -u 91 -mdconfig -a -t vnode -f "$I/multi3-slot2.blob" -u 92 -mdconfig -a -t vnode -f "$I/multi3-slot3-short.blob" -u 93 -truss -f -o /tmp/tc100-short.truss mount -t erofs -o ro \ - -o device.1=/dev/md91 -o device.2=/dev/md92 -o device.3=/dev/md93 \ - /dev/md90 /mnt/g6 -tail -20 /tmp/tc100-short.truss -``` - -After each failure, detach all attached md units in descending order and -verify no EROFS mount or matching GEOM consumer remains. - -## Cleanup and zero-state check - -```sh -umount /mnt/g6 2>/dev/null || true -mdconfig -d -u 93 2>/dev/null || true -mdconfig -d -u 92 2>/dev/null || true -mdconfig -d -u 91 2>/dev/null || true -mdconfig -d -u 90 2>/dev/null || true -kldunload erofs -rm -f /tmp/tc100.cross.* /tmp/tc100.concurrent.* -mount -p | awk '$3 == "erofs" { print }' -mdconfig -l -kldstat -n erofs 2>/dev/null || true -sysctl -n kern.geom.conftxt | grep -E 'md9[0-3]|erofs' || true -``` - -All four final outputs must be empty, with no panic, trap, or hang. diff --git a/tests/TC101-unified-address-mapping.md b/tests/TC101-unified-address-mapping.md deleted file mode 100644 index fbf1175..0000000 --- a/tests/TC101-unified-address-mapping.md +++ /dev/null @@ -1,151 +0,0 @@ -# Test Case: Unified Address and Flatdev Mapping - -**Test ID**: TC101-unified-address-mapping -**Category**: Multi-Device -**Priority**: Critical - -## Objective - -Verify both EROFS mapping forms and fail-closed complete-extent checks: - -1. device ID 0 plus a block in a nonzero slot `uniaddr` range selects that - explicit provider and subtracts the range base; -2. a nonzero device ID in flatdev mode adds `uniaddr` and reads the combined - primary provider; -3. gaps, cross-slot chunks/pclusters, and out-of-range 48-bit addresses return - `EINTEGRITY` before physical I/O fallback. - -## Positive explicit unified mapping - -```sh -I=/root/repo22-g6/images -S=/root/repo22-g6/sources/chunk -mkdir -p /mnt/g6 -kldload /root/repo22-g6/erofs.ko -mdconfig -a -t vnode -f "$I/multi2-unified-primary.erofs" -u 90 -mdconfig -a -t vnode -f "$I/multi2-unified-slot1.blob" -u 91 -mdconfig -a -t vnode -f "$I/multi2-unified-slot2.blob" -u 92 -mount -t erofs -o ro -o device.2=/dev/md92 -o device.1=/dev/md91 \ - /dev/md90 /mnt/g6 -sha256 "$S/unified-address.bin" /mnt/g6/unified-address.bin -cmp "$S/unified-address.bin" /mnt/g6/unified-address.bin -dd if="$S/unified-address.bin" of=/tmp/tc101.unified.src bs=1 skip=61440 count=8192 2>/dev/null -dd if=/mnt/g6/unified-address.bin of=/tmp/tc101.unified.mnt bs=1 skip=61440 count=8192 2>/dev/null -cmp /tmp/tc101.unified.src /tmp/tc101.unified.mnt -umount /mnt/g6 -mdconfig -d -u 92 -mdconfig -d -u 91 -mdconfig -d -u 90 -``` - -The manifest must show that this index is device ID 0 and pblk equals slot-1 -`uniaddr + local_pblk`, not a local primary address. - -## Positive flatdev mappings - -First test nonzero chunk device IDs in one combined provider: - -```sh -mdconfig -a -t vnode -f "$I/multi2-flatdev.erofs" -u 90 -mount -t erofs -o ro /dev/md90 /mnt/g6 -sha256 "$S/cross-device.bin" /mnt/g6/cross-device.bin -cmp "$S/cross-device.bin" /mnt/g6/cross-device.bin -dd if="$S/cross-device.bin" of=/tmp/tc101.flat.src bs=1 skip=126976 count=139264 2>/dev/null -dd if=/mnt/g6/cross-device.bin of=/tmp/tc101.flat.mnt bs=1 skip=126976 count=139264 2>/dev/null -cmp /tmp/tc101.flat.src /tmp/tc101.flat.mnt -umount /mnt/g6 -mdconfig -d -u 90 -``` - -Then test device-ID-0 unified addressing in the combined provider: - -```sh -mdconfig -a -t vnode -f "$I/multi2-unified-flatdev.erofs" -u 90 -mount -t erofs -o ro /dev/md90 /mnt/g6 -cmp "$S/unified-address.bin" /mnt/g6/unified-address.bin -umount /mnt/g6 -mdconfig -d -u 90 -``` - -The 139264-byte range spans the slot-1/slot-2 transition. erofs-utils 1.8.6 -cannot qualify these flatdev forms; the kernel full/range comparisons are the -qualification. - -## Positive 48-bit high mapping - -```sh -mdconfig -a -t vnode -f "$I/multi2-unified48-primary.erofs" -u 90 -mdconfig -a -t vnode -f "$I/multi2-unified48-slot1.blob" -u 91 -mdconfig -a -t vnode -f "$I/multi2-unified48-slot2.blob" -u 92 -mount -t erofs -o ro -o device.1=/dev/md91 -o device.2=/dev/md92 \ - /dev/md90 /mnt/g6 -cmp "$S/unified-address.bin" /mnt/g6/unified-address.bin -umount /mnt/g6 -mdconfig -d -u 92 -mdconfig -d -u 91 -mdconfig -d -u 90 -``` - -This requires nonzero `uniaddr_hi` and `startblk_hi`; zero-high truncation -would read the wrong bytes. - -## Fail-closed extent cases - -Each primary below is checksum-valid. Mount succeeds, and the named cold read -must return exactly `EINTEGRITY` in `truss`: - -| Primary and providers | Mode | Read target | -| --- | --- | --- | -| `bad-unified-gap-*` | explicit and `bad-unified-gap-flatdev.erofs` | `unified-address.bin` | -| `bad-cross-slot-explicit-*` | explicit | `indexed.bin` | -| `bad-cross-slot-unified-*` | explicit and `bad-cross-slot-unified-flatdev.erofs` | `indexed.bin` | -| `bad-lz4-cross-slot-*` | explicit and `bad-lz4-cross-slot-flatdev.erofs` | `external-pcluster.bin` | -| `bad-unified48-out-of-range-*` | explicit | `unified-address.bin` | - -For an explicit chunk case, use this direct command shape with the matching -prefix and target: - -```sh -PREFIX=bad-cross-slot-explicit -TARGET=indexed.bin -mdconfig -a -t vnode -f "$I/$PREFIX-primary.erofs" -u 90 -mdconfig -a -t vnode -f "$I/$PREFIX-slot1.blob" -u 91 -mdconfig -a -t vnode -f "$I/$PREFIX-slot2.blob" -u 92 -mount -t erofs -o ro -o device.1=/dev/md91 -o device.2=/dev/md92 \ - /dev/md90 /mnt/g6 -truss -f -o "/tmp/tc101-$PREFIX.truss" dd if="/mnt/g6/$TARGET" \ - of=/dev/null bs=131072 count=1 -tail -20 "/tmp/tc101-$PREFIX.truss" -umount /mnt/g6 -mdconfig -d -u 92 -mdconfig -d -u 91 -mdconfig -d -u 90 -``` - -For a flatdev row, attach only its `*-flatdev.erofs` as md90 and use no -`device.N` options. For the LZ4 explicit row, use its two one-block slot -providers. Execute every row and mode separately; do not use a runner. - -The explicit crossing providers are physically only as long as their declared -slots. Therefore both declared range and physical media would be exceeded; -`EINTEGRITY` must win before a possible `ENXIO` from I/O. The largest non-NULL -48-bit address (`0xfffffffffffe`; all ones is the hole sentinel) is -representable in 64-bit arithmetic but belongs to no declared range, so it -also returns `EINTEGRITY`. - -## Cleanup and zero-state check - -```sh -umount /mnt/g6 2>/dev/null || true -mdconfig -d -u 92 2>/dev/null || true -mdconfig -d -u 91 2>/dev/null || true -mdconfig -d -u 90 2>/dev/null || true -kldunload erofs -rm -f /tmp/tc101.*.src /tmp/tc101.*.mnt -mount -p | awk '$3 == "erofs" { print }' -mdconfig -l -kldstat -n erofs 2>/dev/null || true -sysctl -n kern.geom.conftxt | grep -E 'md9[0-2]|erofs' || true -``` - -All four final outputs must be empty, with no panic, trap, or hang. diff --git a/tests/TC102-deflate-level1.md b/tests/TC102-deflate-level1.md deleted file mode 100644 index 1e46e16..0000000 --- a/tests/TC102-deflate-level1.md +++ /dev/null @@ -1,54 +0,0 @@ -# Test Case: DEFLATE Level 1 Read Correctness - -**Test ID**: TC102-deflate-level1 - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression -**Priority**: High - -## Objective - -Verify full and offset reads from an erofs-utils 1.8.6 DEFLATE level 1 image. - -## Fixture and Helper - -```sh -work=$(mktemp -d /tmp/repo22-tc102.XXXXXX) -tests/prepare_error_fixtures.sh "$work/fixtures" -cc -O2 -Wall -Wextra -std=c17 tests/read_probe.c -o "$work/read_probe" -dump.erofs -s "$work/fixtures/valid-deflate-level1.erofs" -dump.erofs --path=/compressed.bin -e \ - "$work/fixtures/valid-deflate-level1.erofs" -``` - -The superblock and extent output must identify DEFLATE-compressed data. The -image is generated with `-zdeflate,level=1 -C65536`. - -## FreeBSD Procedure - -```sh -mkdir "$work/mnt" -unit=$(mdconfig -a -t vnode \ - -f "$work/fixtures/valid-deflate-level1.erofs") -mount -t erofs -o ro "/dev/$unit" "$work/mnt" - -expected=$(sha256 -q "$work/fixtures/source/compressed.bin") -actual=$(sha256 -q "$work/mnt/compressed.bin") -test "$actual" = "$expected" -"$work/read_probe" pread "$work/mnt/compressed.bin" 32768 4096 \ - "$work/guest-range.bin" -"$work/read_probe" pread "$work/fixtures/source/compressed.bin" 32768 4096 \ - "$work/source-range.bin" -cmp "$work/source-range.bin" "$work/guest-range.bin" - -umount "$work/mnt" -mdconfig -d -u "${unit#md}" -``` - -## Expected Results - -The complete mounted-file hash equals the source hash, and the fixed 4 KiB -range compares byte-for-byte. The invalid former command -`sha256 output vs original` is not used. diff --git a/tests/TC103-deflate-level6.md b/tests/TC103-deflate-level6.md deleted file mode 100644 index f333698..0000000 --- a/tests/TC103-deflate-level6.md +++ /dev/null @@ -1,46 +0,0 @@ -# Test Case: DEFLATE Compression Level 6 - -**Test ID**: TC103-deflate-level6 - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression -**Priority**: High -**Regression**: None - -## Objective -Verify DEFLATE compressed data path with level 6 (default) compression. - -## Preconditions -- EROFS image created with DEFLATE level 6: `mkfs.erofs -zdeflate,level=6 test.img src/` -- Test files with various entropy levels -- FreeBSD 13.0+ with zlib support - -## Test Steps -1. Create image with DEFLATE level 6 compression -2. Attach and mount: `unit=$(mdconfig -a -t vnode -f test.img); mount -t erofs -o ro /dev/${unit} /mnt` -3. Read text files, binaries, and random data -4. Verify all content matches original -5. Test partial reads at various offsets - -## Expected Results -- All files decompress correctly -- Better compression ratio than level 1 -- Random access works at any offset -- Stable decompression performance - -## Verification Method -- SHA256 checksums match for all files -- Compression ratio >= level 1 -- No decompression failures -- Random read correctness verified - -## Cleanup -```sh -umount /mnt -mdconfig -d -u "${unit}" -``` - -## Notes -Tests feature 58: DEFLATE compressed data path with balanced compression. diff --git a/tests/TC104-deflate-level9.md b/tests/TC104-deflate-level9.md deleted file mode 100644 index 8716f33..0000000 --- a/tests/TC104-deflate-level9.md +++ /dev/null @@ -1,46 +0,0 @@ -# Test Case: DEFLATE Compression Level 9 - -**Test ID**: TC104-deflate-level9 - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression -**Priority**: High -**Regression**: None - -## Objective -Verify DEFLATE compressed data path with level 9 (maximum) compression. - -## Preconditions -- EROFS image created with DEFLATE level 9: `mkfs.erofs -zdeflate,level=9 test.img src/` -- Large test files (1MB+) for compression testing -- FreeBSD 13.0+ with zlib support - -## Test Steps -1. Create image with DEFLATE level 9 compression -2. Attach and mount: `unit=$(mdconfig -a -t vnode -f test.img); mount -t erofs -o ro /dev/${unit} /mnt` -3. Read large compressed files sequentially -4. Test random access throughout file -5. Measure decompression performance - -## Expected Results -- All files decompress correctly -- Best compression ratio among DEFLATE levels -- Slightly slower decompression acceptable -- No data corruption - -## Verification Method -- Verify checksums match original files -- Compression ratio > level 6 -- Decompression throughput > 50 MB/s -- Random access correctness validated - -## Cleanup -```sh -umount /mnt -mdconfig -d -u "${unit}" -``` - -## Notes -Tests feature 58: DEFLATE compressed data path with maximum compression. diff --git a/tests/TC105-zstd-level1.md b/tests/TC105-zstd-level1.md deleted file mode 100644 index 1122af6..0000000 --- a/tests/TC105-zstd-level1.md +++ /dev/null @@ -1,46 +0,0 @@ -# Test Case: zstd Compression Level 1 - -**Test ID**: TC105-zstd-level1 - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression -**Priority**: High -**Regression**: None - -## Objective -Verify zstd compressed data path with level 1 (fastest) compression. - -## Preconditions -- EROFS image created with zstd level 1: `mkfs.erofs -zzstd,level=1 test.img src/` -- Test files of various sizes -- FreeBSD 13.0+ with libzstd - -## Test Steps -1. Create image with zstd level 1 compression -2. Attach and mount: `unit=$(mdconfig -a -t vnode -f test.img); mount -t erofs -o ro /dev/${unit} /mnt` -3. Read files and verify content -4. Test random access patterns -5. Measure throughput - -## Expected Results -- All files decompress correctly -- Fast decompression speed -- Good compression ratio even at level 1 -- Random access works correctly - -## Verification Method -- SHA256 checksums match originals -- Decompression throughput > 200 MB/s -- No zstd decompression errors -- Random reads succeed - -## Cleanup -```sh -umount /mnt -mdconfig -d -u "${unit}" -``` - -## Notes -Tests feature 59: zstd compressed data path with fast compression. diff --git a/tests/TC106-zstd-level15.md b/tests/TC106-zstd-level15.md deleted file mode 100644 index 9402125..0000000 --- a/tests/TC106-zstd-level15.md +++ /dev/null @@ -1,46 +0,0 @@ -# Test Case: zstd Compression Level 15 - -**Test ID**: TC106-zstd-level15 - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression -**Priority**: High -**Regression**: None - -## Objective -Verify zstd compressed data path with level 15 (high) compression. - -## Preconditions -- EROFS image created with zstd level 15: `mkfs.erofs -zzstd,level=15 test.img src/` -- Large test files for compression testing -- FreeBSD 13.0+ with libzstd - -## Test Steps -1. Create image with zstd level 15 compression -2. Attach and mount: `unit=$(mdconfig -a -t vnode -f test.img); mount -t erofs -o ro /dev/${unit} /mnt` -3. Read all test files -4. Verify content integrity -5. Test partial reads - -## Expected Results -- All files decompress correctly -- Better compression than level 1 -- Decompression still fast -- No memory issues - -## Verification Method -- All checksums match -- Compression ratio > zstd level 1 -- Decompression works for all files -- Random access validated - -## Cleanup -```sh -umount /mnt -mdconfig -d -u "${unit}" -``` - -## Notes -Tests feature 59: zstd compressed data path with high compression. diff --git a/tests/TC107-zstd-level22.md b/tests/TC107-zstd-level22.md deleted file mode 100644 index 5491560..0000000 --- a/tests/TC107-zstd-level22.md +++ /dev/null @@ -1,46 +0,0 @@ -# Test Case: zstd Compression Level 22 - -**Test ID**: TC107-zstd-level22 - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression -**Priority**: Medium -**Regression**: None - -## Objective -Verify zstd compressed data path with level 22 (maximum) compression. - -## Preconditions -- EROFS image created with zstd level 22: `mkfs.erofs -zzstd,level=22 test.img src/` -- Large test files (multi-MB) -- FreeBSD 13.0+ with libzstd - -## Test Steps -1. Create image with zstd level 22 compression -2. Attach and mount: `unit=$(mdconfig -a -t vnode -f test.img); mount -t erofs -o ro /dev/${unit} /mnt` -3. Read large files sequentially -4. Test random access -5. Monitor memory usage during decompression - -## Expected Results -- All files decompress correctly -- Maximum compression ratio for zstd -- Acceptable decompression speed -- Stable memory usage - -## Verification Method -- Checksums match original files -- Compression ratio > level 15 -- Decompression throughput > 100 MB/s -- No memory leaks - -## Cleanup -```sh -umount /mnt -mdconfig -d -u "${unit}" -``` - -## Notes -Tests feature 59: zstd compressed data path with maximum compression. diff --git a/tests/TC108-lzma-large-file.md b/tests/TC108-lzma-large-file.md deleted file mode 100644 index 61dccd4..0000000 --- a/tests/TC108-lzma-large-file.md +++ /dev/null @@ -1,49 +0,0 @@ -# Test Case: LZMA Large File Decompression - -**Test ID**: TC108-lzma-large-file - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression -**Priority**: High -**Regression**: None - -## Objective -Verify LZMA decompression works correctly for large files (100MB+). - -## Preconditions -- `images/lzma-large.erofs`, generated with LZMA level 6 -- `/large.bin` and `sources/lzma-large/large.bin`, exactly 104857601 bytes -- FreeBSD 13.0+ with liblzma - -## Test Steps -1. Create image with large LZMA-compressed file -2. Attach and mount: `unit=$(mdconfig -a -t vnode -f test.img); mount -t erofs -o ro /dev/${unit} /mnt` -3. Compare complete source and guest SHA256, then run - `timeout 1800 cmp SOURCE /mnt/large.bin`. -4. Compare 4096 bytes at offsets 0, 52428800, and 103809024 with the source. -5. Record `vmstat -m` while mounted and after cleanup; active EROFS - allocations must return to zero. - -## Expected Results -- Large file reads successfully -- Checksum matches original -- Random access works at any offset -- Memory usage < 256MB during decompression - -## Verification Method -- SHA256 checksum verification -- Random read correctness at offsets: 0, 50MB, 99MB -- Memory usage monitored via top -- No kernel panics or OOM - -## Cleanup -```sh -umount /mnt -mdconfig -d -u "${unit#md}" -rm -f /tmp/out -``` - -## Notes -Tests feature 60: LZMA decompression with large files. diff --git a/tests/TC109-microlzma.md b/tests/TC109-microlzma.md deleted file mode 100644 index b7c75c8..0000000 --- a/tests/TC109-microlzma.md +++ /dev/null @@ -1,48 +0,0 @@ -# Test Case: MicroLZMA Compression - -**Test ID**: TC109-microlzma - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression -**Priority**: Medium -**Regression**: None - -## Objective -Verify MicroLZMA compressed data path works correctly. - -## Preconditions -- `images/microlzma-edge.erofs`, generated with LZMA level 6 -- Actual 1-byte, 4096-byte, and 16384-byte source files -- The 16384-byte inode must be compressed full-index with algorithm 1. The - smaller files are valid flat controls and must not be claimed as compressed. -- FreeBSD 13.0+ with liblzma - -## Test Steps -1. Create image with MicroLZMA-compressed small files -2. Attach and mount: `unit=$(mdconfig -a -t vnode -f test.img); mount -t erofs -o ro /dev/${unit} /mnt` -3. Read all small files -4. Verify content integrity -5. Test edge cases (1-byte, 16KB boundary) - -## Expected Results -- All files read correctly and match their source bytes -- MicroLZMA format handled properly -- Good compression for small files -- No format detection errors - -## Verification Method -- Checksums match for all files -- Verify LZMA format detection in dmesg -- Test files at boundaries: 1B, 4KB, 16KB -- No decompression failures - -## Cleanup -```sh -umount /mnt -mdconfig -d -u "${unit#md}" -``` - -## Notes -Tests feature 60: MicroLZMA variant of LZMA compression. diff --git a/tests/TC110-lzma-corrupt.md b/tests/TC110-lzma-corrupt.md deleted file mode 100644 index e840255..0000000 --- a/tests/TC110-lzma-corrupt.md +++ /dev/null @@ -1,50 +0,0 @@ -# Test Case: LZMA Corrupted Data Handling - -**Test ID**: TC110-lzma-corrupt - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression -**Priority**: High -**Regression**: None - -## Objective -Verify proper error handling when LZMA compressed data is corrupted. - -## Preconditions -- EROFS image with LZMA-compressed files -- `g5_fixtures.py` manifest proving algorithm 1, the target pcluster range, - exact patch offset/length, and a valid superblock checksum -- FreeBSD 13.0+ - -## Test Steps -1. Create valid LZMA-compressed EROFS image -2. Use only the generated `lzma-partial-ref-corrupt.erofs`; do not apply blind - or random offsets. -3. Attach and mount: `unit=$(mdconfig -a -t vnode -f test.img); mount -t erofs -o ro /dev/${unit} /mnt` -4. Run `timeout 20 read_probe expect-error /mnt/a.dat 5`. -5. Compare `/mnt/control.bin` from the same corrupted image with its source, - then check cleanup, allocation count, dmesg, and responsiveness. - -## Expected Results -- Mount succeeds (corruption not in superblock) -- Read returns EIO error -- Error logged to dmesg -- System remains stable (no panic) -- Other uncorrupted files still readable - -## Verification Method -- Verify read fails with errno == EIO -- Check dmesg for LZMA decompression error -- Confirm system stability after error -- Verify other files still accessible - -## Cleanup -```sh -umount /mnt -mdconfig -d -u "${unit#md}" -``` - -## Notes -Tests feature 60: LZMA error path with corrupted compressed data. diff --git a/tests/TC111-manpage-accuracy.md b/tests/TC111-manpage-accuracy.md deleted file mode 100644 index ec3434f..0000000 --- a/tests/TC111-manpage-accuracy.md +++ /dev/null @@ -1,54 +0,0 @@ -# Test Case: Manual Page Accuracy - -**Test ID**: TC111-manpage-accuracy -**Category**: Documentation -**Priority**: Low -**Regression**: None - -## Objective -Verify that manual pages accurately document all mount options, supported features, and behavior. - -## Preconditions -- The repository `docs/erofs.5` source and `README.md` are available. -- Access to the current module source for cross-reference. -- A FreeBSD 15 guest with the current `erofs.ko` and a valid image. -- Do not assume that a filesystem-specific `mount_erofs(8)` exists. - -## Test Steps -1. Confirm whether `/sbin/mount_erofs` exists. If it does not, verify the - documented generic `/sbin/mount -t erofs` path directly. -2. Mount without `-o ro`, with `-o rw`, and with documented standard flags; - record the resulting mount flags and write behavior. -3. Verify every documented filesystem-specific option against `src/super.c` - and an applicable real image. -4. Submit an unknown filesystem-specific option and require `nmount(2)` to - reject it with `EINVAL` without leaving a mount. -5. Cross-reference `README.md`, `docs/features.md`, `docs/architecture.md`, - `docs/erofs.5`, and `tests/TEST-COVERAGE-MATRIX.md` with current source and - qualified manual results. -6. Validate all examples and SEE ALSO references that apply to the qualified - FreeBSD environment. - -## Expected Results -- The documented generic mount path works without `mount_erofs(8)`. -- Every successful EROFS mount is read-only, including a request containing - `-o rw`; mutating operations return `EROFS`. -- Documented filesystem-specific options match the current parser and work on - their applicable fixture types. -- Unknown filesystem-specific options return `EINVAL` and create no mount. -- Feature claims match the implementation and qualified coverage. -- Examples and SEE ALSO entries are accurate. - -## Verification Method -- Record direct command exit status separately from tracing-tool status. -- Verify feature claims against `TEST-COVERAGE-MATRIX.md`. -- Check documentation against source and actual `/sbin/mount`/`nmount(2)` - behavior. -- Validate all code examples on their applicable fixture type. - -## Cleanup -Unmount the test filesystem, detach its md provider, and unload the exact test -module. Assert that no EROFS mount, md provider, or EROFS KLD remains. - -## Notes -Tests feature 61: manual pages accuracy and completeness. diff --git a/tests/TC112-invalid-superblock.md b/tests/TC112-invalid-superblock.md deleted file mode 100644 index 03a471a..0000000 --- a/tests/TC112-invalid-superblock.md +++ /dev/null @@ -1,56 +0,0 @@ -# Test Case: Invalid Superblock Magic - -**Test ID**: TC112-invalid-superblock -**Category**: Error Handling -**Priority**: Critical - -## Objective - -Verify rejection of an invalid EROFS magic at the real field while provider -length and all suffix bytes covered by repo22's production checksum verifier -remain unchanged. - -## Fixture Qualification - -```sh -work=$(mktemp -d /tmp/repo22-tc112.XXXXXX) -tests/prepare_error_fixtures.sh "$work/fixtures" -od -An -tx4 -j 1024 -N 4 "$work/fixtures/bad-magic.erofs" -python3 tests/erofs_fixture.py inspect "$work/fixtures/bad-magic.erofs" -grep '^bad-magic ' "$work/fixtures/fixture-evidence.txt" -wc -c "$work/fixtures/valid-plain.erofs" \ - "$work/fixtures/bad-magic.erofs" -sha256 -q "$work/fixtures/bad-magic.erofs" -``` - -The mutator asserts magic `0xe0f5e1e2` at byte 1024 and replaces it with -`0x21444142`. It intentionally leaves the stored checksum unchanged. The -canonical checksum over the changed magic is invalid, but the production -fixed-magic suffix calculation over `[1032,4096)` remains valid because no -suffix byte changed. Magic validation occurs first. - -## FreeBSD Procedure - -```sh -mkdir "$work/mnt" -unit=$(mdconfig -a -t vnode -f "$work/fixtures/bad-magic.erofs") -set +e -mount -t erofs -o ro "/dev/$unit" "$work/mnt" \ - >"$work/mount.out" 2>"$work/mount.err" -status=$? -set -e -test "$status" -ne 0 -set +e -truss -o "$work/mount.truss" mount -t erofs -o ro "/dev/$unit" \ - "$work/mnt" >/dev/null 2>&1 -set -e -grep -E 'nmount.*ERR#22' "$work/mount.truss" -! mount -p | awk -v path="$work/mnt" '$2 == path { found=1 } - END { exit found ? 0 : 1 }' -mdconfig -d -u "${unit#md}" -``` - -## Expected Results - -Mount returns errno 22 (`EINVAL`) before root vnode creation. No panic, mount, -or md consumer remains. diff --git a/tests/TC113-corrupted-inode.md b/tests/TC113-corrupted-inode.md deleted file mode 100644 index 50a60b1..0000000 --- a/tests/TC113-corrupted-inode.md +++ /dev/null @@ -1,48 +0,0 @@ -# Test Case: Reserved Inode Format Rejection - -**Test ID**: TC113-corrupted-inode -**Category**: Error Handling -**Priority**: Critical - -## Objective - -Verify a checksum-valid image mounts, but lookup/open of the resolved -`/inode-target.txt` inode with reserved `i_format` bit `0x8000` fails with -`EOPNOTSUPP`; an unaffected file must remain readable. - -## Fixture Qualification - -```sh -work=$(mktemp -d /tmp/repo22-tc113.XXXXXX) -tests/prepare_error_fixtures.sh "$work/fixtures" -cc -O2 -Wall -Wextra -std=c17 tests/read_probe.c -o "$work/read_probe" -python3 tests/erofs_fixture.py inspect \ - "$work/fixtures/bad-inode-format.erofs" --path /inode-target.txt -grep '^bad-inode-format ' "$work/fixtures/fixture-evidence.txt" -sha256 -q "$work/fixtures/bad-inode-format.erofs" -``` - -The mutator resolves the root dirent, calculates the inode byte offset from the -metadata block and NID, asserts the old format, sets only reserved bit -`0x8000`, recomputes CRC32C, and preserves provider length. - -## FreeBSD Procedure - -```sh -mkdir "$work/mnt" -unit=$(mdconfig -a -t vnode -f "$work/fixtures/bad-inode-format.erofs") -mount -t erofs -o ro "/dev/$unit" "$work/mnt" -cmp "$work/fixtures/source/control.txt" "$work/mnt/control.txt" -truss -o "$work/read.truss" "$work/read_probe" expect-error \ - "$work/mnt/inode-target.txt" 45 | tee "$work/error.out" -grep -q 'expected_errno=45' "$work/error.out" -grep -E '(openat|read).*ERR#45' "$work/read.truss" -cmp "$work/fixtures/source/control.txt" "$work/mnt/control.txt" -umount "$work/mnt" -mdconfig -d -u "${unit#md}" -``` - -## Expected Results - -Only the target returns errno 45 (`EOPNOTSUPP`). The mount remains usable and -cleans up fully. diff --git a/tests/TC114-out-of-bounds-block.md b/tests/TC114-out-of-bounds-block.md deleted file mode 100644 index e4ce017..0000000 --- a/tests/TC114-out-of-bounds-block.md +++ /dev/null @@ -1,48 +0,0 @@ -# Test Case: Out-of-Bounds FLAT_PLAIN Start Block - -**Test ID**: TC114-out-of-bounds-block -**Category**: Error Handling -**Priority**: Critical - -## Objective - -Verify checked data mapping rejects `/plain.bin` when its resolved -`startblk_lo` equals the declared filesystem block count, while valid files in -the same mounted image remain readable. - -## Fixture Qualification - -```sh -work=$(mktemp -d /tmp/repo22-tc114.XXXXXX) -tests/prepare_error_fixtures.sh "$work/fixtures" -cc -O2 -Wall -Wextra -std=c17 tests/read_probe.c -o "$work/read_probe" -python3 tests/erofs_fixture.py inspect \ - "$work/fixtures/oob-start-block.erofs" --path /plain.bin -grep '^oob-start-block ' "$work/fixtures/fixture-evidence.txt" -sha256 -q "$work/fixtures/oob-start-block.erofs" -``` - -The mutator proves a nonempty FLAT_PLAIN path, records its NID and inode field -offset, sets `startblk_lo` to `blocks_lo`, recomputes CRC32C, and preserves -provider length. - -## FreeBSD Procedure - -```sh -mkdir "$work/mnt" -unit=$(mdconfig -a -t vnode -f "$work/fixtures/oob-start-block.erofs") -mount -t erofs -o ro "/dev/$unit" "$work/mnt" -cmp "$work/fixtures/source/control.txt" "$work/mnt/control.txt" -truss -o "$work/read.truss" "$work/read_probe" expect-error \ - "$work/mnt/plain.bin" 97 | tee "$work/error.out" -grep -q 'expected_errno=97' "$work/error.out" -grep -E 'read.*ERR#97' "$work/read.truss" -cmp "$work/fixtures/source/control.txt" "$work/mnt/control.txt" -umount "$work/mnt" -mdconfig -d -u "${unit#md}" -``` - -## Expected Results - -The target read returns errno 97 (`EINTEGRITY`) without issuing bytes from a -different range. Control data remains exact and cleanup is complete. diff --git a/tests/TC115-unsupported-algorithm.md b/tests/TC115-unsupported-algorithm.md deleted file mode 100644 index ccfc79f..0000000 --- a/tests/TC115-unsupported-algorithm.md +++ /dev/null @@ -1,54 +0,0 @@ -# Test Case: Future Compression Algorithm Rejection - -**Test ID**: TC115-unsupported-algorithm -**Category**: Error Handling -**Priority**: High - -## Objective - -Verify mount-time `EOPNOTSUPP` for a legal compressed image whose -`available_compr_algs` field additionally advertises unknown bit `0x8000`. - -## Fixture Qualification - -```sh -work=$(mktemp -d /tmp/repo22-tc115.XXXXXX) -tests/prepare_error_fixtures.sh "$work/fixtures" -od -An -tx2 -j 1106 -N 2 "$work/fixtures/future-algorithm.erofs" -python3 tests/erofs_fixture.py inspect \ - "$work/fixtures/future-algorithm.erofs" -grep '^future-algorithm ' "$work/fixtures/fixture-evidence.txt" -wc -c "$work/fixtures/valid-lz4.erofs" \ - "$work/fixtures/future-algorithm.erofs" -sha256 -q "$work/fixtures/future-algorithm.erofs" -``` - -The mutator requires `EROFS_FEATURE_INCOMPAT_COMPR_CFGS`, asserts the old -16-bit field at absolute byte 1106, sets only future bit `0x8000`, recomputes -CRC32C, and preserves provider length. - -## FreeBSD Procedure - -```sh -mkdir "$work/mnt" -unit=$(mdconfig -a -t vnode -f "$work/fixtures/future-algorithm.erofs") -set +e -mount -t erofs -o ro "/dev/$unit" "$work/mnt" \ - >"$work/mount.out" 2>"$work/mount.err" -status=$? -set -e -test "$status" -ne 0 -set +e -truss -o "$work/mount.truss" mount -t erofs -o ro "/dev/$unit" \ - "$work/mnt" >/dev/null 2>&1 -set -e -grep -E 'nmount.*ERR#45' "$work/mount.truss" -! mount -p | awk -v path="$work/mnt" '$2 == path { found=1 } - END { exit found ? 0 : 1 }' -mdconfig -d -u "${unit#md}" -``` - -## Expected Results - -Mount returns errno 45 (`EOPNOTSUPP`), not checksum or media-size failure. No -mount or md consumer remains. diff --git a/tests/TC116-truncated-compressed.md b/tests/TC116-truncated-compressed.md deleted file mode 100644 index 0454233..0000000 --- a/tests/TC116-truncated-compressed.md +++ /dev/null @@ -1,69 +0,0 @@ -# Test Case: Targeted Compressed-Stream Corruption - -**Test ID**: TC116-truncated-compressed -**Category**: Error Handling -**Priority**: Critical - -## Objective - -Verify targeted damage inside a proven LZ4 physical extent reaches -`z_erofs_decompress` and returns `EIO`. Provider length must remain identical; -this is not a short-media test. - -## Fixture Qualification - -```sh -work=$(mktemp -d /tmp/repo22-tc116.XXXXXX) -tests/prepare_error_fixtures.sh "$work/fixtures" -cc -O2 -Wall -Wextra -std=c17 tests/read_probe.c -o "$work/read_probe" -cat "$work/fixtures/compressed.extents" -grep '^compressed-stream-corrupt ' "$work/fixtures/fixture-evidence.txt" -wc -c "$work/fixtures/valid-lz4.erofs" \ - "$work/fixtures/compressed-stream-corrupt.erofs" -cat "$work/fixtures/compressed-corrupt-fsck.txt" -sha256 -q "$work/fixtures/compressed-stream-corrupt.erofs" -``` - -The generator parses extent 0 for `/compressed.bin`, proves the 64-byte patch -at extent offset +32 lies wholly inside that extent, changes nonzero bytes to -zero, remains outside the superblock checksum window, preserves provider -length, and requires `fsck.erofs --extract` failure. The superblock checksum -remains valid because payload bytes are outside its range. - -## FreeBSD Procedure - -```sh -mkdir "$work/mnt" -unit=$(mdconfig -a -t vnode \ - -f "$work/fixtures/compressed-stream-corrupt.erofs") -mount -t erofs -o ro "/dev/$unit" "$work/mnt" -cmp "$work/fixtures/source/control.txt" "$work/mnt/control.txt" -probe_module=$(dtrace -l -f z_erofs_decompress | awk \ - '$4 == "z_erofs_decompress" && $5 == "entry" { print $3; exit }') -test -n "$probe_module" -dtrace -l -n "fbt:$probe_module:z_erofs_decompress:entry" -set +e -dtrace -q -n "fbt:$probe_module:z_erofs_decompress:entry { - @calls = count(); } END { printa(\"decompress_calls=%@d\\n\", @calls); }" \ - -c "$work/read_probe expect-error $work/mnt/compressed.bin 5" \ - >"$work/dtrace.out" 2>"$work/dtrace.err" -dtrace_status=$? -set -e -cat "$work/dtrace.out" -test "$dtrace_status" -eq 0 -grep -Eq 'decompress_calls=[1-9][0-9]*' "$work/dtrace.out" -truss -o "$work/read.truss" "$work/read_probe" expect-error \ - "$work/mnt/compressed.bin" 5 | tee "$work/error.out" -grep -q 'expected_errno=5' "$work/error.out" -grep -E 'read.*ERR#5' "$work/read.truss" -cmp "$work/fixtures/source/control.txt" "$work/mnt/control.txt" -umount "$work/mnt" -mdconfig -d -u "${unit#md}" -kldunload dtraceall -``` - -## Expected Results - -Mount succeeds, proving media-size validation passed. FBT records at least one -`z_erofs_decompress` entry for the target command, and the target read returns -errno 5 (`EIO`). Control data remains readable and cleanup is complete. diff --git a/tests/TC117-invalid-xattr-format.md b/tests/TC117-invalid-xattr-format.md deleted file mode 100644 index d2247ee..0000000 --- a/tests/TC117-invalid-xattr-format.md +++ /dev/null @@ -1,33 +0,0 @@ -# Test Case: Invalid xattr formats - -**Test ID**: TC117-invalid-xattr-format -**Fixtures**: `bad-inline-entry.erofs`, `bad-shared-entry.erofs` - -## Objective - -Verify that an oversized inline `e_value_size` and an oversized shared -`e_value_size` fail at xattr access without destabilizing the mount. The -manifest must identify each exact field offset and `before -> ffff` mutation. - -## Manual steps - -Test each image in a separate attach/mount/cleanup cycle: - -```sh -stat -f 'mode=%Sp size=%z' /mnt/repo22-g4/corrupt-inline -truss -o /tmp/tc117-inline.truss getextattr -qq user bad \ - /mnt/repo22-g4/corrupt-inline -grep extattr_get_file /tmp/tc117-inline.truss -``` - -```sh -getextattr -qq -x user local /mnt/repo22-g4/shared-a -truss -o /tmp/tc117-shared.truss getextattr -qq user shared_key \ - /mnt/repo22-g4/shared-a -grep extattr_get_file /tmp/tc117-shared.truss -``` - -## Expected results - -Both malformed gets return `EINTEGRITY` (97); mount/stat and the independent -inline `local` value still work. Neither image leaks a mount/md or hangs. diff --git a/tests/TC118-device-not-found.md b/tests/TC118-device-not-found.md deleted file mode 100644 index b066584..0000000 --- a/tests/TC118-device-not-found.md +++ /dev/null @@ -1,137 +0,0 @@ -# Test Case: Missing, Invalid, and Orphaned External Device - -**Test ID**: TC118-device-not-found -**Category**: Multi-Device / Error Handling -**Priority**: Critical - -## Objective - -Verify exact missing/short/duplicate/path errors, forced GEOM orphan behavior, -concurrent provider ownership, and complete vnode/cdev/consumer cleanup. - -## Fixture qualification - -Use the fresh `multi2` set. The manifest must show `/cold-slot2.bin` has one -chunk with device ID 2; do not read it before the orphan step. -`multi2-slot2-short.blob` is exactly one block below slot 2's declared size. - -## Mount-time errors - -Set up the test directory and load the exact KLD once: - -```sh -I=/root/repo22-g6/images -mkdir -p /mnt/g6 /mnt/g6b -kldload /root/repo22-g6/erofs.ko -``` - -Run each case separately with `truss`, detaching all attached md units after -each failure: - -1. Split primary with no `device.N`: `ENXIO`. -2. Only `device.1`: `ENXIO`. -3. Valid slot 1 plus `multi2-slot2-short.blob`: `ENXIO`. -4. The same md91 assigned to slots 1 and 2: `EINVAL`. -5. Primary md90 also assigned as slot 1, with valid md92 for slot 2: `EINVAL`. - -The short-media command is: - -```sh -mdconfig -a -t vnode -f "$I/multi2-primary.erofs" -u 90 -mdconfig -a -t vnode -f "$I/multi2-slot1.blob" -u 91 -mdconfig -a -t vnode -f "$I/multi2-slot2-short.blob" -u 92 -truss -f -o /tmp/tc118-short.truss mount -t erofs -o ro \ - -o device.1=/dev/md91 -o device.2=/dev/md92 /dev/md90 /mnt/g6 -tail -20 /tmp/tc118-short.truss -mdconfig -d -u 92 -mdconfig -d -u 91 -mdconfig -d -u 90 -``` - -After every case, `mount -p | awk '$3 == "erofs"'` and matching -`kern.geom.conftxt` consumer output must be empty. - -## Forced orphan - -Attach fresh providers and mount without reading `cold-slot2.bin`: - -```sh -mdconfig -a -t vnode -f "$I/multi2-primary.erofs" -u 90 -mdconfig -a -t vnode -f "$I/multi2-slot1.blob" -u 91 -mdconfig -a -t vnode -f "$I/multi2-slot2.blob" -u 92 -mount -t erofs -o ro -o device.1=/dev/md91 -o device.2=/dev/md92 \ - /dev/md90 /mnt/g6 -truss -f -o /tmp/tc118-ebusy.truss mdconfig -d -u 92 -tail -20 /tmp/tc118-ebusy.truss -mdconfig -d -u 92 -o force -truss -f -o /tmp/tc118-orphan-read.truss sha256 \ - /mnt/g6/cold-slot2.bin -tail -20 /tmp/tc118-orphan-read.truss -``` - -Normal detach must return `EBUSY`; forced detach must complete; the first cold -slot-2 read must return exactly `ENXIO` without panic, hang, or leaked vnode. - -```sh -umount /mnt/g6 -mdconfig -d -u 91 -mdconfig -d -u 90 -``` - -## Concurrent mount ownership - -```sh -mdconfig -a -t vnode -f "$I/multi2-primary.erofs" -u 90 -mdconfig -a -t vnode -f "$I/multi2-slot1.blob" -u 91 -mdconfig -a -t vnode -f "$I/multi2-slot2.blob" -u 92 -mount -t erofs -o ro -o device.1=/dev/md91 -o device.2=/dev/md92 \ - /dev/md90 /mnt/g6 -truss -f -o /tmp/tc118-concurrent.truss mount -t erofs -o ro \ - -o device.1=/dev/md91 -o device.2=/dev/md92 /dev/md90 /mnt/g6b -tail -20 /tmp/tc118-concurrent.truss -sha256 /mnt/g6/tc006.bin -``` - -The second mount must return exactly `EBUSY`, and the first mount must remain -readable. Record a kernel failure and do not edit `src/` if FreeBSD permits a -second mount of an already-open provider set. - -```sh -umount /mnt/g6 -mdconfig -d -u 92 -mdconfig -d -u 91 -mdconfig -d -u 90 -``` - -## Pathname preservation - -```sh -truss -f -o /tmp/tc118-primary-enoent.truss mount -t erofs -o ro \ - /no/such/repo22-g6-primary /mnt/g6 -tail -20 /tmp/tc118-primary-enoent.truss -mdconfig -a -t vnode -f "$I/multi2-primary.erofs" -u 90 -mdconfig -a -t vnode -f "$I/multi2-slot1.blob" -u 91 -truss -f -o /tmp/tc118-slot-enoent.truss mount -t erofs -o ro \ - -o device.1=/dev/md91 -o device.2=/no/such/repo22-g6-slot2 \ - /dev/md90 /mnt/g6 -tail -20 /tmp/tc118-slot-enoent.truss -``` - -Both operations must preserve exactly `ENOENT`; the external failure must also -release the already-open slot-1 consumer. - -## Cleanup and zero-state check - -```sh -mdconfig -d -u 91 -mdconfig -d -u 90 -kldunload erofs -mount -p | awk '$3 == "erofs" { print }' -mdconfig -l -kldstat -n erofs 2>/dev/null || true -sysctl -n kern.geom.conftxt | grep -E 'md9[0-2]|erofs' || true -dmesg | tail -120 -``` - -All four final lifecycle outputs must be empty. The dmesg suffix must contain -no panic, trap, assertion, watchdog, or unexpected EROFS error. diff --git a/tests/TC119-data-crc-mismatch.md b/tests/TC119-data-crc-mismatch.md deleted file mode 100644 index 0dbf22e..0000000 --- a/tests/TC119-data-crc-mismatch.md +++ /dev/null @@ -1,56 +0,0 @@ -# Test Case: Uncompressed Data Corruption Boundary - -**Test ID**: TC119-data-crc-mismatch -**Category**: Data Integrity Semantics -**Priority**: High - -## Objective - -Verify the actual EROFS integrity boundary. FLAT_PLAIN payload has no per-file -CRC, so one targeted raw-data bit flip is returned to the caller and detected -only by external byte/hash comparison. A fictional kernel data-CRC error is not -expected. - -## Fixture Qualification - -```sh -work=$(mktemp -d /tmp/repo22-tc119.XXXXXX) -tests/prepare_error_fixtures.sh "$work/fixtures" -cc -O2 -Wall -Wextra -std=c17 tests/read_probe.c -o "$work/read_probe" -python3 tests/erofs_fixture.py inspect \ - "$work/fixtures/raw-data-corrupt.erofs" --path /plain.bin -grep '^raw-data-corrupt ' "$work/fixtures/fixture-evidence.txt" -wc -c "$work/fixtures/valid-plain.erofs" \ - "$work/fixtures/raw-data-corrupt.erofs" -sha256 -q "$work/fixtures/raw-data-corrupt.erofs" -``` - -The mutator resolves `/plain.bin`, proves nonempty FLAT_PLAIN layout, flips bit -`0x80` at file offset 257, records the absolute provider byte, preserves media -size, and verifies that the unchanged superblock checksum remains valid. - -## FreeBSD Procedure - -```sh -mkdir "$work/mnt" -unit=$(mdconfig -a -t vnode -f "$work/fixtures/raw-data-corrupt.erofs") -mount -t erofs -o ro "/dev/$unit" "$work/mnt" -source_hash=$(sha256 -q "$work/fixtures/source/plain.bin") -mounted_hash=$(sha256 -q "$work/mnt/plain.bin") -test "$source_hash" != "$mounted_hash" -"$work/read_probe" pread "$work/fixtures/source/plain.bin" 257 1 \ - "$work/source-byte.bin" -"$work/read_probe" pread "$work/mnt/plain.bin" 257 1 \ - "$work/mounted-byte.bin" -! cmp -s "$work/source-byte.bin" "$work/mounted-byte.bin" -cmp "$work/fixtures/source/control.txt" "$work/mnt/control.txt" -umount "$work/mnt" -mdconfig -d -u "${unit#md}" -``` - -## Expected Results - -Mount and all reads succeed. The exact target byte and complete file hash differ -from source, while control data matches. No `EIO` or data-CRC diagnostic is -expected. Record source, image, mounted-file hashes, target bytes, dmesg delta, -and cleanup. diff --git a/tests/TC120-empty-file.md b/tests/TC120-empty-file.md deleted file mode 100644 index 82d7f36..0000000 --- a/tests/TC120-empty-file.md +++ /dev/null @@ -1,54 +0,0 @@ -# Test Case: Empty File Read - -**Test ID**: TC120-empty-file -**Category**: Boundary & Stress -**Priority**: Medium -**Regression**: None - -## Objective - -Verify exact zero-length inode, EOF, seek, hash, and close behavior. - -## Preconditions - -- Complete `G7-MANUAL-SETUP.md`. -- Load the exact G7 KLD by full path. -- Verify the guest copy of `SOURCE-SHA256SUMS` before mounting. - -## Manual Steps - -```sh -G7=/root/repo22-g7 -FIX=$G7/fixtures -SRC=$FIX/sources/boundaries/empty.bin -IMAGE=$FIX/images/boundaries.erofs -MNT=/mnt/repo22-g7-tc120 -E=$G7/evidence/TC120 -mkdir -p "$MNT" "$E" -unit=$(mdconfig -a -t vnode -f "$IMAGE") -printf 'md=%s\n' "$unit" | tee "$E/provider.txt" -mount -t erofs -o ro "/dev/$unit" "$MNT" -stat -f 'size=%z mode=%Sp name=%N' "$SRC" "$MNT/empty.bin" \ - | tee "$E/stat.txt" -source_hash=$(sha256 -q "$SRC") -target_hash=$(sha256 -q "$MNT/empty.bin") -printf 'source=%s\ntarget=%s\n' "$source_hash" "$target_hash" \ - | tee "$E/hashes.txt" -test "$source_hash" = "$target_hash" -cmp "$SRC" "$MNT/empty.bin" -"$G7/g7_probe" empty "$MNT/empty.bin" | tee "$E/probe.txt" -umount "$MNT" -mdconfig -d -u "${unit#md}" -test -z "$(mount -p | awk -v m="$MNT" '$2 == m { print }')" -test -z "$(mdconfig -l | tr ' ' '\n' | grep -x "$unit")" -``` - -## PASS Gate - -- Both sizes are exactly 0 and both SHA256 values are the empty-file hash. -- `cmp` exits 0. -- The probe reports `read_bytes=0`, both seek offsets 0, and exit 0. -- The exact mount and md provider are absent after cleanup. - -Any kernel panic, trap, hang, or unexpected EROFS dmesg error is KFAIL and -requires an issue report without changing `src`. diff --git a/tests/TC121-maximum-file-size.md b/tests/TC121-maximum-file-size.md deleted file mode 100644 index c47ad86..0000000 --- a/tests/TC121-maximum-file-size.md +++ /dev/null @@ -1,64 +0,0 @@ -# Test Case: Sparse 64-bit File Boundary - -**Test ID**: TC121-maximum-file-size -**Category**: Boundary & Stress -**Priority**: High -**Regression**: None - -## Objective - -Verify a reproducible sparse file across 2 GiB and 4 GiB offset boundaries, -including exact 64-bit size, hole bytes, marker bytes, and EOF behavior. - -## Preconditions - -- Complete `G7-MANUAL-SETUP.md` and its fixture self-check. -- The source is exactly 4294971393 bytes and consumes less than 1 MiB of host - blocks; this is a practical sparse boundary, not a fabricated 16 TiB run. - -## Manual Steps - -```sh -G7=/root/repo22-g7 -FIX=$G7/fixtures -SRC=$FIX/sources/boundaries/maximum/sparse-boundary.bin -IMAGE=$FIX/images/boundaries.erofs -MNT=/mnt/repo22-g7-tc121 -E=$G7/evidence/TC121 -mkdir -p "$MNT" "$E" -stat -f 'source_size=%z source_blocks=%b blocksize=%k' "$SRC" \ - | tee "$E/source-stat.txt" -test "$(stat -f %z "$SRC")" -eq 4294971393 -test "$(stat -f %b "$SRC")" -lt 2048 -unit=$(mdconfig -a -t vnode -f "$IMAGE") -mount -t erofs -o ro "/dev/$unit" "$MNT" -TARGET=$MNT/maximum/sparse-boundary.bin -stat -f 'target_size=%z target_blocks=%b blocksize=%k' "$TARGET" \ - | tee "$E/target-stat.txt" -test "$(stat -f %z "$TARGET")" -eq 4294971393 -: > "$E/ranges.txt" -while IFS="$(printf '\t')" read -r label offset length; do - "$G7/g7_probe" range "$SRC" "$TARGET" "$offset" "$length" \ - > "$E/range-$label.txt" - rc=$? - printf 'label=%s rc=%s\n' "$label" "$rc" | tee -a "$E/ranges.txt" - cat "$E/range-$label.txt" | tee -a "$E/ranges.txt" - test "$rc" -eq 0 || exit 1 -done < "$FIX/SPARSE-RANGES.tsv" -"$G7/g7_probe" eof "$TARGET" 4294971393 | tee "$E/eof.txt" -umount "$MNT" -mdconfig -d -u "${unit#md}" -test -z "$(mount -p | awk -v m="$MNT" '$2 == m { print }')" -test -z "$(mdconfig -l | tr ' ' '\n' | grep -x "$unit")" -``` - -## PASS Gate - -- Source and mounted sizes are exactly 4294971393. -- Every listed range exits 0 with `mismatches=0`; the list covers a pure hole, - block edge, 2 GiB edge, 4 GiB edge, and final 64 bytes. -- A read at exact EOF returns 0 bytes with errno 0. -- The guest remains responsive and cleanup removes the exact mount/provider. - -A range mismatch, overflow errno, panic, trap, or hang is KFAIL. Do not change -kernel source during this regression group. diff --git a/tests/TC122-deep-directory-tree.md b/tests/TC122-deep-directory-tree.md deleted file mode 100644 index 9a04e70..0000000 --- a/tests/TC122-deep-directory-tree.md +++ /dev/null @@ -1,56 +0,0 @@ -# Test Case: Deep Directory Tree - -**Test ID**: TC122-deep-directory-tree -**Category**: Boundary & Stress -**Priority**: Medium -**Regression**: None - -## Objective - -Verify exact traversal, lookup, `getcwd`, and payload integrity through 128 -directory levels. - -## Preconditions - -- Complete `G7-MANUAL-SETUP.md`. -- `DEEP-PATH` came from the verified fixture, not a hand-written `a/b/...` - placeholder. - -## Manual Steps - -```sh -G7=/root/repo22-g7 -FIX=$G7/fixtures -SRCROOT=$FIX/sources/boundaries -IMAGE=$FIX/images/boundaries.erofs -MNT=/mnt/repo22-g7-tc122 -E=$G7/evidence/TC122 -mkdir -p "$MNT" "$E" -deep_path=$(cat "$FIX/DEEP-PATH") -levels=$(printf '%s\n' "$deep_path" | awk -F/ '{ print NF - 2 }') -printf 'levels=%s\npath=%s\n' "$levels" "$deep_path" \ - | tee "$E/path.txt" -test "$levels" -eq 128 -unit=$(mdconfig -a -t vnode -f "$IMAGE") -mount -t erofs -o ro "/dev/$unit" "$MNT" -cmp "$SRCROOT/$deep_path" "$MNT/$deep_path" -source_hash=$(sha256 -q "$SRCROOT/$deep_path") -target_hash=$(sha256 -q "$MNT/$deep_path") -printf 'source=%s\ntarget=%s\n' "$source_hash" "$target_hash" \ - | tee "$E/hashes.txt" -test "$source_hash" = "$target_hash" -deep_dir=$(dirname "$deep_path") -(cd "$MNT/$deep_dir" && pwd -P) | tee "$E/getcwd.txt" -test "$(cat "$E/getcwd.txt")" = "$MNT/$deep_dir" -umount "$MNT" -mdconfig -d -u "${unit#md}" -test -z "$(mount -p | awk -v m="$MNT" '$2 == m { print }')" -test -z "$(mdconfig -l | tr ' ' '\n' | grep -x "$unit")" -``` - -## PASS Gate - -- The metadata count is exactly 128 directory levels. -- Deepest source/mount `cmp` and SHA256 values match. -- `pwd -P` returns the exact mounted deepest directory. -- No stack warning, panic, hang, or leaked mount/provider occurs. diff --git a/tests/TC123-long-filename.md b/tests/TC123-long-filename.md deleted file mode 100644 index 2fc6969..0000000 --- a/tests/TC123-long-filename.md +++ /dev/null @@ -1,55 +0,0 @@ -# Test Case: 255-byte Filename - -**Test ID**: TC123-long-filename -**Category**: Boundary & Stress -**Priority**: Medium -**Regression**: None - -## Objective - -Verify exact `NAME_MAX` lookup, readdir name bytes, stat, and file content. - -## Preconditions - -- Complete `G7-MANUAL-SETUP.md`. -- Use the fixture's ASCII `LONG-NAME`; its byte count must be 255. - -## Manual Steps - -```sh -G7=/root/repo22-g7 -FIX=$G7/fixtures -SRCROOT=$FIX/sources/boundaries -IMAGE=$FIX/images/boundaries.erofs -MNT=/mnt/repo22-g7-tc123 -E=$G7/evidence/TC123 -mkdir -p "$MNT" "$E" -long_name=$(cat "$FIX/LONG-NAME") -name_bytes=$(printf '%s' "$long_name" | wc -c | tr -d ' ') -printf 'name_bytes=%s\nname=%s\n' "$name_bytes" "$long_name" \ - | tee "$E/name.txt" -test "$name_bytes" -eq 255 -unit=$(mdconfig -a -t vnode -f "$IMAGE") -mount -t erofs -o ro "/dev/$unit" "$MNT" -(cd "$SRCROOT" && find longname -type f -print | sort) > "$E/source.names" -(cd "$MNT" && find longname -type f -print | sort) > "$E/target.names" -cmp "$E/source.names" "$E/target.names" -test "$(wc -l < "$E/target.names" | tr -d ' ')" -eq 1 -stat -f 'size=%z name=%N' "$MNT/longname/$long_name" | tee "$E/stat.txt" -cmp "$SRCROOT/longname/$long_name" "$MNT/longname/$long_name" -source_hash=$(sha256 -q "$SRCROOT/longname/$long_name") -target_hash=$(sha256 -q "$MNT/longname/$long_name") -printf 'source=%s\ntarget=%s\n' "$source_hash" "$target_hash" \ - | tee "$E/hashes.txt" -test "$source_hash" = "$target_hash" -umount "$MNT" -mdconfig -d -u "${unit#md}" -test -z "$(mount -p | awk -v m="$MNT" '$2 == m { print }')" -test -z "$(mdconfig -l | tr ' ' '\n' | grep -x "$unit")" -``` - -## PASS Gate - -- The filename is exactly 255 bytes and readdir returns that one exact name. -- Stat succeeds; source/mount `cmp` and SHA256 values match. -- No truncation, extra entry, panic, hang, or cleanup leak occurs. diff --git a/tests/TC124-many-small-files.md b/tests/TC124-many-small-files.md deleted file mode 100644 index e25d802..0000000 --- a/tests/TC124-many-small-files.md +++ /dev/null @@ -1,60 +0,0 @@ -# Test Case: Many Small Files - -**Test ID**: TC124-many-small-files -**Category**: Boundary & Stress -**Priority**: High -**Regression**: None - -## Objective - -Verify exact names, count, sizes, and hashes for 12,000 one-KiB files spread -over 12 directories. - -## Preconditions - -- Complete `G7-MANUAL-SETUP.md` and verify the source inventory. - -## Manual Steps - -```sh -G7=/root/repo22-g7 -FIX=$G7/fixtures -SRCROOT=$FIX/sources/boundaries -IMAGE=$FIX/images/boundaries.erofs -MNT=/mnt/repo22-g7-tc124 -E=$G7/evidence/TC124 -mkdir -p "$MNT" "$E" -unit=$(mdconfig -a -t vnode -f "$IMAGE") -mount -t erofs -o ro "/dev/$unit" "$MNT" -(cd "$SRCROOT" && find many-small -type f -print | sort) > "$E/source.names" -(cd "$MNT" && find many-small -type f -print | sort) > "$E/target.names" -source_count=$(wc -l < "$E/source.names" | tr -d ' ') -target_count=$(wc -l < "$E/target.names" | tr -d ' ') -printf 'source_count=%s\ntarget_count=%s\n' "$source_count" "$target_count" \ - | tee "$E/counts.txt" -test "$source_count" -eq 12000 -test "$target_count" -eq 12000 -cmp "$E/source.names" "$E/target.names" -(cd "$SRCROOT" && find many-small -type f -exec sha256sum {} + | sort) \ - > "$E/source.hashes" -(cd "$MNT" && find many-small -type f -exec sha256sum {} + | sort) \ - > "$E/target.hashes" -cmp "$E/source.hashes" "$E/target.hashes" -test "$(awk '$3 ~ /^sources\/boundaries\/many-small\// && $2 != 1024 \ - { bad++ } END { print bad + 0 }' \ - "$FIX/SOURCE-INVENTORY.tsv")" -eq 0 || \ - awk '$3 ~ /^sources\/boundaries\/many-small\// && $2 != 1024' \ - "$FIX/SOURCE-INVENTORY.tsv" -umount "$MNT" -mdconfig -d -u "${unit#md}" -test -z "$(mount -p | awk -v m="$MNT" '$2 == m { print }')" -test -z "$(mdconfig -l | tr ' ' '\n' | grep -x "$unit")" -``` - -## PASS Gate - -- Source and mounted name lists are identical and contain exactly 12,000 - files. -- Every mounted SHA256 row equals the corresponding source row. -- Every many-small source file is exactly 1024 bytes. -- No missing inode, panic, hang, or cleanup leak occurs. diff --git a/tests/TC125-large-directory.md b/tests/TC125-large-directory.md deleted file mode 100644 index c220390..0000000 --- a/tests/TC125-large-directory.md +++ /dev/null @@ -1,64 +0,0 @@ -# Test Case: Large Directory - -**Test ID**: TC125-large-directory -**Category**: Boundary & Stress -**Priority**: High -**Regression**: None - -## Objective - -Verify exact readdir and lookup behavior for one directory containing 12,000 -direct child files. Record scan timing without a cross-machine time gate. - -## Preconditions - -- Complete `G7-MANUAL-SETUP.md` and verify the source inventory. - -## Manual Steps - -```sh -G7=/root/repo22-g7 -FIX=$G7/fixtures -SRCROOT=$FIX/sources/boundaries -IMAGE=$FIX/images/boundaries.erofs -MNT=/mnt/repo22-g7-tc125 -E=$G7/evidence/TC125 -mkdir -p "$MNT" "$E" -unit=$(mdconfig -a -t vnode -f "$IMAGE") -mount -t erofs -o ro "/dev/$unit" "$MNT" -(cd "$SRCROOT" && find large-dir -type f -print | sort) > "$E/source.names" -/usr/bin/time -p sh -c \ - 'cd "$1" && find large-dir -type f -print | sort > "$2"' \ - sh "$MNT" "$E/target.names" 2> "$E/readdir.time" -cat "$E/readdir.time" -source_count=$(wc -l < "$E/source.names" | tr -d ' ') -target_count=$(wc -l < "$E/target.names" | tr -d ' ') -printf 'source_count=%s\ntarget_count=%s\n' "$source_count" "$target_count" \ - | tee "$E/counts.txt" -test "$source_count" -eq 12000 -test "$target_count" -eq 12000 -cmp "$E/source.names" "$E/target.names" -for name in entry-00000.txt entry-00001.txt entry-05999.txt \ - entry-11998.txt entry-11999.txt; do - cmp "$SRCROOT/large-dir/$name" "$MNT/large-dir/$name" - printf '%s source=%s target=%s\n' "$name" \ - "$(sha256 -q "$SRCROOT/large-dir/$name")" \ - "$(sha256 -q "$MNT/large-dir/$name")" -done | tee "$E/lookups.txt" -(cd "$SRCROOT" && find large-dir -type f -exec sha256sum {} + | sort) \ - > "$E/source.hashes" -(cd "$MNT" && find large-dir -type f -exec sha256sum {} + | sort) \ - > "$E/target.hashes" -cmp "$E/source.hashes" "$E/target.hashes" -umount "$MNT" -mdconfig -d -u "${unit#md}" -test -z "$(mount -p | awk -v m="$MNT" '$2 == m { print }')" -test -z "$(mdconfig -l | tr ' ' '\n' | grep -x "$unit")" -``` - -## PASS Gate - -- Exact sorted names and all 12,000 source/mount hashes match. -- The five boundary/middle lookups match their sources. -- Readdir timing is recorded but has no fixed TCG or cross-machine limit. -- No readdir error, panic, hang, or cleanup leak occurs. diff --git a/tests/TC126-concurrent-reads.md b/tests/TC126-concurrent-reads.md deleted file mode 100644 index 15dad93..0000000 --- a/tests/TC126-concurrent-reads.md +++ /dev/null @@ -1,77 +0,0 @@ -# Test Case: Concurrent Reads - -**Test ID**: TC126-concurrent-reads -**Category**: Boundary & Stress -**Priority**: High -**Regression**: None - -## Objective - -Verify 16 simultaneous full-file reads, with every PID, exit code, expected -source hash, and mounted-file hash recorded and checkable. - -## Preconditions - -- Complete `G7-MANUAL-SETUP.md` and verify the workload source inventory. - -## Manual Steps - -```sh -G7=/root/repo22-g7 -FIX=$G7/fixtures -SRC=$FIX/sources/workloads/concurrent -IMAGE=$FIX/images/workloads.erofs -MNT=/mnt/repo22-g7-tc126 -E=$G7/evidence/TC126 -mkdir -p "$MNT" "$E" -unit=$(mdconfig -a -t vnode -f "$IMAGE") -mount -t erofs -o ro "/dev/$unit" "$MNT" -: > "$E/pids.tsv" -index=0 -while [ "$index" -lt 16 ]; do - file=$(printf 'reader-%02d.bin' "$index") - expected=$(sha256 -q "$SRC/$file") - ( - actual=$(sha256 -q "$MNT/concurrent/$file") - printf 'worker=%02d file=%s expected=%s actual=%s\n' \ - "$index" "$file" "$expected" "$actual" - test "$actual" = "$expected" - ) > "$E/worker-$(printf '%02d' "$index").log" 2>&1 & - pid=$! - printf '%02d\t%s\n' "$index" "$pid" >> "$E/pids.tsv" - index=$((index + 1)) -done -failed=0 -: > "$E/waits.tsv" -while IFS="$(printf '\t')" read -r worker pid; do - wait "$pid" - rc=$? - printf '%s\t%s\t%s\n' "$worker" "$pid" "$rc" >> "$E/waits.tsv" - test "$rc" -eq 0 || failed=$((failed + 1)) -done < "$E/pids.tsv" -cat "$E/pids.tsv" "$E/waits.tsv" "$E"/worker-*.log -test "$failed" -eq 0 -test "$(wc -l < "$E/pids.tsv" | tr -d ' ')" -eq 16 -test "$(wc -l < "$E/waits.tsv" | tr -d ' ')" -eq 16 -alive=0 -while IFS="$(printf '\t')" read -r worker pid; do - if kill -0 "$pid" 2>/dev/null; then - printf 'still-alive worker=%s pid=%s\n' "$worker" "$pid" - alive=$((alive + 1)) - fi -done < "$E/pids.tsv" -test "$alive" -eq 0 -umount "$MNT" -mdconfig -d -u "${unit#md}" -test -z "$(mount -p | awk -v m="$MNT" '$2 == m { print }')" -test -z "$(mdconfig -l | tr ' ' '\n' | grep -x "$unit")" -``` - -## PASS Gate - -- Exactly 16 PIDs and 16 waits are recorded; every wait exit code is 0. -- Every worker log contains identical expected/source and actual/mounted - SHA256 values. -- None of the recorded PIDs remains alive; no `killall` or unrelated-process - cleanup is used. -- No deadlock, panic, hang, dmesg error, mount leak, or provider leak occurs. diff --git a/tests/TC127-memory-pressure.md b/tests/TC127-memory-pressure.md deleted file mode 100644 index b970f7a..0000000 --- a/tests/TC127-memory-pressure.md +++ /dev/null @@ -1,114 +0,0 @@ -# Test Case: Controlled Memory Pressure - -**Test ID**: TC127-memory-pressure -**Category**: Boundary & Stress -**Priority**: High -**Regression**: None - -## Objective - -Verify correct EROFS reads while three synchronized FreeBSD processes touch -memory up to per-process RCTL virtual-memory denial, then prove allocation -failure, release, recovery, PID exit, and read integrity. - -## Preconditions - -- Complete `G7-MANUAL-SETUP.md`. -- `sysctl -n kern.racct.enable` must print 1. Record an ENV result instead of - inventing pressure if this loader facility cannot be enabled. -- Use `rctl vmemoryuse:deny`; do not use the invalid `jail -m 256M` command. - -## Manual Steps - -```sh -G7=/root/repo22-g7 -FIX=$G7/fixtures -SRC=$FIX/sources/workloads/pressure/pressure.bin -IMAGE=$FIX/images/workloads.erofs -MNT=/mnt/repo22-g7-tc127 -E=$G7/evidence/TC127 -mkdir -p "$MNT" "$E" -test "$(sysctl -n kern.racct.enable)" -eq 1 -sysctl kern.racct.enable | tee "$E/racct.txt" -rctl | tee "$E/rctl-before.txt" -vmstat -H 1 5 | tee "$E/vmstat-before.txt" -unit=$(mdconfig -a -t vnode -f "$IMAGE") -mount -t erofs -o ro "/dev/$unit" "$MNT" -: > "$E/pids.tsv" -index=1 -while [ "$index" -le 3 ]; do - rm -f "$E/go-$index" "$E/stop-$index" "$E/pressure-$index.log" - "$G7/g7_probe" pressure "$E/go-$index" "$E/stop-$index" 1024 \ - > "$E/pressure-$index.log" 2>&1 & - pid=$! - printf '%s\t%s\n' "$index" "$pid" >> "$E/pids.tsv" - index=$((index + 1)) -done -attempt=0 -while [ "$attempt" -lt 30 ]; do - ready=$(grep -l '^state=waiting ' "$E"/pressure-*.log 2>/dev/null \ - | wc -l | tr -d ' ') - test "$ready" -eq 3 && break - sleep 1 - attempt=$((attempt + 1)) -done -test "$ready" -eq 3 -: > "$E/rctl-rules.txt" -while IFS="$(printf '\t')" read -r worker pid; do - rctl -a "process:$pid:vmemoryuse:deny=640M" - rctl -l "process:$pid" >> "$E/rctl-rules.txt" - touch "$E/go-$worker" -done < "$E/pids.tsv" -attempt=0 -while [ "$attempt" -lt 120 ]; do - holding=$(grep -l '^state=holding .*allocation_failure=ENOMEM ' \ - "$E"/pressure-*.log 2>/dev/null | wc -l | tr -d ' ') - test "$holding" -eq 3 && break - sleep 1 - attempt=$((attempt + 1)) -done -test "$holding" -eq 3 -vmstat -H 1 10 | tee "$E/vmstat-pressure.txt" -rctl -hu process:$(awk 'NR == 1 { print $2 }' "$E/pids.tsv") \ - | tee "$E/rctl-utilization.txt" -source_hash=$(sha256 -q "$SRC") -target_hash=$(sha256 -q "$MNT/pressure/pressure.bin") -printf 'source=%s\ntarget=%s\n' "$source_hash" "$target_hash" \ - | tee "$E/hashes.txt" -test "$source_hash" = "$target_hash" -cmp "$SRC" "$MNT/pressure/pressure.bin" -while IFS="$(printf '\t')" read -r worker pid; do - touch "$E/stop-$worker" -done < "$E/pids.tsv" -failed=0 -: > "$E/waits.tsv" -while IFS="$(printf '\t')" read -r worker pid; do - wait "$pid" - rc=$? - printf '%s\t%s\t%s\n' "$worker" "$pid" "$rc" >> "$E/waits.tsv" - test "$rc" -eq 0 || failed=$((failed + 1)) - rctl -r "process:$pid:vmemoryuse:deny=640M" 2>/dev/null || true -done < "$E/pids.tsv" -test "$failed" -eq 0 -grep '^state=released .*recovery=ok exit=0$' "$E"/pressure-*.log -vmstat -H 1 10 | tee "$E/vmstat-after.txt" -rctl | tee "$E/rctl-after.txt" -rm -f "$E"/go-* "$E"/stop-* -cat "$E/pids.tsv" "$E/waits.tsv" "$E"/pressure-*.log -umount "$MNT" -mdconfig -d -u "${unit#md}" -test -z "$(mount -p | awk -v m="$MNT" '$2 == m { print }')" -test -z "$(mdconfig -l | tr ' ' '\n' | grep -x "$unit")" -``` - -## PASS Gate - -- RACCT/RCTL availability and before/pressure/after `vmstat` are recorded. -- Three persisted PIDs each report `allocation_failure=ENOMEM`, later report - exact released bytes and `recovery=ok`, and exit 0 when waited. -- The mounted 96 MiB file SHA256 and full bytes match the source while pressure - is held. -- G7 RCTL rules, synchronization files, mount, and provider are removed. - -Different page counts or allocation totals are recording-only. Read mismatch, -panic, hang, unreclaimed process, or unexpected kernel error is KFAIL. diff --git a/tests/TC128-sequential-throughput.md b/tests/TC128-sequential-throughput.md deleted file mode 100644 index e2d6436..0000000 --- a/tests/TC128-sequential-throughput.md +++ /dev/null @@ -1,62 +0,0 @@ -# Test Case: Sequential Throughput - -**Test ID**: TC128-sequential-throughput -**Category**: Boundary & Stress -**Priority**: Medium -**Regression**: None - -## Objective - -Record reproducible sequential-read metrics for the exact 256 MiB fixture -under QEMU TCG while gating PASS only on complete and correct reads. - -## Preconditions - -- Complete `G7-MANUAL-SETUP.md` and record the QEMU TCG configuration. -- Do not apply fixed MB/s thresholds across hosts. - -## Manual Steps - -```sh -G7=/root/repo22-g7 -FIX=$G7/fixtures -SRC=$FIX/sources/workloads/sequential/sequential.bin -IMAGE=$FIX/images/workloads.erofs -MNT=/mnt/repo22-g7-tc128 -E=$G7/evidence/TC128 -mkdir -p "$MNT" "$E" -test "$(stat -f %z "$SRC")" -eq 268435456 -unit=$(mdconfig -a -t vnode -f "$IMAGE") -mount -t erofs -o ro "/dev/$unit" "$MNT" -TARGET=$MNT/sequential/sequential.bin -source_hash=$(sha256 -q "$SRC") -target_hash=$(sha256 -q "$TARGET") -printf 'bytes=268435456\nsource=%s\ntarget=%s\n' \ - "$source_hash" "$target_hash" | tee "$E/hashes.txt" -test "$source_hash" = "$target_hash" -cmp "$SRC" "$TARGET" -: > "$E/runs.tsv" -run=1 -while [ "$run" -le 3 ]; do - /usr/bin/time -p dd if="$TARGET" of=/dev/null bs=1m \ - > "$E/run-$run.log" 2>&1 - rc=$? - printf '%s\t%s\n' "$run" "$rc" >> "$E/runs.tsv" - cat "$E/run-$run.log" - test "$rc" -eq 0 || exit 1 - run=$((run + 1)) -done -cat "$E/runs.tsv" -test "$(awk '$2 != 0 { bad++ } END { print bad + 0 }' "$E/runs.tsv")" -eq 0 -umount "$MNT" -mdconfig -d -u "${unit#md}" -test -z "$(mount -p | awk -v m="$MNT" '$2 == m { print }')" -test -z "$(mdconfig -l | tr ' ' '\n' | grep -x "$unit")" -``` - -## PASS Gate - -- Mounted size is exactly 268435456; SHA256 and full `cmp` match the source. -- All three `dd` reads transfer the full file and exit 0. -- Bytes/second and real/user/sys times are reported as TCG metrics only. -- No fixed speed gate, panic, hang, or cleanup leak occurs. diff --git a/tests/TC129-random-read-pattern.md b/tests/TC129-random-read-pattern.md deleted file mode 100644 index cd53f08..0000000 --- a/tests/TC129-random-read-pattern.md +++ /dev/null @@ -1,68 +0,0 @@ -# Test Case: Deterministic Random Reads - -**Test ID**: TC129-random-read-pattern -**Category**: Boundary & Stress -**Priority**: Medium -**Regression**: None - -## Objective - -Compare every 4 KiB random read to the exact source at deterministic offsets, -and record TCG IOPS/latency without fixed performance thresholds. - -## Preconditions - -- Complete `G7-MANUAL-SETUP.md` and compile the exact `g7_probe.c`. -- Do not use `fio` without data verification and do not impose 1000 IOPS or - 10 ms cross-machine gates. - -## Manual Steps - -```sh -G7=/root/repo22-g7 -FIX=$G7/fixtures -SRC=$FIX/sources/workloads/random/random.bin -IMAGE=$FIX/images/workloads.erofs -MNT=/mnt/repo22-g7-tc129 -E=$G7/evidence/TC129 -mkdir -p "$MNT" "$E" -test "$(stat -f %z "$SRC")" -eq 67108864 -unit=$(mdconfig -a -t vnode -f "$IMAGE") -mount -t erofs -o ro "/dev/$unit" "$MNT" -TARGET=$MNT/random/random.bin -source_hash=$(sha256 -q "$SRC") -target_hash=$(sha256 -q "$TARGET") -printf 'source=%s\ntarget=%s\n' "$source_hash" "$target_hash" \ - | tee "$E/hashes.txt" -test "$source_hash" = "$target_hash" -cmp "$SRC" "$TARGET" -: > "$E/runs.tsv" -run=1 -while [ "$run" -le 3 ]; do - "$G7/g7_probe" random "$SRC" "$TARGET" \ - 0x6a09e667f3bcc909 16384 4096 > "$E/run-$run.log" - rc=$? - digest=$(awk '{ for (i=1; i<=NF; i++) if ($i ~ /^digest=/) print $i }' \ - "$E/run-$run.log") - printf '%s\t%s\t%s\n' "$run" "$rc" "$digest" >> "$E/runs.tsv" - cat "$E/run-$run.log" - test "$rc" -eq 0 || exit 1 - run=$((run + 1)) -done -cat "$E/runs.tsv" -test "$(awk '$2 != 0 { bad++ } END { print bad + 0 }' "$E/runs.tsv")" -eq 0 -test "$(awk '{ print $3 }' "$E/runs.tsv" | sort -u | wc -l | tr -d ' ')" -eq 1 -grep 'mismatches=0' "$E"/run-*.log -umount "$MNT" -mdconfig -d -u "${unit#md}" -test -z "$(mount -p | awk -v m="$MNT" '$2 == m { print }')" -test -z "$(mdconfig -l | tr ' ' '\n' | grep -x "$unit")" -``` - -## PASS Gate - -- Full source/mount SHA256 and bytes match. -- Each run compares exactly 16,384 source/target 4 KiB reads, reports zero - mismatches, exits 0, and produces the same deterministic digest. -- IOPS, mean microseconds, and elapsed time are recorded only as TCG metrics. -- No fixed speed gate, panic, hang, or cleanup leak occurs. diff --git a/tests/TC130-mixed-workload.md b/tests/TC130-mixed-workload.md deleted file mode 100644 index 600d477..0000000 --- a/tests/TC130-mixed-workload.md +++ /dev/null @@ -1,121 +0,0 @@ -# Test Case: Mixed Workload - -**Test ID**: TC130-mixed-workload -**Category**: Boundary & Stress -**Priority**: High -**Regression**: None - -## Objective - -Run simultaneous sequential compare, full hash, two deterministic random-read -streams, multi-file hashes, recursive names, and stat-size inventory. Persist -and wait every PID, then prove complete process/mount/provider cleanup. - -## Preconditions - -- Complete `G7-MANUAL-SETUP.md` and TC126-TC129 prerequisites. - -## Manual Steps - -```sh -G7=/root/repo22-g7 -FIX=$G7/fixtures -SRC=$FIX/sources/workloads -IMAGE=$FIX/images/workloads.erofs -MNT=/mnt/repo22-g7-tc130 -E=$G7/evidence/TC130 -mkdir -p "$MNT" "$E" -rm -f "$E"/*.tmp "$E"/pids.tsv "$E"/waits.tsv -unit=$(mdconfig -a -t vnode -f "$IMAGE") -mount -t erofs -o ro "/dev/$unit" "$MNT" -: > "$E/pids.tsv" -( - cmp "$SRC/sequential/sequential.bin" \ - "$MNT/sequential/sequential.bin" -) > "$E/sequential.log" 2>&1 & -pid=$!; printf 'sequential\t%s\n' "$pid" >> "$E/pids.tsv" -( - expected=$(sha256 -q "$SRC/pressure/pressure.bin") - actual=$(sha256 -q "$MNT/pressure/pressure.bin") - printf 'expected=%s actual=%s\n' "$expected" "$actual" - test "$actual" = "$expected" -) > "$E/full-hash.log" 2>&1 & -pid=$!; printf 'full-hash\t%s\n' "$pid" >> "$E/pids.tsv" -( - "$G7/g7_probe" random "$SRC/random/random.bin" \ - "$MNT/random/random.bin" 0xbb67ae8584caa73b 8192 4096 -) > "$E/random-a.log" 2>&1 & -pid=$!; printf 'random-a\t%s\n' "$pid" >> "$E/pids.tsv" -( - "$G7/g7_probe" random "$SRC/random/random.bin" \ - "$MNT/random/random.bin" 0x3c6ef372fe94f82b 8192 4096 -) > "$E/random-b.log" 2>&1 & -pid=$!; printf 'random-b\t%s\n' "$pid" >> "$E/pids.tsv" -( - index=0 - while [ "$index" -lt 16 ]; do - file=$(printf 'reader-%02d.bin' "$index") - expected=$(sha256 -q "$SRC/concurrent/$file") - actual=$(sha256 -q "$MNT/concurrent/$file") - printf '%s expected=%s actual=%s\n' "$file" "$expected" "$actual" - test "$actual" = "$expected" || exit 1 - index=$((index + 1)) - done -) > "$E/multi-hash.log" 2>&1 & -pid=$!; printf 'multi-hash\t%s\n' "$pid" >> "$E/pids.tsv" -( - (cd "$SRC" && find . -type f -print | sort) > "$E/source.names.tmp" - (cd "$MNT" && find . -type f -print | sort) > "$E/target.names.tmp" - cmp "$E/source.names.tmp" "$E/target.names.tmp" -) > "$E/names.log" 2>&1 & -pid=$!; printf 'names\t%s\n' "$pid" >> "$E/pids.tsv" -( - (cd "$SRC" && find . -type f -exec stat -f '%N %z' {} + | sort) \ - > "$E/source.stat.tmp" - (cd "$MNT" && find . -type f -exec stat -f '%N %z' {} + | sort) \ - > "$E/target.stat.tmp" - cmp "$E/source.stat.tmp" "$E/target.stat.tmp" -) > "$E/stat.log" 2>&1 & -pid=$!; printf 'stat\t%s\n' "$pid" >> "$E/pids.tsv" -test "$(wc -l < "$E/pids.tsv" | tr -d ' ')" -eq 7 -failed=0 -: > "$E/waits.tsv" -while IFS="$(printf '\t')" read -r worker pid; do - wait "$pid" - rc=$? - printf '%s\t%s\t%s\n' "$worker" "$pid" "$rc" >> "$E/waits.tsv" - test "$rc" -eq 0 || failed=$((failed + 1)) -done < "$E/pids.tsv" -cat "$E/pids.tsv" "$E/waits.tsv" "$E"/*.log -test "$failed" -eq 0 -test "$(wc -l < "$E/waits.tsv" | tr -d ' ')" -eq 7 -alive=0 -while IFS="$(printf '\t')" read -r worker pid; do - if kill -0 "$pid" 2>/dev/null; then - printf 'still-alive worker=%s pid=%s\n' "$worker" "$pid" - alive=$((alive + 1)) - fi -done < "$E/pids.tsv" -test "$alive" -eq 0 -mv "$E/source.names.tmp" "$E/source.names" -mv "$E/target.names.tmp" "$E/target.names" -mv "$E/source.stat.tmp" "$E/source.stat" -mv "$E/target.stat.tmp" "$E/target.stat" -test -z "$(find "$E" -name '*.tmp' -print)" -test -z "$(find "$G7/evidence" \( -name 'go-*' -o -name 'stop-*' \) -print)" -umount "$MNT" -mdconfig -d -u "${unit#md}" -test -z "$(mount -p | awk -v m="$MNT" '$2 == m { print }')" -test -z "$(mdconfig -l | tr ' ' '\n' | grep -x "$unit")" -``` - -## PASS Gate - -- Exactly seven PIDs and seven waits are persisted; every child exits 0 and - no recorded PID remains alive. -- Sequential bytes, full hash, both random streams, all 16 file hashes, exact - names, and exact stat sizes match their sources. -- All temporary, synchronization, process, mount, and md resources are gone; - evidence logs remain for audit. -- No `killall`, timeout masking, deadlock, panic, hang, or unexpected dmesg - error occurs. diff --git a/tests/TC131-nfs-export-basic.md b/tests/TC131-nfs-export-basic.md deleted file mode 100644 index 73dc0bb..0000000 --- a/tests/TC131-nfs-export-basic.md +++ /dev/null @@ -1,143 +0,0 @@ -# Test Case: FreeBSD NFSv3 Export Basic Functionality - -**Test ID**: TC131-nfs-export-basic -**Category**: NFS Export -**Priority**: Critical -**Regression**: EROFS `VOP_VPTOFH` / `VFS_FHTOVP` / export updates - -## Objective - -Verify that FreeBSD 15 mountd can install an export on a read-only EROFS -mount, that nfsd can resolve EROFS file handles, and that regular files, -directories, symlinks, and FIFOs are visible through a real NFSv3 client. - -## Preconditions - -- FreeBSD 15 amd64 server/client, optionally the same host over loopback. -- Root privilege for module, md, NFS service, `getfh`, and `fhopen` operations. -- `erofs.ko`, `test-nfs.erofs`, and `tests/nfs_fh_tool.c` available in the - guest. -- The fixture contains `basic/regular.txt`, `basic/subdir`, - `basic/link-to-regular`, and `basic/test.fifo`. - -## Procedure - -1. Build the module and helper: - - ```sh - ./build.sh - cc -O2 -Wall -Wextra -std=c17 tests/nfs_fh_tool.c -o nfs_fh_tool - ``` - -2. Load and mount EROFS on a fixed md unit: - - ```sh - kldload ./erofs.ko - mdconfig -a -t vnode -f test-nfs.erofs -u 42 - mkdir -p /mnt/repo22-erofs - mount -t erofs -o ro /dev/md42 /mnt/repo22-erofs - mount -v | grep /mnt/repo22-erofs - ``` - - The line must show `read-only` and must not show `NFS exported` before - mountd processes `/etc/exports`. - -3. Configure the real FreeBSD mountd/nfsd flow: - - ```sh - cp -p /etc/exports /tmp/exports.before 2>/dev/null || true - printf '%s\n' \ - '/mnt/repo22-erofs -ro -maproot=root -network 127.0.0.0 -mask 255.0.0.0' \ - > /etc/exports - service rpcbind onestart - service mountd onestart - service nfsd onestart - service mountd onereload - rpcinfo -p 127.0.0.1 - showmount -e 127.0.0.1 - mount -v | grep /mnt/repo22-erofs - ``` - - After reload, the EROFS mount line must show `NFS exported`. The flag is - installed by the generic FreeBSD export layer after the filesystem accepts - the export-only `MNT_UPDATE`; EROFS must not set it during the initial mount. - -4. Mount the export through NFSv3 and confirm the negotiated options: - - ```sh - mkdir -p /mnt/repo22-nfs - mount_nfs -o nfsv3,tcp,rdirplus 127.0.0.1:/mnt/repo22-erofs \ - /mnt/repo22-nfs - nfsstat -m - ``` - -5. Verify all required vnode types and read-only behavior: - - ```sh - cmp /mnt/repo22-nfs/basic/regular.txt \ - /mnt/repo22-erofs/basic/regular.txt - test -d /mnt/repo22-nfs/basic/subdir - test "$(readlink /mnt/repo22-nfs/basic/link-to-regular)" = regular.txt - test -p /mnt/repo22-nfs/basic/test.fifo - test "$(stat -f %i /mnt/repo22-nfs/basic/regular.txt)" = \ - "$(stat -f %i /mnt/repo22-erofs/basic/regular.txt)" - ! touch /mnt/repo22-nfs/write-must-fail - ``` - -6. Exercise both shared- and exclusive-lock file-handle resolution locally: - - ```sh - ./nfs_fh_tool capture /mnt/repo22-erofs/basic/regular.txt regular.fh - ./nfs_fh_tool stat regular.fh - ./nfs_fh_tool cat regular.fh regular.out - cmp regular.out /mnt/repo22-erofs/basic/regular.txt - ``` - - `fhstat` requests a shared lock and `fhopen` requests an exclusive lock. - -7. Confirm that NFSv3 used READDIRPLUS and did not issue writes: - - ```sh - nfsstat -c - nfsstat -s - ``` - -## Expected Results - -- mountd accepts the EROFS export-only update. -- `showmount -e` lists the EROFS path and `mount -v` shows `NFS exported` only - after mountd reloads exports. -- NFSv3 regular reads, directory traversal, symlink lookup, FIFO metadata, and - inode numbers match the direct EROFS mount. -- Writes fail with `EROFS`/`Read-only file system`. -- `fhstat` and `fhopen` both resolve the same 64-bit EROFS NID. -- NFS statistics show READDIRPLUS traffic and zero successful write RPCs. - -## Cleanup - -Always remove clients before stopping the loopback server: - -```sh -umount /mnt/repo22-nfs -: > /etc/exports -service mountd onereload -service nfsd onestop -service mountd onestop -service rpcbind onestop -umount /mnt/repo22-erofs -mdconfig -d -u 42 -kldunload erofs -rm -f /etc/exports -``` - -Restore a pre-existing `/etc/exports` instead of removing it when applicable. - -## Notes - -- The EROFS FreeBSD file-handle payload is 16 bytes: - `len`, `pad`, `nid_hi`, `nid_lo`, and a nonzero superblock-seeded per-inode - generation matching `va_gen`. -- Do not use Linux `exportfs`; FreeBSD mountd installs exports through a mount - update carrying the `export` option. -- Throughput is recorded for information only; correctness has no fixed MB/s - threshold. diff --git a/tests/TC132-nfs-file-handle-stability.md b/tests/TC132-nfs-file-handle-stability.md deleted file mode 100644 index b782b59..0000000 --- a/tests/TC132-nfs-file-handle-stability.md +++ /dev/null @@ -1,97 +0,0 @@ -# Test Case: FreeBSD NFS File-Handle Stability and Image Replacement - -**Test ID**: TC132-nfs-file-handle-stability -**Category**: NFS Export -**Priority**: Critical -**Regression**: Constant generation could resolve an old handle in a replacement image - -## Objective - -Verify 16-byte EROFS FIDs with full 64-bit NIDs and stable superblock-seeded -per-inode generations. The same image remounted on the same explicit md unit -must preserve the complete `fhandle_t`; a different image with the same device -number and NID must reject the old handle with `ESTALE`. Verify `va_gen` and -handle generation are identical. - -## Preconditions - -- FreeBSD 15 and `tests/nfs_fh_tool.c` compiled as `nfs_fh_tool`. -- Deterministic `nfs-a.erofs` and `nfs-b.erofs` from the metadata/VFS fixture - generator. They contain the same NIDs/content but different UUIDs. -- Use one explicit md unit for every remount/replacement step because FreeBSD - stores the filesystem ID outside the filesystem-private FID. - -## Direct Handle Procedure - -Perform these steps **before starting NFS clients**, so the direct EROFS mount -can be unmounted and replaced without `EBUSY`. - -1. Attach `nfs-a.erofs` to md80 and mount it. -2. Capture regular, directory, symlink (`lcapture`), and FIFO handles. For the - regular file: - - ```sh - ./nfs_fh_tool describe a.fh - ./nfs_fh_tool stat a.fh - stat -f 'gen=%v ino=%i' /mnt/repo22-erofs/basic/regular.txt - ``` - - Assert `len=16`, `pad=0`, full NID preservation, nonzero generation, and - `describe gen == stat st_gen`. -3. Verify malformed versus stale classification: - - ```sh - ./nfs_fh_tool mutate a.fh bad-len.fh len 15 - ./nfs_fh_tool mutate a.fh bad-pad.fh pad 1 - ./nfs_fh_tool mutate a.fh bad-gen.fh gen_xor 1 - ./nfs_fh_tool mutate a.fh bad-nid.fh nid_hi 0xffffffff - - ./nfs_fh_tool expect-stat bad-len.fh EINVAL - ./nfs_fh_tool expect-open bad-len.fh EINVAL - ./nfs_fh_tool expect-stat bad-pad.fh EINVAL - ./nfs_fh_tool expect-open bad-pad.fh EINVAL - ./nfs_fh_tool expect-stat bad-gen.fh ESTALE - ./nfs_fh_tool expect-open bad-gen.fh ESTALE - ./nfs_fh_tool expect-stat bad-nid.fh ESTALE - ./nfs_fh_tool expect-open bad-nid.fh ESTALE - ``` - -4. Unmount, detach, reattach **the same image** to md80, remount, capture - `a-remount.fh`, compare complete handle bytes, and read through the old - handle. -5. Unmount/detach, attach `nfs-b.erofs` to the same md80, and remount. Capture - `b.fh`; it must have the same NID and fsid but a different generation. - Both `fhstat(a.fh)` and `fhopen(a.fh)` must return `ESTALE`, while `b.fh` - succeeds. - -## NFS Restart Procedure - -After the replacement test is cleaned up, mount/export the qualified NFS -fixture and mount the NFS client. Keep a client descriptor open across nfsd -restart, but prevent the restarted daemon from inheriting the test descriptor: - -```sh -exec 3< /mnt/repo22-nfs/basic/regular.txt -service nfsd onerestart 3<&- -cat <&3 > open-after-restart.out -exec 3<&- -cmp open-after-restart.out /mnt/repo22-erofs/basic/regular.txt -``` - -Without `3<&-` on the service command, nfsd inherits the NFS-client descriptor -and can keep the client mount busy during cleanup. - -## Metabox and Multidevice Regression - -- Capture/resolve a metabox bit-63 NID and mutate it beyond the metabox backing - range; expect `ESTALE` only for invalid NID/generation. -- For an external-data file, a valid handle must still resolve after the data - provider is detached; the subsequent read preserves `ENXIO`/I/O error rather - than rewriting it to `ESTALE`. - -## Cleanup - -Unmount NFS clients first while rpcbind/mountd/nfsd still run. Then clear and -reload `/etc/exports`, stop nfsd, mountd, and rpcbind, unmount EROFS, detach md -providers, and unload the module. Assert every mount, md, module, service PID, -and export entry is gone. diff --git a/tests/TC133-nfs-export-stress.md b/tests/TC133-nfs-export-stress.md deleted file mode 100644 index a82c2a1..0000000 --- a/tests/TC133-nfs-export-stress.md +++ /dev/null @@ -1,176 +0,0 @@ -# Test Case: FreeBSD NFSv3 READDIRPLUS and Export Stress - -**Test ID**: TC133-nfs-export-stress -**Category**: NFS Export -**Priority**: High -**Regression**: directory cookies, EOF, concurrency, and export stability - -## Objective - -Stress the FreeBSD NFSv3 export with a 10,000+ entry directory, small negotiated -readdir sizes, repeated cookie pagination, multiple concurrent traversals, and -parallel reads/stat operations. Verify no duplicates, omissions, premature EOF, -stale handles, panic, or resource leak. - -## Preconditions - -- Complete TC131 server setup. -- A fixture with at least 12,050 deterministically named files in `bigdir`, a - `concurrent` file set, and a larger sequential-read file. -- Four client mount points are available. - -## Procedure - -1. Mount four NFSv3 client views with READDIRPLUS. Request small readdir sizes - on three mounts and record the effective values: - - ```sh - mount_nfs -o nfsv3,tcp,rdirplus,readdirsize=512 \ - 127.0.0.1:/mnt/repo22-erofs /mnt/repo22-nfs-512 - mount_nfs -o nfsv3,tcp,rdirplus,readdirsize=1024 \ - 127.0.0.1:/mnt/repo22-erofs /mnt/repo22-nfs-1024 - mount_nfs -o nfsv3,tcp,rdirplus,readdirsize=4096 \ - 127.0.0.1:/mnt/repo22-erofs /mnt/repo22-nfs-4096 - mount_nfs -o nfsv3,tcp,rdirplus \ - 127.0.0.1:/mnt/repo22-erofs /mnt/repo22-nfs-default - nfsstat -m - ``` - - FreeBSD may clamp very small requests to its client minimum. Record both the - requested and effective values; a clamp is an environment characteristic, - not a test failure. - -2. Traverse each mount and verify exact names, counts, and uniqueness: - - ```sh - jot -w file-%05d 12050 0 > expected.names - for M in 512 1024 4096 default; do - find /mnt/repo22-nfs-$M/bigdir -type f -maxdepth 1 | \ - sed 's#.*/##' > names.$M - test "$(wc -l < names.$M)" -eq 12050 - sort names.$M | uniq -d > duplicates.$M - test ! -s duplicates.$M - sort names.$M > sorted.$M - cmp expected.names sorted.$M - done - sha256 expected.names sorted.* - ``` - -3. Verify `.` and `..` are each returned exactly once and EOF is reached only - after all real entries: - - ```sh - ls -a1 /mnt/repo22-nfs-default/bigdir > dot-list - test "$(wc -l < dot-list)" -eq 12052 - test "$(grep -cx '\.' dot-list)" -eq 1 - test "$(grep -cx '\.\.' dot-list)" -eq 1 - ``` - -4. Force cold cookie pagination by repeatedly unmounting/remounting the small - client and comparing every complete listing: - - ```sh - for round in 1 2 3 4 5; do - umount /mnt/repo22-nfs-512 - mount_nfs -o nfsv3,tcp,rdirplus,readdirsize=512 \ - 127.0.0.1:/mnt/repo22-erofs /mnt/repo22-nfs-512 - find /mnt/repo22-nfs-512/bigdir -type f -maxdepth 1 | \ - sed 's#.*/##' | sort > cold-$round.sorted - cmp expected.names cold-$round.sorted - done - ``` - -5. Run at least eight traversal workers, three rounds each, distributed across - the four mounts. Save every PID and wait for every PID individually; a bare - final `wait` does not prove that all background jobs succeeded: - - ```sh - pids="" - worker=0 - for round in 1 2 3; do - for M in 512 1024 4096 default; do - worker=$((worker + 1)) - (find /mnt/repo22-nfs-$M/bigdir -type f -maxdepth 1 | \ - sed 's#.*/##' | sort | cmp expected.names -) \ - > walk-$worker.log 2>&1 & - pids="$pids $!" - done - done - status=0 - for pid in $pids; do - wait "$pid" || status=1 - done - test "$status" -eq 0 - ``` - -6. Run parallel data and metadata operations: - - ```sh - pids="" - for M in 512 1024 4096 default; do - (find /mnt/repo22-nfs-$M/concurrent -type f -maxdepth 1 -print0 | \ - xargs -0 -n 1 -P 8 cat > /dev/null) > cat-$M.log 2>&1 & - pids="$pids $!" - (find /mnt/repo22-nfs-$M/concurrent -type f -maxdepth 1 -print0 | \ - xargs -0 -n 1 -P 8 stat -f '%i %z' > /dev/null) \ - > stat-$M.log 2>&1 & - pids="$pids $!" - done - status=0 - for pid in $pids; do - wait "$pid" || status=1 - done - test "$status" -eq 0 - ``` - -7. Record throughput as information only: - - ```sh - /usr/bin/time -p dd if=/mnt/repo22-nfs-default/throughput.dat \ - of=/dev/null bs=1m - ``` - -8. Record NFS and kernel state: - - ```sh - nfsstat -c - nfsstat -s - vmstat -H - dmesg - ``` - - READDIRPLUS must be non-zero. Timed-out RPCs, retries, successful write RPCs, - stale-handle messages, traps, and panics must be zero. - -## Expected Results - -- Every listing contains exactly 12,050 unique expected names. -- Cold remount and concurrent listings have identical SHA256 values. -- Cookie pagination has no duplicate or missing entry and no premature EOF. -- Dot-omitted EROFS directories expose one `.` and one `..` through NFS. -- Concurrent reads and stats complete without stale handles or deadlock. -- READDIRPLUS activity is visible in client/server statistics. -- Throughput is reported without a fixed pass/fail threshold. - -## Cleanup - -Unmount every NFS client while nfsd is still running, then reload an empty -export list, stop services, unmount EROFS, detach md, and unload the module: - -```sh -for M in 512 1024 4096 default; do - umount /mnt/repo22-nfs-$M -done -: > /etc/exports -service mountd onereload -service nfsd onestop -service mountd onestop -service rpcbind onestop -umount /mnt/repo22-erofs -mdconfig -d -u 42 -kldunload erofs -``` - -Final EROFS/NFS mount counts, md units, loaded EROFS modules, service PIDs, and -unexpected new dmesg lines must all be zero. Do not stop NFS services before -client unmounts; hard localhost NFS mounts can otherwise become uninterruptible. diff --git a/tests/TC134-metabox-shared-nonzero-base.md b/tests/TC134-metabox-shared-nonzero-base.md deleted file mode 100644 index c9c6e96..0000000 --- a/tests/TC134-metabox-shared-nonzero-base.md +++ /dev/null @@ -1,26 +0,0 @@ -# Test Case: Metabox shared xattr with nonzero base - -**Test ID**: TC134-metabox-shared-nonzero-base -**Fixture**: `metabox-plain.erofs` - -## Objective - -Verify that metabox shared IDs are relative to the nonzero -`xattr_blkaddr << blkszbits`. Require manifest fields `xattr_blkaddr=1`, -carrier xattr base 4096, shared record IDs/offsets, and bit-63 inode NIDs. - -## Manual steps - -```sh -getextattr -qq -x user metaboxshared /mnt/repo22-g4/metabox-a -getextattr -qq -x user metaboxshared /mnt/repo22-g4/metabox-b -sha256 -q /mnt/repo22-g4/metabox-a /mnt/repo22-g4/metabox-b -stat -f 'inode=%i size=%z' /mnt/repo22-g4/metabox-a \ - /mnt/repo22-g4/metabox-b -``` - -## Expected results - -Both values are `nonzero-base`; data hashes match source and inode numbers -retain bit 63. A read from logical offset `shared_id * 4` must not occur. -Clean up. diff --git a/tests/TC135-prefix-metabox-and-primary-fallback.md b/tests/TC135-prefix-metabox-and-primary-fallback.md deleted file mode 100644 index 87dab3b..0000000 --- a/tests/TC135-prefix-metabox-and-primary-fallback.md +++ /dev/null @@ -1,36 +0,0 @@ -# Test Case: Long-prefix backing selection - -**Test ID**: TC135-prefix-metabox-and-primary-fallback -**Fixtures**: `metabox-plain.erofs`, `basic.erofs`, `prefix-primary.erofs` - -## Objective - -Verify prefix table selection in order: metabox, packed inode, then primary -metadata. The manifest must prove each backing and record all changed super -fields and image hashes. - -## Manual steps - -Use a separate attach/mount/cleanup cycle for each image: - -```sh -# metabox-plain.erofs -lsextattr user /mnt/repo22-g4/metabox-a -getextattr -qq -x user repo22.application.component.setting \ - /mnt/repo22-g4/metabox-a - -# basic.erofs (packed backing) -lsextattr user /mnt/repo22-g4/prefix-user-0 -getextattr -qq -x user repo22.application.component.setting \ - /mnt/repo22-g4/prefix-user-0 - -# prefix-primary.erofs (PLAIN_XATTR_PFX) -lsextattr user /mnt/repo22-g4/prefix-user-0 -getextattr -qq -x user repo22.application.component.setting \ - /mnt/repo22-g4/prefix-user-0 -``` - -## Expected results - -Values are `long-prefix-value`, `prefix-value-0`, and `prefix-value-0`. -Every cycle cleans its mount and md provider. diff --git a/tests/TC136-metabox-truncated-super-extension.md b/tests/TC136-metabox-truncated-super-extension.md deleted file mode 100644 index 1a0a6d5..0000000 --- a/tests/TC136-metabox-truncated-super-extension.md +++ /dev/null @@ -1,26 +0,0 @@ -# Test Case: Truncated metabox super extension - -**Test ID**: TC136-metabox-truncated-super-extension -**Fixtures**: `bad-metabox-truncated-extension.erofs`, `bad-ishare-prefix-id.erofs` - -## Objective - -Reject `METABOX` when `sb_extslots=0` leaves `metabox_nid` undeclared, and -reject `ISHARE_XATTRS` when its prefix ID equals `xattr_prefix_count`. - -## Manual steps - -For each image, attach it and trace the read-only mount: - -```sh -truss -f -o /tmp/tc136.truss mount -t erofs -o ro \ - /dev/${unit} /mnt/repo22-g4 -grep nmount /tmp/tc136.truss -mount | grep repo22-g4 || true -mdconfig -d -u "${unit#md}" -``` - -## Expected results - -Both `nmount` calls return `EINTEGRITY` (97). The extension bytes are not -consumed, no mount appears, and detach succeeds without a vnode/md leak. diff --git a/tests/TC137-xattr-declared-image-bounds.md b/tests/TC137-xattr-declared-image-bounds.md deleted file mode 100644 index fc19eb1..0000000 --- a/tests/TC137-xattr-declared-image-bounds.md +++ /dev/null @@ -1,37 +0,0 @@ -# Test Case: Xattr declared-image bounds - -**Test ID**: TC137-xattr-declared-image-bounds -**Fixtures**: `basic.erofs`, `bad-shared-declared-bounds.erofs`, `bad-prefix-declared-bounds.erofs` - -## Objective - -Ensure shared and prefix reads are bounded by superblock `blocks`, even when -the provider contains a valid-looking appended sentinel. Require manifest -provider/declared sizes, sentinel offsets, redirected ID/start, and hashes. - -## Manual steps - -Run three independent cycles: - -```sh -# basic.erofs control -getextattr -qq -x user shared_key /mnt/repo22-g4/shared-a -getextattr -qq -x user local /mnt/repo22-g4/shared-a - -# bad-shared-declared-bounds.erofs -stat -f 'size=%z' /mnt/repo22-g4/shared-a -getextattr -qq -x user local /mnt/repo22-g4/shared-a -truss -o /tmp/tc137-shared.truss getextattr -qq user shared_key \ - /mnt/repo22-g4/shared-a -grep extattr_get_file /tmp/tc137-shared.truss - -# bad-prefix-declared-bounds.erofs -truss -f -o /tmp/tc137-prefix.truss mount -t erofs -o ro \ - /dev/${unit} /mnt/repo22-g4 -grep nmount /tmp/tc137-prefix.truss -``` - -## Expected results - -Control passes. The shared sentinel get and prefix mount return `EINTEGRITY` -(97); normal data and `local` remain readable. Sentinel bytes never appear. diff --git a/tests/TC138-acl-linux-order-and-empty-header.md b/tests/TC138-acl-linux-order-and-empty-header.md deleted file mode 100644 index 84a7237..0000000 --- a/tests/TC138-acl-linux-order-and-empty-header.md +++ /dev/null @@ -1,27 +0,0 @@ -# Test Case: Linux ACL order and empty header - -**Test ID**: TC138-acl-linux-order-and-empty-header -**Fixture**: `basic.erofs` - -## Objective - -Accept Linux ACL tag phases without sorting named IDs, treat a version-only -header as mode fallback, and reject duplicate IDs, bad phase, and bad perms. -The manifest must record rebuilt xattr bodies and `i_xattr_icount` fields. - -## Manual steps - -```sh -getfacl -n /mnt/repo22-g4/acl-unordered -getfacl -n /mnt/repo22-g4/acl-header -truss -o /tmp/tc138-duplicate.truss getfacl -n \ - /mnt/repo22-g4/acl-duplicate -truss -o /tmp/tc138-phase.truss getfacl -n /mnt/repo22-g4/acl-phase -truss -o /tmp/tc138-perm.truss getfacl -n /mnt/repo22-g4/acl-perm -grep __acl_get_file /tmp/tc138-*.truss -``` - -## Expected results - -The first ACL lists user 3002 before 2002. Header-only output is mode-derived -`rw-/r--/r--`. All three malformed ACLs return `EINTEGRITY` (97). Clean up. diff --git a/tests/TC139-fifo-acl-xattr-readonly.md b/tests/TC139-fifo-acl-xattr-readonly.md deleted file mode 100644 index 56a8edb..0000000 --- a/tests/TC139-fifo-acl-xattr-readonly.md +++ /dev/null @@ -1,31 +0,0 @@ -# Test Case: FIFO ACL and xattr read-only VOPs - -**Test ID**: TC139-fifo-acl-xattr-readonly -**Fixture**: `basic.erofs`, `/fifo-node` - -## Objective - -Verify FIFO stat, ACL, and xattr reads without opening FIFO data, and require -read-only errors for all mutations. The transformer must record the regular -placeholder to FIFO `i_mode` change and ACL body fields. - -## Manual steps - -```sh -stat -f 'type=%HT mode=%Sp size=%z inode=%i' /mnt/repo22-g4/fifo-node -getfacl -n /mnt/repo22-g4/fifo-node -lsextattr system /mnt/repo22-g4/fifo-node -lsextattr user /mnt/repo22-g4/fifo-node -getextattr -qq -x system posix_acl_access /mnt/repo22-g4/fifo-node -getextattr -qq -x user fifo_note /mnt/repo22-g4/fifo-node -truss -o /tmp/tc139-acl.truss setfacl -m u::rwx /mnt/repo22-g4/fifo-node -truss -o /tmp/tc139-set.truss setextattr user repo22 changed \ - /mnt/repo22-g4/fifo-node -truss -o /tmp/tc139-del.truss rmextattr system posix_acl_access \ - /mnt/repo22-g4/fifo-node -``` - -## Expected results - -Stat reports FIFO, reads return the exact ACL and `fifo-readonly-xattr`, and -all three mutations return `EROFS` (30). Nothing blocks or logs a trap. Clean up. diff --git a/tests/TC140-compressed-metabox-carrier.md b/tests/TC140-compressed-metabox-carrier.md deleted file mode 100644 index 1d78a90..0000000 --- a/tests/TC140-compressed-metabox-carrier.md +++ /dev/null @@ -1,37 +0,0 @@ -# Test Case: Compressed metabox carrier - -**Test ID**: TC140-compressed-metabox-carrier -**Fixtures**: `metabox-compressed.erofs`, `metabox-fragment.erofs`, four `bad-*` fragment images - -## Objective - -Verify metadata/xattr reads from compressed and fragment-backed compressed -metabox carriers, then reject loop, range, and recursive carrier relations. -Require manifest carrier layout, NIDs, packed size, fragment offset/range, -relocated metadata block, transformed fields, and image/source hashes. - -## Positive steps - -Mount each positive image separately and run: - -```sh -stat -f 'mode=%Sp size=%z inode=%i' /mnt/repo22-g4/metabox-a -sha256 -q /mnt/repo22-g4/metabox-a -lsextattr user /mnt/repo22-g4/metabox-a -getextattr -qq -x user metaboxshared /mnt/repo22-g4/metabox-a -getextattr -qq -x user repo22.application.component.setting \ - /mnt/repo22-g4/metabox-a -getextattr -qq -x user shared-prefix-key /mnt/repo22-g4/metabox-a -``` - -## Negative steps - -Attach and trace-mount `bad-fragment-self-loop.erofs`, -`bad-fragment-range.erofs`, `bad-metabox-recursive-nid.erofs`, and -`bad-packed-recursive-nid.erofs` separately. Record `nmount`, dmesg, -`vmstat -m | grep erofs`, responsiveness, and cleanup after each. - -## Expected results - -Both positives return source hash and exact values. All four negatives return -`EINTEGRITY` (97), with active EROFS allocations, mounts, and md units at zero. diff --git a/tests/TC141-cold-nested-namei.md b/tests/TC141-cold-nested-namei.md deleted file mode 100644 index 1d6de7c..0000000 --- a/tests/TC141-cold-nested-namei.md +++ /dev/null @@ -1,44 +0,0 @@ -# Test Case: Cold Nested Namei and Corruption - -**Test ID**: TC141-cold-nested-namei -**Category**: Directory Lookup -**Priority**: Critical - -## Objective - -Observe nested positive and missing lookup on fresh mounts, complete multi- -block cookies, Linux-compatible padding, and stable EINTEGRITY for three -structured directory corruptions. - -## Procedure - -On a fresh metadata/namei-base.erofs mount, make the first pathname operation: - - ./g3_vfs_probe expect-success open-read \ - /tmp/repo22-g3/mnt/alpha/bravo/charlie/payload.txt - -Compare the payload with the recorded source, require two direct missing -lookups to return errno 2, and run: - - ./readdir_probe /tmp/repo22-g3/mnt/wide 128 - -Repeat the cold positive read and cookie probe on -metadata/namei-padding-nonzero.erofs. - -Finally mount each corruption independently and issue two direct stat calls: - -| Image | Path | Expected errno | -|---|---|---:| -| namei-corrupt-short.erofs | /alpha | 97 | -| namei-corrupt-nameoff.erofs | /alpha | 97 | -| namei-corrupt-name.erofs | /wide/entry-077-abcdefghijklmnopqrstuvwxyz.txt | 97 | - -Run readdir_probe on the affected directory and require its direct syscall -failure to report integrity failure. Unmount and detach between every image. - -## Expected Results - -Fresh positive lookup reads exact data; repeated missing lookup is ENOENT; both -valid variants report all 322 restartable entries. Every corruption remains -EINTEGRITY on both lookup attempts and readdir, without cache masking, panic, -or hang. Record all five image hashes and cleanup. diff --git a/tests/TC142-fragment-backed-compressed-metabox.md b/tests/TC142-fragment-backed-compressed-metabox.md deleted file mode 100644 index 309a0a2..0000000 --- a/tests/TC142-fragment-backed-compressed-metabox.md +++ /dev/null @@ -1,42 +0,0 @@ -# Test Case: Fragment-backed compressed metabox - -**Test ID**: TC142-fragment-backed-compressed-metabox -**Fixture**: `metabox-fragment.erofs` and its four negative variants - -## Objective - -Independently regress packed-carrier initialization before a fragment-backed -metabox read and recursion/range rejection. Do not reuse TC140 guest results. - -## Layout qualification - -`g4_fixtures.py verify` must prove a compressed metabox carrier, whole-file -fragment header with bit 63 set, `fragment_offset + carrier_size <= packed_size`, -ordinary metabox/packed NIDs, bit-63 synthetic inode NIDs, CRC-valid images, -and one-field mutations for self-loop, range, metabox recursion, and packed -recursion. Record source and all image SHA256 values. - -## Positive steps - -```sh -unit=$(mdconfig -a -t vnode -f /tmp/repo22-g4/images/metabox-fragment.erofs) -mount -t erofs -o ro /dev/${unit} /mnt/repo22-g4 -stat -f 'mode=%Sp size=%z inode=%i' /mnt/repo22-g4/metabox-a -sha256 -q /mnt/repo22-g4/metabox-a -lsextattr user /mnt/repo22-g4/metabox-a -getextattr -qq -x user shared-prefix-key /mnt/repo22-g4/metabox-a -getextattr -qq -x user item /mnt/repo22-g4/metabox-a -umount /mnt/repo22-g4 -mdconfig -d -u "${unit#md}" -``` - -## Negative steps - -Trace-mount each of the four negative images in a fresh md cycle. Capture the -exact `nmount` errno/text, pre/post dmesg, `vmstat -m`, guest responsiveness, -and zero mount/md cleanup. - -## Expected results - -Positive file SHA256 and values `shared-value`/`value-000` match source. Every -negative returns `EINTEGRITY` (97), with no leak, hang, panic, trap, or residue. diff --git a/tests/TC143-deflate-zstd-partial-reference.md b/tests/TC143-deflate-zstd-partial-reference.md deleted file mode 100644 index 99fcadc..0000000 --- a/tests/TC143-deflate-zstd-partial-reference.md +++ /dev/null @@ -1,81 +0,0 @@ -# Test Case: DEFLATE and ZSTD Partial References - -**Test ID**: TC143-deflate-zstd-partial-reference - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression -**Priority**: Critical -**Regression**: Non-LZ4 partial-reference decoding - -## Objective -Verify full and partial-reference decoding for raw DEFLATE and ZSTD streams, -including exact full-file SHA256, cross-extent reads, random reads, and -corrupted-stream `EIO` behavior. - -## Preconditions -- FreeBSD 15 guest. -- ZSTDIO-enabled repo22 module for the ZSTD cases. -- Deterministic full-index fixtures whose layout parser proves: - - the full file points at a complete compressed stream; - - the partial file reuses that physical stream; - - the reused HEAD record has `Z_EROFS_LI_PARTIAL_REF` (`0x8000`) set; - - the reused first NONHEAD record has the complete source pcluster's - `D0_CBLKCNT`, not the partial file's original compressed-block count; - - the partial file's logical length is shorter than the source stream's - decompressed length. -- Corrupted copies produced only after locating the target compressed extent; - do not use a blind image offset. - -## Layout Proof -```sh -dump.erofs -s deflate-partial-ref.erofs -dump.erofs --path=/a.dat -e deflate-partial-ref.erofs -dump.erofs --path=/b.dat -e deflate-partial-ref.erofs -dump.erofs -s zstd-partial-ref.erofs -dump.erofs --path=/a.dat -e zstd-partial-ref.erofs -dump.erofs --path=/b.dat -e zstd-partial-ref.erofs -``` - -The byte-level fixture record must assert the original NID, map-header offset, -HEAD record, pblk, and checksum before setting the partial-reference bit or -redirecting a pblk. - -## Test Steps -1. Mount the DEFLATE image and hash `/a.dat` and `/b.dat` completely. -2. Read a range crossing a known DEFLATE logical extent boundary. -3. Read another non-aligned random range and compare it byte-for-byte with the - source. -4. Repeat steps 1-3 for ZSTD. -5. Mount each corrupted copy and read the targeted file through a small C - errno probe. -6. Compare pre/post `dmesg` and active `vmstat -m` EROFS allocations. - -The G5 DEFLATE partial is 100000 bytes because erofs-utils 1.8.6 splits the -1 MiB DEFLATE source according to the 32 KiB DEFLATE window. It reuses the -first 17-block source pcluster and uses offsets 65500/2048 and 90000/4096. -The ZSTD partial is 700000 bytes and uses these checks: - -```sh -dd if=/mnt/repo22/b.dat of=/tmp/guest bs=1 skip=122900 count=512 status=none -dd if=source-b.dat of=/tmp/source bs=1 skip=122900 count=512 status=none -cmp /tmp/guest /tmp/source - -dd if=/mnt/repo22/b.dat of=/tmp/guest bs=1 skip=524287 count=4097 status=none -dd if=source-prefix-b.dat of=/tmp/source bs=1 skip=524287 count=4097 status=none -cmp /tmp/guest /tmp/source -``` - -## Expected Results -- Full DEFLATE and ZSTD streams require complete output and stream completion. -- Partial references succeed after producing the requested logical prefix; - they do not require the reused source frame to finish. -- Complete SHA256 and every boundary/random byte comparison match. -- Targeted corruption returns read errno 5 (`EIO`), with no leaked active - allocation, panic, or trap. -- `control.bin` in each corrupted image remains byte-identical to its source. - -## Cleanup -Unmount each image, detach every md unit, remove temporary range outputs, and -unload the module by its `kldstat` ID. diff --git a/tests/TC144-microlzma-consumption-and-corruption.md b/tests/TC144-microlzma-consumption-and-corruption.md deleted file mode 100644 index 103beb6..0000000 --- a/tests/TC144-microlzma-consumption-and-corruption.md +++ /dev/null @@ -1,52 +0,0 @@ -# Test Case: MicroLZMA Consumption and Corruption - -**Test ID**: TC144-microlzma-consumption-and-corruption - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression -**Priority**: Critical -**Regression**: MicroLZMA full/partial completion semantics - -## Objective -Verify MicroLZMA input-consumption rules for complete streams, early-success -rules for partial references, and deterministic rejection of damaged streams. - -## Preconditions -- FreeBSD 15 guest and repo22 module. -- A full-index LZMA image with: - - `/a.dat`: 1048576-byte complete MicroLZMA stream; - - `/b.dat`: 700000-byte partial reference to `/a.dat`'s pcluster; - - the `/b.dat` HEAD record marked `Z_EROFS_LI_PARTIAL_REF`. - - the reused pblk and `D0_CBLKCNT` both equal `/a.dat`'s values. -- A corrupted copy with a byte changed inside the proven compressed byte range. -- Source files and their SHA256 values available on the guest. - -## Test Steps -1. Prove the pcluster and partial-reference record with `dump.erofs` and a - byte-level parser. -2. Mount the valid image and hash `/a.dat` completely. -3. Hash `/b.dat` completely; this requests only a prefix of the reused stream. -4. Compare reads at offsets 65500 and 524287 against source bytes. -5. Mount the corrupted image and read `/a.dat` with an errno-reporting helper. -6. Confirm the implementation's complete-stream condition includes both - `XZ_STREAM_END` and `buffer.in_pos == srclen`. -7. Compare `vmstat -m` active EROFS allocations and pre/post `dmesg`. - -## Expected Results -- `/a.dat` succeeds only after exact output, `XZ_STREAM_END`, and complete - compressed-input consumption. -- `/b.dat` accepts `XZ_OK` after the requested partial output is filled. -- Both complete SHA256 values and random ranges match. -- The corrupted full stream returns errno 5 (`EIO`). -- `control.bin` in the same corrupted image still matches its source. -- Decoder state is freed on success and error; active EROFS allocations return - to zero after unmount/unload. - -## Cleanup -```sh -umount /mnt/repo22 2>/dev/null || true -mdconfig -d -u "${unit#md}" 2>/dev/null || true -kldunload -i "${module_id}" -``` diff --git a/tests/TC145-zstdio-build-gate.md b/tests/TC145-zstdio-build-gate.md deleted file mode 100644 index 4241ed0..0000000 --- a/tests/TC145-zstdio-build-gate.md +++ /dev/null @@ -1,54 +0,0 @@ -# Test Case: FreeBSD ZSTDIO Build Gate - -**Test ID**: TC145-zstdio-build-gate - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Build / Compression -**Priority**: Critical -**Regression**: Optional-kernel ZSTD symbol references - -## Objective -Verify that repo22 uses FreeBSD's formal ZSTD header/API only when `ZSTDIO` is -enabled, emits no ZSTD symbol references otherwise, rejects ZSTD images -accurately in the disabled build, and reads them in the enabled build. - -## Preconditions -- FreeBSD 15 source tree used by `build.sh`. -- FreeBSD guest kernel built with ZSTDIO, so the enabled module can resolve the - official kernel symbols. -- One LZ4 image and one ZSTD image with known SHA256 output. - -## Test Steps -1. Build without ZSTDIO and save the module outside `build/`: - ```sh - WITH_ZSTDIO=0 ./build.sh - cp build/erofs.ko /tmp/erofs-nozstd.ko - ``` -2. Verify no unresolved `ZSTD_*` or `bcmp` symbol: - ```sh - nm -u /tmp/erofs-nozstd.ko | grep 'ZSTD_' && exit 1 || true - nm -u /tmp/erofs-nozstd.ko | grep -w bcmp && exit 1 || true - ``` -3. Load the disabled module, read the LZ4 control image, then attempt to mount - the ZSTD image. -4. Unload by the exact module ID reported by `kldstat`. -5. Build with ZSTDIO: - ```sh - WITH_ZSTDIO=1 ./build.sh - cp build/erofs.ko /tmp/erofs-zstdio.ko - ``` -6. Verify the undefined ZSTD symbols are official API names from - `` and that `bcmp` is absent. -7. Load the enabled module, mount the same ZSTD image, and verify full SHA256. -8. Unload by exact ID and audit final state. - -## Expected Results -- Disabled build: no `ZSTD_*` references, KLD load succeeds, LZ4 reads, and a - ZSTD image fails mount with `EOPNOTSUPP` and the message - `ZSTD compression requires ZSTDIO support`. -- Enabled build: KLD load succeeds and the ZSTD SHA256 matches. -- Both modules have no unresolved `bcmp`. -- The build rejects `WITH_ZSTDIO` values other than `0` or `1`. -- No module, mount, or md unit remains after cleanup. diff --git a/tests/TC146-head2-interlaced-extent-mapping.md b/tests/TC146-head2-interlaced-extent-mapping.md deleted file mode 100644 index a2be291..0000000 --- a/tests/TC146-head2-interlaced-extent-mapping.md +++ /dev/null @@ -1,85 +0,0 @@ -# Test Case: HEAD2, Interlaced, and Extent Mapping - -**Overall Status Rule**: **PARTIAL** when HEAD2 and interlaced pass but the -explicit mapped-payload subscenario remains **SHELVED**; see -`issues/extent-metadata-fixture-unavailable.md` - -**Test ID**: TC146-head2-interlaced-extent-mapping - -**G5 fixture contract**: Use [G5-MANUAL-SETUP.md](G5-MANUAL-SETUP.md). -Its generated paths, source comparisons, structured corruption offsets, and -cleanup rules supersede placeholder examples in this file. -**Category**: Compression Mapping -**Priority**: Critical -**Regression**: New compressed mapping formats - -## Objective -Verify FreeBSD kernel reads for HEAD2 and interlaced pclusters, and independently -review extent-record mapping when erofs-utils 1.8.6 cannot generate that format. - -## Preconditions -- FreeBSD 15 guest and repo22 module. -- HEAD2 fixture with byte-level assertions: - - incompat bit `EROFS_FEATURE_INCOMPAT_COMPR_HEAD2` is set; - - `Z_EROFS_ADVISE_BIG_PCLUSTER_2` is set; - - a proven full-index HEAD record type is changed from HEAD1 to HEAD2; - - the high algorithm nibble names the decoder used by HEAD2; - - CRC32C is recomputed. -- Interlaced fixture generated by erofs-utils 1.8.6: - ```sh - mkfs.erofs -zlz4 -C4096 -Efragments -T0 \ - interlaced.erofs source-dir - ``` - `dump.erofs -e` must show real 4096-byte plain extents interspersed with - compressed extents. - -## HEAD2 and Interlaced Steps -1. Prove the HEAD2 feature, map-header advise bits, algorithm nibbles, and HEAD2 - record before guest transfer. -2. Mount the HEAD2 image, hash the full file, and compare a read crossing the - first logical pcluster boundary recorded by the manifest. Do not assume the - logical boundary is 65536 merely because the physical pcluster limit is - 65536. -3. Read the targeted corrupted HEAD2 copy and record errno. -4. Mount the erofs-utils 1.8.6 interlaced image, hash the full file, and compare - a range crossing the first compressed/plain transition. -5. Compare pre/post `dmesg` and clean all resources. - -## Extent Metadata Static Review -Run these checks against exactly erofs-utils 1.8.6 and both reference trees: - -```sh -mkfs.erofs -V -grep -R -E 'Z_EROFS_ADVISE_EXTENTS|z_erofs_extent_recsize|struct z_erofs_extent[[:space:]]*\{' \ - /work/build/erofs-utils-v1.8.6-source/include \ - /work/build/erofs-utils-v1.8.6-source/lib -grep -n "struct z_erofs_extent\|z_erofs_extent_recsize" \ - src/erofs_fs.h /work/dev-src-linux/fs/erofs/erofs_fs.h -grep -n "z_erofs_map_blocks_ext" \ - src/zmap.c /work/dev-src-linux/fs/erofs/zmap.c -``` - -Review all four record sizes (4, 8, 16, 32 bytes): -- 4-byte records use the initial 64-bit physical base and accumulated `plen`; -- 8-byte records carry per-record 32-bit physical starts; -- 16/32-byte records use explicit extent counts and binary search by logical - start, with 32-byte records adding `lstart_hi`; -- `plen` format, partial-reference, interlaced/shifted, and final fragment - encodings are decoded in Linux order; -- metadata reads route through the metabox when the inode NID has bit 63; -- malformed zero-count explicit tables and fragment bounds fail closed. - -## Expected Results -- HEAD2 and interlaced full SHA256 and boundary reads match source bytes. -- Targeted HEAD2 corruption returns errno 5 (`EIO`) without a panic/trap. -- Explicit mapped-payload result is **SHELVED**, not PASS, because erofs-utils - 1.8.6 has no extent-record generator or on-disk extent structure and the - structured helper cannot validate a relocated mapped payload. -- The static FreeBSD/Linux format and control-flow review is recorded - separately from guest results. -- TC146 overall is **PARTIAL** unless all three subscenarios have dynamic - positive coverage. - -## Cleanup -Unmount all images, detach md units, unload by exact module ID, and verify no -new `dmesg` lines. diff --git a/tests/TC147-flat-inline-block-bounds.md b/tests/TC147-flat-inline-block-bounds.md deleted file mode 100644 index f8129db..0000000 --- a/tests/TC147-flat-inline-block-bounds.md +++ /dev/null @@ -1,46 +0,0 @@ -# Test Case: FLAT_INLINE Metadata-Block Bounds - -**Test ID**: TC147-flat-inline-block-bounds - -## Objective - -Verify a valid inline payload reads exactly and a checksum-valid inline range -crossing its inode metadata block fails repeatedly with `EINTEGRITY`. - -## Fixture - -Use `images/inline.erofs`, `images/inline-cross-block.erofs`, and -`source/inline/inline.txt`. Evidence records the NID/inode offset, patched -`i_xattr_icount`, inline start at block offset 4068, 31-byte size, valid CRC, -and both image/source hashes. - -## Procedure - -Mount `inline.erofs` on a fresh md unit: - -```sh -stat -f 'size=%z blocks=%b' "$mnt/inline.txt" -cmp /tmp/repo22-g1/source/inline/inline.txt "$mnt/inline.txt" -sha256 /tmp/repo22-g1/source/inline/inline.txt "$mnt/inline.txt" -``` - -Clean it up, then mount `inline-cross-block.erofs` without listing the root: - -```sh -./read_probe expect-error "$mnt/inline.txt" 97 -./read_probe expect-error "$mnt/inline.txt" 97 -set +e -truss -o /tmp/tc147.truss stat "$mnt/inline.txt" -rc=$? -set -e -printf 'stat_rc=%d\n' "$rc" -tail -20 /tmp/tc147.truss -``` - -Record dmesg delta and clean the second mount/provider. - -## Expected Results - -- Positive data matches source and reports 8 sectors. -- Every corrupt lookup/open returns errno 97, never `ENOENT` or stale-vnode - errors; no next-block read, panic, dmesg error, or cleanup residue occurs. diff --git a/tests/TC148-linux-directory-tail-padding.md b/tests/TC148-linux-directory-tail-padding.md deleted file mode 100644 index 65558c5..0000000 --- a/tests/TC148-linux-directory-tail-padding.md +++ /dev/null @@ -1,28 +0,0 @@ -# Test Case: Nonzero Directory Tail Padding - -**Test ID**: TC148-linux-directory-tail-padding -**Category**: Directory Compatibility -**Priority**: Critical - -## Objective - -Verify FreeBSD accepts the structured eight-byte PAD!ERO! patch after the final -directory-name NUL and still returns complete names and restartable cookies. - -## Procedure - -Record metadata/fixture-evidence.txt and the -metadata/namei-padding-nonzero.erofs hash. On a fresh mount: - - printf 'wide entry 079\n' > expected-079.txt - cmp expected-079.txt \ - /tmp/repo22-g3/mnt/wide/entry-079-abcdefghijklmnopqrstuvwxyz.txt - test "$(find /tmp/repo22-g3/mnt/wide -maxdepth 1 -type f | - wc -l)" -eq 320 - ./readdir_probe /tmp/repo22-g3/mnt/wide 128 - -## Expected Results - -The actual patched image mounts, reads exact data, exposes 320 real files, and -reports 322 valid kernel/libc restart positions. Userspace fixture inspection -alone is not PASS. Record image/evidence hashes and cleanup. diff --git a/tests/TC149-vnode-pager-real-faults.md b/tests/TC149-vnode-pager-real-faults.md deleted file mode 100644 index 5f81b71..0000000 --- a/tests/TC149-vnode-pager-real-faults.md +++ /dev/null @@ -1,23 +0,0 @@ -# Test Case: Vnode Pager Real Faults - -**Test ID**: TC149-vnode-pager-real-faults -**Category**: VM Integration -**Priority**: Critical - -## Objective - -Exercise real sequential/randomized mmap faults, partial and full pages around -EOF, read-only shared mappings, and private COW on plain and LZ4 EROFS data. - -## Procedure - -On separate fresh mounts of vfs/vfs-plain.erofs and vfs/vfs-lz4.erofs: - - ./mmap_fault /tmp/repo22-g3/mnt/pager.bin - -## Expected Results - -Both runs print the same 21211-byte logical FNV hash and pass six random page -faults, sequential comparison, EOF zeroing, expected child SIGBUS, EACCES for -writable MAP_SHARED, private COW, and EROFS for O_RDWR. The expected child -SIGBUS dmesg line is evidence, not a kernel failure. Record hashes and cleanup. diff --git a/tests/TC150-48bit-fallback-root.md b/tests/TC150-48bit-fallback-root.md deleted file mode 100644 index 592ded6..0000000 --- a/tests/TC150-48bit-fallback-root.md +++ /dev/null @@ -1,43 +0,0 @@ -# Test Case: 48-bit Fallback Root Union - -**Test ID**: TC150-48bit-fallback-root - -## Objective - -Rerun the Linux-compatible `48BIT=1 && rootnid_8b=0` fallback on the current -exact KLD: the two-byte union remains `rootnid_2b`, not `blocks_hi`. - -## Fixture - -Use `images/fallback-48bit-root2.erofs` and `source/compact/root.txt`. -Structured evidence asserts incompat `0x80`, `rootnid_8b=0`, nonzero -`rootnid_2b`, provider-sized `blocks_lo`, and valid CRC. fsck.erofs 1.8.6 does -not recognize this newer incompat bit. - -## Procedure - -Host: - -```sh -python3 tests/erofs_fixture.py inspect /work/build/repo22-g1/images/fallback-48bit-root2.erofs --path=/root.txt -od -An -tu2 -j 1038 -N 2 /work/build/repo22-g1/images/fallback-48bit-root2.erofs -od -An -tx4 -j 1104 -N 4 /work/build/repo22-g1/images/fallback-48bit-root2.erofs -od -An -tu8 -j 1136 -N 8 /work/build/repo22-g1/images/fallback-48bit-root2.erofs -``` - -Guest after mount: - -```sh -stat -f 'root_ino=%i type=%HT' "$mnt" -cmp /tmp/repo22-g1/source/compact/root.txt "$mnt/root.txt" -sha256 /tmp/repo22-g1/source/compact/root.txt "$mnt/root.txt" -./statfs_probe "$mnt" -df -kT "$mnt" -``` - -## Expected Results - -- Root inode equals `rootnid_2b`; source data matches. -- `statfs` block count equals `blocks_lo` only, with no multi-terabyte size - error. Current exact KLD mount and cleanup pass. -- Historical TC150 evidence cannot substitute for this rerun. diff --git a/tests/TC151-extended-inode-off-max.md b/tests/TC151-extended-inode-off-max.md deleted file mode 100644 index 7d6c12f..0000000 --- a/tests/TC151-extended-inode-off-max.md +++ /dev/null @@ -1,42 +0,0 @@ -# Test Case: Extended Inode Size Above OFF_MAX - -**Test ID**: TC151-extended-inode-off-max - -## Objective - -Rerun rejection of an extended inode with bit 63 set in `i_size` on the current -exact KLD, before open, read, mmap, or pager setup. - -## Fixture - -Use `images/extended-size-bit63.erofs`. Evidence asserts a 64-byte regular -inode, exact NID/offset, `i_size=0x8000000000000000`, valid CRC, and image hash. -Compile `read_probe` and `mmap_fault` natively in FreeBSD. - -## Procedure - -Mount the image, then run every acquisition twice: - -```sh -./read_probe expect-error "$mnt/oversize.dat" 97 -./read_probe expect-error "$mnt/oversize.dat" 97 -set +e -truss -o /tmp/tc151-stat.truss stat "$mnt/oversize.dat"; stat_rc=$? -./mmap_fault "$mnt/oversize.dat" > /tmp/tc151-mmap1 2>&1; mmap1_rc=$? -./mmap_fault "$mnt/oversize.dat" > /tmp/tc151-mmap2 2>&1; mmap2_rc=$? -set -e -printf 'stat_rc=%d mmap_rc=%d,%d\n' "$stat_rc" "$mmap1_rc" "$mmap2_rc" -tail -20 /tmp/tc151-stat.truss -cat /tmp/tc151-mmap1 /tmp/tc151-mmap2 -``` - -Record dmesg delta, remove outputs, unmount, detach, and unload the exact KLD -at the end of G1. - -## Expected Results - -- All direct opens and traced `stat` fail with errno 97 (`EINTEGRITY`). -- Both mmap helpers exit 1 at `open` with the same error; no fd reaches mmap or - pager setup. -- No negative pager size, trap, panic, stale vnode, or cleanup residue occurs. -- Historical TC151 output cannot substitute for this current-KLD rerun. diff --git a/tests/TC152-extent-hole-bounded-read.md b/tests/TC152-extent-hole-bounded-read.md deleted file mode 100644 index c63f719..0000000 --- a/tests/TC152-extent-hole-bounded-read.md +++ /dev/null @@ -1,26 +0,0 @@ -# Test Case: Bounded Read of 5 GiB Extent Hole - -**Test ID**: TC152-extent-hole-bounded-read -**Category**: Compression Extent Mapping -**Priority**: Critical - -## Objective - -Verify a 5 GiB plus one page unmapped compression extent clears only the -requested byte/page, with bounded allocation. - -## Procedure - -Record final/fixture-evidence.txt and the extent-hole-5g.erofs hash. On a fresh -mount, record stat and the erofs vmstat -m row, then run twice: - - /usr/bin/time -l ./sparse_hole_probe \ - /tmp/repo22-g3/mnt/hole.dat 3221225472 - -Record the post-run allocation row, dmesg, and cleanup. - -## Expected Results - -The mounted size is 5368713216. Both one-byte pread calls and real one-page mmap -faults at 3 GiB return zero promptly; allocation does not scale with the 5 GiB -logical hole. No OOM, hang, trap, or panic occurs. diff --git a/tests/TC153-large-directory-block-index.md b/tests/TC153-large-directory-block-index.md deleted file mode 100644 index c57ad6f..0000000 --- a/tests/TC153-large-directory-block-index.md +++ /dev/null @@ -1,43 +0,0 @@ -# Test Case: Large Directory Block Index Width - -**Test ID**: TC153-large-directory-block-index -**Category**: Directory Corruption -**Priority**: Critical -**Latest Result**: PASS on exact 9ae22009f FreeBSD 15 KLD; see the G3 report. - -## Objective - -Verify a valid extended FLAT_PLAIN/Layout0 directory with final block index -2147483648 enters the 64-bit lookup path and consistently reports sparse -midpoint corruption as EINTEGRITY. - -## Procedure - -Record final/tc153-sparse-evidence.txt and the sparse-prefix hash. Copy the -90112-byte prefix, extend the copy sparsely to 8796093091840 bytes, and verify -the prefix hash remains unchanged: - - cp large-dir-intmax-sparse-prefix.erofs TC153-provider.erofs - truncate -s 8796093091840 TC153-provider.erofs - stat -f 'provider_size=%z allocated_sectors=%b' TC153-provider.erofs - unit=$(mdconfig -a -t vnode -f TC153-provider.erofs) - diskinfo "/dev/$unit" - mount -t erofs -o ro "/dev/$unit" /tmp/repo22-g3/mnt - ./g3_vfs_probe stat /tmp/repo22-g3/mnt/huge - ./g3_vfs_probe expect-error stat \ - /tmp/repo22-g3/mnt/huge/missing 97 - ./g3_vfs_probe expect-error stat \ - /tmp/repo22-g3/mnt/huge/missing 97 - ./readdir_probe /tmp/repo22-g3/mnt 128 - ./g3_vfs_probe expect-error stat \ - /tmp/repo22-g3/mnt/huge/missing 97 - -Unmount, detach, and remove the sparse copy. - -## Expected Results - -GEOM reports 8796093091840 bytes while host allocation remains sparse. -The /huge inode reports size 8796093026304 and Layout0 qualification records -directory_blocks=2147483649, last_block=2147483648. Both cold lookups and the -post-root-readdir lookup return errno 97, with no ENOENT cache masking, wrap, -hang, trap, or panic. Record prefix/KLD hashes and complete cleanup. diff --git a/tests/TC154-nfs-per-inode-generation.md b/tests/TC154-nfs-per-inode-generation.md deleted file mode 100644 index 9d33533..0000000 --- a/tests/TC154-nfs-per-inode-generation.md +++ /dev/null @@ -1,77 +0,0 @@ -# Test Case: NFS Per-Inode Generation on Same-Superblock Replacement - -**Test ID**: TC154-nfs-per-inode-generation -**Category**: NFS Export -**Priority**: Critical -**Regression**: A mount-wide superblock hash aliases a replacement inode at the same NID - -## Objective - -Verify that an EROFS file handle keeps the existing 16-byte FreeBSD FID ABI but -uses a stable per-inode generation. Remounting an unchanged image must preserve -the complete handle. Replacing it with an image whose superblock block, UUID, -NID, and file content are identical but whose raw inode metadata differs must -make the old handle return `ESTALE`. - -Also verify that a valid-NID handle resolving to a malformed replacement inode -returns the positive inode-read errno instead of a negative Linux errno or a -successful alias. - -## Fixture - -Generate all review fixtures on the host: - -```sh -python3 tests/review_fixtures.py make \ - --output /work/build/repo22-review-fixes-fixtures -``` - -For `nfs-inode-a.erofs`, `nfs-inode-b.erofs`, and -`nfs-inode-corrupt.erofs`, require the helper evidence to prove: - -- target `/zz-identity.txt` has the same NID and inode offset; -- the inode lies after the complete superblock checksum block; -- the complete checksum block and declared superblock are byte-identical; -- A and B differ only in the compact inode `i_mtime` field; -- the helper's superblock-seeded raw-inode FNV generations are nonzero and - different; -- all three images retain a valid superblock checksum. - -## Procedure - -1. Compile `tests/nfs_fh_tool.c` natively on FreeBSD 15. -2. Attach image A to an explicit md unit, mount it, capture `a.fh`, and compare - `nfs_fh_tool describe`, `nfs_fh_tool stat`, and `stat -f '%v'`. The FID and - `st_gen` values must equal `generation_a` in `fixture-manifest.json`. -3. Unmount and detach image A, then reattach the unchanged image to the same md - unit. Capture `a-remount.fh`, require a byte-for-byte handle match, and read - through `a.fh`. -4. Replace A with image B on the same md unit. Capture `b.fh`, require the same - fsid and NID but `generation_b`, then run: - - ```sh - nfs_fh_tool expect-stat a.fh ESTALE - nfs_fh_tool expect-open a.fh ESTALE - nfs_fh_tool stat b.fh - ``` - -5. Replace B with `nfs-inode-corrupt.erofs` on the same md unit. Resolving - `a.fh` must return positive FreeBSD `EOPNOTSUPP` (45) from inode decoding: - - ```sh - nfs_fh_tool expect-stat a.fh 45 - nfs_fh_tool expect-open a.fh 45 - ``` - -6. Compare pre/post dmesg, unmount, detach the md unit, and unload the exact - module under test. - -## Expected Results - -- Image A has an identical handle and generation across remounts. -- Image B's same-NID replacement has a different generation and both old-handle - operations return `ESTALE`. -- The new handle resolves successfully and `va_gen == FID.gen`. -- The malformed inode propagates errno 45 as a positive error and never returns - a vnode. -- The FID remains exactly 16 bytes with unchanged field offsets. diff --git a/tests/TC155-explicit-extent-pa-wrap.md b/tests/TC155-explicit-extent-pa-wrap.md deleted file mode 100644 index d7f975e..0000000 --- a/tests/TC155-explicit-extent-pa-wrap.md +++ /dev/null @@ -1,60 +0,0 @@ -# Test Case: 4-Byte Explicit Extent Physical Address Wrap - -**Test ID**: TC155-explicit-extent-pa-wrap -**Category**: Compression Mapping -**Priority**: Critical -**Regression**: Unchecked `pa += plen` can wrap and alias the next extent - -## Objective - -Verify that every address/index increment used while walking 4-byte explicit -extent records is checked and that a `uint64_t` physical-address wrap returns -`EINTEGRITY` before the wrapped address can be used for I/O. - -## Fixture - -Generate `extent-pa-wrap.erofs` with: - -```sh -python3 tests/review_fixtures.py make \ - --output /work/build/repo22-review-fixes-fixtures -``` - -erofs-utils 1.8.6 cannot emit explicit extent records, so the helper performs a -minimal structured conversion of a real extended legacy-compressed inode. It -parses the root directory and target inode, then self-checks these fields: - -- target `/extent.bin` remains an extended `COMPRESSED_FULL` inode; -- map header has `Z_EROFS_ADVISE_EXTENTS` and record size 4; -- the 64-bit initial physical base is `0xfffffffffffff000`; -- the first two `plen` records are 8192 and 4096; -- `initial_pa + first_plen > UINT64_MAX`; -- the second logical cluster starts at offset 4096; -- the transformed image has a valid recomputed superblock CRC32C. - -The second cluster is essential: an unchecked implementation wraps the first -accumulation to 4096 and can present that low address as the second extent. - -## Procedure - -1. Attach and mount the fixture read-only on FreeBSD 15. -2. Confirm `stat /mnt/repo22-review/extent.bin` succeeds, proving vnode creation - and map-header parsing completed. -3. Read one byte from logical offset 4096 and capture the syscall with `truss`: - - ```sh - truss -o extent-wrap.truss \ - dd if=/mnt/repo22-review/extent.bin of=/dev/null bs=1 skip=4096 count=1 - ``` - -4. Require nonzero `dd` status and a read/pread result of `ERR#97` in the trace. - Repeat the probe to exercise vnode-cache reuse. -5. Confirm there is no physical read at wrapped offset 4096, panic, trap, or - assertion in dmesg; then unmount, detach, and unload the module. - -## Expected Results - -- Both reads fail promptly with FreeBSD `EINTEGRITY` (97). -- No wrapped physical address reaches decompression or device I/O. -- The guest remains responsive and cleanup leaves no EROFS mount, md provider, - or EROFS module. diff --git a/tests/TC156-inode-timestamp-validation.md b/tests/TC156-inode-timestamp-validation.md deleted file mode 100644 index 2af0ea5..0000000 --- a/tests/TC156-inode-timestamp-validation.md +++ /dev/null @@ -1,51 +0,0 @@ -# Test Case: Compact and Extended Timestamp Validation - -**Test ID**: TC156-inode-timestamp-validation -**Category**: Inode Corruption -**Priority**: Critical -**Regression**: Timestamp arithmetic and conversion accepted malformed values - -## Objective - -Verify checked compact-inode epoch addition, nanoseconds below one billion for -both inode layouts, and rejection of unsigned seconds that cannot be represented -by FreeBSD 15 amd64 `time_t`. - -## Fixtures - -Generate the review fixtures with `tests/review_fixtures.py`. Its structured -field assertions produce: - -| Image | Malformed field | Trigger | -|---|---|---| -| `compact-epoch-wrap.erofs` | `epoch=UINT64_MAX`, root `i_mtime=1` | `uint64_t` addition overflow | -| `compact-epoch-range.erofs` | `epoch=INT64_MAX`, target `i_mtime=1` | result exceeds signed 64-bit `time_t` | -| `compact-nsec-invalid.erofs` | `fixed_nsec=1000000000` | invalid compact nanoseconds | -| `extended-nsec-invalid.erofs` | target `i_mtime_nsec=1000000000` | invalid extended nanoseconds | -| `extended-seconds-range.erofs` | target `i_mtime=INT64_MAX+1` | seconds exceed `time_t` | - -Every image must pass the helper's EROFS field-location and CRC32C checks before -guest transfer. - -## Procedure - -1. For `compact-epoch-wrap.erofs`, attach the image and trace the mount. The - mount must fail with `ERR#97` when the root inode is decoded. -2. For `compact-nsec-invalid.erofs`, trace the mount. Superblock timestamp - validation must fail with `ERR#97` before a vnode is returned. -3. Mount `compact-epoch-range.erofs`. The root timestamp at `INT64_MAX` remains - representable, but `stat /zz-identity.txt` must fail with `ERR#97`. -4. Mount each extended fixture. The root remains usable, while - `stat /extended-time.txt` must fail with `ERR#97` for the targeted field. -5. Repeat every failing access, compare dmesg, and clean the mount and md unit - after each image. - -## Expected Results - -- Compact epoch addition overflow returns `EINTEGRITY`, not a wrapped time. -- Exactly `999999999` remains the maximum accepted nanosecond value; - `1000000000` is rejected for compact and extended timestamps. -- `INT64_MAX` seconds is accepted on the qualified amd64 ABI, while - `INT64_MAX+1` is rejected before assignment to `timespec.tv_sec`. -- No malformed inode creates a vnode with normalized, negative, or wrapped - timestamps, and no panic or assertion occurs. diff --git a/tests/TC157-explicit-extent-order-validation.md b/tests/TC157-explicit-extent-order-validation.md deleted file mode 100644 index ba0bc55..0000000 --- a/tests/TC157-explicit-extent-order-validation.md +++ /dev/null @@ -1,55 +0,0 @@ -# Test Case: Explicit Extent Global Ordering Validation - -**Test ID**: TC157-explicit-extent-order-validation -**Category**: Compression Mapping -**Priority**: Critical -**Regression**: Binary search over an unvalidated explicit extent table can silently select the wrong mapping - -## Objective - -Verify that every 16-byte and 32-byte explicit extent table is validated once, -before binary search, for globally strict `lstart` ordering and logical bounds. -Descending, duplicate, and cross-search-branch violations must return positive -FreeBSD `EINTEGRITY` without reading the referenced compressed payload. - -## Fixtures - -Generate and self-check all final-review fixtures on the host: - -```sh -python3 tests/final_review_fixtures.py make \ - --output /work/build/repo22-final-review-fixtures -``` - -For each record size, the helper emits `descending`, `duplicate`, and -`cross-branch` images. The manifest records the complete `lstart` list, old -binary-search indices, table offsets, payload offset, and SHA256. The -cross-branch shape `[0, 8192, 4096, 12288]` is queried at 4096; the old search -visits records 2 and 3 but never record 1, so a hit-neighbor-only check is not -sufficient. - -## Procedure - -1. Build and load the exact `WITH_ZSTDIO=0` module on FreeBSD 15. -2. For each of the six images, attach an md provider and mount it read-only. -3. Before any lookup of `/extent.bin`, run: - - ```sh - final_review_probe expect-stat-error /mnt/repo22-final/extent.bin 97 - ``` - -4. Wrap the command with `io:::start`, filtered to that md unit. Require a raw - `dd` positive control to report the manifest payload offset 4096, then require - zero events at offset 4096 during every failing target lookup. -5. Repeat one case to confirm the same positive errno, compare dmesg, and clean - the mount, md unit, EROFS KLD, and DTrace modules. - -## Expected Results - -- All six target lookups return `EINTEGRITY` (97). -- Both record sizes reject descending and duplicate `lstart` values globally. -- Cross-branch corruption is rejected even though the old binary search would - not visit the preceding offending record. -- The payload-offset positive control fires and all six failing operations have - zero payload-offset GEOM events. -- No wrapped, negative, or Linux-style errno is returned. diff --git a/tests/TC158-48bit-compressed-blocks-hi.md b/tests/TC158-48bit-compressed-blocks-hi.md deleted file mode 100644 index d9a31dd..0000000 --- a/tests/TC158-48bit-compressed-blocks-hi.md +++ /dev/null @@ -1,45 +0,0 @@ -# Test Case: 48-Bit Extended Compressed Block Count - -**Test ID**: TC158-48bit-compressed-blocks-hi -**Category**: Inode and VFS Metadata -**Priority**: Critical -**Regression**: Extended compressed inodes ignored `i_nb.blocks_hi` - -## Objective - -Verify Linux-compatible union decoding for an extended compressed inode with -`blocks_hi=1`, and exact FreeBSD `va_bytes`/`st_blocks` values through direct and -file-handle/NFS-style getattr paths. - -## Fixture - -`final_review_fixtures.py` emits `compressed-blocks-hi.erofs` as a small seed. -Its manifest records `blocks_lo`, `blocks_hi`, block size, exact `va_bytes`, -`st_blocks`, and the required sparse provider size. Verify the seed SHA256 before -extending it in the guest. - -## Procedure - -1. Copy the seed in the guest and create the qualified sparse provider: - - ```sh - cp compressed-blocks-hi.erofs compressed-blocks-hi-sparse.erofs - truncate -s 17592186056704 compressed-blocks-hi-sparse.erofs - ``` - -2. Attach and mount it read-only with the exact module. -3. Run `final_review_probe stat-blocks` with the manifest's expected value. The - helper compares `stat` and `getfh`/`fhstat` results. -4. Use an FBT entry/return probe on `erofs_getattr` to capture - `a_vap->va_bytes`; require the manifest value. -5. Read at least one block of `/compressed-blocks.bin`, then unmount, detach, - shrink/remove the sparse file, unload DTrace, and unload EROFS. - -## Expected Results - -- `data_blocks == (1ULL << 32) | blocks_lo`. -- `va_bytes == 17592186052608`. -- Direct `stat` and file-handle `fhstat` both report - `st_blocks == 34359738384`. -- The compressed file remains readable; the high block count is allocation - metadata, not a physical read address. diff --git a/tests/TC159-special-setattr-combinations.md b/tests/TC159-special-setattr-combinations.md deleted file mode 100644 index c78ad57..0000000 --- a/tests/TC159-special-setattr-combinations.md +++ /dev/null @@ -1,44 +0,0 @@ -# Test Case: Special Vnode Combined Setattr Rejection - -**Test ID**: TC159-special-setattr-combinations -**Category**: Read-Only Vnode Operations -**Priority**: Critical -**Regression**: A special-vnode size field caused early success and hid other requested mutations - -## Objective - -Verify that size-only setattr remains an ignored no-op for special vnodes, while -a single `VOP_SETATTR` containing size plus mode, ownership, timestamps, or all -three classes returns `EROFS`. - -## Fixture and Probe - -Use `special-setattr.erofs` and its FIFO `/special.fifo`. Build the kernel-side -probe against the same FreeBSD 15 source tree: - -```sh -mkdir setattr-probe -cp tests/final_review_setattr_probe.c setattr-probe/ -cp tests/final_review_setattr_probe.mk setattr-probe/Makefile -make -C setattr-probe SYSDIR=/path/to/freebsd/sys -``` - -The probe performs one locked vnode lookup and one `VOP_SETATTR` per sysctl -trigger, avoiding userspace syscalls that split attributes across operations. - -## Procedure - -1. Mount the fixture read-only and record mode, uid, gid, atime, and mtime. -2. Load `erofs_setattr_probe.ko` and set - `debug.erofs_setattr_probe.path` to the FIFO. -3. Trigger operation 1 (size only); require result 0. -4. Trigger operations 2 through 5 (size+mode, size+owner, size+times, all); - require result 30 (`EROFS`) after each trigger. -5. Require the metadata snapshot to remain unchanged, then unload the probe, - unmount, detach, and unload EROFS. - -## Expected Results - -- Size-only special-vnode setattr returns 0. -- Every combined mutation returns `EROFS`, never false success. -- No mode, ownership, or timestamp changes are observable. diff --git a/tests/TC160-dot-omitted-offmax-cookie.md b/tests/TC160-dot-omitted-offmax-cookie.md deleted file mode 100644 index fbacc4f..0000000 --- a/tests/TC160-dot-omitted-offmax-cookie.md +++ /dev/null @@ -1,39 +0,0 @@ -# Test Case: Dot-Omitted OFF_MAX Cookie Rejection - -**Test ID**: TC160-dot-omitted-offmax-cookie -**Category**: Directory Corruption -**Priority**: Critical -**Regression**: Synthetic dot at `i_size + 1` overflowed signed directory offsets - -## Objective - -Verify that a `dot_omitted` directory with `i_size == OFF_MAX` is rejected before -the synthetic dot entry can create an unrepresentable cookie or negative offset. - -## Fixture - -`dot-omitted-offmax.erofs` contains an extended directory inode with a valid -backing block, flat-plain layout, the dot-omitted format bit, and -`i_size=9223372036854775807`. The structured helper records every mutated field -and the image hash. - -## Procedure - -1. Mount the fixture and require `stat` on `/offmax-dir` to report `OFF_MAX`. -2. Run: - - ```sh - truss -o tc160.truss \ - final_review_probe readdir-offmax /mnt/repo22-final/offmax-dir 97 - ``` - -3. The helper first calls `getdirentries` at `OFF_MAX`, then at offset 0. Require - `EINTEGRITY` both times and require the descriptor offset to remain exactly - `OFF_MAX` and 0 respectively. -4. Require both syscalls to show `ERR#97`, compare dmesg, and clean all resources. - -## Expected Results - -- No synthetic dot dirent or cookie is returned. -- Both reads fail with positive errno 97. -- No file offset becomes negative or advances after the error. diff --git a/tests/TC161-build-nm-failure.md b/tests/TC161-build-nm-failure.md deleted file mode 100644 index bfe60a1..0000000 --- a/tests/TC161-build-nm-failure.md +++ /dev/null @@ -1,38 +0,0 @@ -# Test Case: Build Undefined-Symbol Tool Failure - -**Test ID**: TC161-build-nm-failure -**Category**: Build Qualification -**Priority**: Critical -**Regression**: `nm -u | awk` could pass when `nm` itself failed - -## Objective - -Verify that `build.sh` treats `nm` failure as fatal independently of the awk -`bcmp` check, safely removes its temporary output, preserves prior module output, -and still builds both supported configurations normally. - -## Procedure - -1. On FreeBSD 15, run normal exact-source builds with `WITH_ZSTDIO=0` and 1. - Require both SUCCESS lines and save each module hash. -2. Create a private executable `nm` shim containing: - - ```sh - #!/bin/sh - exit 73 - ``` - -3. Put only that shim directory before the normal system paths and run - `WITH_ZSTDIO=0 ./build.sh`, capturing stdout/stderr and exit status. -4. Require nonzero status, `ERROR: nm failed while checking erofs.ko`, no SUCCESS - line, unchanged hash for the previously published `build/erofs.ko`, and no - `build/obj/nm-undef.*` file. -5. Run one final normal build to prove the trap and shim did not contaminate the - environment. - -## Expected Results - -- The shimmed build fails after module linkage and before publication. -- Awk cannot convert an `nm` execution failure into PASS. -- Temporary output is removed on failure and normal dual-configuration builds - remain successful. diff --git a/tests/TEST-COVERAGE-MATRIX.md b/tests/TEST-COVERAGE-MATRIX.md deleted file mode 100644 index 6052303..0000000 --- a/tests/TEST-COVERAGE-MATRIX.md +++ /dev/null @@ -1,294 +0,0 @@ -# Test Coverage Matrix - repo22 - -## Overview -Manual test specifications for the implemented FreeBSD EROFS paths. A listed -test case is a procedure, not proof that every environment has executed it; -dated reports under `tests/results/manual/` contain execution evidence. - -## Coverage Summary -- **Specification IDs**: TC000-TC161; TC000 is a template -- **Executable results**: 161 total, 160 PASS, TC146 PARTIAL -- **Open kernel failures**: none -- **Approved gap**: positive explicit mapped-payload fixture for TC146 -- **No automated coverage percentage is claimed** - -## Feature-to-Test Mapping - -### 1. Mount Operations (Features 1-2) -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| mount / umount | TC001, TC007, TC008 | Normal, concurrent, with-errors | -| statfs | TC009, TC010 | Basic, 48-bit blocks | - -### 2. Root & VNode (Features 3-4) -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| root vnode lookup | TC011 | Normal | -| vget | TC012, TC013 | Normal, invalid nid | - -### 3. Superblock (Features 5-7) -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| superblock parsing | TC014, TC015 | Normal, corrupted | -| superblock CRC32C verification | TC002, TC016 | Valid, invalid | -| 48-bit block count parsing | TC017, TC018 | Normal, large FS | -| 48-bit root nid parsing | TC019 | Normal | - -### 4. Inode Formats (Features 8-15) -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| compact inode decoding | TC020, TC021, TC022 | Basic, flag-based nlink=1, Linux dev decode | -| extended inode decoding | TC023, TC024 | Normal, large file | -| compact inode link-count handling | TC025 | Flagged single link, explicit hard links, directories | -| dot_omitted handling | TC026, TC027 | With/without | -| uncompressed FLAT_PLAIN read | TC028, TC029, TC030 | Small, medium, large | -| uncompressed FLAT_INLINE read | TC031, TC032 | Normal, zero-length | -| inline tailpacking read | TC033, TC034, TC147 | Normal, boundary, metadata-block/image bounds | - -### 5. File Operations (Features 16-18) -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| regular file read | TC035, TC036, TC037 | Sequential, random, large | -| symlink read | TC038, TC039, TC040 | Short, long, broken | -| directory entry decoding | TC041, TC042, TC141, TC148 | Normal, large, shared validator, Linux tail padding | - -### 6. Directory Operations (Features 19-22) -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| lookup | TC043, TC044, TC045 | Found, not-found, case | -| readdir | TC046, TC047, TC048, TC160 | Basic, large, restart cookies, `OFF_MAX` corruption | -| . handling | TC049 | Dot entry | -| .. handling | TC050 | Dotdot entry | - -### 7. VFS Integration (Features 23-28) -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| namecache integration | TC051, TC052 | Hit, miss | -| vfs_hash integration | TC053 | Normal | -| vn_vget_ino integration | TC054 | Normal | -| getattr | TC055, TC056, TC158 | Compressed 48-bit allocation, generation, decoded special rdev | -| access | TC057, TC058 | Allowed, denied | -| readlink | TC059 | Normal | -| pathconf | TC060 | All queries | - -### 8. Read-Only (Features 29) -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| read-only setattr rejection | TC061, TC062, TC063, TC159 | chmod, chown, write, combined special-vnode attributes | - -### 9. VM Integration (Features 30-31) -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| FreeBSD local vnode pager | TC064, TC065, TC149 | Sync/async ABI, real randomized mmap faults | -| explicit vop_bmap unsupported | TC066, TC149 | Local pager fallback without strategy assumptions | - -### 10. Extended Attributes - Shared (Features 32-36) -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| shared user.* xattr scan | TC067, TC068 | Found, not-found | -| shared user.* xattr list | TC069 | Multiple | -| shared trusted.* xattr get | TC070 | Normal | -| shared security.* xattr get | TC071 | Normal | -| shared system xattr list | TC072 | Normal | - -### 11. Extended Attributes - Container (Features 37-40) -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| shared-ea-in-metabox container | TC073 | Normal | -| inline user.* xattr | TC005, TC074 | Normal, multiple | -| inline trusted.* xattr | TC075 | Normal | -| inline security.* xattr | TC076 | Normal | - -### 12. Extended Attributes - Advanced (Features 41-46) -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| long-prefix user.* xattr | TC077 | Long name | -| long-prefix trusted.* xattr | TC078 | Long name | -| packed prefix table | TC079, TC080 | Normal, lookup | -| metabox-backed shared xattr | TC081 | Normal | -| system namespace xattr subset | TC082 | Specific attrs | -| user namespace xattr subset | TC083 | Filtering | - -### 13. Compression - LZ4 (Features 47-51) -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| LZ4 compression support | TC003, TC084, TC085 | Small, large, corrupt | -| LZ4 compressed data path | TC086, TC087 | Full source compare, deterministic offset compares | -| LZ4 compression config handling | TC088 | Different configs | -| LZ4 compressed pcluster mapping | TC089, TC090 | 4KB, 64KB | -| ztailpacking data path | TC091, TC092 | Normal, edge | - -### 14. Chunk-Based (Features 52-53) -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| chunk-based inode data path | TC093, TC094 | Single-dev, multi-dev | -| chunk index read path | TC095 | Normal | - -### 15. Multi-Device (Features 54-57) -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| device table support | TC096, TC097 | Parse, invalid | -| fragments support | TC098 | Normal | -| multi-device support | TC006, TC099, TC100 | 2-dev, 4-dev, errors | -| unified address to device mapping | TC101 | All devices | - -### 16. Compression - DEFLATE/zstd (Features 58-59) -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| DEFLATE compressed data path | TC102, TC103, TC104 | Level 1 full/range compare, levels 6 and 9 | -| zstd compressed data path | TC105, TC106, TC107 | Level 1, 15, 22 | - -### 17. Compression - LZMA (Feature 60) -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| LZMA/MicroLZMA compressed data path | TC004, TC108, TC109, TC110 | Normal, large, micro, corrupt | - -### 18. Documentation (Feature 61) -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| manual pages | TC111 | Accuracy check | - -### 19. Error Handling (Additional Paths) -| Scenario | Test Cases | Coverage | -|---------|-----------|----------| -| Invalid superblock magic | TC112 | Mount failure | -| Corrupted inode | TC113 | Read error | -| Out-of-bounds block | TC114 | Access error | -| Unsupported algorithm | TC115 | EOPNOTSUPP | -| Targeted compressed-stream corruption | TC116 | Equal provider length, proven extent, EIO | -| Invalid xattr format | TC117 | Parse error | -| Device not found | TC118 | Multi-dev error | -| Uncompressed payload integrity boundary | TC119 | No per-file CRC; source hash detects mutation | - -### 20. Boundary & Stress (Additional Paths) -| Scenario | Test Cases | Coverage | -|---------|-----------|----------| -| Empty file read | TC120 | Zero-length | -| Maximum file size | TC121 | 16TB limit | -| Deep directory tree | TC122 | 100+ levels | -| Long filename | TC123 | 255 chars | -| Many small files | TC124 | 10000+ files | -| Large directory | TC125 | 10000+ entries | -| Concurrent reads | TC126 | Multi-threaded | -| Memory pressure | TC127 | Low memory | -| Sequential throughput | TC128 | Performance | -| Random read pattern | TC129 | Performance | -| Mixed workload | TC130 | Realistic | - -## Execution Path Coverage - -### Normal Paths (Green) -The matrix maps implemented paths to manual procedures. See dated reports for -the subset actually executed on a given FreeBSD 15 environment. - -### Error Paths (Red) -- Mount errors: TC007, TC008, TC112 -- Read errors: TC113, TC114, TC116 -- Lookup errors: TC044, TC117 -- Permission errors: TC061-TC063 -- Device errors: TC097, TC118 -- Compression errors: TC085, TC104, TC110, TC116 -- CRC errors: TC002, TC016 -- End-to-end payload authenticity boundary: TC119 - -### Boundary Conditions (Yellow) -- Zero-length: TC032, TC120 -- Maximum size: TC018, TC121 -- Deep nesting: TC122 -- Long names: TC077, TC078, TC123 -- Large collections: TC042, TC048, TC124, TC125 -- Edge alignments: TC034, TC090, TC092 -- Inline metadata block: TC147 -- Directory tail padding: TC148 -- VM EOF/private mapping: TC149 -- 48-bit fallback superblock union: TC150 -- Extended inode `OFF_MAX` rejection: TC151 -- Multi-GiB compressed extent holes: TC152 -- Directory block indexes above `INT_MAX`: TC153 -- Explicit extent global order: TC157 -- Extended compressed allocation high bits: TC158 -- Combined special-vnode setattr: TC159 -- Dot-omitted `OFF_MAX` cookie: TC160 -- Build-tool failure propagation: TC161 - -## Test Execution Order -1. Basic mount (TC001-TC002) -2. Core functionality (TC003-TC060) -3. Extended features (TC061-TC111) -4. Error handling (TC112-TC119) -5. Stress and NFS tests (TC120-TC133) -6. Metadata/compression completion (TC134-TC149) -7. Final correctness findings and build qualification (TC150-TC161) - -## Coverage Metrics - -No line, branch, path, or feature percentage is asserted. The repository does -not contain an instrumented kernel coverage run or CI harness. - -The suite is manual-only. `tests/erofs_fixture.py`, -`tests/prepare_error_fixtures.sh`, `tests/prepare_directory_fixtures.sh`, -`tests/read_probe.c`, and `tests/readdir_probe.c` prepare or observe explicit -assertions; none is a TC runner. The former wrapper generators, integration -runner, partial image preparer, environment PASS printer, and result collector -were removed because their success output was not acceptance evidence. - -## Test Case Status - -- [x] Specifications TC000-TC161 are present exactly once. -- [x] Execution checklist and result templates are present. -- [x] Dated manual reports record qualified runs and real limitations. -- [x] TC150-TC152 have final-module FreeBSD 15 evidence. -- [x] TC153 has exact-source sparse-provider FreeBSD 15 evidence. -- [x] TC154-TC156 have final-module FreeBSD 15 evidence. -- [x] TC157-TC161 have independent final-source FreeBSD 15 evidence. -- [x] Final result is 160 PASS and TC146 PARTIAL. -- [ ] Automated kernel regression/coverage infrastructure is not implemented. - -### 21. Compression P0 Completion -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| fragment-backed compressed metabox | TC142 | Positive, loop, recursive NID, bounds | -| DEFLATE/ZSTD partial reference | TC143 | Full, partial, random, corrupt | -| MicroLZMA completion semantics | TC144 | Input consumption, partial, corrupt | -| FreeBSD ZSTDIO build gate | TC145 | Disabled/enabled build and KLD | -| HEAD2/interlaced/extent mapping | TC146 | HEAD2/interlaced PASS; explicit mapped payload PARTIAL | - -### 22. Metadata and VFS Semantics -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| FLAT_INLINE metadata-block and declared bounds | TC147 | Positive and checksum-valid cross-block corruption | -| Linux-compatible directory tail padding | TC141, TC148 | Cold lookup, readdir, cookies, strict corruptions | -| Real FreeBSD vnode pager faults | TC064-TC066, TC149 | Plain/LZ4, random faults, EOF, SIGBUS, private COW | -| Linux special-device decode | TC022, TC056 | Compact/extended char, block, FIFO `st_rdev` | -| Real compressed allocation accounting | TC055, TC158 | Algorithms/shapes and extended 48-bit block count | -| Stable per-inode NFS generation | TC132, TC154 | Stable metadata; changed inode metadata makes old handles stale | -| NFS background exit and cleanup | TC133 | Per-PID waits, fd inheritance guard, client-first cleanup | - -### 23. Final Correctness Findings -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| Linux-compatible 48-bit superblock union fallback | TC017, TC019, TC150 | `rootnid_8b == 0` selects `rootnid_2b` and low blocks | -| Extended inode signed-size boundary | TC151 | Bit-63 `i_size` fails before pager setup | -| Bounded compressed extent holes | TC152 | 5 GiB hole, one-byte pread and one-page mmap | -| 64-bit directory block search | TC153 | Multi-TiB Layout 0 provider returned repeated `EINTEGRITY` | - -### 24. Review Fixes -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| Per-inode NFS replacement detection | TC154 | Same superblock, UUID, NID and content; changed raw inode | -| Explicit extent physical-address checked add | TC155 | Four-byte records with `pa + plen` overflow | -| Inode timestamp range validation | TC156 | Compact epoch overflow, signed `time_t`, compact/extended nanoseconds | - -### 25. Independent Final Review -| Feature | Test Cases | Coverage | -|---------|-----------|----------| -| Explicit table global ordering | TC157 | 16/32-byte descending, duplicate, cross-branch rejection | -| Extended compressed `blocks_hi` | TC158 | Direct/file-handle allocation and FBT `va_bytes` | -| Special-vnode setattr combinations | TC159 | Size-only no-op and combined mutation rejection | -| Dot-omitted `OFF_MAX` cookie | TC160 | Both initial offsets return `EINTEGRITY` without advancement | -| Fatal `nm` failure | TC161 | Publication preservation, temp cleanup, post-shim rebuild | - -Latest final-review results are recorded in -`tests/results/manual/2026-08-09T1804Z-final-review-independent/manual-test-report.md`. -TC010, TC060, and TC153 are resolved historical issues. The only approved -remaining gap is TC146's positive explicit mapped-payload fixture. diff --git a/tests/erofs_fixture.py b/tests/erofs_fixture.py deleted file mode 100755 index c632885..0000000 --- a/tests/erofs_fixture.py +++ /dev/null @@ -1,764 +0,0 @@ -#!/usr/bin/env python3 -"""Inspect and make assertion-driven EROFS manual-test fixtures.""" - -from __future__ import annotations - -import argparse -import hashlib -import shutil -import struct -from dataclasses import dataclass -from pathlib import Path - - -SUPER = 1024 -MAGIC = 0xE0F5E1E2 -FEATURE_COMPAT_SB_CHKSUM = 0x00000001 -FEATURE_INCOMPAT_COMPR_CFGS = 0x00000002 -FEATURE_INCOMPAT_48BIT = 0x00000080 -CRC32C_POLYNOMIAL = 0x82F63B78 -CRC32C_INITIAL = 0xFFFFFFFF -KERNEL_CRC32C_SEED = 0x5045B54A - - -def crc32c(data: bytes | bytearray, initial: int = CRC32C_INITIAL) -> int: - checksum = initial - for byte in data: - checksum ^= byte - for _ in range(8): - checksum = (checksum >> 1) ^ ( - CRC32C_POLYNOMIAL if checksum & 1 else 0 - ) - return checksum & 0xFFFFFFFF - - -@dataclass(frozen=True) -class Inode: - nid: int - offset: int - inode_format: int - inode_size: int - xattr_size: int - layout: int - mode: int - size: int - start_block_low: int - start_block_high: int - start_block: int - - -@dataclass(frozen=True) -class DirectoryEntry: - nid: int - offset: int - name_offset: int - end_offset: int - name: bytes - - -class ErofsImage: - def __init__(self, data: bytearray, source: Path): - self.data = data - self.source = source - if len(data) < SUPER + 128: - raise ValueError(f"{source}: shorter than the EROFS superblock") - - @classmethod - def load(cls, path: Path) -> "ErofsImage": - return cls(bytearray(path.read_bytes()), path) - - def clone(self) -> "ErofsImage": - return ErofsImage(bytearray(self.data), self.source) - - def u16(self, offset: int) -> int: - return struct.unpack_from(" int: - return struct.unpack_from(" int: - return struct.unpack_from(" None: - struct.pack_into(" None: - struct.pack_into(" None: - struct.pack_into(" int: - return self.data[SUPER + 12] - - @property - def block_size(self) -> int: - if not 9 <= self.block_bits <= 16: - raise ValueError(f"invalid block bits {self.block_bits}") - return 1 << self.block_bits - - @property - def checksum_end(self) -> int: - span = self.block_size - if span > SUPER: - span -= SUPER - end = SUPER + span - if end > len(self.data): - raise ValueError( - f"checksum window ends at {end}, image size is {len(self.data)}" - ) - return end - - @property - def feature_compat(self) -> int: - return self.u32(SUPER + 8) - - @property - def feature_incompat(self) -> int: - return self.u32(SUPER + 80) - - @property - def blocks(self) -> int: - blocks = self.u32(SUPER + 36) - root8 = self.u64(SUPER + 112) - if self.feature_incompat & FEATURE_INCOMPAT_48BIT and root8 != 0: - blocks |= self.u16(SUPER + 14) << 32 - return blocks - - @property - def root_nid(self) -> int: - root8 = self.u64(SUPER + 112) - if self.feature_incompat & FEATURE_INCOMPAT_48BIT and root8 != 0: - return root8 - return self.u16(SUPER + 14) - - def calculated_checksum(self) -> int: - window = bytearray(self.data[SUPER : self.checksum_end]) - struct.pack_into(" int: - return crc32c( - self.data[SUPER + 8 : self.checksum_end], KERNEL_CRC32C_SEED - ) - - def checksum_valid(self) -> bool: - if not self.feature_compat & FEATURE_COMPAT_SB_CHKSUM: - return True - return self.u32(SUPER + 4) == self.calculated_checksum() - - def kernel_checksum_valid(self) -> bool: - if not self.feature_compat & FEATURE_COMPAT_SB_CHKSUM: - return True - return self.u32(SUPER + 4) == self.kernel_calculated_checksum() - - def update_checksum(self) -> None: - if not self.feature_compat & FEATURE_COMPAT_SB_CHKSUM: - raise ValueError("image does not advertise superblock checksum") - self.put_u32(SUPER + 4, 0) - self.put_u32(SUPER + 4, self.calculated_checksum()) - if not self.checksum_valid(): - raise AssertionError("updated checksum does not verify") - if ( - self.u32(SUPER) == MAGIC - and self.calculated_checksum() != self.kernel_calculated_checksum() - ): - raise AssertionError("canonical and kernel checksum forms differ") - - def validate_superblock(self) -> None: - if self.u32(SUPER) != MAGIC: - raise ValueError(f"unexpected magic {self.u32(SUPER):#010x}") - if self.blocks == 0: - raise ValueError("declared block count is zero") - if not self.checksum_valid(): - raise ValueError("superblock checksum is invalid") - - def inode(self, nid: int) -> Inode: - metadata = self.u32(SUPER + 40) << self.block_bits - offset = metadata + (nid << 5) - if offset > len(self.data) - 32: - raise ValueError(f"nid {nid} maps outside the image at {offset}") - inode_format = self.u16(offset) - inode_size = 64 if inode_format & 1 else 32 - if offset > len(self.data) - inode_size: - raise ValueError(f"nid {nid} extended inode is truncated") - xattr_count = self.u16(offset + 2) - xattr_size = 0 if xattr_count == 0 else 12 + 4 * (xattr_count - 1) - size = self.u64(offset + 8) if inode_size == 64 else self.u32(offset + 8) - start_block_low = self.u32(offset + 16) - start_block_high = ( - self.u16(offset + 6) - if inode_size == 64 - and self.feature_incompat & FEATURE_INCOMPAT_48BIT - else 0 - ) - return Inode( - nid=nid, - offset=offset, - inode_format=inode_format, - inode_size=inode_size, - xattr_size=xattr_size, - layout=(inode_format >> 1) & 7, - mode=self.u16(offset + 4), - size=size, - start_block_low=start_block_low, - start_block_high=start_block_high, - start_block=start_block_low | (start_block_high << 32), - ) - - def directory_entries(self, inode: Inode) -> list[DirectoryEntry]: - if inode.size == 0 or inode.size > self.block_size: - raise ValueError("helper resolves only one-block directories") - if inode.layout == 2: - data_offset = inode.offset + inode.inode_size + inode.xattr_size - elif inode.layout == 0: - data_offset = inode.start_block << self.block_bits - else: - raise ValueError(f"unsupported directory layout {inode.layout}") - if data_offset > len(self.data) - inode.size: - raise ValueError("directory data lies outside the image") - first_name_offset = self.u16(data_offset + 8) - if ( - first_name_offset < 12 - or first_name_offset % 12 != 0 - or first_name_offset >= inode.size - ): - raise ValueError("invalid first directory name offset") - count = first_name_offset // 12 - entries = [] - previous = 0 - for index in range(count): - entry_offset = data_offset + index * 12 - name_offset = self.u16(entry_offset + 8) - end_offset = ( - self.u16(entry_offset + 20) - if index + 1 < count - else inode.size - ) - if ( - name_offset < first_name_offset - or name_offset <= previous - or end_offset <= name_offset - or end_offset > inode.size - ): - raise ValueError("invalid directory name offsets") - slot = bytes( - self.data[ - data_offset + name_offset : data_offset + end_offset - ] - ) - name = slot.split(b"\0", 1)[0] - if not name: - raise ValueError("empty directory name") - entries.append( - DirectoryEntry( - nid=self.u64(entry_offset), - offset=entry_offset, - name_offset=name_offset, - end_offset=end_offset, - name=name, - ) - ) - previous = name_offset - return entries - - def resolve_root_entry(self, path: str) -> tuple[Inode, DirectoryEntry]: - name = path.removeprefix("/").encode("ascii") - if not name or b"/" in name: - raise ValueError("helper accepts one root-level path component") - root = self.inode(self.root_nid) - for entry in self.directory_entries(root): - if entry.name == name: - return root, entry - raise ValueError(f"path not found in root directory: {path}") - - def save(self, path: Path) -> None: - path.write_bytes(self.data) - - -def sha256(path: Path) -> str: - return hashlib.sha256(path.read_bytes()).hexdigest() - - -def write_checksums(output: Path) -> None: - paths = sorted(output.glob("*.erofs")) - with (output / "SHA256SUMS").open("w", encoding="ascii") as sums: - for path in paths: - sums.write(f"{sha256(path)} {path.name}\n") - - -def save_checked(image: ErofsImage, path: Path) -> None: - image.update_checksum() - image.validate_superblock() - image.save(path) - - -def assert_same_length(original: ErofsImage, changed: ErofsImage) -> None: - if len(original.data) != len(changed.data): - raise AssertionError("fixture mutation changed provider length") - - -def make_error_fixtures(args: argparse.Namespace) -> None: - output = args.output - if output.exists(): - raise ValueError(f"output already exists: {output}") - output.mkdir(parents=True) - plain = ErofsImage.load(args.plain) - compressed = ErofsImage.load(args.compressed) - deflate = ErofsImage.load(args.deflate) - plain.validate_superblock() - compressed.validate_superblock() - deflate.validate_superblock() - shutil.copy2(args.plain, output / "valid-plain.erofs") - shutil.copy2(args.compressed, output / "valid-lz4.erofs") - shutil.copy2(args.deflate, output / "valid-deflate-level1.erofs") - evidence = [] - - bad_crc = plain.clone() - patch_offset = SUPER + 64 - old_byte = bad_crc.data[patch_offset] - bad_crc.data[patch_offset] ^= 0x01 - assert_same_length(plain, bad_crc) - if bad_crc.checksum_valid(): - raise AssertionError("bad checksum fixture still verifies") - bad_crc.save(output / "bad-super-crc.erofs") - evidence.append( - f"bad-super-crc patch_offset={patch_offset} checksum_offset={SUPER + 4} " - f"old={old_byte:#04x} new={bad_crc.data[patch_offset]:#04x} " - f"coverage={SUPER}:{plain.checksum_end} recomputed=no " - f"provider_length={len(plain.data)}" - ) - - bad_checksum_field = plain.clone() - old_checksum = bad_checksum_field.u32(SUPER + 4) - bad_checksum_field.put_u32(SUPER + 4, old_checksum ^ 0x00000001) - assert_same_length(plain, bad_checksum_field) - if bad_checksum_field.checksum_valid(): - raise AssertionError("altered checksum field still verifies") - bad_checksum_field.save(output / "bad-checksum-field.erofs") - evidence.append( - f"bad-checksum-field field_offset={SUPER + 4} " - f"old={old_checksum:#010x} new={old_checksum ^ 1:#010x} " - f"coverage={SUPER}:{plain.checksum_end} recomputed=no " - f"provider_length={len(plain.data)}" - ) - - bad_magic = plain.clone() - if bad_magic.u32(SUPER) != MAGIC: - raise AssertionError("unexpected source magic") - bad_magic.put_u32(SUPER, 0x21444142) - assert_same_length(plain, bad_magic) - if bad_magic.checksum_valid() or not bad_magic.kernel_checksum_valid(): - raise AssertionError("bad magic checksum qualification failed") - bad_magic.save(output / "bad-magic.erofs") - evidence.append( - f"bad-magic patch_offset={SUPER} old={MAGIC:#010x} new=0x21444142 " - f"crc=unchanged canonical_valid=no kernel_suffix_valid=yes " - f"provider_length={len(plain.data)}" - ) - - bad_block_size = plain.clone() - old_block_bits = bad_block_size.block_bits - bad_block_size.data[SUPER + 12] = 13 - bad_block_size.update_checksum() - assert_same_length(plain, bad_block_size) - bad_block_size.save(output / "bad-block-size.erofs") - evidence.append( - f"bad-block-size field_offset={SUPER + 12} old={old_block_bits} new=13 " - f"coverage={SUPER}:{bad_block_size.checksum_end} crc=recomputed " - f"provider_length={len(plain.data)}" - ) - - bad_root = plain.clone() - if bad_root.feature_incompat & FEATURE_INCOMPAT_48BIT: - raise AssertionError("plain source unexpectedly has 48-bit feature") - metadata_offset = bad_root.u32(SUPER + 40) << bad_root.block_bits - invalid_root = ( - ((bad_root.blocks << bad_root.block_bits) - metadata_offset) // 32 - + 1024 - ) - bad_root.put_u32( - SUPER + 80, bad_root.feature_incompat | FEATURE_INCOMPAT_48BIT - ) - bad_root.put_u16(SUPER + 14, 0) - bad_root.put_u64(SUPER + 112, invalid_root) - save_checked(bad_root, output / "bad-root-nid.erofs") - assert_same_length(plain, bad_root) - evidence.append( - f"bad-root-nid feature_offset={SUPER + 80} " - f"blocks_hi_offset={SUPER + 14} rootnid_8b_offset={SUPER + 112} " - f"old_root={plain.root_nid} new_root={invalid_root} " - f"crc=recomputed provider_length={len(plain.data)}" - ) - - unsupported_feature = plain.clone() - old_incompat = unsupported_feature.feature_incompat - unsupported_feature.put_u32(SUPER + 80, old_incompat | 0x80000000) - save_checked(unsupported_feature, output / "unsupported-feature.erofs") - assert_same_length(plain, unsupported_feature) - evidence.append( - f"unsupported-feature field_offset={SUPER + 80} " - f"old={old_incompat:#010x} new={old_incompat | 0x80000000:#010x} " - f"crc=recomputed provider_length={len(plain.data)}" - ) - - short_path = output / "truncated-before-super.erofs" - short_path.write_bytes(plain.data[: SUPER - 1]) - evidence.append( - f"truncated-before-super source_length={len(plain.data)} " - f"provider_length={SUPER - 1} required_super_offset={SUPER}" - ) - empty_path = output / "empty-provider.erofs" - empty_path.write_bytes(b"") - evidence.append( - f"empty-provider source_length={len(plain.data)} provider_length=0" - ) - - bad_dirent = plain.clone() - _, entry = bad_dirent.resolve_root_entry("/bad-entry.txt") - invalid_nid = (bad_dirent.blocks << bad_dirent.block_bits) // 32 + 1024 - bad_dirent.put_u64(entry.offset, invalid_nid) - save_checked(bad_dirent, output / "bad-dirent-nid.erofs") - assert_same_length(plain, bad_dirent) - evidence.append( - f"bad-dirent-nid dirent_offset={entry.offset} old_nid={entry.nid} " - f"new_nid={invalid_nid} crc=recomputed " - f"provider_length={len(plain.data)}" - ) - - bad_inode = plain.clone() - _, entry = bad_inode.resolve_root_entry("/inode-target.txt") - inode = bad_inode.inode(entry.nid) - if inode.inode_format & 0x8000: - raise AssertionError("reserved i_format bit already set") - bad_inode.put_u16(inode.offset, inode.inode_format | 0x8000) - save_checked(bad_inode, output / "bad-inode-format.erofs") - assert_same_length(plain, bad_inode) - evidence.append( - f"bad-inode-format nid={inode.nid} inode_offset={inode.offset} " - f"i_format={inode.inode_format:#06x}->{inode.inode_format | 0x8000:#06x} " - f"crc=recomputed provider_length={len(plain.data)}" - ) - - out_of_bounds = plain.clone() - _, entry = out_of_bounds.resolve_root_entry("/plain.bin") - inode = out_of_bounds.inode(entry.nid) - if inode.layout != 0 or inode.size == 0: - raise AssertionError("plain.bin is not nonempty FLAT_PLAIN") - out_of_bounds.put_u32(inode.offset + 16, out_of_bounds.blocks) - save_checked(out_of_bounds, output / "oob-start-block.erofs") - assert_same_length(plain, out_of_bounds) - evidence.append( - f"oob-start-block nid={inode.nid} field_offset={inode.offset + 16} " - f"old={inode.start_block} new={out_of_bounds.blocks} crc=recomputed " - f"provider_length={len(plain.data)}" - ) - - future = compressed.clone() - if not future.feature_incompat & FEATURE_INCOMPAT_COMPR_CFGS: - raise AssertionError("compressed image has no compression config feature") - available_offset = SUPER + 82 - old_algorithms = future.u16(available_offset) - if old_algorithms & 0x8000: - raise AssertionError("future algorithm bit already set") - future.put_u16(available_offset, old_algorithms | 0x8000) - save_checked(future, output / "future-algorithm.erofs") - assert_same_length(compressed, future) - evidence.append( - f"future-algorithm field_offset={available_offset} " - f"old={old_algorithms:#06x} new={old_algorithms | 0x8000:#06x} " - f"crc=recomputed provider_length={len(compressed.data)}" - ) - - compressed_corrupt = compressed.clone() - corrupt_offset = args.compressed_offset + 32 - corrupt_length = 64 - if args.compressed_length < 32 + corrupt_length: - raise ValueError("compressed extent is too short for targeted mutation") - if corrupt_offset + corrupt_length > ( - args.compressed_offset + args.compressed_length - ): - raise ValueError("compressed mutation exceeds selected extent") - if corrupt_offset < compressed_corrupt.checksum_end: - raise ValueError("compressed corruption overlaps checksum window") - if corrupt_offset + corrupt_length > len(compressed_corrupt.data): - raise ValueError("compressed corruption exceeds provider") - old_bytes = bytes( - compressed_corrupt.data[corrupt_offset : corrupt_offset + corrupt_length] - ) - if old_bytes == bytes(corrupt_length): - raise ValueError("compressed mutation would not change bytes") - compressed_corrupt.data[ - corrupt_offset : corrupt_offset + corrupt_length - ] = bytes(corrupt_length) - if not compressed_corrupt.checksum_valid(): - raise AssertionError("payload mutation changed superblock checksum") - assert_same_length(compressed, compressed_corrupt) - compressed_corrupt.save(output / "compressed-stream-corrupt.erofs") - evidence.append( - f"compressed-stream-corrupt extent_offset={args.compressed_offset} " - f"extent_length={args.compressed_length} patch_offset={corrupt_offset} " - f"patch_length={corrupt_length} provider_length={len(compressed.data)} " - "crc=unchanged-valid" - ) - - raw_corrupt = plain.clone() - _, entry = raw_corrupt.resolve_root_entry("/plain.bin") - inode = raw_corrupt.inode(entry.nid) - if inode.layout != 0 or inode.size <= 257: - raise ValueError("plain.bin is not a qualifying FLAT_PLAIN target") - raw_offset = (inode.start_block << raw_corrupt.block_bits) + 257 - if raw_offset < raw_corrupt.checksum_end or raw_offset >= len(raw_corrupt.data): - raise ValueError("raw payload patch offset is invalid") - old_byte = raw_corrupt.data[raw_offset] - raw_corrupt.data[raw_offset] ^= 0x80 - assert_same_length(plain, raw_corrupt) - if not raw_corrupt.checksum_valid(): - raise AssertionError("raw payload mutation changed superblock checksum") - raw_corrupt.save(output / "raw-data-corrupt.erofs") - evidence.append( - f"raw-data-corrupt path=/plain.bin nid={inode.nid} " - f"start_block={inode.start_block} file_offset=257 " - f"patch_offset={raw_offset} old={old_byte:#04x} " - f"new={raw_corrupt.data[raw_offset]:#04x} crc=unchanged-valid " - f"provider_length={len(plain.data)}" - ) - - if plain.feature_incompat & FEATURE_INCOMPAT_48BIT: - raise AssertionError("plain source unexpectedly has 48-bit feature") - if plain.root_nid == 0: - raise AssertionError("48-bit fixture needs a nonzero root nid") - blocks_lo = plain.u32(SUPER + 36) - blocks_hi = 1 - total_blocks = blocks_lo | (blocks_hi << 32) - provider_length = total_blocks << plain.block_bits - - large = plain.clone() - large.put_u32(SUPER + 80, large.feature_incompat | FEATURE_INCOMPAT_48BIT) - large.put_u16(SUPER + 14, blocks_hi) - large.put_u64(SUPER + 112, plain.root_nid) - save_checked(large, output / "48bit-statfs-prefix.erofs") - assert_same_length(plain, large) - evidence.append( - f"48bit-statfs-prefix feature_offset={SUPER + 80} " - f"blocks_lo_offset={SUPER + 36} blocks_lo={blocks_lo} " - f"blocks_hi_offset={SUPER + 14} blocks_hi={blocks_hi} " - f"rootnid_8b_offset={SUPER + 112} rootnid_8b={plain.root_nid} " - f"total_blocks={total_blocks} required_provider_length={provider_length} " - f"prefix_length={len(plain.data)} crc=recomputed" - ) - - high = large.clone() - _, high_entry = high.resolve_root_entry("/high-offset.txt") - high_inode = high.inode(high_entry.nid) - if high_inode.inode_size != 64 or high_inode.layout != 0 or high_inode.size == 0: - raise ValueError("high-offset.txt is not a nonempty extended FLAT_PLAIN file") - if high_inode.start_block_high != 0: - raise ValueError("high-offset.txt already has a high start block") - low_data_offset = high_inode.start_block_low << high.block_bits - low_data = bytes( - high.data[low_data_offset : low_data_offset + high_inode.size] - ) - if low_data == bytes(high_inode.size): - raise ValueError("high-offset.txt low payload is already zero") - high.data[low_data_offset : low_data_offset + high_inode.size] = bytes( - high_inode.size - ) - high.put_u16(high_inode.offset + 6, 1) - save_checked(high, output / "48bit-high-file-prefix.erofs") - assert_same_length(plain, high) - high_inode = high.inode(high_entry.nid) - high_data_offset = high_inode.start_block << high.block_bits - if high_data_offset + high_inode.size > provider_length: - raise ValueError("high-offset target exceeds declared provider") - evidence.append( - f"48bit-high-file-prefix path=/high-offset.txt nid={high_inode.nid} " - f"inode_offset={high_inode.offset} startblk_hi_offset={high_inode.offset + 6} " - f"startblk_lo={high_inode.start_block_low} startblk_hi=1 " - f"start_block={high_inode.start_block} data_offset={high_data_offset} " - f"low_decoy_offset={low_data_offset} low_decoy=zero " - f"file_size={high_inode.size} required_provider_length={provider_length} " - f"prefix_length={len(plain.data)} crc=recomputed" - ) - - (output / "fixture-evidence.txt").write_text( - "\n".join(evidence) + "\n", encoding="ascii" - ) - write_checksums(output) - - -def validate_full_directory_block( - image: ErofsImage, offset: int -) -> list[DirectoryEntry]: - first_name_offset = image.u16(offset + 8) - if first_name_offset < 12 or first_name_offset % 12 != 0: - raise ValueError("invalid first name offset in full directory block") - count = first_name_offset // 12 - entries = [] - previous = 0 - for index in range(count): - entry_offset = offset + index * 12 - name_offset = image.u16(entry_offset + 8) - end_offset = ( - image.u16(entry_offset + 20) - if index + 1 < count - else image.block_size - ) - if ( - name_offset < first_name_offset - or name_offset <= previous - or end_offset <= name_offset - or end_offset > image.block_size - ): - raise ValueError("invalid full-block directory offsets") - name = bytes( - image.data[offset + name_offset : offset + end_offset] - ).split(b"\0", 1)[0] - if not name: - raise ValueError("empty full-block directory name") - entries.append( - DirectoryEntry( - nid=image.u64(entry_offset), - offset=entry_offset, - name_offset=name_offset, - end_offset=end_offset, - name=name, - ) - ) - previous = name_offset - return entries - - -def make_directory_fixtures(args: argparse.Namespace) -> None: - output = args.output - if output.exists(): - raise ValueError(f"output already exists: {output}") - output.mkdir(parents=True) - base = ErofsImage.load(args.base) - base.validate_superblock() - shutil.copy2(args.base, output / "namei-base.erofs") - _, wide_entry = base.resolve_root_entry("/wide") - wide = base.inode(wide_entry.nid) - if wide.layout != 2 or wide.size <= base.block_size: - raise ValueError("wide must be a multi-block FLAT_INLINE directory") - wide_block = wide.start_block << base.block_bits - entries = validate_full_directory_block(base, wide_block) - last = entries[-1] - slot_start = wide_block + last.name_offset - slot_end = wide_block + last.end_offset - padding_nul = base.data.index(0, slot_start, slot_end) - if padding_nul + 9 > wide_block + base.block_size: - raise ValueError("not enough final-name padding to patch") - evidence = [ - f"wide_nid={wide.nid} inode_offset={wide.offset} block_offset={wide_block} " - f"dirents={len(entries)} final_name={last.name.decode('ascii')} " - f"padding_patch={padding_nul + 1}:{padding_nul + 9}" - ] - - nonzero = base.clone() - nonzero.data[padding_nul + 1 : padding_nul + 9] = b"PAD!ERO!" - save_checked(nonzero, output / "namei-padding-nonzero.erofs") - - short = base.clone() - root = short.inode(short.root_nid) - if root.inode_size != 32: - raise ValueError("expected compact root inode") - short.put_u32(root.offset + 8, 8) - save_checked(short, output / "namei-corrupt-short.erofs") - evidence.append(f"short-root inode_offset={root.offset} size=8") - - bad_offset = base.clone() - root = bad_offset.inode(bad_offset.root_nid) - root_entries = bad_offset.directory_entries(root) - if len(root_entries) < 2: - raise ValueError("root needs at least two entries") - bad_offset.put_u16( - root_entries[1].offset + 8, root_entries[0].name_offset - ) - save_checked(bad_offset, output / "namei-corrupt-nameoff.erofs") - evidence.append( - f"bad-nameoff field_offset={root_entries[1].offset + 8} " - f"new={root_entries[0].name_offset}" - ) - - bad_name = base.clone() - slash_offset = slot_start + 5 - if bad_name.data[slash_offset] in (0, ord("/")): - raise ValueError("chosen name byte cannot be patched") - bad_name.data[slash_offset] = ord("/") - save_checked(bad_name, output / "namei-corrupt-name.erofs") - evidence.append(f"bad-name patch_offset={slash_offset} new=0x2f") - - (output / "fixture-evidence.txt").write_text( - "\n".join(evidence) + "\n", encoding="ascii" - ) - write_checksums(output) - - -def inspect_image(args: argparse.Namespace) -> None: - image = ErofsImage.load(args.image) - print(f"image={args.image}") - print(f"provider_bytes={len(image.data)}") - print(f"magic={image.u32(SUPER):#010x} offset={SUPER}") - print(f"block_size={image.block_size} block_bits={image.block_bits}") - print(f"blocks={image.blocks} blocks_lo_offset={SUPER + 36}") - print(f"root_nid={image.root_nid}") - print(f"feature_compat={image.feature_compat:#010x} offset={SUPER + 8}") - print(f"feature_incompat={image.feature_incompat:#010x} offset={SUPER + 80}") - print( - f"checksum={image.u32(SUPER + 4):#010x} offset={SUPER + 4} " - f"calculated={image.calculated_checksum():#010x} " - f"coverage={SUPER}:{image.checksum_end} valid={image.checksum_valid()}" - ) - print( - f"kernel_calculated={image.kernel_calculated_checksum():#010x} " - f"kernel_seed={KERNEL_CRC32C_SEED:#010x} " - f"kernel_coverage={SUPER + 8}:{image.checksum_end} " - f"kernel_valid={image.kernel_checksum_valid()} " - f"equivalent={image.calculated_checksum() == image.kernel_calculated_checksum()}" - ) - if args.path: - _, entry = image.resolve_root_entry(args.path) - inode = image.inode(entry.nid) - print( - f"path={args.path} nid={inode.nid} inode_offset={inode.offset} " - f"i_format={inode.inode_format:#06x} layout={inode.layout} " - f"size={inode.size} start_block_low={inode.start_block_low} " - f"start_block_high={inode.start_block_high} " - f"start_block={inode.start_block}" - ) - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - subparsers = parser.add_subparsers(dest="command", required=True) - - inspect_parser = subparsers.add_parser("inspect") - inspect_parser.add_argument("image", type=Path) - inspect_parser.add_argument("--path") - inspect_parser.set_defaults(function=inspect_image) - - error_parser = subparsers.add_parser("make-error-fixtures") - error_parser.add_argument("--plain", type=Path, required=True) - error_parser.add_argument("--compressed", type=Path, required=True) - error_parser.add_argument("--deflate", type=Path, required=True) - error_parser.add_argument("--compressed-offset", type=int, required=True) - error_parser.add_argument("--compressed-length", type=int, required=True) - error_parser.add_argument("--output", type=Path, required=True) - error_parser.set_defaults(function=make_error_fixtures) - - directory_parser = subparsers.add_parser("make-directory-fixtures") - directory_parser.add_argument("--base", type=Path, required=True) - directory_parser.add_argument("--output", type=Path, required=True) - directory_parser.set_defaults(function=make_directory_fixtures) - return parser.parse_args() - - -def main() -> None: - args = parse_args() - args.function(args) - - -if __name__ == "__main__": - main() diff --git a/tests/final_review_fixtures.py b/tests/final_review_fixtures.py deleted file mode 100644 index 06e5eff..0000000 --- a/tests/final_review_fixtures.py +++ /dev/null @@ -1,412 +0,0 @@ -#!/usr/bin/env python3 -"""Build and self-check fixtures for TC157-TC160.""" - -from __future__ import annotations - -import argparse -import json -import os -from pathlib import Path -import shutil -import stat -import struct -import subprocess - -from review_fixtures import ( - EROFS_INODE_COMPRESSED_FULL, - ErofsImage, - FixtureError, - SUPER, - align, - normalize_times, - run_mkfs, - sha256, -) - - -FEATURE_INCOMPAT_48BIT = 0x80 -EROFS_INODE_FLAT_PLAIN = 0 -EROFS_I_DOT_OMITTED = 1 << 4 -Z_EROFS_ADVISE_EXTENTS = 0x1 -OFF_MAX = (1 << 63) - 1 - - -def map_header_offset(inode) -> int: - return align(inode.offset + inode.inode_size + inode.xattr_size, 8) - - -def record_size(advise: int) -> int: - return 4 << ((advise >> 1) & 3) - - -def binary_search_indices(lstarts: list[int], logical: int) -> list[int]: - visited: list[int] = [] - left = 0 - right = len(lstarts) - while left < right: - middle = left + (right - left) // 2 - visited.append(middle) - if lstarts[middle] > logical: - right = middle - else: - left = middle + 1 - if lstarts[middle] == logical: - right = min(left + 1, right) - return visited - - -def convert_extent_table( - base: ErofsImage, - inode, - recsz: int, - lstarts: list[int], - payload: int, -) -> tuple[ErofsImage, int, int]: - image = base.clone() - header = map_header_offset(inode) - table = align(header + 8, recsz) - table_end = table + recsz * len(lstarts) - if table_end > len(image.data): - raise FixtureError("explicit extent table exceeds the base image") - root = image.inode(image.root_nid) - if not ( - table_end <= root.offset - or root.offset + root.inode_size <= header - ): - raise FixtureError("explicit extent conversion overlaps the root inode") - - advise = Z_EROFS_ADVISE_EXTENTS | ({16: 2, 32: 3}[recsz] << 1) - struct.pack_into("> 32, - lstart & 0xFFFFFFFF, - ) - if recsz == 32: - struct.pack_into("> 32) - image.update_checksum() - return image, header, payload - - -def verify_extent_fixture( - image: ErofsImage, - inode, - recsz: int, - expected: list[int], - kind: str, - logical_probe: int, -) -> tuple[int, list[int]]: - header = map_header_offset(inode) - count = image.u32(header) | (image.u16(header + 6) << 32) - advise = image.u16(header + 4) - if count != len(expected) or record_size(advise) != recsz: - raise FixtureError("explicit extent header self-check failed") - table = align(header + 8, recsz) - observed: list[int] = [] - for index in range(count): - offset = table + index * recsz - value = image.u32(offset + 12) - if recsz == 32: - value |= image.u32(offset + 16) << 32 - observed.append(value) - if observed != expected or any(value >= inode.size for value in observed): - raise FixtureError("explicit extent lstart self-check failed") - violations = [ - index - for index in range(1, len(observed)) - if observed[index] <= observed[index - 1] - ] - if len(violations) != 1: - raise FixtureError("fixture must contain exactly one ordering violation") - violation = violations[0] - if kind == "duplicate" and observed[violation] != observed[violation - 1]: - raise FixtureError("duplicate fixture is not a duplicate") - if kind != "duplicate" and observed[violation] >= observed[violation - 1]: - raise FixtureError("descending fixture is not descending") - visited = binary_search_indices(observed, logical_probe) - if kind == "cross-branch" and violation - 1 in visited: - raise FixtureError("cross-branch violation is visible to the old search") - return table, visited - - -def make_extent_fixtures(output: Path, source: Path) -> tuple[list[str], dict]: - target_source = source / "extent.bin" - target_source.write_bytes(b"extent-ordering-payload\n" * 65536) - normalize_times(source) - base_path = output / ".extent-base.erofs" - run_mkfs( - source, - base_path, - "77777777-1111-4222-8333-000000000157", - "-E", - "legacy-compress,force-inode-extended", - "-z", - "lz4", - "-C4096", - ) - base = ErofsImage.load(base_path) - inode = base.resolve_root_entry("extent.bin") - if inode.inode_size != 64 or inode.layout != EROFS_INODE_COMPRESSED_FULL: - raise FixtureError("extent target is not extended COMPRESSED_FULL") - header = map_header_offset(inode) - payload = base.u32(header + 20) << base.block_bits - if payload == 0 or payload + base.block_size > header: - raise FixtureError("original compressed payload is not isolated from metadata") - - cases = { - "descending": ([0, 4096, 12288, 8192], 8192), - "duplicate": ([0, 4096, 4096, 12288], 4096), - "cross-branch": ([0, 8192, 4096, 12288], 4096), - } - evidence: list[str] = [] - manifest_cases: dict[str, object] = {} - for recsz in (16, 32): - for kind, (lstarts, logical_probe) in cases.items(): - image, header, payload = convert_extent_table( - base, inode, recsz, lstarts, payload - ) - table, visited = verify_extent_fixture( - image, inode, recsz, lstarts, kind, logical_probe - ) - name = f"extent-{recsz}-{kind}.erofs" - path = output / name - image.save(path) - evidence.append( - "extent-order " - f"image={name} nid={inode.nid} recsize={recsz} " - f"header={header} table={table} lstarts={lstarts} " - f"probe={logical_probe} old_search_indices={visited} " - f"payload_offset={payload} violation={kind}" - ) - manifest_cases[name] = { - "path": "/extent.bin", - "nid": inode.nid, - "record_size": recsz, - "header_offset": header, - "table_offset": table, - "lstarts": lstarts, - "logical_probe": logical_probe, - "old_search_indices": visited, - "payload_offset": payload, - "payload_length": image.block_size, - "expected_errno": 97, - } - base_path.unlink() - return evidence, {"cases": manifest_cases} - - -def make_blocks_fixture(output: Path, source: Path) -> tuple[str, dict]: - target_source = source / "compressed-blocks.bin" - target_source.write_bytes(bytes(range(256)) * 4096) - normalize_times(source) - base_path = output / ".blocks-base.erofs" - run_mkfs( - source, - base_path, - "77777777-1111-4222-8333-000000000158", - "-E", - "legacy-compress,force-inode-extended", - "-z", - "lz4", - "-C4096", - ) - image = ErofsImage.load(base_path) - inode = image.resolve_root_entry("compressed-blocks.bin") - if inode.inode_size != 64 or inode.layout != EROFS_INODE_COMPRESSED_FULL: - raise FixtureError("compressed block target has the wrong inode layout") - root_nid = image.root_nid - inode_blocks_lo = image.u32(inode.offset + 16) - if inode_blocks_lo == 0: - raise FixtureError("compressed target has zero blocks_lo") - super_blocks_lo = image.u32(SUPER + 36) - image.put_u64(SUPER + 112, root_nid) - image.put_u32( - SUPER + 80, image.u32(SUPER + 80) | FEATURE_INCOMPAT_48BIT - ) - image.put_u16(SUPER + 14, 1) - image.put_u16(inode.offset + 6, 1) - image.update_checksum() - path = output / "compressed-blocks-hi.erofs" - image.save(path) - base_path.unlink() - - data_blocks = (1 << 32) | inode_blocks_lo - va_bytes = data_blocks << image.block_bits - st_blocks = va_bytes // 512 - provider_blocks = (1 << 32) | super_blocks_lo - provider_size = provider_blocks << image.block_bits - if image.root_nid != root_nid or image.u16(inode.offset + 6) != 1: - raise FixtureError("48-bit inode/superblock self-check failed") - if provider_size <= 16 * 1024**4: - raise FixtureError("sparse provider is not larger than 16 TiB") - evidence = ( - "compressed-blocks-hi " - f"nid={inode.nid} inode_offset={inode.offset} blocks_lo={inode_blocks_lo} " - f"blocks_hi=1 data_blocks={data_blocks} block_size={image.block_size} " - f"va_bytes={va_bytes} st_blocks={st_blocks} " - f"provider_blocks={provider_blocks} provider_size={provider_size}" - ) - manifest = { - "image": path.name, - "path": "/compressed-blocks.bin", - "nid": inode.nid, - "inode_offset": inode.offset, - "blocks_lo": inode_blocks_lo, - "blocks_hi": 1, - "data_blocks": data_blocks, - "block_size": image.block_size, - "va_bytes": va_bytes, - "st_blocks": st_blocks, - "provider_size": provider_size, - "seed_size": len(image.data), - } - return evidence, manifest - - -def make_special_fixture(output: Path, source: Path) -> tuple[str, dict]: - fifo = source / "special.fifo" - os.mkfifo(fifo, 0o640) - normalize_times(source) - path = output / "special-setattr.erofs" - run_mkfs( - source, - path, - "77777777-1111-4222-8333-000000000159", - "-E", - "force-inode-extended", - ) - image = ErofsImage.load(path) - inode = image.resolve_root_entry("special.fifo") - mode = image.u16(inode.offset + 4) - if not stat.S_ISFIFO(mode): - raise FixtureError("special setattr target is not a FIFO") - evidence = ( - f"special-setattr nid={inode.nid} inode_offset={inode.offset} " - f"mode={mode:#o} type=fifo" - ) - return evidence, { - "image": path.name, - "path": "/special.fifo", - "nid": inode.nid, - "mode": mode, - } - - -def make_offmax_fixture(output: Path, source: Path) -> tuple[str, dict]: - directory = source / "offmax-dir" - directory.mkdir() - for index in range(384): - (directory / f"entry-{index:03d}").write_text( - f"entry {index}\n", encoding="ascii" - ) - normalize_times(source) - base_path = output / ".offmax-base.erofs" - run_mkfs( - source, - base_path, - "77777777-1111-4222-8333-000000000160", - "-E", - "force-inode-extended", - ) - image = ErofsImage.load(base_path) - inode = image.resolve_root_entry("offmax-dir") - mode = image.u16(inode.offset + 4) - if inode.inode_size != 64 or not stat.S_ISDIR(mode) or inode.start_block == 0: - raise FixtureError("OFF_MAX target is not an extended backed directory") - inode_format = image.u16(inode.offset) - inode_format &= ~0x0E - inode_format |= EROFS_I_DOT_OMITTED - image.put_u16(inode.offset, inode_format) - image.put_u64(inode.offset + 8, OFF_MAX) - image.update_checksum() - path = output / "dot-omitted-offmax.erofs" - image.save(path) - base_path.unlink() - mutated = image.resolve_root_entry("offmax-dir") - if ( - mutated.inode_size != 64 - or mutated.layout != EROFS_INODE_FLAT_PLAIN - or mutated.size != OFF_MAX - or not image.u16(mutated.offset) & EROFS_I_DOT_OMITTED - ): - raise FixtureError("OFF_MAX directory field self-check failed") - evidence = ( - f"dot-omitted-offmax nid={mutated.nid} inode_offset={mutated.offset} " - f"format={image.u16(mutated.offset):#x} size={mutated.size} " - f"start_block={mutated.start_block} expected_errno=97" - ) - return evidence, { - "image": path.name, - "path": "/offmax-dir", - "nid": mutated.nid, - "inode_offset": mutated.offset, - "size": mutated.size, - "expected_errno": 97, - } - - -def make_fixtures(output: Path) -> None: - if shutil.which("mkfs.erofs") is None: - raise FixtureError("mkfs.erofs is required") - if output.exists() and any(output.iterdir()): - raise FixtureError(f"output directory is not empty: {output}") - output.mkdir(parents=True, exist_ok=True) - sources = output / ".sources" - extent_source = sources / "extent" - blocks_source = sources / "blocks" - special_source = sources / "special" - offmax_source = sources / "offmax" - for source in (extent_source, blocks_source, special_source, offmax_source): - source.mkdir(parents=True) - - evidence, extent_manifest = make_extent_fixtures(output, extent_source) - blocks_evidence, blocks_manifest = make_blocks_fixture(output, blocks_source) - special_evidence, special_manifest = make_special_fixture(output, special_source) - offmax_evidence, offmax_manifest = make_offmax_fixture(output, offmax_source) - evidence.extend([blocks_evidence, special_evidence, offmax_evidence]) - manifest = { - "extent_order": extent_manifest, - "compressed_blocks_hi": blocks_manifest, - "special_setattr": special_manifest, - "dot_omitted_offmax": offmax_manifest, - } - shutil.rmtree(sources) - (output / "fixture-evidence.txt").write_text( - "\n".join(evidence) + "\n", encoding="ascii" - ) - (output / "fixture-manifest.json").write_text( - json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="ascii" - ) - with (output / "SHA256SUMS").open("w", encoding="ascii") as sums: - for path in sorted(output.glob("*.erofs")): - sums.write(f"{sha256(path)} {path.name}\n") - print((output / "fixture-evidence.txt").read_text(encoding="ascii"), end="") - print((output / "SHA256SUMS").read_text(encoding="ascii"), end="") - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - subparsers = parser.add_subparsers(dest="command", required=True) - make_parser = subparsers.add_parser("make") - make_parser.add_argument("--output", type=Path, required=True) - return parser.parse_args() - - -def main() -> None: - args = parse_args() - if args.command == "make": - make_fixtures(args.output) - - -if __name__ == "__main__": - try: - main() - except (FixtureError, OSError, subprocess.CalledProcessError) as error: - raise SystemExit(f"final_review_fixtures.py: {error}") from error diff --git a/tests/final_review_probe.c b/tests/final_review_probe.c deleted file mode 100644 index 6797734..0000000 --- a/tests/final_review_probe.c +++ /dev/null @@ -1,127 +0,0 @@ -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -static uint64_t -parse_u64(const char *text) -{ - char *end; - uintmax_t value; - - errno = 0; - value = strtoumax(text, &end, 0); - if (errno != 0 || *text == '\0' || *end != '\0' || value > UINT64_MAX) - errx(2, "invalid integer: %s", text); - return ((uint64_t)value); -} - -static void -stat_blocks(const char *path, const char *expected_text) -{ - fhandle_t handle; - struct stat direct, by_handle; - uint64_t expected; - - expected = parse_u64(expected_text); - if (stat(path, &direct) != 0) - err(1, "stat %s", path); - if (getfh(path, &handle) != 0) - err(1, "getfh %s", path); - if (fhstat(&handle, &by_handle) != 0) - err(1, "fhstat %s", path); - if ((uint64_t)direct.st_blocks != expected || - (uint64_t)by_handle.st_blocks != expected) - errx(1, "st_blocks direct=%jd handle=%jd expected=%" PRIu64, - (intmax_t)direct.st_blocks, (intmax_t)by_handle.st_blocks, - expected); - printf("direct_blocks=%jd handle_blocks=%jd size=%jd expected=%" PRIu64 - "\n", (intmax_t)direct.st_blocks, (intmax_t)by_handle.st_blocks, - (intmax_t)direct.st_size, expected); -} - -static void -expect_stat_error(const char *path, const char *expected_text) -{ - struct stat status; - int expected; - - expected = (int)parse_u64(expected_text); - errno = 0; - if (stat(path, &status) != -1) - errx(1, "stat unexpectedly succeeded: %s", path); - if (errno != expected) - errx(1, "stat errno=%d expected=%d", errno, expected); - printf("stat_errno=%d path=%s\n", errno, path); -} - -static void -readdir_offmax(const char *path, const char *expected_text) -{ - char buffer[512]; - off_t base, current; - ssize_t bytes; - int descriptor, expected; - - expected = (int)parse_u64(expected_text); - descriptor = open(path, O_RDONLY | O_DIRECTORY); - if (descriptor < 0) - err(1, "open %s", path); - if (lseek(descriptor, OFF_MAX, SEEK_SET) != OFF_MAX) - err(1, "lseek OFF_MAX"); - base = 0; - errno = 0; - bytes = getdirentries(descriptor, buffer, sizeof(buffer), &base); - if (bytes != -1 || errno != expected) - errx(1, "OFF_MAX getdirentries bytes=%zd errno=%d expected=%d", - bytes, errno, expected); - current = lseek(descriptor, 0, SEEK_CUR); - if (current < 0 || current != OFF_MAX) - errx(1, "negative or changed post-error offset: %jd", - (intmax_t)current); - if (lseek(descriptor, 0, SEEK_SET) != 0) - err(1, "lseek zero"); - base = 0; - errno = 0; - bytes = getdirentries(descriptor, buffer, sizeof(buffer), &base); - if (bytes != -1 || errno != expected) - errx(1, "zero-offset getdirentries bytes=%zd errno=%d expected=%d", - bytes, errno, expected); - current = lseek(descriptor, 0, SEEK_CUR); - if (current < 0 || current != 0) - errx(1, "negative or changed zero offset: %jd", (intmax_t)current); - if (close(descriptor) != 0) - err(1, "close %s", path); - printf("offmax_errno=%d offmax_offset=%jd zero_errno=%d zero_offset=%jd\n", - expected, (intmax_t)OFF_MAX, expected, (intmax_t)current); -} - -int -main(int argc, char **argv) -{ - if (argc == 4 && strcmp(argv[1], "stat-blocks") == 0) { - stat_blocks(argv[2], argv[3]); - return (0); - } - if (argc == 4 && strcmp(argv[1], "expect-stat-error") == 0) { - expect_stat_error(argv[2], argv[3]); - return (0); - } - if (argc == 4 && strcmp(argv[1], "readdir-offmax") == 0) { - readdir_offmax(argv[2], argv[3]); - return (0); - } - errx(2, "usage: %s stat-blocks PATH EXPECTED | " - "expect-stat-error PATH ERRNO | readdir-offmax PATH ERRNO", argv[0]); -} diff --git a/tests/final_review_setattr_probe.c b/tests/final_review_setattr_probe.c deleted file mode 100644 index 7ef1856..0000000 --- a/tests/final_review_setattr_probe.c +++ /dev/null @@ -1,87 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -static char probe_path[MAXPATHLEN]; -static int probe_result = -1; - -static int -probe_run(SYSCTL_HANDLER_ARGS) -{ - struct nameidata nd; - struct vattr attributes; - struct vnode *vnode; - int error, operation; - - operation = 0; - error = sysctl_handle_int(oidp, &operation, 0, req); - if (error != 0 || req->newptr == NULL) - return (error); - if (operation < 1 || operation > 5 || probe_path[0] == '\0') - return (EINVAL); - - NDINIT(&nd, LOOKUP, FOLLOW | LOCKLEAF, UIO_SYSSPACE, probe_path); - error = namei(&nd); - if (error != 0) { - probe_result = error; - return (0); - } - vnode = nd.ni_vp; - NDFREE_PNBUF(&nd); - VATTR_NULL(&attributes); - attributes.va_size = 0; - if (operation == 2 || operation == 5) - attributes.va_mode = 0600; - if (operation == 3 || operation == 5) { - attributes.va_uid = 1; - attributes.va_gid = 1; - } - if (operation == 4 || operation == 5) { - attributes.va_atime.tv_sec = 1; - attributes.va_atime.tv_nsec = 0; - attributes.va_mtime.tv_sec = 1; - attributes.va_mtime.tv_nsec = 0; - } - probe_result = VOP_SETATTR(vnode, &attributes, curthread->td_ucred); - vput(vnode); - return (0); -} - -static int -probe_modevent(module_t module, int event, void *arg) -{ - (void)module; - (void)arg; - switch (event) { - case MOD_LOAD: - case MOD_UNLOAD: - return (0); - default: - return (EOPNOTSUPP); - } -} - -SYSCTL_NODE(_debug, OID_AUTO, erofs_setattr_probe, - CTLFLAG_RW | CTLFLAG_MPSAFE, 0, "EROFS setattr regression probe"); -SYSCTL_STRING(_debug_erofs_setattr_probe, OID_AUTO, path, CTLFLAG_RW, - probe_path, sizeof(probe_path), "Target vnode path"); -SYSCTL_PROC(_debug_erofs_setattr_probe, OID_AUTO, run, - CTLTYPE_INT | CTLFLAG_RW | CTLFLAG_MPSAFE, NULL, 0, probe_run, "I", - "1=size; 2=size+mode; 3=size+owner; 4=size+times; 5=all"); -SYSCTL_INT(_debug_erofs_setattr_probe, OID_AUTO, result, CTLFLAG_RD, - &probe_result, 0, "Last VOP_SETATTR errno"); - -static moduledata_t probe_module = { - "erofs_setattr_probe", - probe_modevent, - NULL, -}; - -DECLARE_MODULE(erofs_setattr_probe, probe_module, SI_SUB_DRIVERS, - SI_ORDER_MIDDLE); -MODULE_VERSION(erofs_setattr_probe, 1); diff --git a/tests/final_review_setattr_probe.mk b/tests/final_review_setattr_probe.mk deleted file mode 100644 index eda58e1..0000000 --- a/tests/final_review_setattr_probe.mk +++ /dev/null @@ -1,4 +0,0 @@ -KMOD=erofs_setattr_probe -SRCS=final_review_setattr_probe.c vnode_if.h - -.include diff --git a/tests/g1_fixtures.py b/tests/g1_fixtures.py deleted file mode 100755 index caa366b..0000000 --- a/tests/g1_fixtures.py +++ /dev/null @@ -1,372 +0,0 @@ -#!/usr/bin/env python3 -"""Build assertion-driven structured variants for the G1 manual tests.""" - -from __future__ import annotations - -import argparse -import hashlib -import shutil -import struct -from pathlib import Path - -from erofs_fixture import ErofsImage, FEATURE_INCOMPAT_48BIT, SUPER - - -S_IFMT = 0o170000 -S_IFDIR = 0o040000 -S_IFREG = 0o100000 -S_IFLNK = 0o120000 -S_IFCHR = 0o020000 -S_IFBLK = 0o060000 -S_IFIFO = 0o010000 -FLAT_PLAIN = 0 -FLAT_INLINE = 2 -I_NLINK_1 = 0x10 -NULL_ADDR_48 = (1 << 48) - 1 -LARGE_FILE_SIZE = (1 << 32) + 4097 - - -def inode_for(image: ErofsImage, path: str): - _, entry = image.resolve_root_entry(path) - return image.inode(entry.nid) - - -def assert_inode( - image: ErofsImage, - path: str, - *, - inode_size: int | None = None, - layout: int | None = None, - file_type: int | None = None, -): - inode = inode_for(image, path) - if inode_size is not None and inode.inode_size != inode_size: - raise ValueError(f"{path}: inode size {inode.inode_size}, expected {inode_size}") - if layout is not None and inode.layout != layout: - raise ValueError(f"{path}: layout {inode.layout}, expected {layout}") - if file_type is not None and inode.mode & S_IFMT != file_type: - raise ValueError(f"{path}: mode {inode.mode:#o}, expected type {file_type:#o}") - return inode - - -def save_checked(image: ErofsImage, path: Path) -> None: - image.update_checksum() - image.validate_superblock() - image.save(path) - - -def select_48bit_root(image: ErofsImage) -> int: - root_nid = image.root_nid - if root_nid == 0 or root_nid >= 1 << 48: - raise ValueError(f"root NID {root_nid} is unsuitable for the 48-bit fixture") - image.put_u32(SUPER + 80, image.feature_incompat | FEATURE_INCOMPAT_48BIT) - image.put_u16(SUPER + 14, 0) - image.put_u64(SUPER + 112, root_nid) - return root_nid - - -def rewrite_without_dot(image: ErofsImage, path: str) -> tuple[int, int, int]: - inode = assert_inode( - image, path, inode_size=32, layout=FLAT_INLINE, file_type=S_IFDIR - ) - entries = image.directory_entries(inode) - names = [entry.name for entry in entries] - if names.count(b".") != 1 or b".." not in names: - raise ValueError(f"{path}: expected one dot and an explicit dotdot entry") - kept = [entry for entry in entries if entry.name != b"."] - data_offset = inode.offset + inode.inode_size + inode.xattr_size - data = bytearray(len(kept) * 12) - names_blob = bytearray() - for index, entry in enumerate(kept): - name_offset = len(data) + len(names_blob) - file_type = image.data[entry.offset + 10] - struct.pack_into("= inode.size: - raise ValueError(f"{path}: rebuilt directory did not shrink") - image.data[data_offset : data_offset + inode.size] = bytes(inode.size) - image.data[data_offset : data_offset + len(rebuilt)] = rebuilt - image.put_u32(inode.offset + 8, len(rebuilt)) - image.put_u16(inode.offset, inode.inode_format | I_NLINK_1) - updated = image.inode(inode.nid) - updated_names = [entry.name for entry in image.directory_entries(updated)] - if b"." in updated_names or b".." not in updated_names: - raise AssertionError(f"{path}: dot omission rewrite failed") - return inode.nid, inode.size, len(rebuilt) - - -def image_hash(path: Path) -> str: - return hashlib.sha256(path.read_bytes()).hexdigest() - - -def write_hashes(output: Path) -> None: - with (output / "IMAGE-SHA256SUMS").open("w", encoding="ascii") as sums: - for path in sorted(output.glob("*.erofs")): - sums.write(f"{image_hash(path)} {path.name}\n") - - -def copy_validated(source: Path, destination: Path) -> ErofsImage: - image = ErofsImage.load(source) - image.validate_superblock() - shutil.copy2(source, destination) - return image - - -def make(args: argparse.Namespace) -> None: - output = args.output - if output.exists(): - raise ValueError(f"output already exists: {output}") - output.mkdir(parents=True) - - compact = copy_validated(args.compact, output / "compact.erofs") - extended = copy_validated(args.extended, output / "extended.erofs") - flat = copy_validated(args.flat, output / "flat.erofs") - inline = copy_validated(args.inline, output / "inline.erofs") - evidence = [] - - compact_root = compact.inode(compact.root_nid) - if compact_root.inode_size != 32: - raise ValueError("compact image root is not a compact inode") - explicit_dir = assert_inode( - compact, "/dotdir", inode_size=32, layout=FLAT_INLINE, file_type=S_IFDIR - ) - explicit_names = [entry.name for entry in compact.directory_entries(explicit_dir)] - if b"." not in explicit_names or b".." not in explicit_names: - raise ValueError("compact /dotdir does not contain explicit dot entries") - if explicit_dir.inode_format & I_NLINK_1: - raise ValueError("compact /dotdir unexpectedly advertises dot omission") - evidence.append( - f"compact root_nid={compact.root_nid} inode_size=32 " - f"blocks={compact.blocks} inos={compact.u64(SUPER + 16)} " - f"dotdir_nid={explicit_dir.nid} dotdir_i_format={explicit_dir.inode_format:#06x} " - f"dotdir_names={','.join(name.decode('ascii') for name in explicit_names)}" - ) - - small = assert_inode(compact, "/small.txt", inode_size=32, file_type=S_IFREG) - single = assert_inode(compact, "/single.txt", inode_size=32, file_type=S_IFREG) - hard_a = assert_inode(compact, "/hard-a.txt", inode_size=32, file_type=S_IFREG) - hard_b = assert_inode(compact, "/hard-b.txt", inode_size=32, file_type=S_IFREG) - if hard_a.nid != hard_b.nid or compact.u16(hard_a.offset + 6) != 2: - raise ValueError("compact hard-link fixture does not encode nlink=2") - if compact.u16(single.offset + 6) != 1 or single.inode_format & I_NLINK_1: - raise ValueError("compact single-link baseline is not explicit nlink=1") - evidence.append( - f"compact small_nid={small.nid} single_nid={single.nid} " - f"single_i_format={single.inode_format:#06x} single_i_nb=1 " - f"hardlink_nid={hard_a.nid} hardlink_i_nb=2" - ) - - expected_rdev = 0x543ABC21 - special_fields = [] - for path, expected_type in ( - ("/char-large", S_IFCHR), - ("/block-large", S_IFBLK), - ("/fifo", S_IFIFO), - ): - inode = assert_inode( - compact, path, inode_size=32, file_type=expected_type - ) - raw = compact.u32(inode.offset + 16) - if expected_type in (S_IFCHR, S_IFBLK) and raw != expected_rdev: - raise ValueError(f"{path}: raw rdev {raw:#x}, expected {expected_rdev:#x}") - special_fields.append(f"{path[1:]}:nid={inode.nid}:raw={raw:#010x}") - evidence.append("compact special=" + ",".join(special_fields)) - - bad_dirent = compact.clone() - _, invalid_entry = bad_dirent.resolve_root_entry("/invalid-target.txt") - invalid_nid = (bad_dirent.blocks << bad_dirent.block_bits) // 32 + 1024 - bad_dirent.put_u64(invalid_entry.offset, invalid_nid) - save_checked(bad_dirent, output / "invalid-dirent-nid.erofs") - evidence.append( - f"invalid-dirent entry_offset={invalid_entry.offset} " - f"old_nid={invalid_entry.nid} new_nid={invalid_nid} crc=valid" - ) - - nlink1 = compact.clone() - single = assert_inode(nlink1, "/single.txt", inode_size=32, file_type=S_IFREG) - nlink1.put_u16(single.offset, single.inode_format | I_NLINK_1) - nlink1.put_u16(single.offset + 6, 0x1234) - save_checked(nlink1, output / "compact-nlink1.erofs") - evidence.append( - f"nlink1 nid={single.nid} i_format={single.inode_format | I_NLINK_1:#06x} " - "i_nb=0x1234 crc=valid" - ) - - dot_omitted = compact.clone() - dot_nid, old_size, new_size = rewrite_without_dot(dot_omitted, "/dotdir") - dot_root = select_48bit_root(dot_omitted) - save_checked(dot_omitted, output / "compact-dot-omitted.erofs") - evidence.append( - f"dot-omitted nid={dot_nid} i_format_bit4=1 old_size={old_size} " - f"new_size={new_size} ondisk_names=..,child.txt " - f"feature=0x80 rootnid_8b={dot_root} blocks_hi=0 crc=valid" - ) - - root8 = compact.clone() - root_nid = select_48bit_root(root8) - save_checked(root8, output / "root8-48bit.erofs") - if root8.root_nid != root_nid or root8.blocks != compact.blocks: - raise AssertionError("48-bit root selector self-check failed") - evidence.append( - f"root8 feature={root8.feature_incompat:#x} rootnid_8b={root_nid} " - f"blocks_hi={root8.u16(SUPER + 14)} blocks_lo={root8.u32(SUPER + 36)}" - ) - - fallback = compact.clone() - fallback_root = fallback.root_nid - fallback.put_u32( - SUPER + 80, fallback.feature_incompat | FEATURE_INCOMPAT_48BIT - ) - fallback.put_u64(SUPER + 112, 0) - save_checked(fallback, output / "fallback-48bit-root2.erofs") - if fallback.root_nid != fallback_root or fallback.blocks != compact.blocks: - raise AssertionError("48-bit fallback selector self-check failed") - evidence.append( - f"fallback feature={fallback.feature_incompat:#x} " - f"rootnid_2b={fallback_root} rootnid_8b=0 blocks_lo={fallback.blocks}" - ) - - normal = assert_inode( - extended, "/large-file.bin", inode_size=64, file_type=S_IFREG - ) - evidence.append( - f"extended large-file_nid={normal.nid} inode_off={normal.offset} " - f"size={normal.size} inode_size=64" - ) - - large_hole = extended.clone() - huge = assert_inode( - large_hole, "/huge-sparse.dat", inode_size=64, file_type=S_IFREG - ) - large_hole.put_u16(huge.offset, (huge.inode_format & ~(7 << 1)) | (FLAT_PLAIN << 1)) - large_hole.put_u16(huge.offset + 6, (NULL_ADDR_48 >> 32) & 0xFFFF) - large_hole.put_u32(huge.offset + 16, NULL_ADDR_48 & 0xFFFFFFFF) - large_hole.put_u64(huge.offset + 8, LARGE_FILE_SIZE) - save_checked(large_hole, output / "extended-large-hole.erofs") - evidence.append( - f"extended-large-hole nid={huge.nid} inode_off={huge.offset} " - f"size={LARGE_FILE_SIZE} layout=0 startblk=0xffffffffffff crc=valid" - ) - - oversize = extended.clone() - oversize_inode = assert_inode( - oversize, "/oversize.dat", inode_size=64, file_type=S_IFREG - ) - oversize.put_u64(oversize_inode.offset + 8, 1 << 63) - save_checked(oversize, output / "extended-size-bit63.erofs") - evidence.append( - f"extended-size-bit63 nid={oversize_inode.nid} " - f"inode_off={oversize_inode.offset} i_size=0x8000000000000000 crc=valid" - ) - - for path in ( - "/small.txt", - "/medium.dat", - "/large.bin", - "/data.txt", - "/random-test.bin", - "/huge-data.bin", - ): - inode = assert_inode(flat, path, layout=FLAT_PLAIN, file_type=S_IFREG) - evidence.append( - f"flat path={path} nid={inode.nid} layout={inode.layout} size={inode.size}" - ) - longlink = assert_inode( - flat, "/longlink", layout=FLAT_INLINE, file_type=S_IFLNK - ) - if longlink.size <= 60: - raise ValueError("longlink target is not longer than 60 bytes") - evidence.append( - f"flat path=/longlink nid={longlink.nid} layout={longlink.layout} " - f"size={longlink.size}" - ) - - inline_zero = inline.clone() - empty = assert_inode(inline_zero, "/empty.txt", inode_size=32, file_type=S_IFREG) - inline_zero.put_u16( - empty.offset, (empty.inode_format & ~(7 << 1)) | (FLAT_INLINE << 1) - ) - save_checked(inline_zero, output / "inline-zero.erofs") - empty_after = assert_inode( - inline_zero, - "/empty.txt", - inode_size=32, - layout=FLAT_INLINE, - file_type=S_IFREG, - ) - if empty_after.size != 0: - raise AssertionError("inline zero fixture size changed") - evidence.append( - f"inline-zero nid={empty_after.nid} i_format={empty_after.inode_format:#06x} size=0" - ) - - inline_expectations = { - "/tiny.txt": FLAT_INLINE, - "/inline.txt": FLAT_INLINE, - "/tailpacked.dat": FLAT_INLINE, - "/file-4095.dat": FLAT_PLAIN, - "/file-4096.dat": FLAT_PLAIN, - "/file-4097.dat": FLAT_INLINE, - "/link1": FLAT_INLINE, - "/brokenlink": FLAT_INLINE, - } - for path, layout in inline_expectations.items(): - expected_type = S_IFLNK if path in ("/link1", "/brokenlink") else S_IFREG - inode = assert_inode(inline, path, layout=layout, file_type=expected_type) - evidence.append( - f"inline path={path} nid={inode.nid} layout={inode.layout} size={inode.size}" - ) - - inline_cross = inline.clone() - cross = assert_inode( - inline_cross, - "/inline.txt", - inode_size=32, - layout=FLAT_INLINE, - file_type=S_IFREG, - ) - if inline_cross.u16(cross.offset + 2) != 0: - raise ValueError("inline.txt unexpectedly has inline xattrs") - chosen_count = None - chosen_offset = None - for count in range(1, 65536): - xattr_size = 12 + 4 * (count - 1) - data_offset = cross.offset + cross.inode_size + xattr_size - block_offset = data_offset % inline_cross.block_size - if block_offset + cross.size > inline_cross.block_size: - chosen_count = count - chosen_offset = data_offset - break - if chosen_count is None or chosen_offset is None: - raise AssertionError("could not construct cross-block inline range") - inline_cross.put_u16(cross.offset + 2, chosen_count) - save_checked(inline_cross, output / "inline-cross-block.erofs") - evidence.append( - f"inline-cross nid={cross.nid} inode_off={cross.offset} " - f"xattr_icount=0->{chosen_count} data_blockoff={chosen_offset % inline_cross.block_size} " - f"size={cross.size} crc=valid" - ) - - (output / "fixture-evidence.txt").write_text( - "\n".join(evidence) + "\n", encoding="ascii" - ) - write_hashes(output) - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--compact", type=Path, required=True) - parser.add_argument("--extended", type=Path, required=True) - parser.add_argument("--flat", type=Path, required=True) - parser.add_argument("--inline", dest="inline", type=Path, required=True) - parser.add_argument("--output", type=Path, required=True) - return parser.parse_args() - - -def main() -> None: - make(parse_args()) - - -if __name__ == "__main__": - main() diff --git a/tests/g3_fixtures.py b/tests/g3_fixtures.py deleted file mode 100755 index 0fa282f..0000000 --- a/tests/g3_fixtures.py +++ /dev/null @@ -1,305 +0,0 @@ -#!/usr/bin/env python3 -"""Build and self-check deterministic fixtures for the G3 manual set.""" - -from __future__ import annotations - -import argparse -import hashlib -import os -from pathlib import Path -import shutil -import stat -import subprocess - -from erofs_fixture import ErofsImage, SUPER - - -FLAT_PLAIN = 0 -HUGE_DIRECTORY_BLOCKS = (1 << 31) + 1 -S_IFMT = 0o170000 - - -def sha256(path: Path) -> str: - return hashlib.sha256(path.read_bytes()).hexdigest() - - -def set_epoch(path: Path) -> None: - for entry in sorted(path.rglob("*"), reverse=True): - os.utime(entry, (0, 0), follow_symlinks=False) - os.utime(path, (0, 0), follow_symlinks=False) - - -def deterministic_bytes(length: int) -> bytes: - return bytes( - ((index * 131 + 17) ^ (index >> 3)) & 0xFF - for index in range(length) - ) - - -def make_source(source: Path) -> None: - testdir = source / "testdir" - (testdir / "subdir").mkdir(parents=True) - for index in range(5): - (testdir / f"dir-{index:02d}").mkdir() - (source / "parent" / "child" / "grandchild").mkdir(parents=True) - - (testdir / "file.txt").write_text("repo22 G3 file payload\n", encoding="ascii") - for index in range(12): - (testdir / f"regular-{index:02d}.txt").write_text( - f"regular {index:02d}\n", encoding="ascii" - ) - for name in ("File.txt", "FILE.TXT", "FiLe.TxT"): - (testdir / name).write_text(f"case variant {name}\n", encoding="ascii") - for index in range(1, 4): - (testdir / f"file{index}.txt").write_text( - f"cache file {index}\n", encoding="ascii" - ) - (testdir / "subdir" / "child.txt").write_text("child\n", encoding="ascii") - (testdir / "script.sh").write_text("#!/bin/sh\nexit 0\n", encoding="ascii") - (testdir / "rootonly.txt").write_text("restricted\n", encoding="ascii") - (testdir / "writeonly.txt").write_text("write only mode\n", encoding="ascii") - (testdir / "noexec.txt").write_text("not executable\n", encoding="ascii") - long_name = "long-" + "x" * 250 - (testdir / long_name).write_text("255 byte name\n", encoding="ascii") - long_target = "subdir/" + "y" * 112 - (testdir / "subdir" / ("y" * 112)).write_text( - "long target\n", encoding="ascii" - ) - - os.symlink("file.txt", testdir / "shortlink") - os.symlink("subdir/child.txt", testdir / "relative-link") - os.symlink("missing-target", testdir / "broken-link") - os.symlink(long_target, testdir / "long-link") - - (source / "parent" / "file.txt").write_text("parent file\n", encoding="ascii") - (source / "parent" / "child" / "grandchild" / "marker.txt").write_text( - "grandchild\n", encoding="ascii" - ) - (source / "pager.bin").write_bytes(deterministic_bytes(5 * 4096 + 731)) - - os.chmod(testdir / "script.sh", 0o755) - os.chmod(testdir / "rootonly.txt", 0o600) - os.chmod(testdir / "writeonly.txt", 0o200) - os.chmod(testdir / "noexec.txt", 0o644) - set_epoch(source) - - -def directory_data_offset(image: ErofsImage, inode) -> int: - if inode.layout == 2: - return inode.offset + inode.inode_size + inode.xattr_size - if inode.layout == FLAT_PLAIN: - return inode.start_block << image.block_bits - raise ValueError(f"unsupported directory layout {inode.layout}") - - -def directory_entries(image: ErofsImage, inode): - if inode.size == 0 or inode.size > image.block_size: - raise ValueError("G3 qualifier accepts only one-block path components") - return image.directory_entries(inode) - - -def resolve(image: ErofsImage, path: str): - inode = image.inode(image.root_nid) - for component in path.strip("/").split("/"): - if not component: - continue - entry = next( - ( - item - for item in directory_entries(image, inode) - if item.name.decode("ascii") == component - ), - None, - ) - if entry is None: - raise ValueError(f"path not found: {path}") - inode = image.inode(entry.nid) - return inode - - -def validate_image(image_path: Path, source: Path) -> list[str]: - image = ErofsImage.load(image_path) - image.validate_superblock() - evidence = [ - f"image={image_path.name} provider_bytes={len(image.data)} " - f"block_size={image.block_size} blocks={image.blocks} " - f"root_nid={image.root_nid} checksum_valid={image.checksum_valid()}" - ] - paths = [ - "/testdir", - "/testdir/file.txt", - "/testdir/script.sh", - "/testdir/rootonly.txt", - "/testdir/writeonly.txt", - "/testdir/shortlink", - "/testdir/long-link", - "/pager.bin", - "/parent/child/grandchild", - ] - for path in paths: - source_path = source / path.lstrip("/") - source_stat = source_path.lstat() - inode = resolve(image, path) - if inode.mode & S_IFMT != stat.S_IFMT(source_stat.st_mode): - raise ValueError(f"{path}: inode type differs from source") - if inode.mode & 0o7777 != source_stat.st_mode & 0o7777: - raise ValueError(f"{path}: inode permissions differ from source") - expected_size = None - if source_path.is_symlink(): - expected_size = len(os.readlink(source_path).encode("ascii")) - elif source_path.is_file(): - expected_size = source_stat.st_size - if expected_size is not None and inode.size != expected_size: - raise ValueError(f"{path}: size {inode.size}, expected {expected_size}") - evidence.append( - f"path={path} nid={inode.nid} inode_offset={inode.offset} " - f"layout={inode.layout} mode={inode.mode:#07o} size={inode.size} " - f"start_block={inode.start_block}" - ) - - source_names = sorted(item.name for item in (source / "testdir").iterdir()) - testdir = resolve(image, "/testdir") - image_names = sorted( - entry.name.decode("ascii") - for entry in directory_entries(image, testdir) - if entry.name not in (b".", b"..") - ) - if source_names != image_names: - raise ValueError("/testdir names differ from source") - evidence.append( - f"path=/testdir real_entries={len(image_names)} " - f"directory_size={testdir.size} layout={testdir.layout}" - ) - return evidence - - -def write_source_hashes(source: Path, output: Path) -> None: - with output.open("w", encoding="ascii") as sums: - for path in sorted(source.rglob("*")): - if path.is_file() and not path.is_symlink(): - sums.write(f"{sha256(path)} {path.relative_to(source)}\n") - - -def make_vfs(output: Path) -> None: - if output.exists(): - raise ValueError(f"output already exists: {output}") - source = output / "source" - output.mkdir(parents=True) - make_source(source) - plain = output / "vfs-plain.erofs" - lz4 = output / "vfs-lz4.erofs" - common = [ - "mkfs.erofs", - "-d0", - "-T0", - "--all-time", - "--all-root", - "--workers=1", - ] - subprocess.run( - common - + [ - "-x-1", - "-E", - "noinline_data", - "-U", - "33333333-4444-5555-6666-777777777771", - str(plain), - str(source), - ], - check=True, - ) - subprocess.run( - common - + [ - "-z", - "lz4", - "-U", - "33333333-4444-5555-6666-777777777772", - str(lz4), - str(source), - ], - check=True, - ) - evidence = validate_image(plain, source) + validate_image(lz4, source) - (output / "fixture-evidence.txt").write_text( - "\n".join(evidence) + "\n", encoding="ascii" - ) - (output / "expected-testdir.txt").write_text( - "\n".join(sorted(item.name for item in (source / "testdir").iterdir())) - + "\n", - encoding="ascii", - ) - write_source_hashes(source, output / "SOURCE-SHA256SUMS") - with (output / "IMAGE-SHA256SUMS").open("w", encoding="ascii") as sums: - for path in (lz4, plain): - sums.write(f"{sha256(path)} {path.name}\n") - print((output / "fixture-evidence.txt").read_text(encoding="ascii"), end="") - print((output / "IMAGE-SHA256SUMS").read_text(encoding="ascii"), end="") - - -def make_large_prefix(source: Path, output: Path, evidence_path: Path) -> None: - image = ErofsImage.load(source) - image.validate_superblock() - huge = resolve(image, "/huge") - expected_size = HUGE_DIRECTORY_BLOCKS * image.block_size - if huge.inode_size != 64 or huge.layout != FLAT_PLAIN: - raise ValueError("TC153 directory is not extended FLAT_PLAIN/Layout0") - if huge.size != expected_size: - raise ValueError(f"TC153 directory size {huge.size}, expected {expected_size}") - if huge.start_block == 0: - raise ValueError("TC153 directory has no plain-data start block") - provider_blocks = huge.start_block + HUGE_DIRECTORY_BLOCKS - if provider_blocks > 0xFFFFFFFF: - raise ValueError("TC153 sparse provider does not fit blocks_lo") - image.put_u32(SUPER + 36, provider_blocks) - image.update_checksum() - image.validate_superblock() - output.parent.mkdir(parents=True, exist_ok=True) - image.save(output) - - checked = ErofsImage.load(output) - checked_huge = resolve(checked, "/huge") - if ( - checked.blocks != provider_blocks - or checked_huge.layout != FLAT_PLAIN - or checked_huge.size != expected_size - ): - raise AssertionError("TC153 sparse prefix self-check failed") - provider_bytes = provider_blocks * checked.block_size - evidence_path.write_text( - "tc153-sparse-prefix " - f"nid={checked_huge.nid} inode_offset={checked_huge.offset} " - f"layout=flat-plain start_block={checked_huge.start_block} " - f"directory_blocks={HUGE_DIRECTORY_BLOCKS} " - f"last_block={HUGE_DIRECTORY_BLOCKS - 1} " - f"blocks_lo={provider_blocks} provider_bytes={provider_bytes} " - f"prefix_bytes={len(checked.data)} checksum_valid={checked.checksum_valid()} " - f"sha256={sha256(output)}\n", - encoding="ascii", - ) - print(evidence_path.read_text(encoding="ascii"), end="") - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - subparsers = parser.add_subparsers(dest="command", required=True) - vfs = subparsers.add_parser("make-vfs") - vfs.add_argument("--output", type=Path, required=True) - large = subparsers.add_parser("make-large-prefix") - large.add_argument("--source", type=Path, required=True) - large.add_argument("--output", type=Path, required=True) - large.add_argument("--evidence", type=Path, required=True) - return parser.parse_args() - - -def main() -> None: - args = parse_args() - if args.command == "make-vfs": - make_vfs(args.output) - else: - make_large_prefix(args.source, args.output, args.evidence) - - -if __name__ == "__main__": - main() diff --git a/tests/g3_vfs_probe.c b/tests/g3_vfs_probe.c deleted file mode 100644 index 1679af6..0000000 --- a/tests/g3_vfs_probe.c +++ /dev/null @@ -1,208 +0,0 @@ -#define _POSIX_C_SOURCE 200809L - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -static int -parse_errno(const char *text) -{ - char *end; - long value; - - errno = 0; - value = strtol(text, &end, 0); - if (errno != 0 || *text == '\0' || *end != '\0' || value <= 0 || - value > 255) - errx(2, "invalid errno: %s", text); - return ((int)value); -} - -static int -run_operation(const char *operation, const char *path) -{ - struct stat sb; - int fd; - - if (strcmp(operation, "stat") == 0) - return (stat(path, &sb)); - if (strcmp(operation, "lstat") == 0) - return (lstat(path, &sb)); - if (strcmp(operation, "open-read") == 0) { - fd = open(path, O_RDONLY); - if (fd < 0) - return (-1); - return (close(fd)); - } - if (strcmp(operation, "open-rdwr") == 0) { - fd = open(path, O_RDWR); - if (fd < 0) - return (-1); - return (close(fd)); - } - if (strcmp(operation, "access-read") == 0) - return (access(path, R_OK)); - if (strcmp(operation, "access-write") == 0) - return (access(path, W_OK)); - if (strcmp(operation, "access-exec") == 0) - return (access(path, X_OK)); - if (strcmp(operation, "chmod") == 0) - return (chmod(path, 0777)); - if (strcmp(operation, "chown") == 0) - return (chown(path, 65534, 65534)); - if (strcmp(operation, "truncate") == 0) - return (truncate(path, 0)); - if (strcmp(operation, "create") == 0) { - fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0600); - if (fd < 0) - return (-1); - if (close(fd) != 0) - return (-1); - return (unlink(path)); - } - errx(2, "unknown operation: %s", operation); -} - -static void -expect_result(const char *operation, const char *path, int expected_errno) -{ - int error, result; - - errno = 0; - result = run_operation(operation, path); - error = errno; - if (expected_errno == 0) { - if (result != 0) - errx(1, "%s %s returned errno %d (%s)", operation, path, - error, strerror(error)); - printf("op=%s result=success errno=0\n", operation); - return; - } - if (result == 0) - errx(1, "%s %s unexpectedly succeeded", operation, path); - if (error != expected_errno) - errx(1, "%s %s returned errno %d (%s), expected %d", operation, - path, error, strerror(error), expected_errno); - printf("op=%s result=error errno=%d message=%s\n", operation, - error, strerror(error)); -} - -static long -checked_pathconf(const char *path, int name, const char *label, int *errorp) -{ - long value; - - errno = 0; - value = pathconf(path, name); - *errorp = value == -1 ? errno : 0; - if (*errorp != 0) - printf("%s=error:%d ", label, *errorp); - else - printf("%s=%ld ", label, value); - return (value); -} - -static void -show_pathconf(const char *path) -{ - int chown_error, filesizebits_error, link_error, name_error, - no_trunc_error, path_error; - long chown_restricted, filesizebits, link_max, name_max, no_trunc, - path_max; - - name_max = checked_pathconf(path, _PC_NAME_MAX, "name_max", - &name_error); - path_max = checked_pathconf(path, _PC_PATH_MAX, "path_max", - &path_error); - filesizebits = checked_pathconf(path, _PC_FILESIZEBITS, "filesizebits", - &filesizebits_error); - link_max = checked_pathconf(path, _PC_LINK_MAX, "link_max", - &link_error); - no_trunc = checked_pathconf(path, _PC_NO_TRUNC, "no_trunc", - &no_trunc_error); - chown_restricted = checked_pathconf(path, _PC_CHOWN_RESTRICTED, - "chown_restricted", &chown_error); - putchar('\n'); - if (name_error != 0 || path_error != 0 || filesizebits_error != 0 || - link_error != 0 || no_trunc_error != 0 || chown_error != 0 || - name_max != 255 || path_max <= 0 || filesizebits != 64 || - link_max <= 0 || no_trunc != 1 || chown_restricted != 1) - errx(1, "unexpected EROFS pathconf values"); -} - -static void -show_readlink(const char *path) -{ - unsigned char target[4096]; - uint64_t hash; - ssize_t length; - - length = readlink(path, (char *)target, sizeof(target)); - if (length < 0) - err(1, "readlink %s", path); - if ((size_t)length == sizeof(target)) - errx(1, "symlink target is too long"); - hash = UINT64_C(14695981039346656037); - for (ssize_t index = 0; index < length; index++) { - hash ^= target[index]; - hash *= UINT64_C(1099511628211); - } - target[length] = '\0'; - printf("length=%zd fnv1a64=%016" PRIx64 " target=%s\n", length, - hash, target); -} - -static void -show_stat(const char *path) -{ - struct stat sb; - - if (lstat(path, &sb) != 0) - err(1, "lstat %s", path); - printf("ino=%ju mode=%#jo uid=%ju gid=%ju nlink=%ju size=%jd " - "blocks=%jd blksize=%jd", - (uintmax_t)sb.st_ino, (uintmax_t)sb.st_mode, - (uintmax_t)sb.st_uid, (uintmax_t)sb.st_gid, - (uintmax_t)sb.st_nlink, (intmax_t)sb.st_size, - (intmax_t)sb.st_blocks, (intmax_t)sb.st_blksize); -#ifdef __FreeBSD__ - printf(" gen=%ju", (uintmax_t)sb.st_gen); -#endif - putchar('\n'); -} - -int -main(int argc, char **argv) -{ - if (argc == 5 && strcmp(argv[1], "expect-error") == 0) { - expect_result(argv[2], argv[3], parse_errno(argv[4])); - return (0); - } - if (argc == 4 && strcmp(argv[1], "expect-success") == 0) { - expect_result(argv[2], argv[3], 0); - return (0); - } - if (argc == 3 && strcmp(argv[1], "pathconf") == 0) { - show_pathconf(argv[2]); - return (0); - } - if (argc == 3 && strcmp(argv[1], "readlink") == 0) { - show_readlink(argv[2]); - return (0); - } - if (argc == 3 && strcmp(argv[1], "stat") == 0) { - show_stat(argv[2]); - return (0); - } - errx(2, "usage: %s expect-error OP PATH ERRNO | " - "expect-success OP PATH | pathconf PATH | readlink PATH | stat PATH", - argv[0]); -} diff --git a/tests/g4_fixtures.py b/tests/g4_fixtures.py deleted file mode 100755 index f3cb8a0..0000000 --- a/tests/g4_fixtures.py +++ /dev/null @@ -1,1277 +0,0 @@ -#!/usr/bin/env python3 -"""Build and verify the deterministic xattr, ACL, and metabox G4 fixtures.""" - -from __future__ import annotations - -import argparse -from dataclasses import dataclass -import hashlib -import json -import os -from pathlib import Path -import shutil -import stat -import struct -import subprocess - - -SUPER = 1024 -MAGIC = 0xE0F5E1E2 -FEATURE_COMPAT_SB_CHKSUM = 0x00000001 -FEATURE_COMPAT_SHARED_EA_IN_METABOX = 0x00000008 -FEATURE_COMPAT_PLAIN_XATTR_PFX = 0x00000010 -FEATURE_COMPAT_ISHARE_XATTRS = 0x00000020 -FEATURE_INCOMPAT_FRAGMENTS = 0x00000020 -FEATURE_INCOMPAT_XATTR_PREFIXES = 0x00000040 -FEATURE_INCOMPAT_METABOX = 0x00000100 -XATTR_INDEX_USER = 1 -XATTR_INDEX_ACL_ACCESS = 2 -XATTR_INDEX_ACL_DEFAULT = 3 -XATTR_INDEX_TRUSTED = 4 -XATTR_INDEX_SECURITY = 6 -XATTR_LONG_PREFIX = 0x80 -METABOX_NID_BIT = 1 << 63 -LAYOUT_FLAT_PLAIN = 0 -LAYOUT_COMPRESSED_FULL = 1 -LAYOUT_FLAT_INLINE = 2 -LAYOUT_COMPRESSED_COMPACT = 3 -ACL_VERSION = 2 -ACL_USER_OBJ = 0x01 -ACL_USER = 0x02 -ACL_GROUP_OBJ = 0x04 -ACL_GROUP = 0x08 -ACL_MASK = 0x10 -ACL_OTHER = 0x20 -ACL_UNDEFINED_ID = 0xFFFFFFFF -BLOCK_SIZE = 4096 -METABOX_SIZE = 32768 -METABOX_INODE_A = 16 -METABOX_INODE_B = 32 -METABOX_PREFIX_OFFSET = 8192 -METABOX_XATTR_BASE = 4096 -CRC32C_POLYNOMIAL = 0x82F63B78 - - -class FixtureError(RuntimeError): - pass - - -@dataclass(frozen=True) -class Inode: - nid: int - offset: int - inode_format: int - inode_size: int - xattr_size: int - layout: int - mode: int - size: int - start_block: int - - -@dataclass(frozen=True) -class DirectoryEntry: - name: bytes - nid: int - offset: int - - -@dataclass(frozen=True) -class XattrEntry: - offset: int - size: int - name_index: int - name: bytes - value: bytes - - -def align(value: int, alignment: int) -> int: - return (value + alignment - 1) & -alignment - - -def sha256_bytes(data: bytes | bytearray) -> str: - return hashlib.sha256(data).hexdigest() - - -def sha256_path(path: Path) -> str: - return sha256_bytes(path.read_bytes()) - - -def crc32c(data: bytes | bytearray) -> int: - checksum = 0xFFFFFFFF - for byte in data: - checksum ^= byte - for _ in range(8): - checksum = (checksum >> 1) ^ ( - CRC32C_POLYNOMIAL if checksum & 1 else 0 - ) - return checksum & 0xFFFFFFFF - - -class ErofsImage: - def __init__(self, data: bytes | bytearray, source: Path) -> None: - self.data = bytearray(data) - self.source = source - self.validate_superblock() - - @classmethod - def load(cls, path: Path) -> "ErofsImage": - return cls(path.read_bytes(), path) - - def clone(self) -> "ErofsImage": - return ErofsImage(self.data, self.source) - - def u16(self, offset: int) -> int: - return struct.unpack_from(" int: - return struct.unpack_from(" int: - return struct.unpack_from(" None: - struct.pack_into(" None: - struct.pack_into(" None: - struct.pack_into(" int: - return self.data[SUPER + 12] - - @property - def block_size(self) -> int: - return 1 << self.block_bits - - @property - def feature_compat(self) -> int: - return self.u32(SUPER + 8) - - @property - def feature_incompat(self) -> int: - return self.u32(SUPER + 80) - - @property - def blocks(self) -> int: - return self.u32(SUPER + 36) - - @property - def declared_size(self) -> int: - return self.blocks << self.block_bits - - @property - def root_nid(self) -> int: - return self.u16(SUPER + 14) - - @property - def meta_offset(self) -> int: - return self.u32(SUPER + 40) << self.block_bits - - @property - def packed_nid(self) -> int: - return self.u64(SUPER + 96) - - @property - def checksum_end(self) -> int: - return SUPER + self.block_size - SUPER - - def calculated_checksum(self) -> int: - window = bytearray(self.data[SUPER : self.checksum_end]) - struct.pack_into(" None: - if not self.feature_compat & FEATURE_COMPAT_SB_CHKSUM: - raise FixtureError(f"{self.source}: checksum feature is required") - self.put_u32(SUPER + 4, 0) - self.put_u32(SUPER + 4, self.calculated_checksum()) - if self.u32(SUPER + 4) != self.calculated_checksum(): - raise AssertionError("updated checksum does not verify") - - def validate_superblock(self) -> None: - if len(self.data) < SUPER + 144: - raise FixtureError(f"{self.source}: image is shorter than 1168 bytes") - if self.u32(SUPER) != MAGIC: - raise FixtureError(f"{self.source}: bad magic") - if not 9 <= self.block_bits <= 16: - raise FixtureError(f"{self.source}: invalid block bits") - if self.blocks == 0 or self.declared_size > len(self.data): - raise FixtureError(f"{self.source}: invalid declared size") - if ( - self.feature_compat & FEATURE_COMPAT_SB_CHKSUM - and self.u32(SUPER + 4) != self.calculated_checksum() - ): - raise FixtureError(f"{self.source}: bad checksum") - - def inode(self, nid: int) -> Inode: - offset = self.meta_offset + (nid << 5) - if offset > self.declared_size - 32: - raise FixtureError(f"{self.source}: nid {nid} is outside the image") - inode_format = self.u16(offset) - inode_size = 64 if inode_format & 1 else 32 - count = self.u16(offset + 2) - xattr_size = 0 if count == 0 else 12 + 4 * (count - 1) - size = self.u64(offset + 8) if inode_size == 64 else self.u32(offset + 8) - return Inode( - nid=nid, - offset=offset, - inode_format=inode_format, - inode_size=inode_size, - xattr_size=xattr_size, - layout=(inode_format >> 1) & 7, - mode=self.u16(offset + 4), - size=size, - start_block=self.u32(offset + 16), - ) - - def inode_data(self, inode: Inode) -> bytes: - if inode.layout == LAYOUT_FLAT_PLAIN: - start = inode.start_block << self.block_bits - end = start + inode.size - if end > self.declared_size: - raise FixtureError("plain inode data exceeds the declared image") - return bytes(self.data[start:end]) - if inode.layout != LAYOUT_FLAT_INLINE: - raise FixtureError(f"inode {inode.nid}: unsupported host layout {inode.layout}") - full_size = inode.size // self.block_size * self.block_size - if inode.size and inode.size % self.block_size == 0: - full_size -= self.block_size - full_start = inode.start_block << self.block_bits - inline_start = inode.offset + inode.inode_size + inode.xattr_size - full = self.data[full_start : full_start + full_size] - tail = self.data[inline_start : inline_start + inode.size - full_size] - if len(full) + len(tail) != inode.size: - raise FixtureError("inline inode data is truncated") - return bytes(full + tail) - - def directory_entries(self, inode: Inode) -> list[DirectoryEntry]: - data = self.inode_data(inode) - entries: list[DirectoryEntry] = [] - data_base = ( - inode.start_block << self.block_bits - if inode.layout == LAYOUT_FLAT_PLAIN - else inode.offset + inode.inode_size + inode.xattr_size - ) - for block_start in range(0, len(data), self.block_size): - block = data[block_start : block_start + self.block_size] - if len(block) < 12: - raise FixtureError("short directory block") - first_name = struct.unpack_from(" len(block): - raise FixtureError("invalid directory first-name offset") - count = first_name // 12 - for index in range(count): - item = index * 12 - name_start = struct.unpack_from(" tuple[DirectoryEntry, Inode]: - encoded = name.removeprefix("/").encode("ascii") - root = self.inode(self.root_nid) - for entry in self.directory_entries(root): - if entry.name == encoded: - return entry, self.inode(entry.nid) - raise FixtureError(f"{self.source}: root entry not found: {name}") - - def inline_xattrs(self, inode: Inode) -> tuple[int, list[XattrEntry]]: - if inode.xattr_size < 12: - raise FixtureError(f"inode {inode.nid} has no xattr header") - body = inode.offset + inode.inode_size - shared_count = self.data[body + 4] - cursor = body + 12 + shared_count * 4 - end = body + inode.xattr_size - if cursor > end: - raise FixtureError("shared xattr array exceeds inode body") - entries = [] - while cursor < end: - if cursor > end - 4: - raise FixtureError("truncated inline xattr header") - name_len = self.data[cursor] - name_index = self.data[cursor + 1] - value_len = self.u16(cursor + 2) - size = align(4 + name_len + value_len, 4) - if size > end - cursor: - raise FixtureError("inline xattr entry exceeds inode body") - entries.append( - XattrEntry( - offset=cursor, - size=size, - name_index=name_index, - name=bytes(self.data[cursor + 4 : cursor + 4 + name_len]), - value=bytes( - self.data[ - cursor + 4 + name_len : cursor + 4 + name_len + value_len - ] - ), - ) - ) - cursor += size - return shared_count, entries - - def shared_ids(self, inode: Inode) -> list[int]: - body = inode.offset + inode.inode_size - count = self.data[body + 4] - return [self.u32(body + 12 + index * 4) for index in range(count)] - - def shared_xattr(self, shared_id: int) -> XattrEntry: - offset = (self.u32(SUPER + 44) << self.block_bits) + shared_id * 4 - if offset > self.declared_size - 4: - raise FixtureError(f"shared id {shared_id} starts outside the image") - name_len = self.data[offset] - value_len = self.u16(offset + 2) - size = align(4 + name_len + value_len, 4) - if size > self.declared_size - offset: - raise FixtureError(f"shared id {shared_id} exceeds the image") - return XattrEntry( - offset=offset, - size=size, - name_index=self.data[offset + 1], - name=bytes(self.data[offset + 4 : offset + 4 + name_len]), - value=bytes( - self.data[offset + 4 + name_len : offset + 4 + name_len + value_len] - ), - ) - - def find_xattr(self, path: str, name: bytes) -> tuple[str, XattrEntry, int | None]: - _, inode = self.resolve_root(path) - _, inline = self.inline_xattrs(inode) - for entry in inline: - if entry.name == name: - return "inline", entry, None - for index, shared_id in enumerate(self.shared_ids(inode)): - entry = self.shared_xattr(shared_id) - if entry.name == name: - return "shared", entry, index - raise FixtureError(f"{path}: xattr {name!r} not found") - - def prefix_records(self) -> list[tuple[int, int, bytes]]: - count = self.data[SUPER + 91] - if count == 0: - return [] - if self.feature_compat & FEATURE_COMPAT_PLAIN_XATTR_PFX: - backing = bytes(self.data[: self.declared_size]) - else: - if self.packed_nid == 0: - raise FixtureError("prefix table has no packed backing") - backing = self.inode_data(self.inode(self.packed_nid)) - cursor = self.u32(SUPER + 92) << 2 - records = [] - for _ in range(count): - cursor = align(cursor, 4) - if cursor > len(backing) - 3: - raise FixtureError("prefix record header exceeds backing") - length = struct.unpack_from(" len(backing): - raise FixtureError("invalid prefix record") - records.append((cursor, backing[cursor + 2], backing[cursor + 3 : cursor + 2 + length])) - cursor += 2 + length - return records - - def save(self, path: Path) -> None: - path.write_bytes(self.data) - - -def xattr_entry(name_index: int, name: bytes, value: bytes) -> bytes: - raw = struct.pack(" bytes: - return struct.pack(" bytes: - entries = [(ACL_USER_OBJ, 7, ACL_UNDEFINED_ID)] - entries.extend((ACL_USER, 5, identifier) for identifier in named_users) - entries.extend( - [ - (ACL_GROUP_OBJ, 5, ACL_UNDEFINED_ID), - (ACL_MASK, 5, ACL_UNDEFINED_ID), - (ACL_OTHER, 1, ACL_UNDEFINED_ID), - ] - ) - return acl_value(entries) - - -def prefix_record(base_index: int, infix: bytes) -> bytes: - payload = bytes([base_index]) + infix - return struct.pack(" int: - start = align(offset, 4) - blob[start : start + len(payload)] = payload - return start - - -def synthetic_inode(content: bytes, shared_ids: list[int], item_value: bytes) -> bytes: - header = bytearray(12 + 4 * len(shared_ids)) - header[4] = len(shared_ids) - for index, shared_id in enumerate(shared_ids): - struct.pack_into(" tuple[bytes, dict[str, object]]: - payload = bytearray(METABOX_SIZE) - shared_offset = METABOX_XATTR_BASE - shared_records = [] - for name_index, name, value in ( - (XATTR_INDEX_USER, b"metaboxshared", b"nonzero-base"), - (XATTR_LONG_PREFIX | 0, b"setting", b"long-prefix-value"), - (XATTR_INDEX_USER, b"shared-prefix-key", b"shared-value"), - ): - raw = xattr_entry(name_index, name, value) - start = add_aligned(payload, shared_offset, raw) - shared_id = (start - METABOX_XATTR_BASE) // 4 - shared_records.append((shared_id, start, raw)) - shared_offset = start + len(raw) - - prefix_cursor = METABOX_PREFIX_OFFSET - prefix_fields = [] - for base_index, infix in ( - (XATTR_INDEX_USER, b"repo22.application.component."), - (XATTR_INDEX_TRUSTED, b"repo22.trusted.deep."), - ): - raw = prefix_record(base_index, infix) - start = add_aligned(payload, prefix_cursor, raw) - prefix_fields.append((start, base_index, infix)) - prefix_cursor = start + len(raw) - - inode_a = synthetic_inode( - b"metabox-file-a\n", - [shared_records[0][0], shared_records[1][0], shared_records[2][0]], - b"value-000", - ) - inode_b = synthetic_inode( - b"metabox-file-b\n", - [shared_records[0][0]], - b"value-001", - ) - offset_a = METABOX_INODE_A << 5 - offset_b = METABOX_INODE_B << 5 - if offset_a + len(inode_a) >= offset_b: - raise AssertionError("synthetic metabox inodes overlap") - payload[offset_a : offset_a + len(inode_a)] = inode_a - payload[offset_b : offset_b + len(inode_b)] = inode_b - - manifest = { - "size": len(payload), - "sha256": sha256_bytes(payload), - "inode_a": {"nid": METABOX_INODE_A, "offset": offset_a}, - "inode_b": {"nid": METABOX_INODE_B, "offset": offset_b}, - "xattr_base": METABOX_XATTR_BASE, - "shared": [ - { - "id": shared_id, - "offset": offset, - "sha256": sha256_bytes(raw), - } - for shared_id, offset, raw in shared_records - ], - "prefix_start": METABOX_PREFIX_OFFSET // 4, - "prefixes": [ - {"offset": offset, "base_index": base_index, "infix": infix.decode("ascii")} - for offset, base_index, infix in prefix_fields - ], - } - return bytes(payload), manifest - - -def write_file(path: Path, data: bytes = b"") -> None: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_bytes(data) - - -def set_user_xattr(path: Path, name: str, value: bytes) -> None: - os.setxattr(path, f"user.{name}", value) - - -def normalize_times(root: Path) -> None: - for path in sorted(root.rglob("*"), reverse=True): - os.utime(path, (0, 0), follow_symlinks=False) - os.utime(root, (0, 0), follow_symlinks=False) - - -def make_sources(output: Path) -> dict[str, object]: - source_root = output / "source" - basic = source_root / "basic" - carrier = source_root / "carrier" - basic.mkdir(parents=True) - carrier.mkdir(parents=True) - metabox_payload, metabox_manifest = make_metabox_payload() - - basic_files: dict[str, dict[str, bytes]] = { - "inline-user": { - "comment": b"inline-user-value\x00tail", - "special.chars": b"special-value", - }, - "inline-multi": { - "attr1": b"one", - "attr2": b"two\x00binary", - "attr3": bytes(range(32)), - }, - "inline-trusted": {"admin": b"trusted-inline-value"}, - "inline-security": { - "capability": bytes.fromhex("0100000200000000aabbccdd"), - "selinux": b"system_u:object_r:repo22_t:s0\x00", - }, - "corrupt-inline": {"bad": b"bad-format-target"}, - "user-subset": { - "comment": b"subset-comment", - "author": b"repo22", - "checksum": b"sha256:0123456789abcdef", - "com.repo22.app.setting": b"enabled", - }, - } - for name, attrs in basic_files.items(): - path = basic / name - write_file(path, f"{name}\n".encode("ascii")) - for xattr_name, value in attrs.items(): - set_user_xattr(path, xattr_name, value) - - for name in ("shared-a", "shared-b", "shared-multi"): - path = basic / name - write_file(path, f"{name}\n".encode("ascii")) - set_user_xattr(path, "shared_key", b"shared-value\x00exact") - set_user_xattr(path, "shared_comment", b"shared-comment") - set_user_xattr(path, "shared_binary", bytes(range(16))) - set_user_xattr(basic / "shared-a", "local", b"in-bounds-local") - - for name in ("trusted-shared-a", "trusted-shared-b", "trusted-shared-c"): - path = basic / name - write_file(path, f"{name}\n".encode("ascii")) - set_user_xattr(path, "config", b"trusted-shared-value") - - for name in ("security-shared-a", "security-shared-b", "security-shared-c"): - path = basic / name - write_file(path, f"{name}\n".encode("ascii")) - set_user_xattr(path, "selinux", b"system_u:object_r:shared_repo22_t:s0\x00") - - for index in range(4): - path = basic / f"prefix-user-{index}" - write_file(path, f"prefix user {index}\n".encode("ascii")) - set_user_xattr( - path, - "repo22.application.component.setting", - f"prefix-value-{index}".encode("ascii"), - ) - for index in range(3): - path = basic / f"prefix-trusted-{index}" - write_file(path, f"prefix trusted {index}\n".encode("ascii")) - set_user_xattr( - path, - "repo22.trusted.deep.setting", - f"trusted-prefix-value-{index}".encode("ascii"), - ) - - acl_values = { - "acl-unordered": canonical_acl(3002, 2002), - "acl-header": struct.pack(" dict[str, object]: - entries = [] - for path in sorted([root, *root.rglob("*")]): - info = path.lstat() - relative = path.relative_to(root).as_posix() or "." - item: dict[str, object] = { - "path": relative, - "mode": stat.S_IFMT(info.st_mode) | stat.S_IMODE(info.st_mode), - "uid": info.st_uid, - "gid": info.st_gid, - "size": info.st_size, - } - if path.is_file(): - item["content_sha256"] = sha256_path(path) - names = sorted(os.listxattr(path, follow_symlinks=False)) - item["xattrs"] = { - name: os.getxattr(path, name, follow_symlinks=False).hex() - for name in names - } - entries.append(item) - canonical = json.dumps(entries, separators=(",", ":"), sort_keys=True).encode("ascii") - return {"sha256": sha256_bytes(canonical), "entries": entries} - - -def tool_version() -> str: - result = subprocess.run( - ["mkfs.erofs", "-V"], - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - ) - version = result.stdout.strip() - if "erofs-utils) 1.8.6" not in version: - raise FixtureError(f"mkfs.erofs 1.8.6 is required, got: {version}") - return version - - -def run_mkfs(source: Path, image: Path, uuid: str, *extra: str) -> list[str]: - command = [ - "mkfs.erofs", - "-d0", - "-T0", - "--all-time", - "--all-root", - "--workers=1", - "--sort=path", - "-U", - uuid, - "--xattr-prefix=user.repo22.application.component.", - "--xattr-prefix=user.repo22.trusted.deep.", - *extra, - str(image), - str(source), - ] - subprocess.run(command, check=True) - return command - - -class TransformLog: - def __init__(self) -> None: - self.records: list[dict[str, object]] = [] - - def bytes(self, image: ErofsImage, field: str, offset: int, after: bytes) -> None: - before = bytes(image.data[offset : offset + len(after)]) - if len(before) != len(after): - raise FixtureError(f"{field}: patch exceeds image") - image.data[offset : offset + len(after)] = after - self.records.append( - { - "field": field, - "offset": offset, - "size": len(after), - "before_hex": before.hex(), - "after_hex": after.hex(), - } - ) - - def u8(self, image: ErofsImage, field: str, offset: int, value: int) -> None: - self.bytes(image, field, offset, bytes([value])) - - def u16(self, image: ErofsImage, field: str, offset: int, value: int) -> None: - self.bytes(image, field, offset, struct.pack(" None: - self.bytes(image, field, offset, struct.pack(" None: - self.bytes(image, field, offset, struct.pack(" int: - for prefix_id, (_, _, record_infix) in enumerate(image.prefix_records()): - if record_infix == infix: - return prefix_id - raise FixtureError(f"prefix not found: {infix!r}") - - -def patch_namespace_entries(image: ErofsImage, log: TransformLog) -> None: - for path, name, expected_storage, new_index in ( - ("/inline-trusted", b"admin", "inline", XATTR_INDEX_TRUSTED), - ("/inline-security", b"capability", "inline", XATTR_INDEX_SECURITY), - ("/inline-security", b"selinux", "inline", XATTR_INDEX_SECURITY), - ("/trusted-shared-a", b"config", "shared", XATTR_INDEX_TRUSTED), - ("/security-shared-a", b"selinux", "shared", XATTR_INDEX_SECURITY), - ): - storage, entry, _ = image.find_xattr(path, name) - if storage != expected_storage or entry.name_index != XATTR_INDEX_USER: - raise FixtureError(f"{path}:{name!r} has unexpected storage/index") - log.u8(image, f"{path}:{name.decode()}.e_name_index", entry.offset + 1, new_index) - - trusted_prefix_id = find_prefix_id(image, b"repo22.trusted.deep.") - packed = image.inode(image.packed_nid) - packed_data = image.inode_data(packed) - record_offset = image.prefix_records()[trusted_prefix_id][0] - if packed.layout != LAYOUT_FLAT_INLINE: - raise FixtureError("expected inline packed prefix carrier") - physical = packed.offset + packed.inode_size + packed.xattr_size + record_offset + 2 - if packed_data[record_offset + 2] != XATTR_INDEX_USER: - raise FixtureError("trusted prefix base is not the user placeholder") - log.u8(image, "packed_prefix.trusted.base_index", physical, XATTR_INDEX_TRUSTED) - - -def replace_acl_entry(image: ErofsImage, path: str, value: bytes, log: TransformLog) -> None: - _, inode = image.resolve_root(path) - shared_count, entries = image.inline_xattrs(inode) - placeholders = [entry for entry in entries if entry.name == b"acl_access_placeholder"] - if shared_count != 0 or len(placeholders) != 1: - raise FixtureError(f"{path}: ACL placeholder is not one inline xattr") - body = bytearray(12) - for entry in entries: - if entry.name == b"acl_access_placeholder": - body.extend(xattr_entry(XATTR_INDEX_ACL_ACCESS, b"", value)) - else: - body.extend(xattr_entry(entry.name_index, entry.name, entry.value)) - body.extend(bytes(align(len(body), 4) - len(body))) - if len(body) > inode.xattr_size: - raise FixtureError(f"{path}: rebuilt ACL body grew") - padded = body + bytes(inode.xattr_size - len(body)) - log.bytes(image, f"{path}.xattr_body", inode.offset + inode.inode_size, padded) - icount = 1 + (len(body) - 12) // 4 - log.u16(image, f"{path}.i_xattr_icount", inode.offset + 2, icount) - - -def patch_acls(image: ErofsImage, log: TransformLog) -> None: - acl_values = { - "/acl-unordered": canonical_acl(3002, 2002), - "/acl-header": struct.pack(" None: - image.update_checksum() - image.validate_superblock() - image.save(path) - - -def transformed_basic(base: Path) -> tuple[ErofsImage, TransformLog]: - image = ErofsImage.load(base) - log = TransformLog() - patch_namespace_entries(image, log) - patch_acls(image, log) - image.update_checksum() - return image, log - - -def add_metabox_fields( - image: ErofsImage, - log: TransformLog, - carrier_nid: int, - clear_packed: bool, -) -> None: - if image.u32(SUPER + 40) != 0: - raise FixtureError("metabox conversion expects metadata in block zero") - metadata_copy = bytes(image.data[: image.block_size]) - relocated_offset = align(len(image.data), image.block_size) - image.data.extend(bytes(relocated_offset - len(image.data))) - image.data.extend(bytes(image.block_size)) - image.data[relocated_offset : relocated_offset + image.block_size] = metadata_copy - log.u32(image, "super.blocks_lo", SUPER + 36, len(image.data) // image.block_size) - log.u32(image, "super.meta_blkaddr", SUPER + 40, relocated_offset // image.block_size) - log.u32( - image, - "super.feature_compat", - SUPER + 8, - image.feature_compat | FEATURE_COMPAT_SHARED_EA_IN_METABOX, - ) - log.u8(image, "super.sb_extslots", SUPER + 13, 1) - log.u32( - image, - "super.feature_incompat", - SUPER + 80, - image.feature_incompat | FEATURE_INCOMPAT_METABOX | FEATURE_INCOMPAT_XATTR_PREFIXES, - ) - log.u32(image, "super.xattr_blkaddr", SUPER + 44, METABOX_XATTR_BASE // BLOCK_SIZE) - log.u8(image, "super.xattr_prefix_count", SUPER + 91, 2) - log.u32(image, "super.xattr_prefix_start", SUPER + 92, METABOX_PREFIX_OFFSET // 4) - log.u64(image, "super.metabox_nid", SUPER + 128, carrier_nid) - if clear_packed: - log.u64(image, "super.packed_nid", SUPER + 96, 0) - for path, nid in (("/metabox-a", METABOX_INODE_A), ("/metabox-b", METABOX_INODE_B)): - entry, _ = image.resolve_root(path) - log.u64(image, f"{path}.dirent_nid", entry.offset, METABOX_NID_BIT | nid) - - -def make_primary_prefix(image: ErofsImage, log: TransformLog) -> None: - packed_data = image.inode_data(image.inode(image.packed_nid)) - start = align(len(image.data), image.block_size) - if len(image.data) < start: - image.data.extend(bytes(start - len(image.data))) - image.data.extend(packed_data) - image.data.extend(bytes(align(len(image.data), image.block_size) - len(image.data))) - new_blocks = len(image.data) // image.block_size - log.u32(image, "super.blocks_lo", SUPER + 36, new_blocks) - log.u32( - image, - "super.feature_compat", - SUPER + 8, - image.feature_compat | FEATURE_COMPAT_PLAIN_XATTR_PFX, - ) - log.u32(image, "super.xattr_prefix_start", SUPER + 92, start // 4) - log.u64(image, "super.packed_nid", SUPER + 96, 0) - - -def save_variant( - output: Path, - name: str, - image: ErofsImage, - log: TransformLog, - transforms: dict[str, object], - base_name: str, - semantics: dict[str, object] | None = None, -) -> None: - path = output / "images" / name - checksum_and_save(image, path) - transforms[name] = { - "base": base_name, - "image_sha256": sha256_path(path), - "provider_size": path.stat().st_size, - "declared_size": image.declared_size, - "fields": log.records, - "semantics": semantics or {}, - } - - -def make_variants(output: Path, base_commands: dict[str, list[str]]) -> dict[str, object]: - bases = output / "bases" - images = output / "images" - images.mkdir() - transforms: dict[str, object] = {} - - basic, basic_log = transformed_basic(bases / "basic-base.erofs") - save_variant(output, "basic.erofs", basic.clone(), basic_log, transforms, "basic-base.erofs") - - primary = basic.clone() - primary_log = TransformLog() - make_primary_prefix(primary, primary_log) - save_variant(output, "prefix-primary.erofs", primary, primary_log, transforms, "basic.erofs") - - metabox_plain = ErofsImage.load(bases / "carrier-plain-base.erofs") - metabox_plain_log = TransformLog() - _, carrier_inode = metabox_plain.resolve_root("/metabox-carrier.bin") - if carrier_inode.layout != LAYOUT_FLAT_PLAIN or carrier_inode.size != METABOX_SIZE: - raise FixtureError("plain metabox carrier has the wrong layout or size") - add_metabox_fields(metabox_plain, metabox_plain_log, carrier_inode.nid, True) - save_variant( - output, - "metabox-plain.erofs", - metabox_plain, - metabox_plain_log, - transforms, - "carrier-plain-base.erofs", - {"carrier_nid": carrier_inode.nid, "carrier_layout": carrier_inode.layout}, - ) - - compressed = ErofsImage.load(bases / "carrier-compressed-base.erofs") - compressed_log = TransformLog() - _, compressed_carrier = compressed.resolve_root("/metabox-carrier.bin") - if compressed_carrier.layout not in (LAYOUT_COMPRESSED_FULL, LAYOUT_COMPRESSED_COMPACT): - raise FixtureError("compressed metabox carrier is not compressed") - add_metabox_fields(compressed, compressed_log, compressed_carrier.nid, True) - save_variant( - output, - "metabox-compressed.erofs", - compressed, - compressed_log, - transforms, - "carrier-compressed-base.erofs", - {"carrier_nid": compressed_carrier.nid, "carrier_layout": compressed_carrier.layout}, - ) - - fragment = ErofsImage.load(bases / "carrier-fragment-base.erofs") - fragment_log = TransformLog() - _, fragment_carrier = fragment.resolve_root("/metabox-carrier.bin") - if fragment_carrier.layout not in (LAYOUT_COMPRESSED_FULL, LAYOUT_COMPRESSED_COMPACT): - raise FixtureError("fragment metabox carrier is not compressed") - add_metabox_fields(fragment, fragment_log, fragment_carrier.nid, False) - _, fragment_carrier = fragment.resolve_root("/metabox-carrier.bin") - header_offset = align( - fragment_carrier.offset + fragment_carrier.inode_size + fragment_carrier.xattr_size, - 8, - ) - fragment_header = fragment.u64(header_offset) - if not fragment_header & METABOX_NID_BIT: - raise FixtureError("metabox carrier is not a whole-file fragment") - fragment_offset = fragment_header ^ METABOX_NID_BIT - packed_inode = fragment.inode(fragment.packed_nid) - if fragment_offset + fragment_carrier.size > packed_inode.size: - raise FixtureError("positive fragment range exceeds packed inode") - fragment_semantics = { - "carrier_nid": fragment_carrier.nid, - "carrier_layout": fragment_carrier.layout, - "fragment_header_offset": header_offset, - "fragment_header": fragment_header, - "fragment_offset": fragment_offset, - "carrier_size": fragment_carrier.size, - "packed_nid": fragment.packed_nid, - "packed_size": packed_inode.size, - } - save_variant( - output, - "metabox-fragment.erofs", - fragment, - fragment_log, - transforms, - "carrier-fragment-base.erofs", - fragment_semantics, - ) - - bad_inline = basic.clone() - bad_inline_log = TransformLog() - storage, entry, _ = bad_inline.find_xattr("/corrupt-inline", b"bad") - if storage != "inline": - raise FixtureError("corrupt-inline target is not inline") - bad_inline_log.u16(bad_inline, "/corrupt-inline.e_value_size", entry.offset + 2, 0xFFFF) - save_variant(output, "bad-inline-entry.erofs", bad_inline, bad_inline_log, transforms, "basic.erofs") - - bad_shared = basic.clone() - bad_shared_log = TransformLog() - storage, shared_entry, _ = bad_shared.find_xattr("/shared-a", b"shared_key") - if storage != "shared": - raise FixtureError("shared corruption target is not shared") - bad_shared_log.u16(bad_shared, "shared_key.e_value_size", shared_entry.offset + 2, 0xFFFF) - save_variant(output, "bad-shared-entry.erofs", bad_shared, bad_shared_log, transforms, "basic.erofs") - - shared_oob = basic.clone() - shared_oob_log = TransformLog() - _, shared_inode = shared_oob.resolve_root("/shared-a") - target_id = None - target_array_index = None - for array_index, shared_id in enumerate(shared_oob.shared_ids(shared_inode)): - if shared_oob.shared_xattr(shared_id).name == b"shared_key": - target_id = shared_id - target_array_index = array_index - break - if target_id is None or target_array_index is None: - raise FixtureError("shared OOB target ID not found") - declared_end = shared_oob.declared_size - sentinel = xattr_entry(XATTR_INDEX_USER, b"shared_key", b"sentinel-must-not-leak") - shared_oob.data.extend(bytes(BLOCK_SIZE)) - shared_oob.data[declared_end : declared_end + len(sentinel)] = sentinel - body = shared_inode.offset + shared_inode.inode_size - shared_oob_log.u32( - shared_oob, - "/shared-a.shared_key_id", - body + 12 + target_array_index * 4, - declared_end // 4, - ) - save_variant( - output, - "bad-shared-declared-bounds.erofs", - shared_oob, - shared_oob_log, - transforms, - "basic.erofs", - {"sentinel_offset": declared_end, "old_shared_id": target_id}, - ) - - prefix_oob = basic.clone() - prefix_oob_log = TransformLog() - prefix_declared_end = prefix_oob.declared_size - sentinel_prefix = prefix_record(XATTR_INDEX_USER, b"sentinel-never-visible.") - prefix_oob.data.extend(bytes(BLOCK_SIZE)) - prefix_oob.data[prefix_declared_end : prefix_declared_end + len(sentinel_prefix)] = sentinel_prefix - prefix_oob_log.u32( - prefix_oob, - "super.feature_compat", - SUPER + 8, - prefix_oob.feature_compat | FEATURE_COMPAT_PLAIN_XATTR_PFX, - ) - prefix_oob_log.u8(prefix_oob, "super.xattr_prefix_count", SUPER + 91, 1) - prefix_oob_log.u32( - prefix_oob, - "super.xattr_prefix_start", - SUPER + 92, - prefix_declared_end // 4, - ) - prefix_oob_log.u64(prefix_oob, "super.packed_nid", SUPER + 96, 0) - save_variant( - output, - "bad-prefix-declared-bounds.erofs", - prefix_oob, - prefix_oob_log, - transforms, - "basic.erofs", - {"sentinel_offset": prefix_declared_end}, - ) - - truncated = basic.clone() - truncated_log = TransformLog() - _, truncated_carrier = truncated.resolve_root("/metabox-carrier.bin") - truncated_log.u32( - truncated, - "super.feature_incompat", - SUPER + 80, - truncated.feature_incompat | FEATURE_INCOMPAT_METABOX, - ) - truncated_log.u64(truncated, "super.metabox_nid", SUPER + 128, truncated_carrier.nid) - save_variant(output, "bad-metabox-truncated-extension.erofs", truncated, truncated_log, transforms, "basic.erofs") - - ishare = basic.clone() - ishare_log = TransformLog() - ishare_log.u32( - ishare, - "super.feature_compat", - SUPER + 8, - ishare.feature_compat | FEATURE_COMPAT_ISHARE_XATTRS, - ) - ishare_log.u8( - ishare, - "super.ishare_xattr_prefix_id", - SUPER + 105, - ishare.data[SUPER + 91], - ) - save_variant(output, "bad-ishare-prefix-id.erofs", ishare, ishare_log, transforms, "basic.erofs") - - positive_fragment_path = output / "images" / "metabox-fragment.erofs" - positive_fragment = ErofsImage.load(positive_fragment_path) - positive_semantics = transforms["metabox-fragment.erofs"]["semantics"] - - self_loop = positive_fragment.clone() - self_loop_log = TransformLog() - self_loop_log.u64( - self_loop, - "super.packed_nid", - SUPER + 96, - int(positive_semantics["carrier_nid"]), - ) - save_variant(output, "bad-fragment-self-loop.erofs", self_loop, self_loop_log, transforms, "metabox-fragment.erofs") - - range_bad = positive_fragment.clone() - range_log = TransformLog() - range_log.u64( - range_bad, - "metabox.fragment_header", - int(positive_semantics["fragment_header_offset"]), - METABOX_NID_BIT | int(positive_semantics["packed_size"]), - ) - save_variant(output, "bad-fragment-range.erofs", range_bad, range_log, transforms, "metabox-fragment.erofs") - - recursive_metabox = positive_fragment.clone() - recursive_metabox_log = TransformLog() - recursive_metabox_log.u64( - recursive_metabox, - "super.metabox_nid", - SUPER + 128, - METABOX_NID_BIT | int(positive_semantics["carrier_nid"]), - ) - save_variant( - output, - "bad-metabox-recursive-nid.erofs", - recursive_metabox, - recursive_metabox_log, - transforms, - "metabox-fragment.erofs", - ) - - recursive_packed = positive_fragment.clone() - recursive_packed_log = TransformLog() - recursive_packed_log.u64( - recursive_packed, - "super.packed_nid", - SUPER + 96, - METABOX_NID_BIT | int(positive_semantics["packed_nid"]), - ) - save_variant( - output, - "bad-packed-recursive-nid.erofs", - recursive_packed, - recursive_packed_log, - transforms, - "metabox-fragment.erofs", - ) - - return {"base_commands": base_commands, "images": transforms} - - -def write_checksums(output: Path) -> None: - paths = sorted((output / "bases").glob("*.erofs")) + sorted((output / "images").glob("*.erofs")) - with (output / "IMAGE-SHA256SUMS").open("w", encoding="ascii") as sums: - for path in paths: - sums.write(f"{sha256_path(path)} {path.relative_to(output)}\n") - - -def verify_output(output: Path) -> None: - manifest = json.loads((output / "fixture-manifest.json").read_text(encoding="ascii")) - inventory = source_inventory(output / "source") - if inventory["sha256"] != manifest["source"]["sha256"]: - raise FixtureError("source inventory hash changed") - for name, item in manifest["transforms"]["images"].items(): - path = output / "images" / name - if sha256_path(path) != item["image_sha256"]: - raise FixtureError(f"{name}: image hash changed") - data = path.read_bytes() - for field in item["fields"]: - offset = field["offset"] - size = field["size"] - if data[offset : offset + size].hex() != field["after_hex"]: - raise FixtureError(f"{name}: field check failed: {field['field']}") - image = ErofsImage.load(path) - if image.declared_size != item["declared_size"]: - raise FixtureError(f"{name}: declared size changed") - if len(data) != item["provider_size"]: - raise FixtureError(f"{name}: provider size changed") - checksums = {} - for line in (output / "IMAGE-SHA256SUMS").read_text(encoding="ascii").splitlines(): - digest, relative = line.split(" ", 1) - checksums[relative] = digest - for relative, digest in checksums.items(): - if sha256_path(output / relative) != digest: - raise FixtureError(f"checksum list mismatch: {relative}") - - -def make(output: Path) -> None: - if shutil.which("mkfs.erofs") is None: - raise FixtureError("mkfs.erofs is required") - if output.exists(): - raise FixtureError(f"output already exists: {output}") - output.mkdir(parents=True) - version = tool_version() - source_details = make_sources(output) - source = source_inventory(output / "source") - bases = output / "bases" - bases.mkdir() - base_commands = { - "basic-base.erofs": run_mkfs( - output / "source" / "basic", - bases / "basic-base.erofs", - "44444444-4444-4444-8444-444444444404", - ), - "carrier-plain-base.erofs": run_mkfs( - output / "source" / "carrier", - bases / "carrier-plain-base.erofs", - "44444444-4444-4444-8444-444444444434", - ), - "carrier-compressed-base.erofs": run_mkfs( - output / "source" / "carrier", - bases / "carrier-compressed-base.erofs", - "44444444-4444-4444-8444-444444444440", - "-zlz4", - "-C4096", - "-Elegacy-compress", - ), - "carrier-fragment-base.erofs": run_mkfs( - output / "source" / "carrier", - bases / "carrier-fragment-base.erofs", - "44444444-4444-4444-8444-444444444442", - "-zlz4", - "-C4096", - "-Eall-fragments", - ), - } - transforms = make_variants(output, base_commands) - write_checksums(output) - manifest = { - "generator": "g4_fixtures.py", - "mkfs_version": version, - "source": source, - "source_details": source_details, - "transforms": transforms, - } - (output / "fixture-manifest.json").write_text( - json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="ascii" - ) - (output / "SOURCE-SHA256").write_text(f"{source['sha256']} source-inventory\n", encoding="ascii") - (output / "mkfs-version.txt").write_text(version + "\n", encoding="ascii") - verify_output(output) - print(f"source_sha256={source['sha256']}") - print((output / "IMAGE-SHA256SUMS").read_text(encoding="ascii"), end="") - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - subparsers = parser.add_subparsers(dest="command", required=True) - make_parser = subparsers.add_parser("make") - make_parser.add_argument("--output", required=True, type=Path) - verify_parser = subparsers.add_parser("verify") - verify_parser.add_argument("--output", required=True, type=Path) - return parser.parse_args() - - -def main() -> None: - args = parse_args() - if args.command == "make": - make(args.output) - else: - verify_output(args.output) - - -if __name__ == "__main__": - try: - main() - except (FixtureError, OSError, subprocess.CalledProcessError) as error: - raise SystemExit(f"g4_fixtures.py: {error}") from error diff --git a/tests/g5_fixtures.py b/tests/g5_fixtures.py deleted file mode 100644 index e633e42..0000000 --- a/tests/g5_fixtures.py +++ /dev/null @@ -1,1109 +0,0 @@ -#!/usr/bin/env python3 -"""Generate and verify deterministic repo22 G5 compression fixtures.""" - -from __future__ import annotations - -import argparse -from collections import Counter -import hashlib -import json -from pathlib import Path -import re -import struct -import subprocess -from typing import Any - -from erofs_fixture import ErofsImage, Inode, SUPER, sha256 - - -BLOCK_SIZE = 4096 -FIXED_UUID = "00000000-0000-0000-0000-000000000000" -FEATURE_INCOMPAT_COMPR_HEAD2 = 0x00000008 -LAYOUT_COMPRESSED_FULL = 1 -LAYOUT_COMPRESSED_COMPACT = 3 -Z_EROFS_ADVISE_COMPACTED_2B = 0x0001 -Z_EROFS_ADVISE_BIG_PCLUSTER_1 = 0x0002 -Z_EROFS_ADVISE_BIG_PCLUSTER_2 = 0x0004 -Z_EROFS_ADVISE_INLINE_PCLUSTER = 0x0008 -Z_EROFS_ADVISE_INTERLACED_PCLUSTER = 0x0010 -Z_EROFS_LCLUSTER_TYPE_MASK = 0x0003 -Z_EROFS_LCLUSTER_TYPE_PLAIN = 0 -Z_EROFS_LCLUSTER_TYPE_HEAD1 = 1 -Z_EROFS_LCLUSTER_TYPE_NONHEAD = 2 -Z_EROFS_LCLUSTER_TYPE_HEAD2 = 3 -Z_EROFS_LI_PARTIAL_REF = 0x8000 -Z_EROFS_LI_D0_CBLKCNT = 0x0800 -ALGORITHMS = { - "lz4": 0, - "lzma": 1, - "deflate": 2, - "zstd": 3, -} - - -class FixtureError(RuntimeError): - pass - - -def align(value: int, alignment: int) -> int: - return (value + alignment - 1) & -alignment - - -def relative(path: Path, root: Path) -> str: - return str(path.relative_to(root)) - - -def run( - command: list[str], - *, - log: Path | None = None, - check: bool = True, -) -> subprocess.CompletedProcess[str]: - result = subprocess.run( - command, - check=False, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - ) - if log is not None: - log.write_text( - "$ " + " ".join(command) + "\n" + result.stdout, - encoding="utf-8", - ) - if check and result.returncode != 0: - raise FixtureError( - f"command failed ({result.returncode}): {' '.join(command)}\n" - f"{result.stdout}" - ) - return result - - -def write_repeated(path: Path, size: int, label: str) -> None: - token = (label.encode("ascii") + b"\n") * 64 - chunk = (token * ((1024 * 1024 + len(token) - 1) // len(token)))[ - : 1024 * 1024 - ] - remaining = size - with path.open("wb") as output: - while remaining: - amount = min(remaining, len(chunk)) - output.write(chunk[:amount]) - remaining -= amount - - -def mixed_chunk(seed: str, chunk_index: int, size: int) -> bytes: - output = bytearray() - block_index = 0 - while len(output) < size: - amount = min(BLOCK_SIZE, size - len(output)) - if block_index % 4 == 0: - material = hashlib.shake_256( - f"{seed}:{chunk_index}:{block_index}".encode("ascii") - ).digest(amount) - else: - token = ( - f"repo22-g5:{seed}:{chunk_index % 7}:{block_index % 11}\n" - ).encode("ascii") - material = (token * ((amount + len(token) - 1) // len(token)))[ - :amount - ] - output.extend(material) - block_index += 1 - return bytes(output) - - -def write_mixed(path: Path, size: int, seed: str) -> None: - chunk_size = 1024 * 1024 - remaining = size - index = 0 - with path.open("wb") as output: - while remaining: - amount = min(remaining, chunk_size) - output.write(mixed_chunk(seed, index, amount)) - remaining -= amount - index += 1 - - -def write_interlaced(path: Path, size: int) -> None: - remaining = size - extent = 0 - with path.open("wb") as output: - while remaining: - compressible_size = min(remaining, 16384) - token = f"repo22-interlaced-{extent % 13:02d}\n".encode("ascii") - output.write( - (token * ((compressible_size + len(token) - 1) // len(token)))[ - :compressible_size - ] - ) - remaining -= compressible_size - if not remaining: - break - random_size = min(remaining, BLOCK_SIZE) - output.write( - hashlib.shake_256( - f"repo22-interlaced-random-{extent}".encode("ascii") - ).digest(random_size) - ) - remaining -= random_size - extent += 1 - - -def create_sources(source_root: Path) -> None: - directories = { - "lz4": source_root / "lz4", - "large": source_root / "large", - "levels": source_root / "levels", - "lzma-large": source_root / "lzma-large", - "microlzma": source_root / "microlzma", - "partial": source_root / "partial", - "partial-deflate": source_root / "partial-deflate", - "shape": source_root / "shape", - "ztail": source_root / "ztail", - } - for directory in directories.values(): - directory.mkdir(parents=True) - - write_mixed(directories["lz4"] / "compressed.bin", 8 * 1024 * 1024, "lz4") - write_mixed( - directories["large"] / "large.bin", 256 * 1024 * 1024, "large" - ) - write_mixed( - directories["levels"] / "level.dat", 8 * 1024 * 1024, "levels" - ) - write_mixed( - directories["lzma-large"] / "large.bin", - 100 * 1024 * 1024 + 1, - "lzma-large", - ) - (directories["microlzma"] / "one-byte.bin").write_bytes(b"G") - write_repeated( - directories["microlzma"] / "block-4k.bin", BLOCK_SIZE, "microlzma-4k" - ) - write_repeated( - directories["microlzma"] / "boundary-16k.bin", - 4 * BLOCK_SIZE, - "microlzma-16k", - ) - write_repeated( - directories["partial"] / "a.dat", 1024 * 1024, "partial-stream" - ) - source_a = directories["partial"] / "a.dat" - source_b = directories["partial"] / "b.dat" - with source_a.open("rb") as source, source_b.open("wb") as target: - target.write(source.read(700000)) - write_mixed( - directories["partial"] / "control.bin", - 32768, - "partial-control", - ) - write_repeated( - directories["partial-deflate"] / "a.dat", - 1024 * 1024, - "partial-stream", - ) - with (directories["partial-deflate"] / "a.dat").open("rb") as source, ( - directories["partial-deflate"] / "b.dat" - ).open("wb") as target: - target.write(source.read(100000)) - write_mixed( - directories["partial-deflate"] / "control.bin", - 32768, - "partial-control", - ) - write_interlaced(directories["shape"] / "shape.dat", 1024 * 1024) - write_repeated(directories["ztail"] / "inline.dat", 131071, "ztail-inline") - write_mixed( - directories["ztail"] / "exact-pcluster.dat", BLOCK_SIZE, "ztail-exact" - ) - write_repeated( - directories["ztail"] / "one-byte-tail.dat", - BLOCK_SIZE + 1, - "ztail-one-byte", - ) - write_repeated( - directories["ztail"] / "max-tail.dat", - BLOCK_SIZE * 2 - 1, - "ztail-max", - ) - (directories["ztail"] / "zero-tail.dat").write_bytes(b"") - - -def source_inventory(source_root: Path) -> list[dict[str, Any]]: - inventory = [] - for path in sorted(item for item in source_root.rglob("*") if item.is_file()): - inventory.append( - { - "path": relative(path, source_root.parent), - "size": path.stat().st_size, - "sha256": sha256(path), - } - ) - return inventory - - -def mkfs_image( - *, - output_root: Path, - image_name: str, - source: Path, - options: list[str], - command_log: list[dict[str, Any]], -) -> Path: - image = output_root / "images" / image_name - command = [ - "mkfs.erofs", - "--workers=1", - "--sort=path", - "-T0", - "--all-time", - f"-U{FIXED_UUID}", - "--all-root", - *options, - str(image), - str(source), - ] - log = output_root / "logs" / f"mkfs-{image.stem}.log" - run(command, log=log) - command_log.append( - { - "image": relative(image, output_root), - "source": relative(source, output_root), - "options": options, - "log": relative(log, output_root), - } - ) - parsed = ErofsImage.load(image) - parsed.validate_superblock() - return image - - -def inode_for(image: ErofsImage, path: str) -> Inode: - _, entry = image.resolve_root_entry(path) - return image.inode(entry.nid) - - -def map_header(image: ErofsImage, inode: Inode) -> dict[str, int]: - if inode.layout not in (LAYOUT_COMPRESSED_FULL, LAYOUT_COMPRESSED_COMPACT): - raise FixtureError(f"inode layout {inode.layout} is not compressed") - offset = align(inode.offset + inode.inode_size + inode.xattr_size, 8) - if offset > len(image.data) - 8: - raise FixtureError("compressed map header exceeds image") - raw0, advise, algorithm, clusterbits = struct.unpack_from( - "> 4, - "clusterbits_raw": clusterbits, - "lcluster_bits": image.block_bits + (clusterbits & 0x07), - "idata_size": raw0 >> 16, - } - - -def full_index_offset(image: ErofsImage, inode: Inode) -> int: - header = map_header(image, inode) - return align(header["offset"] + 8, 8) + 8 - - -def full_record(image: ErofsImage, inode: Inode, lcn: int) -> dict[str, int]: - if inode.layout != LAYOUT_COMPRESSED_FULL: - raise FixtureError("full record requested from a non-full inode") - offset = full_index_offset(image, inode) + lcn * 8 - if offset > len(image.data) - 8: - raise FixtureError("full index record exceeds image") - advise, clusterofs, word = struct.unpack_from("> 16, - } - - -def full_index_summary(image: ErofsImage, inode: Inode) -> dict[str, Any]: - header = map_header(image, inode) - count = (inode.size + (1 << header["lcluster_bits"]) - 1) >> header[ - "lcluster_bits" - ] - types: Counter[int] = Counter() - heads = [] - partial_heads = [] - for lcn in range(count): - record = full_record(image, inode, lcn) - types[record["type"]] += 1 - if record["type"] in ( - Z_EROFS_LCLUSTER_TYPE_PLAIN, - Z_EROFS_LCLUSTER_TYPE_HEAD1, - Z_EROFS_LCLUSTER_TYPE_HEAD2, - ): - if len(heads) < 16: - heads.append( - { - "lcn": lcn, - "offset": record["offset"], - "type": record["type"], - "pblk": record["pblk"], - "clusterofs": record["clusterofs"], - } - ) - if record["partial_ref"]: - partial_heads.append( - { - "lcn": lcn, - "offset": record["offset"], - "pblk": record["pblk"], - } - ) - return { - "record_count": count, - "type_counts": {str(key): value for key, value in sorted(types.items())}, - "first_heads": heads, - "partial_heads": partial_heads, - } - - -EXTENT_RE = re.compile( - r"^\s*\d+:\s*(\d+)\.\.\s*(\d+)\s*\|\s*(\d+)\s*:" - r"\s*(\d+)\.\.\s*(\d+)\s*\|\s*(\d+)\s*$" -) - - -def dump_extents(image: Path, path: str, log: Path | None = None) -> list[dict[str, int]]: - result = run( - ["dump.erofs", f"--path={path}", "-e", str(image)], - log=log, - ) - extents = [] - for line in result.stdout.splitlines(): - match = EXTENT_RE.match(line) - if match is None: - continue - logical_start, logical_end, logical_length, physical_start, physical_end, physical_length = ( - int(value) for value in match.groups() - ) - if logical_end - logical_start != logical_length: - raise FixtureError("dump.erofs reported an inconsistent logical extent") - if physical_end - physical_start != physical_length: - raise FixtureError("dump.erofs reported an inconsistent physical extent") - extents.append( - { - "logical_start": logical_start, - "logical_end": logical_end, - "logical_length": logical_length, - "physical_start": physical_start, - "physical_end": physical_end, - "physical_length": physical_length, - } - ) - if not extents: - raise FixtureError(f"dump.erofs reported no extents for {image}:{path}") - return extents - - -def extent_summary(extents: list[dict[str, int]]) -> dict[str, Any]: - transitions = [] - for index in range(1, len(extents)): - previous = extents[index - 1] - current = extents[index] - previous_plain = previous["logical_length"] == previous["physical_length"] - current_plain = current["logical_length"] == current["physical_length"] - if previous_plain != current_plain: - transitions.append( - { - "logical_offset": current["logical_start"], - "previous_plain": previous_plain, - "current_plain": current_plain, - } - ) - return { - "count": len(extents), - "first": extents[:8], - "last": extents[-1], - "max_logical_length": max(item["logical_length"] for item in extents), - "max_physical_length": max(item["physical_length"] for item in extents), - "plain_count": sum( - item["physical_length"] != 0 - and item["logical_length"] == item["physical_length"] - for item in extents - ), - "compressed_count": sum( - item["physical_length"] != 0 - and item["logical_length"] > item["physical_length"] - for item in extents - ), - "hole_or_fragment_count": sum( - item["physical_length"] == 0 for item in extents - ), - "transitions": transitions[:8], - } - - -def inspect_path( - output_root: Path, - image_path: Path, - path: str, - *, - write_log: bool = True, -) -> dict[str, Any]: - image = ErofsImage.load(image_path) - image.validate_superblock() - inode = inode_for(image, path) - details: dict[str, Any] = { - "path": path, - "nid": inode.nid, - "inode_offset": inode.offset, - "inode_size": inode.inode_size, - "xattr_size": inode.xattr_size, - "layout": inode.layout, - "size": inode.size, - } - if inode.layout in (LAYOUT_COMPRESSED_FULL, LAYOUT_COMPRESSED_COMPACT): - details["map_header"] = map_header(image, inode) - log = None - if write_log: - safe_path = path.removeprefix("/").replace("/", "-") - log = output_root / "logs" / f"dump-{image_path.stem}-{safe_path}.log" - details["dump_log"] = relative(log, output_root) - details["extents"] = extent_summary(dump_extents(image_path, path, log)) - if inode.layout == LAYOUT_COMPRESSED_FULL: - details["full_index"] = full_index_summary(image, inode) - return details - - -def assert_compressed( - details: dict[str, Any], - *, - algorithm: str, - layout: int | None = None, -) -> None: - if details["layout"] not in (LAYOUT_COMPRESSED_FULL, LAYOUT_COMPRESSED_COMPACT): - raise FixtureError(f"{details['path']} was not compressed") - if layout is not None and details["layout"] != layout: - raise FixtureError( - f"{details['path']} layout {details['layout']} != expected {layout}" - ) - actual = details["map_header"]["head1_algorithm"] - if actual != ALGORITHMS[algorithm]: - raise FixtureError( - f"{details['path']} algorithm {actual} != {ALGORITHMS[algorithm]}" - ) - - -def patch_partial_reference( - base: Path, - output: Path, - *, - algorithm: str, -) -> dict[str, Any]: - image = ErofsImage.load(base) - inode_a = inode_for(image, "/a.dat") - inode_b = inode_for(image, "/b.dat") - if inode_a.layout != LAYOUT_COMPRESSED_FULL or inode_b.layout != LAYOUT_COMPRESSED_FULL: - raise FixtureError("partial-reference transform requires full indexes") - header_a = map_header(image, inode_a) - header_b = map_header(image, inode_b) - expected_algorithm = ALGORITHMS[algorithm] - if ( - header_a["head1_algorithm"] != expected_algorithm - or header_b["head1_algorithm"] != expected_algorithm - ): - raise FixtureError("partial-reference source algorithm mismatch") - record_a = full_record(image, inode_a, 0) - record_b = full_record(image, inode_b, 0) - record_a_next = full_record(image, inode_a, 1) - record_b_next = full_record(image, inode_b, 1) - if record_a["type"] != Z_EROFS_LCLUSTER_TYPE_HEAD1: - raise FixtureError("a.dat first record is not HEAD1") - if record_b["type"] != Z_EROFS_LCLUSTER_TYPE_HEAD1: - raise FixtureError("b.dat first record is not HEAD1") - if record_a["pblk"] == record_b["pblk"]: - raise FixtureError("mkfs unexpectedly reused the partial pcluster") - if record_b["advise"] & Z_EROFS_LI_PARTIAL_REF: - raise FixtureError("b.dat was already marked partial") - if inode_b.size >= inode_a.size: - raise FixtureError("partial file is not shorter than the complete stream") - if ( - record_a_next["type"] != Z_EROFS_LCLUSTER_TYPE_NONHEAD - or record_b_next["type"] != Z_EROFS_LCLUSTER_TYPE_NONHEAD - or not record_a_next["delta0"] & Z_EROFS_LI_D0_CBLKCNT - or not record_b_next["delta0"] & Z_EROFS_LI_D0_CBLKCNT - ): - raise FixtureError("partial source lacks a first-pcluster block count") - - struct.pack_into( - " dict[str, Any]: - image = ErofsImage.load(base) - inode = inode_for(image, "/shape.dat") - if inode.layout != LAYOUT_COMPRESSED_FULL: - raise FixtureError("HEAD2 transform requires full indexes") - header = map_header(image, inode) - if header["head1_algorithm"] != ALGORITHMS["lz4"]: - raise FixtureError("HEAD2 base is not LZ4") - if not header["advise"] & Z_EROFS_ADVISE_BIG_PCLUSTER_1: - raise FixtureError("HEAD2 base lacks HEAD1 big-pcluster advise") - - count = (inode.size + (1 << header["lcluster_bits"]) - 1) >> header[ - "lcluster_bits" - ] - target_lcn = None - target = None - for lcn in range(count): - record = full_record(image, inode, lcn) - if record["type"] == Z_EROFS_LCLUSTER_TYPE_HEAD1: - target_lcn = lcn - target = record - break - if target_lcn is None or target is None: - raise FixtureError("HEAD2 base has no HEAD1 record") - - old_incompat = image.feature_incompat - old_advise = header["advise"] - old_algorithm = header["algorithm_raw"] - new_advise = old_advise | Z_EROFS_ADVISE_BIG_PCLUSTER_2 - new_algorithm = (old_algorithm & 0x0F) | ((old_algorithm & 0x0F) << 4) - new_record_advise = ( - target["advise"] & ~Z_EROFS_LCLUSTER_TYPE_MASK - ) | Z_EROFS_LCLUSTER_TYPE_HEAD2 - image.put_u32(SUPER + 80, old_incompat | FEATURE_INCOMPAT_COMPR_HEAD2) - struct.pack_into(" dict[str, Any]: - source = ErofsImage.load(image_path) - inode = inode_for(source, path) - header = map_header(source, inode) - if header["head1_algorithm"] != ALGORITHMS[algorithm]: - raise FixtureError("corruption target algorithm mismatch") - extents = dump_extents(image_path, path) - target = next( - ( - extent - for extent in extents - if extent["physical_length"] > 0 - and extent["logical_length"] > extent["physical_length"] - ), - None, - ) - if target is None: - raise FixtureError("no compressed extent available for corruption") - start = target["physical_start"] - end = target["physical_end"] - if start < source.checksum_end or end > len(source.data): - raise FixtureError("target compressed extent is outside payload bounds") - payload = source.data[start:end] - nonzero = [index for index, value in enumerate(payload) if value != 0] - if len(nonzero) < 16: - raise FixtureError("target compressed extent has too little encoded data") - patch_start = start + nonzero[0] - patch_length = min(64, end - patch_start) - old = bytes(source.data[patch_start : patch_start + patch_length]) - for index in range(patch_start, patch_start + patch_length): - source.data[index] ^= 0xA5 - new = bytes(source.data[patch_start : patch_start + patch_length]) - if old == new: - raise AssertionError("compressed corruption did not change bytes") - if not source.checksum_valid(): - raise FixtureError("payload corruption invalidated the superblock checksum") - source.save(output) - reopened = ErofsImage.load(output) - reopened.validate_superblock() - return { - "path": path, - "algorithm": algorithm, - "algorithm_id": ALGORITHMS[algorithm], - "nid": inode.nid, - "map_header_offset": header["offset"], - "extent_logical_start": target["logical_start"], - "extent_logical_length": target["logical_length"], - "pcluster_start": start, - "pcluster_length": target["physical_length"], - "patch_offset": patch_start, - "patch_length": patch_length, - "old_sha256": hashlib.sha256(old).hexdigest(), - "new_sha256": hashlib.sha256(new).hexdigest(), - "superblock_checksum_valid": True, - } - - -def write_checksum_file(root: Path, paths: list[Path], name: str) -> None: - with (root / name).open("w", encoding="ascii") as output: - for path in sorted(paths): - output.write(f"{sha256(path)} {relative(path, root)}\n") - - -def explicit_probe( - output_root: Path, - attempt_image: Path, - utils_source: Path | None, -) -> dict[str, Any]: - attempt = inspect_path(output_root, attempt_image, "/shape.dat") - header = attempt["map_header"] - tokens = [ - "Z_EROFS_ADVISE_EXTENTS", - "z_erofs_extent_recsize", - "struct z_erofs_extent {", - ] - matches: dict[str, list[str]] = {token: [] for token in tokens} - if utils_source is not None: - for base in (utils_source / "include", utils_source / "lib"): - if not base.is_dir(): - continue - for path in sorted(item for item in base.rglob("*") if item.is_file()): - try: - content = path.read_text(encoding="utf-8", errors="ignore") - except OSError: - continue - for token in tokens: - if token in content: - matches[token].append(str(path)) - return { - "status": "SHELVED", - "attempt_image": relative(attempt_image, output_root), - "attempt_layout": attempt["layout"], - "attempt_map_header_offset": header["offset"], - "attempt_h_advise": header["advise"], - "attempt_explicit_bit": bool( - attempt["layout"] == LAYOUT_COMPRESSED_FULL - and header["advise"] & Z_EROFS_ADVISE_COMPACTED_2B - ), - "mapped_payload_generated": False, - "erofs_utils_source": str(utils_source) if utils_source else None, - "source_token_matches": matches, - "structured_transform_attempt": ( - "Refused: converting legacy lcluster indexes into variable-size " - "extent records requires relocating subsequent metadata and payload; " - "the helper cannot validate a mapped result with erofs-utils 1.8.6." - ), - } - - -def build_fixtures(args: argparse.Namespace) -> None: - output_root = args.output.resolve() - if output_root.exists(): - raise FixtureError(f"output already exists: {output_root}") - (output_root / "images").mkdir(parents=True) - (output_root / "logs").mkdir() - (output_root / "sources").mkdir() - (output_root / "base-images").mkdir() - create_sources(output_root / "sources") - - version = run(["mkfs.erofs", "-V"]).stdout.strip() - if "1.8.6" not in version: - raise FixtureError(f"mkfs.erofs 1.8.6 is required, got: {version}") - - commands: list[dict[str, Any]] = [] - images: dict[str, Path] = {} - targets: dict[str, dict[str, Any]] = {} - - def generate( - name: str, - source_dir: str, - options: list[str], - path: str, - algorithm: str, - layout: int | None = None, - ) -> dict[str, Any]: - image = mkfs_image( - output_root=output_root, - image_name=name, - source=output_root / "sources" / source_dir, - options=options, - command_log=commands, - ) - images[name] = image - details = inspect_path(output_root, image, path) - assert_compressed(details, algorithm=algorithm, layout=layout) - targets[f"{name}:{path}"] = details - return details - - compact_4k = generate( - "lz4-compact-4k.erofs", - "lz4", - ["-zlz4", "-C4096"], - "/compressed.bin", - "lz4", - LAYOUT_COMPRESSED_COMPACT, - ) - full_4k = generate( - "lz4-full-4k.erofs", - "lz4", - ["-zlz4", "-C4096", "-Elegacy-compress"], - "/compressed.bin", - "lz4", - LAYOUT_COMPRESSED_FULL, - ) - compact_64k = generate( - "lz4-compact-64k.erofs", - "lz4", - ["-zlz4", "-C65536"], - "/compressed.bin", - "lz4", - LAYOUT_COMPRESSED_COMPACT, - ) - compact_256k = generate( - "lz4-compact-256k.erofs", - "lz4", - ["-zlz4", "-C262144"], - "/compressed.bin", - "lz4", - LAYOUT_COMPRESSED_COMPACT, - ) - generate( - "lz4-large.erofs", - "large", - ["-zlz4", "-C65536"], - "/large.bin", - "lz4", - ) - ztail = generate( - "lz4-ztail.erofs", - "ztail", - ["-zlz4", "-C4096", "-Eztailpacking"], - "/inline.dat", - "lz4", - ) - for path in ( - "/exact-pcluster.dat", - "/one-byte-tail.dat", - "/max-tail.dat", - "/zero-tail.dat", - ): - details = inspect_path(output_root, images["lz4-ztail.erofs"], path) - targets[f"lz4-ztail.erofs:{path}"] = details - - if not compact_4k["map_header"]["advise"] & Z_EROFS_ADVISE_COMPACTED_2B: - raise FixtureError("4K compact image lacks compacted index advise") - if full_4k["map_header"]["advise"] & Z_EROFS_ADVISE_COMPACTED_2B: - raise FixtureError("full-index image advertises compacted indexes") - for details, requested in ((compact_64k, 65536), (compact_256k, 262144)): - header = details["map_header"] - if not header["advise"] & Z_EROFS_ADVISE_BIG_PCLUSTER_1: - raise FixtureError(f"{requested} image lacks big-pcluster advise") - if details["extents"]["max_physical_length"] <= BLOCK_SIZE: - raise FixtureError(f"{requested} image has no multi-block pcluster") - if not ztail["map_header"]["advise"] & Z_EROFS_ADVISE_INLINE_PCLUSTER: - raise FixtureError("ztailpacking target lacks inline-pcluster advise") - if ztail["map_header"]["idata_size"] == 0: - raise FixtureError("ztailpacking target has zero inline encoded size") - - for level in (1, 6, 9): - generate( - f"deflate-level{level}.erofs", - "levels", - [f"-zdeflate,level={level}", "-C65536", "-Elegacy-compress"], - "/level.dat", - "deflate", - LAYOUT_COMPRESSED_FULL, - ) - for level in (1, 15, 22): - generate( - f"zstd-level{level}.erofs", - "levels", - [f"-zzstd,level={level}", "-C65536", "-Elegacy-compress"], - "/level.dat", - "zstd", - LAYOUT_COMPRESSED_FULL, - ) - generate( - "lzma-level6.erofs", - "levels", - ["-zlzma,level=6", "-C65536", "-Elegacy-compress"], - "/level.dat", - "lzma", - LAYOUT_COMPRESSED_FULL, - ) - generate( - "lzma-large.erofs", - "lzma-large", - ["-zlzma,level=6", "-C65536", "-Elegacy-compress"], - "/large.bin", - "lzma", - LAYOUT_COMPRESSED_FULL, - ) - generate( - "microlzma-edge.erofs", - "microlzma", - ["-zlzma,level=6", "-C4096", "-Elegacy-compress"], - "/boundary-16k.bin", - "lzma", - LAYOUT_COMPRESSED_FULL, - ) - for path in ("/one-byte.bin", "/block-4k.bin"): - details = inspect_path(output_root, images["microlzma-edge.erofs"], path) - targets[f"microlzma-edge.erofs:{path}"] = details - - partials: dict[str, Any] = {} - corruptions: dict[str, Any] = {} - for algorithm, compressor in ( - ("deflate", "-zdeflate,level=1"), - ("zstd", "-zzstd,level=1"), - ("lzma", "-zlzma,level=6"), - ): - base_name = f"{algorithm}-partial-base.erofs" - base = mkfs_image( - output_root=output_root, - image_name=f"../base-images/{base_name}", - source=output_root - / "sources" - / ("partial-deflate" if algorithm == "deflate" else "partial"), - options=[compressor, "-C1048576", "-Elegacy-compress"], - command_log=commands, - ) - valid = output_root / "images" / f"{algorithm}-partial-ref.erofs" - partials[algorithm] = patch_partial_reference( - base, valid, algorithm=algorithm - ) - images[valid.name] = valid - for path in ("/a.dat", "/b.dat"): - details = inspect_path(output_root, valid, path) - assert_compressed( - details, - algorithm=algorithm, - layout=LAYOUT_COMPRESSED_FULL, - ) - targets[f"{valid.name}:{path}"] = details - corrupt = output_root / "images" / f"{algorithm}-partial-ref-corrupt.erofs" - corruptions[algorithm] = corrupt_target_extent( - valid, - corrupt, - path="/a.dat", - algorithm=algorithm, - ) - images[corrupt.name] = corrupt - - head2_base = mkfs_image( - output_root=output_root, - image_name="../base-images/head2-base.erofs", - source=output_root / "sources" / "shape", - options=["-zlz4", "-C65536", "-Elegacy-compress"], - command_log=commands, - ) - head2 = output_root / "images" / "head2.erofs" - head2_transform = patch_head2(head2_base, head2) - images[head2.name] = head2 - head2_details = inspect_path(output_root, head2, "/shape.dat") - assert_compressed( - head2_details, - algorithm="lz4", - layout=LAYOUT_COMPRESSED_FULL, - ) - targets[f"{head2.name}:/shape.dat"] = head2_details - head2_corrupt = output_root / "images" / "head2-corrupt.erofs" - corruptions["head2"] = corrupt_target_extent( - head2, - head2_corrupt, - path="/shape.dat", - algorithm="lz4", - ) - images[head2_corrupt.name] = head2_corrupt - - interlaced = generate( - "interlaced.erofs", - "shape", - ["-zlz4", "-C4096", "-Efragments"], - "/shape.dat", - "lz4", - ) - if not interlaced["map_header"]["advise"] & Z_EROFS_ADVISE_INTERLACED_PCLUSTER: - raise FixtureError("interlaced target lacks interlaced advise") - if not interlaced["extents"]["transitions"]: - raise FixtureError("interlaced target has no compressed/plain transition") - - extent_attempt = generate( - "extent-attempt.erofs", - "shape", - [ - "-zlz4", - "-C65536", - "-Elegacy-compress", - "--max-extent-bytes=65536", - ], - "/shape.dat", - "lz4", - LAYOUT_COMPRESSED_FULL, - ) - if extent_attempt["map_header"]["advise"] & Z_EROFS_ADVISE_COMPACTED_2B: - raise FixtureError("extent attempt unexpectedly selected the explicit bit") - - image_paths = sorted(set(images.values())) - source_paths = sorted( - path for path in (output_root / "sources").rglob("*") if path.is_file() - ) - write_checksum_file(output_root, image_paths, "SHA256SUMS") - write_checksum_file(output_root, source_paths, "SOURCE-SHA256SUMS") - - manifest = { - "schema": 1, - "mkfs_version": version, - "fixed_uuid": FIXED_UUID, - "source_inventory": source_inventory(output_root / "sources"), - "commands": commands, - "images": { - relative(path, output_root): { - "size": path.stat().st_size, - "sha256": sha256(path), - } - for path in image_paths - }, - "targets": targets, - "partial_references": partials, - "corruptions": corruptions, - "head2_transform": head2_transform, - "explicit_extent": explicit_probe( - output_root, - images["extent-attempt.erofs"], - args.erofs_utils_source.resolve() - if args.erofs_utils_source is not None - else None, - ), - } - (output_root / "fixture-manifest.json").write_text( - json.dumps(manifest, indent=2, sort_keys=True) + "\n", - encoding="ascii", - ) - verify_output(output_root) - - -def verify_output(output_root: Path) -> None: - output_root = output_root.resolve() - manifest_path = output_root / "fixture-manifest.json" - manifest = json.loads(manifest_path.read_text(encoding="ascii")) - for path_text, expected in manifest["images"].items(): - path = output_root / path_text - if not path.is_file(): - raise FixtureError(f"missing image: {path_text}") - if path.stat().st_size != expected["size"]: - raise FixtureError(f"image size changed: {path_text}") - if sha256(path) != expected["sha256"]: - raise FixtureError(f"image hash changed: {path_text}") - ErofsImage.load(path).validate_superblock() - for expected in manifest["source_inventory"]: - path = output_root / expected["path"] - if path.stat().st_size != expected["size"] or sha256(path) != expected["sha256"]: - raise FixtureError(f"source changed: {expected['path']}") - for key, expected in manifest["targets"].items(): - image_name, path = key.split(":", 1) - actual = inspect_path( - output_root, - output_root / "images" / image_name, - path, - write_log=False, - ) - actual.pop("dump_log", None) - comparable = dict(expected) - comparable.pop("dump_log", None) - if actual != comparable: - raise FixtureError(f"structured target fields changed: {key}") - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - subparsers = parser.add_subparsers(dest="command", required=True) - create = subparsers.add_parser("create") - create.add_argument("--output", type=Path, required=True) - create.add_argument("--erofs-utils-source", type=Path) - verify = subparsers.add_parser("verify") - verify.add_argument("--output", type=Path, required=True) - return parser.parse_args() - - -def main() -> None: - args = parse_args() - if args.command == "create": - build_fixtures(args) - else: - verify_output(args.output) - - -if __name__ == "__main__": - main() diff --git a/tests/g6_multidev_fixtures.py b/tests/g6_multidev_fixtures.py deleted file mode 100644 index cb3ccfa..0000000 --- a/tests/g6_multidev_fixtures.py +++ /dev/null @@ -1,1218 +0,0 @@ -#!/usr/bin/env python3 -"""Generate and verify assertion-driven G6 chunk/multidevice fixtures.""" - -from __future__ import annotations - -import argparse -import hashlib -import json -import math -import os -import shutil -import struct -import subprocess -import tempfile -from dataclasses import dataclass -from pathlib import Path -from typing import Any, Callable - -from erofs_fixture import ErofsImage, SUPER - - -FEATURE_INCOMPAT_CHUNKED_FILE = 0x00000004 -FEATURE_INCOMPAT_DEVICE_TABLE = 0x00000008 -FEATURE_INCOMPAT_48BIT = 0x00000080 -CHUNK_FORMAT_BLOCK_BITS_MASK = 0x001F -CHUNK_FORMAT_INDEXES = 0x0020 -CHUNK_FORMAT_48BIT = 0x0040 -DEVICE_SLOT_SIZE = 128 -DEVICE_SLOT_FIELDS = 64 -BLOCK_SIZE = 4096 - -CHUNK_UUID = "67360006-0093-0094-0095-000000000101" -FRAGMENT_UUID = "67360098-0000-0000-0000-000000000098" -PCLUSTER_UUID = "67360094-0101-0000-0000-000000000094" - -CHUNK_FILES = { - "tc006.bin": 24, - "cross-device.bin": 96, - "indexed.bin": 40, - "cold-slot2.bin": 32, - "unified-address.bin": 32, -} - - -class FixtureError(RuntimeError): - pass - - -@dataclass(frozen=True) -class ChunkEntry: - offset: int - start_block_high: int - device_id: int - start_block_low: int - - @property - def start_block(self) -> int: - return self.start_block_low | (self.start_block_high << 32) - - -@dataclass(frozen=True) -class ChunkLayout: - path: str - nid: int - inode_offset: int - format_offset: int - chunk_format: int - chunk_bits: int - chunk_size: int - entry_size: int - index_base: int - entries: tuple[ChunkEntry, ...] - - -@dataclass(frozen=True) -class DeviceSlot: - blocks: int - uniaddr: int - - -@dataclass -class MultiFixture: - primary: ErofsImage - providers: list[bytearray] - slots: list[DeviceSlot] - - -def align_up(value: int, alignment: int) -> int: - return (value + alignment - 1) & ~(alignment - 1) - - -def sha256_bytes(data: bytes | bytearray) -> str: - return hashlib.sha256(data).hexdigest() - - -def sha256_file(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as stream: - for chunk in iter(lambda: stream.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest() - - -def deterministic_block(path: str, block: int) -> bytes: - seed = f"repo22-g6:{path}:{block}".encode("ascii") - return b"".join( - hashlib.sha256(seed + index.to_bytes(4, "little")).digest() - for index in range(128) - )[:BLOCK_SIZE] - - -def run( - command: list[str], *, stdout: bool = False, cwd: Path | None = None -) -> str: - result = subprocess.run( - command, - check=True, - cwd=cwd, - stdout=subprocess.PIPE if stdout else subprocess.DEVNULL, - stderr=subprocess.STDOUT if stdout else None, - text=True, - ) - return result.stdout if stdout else "" - - -def tool_version(tool: str) -> str: - output = run([tool, "-V"], stdout=True).strip().splitlines() - if not output: - raise FixtureError(f"{tool} returned no version") - return output[0] - - -def write_chunk_sources(root: Path) -> None: - root.mkdir(parents=True) - for name, blocks in CHUNK_FILES.items(): - with (root / name).open("wb") as stream: - for block in range(blocks): - stream.write(deterministic_block(name, block)) - - -def write_fragment_source(root: Path) -> None: - root.mkdir(parents=True) - seed = deterministic_block("fragment.dat", 0) - data = (seed * math.ceil(100000 / len(seed)))[:100000] - (root / "fragment.dat").write_bytes(data) - - -def write_pcluster_source(root: Path) -> None: - root.mkdir(parents=True) - seed = b"".join( - hashlib.sha256(f"repo22-g6:pcluster:{index}".encode("ascii")).digest() - for index in range(188) - )[:6000] - data = (seed * math.ceil(131072 / len(seed)))[:131072] - (root / "external-pcluster.bin").write_bytes(data) - - -def make_image( - mkfs: str, - output: Path, - source: Path, - uuid: str, - options: list[str], -) -> None: - run( - [ - mkfs, - "-T0", - "--all-time", - "--all-root", - "--workers=1", - f"-U{uuid}", - *options, - str(output), - str(source), - ] - ) - - -def chunk_layout(image: ErofsImage, path: str) -> ChunkLayout: - _, entry = image.resolve_root_entry(path) - inode = image.inode(entry.nid) - if inode.layout != 4: - raise FixtureError(f"{path}: expected chunk layout, got {inode.layout}") - chunk_format, reserved = struct.unpack_from( - " image.blocks * image.block_size: - raise FixtureError(f"{path}: chunk index array exceeds declared image") - entries = [] - for index in range(count): - offset = index_base + index * entry_size - if entry_size == 8: - high, device_id, low = struct.unpack_from( - " None: - for path in paths: - layout = chunk_layout(image, path) - if layout.entry_size != 8: - raise FixtureError(f"{path}: blob baseline has no 8-byte indexes") - minimum = 3 if path in ("/cross-device.bin", "/indexed.bin") else 1 - if len(layout.entries) < minimum: - raise FixtureError( - f"{path}: need at least {minimum} chunk indexes" - ) - for entry in layout.entries: - if entry.start_block_high != 0 or entry.device_id != 1: - raise FixtureError(f"{path}: unexpected mkfs chunk index {entry}") - - -def patch_chunk_entry( - image: ErofsImage, - entry: ChunkEntry, - *, - expected: tuple[int, int, int], - replacement: tuple[int, int, int], -) -> None: - current = struct.unpack_from(" None: - for path in paths: - layout = chunk_layout(image, path) - if layout.entry_size != 8: - raise FixtureError(f"{path}: 48-bit conversion requires indexes") - expected = layout.chunk_format - if expected & CHUNK_FORMAT_48BIT: - raise FixtureError(f"{path}: already has 48-bit indexes") - current = image.u16(layout.format_offset) - if current != expected: - raise FixtureError(f"{path}: chunk format changed before patch") - image.put_u16(layout.format_offset, expected | CHUNK_FORMAT_48BIT) - - -def enable_48bit(image: ErofsImage) -> None: - if image.feature_incompat & FEATURE_INCOMPAT_48BIT: - raise FixtureError("image already has 48-bit feature") - root_nid = image.root_nid - image.put_u32( - SUPER + 80, image.feature_incompat | FEATURE_INCOMPAT_48BIT - ) - image.put_u16(SUPER + 14, 0) - image.put_u64(SUPER + 112, root_nid) - - -def decode_slots(image: ErofsImage) -> list[DeviceSlot]: - extra, slot_offset = struct.unpack_from(" len(image.data): - raise FixtureError("device table lies outside provider bytes") - slots = [] - for index in range(extra): - offset = table_offset + index * DEVICE_SLOT_SIZE + DEVICE_SLOT_FIELDS - blocks_low, uniaddr_low, blocks_high, uniaddr_high = struct.unpack_from( - " None: - if not slots: - raise FixtureError("device table needs at least one slot") - if table_offset % DEVICE_SLOT_SIZE != 0: - raise FixtureError("device table is not 128-byte aligned") - required = max( - primary_blocks * image.block_size, - table_offset + len(slots) * DEVICE_SLOT_SIZE, - ) - if len(image.data) > required: - raise FixtureError("primary metadata exceeds requested primary size") - image.data.extend(bytes(required - len(image.data))) - image.put_u32(SUPER + 36, primary_blocks) - image.put_u32( - SUPER + 80, image.feature_incompat | FEATURE_INCOMPAT_DEVICE_TABLE - ) - image.put_u16(SUPER + 86, len(slots)) - image.put_u16(SUPER + 88, table_offset // DEVICE_SLOT_SIZE) - image.data[ - table_offset : table_offset + len(slots) * DEVICE_SLOT_SIZE - ] = bytes(len(slots) * DEVICE_SLOT_SIZE) - for index, slot in enumerate(slots): - if slot.blocks <= 0 or slot.blocks >= 1 << 48: - raise FixtureError(f"slot {index + 1}: invalid blocks {slot.blocks}") - if slot.uniaddr < 0 or slot.uniaddr >= 1 << 48: - raise FixtureError(f"slot {index + 1}: invalid uniaddr {slot.uniaddr}") - offset = table_offset + index * DEVICE_SLOT_SIZE - tag = f"repo22-g6-slot-{index + 1}".encode("ascii") - image.data[offset : offset + len(tag)] = tag - struct.pack_into( - "> 32, - slot.uniaddr >> 32, - ) - - -def finalize_image(image: ErofsImage, path: Path) -> None: - image.update_checksum() - image.validate_superblock() - image.save(path) - - -def build_single_indexed( - baseline: ErofsImage, blob: bytes, paths: list[str] -) -> ErofsImage: - image = baseline.clone() - if len(image.data) != BLOCK_SIZE or len(blob) % BLOCK_SIZE != 0: - raise FixtureError("single indexed folding requires aligned providers") - for path in paths: - layout = chunk_layout(image, path) - for entry in layout.entries: - patch_chunk_entry( - image, - entry, - expected=(0, 1, entry.start_block_low), - replacement=(0, 0, entry.start_block_low + 1), - ) - image.data.extend(blob) - image.put_u32(SUPER + 36, len(image.data) // BLOCK_SIZE) - image.put_u32( - SUPER + 80, image.feature_incompat & ~FEATURE_INCOMPAT_DEVICE_TABLE - ) - image.put_u16(SUPER + 86, 0) - image.put_u16(SUPER + 88, 0) - return image - - -def build_multislot( - baseline: ErofsImage, - blob: bytes, - paths: list[str], - slot_count: int, -) -> MultiFixture: - image = baseline.clone() - providers = [bytearray() for _ in range(slot_count)] - for path in paths: - layout = chunk_layout(image, path) - blocks_per_chunk = layout.chunk_size // BLOCK_SIZE - for index, entry in enumerate(layout.entries): - if entry.start_block_high != 0 or entry.device_id != 1: - raise FixtureError(f"{path}: unexpected source index {entry}") - source_start = entry.start_block * BLOCK_SIZE - source_end = source_start + blocks_per_chunk * BLOCK_SIZE - if source_end > len(blob): - raise FixtureError(f"{path}: source chunk exceeds blob provider") - slot = 1 if path == "/cold-slot2.bin" and slot_count >= 2 else ( - index % slot_count - ) - local_block = len(providers[slot]) // BLOCK_SIZE - providers[slot].extend(blob[source_start:source_end]) - patch_chunk_entry( - image, - entry, - expected=(0, 1, entry.start_block_low), - replacement=(0, slot + 1, local_block), - ) - for index, provider in enumerate(providers): - provider.extend(bytes((index + 1) * BLOCK_SIZE)) - slots = [] - uniaddr = 2 - for provider in providers: - blocks = len(provider) // BLOCK_SIZE - slots.append(DeviceSlot(blocks, uniaddr)) - uniaddr += blocks - write_device_table(image, slots) - return MultiFixture(image, providers, slots) - - -def clone_multifixture(fixture: MultiFixture) -> MultiFixture: - return MultiFixture( - fixture.primary.clone(), - [bytearray(provider) for provider in fixture.providers], - list(fixture.slots), - ) - - -def rewrite_slots( - fixture: MultiFixture, - slots: list[DeviceSlot], - *, - table_offset: int = BLOCK_SIZE, -) -> None: - fixture.slots = slots - write_device_table(fixture.primary, slots, table_offset=table_offset) - - -def build_flatdev(fixture: MultiFixture) -> bytes: - end_block = max( - [fixture.primary.blocks] - + [slot.uniaddr + slot.blocks for slot in fixture.slots] - ) - data = bytearray(end_block * BLOCK_SIZE) - data[: len(fixture.primary.data)] = fixture.primary.data - for slot, provider in zip(fixture.slots, fixture.providers, strict=True): - if len(provider) != slot.blocks * BLOCK_SIZE: - raise FixtureError("provider length does not match slot declaration") - start = slot.uniaddr * BLOCK_SIZE - data[start : start + len(provider)] = provider - return bytes(data) - - -def first_entry(image: ErofsImage, path: str, index: int = 0) -> ChunkEntry: - layout = chunk_layout(image, path) - try: - return layout.entries[index] - except IndexError as error: - raise FixtureError(f"{path}: missing chunk index {index}") from error - - -def patch_unified_entry( - fixture: MultiFixture, - path: str, - *, - index: int = 0, -) -> None: - entry = first_entry(fixture.primary, path, index) - if entry.device_id < 1 or entry.device_id > len(fixture.slots): - raise FixtureError(f"{path}: source index has no external slot") - slot = fixture.slots[entry.device_id - 1] - global_block = slot.uniaddr + entry.start_block - patch_chunk_entry( - fixture.primary, - entry, - expected=(entry.start_block_high, entry.device_id, entry.start_block_low), - replacement=(global_block >> 32, 0, global_block & 0xFFFFFFFF), - ) - - -def patch_table_field( - image: ErofsImage, - slot: int, - field_offset: int, - fmt: str, - value: int, -) -> None: - _, slot_offset = struct.unpack_from(" tuple[MultiFixture, int]: - _, entry = lz4_image.resolve_root_entry("/external-pcluster.bin") - inode = lz4_image.inode(entry.nid) - if inode.layout != 1 or inode.size != 131072: - raise FixtureError("LZ4 source is not a legacy full-index inode") - header = align_up(inode.offset + inode.inode_size + inode.xattr_size, 8) - index_offset = header + 16 - advise, cluster_offset, pblk = struct.unpack_from( - " MultiFixture: - image = positive.primary.clone() - providers = [ - bytearray(positive.providers[0][:BLOCK_SIZE]), - bytearray(positive.providers[0][BLOCK_SIZE:]), - ] - slots = [DeviceSlot(1, 2), DeviceSlot(1, 3)] - write_device_table(image, slots) - current = struct.unpack_from(" dict[str, Any]: - extra, slot_offset = struct.unpack_from(" None: - source_files = sorted( - path.relative_to(source) for path in source.rglob("*") if path.is_file() - ) - extracted_files = sorted( - path.relative_to(extracted) - for path in extracted.rglob("*") - if path.is_file() - ) - if source_files != extracted_files: - raise FixtureError( - f"extracted paths differ: {source_files} != {extracted_files}" - ) - for relative in source_files: - if sha256_file(source / relative) != sha256_file(extracted / relative): - raise FixtureError(f"extracted checksum differs: {relative}") - - -def fsck_extract( - fsck: str, - primary: Path, - devices: list[Path], - source: Path, - scratch_parent: Path, -) -> None: - with tempfile.TemporaryDirectory(prefix="repo22-g6-fsck-", dir=scratch_parent) as tmp: - extracted = Path(tmp) / "extracted" - command = [fsck] - command.extend(f"--device={device}" for device in devices) - command.extend([f"--extract={extracted}", str(primary)]) - run(command) - compare_trees(source, extracted) - - -def write_provider(path: Path, data: bytes | bytearray) -> None: - if len(data) == 0 or len(data) % BLOCK_SIZE != 0: - raise FixtureError(f"{path.name}: provider is not block aligned") - path.write_bytes(data) - - -def save_multifixture( - output: Path, - name: str, - fixture: MultiFixture, - paths: list[str], - manifest: dict[str, Any], -) -> tuple[Path, list[Path]]: - primary_path = output / "images" / f"{name}-primary.erofs" - finalize_image(fixture.primary, primary_path) - provider_paths = [] - for index, provider in enumerate(fixture.providers, 1): - provider_path = output / "images" / f"{name}-slot{index}.blob" - write_provider(provider_path, provider) - provider_paths.append(provider_path) - manifest["fixtures"][name] = { - "primary": str(primary_path.relative_to(output)), - "providers": [str(path.relative_to(output)) for path in provider_paths], - "image": image_description(fixture.primary, paths), - } - return primary_path, provider_paths - - -def save_image_fixture( - output: Path, - name: str, - image: ErofsImage, - paths: list[str], - manifest: dict[str, Any], -) -> Path: - path = output / "images" / f"{name}.erofs" - finalize_image(image, path) - manifest["fixtures"][name] = { - "primary": str(path.relative_to(output)), - "providers": [], - "image": image_description(image, paths), - } - return path - - -def add_mutation( - manifest: dict[str, Any], - artifact: Path, - output: Path, - label: str, - offset: int, - fmt: str, - value: int, -) -> None: - manifest["mutations"].append( - { - "artifact": str(artifact.relative_to(output)), - "label": label, - "offset": offset, - "format": fmt, - "value": value, - } - ) - - -def save_negative( - output: Path, - name: str, - image: ErofsImage, - manifest: dict[str, Any], -) -> Path: - path = output / "images" / f"{name}.erofs" - finalize_image(image, path) - manifest["negative_images"][name] = str(path.relative_to(output)) - return path - - -def generate(args: argparse.Namespace) -> None: - output: Path = args.output.resolve() - if output.exists(): - raise FixtureError(f"output already exists: {output}") - output.mkdir(parents=True) - (output / "images").mkdir() - chunk_source = output / "sources" / "chunk" - fragment_source = output / "sources" / "fragment" - pcluster_source = output / "sources" / "pcluster" - write_chunk_sources(chunk_source) - write_fragment_source(fragment_source) - write_pcluster_source(pcluster_source) - - mkfs_version = tool_version(args.mkfs) - fsck_version = tool_version(args.fsck) - if "1.8.6" not in mkfs_version or "1.8.6" not in fsck_version: - raise FixtureError( - f"G6 requires erofs-utils 1.8.6, got {mkfs_version!r}, {fsck_version!r}" - ) - - base_primary_path = output / "images" / "mkfs-blob-primary.erofs" - base_blob_path = output / "images" / "mkfs-blob-slot1.blob" - base_blob_path.write_bytes(b"") - make_image( - args.mkfs, - base_primary_path, - chunk_source, - CHUNK_UUID, - ["--chunksize=4096", f"--blobdev={base_blob_path}"], - ) - baseline = ErofsImage.load(base_primary_path) - baseline.validate_superblock() - paths = [f"/{name}" for name in CHUNK_FILES] - assert_chunk_baseline(baseline, paths) - base_blob = base_blob_path.read_bytes() - if len(baseline.data) != BLOCK_SIZE or len(base_blob) % BLOCK_SIZE != 0: - raise FixtureError("mkfs blob baseline has unexpected provider sizes") - - manifest: dict[str, Any] = { - "schema": 1, - "generator": "tests/g6_multidev_fixtures.py", - "erofs_utils": {"mkfs": mkfs_version, "fsck": fsck_version}, - "fixtures": {}, - "negative_images": {}, - "mutations": [], - "artifacts": {}, - "host_qualification": { - "flatdev": ( - "kernel-only: erofs-utils 1.8.6 requires --device for " - "nonzero chunk device IDs" - ), - "external_pcluster": ( - "kernel-only after relocation: erofs-utils 1.8.6 does not " - "apply the device-ID-0 unified mapping used by the kernel" - ), - }, - } - manifest["fixtures"]["mkfs-blob"] = { - "primary": str(base_primary_path.relative_to(output)), - "providers": [str(base_blob_path.relative_to(output))], - "image": image_description(baseline, paths), - } - - single = build_single_indexed(baseline, base_blob, paths) - single_path = save_image_fixture( - output, "single-indexed", single, paths, manifest - ) - - multi2 = build_multislot(baseline, base_blob, paths, 2) - multi2_primary, multi2_providers = save_multifixture( - output, "multi2", multi2, paths, manifest - ) - flatdev_path = output / "images" / "multi2-flatdev.erofs" - write_provider(flatdev_path, build_flatdev(multi2)) - manifest["fixtures"]["multi2-flatdev"] = { - "primary": str(flatdev_path.relative_to(output)), - "providers": [], - "image": image_description(ErofsImage.load(flatdev_path), paths), - } - - multi3 = build_multislot(baseline, base_blob, paths, 3) - multi3_primary, multi3_providers = save_multifixture( - output, "multi3", multi3, paths, manifest - ) - - slot_zero = clone_multifixture(multi2) - zero_slots = [DeviceSlot(slot_zero.slots[0].blocks, 0), slot_zero.slots[1]] - rewrite_slots(slot_zero, zero_slots) - slot_zero_primary, slot_zero_providers = save_multifixture( - output, "multi2-uniaddr0", slot_zero, paths, manifest - ) - - table_zero = clone_multifixture(multi2) - table_bytes = bytes( - table_zero.primary.data[ - BLOCK_SIZE : BLOCK_SIZE + 2 * DEVICE_SLOT_SIZE - ] - ) - table_zero.primary.data[: 2 * DEVICE_SLOT_SIZE] = table_bytes - table_zero.primary.put_u16(SUPER + 88, 0) - table_zero_primary, table_zero_providers = save_multifixture( - output, "multi2-devt0", table_zero, paths, manifest - ) - - unified = clone_multifixture(multi2) - patch_unified_entry(unified, "/unified-address.bin") - unified_primary, unified_providers = save_multifixture( - output, "multi2-unified", unified, paths, manifest - ) - unified_flatdev_path = output / "images" / "multi2-unified-flatdev.erofs" - write_provider(unified_flatdev_path, build_flatdev(unified)) - manifest["fixtures"]["multi2-unified-flatdev"] = { - "primary": str(unified_flatdev_path.relative_to(output)), - "providers": [], - "image": image_description( - ErofsImage.load(unified_flatdev_path), paths - ), - } - - unified48 = clone_multifixture(multi2) - enable_48bit(unified48.primary) - set_chunk_48bit(unified48.primary, paths) - high_start = (1 << 32) + 2 - high_slots = [] - cursor = high_start - for slot in unified48.slots: - high_slots.append(DeviceSlot(slot.blocks, cursor)) - cursor += slot.blocks - rewrite_slots(unified48, high_slots) - patch_unified_entry(unified48, "/unified-address.bin") - unified48_primary, unified48_providers = save_multifixture( - output, "multi2-unified48", unified48, paths, manifest - ) - - unified48_oob = clone_multifixture(unified48) - unified48_oob_entry = first_entry( - unified48_oob.primary, "/unified-address.bin" - ) - patch_chunk_entry( - unified48_oob.primary, - unified48_oob_entry, - expected=( - unified48_oob_entry.start_block_high, - 0, - unified48_oob_entry.start_block_low, - ), - replacement=(0xFFFF, 0, 0xFFFFFFFE), - ) - unified48_oob_primary, unified48_oob_providers = save_multifixture( - output, "bad-unified48-out-of-range", unified48_oob, paths, manifest - ) - add_mutation( - manifest, - unified48_oob_primary, - output, - "device-ID-0 chunk address is the largest non-NULL 48-bit block", - unified48_oob_entry.offset, - " tuple[int, str, int, str]: - value = image.blocks * BLOCK_SIZE // DEVICE_SLOT_SIZE - image.put_u16(SUPER + 88, value) - return SUPER + 88, " tuple[int, str, int, str]: - patch_table_field(image, 1, 0, " tuple[int, str, int, str]: - patch_table_field(image, 1, 4, " tuple[int, str, int, str]: - value = multi2.slots[0].uniaddr - patch_table_field(image, 2, 4, " tuple[int, str, int, str]: - patch_table_field(image, 1, 0, " tuple[int, str, int, str]: - enable_48bit(image) - patch_table_field(image, 1, 0, " Any: - size = struct.calcsize(fmt) - with path.open("rb") as stream: - stream.seek(offset) - data = stream.read(size) - if len(data) != size: - raise FixtureError(f"{path}: field at {offset} is truncated") - values = struct.unpack(fmt, data) - return values[0] if len(values) == 1 else list(values) - - -def verify_image_description(path: Path, expected: dict[str, Any]) -> None: - image = ErofsImage.load(path) - paths = list(expected["chunks"]) - actual = image_description(image, paths) - if actual != expected: - raise FixtureError(f"{path}: parsed image fields differ from manifest") - - -def verify_output(output: Path) -> None: - manifest_path = output / "manifest.json" - manifest = json.loads(manifest_path.read_text(encoding="ascii")) - if manifest.get("schema") != 1: - raise FixtureError("unsupported G6 manifest schema") - for relative, expected in manifest["artifacts"].items(): - path = output / relative - if not path.is_file(): - raise FixtureError(f"missing artifact: {relative}") - if path.stat().st_size != expected["bytes"]: - raise FixtureError(f"size differs: {relative}") - if sha256_file(path) != expected["sha256"]: - raise FixtureError(f"SHA256 differs: {relative}") - for fixture in manifest["fixtures"].values(): - image = fixture.get("image") - if image is not None: - verify_image_description(output / fixture["primary"], image) - for mutation in manifest["mutations"]: - actual = unpack_assertion( - output / mutation["artifact"], - mutation["offset"], - mutation["format"], - ) - if actual != mutation["value"]: - raise FixtureError( - f"{mutation['label']}: expected {mutation['value']}, got {actual}" - ) - run(["sha256sum", "-c", "SHA256SUMS"], stdout=True, cwd=output) - print( - f"verified={output} artifacts={len(manifest['artifacts'])} " - f"field_assertions={len(manifest['mutations'])}" - ) - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - subparsers = parser.add_subparsers(dest="command", required=True) - - generate_parser = subparsers.add_parser("generate") - generate_parser.add_argument("--output", required=True, type=Path) - generate_parser.add_argument("--mkfs", default=shutil.which("mkfs.erofs")) - generate_parser.add_argument("--fsck", default=shutil.which("fsck.erofs")) - generate_parser.set_defaults(function=generate) - - verify_parser = subparsers.add_parser("verify") - verify_parser.add_argument("output", type=Path) - verify_parser.set_defaults(function=lambda args: verify_output(args.output.resolve())) - return parser.parse_args() - - -def main() -> None: - args = parse_args() - if args.command == "generate" and (args.mkfs is None or args.fsck is None): - raise FixtureError("mkfs.erofs and fsck.erofs are required") - args.function(args) - - -if __name__ == "__main__": - os.environ.setdefault("PYTHONDONTWRITEBYTECODE", "1") - main() diff --git a/tests/g7_fixtures.py b/tests/g7_fixtures.py deleted file mode 100644 index 24a2f8e..0000000 --- a/tests/g7_fixtures.py +++ /dev/null @@ -1,495 +0,0 @@ -#!/usr/bin/env python3 -"""Generate and verify deterministic repo22 G7 stress fixtures.""" - -from __future__ import annotations - -import argparse -import hashlib -import json -import os -from pathlib import Path -import subprocess -from typing import Any - - -BLOCK_SIZE = 4096 -MIB = 1024 * 1024 -SPARSE_SIZE = 4 * 1024 * MIB + BLOCK_SIZE + 1 -DEEP_LEVELS = 128 -MANY_SMALL_COUNT = 12000 -LARGE_DIRECTORY_COUNT = 12000 -CONCURRENT_FILE_COUNT = 16 -CONCURRENT_FILE_SIZE = 2 * MIB -PRESSURE_FILE_SIZE = 96 * MIB -SEQUENTIAL_FILE_SIZE = 256 * MIB -RANDOM_FILE_SIZE = 64 * MIB -FIXED_UUIDS = { - "boundaries.erofs": "00000000-0000-0000-0000-000000000071", - "workloads.erofs": "00000000-0000-0000-0000-000000000072", -} -SPARSE_MARKERS = ( - (0, b"repo22-g7-start"), - (BLOCK_SIZE - 8, b"g7-block-edge"), - (2**31 - 8, b"g7-2gib-edge"), - (2**32 - 8, b"g7-4gib-edge"), - (SPARSE_SIZE - 16, b"repo22-g7-end!!"), -) -SPARSE_RANGES = ( - ("start", 0, 64), - ("block-edge", BLOCK_SIZE - 32, 64), - ("one-gib-hole", 1024 * MIB, BLOCK_SIZE), - ("two-gib-edge", 2**31 - 32, 64), - ("four-gib-edge", 2**32 - 32, 64), - ("eof-edge", SPARSE_SIZE - 64, 64), -) - - -class FixtureError(RuntimeError): - pass - - -def sha256(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as source: - while chunk := source.read(MIB): - digest.update(chunk) - return digest.hexdigest() - - -def run(command: list[str], log: Path | None = None) -> str: - result = subprocess.run( - command, - check=False, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - ) - if log is not None: - log.write_text( - "$ " + " ".join(command) + "\n" + result.stdout, - encoding="utf-8", - ) - if result.returncode != 0: - raise FixtureError( - f"command failed ({result.returncode}): {' '.join(command)}\n" - f"{result.stdout}" - ) - return result.stdout - - -def repeated_bytes(label: str, size: int) -> bytes: - token = (label + "\n").encode("ascii") - return (token * ((size + len(token) - 1) // len(token)))[:size] - - -def write_pattern( - path: Path, - size: int, - seed: str, - random_period: int, -) -> None: - remaining = size - block_index = 0 - with path.open("wb") as output: - while remaining: - amount = min(BLOCK_SIZE, remaining) - if block_index % random_period == 0: - block = hashlib.shake_256( - f"repo22-g7:{seed}:{block_index}".encode("ascii") - ).digest(amount) - else: - block = repeated_bytes( - f"repo22-g7:{seed}:{block_index % 97:02d}", amount - ) - output.write(block) - remaining -= amount - block_index += 1 - - -def create_sparse(path: Path) -> None: - with path.open("wb") as output: - output.truncate(SPARSE_SIZE) - for offset, marker in SPARSE_MARKERS: - output.seek(offset) - output.write(marker) - - -def create_boundaries(root: Path) -> dict[str, Any]: - root.mkdir(parents=True) - (root / "empty.bin").write_bytes(b"") - - maximum = root / "maximum" - maximum.mkdir() - create_sparse(maximum / "sparse-boundary.bin") - - deep = root / "deep" - deep.mkdir() - current = deep - components = [] - for level in range(DEEP_LEVELS): - component = f"d{level:03d}" - components.append(component) - current /= component - current.mkdir() - deep_payload = current / "payload.bin" - deep_payload.write_bytes(repeated_bytes("repo22-g7-deep", BLOCK_SIZE)) - deep_path = str(Path("deep", *components, "payload.bin")) - - long_directory = root / "longname" - long_directory.mkdir() - long_name = "n" * 255 - (long_directory / long_name).write_bytes( - repeated_bytes("repo22-g7-long-name", BLOCK_SIZE) - ) - - many_small = root / "many-small" - many_small.mkdir() - for index in range(MANY_SMALL_COUNT): - shard = many_small / f"shard-{index // 1000:02d}" - shard.mkdir(exist_ok=True) - (shard / f"file-{index:05d}.bin").write_bytes( - repeated_bytes(f"repo22-g7-small:{index:05d}", 1024) - ) - - large_directory = root / "large-dir" - large_directory.mkdir() - for index in range(LARGE_DIRECTORY_COUNT): - (large_directory / f"entry-{index:05d}.txt").write_bytes( - f"repo22-g7-large-dir:{index:05d}\n".encode("ascii") - ) - - return { - "deep_path": deep_path, - "long_name": long_name, - } - - -def create_workloads(root: Path) -> None: - root.mkdir(parents=True) - concurrent = root / "concurrent" - concurrent.mkdir() - for index in range(CONCURRENT_FILE_COUNT): - write_pattern( - concurrent / f"reader-{index:02d}.bin", - CONCURRENT_FILE_SIZE, - f"concurrent-{index:02d}", - 8, - ) - - pressure = root / "pressure" - pressure.mkdir() - write_pattern( - pressure / "pressure.bin", - PRESSURE_FILE_SIZE, - "pressure", - 8, - ) - - sequential = root / "sequential" - sequential.mkdir() - write_pattern( - sequential / "sequential.bin", - SEQUENTIAL_FILE_SIZE, - "sequential", - 4, - ) - - random_directory = root / "random" - random_directory.mkdir() - write_pattern( - random_directory / "random.bin", - RANDOM_FILE_SIZE, - "random", - 1, - ) - - -def normalize_modes(root: Path) -> None: - for path in sorted(root.rglob("*")): - os.chmod(path, 0o755 if path.is_dir() else 0o644) - os.chmod(root, 0o755) - - -def inventory(source_root: Path, output_root: Path) -> list[dict[str, Any]]: - records = [] - for path in sorted(item for item in source_root.rglob("*") if item.is_file()): - records.append( - { - "sha256": sha256(path), - "size": path.stat().st_size, - "path": str(path.relative_to(output_root)), - } - ) - return records - - -def inventory_text(records: list[dict[str, Any]]) -> str: - lines = ["sha256\tsize\tpath"] - lines.extend( - f"{record['sha256']}\t{record['size']}\t{record['path']}" - for record in records - ) - return "\n".join(lines) + "\n" - - -def checksum_text(records: list[dict[str, Any]]) -> str: - return "".join( - f"{record['sha256']} {record['path']}\n" for record in records - ) - - -def create_images(output_root: Path) -> list[dict[str, Any]]: - images = output_root / "images" - logs = output_root / "logs" - images.mkdir() - logs.mkdir() - records = [] - for image_name, source_name in ( - ("boundaries.erofs", "boundaries"), - ("workloads.erofs", "workloads"), - ): - image = images / image_name - command = [ - "mkfs.erofs", - "--workers=1", - "--sort=path", - "--all-root", - "-T0", - "--all-time", - f"-U{FIXED_UUIDS[image_name]}", - "-z", - "lz4", - str(image), - str(output_root / "sources" / source_name), - ] - run(command, logs / f"mkfs-{source_name}.log") - run( - ["fsck.erofs", "-d0", str(image)], - logs / f"fsck-{source_name}.log", - ) - records.append( - { - "sha256": sha256(image), - "size": image.stat().st_size, - "path": str(image.relative_to(output_root)), - "command": command, - } - ) - return records - - -def write_metadata( - output_root: Path, - source_records: list[dict[str, Any]], - image_records: list[dict[str, Any]], - boundary_metadata: dict[str, Any], -) -> None: - source_inventory = inventory_text(source_records) - image_checksums = checksum_text(image_records) - (output_root / "SOURCE-INVENTORY.tsv").write_text( - source_inventory, encoding="utf-8" - ) - (output_root / "SOURCE-SHA256SUMS").write_text( - checksum_text(source_records), encoding="utf-8" - ) - (output_root / "SHA256SUMS").write_text( - image_checksums, encoding="utf-8" - ) - (output_root / "DEEP-PATH").write_text( - boundary_metadata["deep_path"] + "\n", encoding="ascii" - ) - (output_root / "LONG-NAME").write_text( - boundary_metadata["long_name"] + "\n", encoding="ascii" - ) - (output_root / "SPARSE-RANGES.tsv").write_text( - "".join( - f"{label}\t{offset}\t{length}\n" - for label, offset, length in SPARSE_RANGES - ), - encoding="ascii", - ) - manifest = { - "format": 1, - "block_size": BLOCK_SIZE, - "deep_levels": DEEP_LEVELS, - "deep_path": boundary_metadata["deep_path"], - "long_name_bytes": len(boundary_metadata["long_name"].encode("ascii")), - "many_small_count": MANY_SMALL_COUNT, - "large_directory_count": LARGE_DIRECTORY_COUNT, - "concurrent_file_count": CONCURRENT_FILE_COUNT, - "concurrent_file_size": CONCURRENT_FILE_SIZE, - "pressure_file_size": PRESSURE_FILE_SIZE, - "sequential_file_size": SEQUENTIAL_FILE_SIZE, - "random_file_size": RANDOM_FILE_SIZE, - "sparse_size": SPARSE_SIZE, - "sparse_markers": [ - {"offset": offset, "hex": marker.hex()} - for offset, marker in SPARSE_MARKERS - ], - "sparse_ranges": [ - {"label": label, "offset": offset, "length": length} - for label, offset, length in SPARSE_RANGES - ], - "source_count": len(source_records), - "source_inventory_sha256": hashlib.sha256( - source_inventory.encode("utf-8") - ).hexdigest(), - "image_count": len(image_records), - "images": image_records, - } - (output_root / "fixture-manifest.json").write_text( - json.dumps(manifest, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - - -def create(output_root: Path) -> None: - if output_root.exists(): - raise FixtureError(f"output already exists: {output_root}") - sources = output_root / "sources" - sources.mkdir(parents=True) - boundary_metadata = create_boundaries(sources / "boundaries") - create_workloads(sources / "workloads") - normalize_modes(sources) - source_records = inventory(sources, output_root) - image_records = create_images(output_root) - write_metadata( - output_root, - source_records, - image_records, - boundary_metadata, - ) - verify(output_root) - - -def require(condition: bool, message: str) -> None: - if not condition: - raise FixtureError(message) - - -def verify(output_root: Path) -> None: - manifest_path = output_root / "fixture-manifest.json" - require(manifest_path.is_file(), "missing fixture-manifest.json") - manifest = json.loads(manifest_path.read_text(encoding="utf-8")) - sources = output_root / "sources" - source_records = inventory(sources, output_root) - source_text = inventory_text(source_records) - require( - source_text == (output_root / "SOURCE-INVENTORY.tsv").read_text( - encoding="utf-8" - ), - "source inventory mismatch", - ) - require( - checksum_text(source_records) - == (output_root / "SOURCE-SHA256SUMS").read_text(encoding="utf-8"), - "source checksum list mismatch", - ) - require(len(source_records) == manifest["source_count"], "source count mismatch") - require( - hashlib.sha256(source_text.encode("utf-8")).hexdigest() - == manifest["source_inventory_sha256"], - "source inventory hash mismatch", - ) - - sparse = sources / "boundaries" / "maximum" / "sparse-boundary.bin" - require(sparse.stat().st_size == SPARSE_SIZE, "sparse file size mismatch") - require( - sparse.stat().st_blocks * 512 < MIB, - "sparse source unexpectedly consumes at least 1 MiB", - ) - with sparse.open("rb") as source: - for offset, marker in SPARSE_MARKERS: - source.seek(offset) - require(source.read(len(marker)) == marker, f"marker mismatch at {offset}") - - deep_path = (output_root / "DEEP-PATH").read_text(encoding="ascii").strip() - require(deep_path == manifest["deep_path"], "deep path metadata mismatch") - require((sources / "boundaries" / deep_path).is_file(), "deep payload missing") - require( - len(Path(deep_path).parts) - 2 == DEEP_LEVELS, - "deep directory level mismatch", - ) - long_name = (output_root / "LONG-NAME").read_text(encoding="ascii").strip() - require(len(long_name.encode("ascii")) == 255, "long name is not 255 bytes") - require( - (sources / "boundaries" / "longname" / long_name).is_file(), - "long-name source missing", - ) - require( - sum(1 for path in (sources / "boundaries" / "many-small").rglob("*") if path.is_file()) - == MANY_SMALL_COUNT, - "many-small count mismatch", - ) - require( - sum(1 for path in (sources / "boundaries" / "large-dir").iterdir() if path.is_file()) - == LARGE_DIRECTORY_COUNT, - "large-directory count mismatch", - ) - require( - sum(1 for path in (sources / "workloads" / "concurrent").iterdir() if path.is_file()) - == CONCURRENT_FILE_COUNT, - "concurrent source count mismatch", - ) - - image_records = [] - logs = output_root / "logs" - for image in sorted((output_root / "images").glob("*.erofs")): - run(["fsck.erofs", "-d0", str(image)], logs / f"verify-{image.stem}.log") - image_records.append( - { - "sha256": sha256(image), - "size": image.stat().st_size, - "path": str(image.relative_to(output_root)), - } - ) - require(len(image_records) == manifest["image_count"], "image count mismatch") - expected_images = [ - {key: record[key] for key in ("sha256", "size", "path")} - for record in manifest["images"] - ] - require(image_records == expected_images, "image inventory mismatch") - require( - checksum_text(image_records) - == (output_root / "SHA256SUMS").read_text(encoding="utf-8"), - "image checksum list mismatch", - ) - sparse_dump = run( - [ - "dump.erofs", - "--path=/maximum/sparse-boundary.bin", - str(output_root / "images" / "boundaries.erofs"), - ] - ) - require(f"Size: {SPARSE_SIZE} " in sparse_dump, "image sparse size mismatch") - print( - "verified " - f"sources={len(source_records)} images={len(image_records)} " - f"inventory_sha256={manifest['source_inventory_sha256']}" - ) - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser() - subparsers = parser.add_subparsers(dest="command", required=True) - for command in ("create", "verify"): - subparser = subparsers.add_parser(command) - subparser.add_argument("--output", type=Path, required=True) - return parser.parse_args() - - -def main() -> int: - args = parse_args() - try: - if args.command == "create": - create(args.output.resolve()) - else: - verify(args.output.resolve()) - except FixtureError as error: - print(f"error: {error}") - return 1 - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tests/g7_probe.c b/tests/g7_probe.c deleted file mode 100644 index 522a733..0000000 --- a/tests/g7_probe.c +++ /dev/null @@ -1,356 +0,0 @@ -#define _POSIX_C_SOURCE 200809L - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define PRESSURE_CHUNK (8U * 1024U * 1024U) - -static void -usage(void) -{ - fprintf(stderr, - "usage:\n" - " g7_probe empty FILE\n" - " g7_probe eof FILE OFFSET\n" - " g7_probe range SOURCE TARGET OFFSET LENGTH\n" - " g7_probe random SOURCE TARGET SEED OPERATIONS BLOCK_SIZE\n" - " g7_probe pressure GO_FILE STOP_FILE REQUEST_MIB\n"); -} - -static int -parse_u64(const char *value, uint64_t *result) -{ - char *end; - unsigned long long parsed; - - errno = 0; - parsed = strtoull(value, &end, 0); - if (errno != 0 || *value == '\0' || *end != '\0') - return (-1); - *result = parsed; - return (0); -} - -static int -read_exact_at(int descriptor, unsigned char *buffer, size_t length, - off_t offset) -{ - size_t total; - ssize_t amount; - - total = 0; - while (total < length) { - amount = pread(descriptor, buffer + total, length - total, - offset + (off_t)total); - if (amount < 0 && errno == EINTR) - continue; - if (amount <= 0) - return (-1); - total += (size_t)amount; - } - return (0); -} - -static uint64_t -digest_bytes(uint64_t digest, const unsigned char *buffer, size_t length) -{ - size_t index; - - for (index = 0; index < length; index++) { - digest ^= buffer[index]; - digest *= UINT64_C(1099511628211); - } - return (digest); -} - -static int -empty_probe(const char *path) -{ - struct stat status; - unsigned char byte; - ssize_t amount; - int descriptor; - - descriptor = open(path, O_RDONLY); - if (descriptor < 0) - return (1); - if (fstat(descriptor, &status) != 0 || status.st_size != 0) { - close(descriptor); - return (1); - } - amount = read(descriptor, &byte, sizeof(byte)); - if (amount != 0 || lseek(descriptor, 0, SEEK_SET) != 0 || - lseek(descriptor, 0, SEEK_END) != 0) { - close(descriptor); - return (1); - } - close(descriptor); - printf("size=0 read_bytes=0 seek_set=0 seek_end=0 exit=0\n"); - return (0); -} - -static int -eof_probe(const char *path, const char *offset_value) -{ - unsigned char byte; - uint64_t offset; - ssize_t amount; - int descriptor; - - if (parse_u64(offset_value, &offset) != 0 || offset > INT64_MAX) - return (1); - descriptor = open(path, O_RDONLY); - if (descriptor < 0) - return (1); - amount = pread(descriptor, &byte, sizeof(byte), (off_t)offset); - close(descriptor); - if (amount != 0) - return (1); - printf("offset=%" PRIu64 " read_bytes=0 errno=0 exit=0\n", offset); - return (0); -} - -static int -range_probe(const char *source_path, const char *target_path, - const char *offset_value, const char *length_value) -{ - unsigned char *source_buffer, *target_buffer; - uint64_t digest, length, offset; - int source, target, result; - - if (parse_u64(offset_value, &offset) != 0 || - parse_u64(length_value, &length) != 0 || length == 0 || - length > SIZE_MAX || offset > INT64_MAX) - return (1); - source_buffer = malloc((size_t)length); - target_buffer = malloc((size_t)length); - if (source_buffer == NULL || target_buffer == NULL) { - free(source_buffer); - free(target_buffer); - return (1); - } - source = open(source_path, O_RDONLY); - target = open(target_path, O_RDONLY); - result = 1; - if (source >= 0 && target >= 0 && - read_exact_at(source, source_buffer, (size_t)length, - (off_t)offset) == 0 && - read_exact_at(target, target_buffer, (size_t)length, - (off_t)offset) == 0 && - memcmp(source_buffer, target_buffer, (size_t)length) == 0) { - digest = digest_bytes(UINT64_C(1469598103934665603), - source_buffer, (size_t)length); - printf("offset=%" PRIu64 " length=%" PRIu64 - " mismatches=0 digest=%016" PRIx64 " exit=0\n", - offset, length, digest); - result = 0; - } - if (source >= 0) - close(source); - if (target >= 0) - close(target); - free(source_buffer); - free(target_buffer); - return (result); -} - -static uint64_t -xorshift64star(uint64_t *state) -{ - uint64_t value; - - value = *state; - value ^= value >> 12; - value ^= value << 25; - value ^= value >> 27; - *state = value; - return (value * UINT64_C(2685821657736338717)); -} - -static double -elapsed_seconds(const struct timespec *start, const struct timespec *end) -{ - return ((double)(end->tv_sec - start->tv_sec) + - (double)(end->tv_nsec - start->tv_nsec) / 1000000000.0); -} - -static int -random_probe(int argc, char **argv) -{ - struct stat source_status, target_status; - struct timespec start, end; - unsigned char *source_buffer, *target_buffer; - uint64_t block_size, digest, index, input_seed, operations, offset, seed; - uint64_t slots; - double elapsed; - int source, target, result; - - if (argc != 7 || parse_u64(argv[4], &seed) != 0 || seed == 0 || - parse_u64(argv[5], &operations) != 0 || operations == 0 || - parse_u64(argv[6], &block_size) != 0 || block_size == 0 || - block_size > SIZE_MAX) - return (1); - input_seed = seed; - source = open(argv[2], O_RDONLY); - target = open(argv[3], O_RDONLY); - if (source < 0 || target < 0 || fstat(source, &source_status) != 0 || - fstat(target, &target_status) != 0 || source_status.st_size <= 0 || - source_status.st_size != target_status.st_size || - (uint64_t)source_status.st_size < block_size) { - if (source >= 0) - close(source); - if (target >= 0) - close(target); - return (1); - } - source_buffer = malloc((size_t)block_size); - target_buffer = malloc((size_t)block_size); - if (source_buffer == NULL || target_buffer == NULL) { - close(source); - close(target); - free(source_buffer); - free(target_buffer); - return (1); - } - slots = (uint64_t)source_status.st_size / block_size; - digest = UINT64_C(1469598103934665603); - result = 1; - clock_gettime(CLOCK_MONOTONIC, &start); - for (index = 0; index < operations; index++) { - offset = (xorshift64star(&seed) % slots) * block_size; - if (read_exact_at(source, source_buffer, (size_t)block_size, - (off_t)offset) != 0 || - read_exact_at(target, target_buffer, (size_t)block_size, - (off_t)offset) != 0 || - memcmp(source_buffer, target_buffer, (size_t)block_size) != 0) - goto out; - digest = digest_bytes(digest, target_buffer, (size_t)block_size); - } - clock_gettime(CLOCK_MONOTONIC, &end); - elapsed = elapsed_seconds(&start, &end); - printf("seed=0x%016" PRIx64 " operations=%" PRIu64 - " block_size=%" PRIu64 - " bytes=%" PRIu64 " mismatches=0 digest=%016" PRIx64 - " elapsed_seconds=%.6f iops=%.2f mean_us=%.2f exit=0\n", - input_seed, operations, block_size, operations * block_size, digest, elapsed, - (double)operations / elapsed, elapsed * 1000000.0 / operations); - result = 0; -out: - close(source); - close(target); - free(source_buffer); - free(target_buffer); - return (result); -} - -static int -wait_for_path(const char *path, unsigned int attempts) -{ - struct timespec delay; - unsigned int attempt; - - delay.tv_sec = 0; - delay.tv_nsec = 100000000; - for (attempt = 0; attempt < attempts; attempt++) { - if (access(path, F_OK) == 0) - return (0); - nanosleep(&delay, NULL); - } - return (-1); -} - -static int -pressure_probe(const char *go_path, const char *stop_path, - const char *request_value) -{ - struct rusage usage; - unsigned char *recovery; - unsigned char **chunks; - uint64_t allocated, index, page_size, request_mib, requested; - int failure_errno, recovery_ok; - - if (parse_u64(request_value, &request_mib) != 0 || request_mib == 0 || - request_mib > 4096) - return (1); - requested = request_mib * 1024 * 1024; - chunks = calloc((size_t)(requested / PRESSURE_CHUNK + 1), sizeof(*chunks)); - if (chunks == NULL) - return (1); - printf("state=waiting pid=%ld requested_bytes=%" PRIu64 "\n", - (long)getpid(), requested); - fflush(stdout); - if (wait_for_path(go_path, 600) != 0) { - free(chunks); - return (1); - } - page_size = (uint64_t)sysconf(_SC_PAGESIZE); - allocated = 0; - failure_errno = 0; - for (index = 0; allocated < requested; index++) { - size_t offset; - - errno = 0; - chunks[index] = malloc(PRESSURE_CHUNK); - if (chunks[index] == NULL) { - failure_errno = errno; - break; - } - for (offset = 0; offset < PRESSURE_CHUNK; offset += page_size) - chunks[index][offset] = (unsigned char)(index + offset); - allocated += PRESSURE_CHUNK; - } - getrusage(RUSAGE_SELF, &usage); - printf("state=holding pid=%ld allocated_bytes=%" PRIu64 - " allocation_failure=%s failure_errno=%d maxrss=%ld\n", - (long)getpid(), allocated, - failure_errno == ENOMEM ? "ENOMEM" : "none", failure_errno, - usage.ru_maxrss); - fflush(stdout); - if (wait_for_path(stop_path, 3000) != 0) { - for (index = 0; index < allocated / PRESSURE_CHUNK; index++) - free(chunks[index]); - free(chunks); - return (1); - } - for (index = 0; index < allocated / PRESSURE_CHUNK; index++) - free(chunks[index]); - free(chunks); - recovery = malloc(1024 * 1024); - recovery_ok = recovery != NULL; - if (recovery != NULL) { - recovery[0] = 1; - free(recovery); - } - printf("state=released pid=%ld released_bytes=%" PRIu64 - " recovery=%s exit=%d\n", - (long)getpid(), allocated, recovery_ok ? "ok" : "failed", - failure_errno == ENOMEM && recovery_ok ? 0 : 1); - return (failure_errno == ENOMEM && recovery_ok ? 0 : 1); -} - -int -main(int argc, char **argv) -{ - if (argc == 3 && strcmp(argv[1], "empty") == 0) - return (empty_probe(argv[2])); - if (argc == 4 && strcmp(argv[1], "eof") == 0) - return (eof_probe(argv[2], argv[3])); - if (argc == 6 && strcmp(argv[1], "range") == 0) - return (range_probe(argv[2], argv[3], argv[4], argv[5])); - if (argc == 7 && strcmp(argv[1], "random") == 0) - return (random_probe(argc, argv)); - if (argc == 5 && strcmp(argv[1], "pressure") == 0) - return (pressure_probe(argv[2], argv[3], argv[4])); - usage(); - return (64); -} diff --git a/tests/mmap_fault.c b/tests/mmap_fault.c deleted file mode 100644 index f3ca7ca..0000000 --- a/tests/mmap_fault.c +++ /dev/null @@ -1,200 +0,0 @@ -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -static void -pread_all(int fd, unsigned char *buf, size_t len) -{ - size_t done; - ssize_t nr; - - for (done = 0; done < len; done += nr) { - nr = pread(fd, buf + done, len - done, done); - if (nr < 0) - err(1, "pread"); - if (nr == 0) - errx(1, "unexpected EOF at %zu", done); - } -} - -static uint64_t -fnv1a64(const unsigned char *buf, size_t len) -{ - uint64_t hash; - - hash = UINT64_C(14695981039346656037); - for (size_t i = 0; i < len; i++) { - hash ^= buf[i]; - hash *= UINT64_C(1099511628211); - } - return (hash); -} - -static uint32_t -xorshift32(uint32_t *state) -{ - uint32_t value; - - value = *state; - value ^= value << 13; - value ^= value >> 17; - value ^= value << 5; - *state = value; - return (value); -} - -static void -verify_random_faults(const unsigned char *map, const unsigned char *expected, - size_t size, size_t pagesize) -{ - size_t *order; - size_t npages, page, span; - uint32_t state; - - npages = (size + pagesize - 1) / pagesize; - order = calloc(npages, sizeof(*order)); - if (order == NULL) - err(1, "calloc page order"); - for (size_t i = 0; i < npages; i++) - order[i] = i; - state = UINT32_C(0x5eed22); - for (size_t i = npages; i > 1; i--) { - size_t other; - - other = xorshift32(&state) % i; - page = order[i - 1]; - order[i - 1] = order[other]; - order[other] = page; - } - for (size_t i = 0; i < npages; i++) { - page = order[i] * pagesize; - span = size - page < pagesize ? size - page : pagesize; - if (memcmp(map + page, expected + page, span) != 0) - errx(1, "random page mismatch at offset %zu", page); - } - free(order); -} - -static void -verify_sigbus(const unsigned char *map, size_t eof_page) -{ - pid_t child; - int status; - - child = fork(); - if (child < 0) - err(1, "fork"); - if (child == 0) { - volatile unsigned char value; - - value = map[eof_page]; - _exit(value == 0 ? 0 : 1); - } - if (waitpid(child, &status, 0) != child) - err(1, "waitpid"); - if (!WIFSIGNALED(status) || WTERMSIG(status) != SIGBUS) - errx(1, "full page beyond EOF did not raise SIGBUS (status=%#x)", - status); -} - -int -main(int argc, char **argv) -{ - unsigned char *expected, *map, *private_map, byte; - struct stat sb; - size_t eof_page, map_len, pagesize, private_offset; - uint64_t hash; - int fd, rwfd, shared_errno; - - if (argc != 2) - errx(2, "usage: mmap_fault file"); - pagesize = (size_t)getpagesize(); - fd = open(argv[1], O_RDONLY); - if (fd < 0) - err(1, "open %s", argv[1]); - if (fstat(fd, &sb) != 0) - err(1, "fstat %s", argv[1]); - if (sb.st_size <= (off_t)(pagesize * 3)) - errx(1, "fixture must exceed three VM pages"); - if ((uintmax_t)sb.st_size > SIZE_MAX - pagesize * 2) - errx(1, "fixture is too large"); - eof_page = ((size_t)sb.st_size + pagesize - 1) / pagesize * pagesize; - map_len = eof_page + pagesize; - expected = malloc((size_t)sb.st_size); - if (expected == NULL) - err(1, "malloc expected data"); - pread_all(fd, expected, (size_t)sb.st_size); - - map = mmap(NULL, map_len, PROT_READ, MAP_PRIVATE, fd, 0); - if (map == MAP_FAILED) - err(1, "mmap read-only private"); - if (madvise(map, map_len, MADV_DONTNEED) != 0) - err(1, "madvise MADV_DONTNEED"); - verify_random_faults(map, expected, (size_t)sb.st_size, pagesize); - if (memcmp(map, expected, (size_t)sb.st_size) != 0) - errx(1, "sequential mmap data differs from pread"); - for (size_t i = (size_t)sb.st_size; i < eof_page; i++) { - if (map[i] != 0) - errx(1, "non-zero byte in partial EOF page at %zu", i); - } - verify_sigbus(map, eof_page); - hash = fnv1a64(map, (size_t)sb.st_size); - - errno = 0; - private_map = mmap(NULL, eof_page, PROT_READ | PROT_WRITE, MAP_SHARED, - fd, 0); - shared_errno = errno; - if (private_map != MAP_FAILED) { - munmap(private_map, eof_page); - errx(1, "writable MAP_SHARED unexpectedly succeeded"); - } - if (shared_errno != EACCES) - errx(1, "writable MAP_SHARED returned %s, expected Permission denied", - strerror(shared_errno)); - - private_map = mmap(NULL, eof_page, PROT_READ | PROT_WRITE, MAP_PRIVATE, - fd, 0); - if (private_map == MAP_FAILED) - err(1, "mmap writable private"); - private_offset = (size_t)sb.st_size / 2; - byte = expected[private_offset]; - private_map[private_offset] ^= UINT8_C(0xff); - if (private_map[private_offset] == byte) - errx(1, "private mapping did not change"); - if (pread(fd, &byte, 1, private_offset) != 1) - err(1, "pread private verification"); - if (byte != expected[private_offset]) - errx(1, "MAP_PRIVATE modified the EROFS file"); - - errno = 0; - rwfd = open(argv[1], O_RDWR); - if (rwfd >= 0) { - close(rwfd); - errx(1, "O_RDWR unexpectedly succeeded"); - } - if (errno != EROFS) - errx(1, "O_RDWR returned %s, expected Read-only file system", - strerror(errno)); - - if (munmap(private_map, eof_page) != 0 || munmap(map, map_len) != 0) - err(1, "munmap"); - free(expected); - if (close(fd) != 0) - err(1, "close"); - printf("PASS size=%jd pages=%zu fnv1a64=%016" PRIx64 - " random-faults=%zu eof-zero=PASS sigbus=PASS private-cow=PASS\n", - (intmax_t)sb.st_size, eof_page / pagesize, hash, - ((size_t)sb.st_size + pagesize - 1) / pagesize); - return (0); -} diff --git a/tests/nfs_fh_tool.c b/tests/nfs_fh_tool.c deleted file mode 100644 index 2f252d0..0000000 --- a/tests/nfs_fh_tool.c +++ /dev/null @@ -1,283 +0,0 @@ -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -struct erofs_test_fid { - uint16_t len; - uint16_t pad; - uint32_t nid_hi; - uint32_t nid_lo; - uint32_t gen; -}; - -_Static_assert(sizeof(struct erofs_test_fid) == 16, - "unexpected EROFS test file handle size"); -_Static_assert(sizeof(struct erofs_test_fid) <= sizeof(struct fid), - "EROFS test file handle does not fit struct fid"); - -static void -usage(void) -{ - fprintf(stderr, - "usage:\n" - " nfs_fh_tool capture path handle\n" - " nfs_fh_tool lcapture path handle\n" - " nfs_fh_tool describe handle\n" - " nfs_fh_tool compare handle1 handle2\n" - " nfs_fh_tool stat handle\n" - " nfs_fh_tool cat handle output\n" - " nfs_fh_tool mutate input output field value\n" - " nfs_fh_tool expect-stat handle errno\n" - " nfs_fh_tool expect-open handle errno\n"); - exit(2); -} - -static void -read_handle(const char *path, fhandle_t *fh) -{ - FILE *fp; - - fp = fopen(path, "rb"); - if (fp == NULL) - err(1, "fopen %s", path); - if (fread(fh, sizeof(*fh), 1, fp) != 1) - err(1, "fread %s", path); - if (fgetc(fp) != EOF) - errx(1, "%s has trailing data", path); - if (fclose(fp) != 0) - err(1, "fclose %s", path); -} - -static void -write_handle(const char *path, const fhandle_t *fh) -{ - FILE *fp; - - fp = fopen(path, "wb"); - if (fp == NULL) - err(1, "fopen %s", path); - if (fwrite(fh, sizeof(*fh), 1, fp) != 1) - err(1, "fwrite %s", path); - if (fclose(fp) != 0) - err(1, "fclose %s", path); -} - -static int -parse_errno(const char *name) -{ - char *end; - long value; - - if (strcmp(name, "EINVAL") == 0) - return (EINVAL); - if (strcmp(name, "ESTALE") == 0) - return (ESTALE); - errno = 0; - value = strtol(name, &end, 0); - if (errno != 0 || *end != '\0' || value <= 0 || value > INT_MAX) - errx(2, "invalid errno: %s", name); - return ((int)value); -} - -static uint64_t -parse_value(const char *text) -{ - char *end; - uintmax_t value; - - errno = 0; - value = strtoumax(text, &end, 0); - if (errno != 0 || *end != '\0' || value > UINT64_MAX) - errx(2, "invalid value: %s", text); - return ((uint64_t)value); -} - -static void -capture(const char *path, const char *output, int nofollow) -{ - fhandle_t fh; - int error; - - bzero(&fh, sizeof(fh)); - error = nofollow ? lgetfh(path, &fh) : getfh(path, &fh); - if (error != 0) - err(1, "%s %s", nofollow ? "lgetfh" : "getfh", path); - write_handle(output, &fh); -} - -static void -describe(const char *path) -{ - struct erofs_test_fid efid; - fhandle_t fh; - uint64_t nid; - - read_handle(path, &fh); - bzero(&efid, sizeof(efid)); - memcpy(&efid, &fh.fh_fid, sizeof(efid)); - nid = ((uint64_t)efid.nid_hi << 32) | efid.nid_lo; - printf("fsid=%08x:%08x len=%u pad=%u nid=%016" PRIx64 - " gen=%u\n", (unsigned int)fh.fh_fsid.val[0], - (unsigned int)fh.fh_fsid.val[1], efid.len, efid.pad, nid, efid.gen); -} - -static void -compare(const char *left, const char *right) -{ - fhandle_t a, b; - - read_handle(left, &a); - read_handle(right, &b); - if (memcmp(&a, &b, sizeof(a)) != 0) - errx(1, "file handles differ"); -} - -static void -stat_handle(const char *path) -{ - fhandle_t fh; - struct stat sb; - - read_handle(path, &fh); - if (fhstat(&fh, &sb) != 0) - err(1, "fhstat %s", path); - printf("mode=%#o ino=%ju gen=%u size=%jd\n", (unsigned int)sb.st_mode, - (uintmax_t)sb.st_ino, (unsigned int)sb.st_gen, - (intmax_t)sb.st_size); -} - -static void -cat_handle(const char *handle, const char *output) -{ - char buf[65536]; - fhandle_t fh; - ssize_t nr, nw; - int fd, outfd; - - read_handle(handle, &fh); - fd = fhopen(&fh, O_RDONLY); - if (fd < 0) - err(1, "fhopen %s", handle); - outfd = open(output, O_WRONLY | O_CREAT | O_TRUNC, 0600); - if (outfd < 0) - err(1, "open %s", output); - while ((nr = read(fd, buf, sizeof(buf))) > 0) { - for (ssize_t done = 0; done < nr; done += nw) { - nw = write(outfd, buf + done, nr - done); - if (nw < 0) - err(1, "write %s", output); - } - } - if (nr < 0) - err(1, "read %s", handle); - if (close(outfd) != 0) - err(1, "close %s", output); - if (close(fd) != 0) - err(1, "close fhopen"); -} - -static void -mutate(const char *input, const char *output, const char *field, - const char *text) -{ - struct erofs_test_fid efid; - fhandle_t fh; - uint64_t value; - - read_handle(input, &fh); - bzero(&efid, sizeof(efid)); - memcpy(&efid, &fh.fh_fid, sizeof(efid)); - value = parse_value(text); - if (strcmp(field, "len") == 0) { - if (value > UINT16_MAX) - errx(2, "len is too large"); - efid.len = value; - } else if (strcmp(field, "pad") == 0) { - if (value > UINT16_MAX) - errx(2, "pad is too large"); - efid.pad = value; - } else if (strcmp(field, "nid_hi") == 0) { - if (value > UINT32_MAX) - errx(2, "nid_hi is too large"); - efid.nid_hi = value; - } else if (strcmp(field, "nid_lo") == 0) { - if (value > UINT32_MAX) - errx(2, "nid_lo is too large"); - efid.nid_lo = value; - } else if (strcmp(field, "gen") == 0) { - if (value > UINT32_MAX) - errx(2, "gen is too large"); - efid.gen = value; - } else if (strcmp(field, "gen_xor") == 0) { - if (value == 0 || value > UINT32_MAX) - errx(2, "gen_xor must be a non-zero uint32_t"); - efid.gen ^= value; - } else { - errx(2, "unknown field: %s", field); - } - memcpy(&fh.fh_fid, &efid, sizeof(efid)); - write_handle(output, &fh); -} - -static void -expect_failure(const char *path, const char *error_name, int use_open) -{ - fhandle_t fh; - struct stat sb; - int expected, fd, result; - - read_handle(path, &fh); - expected = parse_errno(error_name); - errno = 0; - if (use_open) { - fd = fhopen(&fh, O_RDONLY); - result = fd; - if (fd >= 0) - close(fd); - } else { - result = fhstat(&fh, &sb); - } - if (result != -1) - errx(1, "%s unexpectedly succeeded", use_open ? "fhopen" : "fhstat"); - if (errno != expected) - errx(1, "%s returned %s, expected %s", - use_open ? "fhopen" : "fhstat", strerror(errno), - strerror(expected)); -} - -int -main(int argc, char **argv) -{ - if (argc == 4 && strcmp(argv[1], "capture") == 0) - capture(argv[2], argv[3], 0); - else if (argc == 4 && strcmp(argv[1], "lcapture") == 0) - capture(argv[2], argv[3], 1); - else if (argc == 3 && strcmp(argv[1], "describe") == 0) - describe(argv[2]); - else if (argc == 4 && strcmp(argv[1], "compare") == 0) - compare(argv[2], argv[3]); - else if (argc == 3 && strcmp(argv[1], "stat") == 0) - stat_handle(argv[2]); - else if (argc == 4 && strcmp(argv[1], "cat") == 0) - cat_handle(argv[2], argv[3]); - else if (argc == 6 && strcmp(argv[1], "mutate") == 0) - mutate(argv[2], argv[3], argv[4], argv[5]); - else if (argc == 4 && strcmp(argv[1], "expect-stat") == 0) - expect_failure(argv[2], argv[3], 0); - else if (argc == 4 && strcmp(argv[1], "expect-open") == 0) - expect_failure(argv[2], argv[3], 1); - else - usage(); - return (0); -} diff --git a/tests/prepare_directory_fixtures.sh b/tests/prepare_directory_fixtures.sh deleted file mode 100755 index 049d6a3..0000000 --- a/tests/prepare_directory_fixtures.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/sh -set -eu - -script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) -output_dir=${1:?usage: prepare_directory_fixtures.sh OUTPUT-DIRECTORY} - -for tool in mkfs.erofs fsck.erofs dump.erofs python3; do - command -v "$tool" >/dev/null 2>&1 || { - echo "missing tool: $tool" >&2 - exit 1 - } -done -test ! -e "$output_dir" || { - echo "output already exists: $output_dir" >&2 - exit 1 -} - -work_dir=$(mktemp -d "${TMPDIR:-/tmp}/repo22-directory.XXXXXX") -trap 'rm -rf "$work_dir"' EXIT HUP INT TERM -source_dir="$work_dir/source" -mkdir -p "$source_dir/alpha/bravo/charlie" \ - "$source_dir/alpha/sibling" "$source_dir/wide" - -printf 'cold nested lookup payload\n' \ - > "$source_dir/alpha/bravo/charlie/payload.txt" -printf 'repeat lookup payload\n' > "$source_dir/alpha/bravo/repeat.txt" -printf 'sibling marker\n' > "$source_dir/alpha/sibling/marker.txt" -: > "$work_dir/expected-wide.txt" -index=0 -while [ "$index" -lt 320 ]; do - name=$(printf 'entry-%03d-abcdefghijklmnopqrstuvwxyz.txt' "$index") - printf 'wide entry %03d\n' "$index" > "$source_dir/wide/$name" - printf '%s\n' "$name" >> "$work_dir/expected-wide.txt" - index=$((index + 1)) -done -find "$source_dir" -exec touch -h -t 197001010000.00 {} + - -mkfs.erofs --quiet -d0 -x-1 -T0 --all-time --all-root --workers=1 \ - -U 11111111-2222-3333-4444-555555555554 \ - "$work_dir/namei-base.erofs" "$source_dir" -python3 "$script_dir/erofs_fixture.py" make-directory-fixtures \ - --base "$work_dir/namei-base.erofs" --output "$output_dir" -cp -R "$source_dir" "$output_dir/source" -cp "$work_dir/expected-wide.txt" "$output_dir/expected-wide.txt" - -fsck.erofs -d1 "$output_dir/namei-padding-nonzero.erofs" -dump.erofs --cat \ - --path=/wide/entry-079-abcdefghijklmnopqrstuvwxyz.txt \ - "$output_dir/namei-padding-nonzero.erofs" \ - > "$output_dir/padding-file.txt" -cmp "$source_dir/wide/entry-079-abcdefghijklmnopqrstuvwxyz.txt" \ - "$output_dir/padding-file.txt" -cat "$output_dir/fixture-evidence.txt" -cat "$output_dir/SHA256SUMS" diff --git a/tests/prepare_error_fixtures.sh b/tests/prepare_error_fixtures.sh deleted file mode 100755 index aea11eb..0000000 --- a/tests/prepare_error_fixtures.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/bin/sh -set -eu - -script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) -output_dir=${1:?usage: prepare_error_fixtures.sh OUTPUT-DIRECTORY} - -for tool in mkfs.erofs dump.erofs fsck.erofs python3; do - command -v "$tool" >/dev/null 2>&1 || { - echo "missing tool: $tool" >&2 - exit 1 - } -done -test ! -e "$output_dir" || { - echo "output already exists: $output_dir" >&2 - exit 1 -} - -work_dir=$(mktemp -d "${TMPDIR:-/tmp}/repo22-errors.XXXXXX") -trap 'rm -rf "$work_dir"' EXIT HUP INT TERM -source_dir="$work_dir/source" -mkdir -p "$source_dir" - -printf 'directory entry target\n' > "$source_dir/bad-entry.txt" -printf 'inode format target\n' > "$source_dir/inode-target.txt" -printf 'high offset data path\n' > "$source_dir/high-offset.txt" -printf 'control file\n' > "$source_dir/control.txt" -dd if=/dev/zero of="$source_dir/plain.bin" bs=1048576 count=2 status=none -awk 'BEGIN { for (i = 0; i < 131072; i++) - printf "%08d deterministic compressed payload\n", i }' \ - > "$source_dir/compressed.bin" -find "$source_dir" -exec touch -h -t 197001010000.00 {} + - -mkfs.erofs --quiet -d0 -x-1 -T0 --all-time --all-root --workers=1 \ - -U 33333333-4444-5555-6666-777777777771 \ - -E noinline_data,force-inode-extended \ - "$work_dir/plain.erofs" "$source_dir" -mkfs.erofs --quiet -d0 -x-1 -T0 --all-time --all-root --workers=1 \ - -U 33333333-4444-5555-6666-777777777772 \ - -E legacy-compress,force-inode-extended -zlz4 -C65536 \ - "$work_dir/lz4.erofs" "$source_dir" -mkfs.erofs --quiet -d0 -x-1 -T0 --all-time --all-root --workers=1 \ - -U 33333333-4444-5555-6666-777777777773 \ - -E legacy-compress,force-inode-extended -zdeflate,level=1 -C65536 \ - "$work_dir/deflate.erofs" "$source_dir" - -dump.erofs --path=/compressed.bin -e "$work_dir/lz4.erofs" \ - > "$work_dir/compressed.extents" -set -- $(awk '/^[[:space:]]*0:/ { - gsub(/\.\./, "", $7); print $7, $NF; exit -}' "$work_dir/compressed.extents") -test "$#" -eq 2 || { - echo "could not parse first compressed extent" >&2 - exit 1 -} -compressed_offset=$1 -compressed_length=$2 - -python3 "$script_dir/erofs_fixture.py" make-error-fixtures \ - --plain "$work_dir/plain.erofs" \ - --compressed "$work_dir/lz4.erofs" \ - --deflate "$work_dir/deflate.erofs" \ - --compressed-offset "$compressed_offset" \ - --compressed-length "$compressed_length" \ - --output "$output_dir" -cp -R "$source_dir" "$output_dir/source" -cp "$work_dir/compressed.extents" "$output_dir/compressed.extents" - -extract_dir="$work_dir/extract-control" -mkdir "$extract_dir" -fsck.erofs --extract="$extract_dir" "$output_dir/valid-lz4.erofs" -cmp "$source_dir/compressed.bin" "$extract_dir/compressed.bin" -mkdir "$work_dir/extract-deflate" -fsck.erofs --extract="$work_dir/extract-deflate" \ - "$output_dir/valid-deflate-level1.erofs" -cmp "$source_dir/compressed.bin" "$work_dir/extract-deflate/compressed.bin" -mkdir "$work_dir/extract-corrupt" -set +e -fsck.erofs --extract="$work_dir/extract-corrupt" \ - "$output_dir/compressed-stream-corrupt.erofs" \ - > "$output_dir/compressed-corrupt-fsck.txt" 2>&1 -fsck_status=$? -set -e -test "$fsck_status" -ne 0 || { - echo "corrupted compressed stream unexpectedly extracted" >&2 - exit 1 -} -printf 'compressed_corrupt_fsck_status=%s\n' "$fsck_status" \ - >> "$output_dir/fixture-evidence.txt" -cat "$output_dir/fixture-evidence.txt" -cat "$output_dir/SHA256SUMS" diff --git a/tests/prepare_g1_fixtures.sh b/tests/prepare_g1_fixtures.sh deleted file mode 100755 index 658eb39..0000000 --- a/tests/prepare_g1_fixtures.sh +++ /dev/null @@ -1,196 +0,0 @@ -#!/bin/sh -set -eu - -script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) -output=${1:?usage: prepare_g1_fixtures.sh OUTPUT} -source_dir="$output/source" -base_dir="$output/base" -image_dir="$output/images" -expected_dir="$output/expected" - -for tool in mkfs.erofs fsck.erofs dump.erofs python3 sha256sum stat; do - command -v "$tool" >/dev/null 2>&1 || { - echo "missing tool: $tool" >&2 - exit 1 - } -done -test "$(id -u)" -eq 0 || { - echo "root is required to create device-node fixtures" >&2 - exit 1 -} -test ! -e "$output" || { - echo "output already exists: $output" >&2 - exit 1 -} - -mkdir -p \ - "$source_dir/compact/dir" \ - "$source_dir/compact/dotdir" \ - "$source_dir/extended" \ - "$source_dir/flat/very/long/path/to/a/deeply/nested/deterministic/target/directory" \ - "$source_dir/inline" \ - "$base_dir" "$expected_dir" - -SOURCE_DIR="$source_dir" EXPECTED_DIR="$expected_dir" python3 <<'PY' -from pathlib import Path -import os - -source = Path(os.environ["SOURCE_DIR"]) -expected = Path(os.environ["EXPECTED_DIR"]) - - -def write_pattern(path: Path, size: int, seed: int) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - with path.open("wb") as out: - offset = 0 - while offset < size: - count = min(65536, size - offset) - out.write(bytes( - (((offset + index) * 131 + seed) ^ ((offset + index) >> 5)) & 0xff - for index in range(count) - )) - offset += count - - -compact = source / "compact" -(compact / "root.txt").write_text("repo22 G1 root payload\n", encoding="ascii") -write_pattern(compact / "testfile.txt", 65537, 1) -(compact / "small.txt").write_text("compact inode payload\n", encoding="ascii") -(compact / "single.txt").write_text("single-link payload\n", encoding="ascii") -(compact / "hard-a.txt").write_text("hard-link payload\n", encoding="ascii") -(compact / "dotdir" / "child.txt").write_text("dot directory child\n", encoding="ascii") -(compact / "invalid-target.txt").write_text("invalid nid target\n", encoding="ascii") -(compact / "special-target.txt").write_text("special target\n", encoding="ascii") - -extended = source / "extended" -write_pattern(extended / "large-file.bin", 2 * 1024 * 1024 + 731, 2) -(extended / "huge-sparse.dat").write_bytes(b"x") -(extended / "oversize.dat").write_bytes(b"x") - -flat = source / "flat" -(flat / "small.txt").write_text("flat plain small payload\n", encoding="ascii") -write_pattern(flat / "medium.dat", 100 * 1024 + 17, 3) -write_pattern(flat / "large.bin", 10 * 1024 * 1024 + 31, 4) -write_pattern(flat / "data.txt", 256 * 1024 + 19, 5) -write_pattern(flat / "random-test.bin", 1024 * 1024, 6) -write_pattern(flat / "huge-data.bin", 100 * 1024 * 1024 + 257, 7) -(flat / "very" / "long" / "path" / "to" / "a" / "deeply" / "nested" / - "deterministic" / "target" / "directory" / "file.txt").write_text( - "long symlink target payload\n", encoding="ascii" -) - -inline = source / "inline" -(inline / "tiny.txt").write_text("inline data\n", encoding="ascii") -(inline / "empty.txt").write_bytes(b"") -(inline / "inline.txt").write_bytes(b"inline-tail-payload-0123456789\n") -write_pattern(inline / "tailpacked.dat", 5000, 8) -write_pattern(inline / "file-4095.dat", 4095, 9) -write_pattern(inline / "file-4096.dat", 4096, 10) -write_pattern(inline / "file-4097.dat", 4097, 11) -(inline / "target.txt").write_text("short symlink target payload\n", encoding="ascii") - -for source_path, offset, length, output_name in ( - (flat / "medium.dat", 10 * 4096, 5 * 4096, "medium-10-5.bin"), - (flat / "random-test.bin", 0, 10 * 1024, "random-start.bin"), - (flat / "random-test.bin", 500 * 1024, 10 * 1024, "random-mid.bin"), - (flat / "random-test.bin", 1000 * 1024, 24 * 1024, "random-end.bin"), - (flat / "huge-data.bin", 50 * 1024 * 1024, 5 * 1024 * 1024, - "huge-sample.bin"), -): - with source_path.open("rb") as handle: - handle.seek(offset) - data = handle.read(length) - if len(data) != length: - raise RuntimeError(f"short expected range from {source_path}") - (expected / output_name).write_bytes(data) -(expected / "large-hole-zero-64k.bin").write_bytes(bytes(65536)) -(expected / "large-hole-description.txt").write_text( - "path=/huge-sparse.dat size=4294971393 content=all-zero\n", encoding="ascii" -) -PY - -ln "$source_dir/compact/testfile.txt" "$source_dir/compact/dir/testfile.txt" -ln "$source_dir/compact/hard-a.txt" "$source_dir/compact/hard-b.txt" -ln -s special-target.txt "$source_dir/compact/special-link" -mknod "$source_dir/compact/char-large" c 2748 344865 -mknod "$source_dir/compact/block-large" b 2748 344865 -mkfifo "$source_dir/compact/fifo" -ln -s target.txt "$source_dir/inline/link1" -ln -s /nonexistent/repo22-g1-target "$source_dir/inline/brokenlink" -ln -s very/long/path/to/a/deeply/nested/deterministic/target/directory/file.txt \ - "$source_dir/flat/longlink" -find "$source_dir" -exec touch -h -t 197001010000.00 {} + - -build_image() -{ - uuid=$1 - image=$2 - source=$3 - shift 3 - mkfs.erofs -d0 -x-1 -T0 --all-time --all-root --workers=1 \ - -U "$uuid" "$@" "$base_dir/$image" "$source_dir/$source" -} - -build_image 31000000-0000-0000-0000-000000000001 \ - compact.erofs compact -E force-inode-compact -build_image 31000000-0000-0000-0000-000000000002 \ - extended.erofs extended -E force-inode-extended -build_image 31000000-0000-0000-0000-000000000003 \ - flat.erofs flat -E noinline_data,force-inode-compact -build_image 31000000-0000-0000-0000-000000000004 \ - inline.erofs inline -E force-inode-compact - -python3 "$script_dir/g1_fixtures.py" \ - --compact "$base_dir/compact.erofs" \ - --extended "$base_dir/extended.erofs" \ - --flat "$base_dir/flat.erofs" \ - --inline "$base_dir/inline.erofs" \ - --output "$image_dir" - -for image in compact.erofs extended.erofs flat.erofs inline.erofs \ - compact-nlink1.erofs inline-zero.erofs extended-large-hole.erofs; do - fsck.erofs "$image_dir/$image" >/dev/null -done - -printf '%s\n' \ - '48-bit fixtures are field/CRC checked by g1_fixtures.py;' \ - 'production fsck.erofs 1.8.6 does not recognize incompat feature 0x80.' - -dump.erofs --cat --path=/small.txt "$image_dir/compact.erofs" | - cmp - "$source_dir/compact/small.txt" -dump.erofs --cat --path=/large-file.bin "$image_dir/extended.erofs" | - cmp - "$source_dir/extended/large-file.bin" -dump.erofs --cat --path=/medium.dat "$image_dir/flat.erofs" | - cmp - "$source_dir/flat/medium.dat" -dump.erofs --cat --path=/tailpacked.dat "$image_dir/inline.erofs" | - cmp - "$source_dir/inline/tailpacked.dat" - -( - cd "$output" - find source expected -type f -print0 | sort -z | xargs -0 sha256sum -) > "$output/SOURCE-SHA256SUMS" -( - cd "$output" - find source -type l -print | sort | while IFS= read -r path; do - printf '%s -> %s\n' "$path" "$(readlink "$path")" - done - find source -type p -printf '%p type=fifo\n' - for kind in block:b char:c; do - label=${kind%:*} - type=${kind#*:} - find source -type "$type" -print | sort | while IFS= read -r path; do - set -- $(stat -c '%t %T' "$path") - printf '%s type=%s major=%d minor=%d\n' \ - "$path" "$label" "$((0x$1))" "$((0x$2))" - done - done -) > "$output/SOURCE-METADATA" -( - cd "$output" - sha256sum SOURCE-METADATA -) > "$output/SOURCE-METADATA.sha256" - -cat "$image_dir/fixture-evidence.txt" -cat "$output/SOURCE-SHA256SUMS" -cat "$output/SOURCE-METADATA" -cat "$image_dir/IMAGE-SHA256SUMS" diff --git a/tests/prepare_g3_fixtures.sh b/tests/prepare_g3_fixtures.sh deleted file mode 100755 index 34180cd..0000000 --- a/tests/prepare_g3_fixtures.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/sh -set -eu - -script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) -output_dir=${1:?usage: prepare_g3_fixtures.sh OUTPUT-DIRECTORY} - -for tool in mkfs.erofs python3 sha256sum; do - command -v "$tool" >/dev/null 2>&1 || { - echo "missing tool: $tool" >&2 - exit 1 - } -done -test ! -e "$output_dir" || { - echo "output already exists: $output_dir" >&2 - exit 1 -} - -mkdir -p "$output_dir" -python3 "$script_dir/g3_fixtures.py" make-vfs \ - --output "$output_dir/vfs" - -FIXTURE_DIR="$output_dir/metadata-source" \ -ARTIFACT_DIR="$output_dir/metadata" \ - "$script_dir/results/manual/2026-08-08T2337Z-metadata-vfs/prepare-fixtures.sh" - -find "$output_dir/metadata-source/namei/wide" -mindepth 1 -maxdepth 1 \ - -type f -printf '%f\n' | LC_ALL=C sort \ - > "$output_dir/metadata/expected-wide.txt" -test "$(wc -l < "$output_dir/metadata/expected-wide.txt")" -eq 320 - -FIXTURE_DIR="$output_dir/final-source" \ -ARTIFACT_DIR="$output_dir/final" \ - "$script_dir/results/manual/2026-08-09T0124Z-final-review/prepare-fixtures.sh" - -python3 "$script_dir/g3_fixtures.py" make-large-prefix \ - --source "$output_dir/final/large-dir-intmax.erofs" \ - --output "$output_dir/final/large-dir-intmax-sparse-prefix.erofs" \ - --evidence "$output_dir/final/tc153-sparse-evidence.txt" - -(cd "$output_dir/vfs" && sha256sum -c IMAGE-SHA256SUMS) -(cd "$output_dir/vfs/source" && sha256sum -c ../SOURCE-SHA256SUMS) -(cd "$output_dir/metadata" && sha256sum -c SHA256SUMS) -(cd "$output_dir/final" && sha256sum -c SHA256SUMS) -sha256sum "$output_dir/final/large-dir-intmax-sparse-prefix.erofs" \ - > "$output_dir/final/TC153-SPARSE-SHA256" diff --git a/tests/read_probe.c b/tests/read_probe.c deleted file mode 100644 index e6d06c1..0000000 --- a/tests/read_probe.c +++ /dev/null @@ -1,128 +0,0 @@ -#define _POSIX_C_SOURCE 200809L - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -static uint64_t -parse_number(const char *text, const char *what) -{ - char *end; - uintmax_t value; - - errno = 0; - value = strtoumax(text, &end, 0); - if (errno != 0 || *text == '\0' || *end != '\0' || value > INT64_MAX) - errx(2, "invalid %s: %s", what, text); - return ((uint64_t)value); -} - -static void -write_all(int fd, const unsigned char *buffer, size_t length) -{ - size_t done; - ssize_t written; - - for (done = 0; done < length; done += (size_t)written) { - written = write(fd, buffer + done, length - done); - if (written < 0) - err(1, "write output"); - } -} - -static void -pread_range(const char *path, uint64_t raw_offset, uint64_t raw_length, - const char *output) -{ - unsigned char buffer[65536]; - uint64_t done; - ssize_t count; - size_t request; - int fd, outfd; - - if (raw_length > INT64_MAX - raw_offset) - errx(2, "offset plus length overflows off_t"); - fd = open(path, O_RDONLY); - if (fd < 0) - err(1, "open %s", path); - outfd = open(output, O_WRONLY | O_CREAT | O_TRUNC, 0600); - if (outfd < 0) - err(1, "open %s", output); - for (done = 0; done < raw_length; done += (uint64_t)count) { - request = raw_length - done < sizeof(buffer) ? - (size_t)(raw_length - done) : sizeof(buffer); - count = pread(fd, buffer, request, (off_t)(raw_offset + done)); - if (count < 0) - err(1, "pread %s at %ju", path, - (uintmax_t)(raw_offset + done)); - if (count == 0) - errx(1, "unexpected EOF at %ju", (uintmax_t)(raw_offset + done)); - write_all(outfd, buffer, (size_t)count); - } - if (close(outfd) != 0 || close(fd) != 0) - err(1, "close"); - printf("bytes=%ju offset=%ju\n", (uintmax_t)raw_length, - (uintmax_t)raw_offset); -} - -static void -expect_error(const char *path, int expected) -{ - unsigned char buffer[65536]; - ssize_t count; - int fd; - - fd = open(path, O_RDONLY); - if (fd < 0) { - if (errno != expected) - errx(1, "open returned %s, expected errno %d", - strerror(errno), expected); - printf("expected_errno=%d stage=open\n", expected); - return; - } - for (;;) { - errno = 0; - count = read(fd, buffer, sizeof(buffer)); - if (count < 0) - break; - if (count == 0) - errx(1, "read reached EOF without expected errno %d", expected); - } - if (errno != expected) - errx(1, "read returned %s, expected errno %d", - strerror(errno), expected); - if (close(fd) != 0) - err(1, "close"); - printf("expected_errno=%d stage=read\n", expected); -} - -int -main(int argc, char **argv) -{ - uint64_t offset, length, error_number; - - if (argc == 6 && strcmp(argv[1], "pread") == 0) { - offset = parse_number(argv[3], "offset"); - length = parse_number(argv[4], "length"); - pread_range(argv[2], offset, length, argv[5]); - return (0); - } - if (argc == 4 && strcmp(argv[1], "expect-error") == 0) { - error_number = parse_number(argv[3], "errno"); - if (error_number == 0 || error_number > INT_MAX) - errx(2, "errno is out of range"); - expect_error(argv[2], (int)error_number); - return (0); - } - errx(2, "usage: %s pread FILE OFFSET LENGTH OUTPUT | " - "expect-error FILE ERRNO", argv[0]); -} diff --git a/tests/readdir_probe.c b/tests/readdir_probe.c deleted file mode 100644 index 7cb773f..0000000 --- a/tests/readdir_probe.c +++ /dev/null @@ -1,329 +0,0 @@ -#ifndef __FreeBSD__ -#define _DEFAULT_SOURCE -#endif - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -struct observed_entry { - char *name; - off_t cookie; - unsigned char type; -}; - -struct observed_list { - struct observed_entry *entries; - size_t count; - size_t capacity; -}; - -static void -append_entry(struct observed_list *list, const char *name, off_t cookie, - unsigned char type) -{ - struct observed_entry *entries; - - if (list->count == list->capacity) { - list->capacity = list->capacity == 0 ? 64 : list->capacity * 2; - entries = realloc(list->entries, - list->capacity * sizeof(*list->entries)); - if (entries == NULL) - err(1, "realloc entries"); - list->entries = entries; - } - list->entries[list->count].name = strdup(name); - if (list->entries[list->count].name == NULL) - err(1, "strdup"); - list->entries[list->count].cookie = cookie; - list->entries[list->count].type = type; - list->count++; -} - -static uint64_t -entry_hash(const struct observed_list *list) -{ - uint64_t hash; - - hash = UINT64_C(14695981039346656037); - for (size_t index = 0; index < list->count; index++) { - for (const unsigned char *name = - (const unsigned char *)list->entries[index].name; - *name != '\0'; name++) { - hash ^= *name; - hash *= UINT64_C(1099511628211); - } - hash ^= 0; - hash *= UINT64_C(1099511628211); - hash ^= list->entries[index].type; - hash *= UINT64_C(1099511628211); - } - return (hash); -} - -static void -print_type_counts(const struct observed_list *list) -{ - size_t block, character, directory, fifo, link, other, regular, socket, - unknown; - - block = character = directory = fifo = link = other = regular = socket = - unknown = 0; - for (size_t index = 0; index < list->count; index++) { - switch (list->entries[index].type) { - case DT_BLK: - block++; - break; - case DT_CHR: - character++; - break; - case DT_DIR: - directory++; - break; - case DT_FIFO: - fifo++; - break; - case DT_LNK: - link++; - break; - case DT_REG: - regular++; - break; - case DT_SOCK: - socket++; - break; - case DT_UNKNOWN: - unknown++; - break; - default: - other++; - break; - } - } - printf("types=dir:%zu,reg:%zu,lnk:%zu,chr:%zu,blk:%zu,fifo:%zu," - "sock:%zu,unknown:%zu,other:%zu ", directory, regular, link, - character, block, fifo, socket, unknown, other); -} - -static void -free_list(struct observed_list *list) -{ - for (size_t index = 0; index < list->count; index++) - free(list->entries[index].name); - free(list->entries); -} - -static void -check_unique_names(const struct observed_list *list) -{ - for (size_t left = 0; left < list->count; left++) { - for (size_t right = left + 1; right < list->count; right++) { - if (strcmp(list->entries[left].name, - list->entries[right].name) == 0) - errx(1, "duplicate name: %s", - list->entries[left].name); - } - } -} - -static struct observed_list -scan_getdirentries(const char *path, size_t buffer_size) -{ - struct observed_list list = { 0 }; - struct dirent *entry; - char *buffer, *end; - off_t base, previous; - ssize_t bytes; - int fd; - - fd = open(path, O_RDONLY | O_DIRECTORY); - if (fd < 0) - err(1, "open %s", path); - buffer = malloc(buffer_size); - if (buffer == NULL) - err(1, "malloc"); - previous = 0; - for (;;) { - base = -1; - bytes = getdirentries(fd, buffer, buffer_size, &base); - if (bytes < 0) - err(1, "getdirentries %s", path); - if (bytes == 0) - break; - end = buffer + bytes; - for (entry = (struct dirent *)buffer; (char *)entry < end; - entry = (struct dirent *)((char *)entry + entry->d_reclen)) { - if (entry->d_reclen == 0 || - (char *)entry + entry->d_reclen > end) - errx(1, "invalid dirent record"); - if (entry->d_off <= previous) - errx(1, "non-increasing d_off %jd after %jd", - (intmax_t)entry->d_off, (intmax_t)previous); - append_entry(&list, entry->d_name, entry->d_off, - entry->d_type); - previous = entry->d_off; - } - } - free(buffer); - if (close(fd) != 0) - err(1, "close %s", path); - check_unique_names(&list); - return (list); -} - -static void -verify_kernel_cookies(const char *path, size_t buffer_size, - const struct observed_list *list) -{ - struct dirent *entry; - char *buffer; - off_t base; - ssize_t bytes; - int fd; - - buffer = malloc(buffer_size); - if (buffer == NULL) - err(1, "malloc restart buffer"); - for (size_t index = 0; index < list->count; index++) { - fd = open(path, O_RDONLY | O_DIRECTORY); - if (fd < 0) - err(1, "open restart %s", path); - if (lseek(fd, list->entries[index].cookie, SEEK_SET) < 0) - err(1, "lseek cookie %jd", - (intmax_t)list->entries[index].cookie); - base = -1; - bytes = getdirentries(fd, buffer, buffer_size, &base); - if (bytes < 0) - err(1, "getdirentries restart"); - if (index + 1 == list->count) { - if (bytes != 0) - errx(1, "final d_off cookie did not reach EOF"); - } else { - if (bytes <= 0) - errx(1, "restart cookie returned no dirent"); - entry = (struct dirent *)buffer; - if (entry->d_reclen == 0 || entry->d_reclen > (size_t)bytes) - errx(1, "restart cookie returned a truncated dirent"); - if (strcmp(entry->d_name, list->entries[index + 1].name) != 0) - errx(1, "cookie %jd resumed at %s, expected %s", - (intmax_t)list->entries[index].cookie, - entry->d_name, list->entries[index + 1].name); - } - if (close(fd) != 0) - err(1, "close restart"); - } - free(buffer); -} - -static struct observed_list -scan_readdir(const char *path) -{ - struct observed_list list = { 0 }; - struct dirent *entry; - DIR *directory; - long cookie, previous; - - directory = opendir(path); - if (directory == NULL) - err(1, "opendir %s", path); - previous = 0; - while ((entry = readdir(directory)) != NULL) { - cookie = telldir(directory); - if (cookie <= previous) - errx(1, "non-increasing telldir cookie %ld after %ld", - cookie, previous); - append_entry(&list, entry->d_name, (off_t)cookie, - entry->d_type); - previous = cookie; - } - if (closedir(directory) != 0) - err(1, "closedir %s", path); - check_unique_names(&list); - return (list); -} - -static void -verify_seekdir(const char *path, const struct observed_list *list) -{ - struct dirent *entry; - DIR *directory; - long cookie; - - for (size_t index = 0; index < list->count; index++) { - directory = opendir(path); - if (directory == NULL) - err(1, "opendir restart %s", path); - for (size_t position = 0; position <= index; position++) { - entry = readdir(directory); - if (entry == NULL || - strcmp(entry->d_name, list->entries[position].name) != 0) - errx(1, "seekdir setup differs at entry %zu", - position); - } - cookie = telldir(directory); - if ((off_t)cookie != list->entries[index].cookie) - errx(1, "telldir cookie differs at entry %zu", index); - seekdir(directory, cookie); - entry = readdir(directory); - if (index + 1 == list->count) { - if (entry != NULL) - errx(1, "final telldir cookie did not reach EOF"); - } else if (entry == NULL || - strcmp(entry->d_name, list->entries[index + 1].name) != 0) { - errx(1, "seekdir cookie %jd resumed at %s, expected %s", - (intmax_t)list->entries[index].cookie, - entry == NULL ? "EOF" : entry->d_name, - list->entries[index + 1].name); - } - if (closedir(directory) != 0) - err(1, "closedir restart"); - } -} - -int -main(int argc, char **argv) -{ - struct observed_list kernel, libc; - char *end; - unsigned long raw_size; - size_t buffer_size; - - if (argc < 2 || argc > 3) - errx(2, "usage: %s directory [buffer-size]", argv[0]); - buffer_size = 128; - if (argc == 3) { - raw_size = strtoul(argv[2], &end, 0); - if (*argv[2] == '\0' || *end != '\0' || raw_size < 64 || - raw_size > 1024 * 1024) - errx(2, "invalid buffer size: %s", argv[2]); - buffer_size = (size_t)raw_size; - } - kernel = scan_getdirentries(argv[1], buffer_size); - verify_kernel_cookies(argv[1], buffer_size, &kernel); - libc = scan_readdir(argv[1]); - verify_seekdir(argv[1], &libc); - if (kernel.count != libc.count) - errx(1, "getdirentries count %zu differs from readdir count %zu", - kernel.count, libc.count); - for (size_t index = 0; index < kernel.count; index++) { - if (strcmp(kernel.entries[index].name, libc.entries[index].name) != 0) - errx(1, "API order differs at entry %zu", index); - if (kernel.entries[index].type != libc.entries[index].type) - errx(1, "API type differs at entry %zu", index); - } - print_type_counts(&kernel); - printf("entries=%zu d_off_restarts=%zu seekdir_restarts=%zu " - "buffer=%zu fnv1a64=%016" PRIx64 "\n", kernel.count, - kernel.count, libc.count, buffer_size, entry_hash(&kernel)); - free_list(&libc); - free_list(&kernel); - return (0); -} diff --git a/tests/results/manual/2026-08-08T1037Z/manual-test-report.md b/tests/results/manual/2026-08-08T1037Z/manual-test-report.md deleted file mode 100644 index 75d92bf..0000000 --- a/tests/results/manual/2026-08-08T1037Z/manual-test-report.md +++ /dev/null @@ -1,188 +0,0 @@ -# repo22 手工测试执行记录 - -- 执行时间:2026-08-08 10:36-10:38 UTC -- 工作区:`/work` -- 仓库:`/work/repo-community/repo22` -- 执行方式:严格按 Markdown 用例进行前置条件检查;未生成 wrapper 或 CI;未修改代码;未 commit/push -- 总结:TC001、TC002、TC007、TC008、TC009、TC010 均为 **BLOCKED**。没有用例进入 FreeBSD 客体内的正式测试步骤,因此没有伪造 PASS/FAIL。 - -## 环境版本 - -实际命令: - -```sh -date -u '+%Y-%m-%dT%H:%M:%SZ' -uname -a -cat /etc/os-release -``` - -实际输出: - -```text -2026-08-08T10:37:36Z -Linux da8f2da26d77 6.12.74+deb13+1-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.74-2 (2026-03-08) x86_64 GNU/Linux -PRETTY_NAME="Debian GNU/Linux 13 (trixie)" -VERSION_ID="13" -DEBIAN_VERSION_FULL=13.5 -``` - -结论:当前 shell 位于 Linux 容器,不是 FreeBSD;`kldload`、`kldstat`、`mdconfig` 等用例命令不能在此环境代替执行。 - -## FreeBSD VM 可访问性 - -实际命令: - -```sh -timeout 3 bash -c ' /tmp/repo22-ssh-askpass.sh <<'EOF' -#!/bin/sh -printf '%s\n' '<固定测试密码>' -EOF -chmod 700 /tmp/repo22-ssh-askpass.sh -``` - -随后所有 SSH/SCP 命令使用以下认证前缀和选项: - -```sh -DISPLAY=:0 SSH_ASKPASS=/tmp/repo22-ssh-askpass.sh \ -SSH_ASKPASS_REQUIRE=force setsid -w ssh \ - -o StrictHostKeyChecking=no \ - -o UserKnownHostsFile=/dev/null \ - -o PreferredAuthentications=keyboard-interactive,password \ - -o PubkeyAuthentication=no \ - -p 9222 root@127.0.0.1 '' -``` - -## 2. Guest 环境验证 - -实际 guest 命令: - -```sh -echo "== UTC date =="; date -u "+%Y-%m-%dT%H:%M:%SZ" -echo "== identity =="; id -echo "== uname =="; uname -a -echo "== freebsd-version =="; freebsd-version -ku 2>&1 -echo "== hostname =="; hostname -echo "== architecture =="; uname -m; sysctl -n hw.machine_arch 2>&1 -echo "== securelevel =="; sysctl kern.securelevel -echo "== module tools =="; command -v kldload; command -v kldstat; command -v kldunload -echo "== filesystem tools =="; command -v mdconfig; command -v mount; command -v umount; command -v sha256; command -v stat -echo "== existing erofs module/mount/md =="; kldstat | grep -i erofs || true; mount | grep -i erofs || true; mdconfig -l -echo "== temp space =="; df -h /tmp /root -``` - -实际输出: - -```text -Warning: Permanently added '[127.0.0.1]:9222' (ED25519) to the list of known hosts. -== UTC date == -2026-08-08T11:10:13Z -== identity == -uid=0(root) gid=0(wheel) groups=0(wheel),5(operator) -== uname == -FreeBSD freebsd-build 15.0-RELEASE-p8 FreeBSD 15.0-RELEASE-p8 releng/15.0-n281036-53054229dcb3 GENERIC amd64 -== freebsd-version == -15.0-RELEASE-p8 -15.0-RELEASE-p8 -== hostname == -freebsd-build -== architecture == -amd64 -amd64 -== securelevel == -kern.securelevel: -1 -== module tools == -/sbin/kldload -/sbin/kldstat -/sbin/kldunload -== filesystem tools == -/sbin/mdconfig -/sbin/mount -/sbin/umount -/sbin/sha256 -/usr/bin/stat -== existing erofs module/mount/md == -== temp space == -Filesystem Size Used Avail Capacity Mounted on -/dev/vtbd0p2 112G 12G 91G 12% / -/dev/vtbd0p2 112G 12G 91G 12% / -``` - -结论:guest 连接及基础环境 PASS;开始时没有已加载 EROFS 模块、EROFS 挂载或 md 设备。 - -## 3. 等待 ABI 提交 - -轮询条件:HEAD 必须晚于测试开始时的 `db524d69f`,且 -`git status --porcelain -- repo-community/repo22` 行数必须为 0。 - -实际命令: - -```sh -baseline=db524d69f -for attempt in $(seq 1 40); do - now=$(date -u '+%Y-%m-%dT%H:%M:%SZ') - head=$(git rev-parse --short HEAD) - subject=$(git log -1 --pretty=%s) - dirty=$(git status --porcelain -- repo-community/repo22 | wc -l) - printf '%s attempt=%02d HEAD=%s dirty_repo22=%s subject=%s\n' \ - "$now" "$attempt" "$head" "$dirty" "$subject" - if [ "$head" != "$baseline" ] && [ "$dirty" -eq 0 ]; then - echo 'READY: repo22 has a newer commit and its scoped worktree is clean' - git log --date=iso-strict -5 --pretty=format:'%h %ad %an %s' - echo - exit 0 - fi - sleep 15 -done -``` - -实际输出: - -```text -2026-08-08T11:10:33Z attempt=01 HEAD=db524d69f dirty_repo22=6 subject=repo22: record initial manual test blockers -2026-08-08T11:10:49Z attempt=02 HEAD=db524d69f dirty_repo22=6 subject=repo22: record initial manual test blockers -2026-08-08T11:11:04Z attempt=03 HEAD=db524d69f dirty_repo22=6 subject=repo22: record initial manual test blockers -2026-08-08T11:11:20Z attempt=04 HEAD=db524d69f dirty_repo22=6 subject=repo22: record initial manual test blockers -2026-08-08T11:11:35Z attempt=05 HEAD=db524d69f dirty_repo22=19 subject=repo22: record initial manual test blockers -2026-08-08T11:11:50Z attempt=06 HEAD=db524d69f dirty_repo22=19 subject=repo22: record initial manual test blockers -2026-08-08T11:12:05Z attempt=07 HEAD=db524d69f dirty_repo22=19 subject=repo22: record initial manual test blockers -2026-08-08T11:12:21Z attempt=08 HEAD=db524d69f dirty_repo22=19 subject=repo22: record initial manual test blockers -2026-08-08T11:12:36Z attempt=09 HEAD=f2caaae28 dirty_repo22=0 subject=repo22: align core on-disk structures -READY: repo22 has a newer commit and its scoped worktree is clean -f2caaae28 2026-08-08T11:12:36Z Ruicheng Pan repo22: align core on-disk structures -db524d69f 2026-08-08T10:46:37Z Ruicheng Pan repo22: record initial manual test blockers -524dcfac5 2026-08-08T10:43:55Z Ruicheng Pan repo22: harden core I/O and build path -173385f79 2026-08-08T10:09:38Z Ruicheng Pan Add repo22 based on repo21 for comprehensive validation and review -c782c1dc8 2026-06-18T20:01:35Z Ruicheng Pan 补充和完善 repo21_explain/src 中文注释 -``` - -## 4. 宿主构建 repo22 erofs.ko - -实际命令(工作目录 `/work/repo-community/repo22`): - -```sh -date -u '+%Y-%m-%dT%H:%M:%SZ' -git rev-parse HEAD -clang --version | head -1 -FREEBSD_SRC=/work/dev-freebsd-releng ./build.sh -echo "build_exit=$?" -ls -l --full-time build/erofs.ko -sha256sum build/erofs.ko -``` - -实际输出(编译器对 FreeBSD 内核头文件产生了多组重复的 builtin redeclaration -warning;构建未使用 `-Werror`,没有编译或链接 error): - -```text -2026-08-08T11:13:13Z -f2caaae2890a910d0f11547d9ff5f7cf92c062a2 -Debian clang version 19.1.7 (3+b1) -==> Building repo22 erofs.ko -[data.c、dir.c、erofs_vnops.c、inode.c、namei.c、super.c、xattr.c、lz4.c、deflate.c、zstd.c、lzma.c 编译期间重复出现以下 warning:] -warning: incompatible redeclaration of library function 'log' [-Wincompatible-library-redeclaration] -warning: incompatible redeclaration of library function 'strdup' [-Wincompatible-library-redeclaration] -warning: incompatible redeclaration of library function 'strndup' [-Wincompatible-library-redeclaration] -warning: incompatible redeclaration of library function 'free' [-Wincompatible-library-redeclaration] -warning: incompatible redeclaration of library function 'malloc' [-Wincompatible-library-redeclaration] -warning: incompatible redeclaration of library function 'realloc' [-Wincompatible-library-redeclaration] -/work/repo-community/repo22/src/lzma.c:133:19: warning: result of comparison of constant 256 with expression of type 'uint8_t' (aka 'unsigned char') is always true [-Wtautological-constant-out-of-range-compare] -/work/repo-community/repo22/src/lzma.c:135:16: warning: result of comparison of constant 256 with expression of type 'uint8_t' (aka 'unsigned char') is always true [-Wtautological-constant-out-of-range-compare] -==> SUCCESS: /work/repo-community/repo22/build/erofs.ko -build_exit=0 --rw-r--r-- 1 root root 45088 2026-08-08 11:13:16.704187568 +0000 build/erofs.ko -f119bb2c902bf6e221b532b1c8e7668caaa5c744c46b4eac8f9f105cd7494e9f build/erofs.ko -``` - -说明:原始构建输出包含每个源文件的同类完整 warning 展开;本报告逐类保留了实际 -warning 文本和两个非重复的 `lzma.c` warning,并保留了完整的成功、退出码、尺寸和哈希证据。 - -## 5. SCP 模块到 guest - -实际命令(密码由临时 askpass 提供): - -```sh -DISPLAY=:0 SSH_ASKPASS=/tmp/repo22-ssh-askpass.sh \ -SSH_ASKPASS_REQUIRE=force setsid -w scp -O \ - -o StrictHostKeyChecking=no \ - -o UserKnownHostsFile=/dev/null \ - -o PreferredAuthentications=keyboard-interactive,password \ - -o PubkeyAuthentication=no \ - -P 9222 build/erofs.ko root@127.0.0.1:/root/repo22-erofs.ko -echo "scp_exit=$?" -``` - -实际输出: - -```text -Warning: Permanently added '[127.0.0.1]:9222' (ED25519) to the list of known hosts. -scp_exit=0 -``` - -guest 校验命令: - -```sh -ls -l /root/repo22-erofs.ko -sha256 /root/repo22-erofs.ko -``` - -实际输出: - -```text --rw-r--r-- 1 root wheel 45088 Aug 8 11:13 /root/repo22-erofs.ko -SHA256 (/root/repo22-erofs.ko) = f119bb2c902bf6e221b532b1c8e7668caaa5c744c46b4eac8f9f105cd7494e9f -``` - -结论:复制成功且字节身份一致。 - -## 6. Guest 手工模块加载测试 - -### 6.1 加载前状态与 dmesg - -实际命令: - -```sh -kldstat | grep -i erofs -echo "pre_kldstat_grep_exit=$?" -dmesg | tail -30 -``` - -实际输出: - -```text -pre_kldstat_grep_exit=1 -device_attach: fdc0 attach returned 6 -ppc0: port 0x378-0x37f irq 7 on acpi0 -ppc0: Generic chipset (NIBBLE-only) in COMPATIBLE mode -ppbus0: on ppc0 -lpt0: on ppbus0 -lpt0: Interrupt-driven port -ppi0: on ppbus0 -uart0: <16550 or compatible> port 0x3f8-0x3ff irq 4 flags 0x10 on acpi0 -orm0: at iomem 0xe7800-0xeffff pnpid ORM0000 on isa0 -vga0: at port 0x3c0-0x3df iomem 0xa0000-0xbffff pnpid PNP0900 on isa0 -attimer0: at port 0x40 on isa0 -Timecounter "i8254" frequency 1193182 Hz quality 0 -Event timer "i8254" frequency 1193182 Hz quality 100 -attimer0: non-PNP ISA device will be removed from GENERIC in FreeBSD 16. -Timecounters tick every 10.000 msec -usb_needs_explore_all: no devclass -Trying to mount root from ufs:/dev/vtbd0p2 [rw]... -WARNING: / was not properly dismounted -WARNING: /: mount pending error: blocks 0 files 32 -cd0 at ata1 bus 0 scbus1 target 0 lun 0 -cd0: Removable CD-ROM SCSI device -cd0: Serial Number QM00003 -cd0: 16.700MB/s transfers (WDMA2, ATAPI 12bytes, PIO 65534bytes) -cd0: Attempt to query device size failed: NOT READY, Medium not present -intsmb0: irq 9 at device 1.3 on pci0 -intsmb0: intr IRQ 9 enabled revision 0 -smbus0: on intsmb0 -lo0: link state changed to UP -vtnet0: link state changed to UP -Security policy loaded: MAC/ntpd (mac_ntpd) -``` - -### 6.2 kldload - -实际命令: - -```sh -kldload /root/repo22-erofs.ko -echo "kldload_exit=$?" -``` - -实际输出: - -```text -kldload: an error occurred while loading module /root/repo22-erofs.ko. Please check dmesg(8) for more details. -kldload_exit=1 -``` - -失败后的实际 dmesg 命令: - -```sh -dmesg | tail -80 -``` - -实际输出末尾(失败相关行): - -```text -link_elf_obj: symbol bcmp undefined -linker_load_file: /root/repo22-erofs.ko - unsupported file type -``` - -最终复核命令: - -```sh -date -u "+%Y-%m-%dT%H:%M:%SZ" -kldstat | grep -i erofs -echo "kldstat_grep_exit=$?" -mount | grep -i erofs -echo "mount_grep_exit=$?" -mdconfig -l -dmesg | grep -E "bcmp|repo22-erofs|linker_load_file" | tail -20 -ls -l /root/repo22-erofs.ko -sha256 /root/repo22-erofs.ko -``` - -实际输出: - -```text -2026-08-08T11:14:44Z -kldstat_grep_exit=1 -mount_grep_exit=1 -link_elf_obj: symbol bcmp undefined -linker_load_file: /root/repo22-erofs.ko - unsupported file type --rw-r--r-- 1 root wheel 45088 Aug 8 11:13 /root/repo22-erofs.ko -SHA256 (/root/repo22-erofs.ko) = f119bb2c902bf6e221b532b1c8e7668caaa5c744c46b4eac8f9f105cd7494e9f -``` - -结论:**模块加载 FAIL**。失败不是 SCP 损坏导致,因为宿主和 guest 哈希完全一致。 -FreeBSD 内核链接器无法解析模块引用的 `bcmp` 符号。 - -### 6.3 kldstat 与 kldunload - -- 加载后的 `kldstat` 验证:**BLOCKED**。加载操作返回 1,最终复核也确认没有 EROFS 模块。 -- `kldunload erofs`:**BLOCKED / NOT RUN**。模块从未成功加载;执行卸载不能构成有效卸载测试。 -- 已按“任何步骤失败立即记录并停止”要求停止,没有尝试替换模块、修改 guest、绕过未解析符号或继续文件系统测试。 - -## 7. mkfs.erofs 与 plain 镜像 - -宿主工具前置检查实际命令: - -```sh -command -v mkfs.erofs -mkfs.erofs -V -``` - -实际输出: - -```text -/usr/bin/mkfs.erofs -mkfs.erofs (erofs-utils) 1.8.6 -available compressors: lz4, lz4hc, lzma, deflate, libdeflate, zstd -``` - -状态:**NOT RUN**。工具版本满足要求,但 `kldload` 已失败,因此没有生成源数据、 -没有生成 plain 镜像、没有复制镜像到 guest。这样避免在首个失败后继续推进并产生误导性结果。 - -## 8. TC001-mount-basic - -状态:**BLOCKED / NOT RUN**。 - -阻塞原因:`TC001-mount-basic.md` 的前置条件要求 FreeBSD 系统已加载 EROFS 内核模块; -本轮 `kldload /root/repo22-erofs.ko` 返回 1。因而以下命令均未执行: - -```sh -mdconfig -a -t vnode -f test.erofs -u 0 -mkdir -p /mnt/test -mount -t erofs /dev/md0 /mnt/test -mount | grep erofs -df -h /mnt/test -sha256 /mnt/test/<测试文件> -umount /mnt/test -mdconfig -d -u md0 -``` - -没有把 BLOCKED 记录为 PASS 或文件系统行为 FAIL。 - -## 9. TC009-statfs-basic - -状态:**BLOCKED / NOT RUN**。 - -用户要求仅在 TC001 通过后执行 TC009。TC001 因模块加载失败没有进入测试步骤,故 -`df -h`、`df -i`、`stat -f` 和只读标志检查均未执行。 - -## 10. 工作树与提交状态 - -ABI 提交完成后的实际确认: - -```text -HEAD=f2caaae2890a910d0f11547d9ff5f7cf92c062a2 -git status --porcelain -- repo-community/repo22 -<无输出> -``` - -本报告是本轮唯一预期新增文件。未修改 repo22 源码,未 commit,未 push。 - -## 最终判定 - -- 手工测试准备环境:PASS -- repo22 模块宿主构建:PASS -- 模块传输与完整性:PASS -- FreeBSD guest 模块加载:**FAIL** -- 失败根因证据:`link_elf_obj: symbol bcmp undefined` -- TC001:BLOCKED / NOT RUN -- TC009:BLOCKED / NOT RUN -- 总体:**FAIL / BLOCKED** - diff --git a/tests/results/manual/2026-08-08T1138Z/manual-test-report.md b/tests/results/manual/2026-08-08T1138Z/manual-test-report.md deleted file mode 100644 index 81695f2..0000000 --- a/tests/results/manual/2026-08-08T1138Z/manual-test-report.md +++ /dev/null @@ -1,513 +0,0 @@ -# repo22 手工基础测试报告 - -- 执行时间:2026-08-08 11:38-11:43 UTC -- 工作区:`/work` -- repo22:`/work/repo-community/repo22` -- Git 分支:`main` -- 测试源码提交:`0c173fa5c07a0c2af7dafcf8f89f0167ae3e1f9d` -- FreeBSD VM:SSH `127.0.0.1:9222` -- 执行方式:手工构建、SSH/SCP 和逐条 shell 命令;未生成测试 wrapper 或 CI;未修改源码 -- 前一轮失败报告:保留 `tests/results/manual/2026-08-08T1114Z/manual-test-report.md` -- 总体结论:**TC001 PASS;TC009 FAIL(用例中的 GNU 风格 `stat` 命令与 FreeBSD `stat(1)` 不兼容)** - -## 结果摘要 - -| 项目 | 状态 | 实际结果 | -|---|---|---| -| repo22 模块重建 | PASS | `build/erofs.ko`,41712 字节 | -| `bcmp` 构建检查 | PASS | `nm -u` 中没有 `bcmp` | -| 模块传输完整性 | PASS | host/guest SHA-256 均为 `6c13819ee36b1f7fe62ea084640fdefb2e871f245462291ce4c18f1ee93a684f` | -| `kldload` / `kldstat` / `kldunload` | PASS | canonical `/root/erofs.ko` 三步退出码均为 0 | -| 固定 plain 镜像 | PASS | 10 MiB、2560×4096 blocks、重复构建逐字节相同 | -| TC001-mount-basic | **PASS** | 挂载、只读标志、容量、读取、卸载均符合预期 | -| TC009-statfs-basic | **FAIL** | `df`/只读/容量正确;原文步骤 3 未显示文件系统信息,步骤 4 返回 1 | -| 清理 | PASS | 无 EROFS 模块、挂载和 md 设备残留;guest 临时文件已删除 | - -## 1. Host 环境与源码状态 - -实际命令: - -```sh -date -u '+%Y-%m-%dT%H:%M:%SZ' -uname -a -cat /etc/os-release -git rev-parse HEAD -git branch --show-current -git show HEAD:repo-community/repo22/build.sh | sha256sum -sha256sum repo-community/repo22/build.sh -git diff -- repo-community/repo22/src repo-community/repo22/build.sh -``` - -实际输出: - -```text -2026-08-08T11:38:49Z -Linux da8f2da26d77 6.12.74+deb13+1-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.74-2 (2026-03-08) x86_64 GNU/Linux -PRETTY_NAME="Debian GNU/Linux 13 (trixie)" -VERSION_ID="13" -DEBIAN_VERSION_FULL=13.5 -0c173fa5c07a0c2af7dafcf8f89f0167ae3e1f9d -main -70b6c015b33479c271e11abf36108c7f9e7ca9e2b9aa9d931fe7ae21f27f59ff - -70b6c015b33479c271e11abf36108c7f9e7ca9e2b9aa9d931fe7ae21f27f59ff repo-community/repo22/build.sh - -``` - -## 2. 重新构建模块与 `bcmp` 检查 - -实际命令: - -```sh -cd /work/repo-community/repo22 -FREEBSD_SRC=/work/dev-freebsd-releng ./build.sh -ls -l --full-time build/erofs.ko -sha256sum build/erofs.ko -nm -u build/erofs.ko | awk '$NF == "bcmp" {print; found=1} END {if (!found) print "none"}' -``` - -实际输出: - -```text -==> Building repo22 erofs.ko -/work/repo-community/repo22/src/lzma.c:133:19: warning: result of comparison of constant 256 with expression of type 'uint8_t' (aka 'unsigned char') is always true [-Wtautological-constant-out-of-range-compare] - 133 | } while (byte < 0x100); - | ~~~~ ^ ~~~~~ -/work/repo-community/repo22/src/lzma.c:135:16: warning: result of comparison of constant 256 with expression of type 'uint8_t' (aka 'unsigned char') is always true [-Wtautological-constant-out-of-range-compare] - 135 | while (byte < 0x100) - | ~~~~ ^ ~~~~~ -2 warnings generated. -==> SUCCESS: /work/repo-community/repo22/build/erofs.ko --rw-r--r-- 1 root root 41712 2026-08-08 11:38:52.056076393 +0000 build/erofs.ko -6c13819ee36b1f7fe62ea084640fdefb2e871f245462291ce4c18f1ee93a684f build/erofs.ko -none -``` - -结论:构建退出 0,上一轮导致 guest 加载失败的未解析 `bcmp` 已不在模块中。 - -## 3. SSH 与 Guest 基线 - -普通公钥探测: - -```sh -timeout 5 bash -c ' /tmp/repo22-ssh-askpass.sh <<'ASKPASS' -#!/bin/sh -printf '%s\n' "$REPO22_VM_PASS" -ASKPASS -chmod 700 /tmp/repo22-ssh-askpass.sh -export REPO22_VM_PASS='' -export DISPLAY=:0 SSH_ASKPASS=/tmp/repo22-ssh-askpass.sh SSH_ASKPASS_REQUIRE=force -setsid -w ssh \ - -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - -o PreferredAuthentications=keyboard-interactive,password \ - -o PubkeyAuthentication=no -p 9222 root@127.0.0.1 ' -date -u "+%Y-%m-%dT%H:%M:%SZ" -uname -a -freebsd-version -ku -id -sysctl kern.securelevel -command -v kldload; command -v kldstat; command -v kldunload -command -v mdconfig; command -v mount; command -v umount -command -v stat; command -v sha256 -kldstat | grep -i erofs; echo "kldstat_erofs_exit=$?" -mount | grep -i erofs; echo "mount_erofs_exit=$?" -mdconfig -l -' -``` - -```text -2026-08-08T11:39:48Z -FreeBSD freebsd-build 15.0-RELEASE-p8 FreeBSD 15.0-RELEASE-p8 releng/15.0-n281036-53054229dcb3 GENERIC amd64 -15.0-RELEASE-p8 -15.0-RELEASE-p8 -uid=0(root) gid=0(wheel) groups=0(wheel),5(operator) -kern.securelevel: -1 -/sbin/kldload -/sbin/kldstat -/sbin/kldunload -/sbin/mdconfig -/sbin/mount -/sbin/umount -/usr/bin/stat -/sbin/sha256 -kldstat_erofs_exit=1 -mount_erofs_exit=1 - -``` - -结论:VM 是 FreeBSD 15.0-RELEASE-p8 amd64;初始无 EROFS 模块、挂载或 md 设备。 - -## 4. 模块复制与生命周期回归 - -```sh -setsid -w scp -O \ - -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - -o PreferredAuthentications=keyboard-interactive,password \ - -o PubkeyAuthentication=no -P 9222 \ - repo-community/repo22/build/erofs.ko \ - root@127.0.0.1:/root/repo22-erofs.ko -``` - -首次使用非 canonical 文件名加载的实际命令与输出: - -```sh -ls -l /root/repo22-erofs.ko -sha256 /root/repo22-erofs.ko -dmesg | grep -E "bcmp|repo22-erofs|linker_load_file|erofs" | tail -20 -kldload /root/repo22-erofs.ko; echo "kldload_exit=$?" -kldstat | grep -i erofs; echo "kldstat_grep_exit=$?" -dmesg | grep -E "bcmp|repo22-erofs|linker_load_file|erofs" | tail -20 -kldunload erofs; echo "kldunload_exit=$?" -``` - -```text --rw-r--r-- 1 root wheel 41712 Aug 8 11:40 /root/repo22-erofs.ko -SHA256 (/root/repo22-erofs.ko) = 6c13819ee36b1f7fe62ea084640fdefb2e871f245462291ce4c18f1ee93a684f -link_elf_obj: symbol bcmp undefined -linker_load_file: /root/repo22-erofs.ko - unsupported file type -kldload_exit=0 - 5 1 0xffffffff82822000 5d30 repo22-erofs.ko -kldstat_grep_exit=0 -link_elf_obj: symbol bcmp undefined -linker_load_file: /root/repo22-erofs.ko - unsupported file type -kldunload: can't find file erofs -kldunload_exit=1 -``` - -两条 `bcmp`/`unsupported file type` 是本轮加载前已存在的上一轮 dmesg 历史行;加载前后输出相同,本轮 `kldload_exit=0`,没有新增链接错误。首次卸载失败仅因加载文件名是 `repo22-erofs.ko`。 - -按模块 ID 清理后,用 canonical 文件名重新执行完整生命周期: - -```sh -module_id=$(kldstat | awk '/repo22-erofs[.]ko/ {print $1}') -echo "module_id=$module_id" -kldunload -i "$module_id"; echo "kldunload_by_id_exit=$?" -kldstat | grep -i erofs; echo "post_id_unload_kldstat_exit=$?" -cp -f /root/repo22-erofs.ko /root/erofs.ko -sha256 /root/erofs.ko -kldload /root/erofs.ko; echo "kldload_exit=$?" -kldstat | grep -i erofs; echo "kldstat_grep_exit=$?" -kldunload erofs; echo "kldunload_exit=$?" -kldstat | grep -i erofs; echo "post_unload_kldstat_grep_exit=$?" -``` - -```text -module_id=5 -kldunload_by_id_exit=0 -post_id_unload_kldstat_exit=1 -SHA256 (/root/erofs.ko) = 6c13819ee36b1f7fe62ea084640fdefb2e871f245462291ce4c18f1ee93a684f -kldload_exit=0 - 5 1 0xffffffff82822000 5d30 erofs.ko -kldstat_grep_exit=0 -kldunload_exit=0 -post_unload_kldstat_grep_exit=1 -``` - -结论:`bcmp` 修复后的 `kldload`、`kldstat`、`kldunload` 回归 **PASS**。 - -## 5. 固定 plain 镜像 - -工具检查: - -```sh -command -v mkfs.erofs -mkfs.erofs -V -command -v dump.erofs -dump.erofs -V -``` - -```text -/usr/bin/mkfs.erofs -mkfs.erofs (erofs-utils) 1.8.6 -available compressors: lz4, lz4hc, lzma, deflate, libdeflate, zstd -/usr/bin/dump.erofs -dump.erofs (erofs-utils) 1.8.6 -``` - -首次尝试显式禁用 inline: - -```sh -mkfs.erofs -b4096 -x-1 -T0 --all-root --ignore-mtime \ - -Enoinline -L repo22-manual \ - /tmp/repo22-manual-plain/test.erofs \ - /tmp/repo22-manual-plain/src -``` - -```text - erofs: unknown extended option noinline -Try 'mkfs.erofs --help' for more information. -mkfs.erofs 1.8.6 -``` - -该命令退出 1 且未产出镜像。随后用整块大小、无压缩的主文件,并固定时间戳、UUID、label、属主和 xattr 设置: - -```sh -rm -rf /tmp/repo22-manual-plain -mkdir -p /tmp/repo22-manual-plain/src -printf 'repo22 FreeBSD EROFS manual test\n' > /tmp/repo22-manual-plain/src/README.txt -dd if=/dev/zero of=/tmp/repo22-manual-plain/src/plain.bin bs=4096 count=2559 status=none -touch -d @0 /tmp/repo22-manual-plain/src \ - /tmp/repo22-manual-plain/src/README.txt \ - /tmp/repo22-manual-plain/src/plain.bin -mkfs.erofs -b4096 -x-1 -T0 --all-root --ignore-mtime \ - -U 00000000-0000-0000-0000-000000000022 -L repo22-manual \ - /tmp/repo22-manual-plain/test.erofs /tmp/repo22-manual-plain/src -mkfs.erofs -b4096 -x-1 -T0 --all-root --ignore-mtime \ - -U 00000000-0000-0000-0000-000000000022 -L repo22-manual \ - /tmp/repo22-manual-plain/test-repeat.erofs /tmp/repo22-manual-plain/src -cmp /tmp/repo22-manual-plain/test.erofs /tmp/repo22-manual-plain/test-repeat.erofs -echo "cmp_exit=$?" -sha256sum /tmp/repo22-manual-plain/src/README.txt \ - /tmp/repo22-manual-plain/src/plain.bin \ - /tmp/repo22-manual-plain/test.erofs \ - /tmp/repo22-manual-plain/test-repeat.erofs -dump.erofs -s /tmp/repo22-manual-plain/test.erofs -``` - -关键实际输出: - -```text -Filesystem UUID: 00000000-0000-0000-0000-000000000022 -Filesystem total blocks: 2560 (of 4096-byte blocks) -Filesystem total inodes: 3 -Filesystem total metadata blocks: 1 -cmp_exit=0 -c1f6e23b161735085a00a36cb72926bbe187a63eabcb4425a4f5acad5fe3ff4a /tmp/repo22-manual-plain/src/README.txt -20bf72d8cbc4dbc7e214b671f2547e3e8d57d455d10ac6b645656ef26afc6880 /tmp/repo22-manual-plain/src/plain.bin -fec83f76b8b7eb1d83a9a51246e093c319061838b04633184e9a0c601a386ff3 /tmp/repo22-manual-plain/test.erofs -fec83f76b8b7eb1d83a9a51246e093c319061838b04633184e9a0c601a386ff3 /tmp/repo22-manual-plain/test-repeat.erofs -Filesystem magic number: 0xE0F5E1E2 -Filesystem blocksize: 4096 -Filesystem blocks: 2560 -Filesystem inode count: 3 -Filesystem created: Thu Jan 1 00:00:00 1970 -Filesystem features: sb_csum mtime -Filesystem UUID: 00000000-0000-0000-0000-000000000022 -``` - -镜像大小为 10485760 字节,即 10 MiB。两次独立输出逐字节相同。 - -复制并验证: - -```sh -setsid -w scp -O <同上 SSH 认证选项> -P 9222 \ - /tmp/repo22-manual-plain/test.erofs root@127.0.0.1:/root/test.erofs -ls -l /root/test.erofs -sha256 /root/test.erofs -``` - -```text --rw-r--r-- 1 root wheel 10485760 Aug 8 11:42 /root/test.erofs -SHA256 (/root/test.erofs) = fec83f76b8b7eb1d83a9a51246e093c319061838b04633184e9a0c601a386ff3 -``` - -## 6. TC001-mount-basic - -原文步骤前仅做规定前置条件:把 canonical 模块放入标准搜索目录,并附加镜像为 `/dev/md0`。 - -```sh -ls -l /boot/modules/erofs.ko -cp -f /root/erofs.ko /boot/modules/erofs.ko -sha256 /boot/modules/erofs.ko -umount /mnt/test 2>/dev/null -mdconfig -d -u md0 2>/dev/null -mdconfig -a -t vnode -f /root/test.erofs -u 0 -echo "mdconfig_attach_exit=$?" -mdconfig -lv -``` - -```text -ls: /boot/modules/erofs.ko: No such file or directory -SHA256 (/boot/modules/erofs.ko) = 6c13819ee36b1f7fe62ea084640fdefb2e871f245462291ce4c18f1ee93a684f -mdconfig_attach_exit=0 -md0 vnode 10M /root/test.erofs - -``` - -原文步骤 1-5: - -```sh -kldload erofs; echo "tc001_step1_exit=$?" -kldstat | grep erofs; echo "tc001_step2_pipeline_exit=$?" -mkdir -p /mnt/test; echo "tc001_step3_exit=$?" -mount -t erofs /dev/md0 /mnt/test; echo "tc001_step4_exit=$?" -mount | grep erofs; echo "tc001_step5a_pipeline_exit=$?" -df -h /mnt/test; echo "tc001_step5b_exit=$?" -``` - -```text -tc001_step1_exit=0 - 5 1 0xffffffff82822000 5d30 erofs.ko -tc001_step2_pipeline_exit=0 -tc001_step3_exit=0 -tc001_step4_exit=0 -/dev/md0 on /mnt/test (erofs, local, read-only) -tc001_step5a_pipeline_exit=0 -Filesystem Size Used Avail Capacity Mounted on -/dev/md0 10M 10M 0B 100% /mnt/test -tc001_step5b_exit=0 -``` - -补充读取验证: - -```sh -ls -la /mnt/test -sha256 /mnt/test/README.txt /mnt/test/plain.bin -cat /mnt/test/README.txt -echo "tc001_content_exit=$?" -dmesg | tail -20 -``` - -```text -ls: /mnt/test/.: Operation not supported -ls: /mnt/test/README.txt: Operation not supported -ls: /mnt/test/plain.bin: Operation not supported -total 10248 -drwxr-xr-x 2 root wheel 70 Jan 1 1970 . -drwxr-xr-x 3 root wheel 512 Aug 8 11:42 .. --rw-r--r-- 1 root wheel 33 Jan 1 1970 README.txt --rw-r--r-- 1 root wheel 10481664 Jan 1 1970 plain.bin -SHA256 (/mnt/test/README.txt) = c1f6e23b161735085a00a36cb72926bbe187a63eabcb4425a4f5acad5fe3ff4a -SHA256 (/mnt/test/plain.bin) = 20bf72d8cbc4dbc7e214b671f2547e3e8d57d455d10ac6b645656ef26afc6880 -repo22 FreeBSD EROFS manual test -tc001_content_exit=0 -``` - -`ls -la` 在读取附加 stat/pathconf 信息时打印 `Operation not supported`,但仍列出目录;两个文件均完整可读且 SHA-256 与 host 一致。该补充现象不属于 TC001 规定判定项,单独保留为观察。`dmesg | tail -20` 没有本轮 mount 错误;末尾两条 `bcmp` 行是上一轮历史记录。 - -原文步骤 6-7: - -```sh -umount /mnt/test; echo "tc001_step6_exit=$?" -mount | grep erofs; echo "tc001_step7_pipeline_exit=$?" -``` - -```text -tc001_step6_exit=0 - -tc001_step7_pipeline_exit=1 -``` - -步骤 7 的 grep 返回 1 正是“没有 EROFS 挂载”的预期。 - -**TC001 最终判定:PASS。** - -## 7. TC009-statfs-basic - -原文步骤 1-2: - -```sh -mount -t erofs /dev/md0 /mnt/test; echo "tc009_step1_exit=$?" -df -h /mnt/test; echo "tc009_step2a_exit=$?" -df -i /mnt/test; echo "tc009_step2b_exit=$?" -``` - -```text -tc009_step1_exit=0 -Filesystem Size Used Avail Capacity Mounted on -/dev/md0 10M 10M 0B 100% /mnt/test -tc009_step2a_exit=0 -Filesystem 1K-blocks Used Avail Capacity iused ifree %iused Mounted on -/dev/md0 10240 10240 0 100% 3 0 100% /mnt/test -tc009_step2b_exit=0 -``` - -总容量 10 MiB、可用块 0、inode 总数 3、可用 inode 0。 - -严格执行原文步骤 3: - -```sh -stat -f /mnt/test -echo "tc009_step3_exit=$?" -``` - -```text -/mnt/test -tc009_step3_exit=0 -``` - -FreeBSD `stat(1)` 的 `-f` 表示“后一个参数是格式字符串”,不是 GNU `stat -f` 的“显示文件系统状态”。命令只回显 `/mnt/test`,没有达到用例要求的“Shows EROFS filesystem type”。 - -严格执行原文步骤 4: - -```sh -stat -f -f "%b %f %c %a %d %i %t %n" /mnt/test -echo "tc009_step4_exit=$?" -``` - -```text -stat: %b %f %c %a %d %i %t %n: No such file or directory --f -tc009_step4_exit=1 -``` - -原文步骤 5 与补充容量核对: - -```sh -mount | grep /mnt/test | grep read-only -echo "tc009_step5_pipeline_exit=$?" -mount | grep /mnt/test -df -k /mnt/test -``` - -```text -/dev/md0 on /mnt/test (erofs, local, read-only) -tc009_step5_pipeline_exit=0 -/dev/md0 on /mnt/test (erofs, local, read-only) -Filesystem 1024-blocks Used Avail Capacity Mounted on -/dev/md0 10240 10240 0 100% /mnt/test -``` - -`10240` 个 1 KiB 块等于 `2560` 个 4096-byte 块;只读标志、文件系统类型和 free=0 均正确。但严格按用例判定,步骤 3 未产生要求的信息,步骤 4 退出 1。 - -**TC009 最终判定:FAIL。失败原因是测试文档中的 `stat` 命令与 FreeBSD `stat(1)` CLI 语义不兼容;本轮未修改测试文档或源码。** - -## 8. 清理与最终状态 - -```sh -umount /mnt/test; echo "cleanup_umount_exit=$?" -mdconfig -d -u md0; echo "cleanup_mdconfig_exit=$?" -kldunload erofs; echo "cleanup_kldunload_exit=$?" -rmdir /mnt/test; echo "cleanup_rmdir_exit=$?" -rm -f /boot/modules/erofs.ko /root/test.erofs /root/erofs.ko /root/repo22-erofs.ko -echo "cleanup_rm_exit=$?" -kldstat | grep -i erofs; echo "final_kldstat_grep_exit=$?" -mount | grep -i erofs; echo "final_mount_grep_exit=$?" -mdconfig -l -``` - -```text -cleanup_umount_exit=0 -cleanup_mdconfig_exit=0 -cleanup_kldunload_exit=0 -cleanup_rmdir_exit=0 -cleanup_rm_exit=0 -final_kldstat_grep_exit=1 -final_mount_grep_exit=1 - -``` - -结论:guest 清理完成,无模块、挂载、md 设备或本轮临时文件残留。 - -## 最终判定 - -- `bcmp` 修复后模块构建与 host 检查:**PASS** -- guest `kldload` / `kldstat` / `kldunload`:**PASS** -- 固定 10 MiB plain 镜像生成、重复性与 SHA-256:**PASS** -- TC001-mount-basic:**PASS** -- TC009-statfs-basic:**FAIL(测试命令不兼容 FreeBSD `stat(1)`)** -- BLOCKED:**0** -- 源码修改:**无** -- wrapper/CI:**未生成** diff --git a/tests/results/manual/2026-08-08T1307Z/manual-test-report.md b/tests/results/manual/2026-08-08T1307Z/manual-test-report.md deleted file mode 100644 index 1ad6712..0000000 --- a/tests/results/manual/2026-08-08T1307Z/manual-test-report.md +++ /dev/null @@ -1,141 +0,0 @@ -# repo22 压缩功能手工测试报告 - -- 执行日期:2026-08-08(UTC) -- 执行时段:约 12:58-13:07 UTC -- 测试对象:`/work/repo-community/repo22` -- 测试提交:`f230129565677e602510347a55a80856237fe909` -- 远端基线:执行前 `xdm/main` 指向同一提交 -- FreeBSD VM:15.0-RELEASE-p8 amd64,QEMU TCG,SSH `127.0.0.1:9222` -- 镜像工具:erofs-utils 1.8.6 -- 执行方式:逐条 host/guest shell 命令、SSH/SCP、`mdconfig` 和真实内核挂载;未实现 CI 或测试 wrapper -- 总体结论:**压缩功能不可验收。真实 compact LZ4 镜像可挂载,但首个 4 KiB 数据读取返回 `EINTEGRITY`,输出 0 字节。** - -## 状态定义 - -| 状态 | 含义 | -|---|---| -| PASS | 该步骤已经在本轮实际执行,结果符合预期 | -| KERNEL-FAIL | 真实镜像进入 FreeBSD 内核路径后失败 | -| MKFS-UNAVAILABLE | erofs-utils 1.8.6 不能直接生成测试要求的镜像 | -| TEST-DOC | 当前 Markdown 的命令或预期不能可靠验证目标行为 | -| NOT RUN | 已准备 fixture,但收到立即停止新增测试的指令后未进入对应内核路径 | - -## 最先出现的内核失败 - -使用 erofs-utils 1.8.6 生成根目录单文件 compact LZ4 镜像: - -```sh -mkfs.erofs -T0 --all-root --ignore-mtime -x-1 -Uclear \ - -zlz4 -C4096 lz4-root-compact-4k.erofs src-root-lz4 -``` - -`dump.erofs` 确认该文件不是 plain fallback: - -```text -Filesystem features: sb_csum mtime 0padding -Path : /test-lz4.txt -Size: 1048576 On-disk size: 8192 regular file -NID: 39 Layout: 3 Compression ratio: 0.78% -Ext 0: logical 0..639004, physical 4096..8192, physical length 4096 -Ext 1: logical 639004..1048576, physical 8192..12288, physical length 4096 -``` - -Guest 中模块加载和挂载成功: - -```text -SHA256 (/root/erofs.ko) = c81957971e670c8b8cc45d5119f568eb84adb6114699538588e5a842ea4d97b3 -kldload_rc=0 -5 1 0xffffffff82822000 8468 erofs.ko -/dev/md0 on /mnt/repo22-test (erofs, local, read-only) -``` - -文件 lookup 和 `stat` 成功后,第一次读取即失败: - -```text -$ sha256 /mnt/repo22-test/test-lz4.txt -sha256: /mnt/repo22-test/test-lz4.txt: Integrity check failed -read_rc=1 - -$ dd if=/mnt/repo22-test/test-lz4.txt of=/tmp/repo22-first4k bs=4096 count=1 -dd: /mnt/repo22-test/test-lz4.txt: Integrity check failed -0+0 records in -0+0 records out -0 bytes transferred -first_read_rc=1 -``` - -因此首个确定性压缩内核失败是: - -- fixture:真实 `mkfs.erofs 1.8.6` compact LZ4,Layout 3; -- mount:PASS; -- lookup/stat:PASS; -- 首个 4 KiB read:**KERNEL-FAIL / `EINTEGRITY`**; -- 用户缓冲区:0 字节,没有把部分输出误判为成功; -- 稳定性:命令后 VM 仍响应,时间为 `2026-08-08T13:06:43Z`,未观察到 panic 或 hang。 - -并发静态审查已经指出 compact 索引位置按 filesystem block size 计算,而不是按 compact index pack 大小计算;本轮失败与该问题相符,但手工测试本身只证明“真实 compact LZ4 首读失败”,不单独宣称完成了代码级根因证明。 - -## 已尝试 TC 结果 - -| TC | 状态 | 本轮证据 | -|---|---|---| -| TC003-lz4-compressed-read | **KERNEL-FAIL** | 真实 compact LZ4 镜像 mount PASS;完整 hash 与首个 4 KiB read 都返回 `EINTEGRITY` | -| TC084-lz4-basic | **KERNEL-FAIL** | 与 TC003 共用的最小真实 LZ4 基础读路径失败,无法进行透明解压和内容比较 | -| TC086-lz4-sequential-read | **TEST-DOC** | 用例要求不存在的 `vfs:erofs::read` DTrace provider,并把 TCG VM 上固定吞吐量当功能判据 | -| TC088-lz4-config-handling | **TEST-DOC** | 4K/64K/256K fixture 均生成;“pcluster 越大则镜像必然更小、随机读必然更慢”不是对任意语料都成立的功能断言 | -| TC090-lz4-pcluster-64k | **TEST-DOC** | 步骤 4 把 `dd ... of=/tmp/out` 再 pipe 给 `tail/head`,pipe 中没有文件数据,无法验证跨边界读取 | -| TC098-fragments-support | **TEST-DOC** | EROFS fragments 使用 packed inode,不是 Markdown 所写的独立 fragment device;`-o device=/dev/md1` 验证的是多设备而非 fragments | -| TC101-unified-address-mapping | **TEST-DOC** | 前置要求多设备,但 mount 命令只提供 device0;`ktrace` 也不能直接证明驱动选中了正确 EROFS device id | -| TC110-lzma-corrupt | **TEST-DOC** | 固定覆盖 image offset 8192 没有先定位目标文件的 compressed extent,可能破坏无关数据或元数据,不能确定性验证 LZMA 错误路径 | -| TC115-unsupported-algorithm | **MKFS-UNAVAILABLE** | mkfs 1.8.6 只提供 lz4/lz4hc/lzma/deflate/libdeflate/zstd,不能直接生成 future algorithm ID;需要有校验意识的定向 ABI patch fixture | -| TC116-truncated-compressed | **TEST-DOC** | 盲目从镜像末尾截去 4096 字节不保证命中目标 compressed extent;可能只移除 padding、其他文件或元数据 | - -## 已生成但按停止指令未运行的 fixture - -以下 fixture 的 `mkfs.erofs` 和目标布局检查已经成功,但没有据此把对应 TC 标为 PASS: - -| TC 范围 | Fixture 准备结果 | TC 状态 | -|---|---|---| -| TC004、TC108、TC109 | 101 MiB MicroLZMA/LZMA level 6;Layout 3;19 个真实 compressed extents | NOT RUN | -| TC085、TC087、TC089 | 256 MiB/50 MiB/4K LZ4 corpus;compact 4K 与 legacy full 镜像均生成 | NOT RUN | -| TC090 | compact 64K big-pcluster 镜像生成,superblock 含 `compr_cfgs big_pcluster` | TEST-DOC,未运行内核读 | -| TC091、TC092 | `-Eztailpacking` 镜像生成,superblock 含 `ztailpacking` | NOT RUN | -| TC093、TC095 | single-device 1 MiB chunk 与 512 KiB chunk-index 镜像生成,Layout 4 | NOT RUN | -| TC094、TC096、TC099 | main image + 5 MiB external blob 生成;首次必须预创建 blob 文件后 mkfs 才成功 | NOT RUN | -| TC097、TC100 | 没有生成确定性 invalid table 或四外部设备 fixture | NOT RUN | -| TC102-TC104 | DEFLATE level 1/6/9 镜像均生成,file1m 为真实 Layout 3 | NOT RUN | -| TC105-TC107 | ZSTD level 1/15/22 镜像均生成,file1m 为真实 Layout 3 | NOT RUN | - -收到“立即停止新增测试”指令后,没有继续 mount/read 上述镜像,也没有继续制作 corruption、unsupported algorithm、interlaced、extent 或 partial-ref 变体。 - -## Fixture 审核摘要 - -本轮生成并由 `dump.erofs` 确认的主要布局: - -| 镜像 | 关键特征 | -|---|---| -| `lz4-compact-4k.erofs` | Layout 3 compact;`sb_csum mtime 0padding` | -| `lz4-full-4k.erofs` | Layout 1 full;`-Elegacy-compress` | -| `lz4-compact-64k.erofs` | Layout 3;`compr_cfgs big_pcluster`;真实物理 extent 最大 53248 字节 | -| `lz4-compact-256k.erofs` | Layout 3;`compr_cfgs big_pcluster` | -| `lz4-ztailpacking.erofs` | `ztailpacking` incompat feature | -| `lz4-fragments.erofs` | `fragments dedupe`,目标文件物理长度由 packed inode 提供 | -| `lzma-level6.erofs` | 101 MiB 逻辑文件,126976 字节 on-disk,Layout 3 | -| `deflate-level{1,6,9}.erofs` | 三个等级均为真实 compressed Layout 3 | -| `zstd-level{1,15,22}.erofs` | 三个等级均为真实 compressed Layout 3 | -| `chunk-single-1m.erofs` | `chunked_file`,Layout 4,三个 1 MiB extent | -| `chunk-index-512k.erofs` | `chunked_file`,Layout 4,8-byte chunk index fixture | -| `chunk-multidev-main.erofs` + `.blob` | 4 KiB metadata image + 5 MiB external blob | - -所有镜像、source corpus、VM overlay 和模块 build 产物仅位于 `/work/build`、guest `/root` 或 repo22 忽略的 `build/`,没有加入 Git。 - -## 其他观察 - -1. 多文件嵌套目录镜像首次直接 lookup `/files/test-lz4.txt` 偶发返回 `ENOENT`;先执行目录枚举后 `stat` 可成功。根目录单文件 fixture 消除了该前置干扰,并稳定进入压缩 read 后返回 `EINTEGRITY`。 -2. FreeBSD `ls -l` 对 EROFS 条目打印 `Operation not supported`,同时仍能列出项目;这看起来来自 ACL/扩展属性查询,不应当被误记为目录读取失败。 -3. dmesg 中的 `bcmp undefined` 是前一轮旧模块的历史日志;本轮 canonical `/root/erofs.ko` 的 `kldload` 返回 0,且模块 SHA-256 为新的 `c819...97b3`。 -4. 并发静态审查另报 incompat `0x8`、64K `clusterofs` 截断、通用剥前导零和短输出泄漏。本轮遵照停止指令没有再构造独立运行时 fixture 复核这些问题。 - -## 结论 - -`f2301295` 的压缩重构已经达到“模块可加载、真实压缩镜像可挂载”的阶段,但尚未达到“可读取最基本 compact LZ4 文件”的最低 feature gate。应先修复 compact zmap/read 闭环,并针对静态审查列出的五个确定性阻断补足定向手测,再恢复其余算法和高级布局验证。 diff --git a/tests/results/manual/2026-08-08T1332Z/manual-test-report.md b/tests/results/manual/2026-08-08T1332Z/manual-test-report.md deleted file mode 100644 index 598d04b..0000000 --- a/tests/results/manual/2026-08-08T1332Z/manual-test-report.md +++ /dev/null @@ -1,84 +0,0 @@ -# repo22 compact LZ4 root-cause verification - -- Date: 2026-08-08 UTC -- Source parent: `c939c472134c69fa4665f52ab1aec9a17cd583b9` -- FreeBSD guest: 15.0-RELEASE-p8 amd64, QEMU TCG -- Image tool: erofs-utils 1.8.6 -- Scope: the existing minimal compact LZ4 fixture only; no CI or wrapper was added - -## Changes under verification - -1. Accept incompat bit `0x8` for compressed HEAD2 while rejecting images with - external device slots until true multi-device I/O exists. -2. Widen zmap `clusterofs` so the 64 KiB NONHEAD sentinel is representable. -3. Count compact indexes using the logical cluster size. -4. Apply leading zero padding according to the algorithm and the LZ4 - `0padding` feature instead of stripping it unconditionally for all streams. -5. Zero decompression targets and require exact output lengths. -6. Apply DEFLATE `windowbits` and ZSTD `windowlog` to the decoder calls. -7. Separate LZ4 full and partial completion rules and validate input/output - boundaries. -8. Permit valid compact NONHEAD deltas to cross compact-pack boundaries. The - previous BSD-only restriction caused the first real image read to fail at - the second compressed extent head. - -## Build and module lifecycle - -`./build.sh` completed successfully. The resulting module had no unresolved -`bcmp` symbol and contained no temporary zmap/zdata diagnostic strings. - -On the guest, all lifecycle operations succeeded: - -```text -kldload_rc=0 -mount_rc=0 -umount_rc=0 -md_detach_rc=0 -kldunload_rc=0 -``` - -## Real image result - -The fixture was generated by the existing manual-test command: - -```sh -mkfs.erofs -T0 --all-root --ignore-mtime -x-1 -Uclear \ - -zlz4 -C4096 lz4-root-compact-4k.erofs src-root-lz4 -``` - -`dump.erofs` identifies `/test-lz4.txt` as layout 3 (compact), 1 MiB logical, -8 KiB on disk, with two compressed extents. The same image previously failed -its first read with `EINTEGRITY` on `f2301295`. - -Final FreeBSD results: - -```text -SHA256 (/mnt/repo22-rootfix/test-lz4.txt) = 370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52 -SHA256 (source test-lz4.txt) = 370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52 -SHA256 (first 4096 bytes from EROFS) = 3d6d283a95f80b3e4a9c243cf82dc0644283910b6a3be931fede5d771e132b14 -SHA256 (first 4096 bytes from source) = 3d6d283a95f80b3e4a9c243cf82dc0644283910b6a3be931fede5d771e132b14 -full_read_rc=0 -first_4k_read_rc=0 -``` - -Result: the minimal real compact LZ4 mount/read gate passes. - -## Integration follow-up - -The integration review after `6c9543892` found that the leading-padding -predicate still selected every non-LZ4 algorithm. The predicate was narrowed -to LZ4 streams with the `0padding` incompat feature, matching the stated -algorithm-specific behavior. `./build.sh` and the same FreeBSD 15 compact LZ4 -mount/read lifecycle were rerun after this correction; the full-file SHA256 -remained `370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52`, -the first 4096-byte read returned 4096 bytes, and module unload succeeded. - -## Remaining gaps - -This batch does not claim runtime completion for legacy full indexes, 64 KiB -or larger pclusters, ztailpacking, interlaced pclusters, fragments, extents, -partial references, MicroLZMA, DEFLATE, or ZSTD images. DEFLATE and ZSTD -configuration handling was corrected and the module loaded on this FreeBSD 15 -kernel, but those algorithms were not image-tested in this converged batch. -The LZ4 partial-decoding branch was code-reviewed but is not exercised by the -current synchronous full-extent read path. diff --git a/tests/results/manual/2026-08-08T1356Z/manual-test-report.md b/tests/results/manual/2026-08-08T1356Z/manual-test-report.md deleted file mode 100644 index dceadfb..0000000 --- a/tests/results/manual/2026-08-08T1356Z/manual-test-report.md +++ /dev/null @@ -1,88 +0,0 @@ -# repo22 non-LZ4 leading-zero verification - -- Date: 2026-08-08 UTC -- Source baseline: `7cba92c0ec434b96180200b09e5dddfa213e67f5` -- FreeBSD guest: 15.0-RELEASE-p8 amd64, QEMU TCG -- Image tool: erofs-utils 1.8.6 -- Scope: MicroLZMA, DEFLATE, and ZSTD leading-zero handling only - -## Code change - -Linux 7.1-rc1 calls `z_erofs_fixup_insize()` for every MicroLZMA, DEFLATE, -and ZSTD stream, while LZ4 only requires it when the `LZ4_0PADDING` feature is -enabled. The FreeBSD dispatcher now applies the same predicate. Existing -first-block limits, empty-input rejection, and input-length subtraction remain -unchanged. - -## Build and module lifecycle - -`./build.sh` completed successfully and produced `build/erofs.ko` with SHA256: - -```text -4b3459a02da29642cb8e61f58e34ff5266e668e86256937c91b8af7e0e31fdcb -``` - -FreeBSD 15 results: - -```text -lifecycle_kldload_rc=0 -lifecycle_kldunload_rc=0 -dmesg_delta_lines=0 -dmesg_error_grep_rc=1 -``` - -The last two lines mean the isolated lifecycle and read pass added no kernel -messages, so no decompression, linker, panic, or integrity error was present. - -## Deterministic images - -The source file was 1 MiB and had SHA256: - -```text -370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52 -``` - -The images were regenerated with erofs-utils 1.8.6: - -```sh -mkfs.erofs -T0 --all-root --ignore-mtime -x-1 -Uclear \ - -zlzma,level=6 -C4096 lzma-root.erofs src-root -mkfs.erofs -T0 --all-root --ignore-mtime -x-1 -Uclear \ - -zdeflate,level=6 -C4096 deflate-root.erofs src-root -mkfs.erofs -T0 --all-root --ignore-mtime -x-1 -Uclear \ - -zzstd,level=3 -C4096 zstd-root.erofs src-root -``` - -`dump.erofs` reported layout 3 for `/test-lz4.txt` in all images. MicroLZMA -and ZSTD used one 4096-byte physical extent; DEFLATE used two 4096-byte -physical extents. - -## FreeBSD read results - -Each image was attached with `mdconfig`, mounted read-only, read completely by -`sha256`, unmounted, and detached before testing the next image. - -```text -lzma mount_rc=0 read_rc=0 sha256=370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52 -lzma umount_rc=0 detach_rc=0 -deflate mount_rc=0 read_rc=0 sha256=370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52 -deflate umount_rc=0 detach_rc=0 -zstd mount_rc=0 read_rc=0 sha256=370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52 -zstd umount_rc=0 detach_rc=0 -``` - -Result: MicroLZMA, DEFLATE, and ZSTD all pass real-image full-file integrity -verification on this FreeBSD 15 kernel configuration. - -## Notes and limits - -An exploratory image with the file below a `/files` directory mounted, but the -target lookup failed. Root-level fixtures were used to isolate decompression -from the separately tracked directory lookup work. This does not change the -three successful compressed read results above. - -This batch does not claim coverage for other compression levels, large files, -random access, corrupt streams, alternative FreeBSD kernel configurations, or -other compression mapping features. DEFLATE and ZSTD kernel symbols were -available on the tested FreeBSD 15 GENERIC kernel; no unsupported algorithm or -kernel-symbol limitation was encountered. diff --git a/tests/results/manual/2026-08-08T1413Z/manual-test-report.md b/tests/results/manual/2026-08-08T1413Z/manual-test-report.md deleted file mode 100644 index 26d68f0..0000000 --- a/tests/results/manual/2026-08-08T1413Z/manual-test-report.md +++ /dev/null @@ -1,99 +0,0 @@ -# repo22 compressed mapping shape manual test report - -- Date: 2026-08-08 UTC -- Source baseline: `b1f9e7c0ef5ffb9c4aab38e4047534ad2071af6c` -- Remote baseline before testing: `xdm/main` at the same commit -- FreeBSD guest: 15.0-RELEASE-p8 amd64, QEMU TCG -- Image tool: erofs-utils 1.8.6 -- Module SHA256: `4b3459a02da29642cb8e61f58e34ff5266e668e86256937c91b8af7e0e31fdcb` -- Scope: manual image generation, layout inspection, and the completed compact-index kernel reads; no CI or test wrapper was added - -## Status definitions - -| Status | Meaning | -|---|---| -| PASS | The stated mkfs/layout or kernel operation was actually run and matched its deterministic expectation | -| KERNEL-FAIL | A real image reached the FreeBSD kernel path and failed | -| MKFS-UNAVAILABLE | erofs-utils 1.8.6 cannot generate the required format directly | -| TEST-DOC | The existing Markdown command or assertion does not deterministically test the stated feature | -| NOT RUN | The fixture was generated, but kernel execution stopped on user request and no feature pass is claimed | - -## Generated fixture matrix - -All images were regenerated under `/work/build/repo22-shape-manual-20260808T1407Z` with fixed timestamps, root ownership, disabled xattrs, and a cleared UUID. Images, sources, VM state, and module build products were not staged. - -| Shape | mkfs/layout status | Evidence | Kernel status | -|---|---|---|---| -| Compact indexes | PASS | default `-zlz4 -C4096`; `/shape.dat` is Layout 3 with two compressed extents | PASS | -| Legacy full indexes | PASS | `-Elegacy-compress`; `/shape.dat` is Layout 1 with two compressed extents | NOT RUN | -| 64 KiB big pcluster | PASS | `-C65536`; superblock reports `compr_cfgs big_pcluster`; Layout 3 has one 1 MiB logical extent backed by 8192 bytes | NOT RUN | -| Inline ztailpacking | PASS | `-Eztailpacking`; superblock reports `ztailpacking`; `/inline.dat` has on-disk size 0 and physical bytes `1352..1787` inside the metadata block | NOT RUN | -| Fragments / packed inode | PASS | `-Eall-fragments`; superblock reports packed NID 42 plus `fragments dedupe`; `/fragment.dat` has on-disk size 0 | NOT RUN | -| Partial reference | PASS | `-Ededupe`; mkfs reports `Dedupe 409572 compressed data (delta 315374)` and `/b.dat` reuses the physical extent at `8192..12288` | NOT RUN | -| Extent metadata format | MKFS-UNAVAILABLE | erofs-utils v1.8.6 has no extent-map advise/structure or mkfs option for the newer extent metadata format; `--max-extent-bytes` only limits decompressed extent size | NOT RUN | - -Host-side `fsck.erofs --extract` succeeded for the initially generated compact, full, big-pcluster, ztailpacking-option, and all-fragments images. This is fixture validation only, not a FreeBSD kernel feature pass. - -## Completed FreeBSD compact-index verification - -The compact image used a deterministic 1 MiB file. `dump.erofs` reported: - -```text -Layout: 3 -Ext 0: logical 0..639004, physical 4096..8192, physical length 4096 -Ext 1: logical 639004..1048576, physical 8192..12288, physical length 4096 -``` - -Guest operations completed as follows: - -```text -kldload_rc=0 -md_attach_rc=0 -mount_rc=0 -stat_size=1048576 -full_sha256=370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52 -expected_full_sha256=370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52 -cross_extent_offset=638972 -cross_extent_length=128 -cross_extent_sha256=941b6e3cb9a7384428d93ca248eb9630782538e04caba30dbfbe31baacca8f60 -expected_cross_extent_sha256=941b6e3cb9a7384428d93ca248eb9630782538e04caba30dbfbe31baacca8f60 -``` - -Result: compact full-file integrity and a read crossing the logical boundary immediately before the second compressed extent both pass. - -## First failure and dmesg - -No KERNEL-FAIL occurred in this batch. The first incomplete command was: - -```sh -dd if=/mnt/repo22-shape/shape.dat bs=1 skip=777777 count=8192 | sha256 -q -``` - -It was operator-terminated when the user requested immediate convergence. Byte-sized `dd` caused repeated page reads under QEMU TCG and was an inefficient manual command; it did not return a kernel error and is not classified as KERNEL-FAIL. - -The guest was responsive after termination, and the mount and md device were cleaned up. The dmesg tail contained only successful mapping diagnostics such as: - -```text -erofs zread: nid=39 la=1044480 mapla=639004 llen=409572 pa=8192 plen=4096 flags=1 alg=0 -erofs zread: nid=39 la=0 mapla=0 llen=639004 pa=4096 plen=4096 flags=1 alg=0 -``` - -There was no new `EINTEGRITY`, decompression error, panic, trap, or hang message. The volume of unconditional `erofs zread` diagnostics should be reviewed separately as a style/logging issue. - -## TC disposition - -| TC area | Status | Result | -|---|---|---| -| TC084 compact LZ4 basic | PASS | Complete SHA256 and cross-extent boundary read match the source | -| TC088 pcluster configuration | TEST-DOC | Previous monotonic ratio/performance assertions were invalid; commands and pass criteria were corrected | -| TC090 64 KiB pcluster | TEST-DOC | Previous pipeline read from files redirected away from the pipe; deterministic cross-boundary command was corrected; kernel execution remains NOT RUN | -| TC091 ztailpacking | TEST-DOC | Enabling the option alone did not tail-pack the first 1 MiB target; the TC now requires `dump.erofs` proof of an inline extent; kernel execution remains NOT RUN | -| TC098 fragments | TEST-DOC | Previous TC incorrectly described fragments as an external device; it now tests the same-image packed inode; kernel execution remains NOT RUN | -| TC115 unsupported algorithm | MKFS-UNAVAILABLE | mkfs.erofs 1.8.6 only emits its supported algorithm IDs; a checksum-aware ABI patch fixture is required | -| TC116 truncated compressed data | TEST-DOC | Blind `truncate -s -4096` was nondeterministic; the TC now truncates inside a located target extent; kernel execution remains NOT RUN | - -TC085-TC087, TC089, TC092-TC101 other than TC098 were read for dependencies but were not executed in this converged batch. Multi-device and chunk tests are independent follow-up work and are not implied by the packed-inode fragment fixture. - -## Conclusion - -The completed kernel evidence establishes compact-index LZ4 full-read and compressed-extent-boundary correctness on the tested FreeBSD 15 guest. Legacy full indexes, big pclusters, inline ztailpacking, fragments, and partial references are confirmed as real erofs-utils 1.8.6 fixtures but remain explicitly NOT RUN in the FreeBSD kernel after the stop instruction. No kernel failure was observed. diff --git a/tests/results/manual/2026-08-08T1427Z/manual-test-report.md b/tests/results/manual/2026-08-08T1427Z/manual-test-report.md deleted file mode 100644 index afa346a..0000000 --- a/tests/results/manual/2026-08-08T1427Z/manual-test-report.md +++ /dev/null @@ -1,53 +0,0 @@ -# repo22 remaining compressed mapping manual read report - -- Date: 2026-08-08 UTC -- Source baseline: `df5b92c902295dc74a08aef89fd93d698bbddf21` -- Remote baseline before testing: `xdm/main` at the same commit -- FreeBSD guest: 15.0-RELEASE-p8 amd64, QEMU TCG -- Image tool: erofs-utils 1.8.6 -- Scope: kernel mount/read verification for the five previously generated compression layouts only; no source, CI, or test-wrapper changes - -## Result matrix - -| Mode | Fixture proof | Mount | Full SHA256 | Offset/boundary read | Isolated dmesg | Result | -|---|---|---:|---|---|---|---| -| Legacy full index | Layout 1, two compressed extents | PASS | `EIO`, no digest | `EIO` at offset 638972 across the 639004 extent boundary | No panic, trap, integrity, corruption, or decompression-error signature; guest responsive | **KERNEL-FAIL** | -| 64 KiB pcluster | Layout 3; `compr_cfgs big_pcluster`; one 1 MiB logical extent backed by 8192 bytes | PASS | Match | Match at offset 65504, length 64 | No error signature; guest responsive | **PASS** | -| Inline ztailpacking | `ztailpacking`; compressed extent at physical 1352..1787 inside metadata block | PASS | Match | Match at offset 31744, length 2048 | No error signature; guest responsive | **PASS** | -| Fragments / packed inode | `fragments`; packed NID; whole-file extent has zero direct on-disk bytes | PASS | Match | Match at offset 65536, length 8192 | No error signature; guest responsive | **PASS** | -| Partial reference | `/b.dat` Layout 1 reuses `/a.dat` physical extent 8192..12288 at logical 641052..1050624 | PASS | `/b.dat` returns `EIO` | `EIO` at offset 641020 crossing into the reused extent; companion Layout 3 `/a.dat` matches | No error signature; guest responsive | **KERNEL-FAIL** | - -All five fixtures contain the requested layout. No mode is classified as `FIXTURE-NOT-PROVEN` or `NOT RUN`. - -## Integrity evidence - -| Target | Full SHA256 | -|---|---| -| Legacy full reference | `370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52` | -| 64 KiB pcluster, actual and expected | `370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52` | -| Inline ztailpacking, actual and expected | `e2aa4a0a0cbcf422f397c7069a38ae0f073781386958e7db0dfa3ff2ca075513` | -| Packed fragment, actual and expected | `a3a83e5c524b5ed446a06ce78cf407192a0c80119f15d2bc3489a50515eb49e0` | -| Partial-ref `/b.dat` reference | `61b17076c2dfae88da7912d00df534b894cf6d27178863c6d8e91f8617ebb91e` | -| Partial-ref companion `/a.dat`, actual and expected | `370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52` | - -Offset-read evidence: - -| Mode | Actual | Expected | -|---|---|---| -| Legacy full | `f387a3a74488528803454f0c05601fb632d919f1bc5281538f687a3a6231ebea` from partial failed output | `941b6e3cb9a7384428d93ca248eb9630782538e04caba30dbfbe31baacca8f60` | -| 64 KiB pcluster | `b6ebba880cfcc438044f943370b9936a130ce1cecf13d784a1eb871f0a126182` | same | -| Inline ztailpacking | `3a630b7e9618c7e27efb1483058baaa0475ca325c3ecafb42778871d700afa96` | same | -| Packed fragment | `a826cc36ad8c9eaa1606713117fcf118b7bae5fee7503b5619dbf024213059db` | same | -| Partial reference | empty output `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` | `941b6e3cb9a7384428d93ca248eb9630782538e04caba30dbfbe31baacca8f60` | - -## Failure disposition - -The legacy full-index image mounts, but both the complete read and a read crossing its compressed-extent boundary fail with `EIO`. The fixture and its source remain under `/work/build/repo22-shape-manual-20260808T1407Z`; copies plus per-mode dmesg files remain in the guest under `/root/repo22-short-20260808`. - -The partial-reference fixture is proven: `/b.dat` reuses the final physical compressed extent of `/a.dat`. However, `/b.dat` itself uses Layout 1 and fails before partial-reference semantics can be accepted. The same image's Layout 3 `/a.dat` reads correctly. This result is therefore a kernel failure with the legacy full-index path as the immediate blocker, not a fixture failure. - -Each mode used a separate md attachment and mount lifecycle. Cleanup succeeded after every run, the module remained loaded, and the guest answered a responsiveness check after every failure and pass. The isolated dmesg scans contained no panic, trap, explicit integrity, corruption, I/O-error, or decompression-error message; the two read failures surfaced only as user-visible `EIO`. - -## Conclusion - -The 64 KiB big-pcluster, inline ztailpacking, and packed-inode fragment read paths pass the requested kernel verification. Legacy full indexes remain broken. The proven partial-reference fixture also fails because its target file traverses the broken Layout 1 path, so partial-reference support cannot be accepted until that blocker is fixed and the same fixture is rerun. diff --git a/tests/results/manual/2026-08-08T1444Z/manual-test-report.md b/tests/results/manual/2026-08-08T1444Z/manual-test-report.md deleted file mode 100644 index 6bb08b2..0000000 --- a/tests/results/manual/2026-08-08T1444Z/manual-test-report.md +++ /dev/null @@ -1,41 +0,0 @@ -# repo22 legacy full-index fix manual report - -- Date: 2026-08-08 UTC -- Source baseline: `377d467a652beb2124d4adc0d1f8e735fca22344` -- FreeBSD guest: 15.0-RELEASE-p8 amd64, QEMU TCG -- Module SHA256: `f3b9fa70bfb8e98e2cef436583fd13cb91787514a5be76f9f108698d7586ca3c` -- Scope: legacy full-index and partial-reference minimum regression only - -## Root cause and fix - -The proven legacy Layout 1 fixture maps its second extent as `HEAD1` with -`clusterofs=28`, `pblk=2`, `m_la=639004`, `m_llen=409572`, and -`m_plen=4096`. The LZ4 stream completes after consuming 2627 bytes; the -remaining 1469 bytes of the pcluster are zero padding. The repo22 decoder -incorrectly required all 4096 input bytes to be consumed and returned `EIO`. - -The decoder now accepts complete output only when the remaining pcluster bytes -are all zero, while continuing to reject nonzero trailing data. The mapping -recorder also no longer clears `compressedblks` and `partialref` on every -lcluster load, matching the Linux state machine and preserving lookback state. - -## Results - -| Test | Mount | Full SHA256 | Boundary read | Result | -|---|---:|---|---|---| -| Legacy full index `/shape.dat` | PASS | `370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52` matches | offset 638972, 128 bytes: `941b6e3cb9a7384428d93ca248eb9630782538e04caba30dbfbe31baacca8f60` matches | **PASS** | -| Partial reference `/b.dat` | PASS | read returns `EIO` | offset 641020 read returns no data | **KERNEL-FAIL** | - -`./build.sh`, `kldload`, and exact-name `kldunload` passed. The isolated dmesg -delta was empty, with no panic, trap, or integrity diagnostic. - -## Remaining partial-reference hypothesis - -The minimal reproducer is the existing -`lz4-partial-ref.erofs` fixture: mount it and read `/b.dat`; the companion -compact `/a.dat` remains the known-good source extent. The full-index mapping -blocker is removed, but `zdata.c` still invokes `z_erofs_decompress()` with -`partial=false` for every mapped extent even when `EROFS_MAP_PARTIAL_REF` is -set. Linux propagates this state to partial LZ4 decoding. Per the convergence -instruction, this batch records that hypothesis without further debugging or -additional source changes. diff --git a/tests/results/manual/2026-08-08T1455Z/manual-test-report.md b/tests/results/manual/2026-08-08T1455Z/manual-test-report.md deleted file mode 100644 index af8bc81..0000000 --- a/tests/results/manual/2026-08-08T1455Z/manual-test-report.md +++ /dev/null @@ -1,33 +0,0 @@ -# repo22 partial-reference fix manual report - -- Date: 2026-08-08 UTC -- Source baseline: `616d23e59b2bc16d9735ca5ad07502a21153bc0a` -- FreeBSD guest: 15.0-RELEASE-p8 amd64, QEMU TCG -- Module SHA256: `0270671fd73c4ee604294a5f94863f1fc8cc9a63083a4c83f89887891f06f1b2` -- Scope: partial-reference output range and LZ4 regression only - -## Root cause and fix - -`zmap.c` correctly marked the reused extent with `EROFS_MAP_PARTIAL_REF`, but -`zdata.c` always decompressed the complete mapped logical length and passed -`partial=false`. Linux keeps such pclusters in partial-decoding mode and sets -the decompressor output size to the highest byte needed by the current read. - -The synchronous FreeBSD path now preserves that behavior. For a partial -reference, it decodes only through `mapoff + want`, passes `partial=true`, and -copies exactly the requested `want` bytes beginning at `mapoff`. Non-partial -extents still require full logical output and retain strict LZ4 trailing-data -validation. - -## Results - -| Test | Mount | Full SHA256 | Boundary read | Result | -|---|---:|---|---|---| -| Partial reference `/b.dat` | PASS | `61b17076c2dfae88da7912d00df534b894cf6d27178863c6d8e91f8617ebb91e` matches | offset 641020, 128 bytes: `941b6e3cb9a7384428d93ca248eb9630782538e04caba30dbfbe31baacca8f60` matches | **PASS** | -| Legacy full index `/shape.dat` | PASS | `370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52` matches | smoke only | **PASS** | -| Compact index `/shape.dat` | PASS | `370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52` matches | smoke only | **PASS** | - -`./build.sh`, `kldload`, and exact-name `kldunload` passed. The isolated dmesg -delta was empty, with no panic, trap, integrity diagnostic, or decompression -error. Build objects, kernel module output, images, and VM overlays were not -staged for commit. diff --git a/tests/results/manual/2026-08-08T1705Z/manual-test-report.md b/tests/results/manual/2026-08-08T1705Z/manual-test-report.md deleted file mode 100644 index 7ef90a2..0000000 --- a/tests/results/manual/2026-08-08T1705Z/manual-test-report.md +++ /dev/null @@ -1,46 +0,0 @@ -# repo22 ACL/xattr integration smoke report - -- Date: 2026-08-08 UTC -- Source baseline before this batch: `8e629bb9f798f0c76497bd2b623329a0aff430b9` -- FreeBSD guest: 15.0-RELEASE-p8 amd64, QEMU TCG -- Module SHA256: `18967bd32e89b08a2363241ed4b9b67344b7bfb555f1f77daa64f382925e4fda` -- Scope: ACL/xattr worktree integration, build/link validation, and minimal - Markdown-directed manual smoke tests - -## Results - -| Check | Result | Evidence | -|---|---|---| -| `./build.sh` | PASS | `build/erofs.ko` generated without errors | -| unresolved `bcmp` | PASS | `nm -u build/erofs.ko` contains no `bcmp` | -| ACL kernel dependency | PASS | module declares `acl_posix1e`; `kldload` succeeds | -| module lifecycle | PASS | `kldload`, `kldstat`, and exact module-ID `kldunload` succeed | -| TC005 user xattr smoke | PASS | `user.comment` lists and reads as `repo22-user-xattr` | -| TC082 access ACL smoke | PASS | system xattr lists `posix_acl_access`; raw ACL reads successfully | -| FreeBSD ACL decode | PASS | `getfacl` reports named user, group, mask, and other entries | -| TC060 ACL capability | PASS | `getconf ACL_EXTENDED` returns `1` | -| read-only access smoke | PASS | read succeeds and write access is denied | -| cleanup | PASS | no EROFS module, mount, or md device remains | - -The ACL fixture contained a real extended POSIX.1e access ACL with a named -user and mask, so Linux did not collapse it into mode bits. `dump.erofs` -reported 88 bytes of inode xattrs. FreeBSD returned: - -```text -user::rw- -user:ntpd:r-- -group::r-- -mask::r-- -other::--- -``` - -The fixture image, source directory, kernel module, VM overlay, and generated -objects remained under `/work/build` or `repo22/build` and were not staged. - -## Remaining scope - -- Default-directory ACL retrieval and access checks under multiple credentials - were not exercised in this smoke batch. -- Shared-xattr base calculation, long/packed prefixes, xattr filters, malformed - xattr bounds, and metabox-backed shared xattrs still require dedicated tests. -- Multi-device work is outside this ACL/xattr integration batch. diff --git a/tests/results/manual/2026-08-08T1800Z-namei/manual-test-report.md b/tests/results/manual/2026-08-08T1800Z-namei/manual-test-report.md deleted file mode 100644 index af74156..0000000 --- a/tests/results/manual/2026-08-08T1800Z-namei/manual-test-report.md +++ /dev/null @@ -1,112 +0,0 @@ -# repo22 Cold Nested Namei Regression Report - -- Date: 2026-08-08 UTC -- Baseline: `29a215dd4519579f6313b3df67d524a6b4bdf3ca` -- Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG -- Final module SHA-256: - `50a19ea96c7414f73d92049671e66a9eacc9ff5abe27950a46d16c8e6c763baf` -- Result: PASS - -## Root Cause - -FreeBSD pathname lookup passes a component as `cn_nameptr` plus -`cn_namelen`. An intermediate component is followed by `/` in the pathname -buffer and is not NUL-terminated at `cn_namelen`. The imported Linux EROFS -comparison assumed Linux dentry-name termination, ignored the supplied length, -and tested `qn_name[i] == '\0'` after matching the on-disk name. Therefore a -final component worked, while the same name used as an intermediate component -compared greater than the on-disk entry and returned `ENOENT`. Looking up the -parent as a final component first populated the FreeBSD namecache and hid the -bug on the next nested lookup. - -The old error path also inserted a negative cache entry for every lookup -error, including integrity and I/O errors, which could mask later corruption -as `ENOENT`. - -## Implementation - -- `src/namei.c` - - Compares pathname components by explicit length without reading beyond - `cn_namelen`. - - Uses unsigned-byte ordering compatible with EROFS directory sorting. - - Validates the minimum block size before reading the first dirent. - - Validates the dirent-array boundary, strictly increasing name offsets, - name-slot bounds, name length, and zero-only NUL padding. - - Inserts negative namecache entries only for real `ENOENT` misses. -- `src/dir.c` - - Applies the same directory-block and name-padding validation to `readdir`. - - Determines the actual last-name length before enforcing `EROFS_NAME_LEN`, - so valid full-block zero padding is accepted. - - Keeps on-disk offsets unchanged and appends a synthetic `.` at `i_size` - for `dot_omitted`, matching Linux EROFS. - - Aligns restart positions relative to each directory block and preserves - the `i_size` cookie needed to resume the synthetic dot entry. - - Initializes returned cookie-array outputs before allocation. - -## Deterministic Fixture - -`prepare-fixtures.sh` creates the same image twice and requires `cmp` success. -The base image contains a cold multi-level path and a 320-file, multi-block -directory. Fixed timestamp, ownership, UUID, worker count, and uncompressed -layout are used. - -Final image hashes: - -```text -2e9fd75159011ced31646a38f942fa0822d5b3e8e19b7df55b844575fba991ea corrupt-nameoff.erofs -63d12232f9ea0dc7f7ff477d20b95a8412d191fc10bf4b67dacc47e050c379fb corrupt-padding.erofs -77f8a56f969e173d291ccbd957821f1ba284d3e54d875dbcdb9bc398024dfa5a corrupt-short-block.erofs -11064ffbb814ff41027aebc8f36e56d2f24affb7b50836948b3ffbf6d79dee0e dot-omitted.erofs -7a2c244ec10e9a43531b0e8b92e26b11f52d67bab00528b8ba2f584e20e57420 nested-repeat.erofs -7a2c244ec10e9a43531b0e8b92e26b11f52d67bab00528b8ba2f584e20e57420 nested.erofs -``` - -## Build Results - -- `./build.sh`: PASS. -- Final `build/erofs.ko` SHA-256 remained - `50a19ea96c7414f73d92049671e66a9eacc9ff5abe27950a46d16c8e6c763baf`. -- `nm -u build/erofs.ko | grep -w bcmp`: no match. -- `git diff --check` for all scoped source and test files: PASS. - -The final integration rerun used the same combined module and these guest -commands: - -```sh -cd /root/repo22-namei -cc -Wall -Wextra -O2 -o readdir_probe readdir_probe.c -./vm-regression.sh -``` - -`vm-regression.sh` performs `kldload`, creates each vnode-backed md device, -mounts it with `mount -t erofs`, executes the cold lookup and readdir probes, -unmounts and detaches each image, and finishes with exact module unload. - -## FreeBSD VM Results - -- `kldload`: PASS. -- Cold direct read of - `/alpha/bravo/charlie/payload.txt` without parent lookup or `readdir`: PASS. -- Repeated lookup and sibling nested lookup: PASS. -- Two negative lookups followed by an existing nested lookup: PASS. -- Multi-block `wide` readdir: 322 dirents (`.`, `..`, 320 files), PASS. -- Resume from every one of the 322 returned `d_off` cookies: PASS. -- `dot_omitted` root cookies: `12`, `24`, `47`, `48`; resume at `47` - returns only `.`, and resume at `48` returns EOF: PASS. -- Short directory block: two lookups and direct `getdirentries` all return - `EINTEGRITY`, PASS. -- Non-monotonic `nameoff`: two lookups and direct `getdirentries` all return - `EINTEGRITY`, PASS. -- Nonzero data after NUL padding: two lookups and direct `getdirentries` all - return `EINTEGRITY`, PASS. -- `kldunload`: PASS. -- Post-test EROFS module, mount, and md-device state: clean. -- Final explicit state check: zero EROFS modules, zero EROFS mounts, zero md - devices, and no recent panic or fatal trap in dmesg. - -## Remaining Scope - -No unresolved issue remains for the requested cold lookup and directory -regression. The NFS-specific `a_cookies` consumer path was not exercised by an -NFS export; the tested `d_off` restart-cookie sequence uses the same generated -cookie values. diff --git a/tests/results/manual/2026-08-08T1800Z-namei/prepare-fixtures.sh b/tests/results/manual/2026-08-08T1800Z-namei/prepare-fixtures.sh deleted file mode 100755 index 06f434c..0000000 --- a/tests/results/manual/2026-08-08T1800Z-namei/prepare-fixtures.sh +++ /dev/null @@ -1,199 +0,0 @@ -#!/bin/sh -set -eu - -script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) -fixture_dir="$script_dir/fixture" -artifact_dir="$script_dir/artifacts" - -mkdir -p \ - "$fixture_dir/alpha/bravo/charlie" \ - "$fixture_dir/alpha/sibling" \ - "$fixture_dir/wide" \ - "$artifact_dir" - -printf '%s\n' 'cold nested lookup payload' > \ - "$fixture_dir/alpha/bravo/charlie/payload.txt" -printf '%s\n' 'repeat lookup payload' > \ - "$fixture_dir/alpha/bravo/repeat.txt" -printf '%s\n' 'sibling marker' > \ - "$fixture_dir/alpha/sibling/marker.txt" - -index=0 -while [ "$index" -lt 320 ]; do - name=$(printf 'entry-%03d-abcdefghijklmnopqrstuvwxyz.txt' "$index") - printf 'wide entry %03d\n' "$index" > "$fixture_dir/wide/$name" - index=$((index + 1)) -done - -find "$fixture_dir" -exec touch -h -t 197001010000.00 {} + - -build_image() -{ - image=$1 - mkfs.erofs -d0 -x-1 -T0 --all-time --all-root --workers=1 \ - -U 11111111-2222-3333-4444-555555555555 \ - "$image" "$fixture_dir" -} - -build_image "$artifact_dir/nested.erofs" -build_image "$artifact_dir/nested-repeat.erofs" -cmp "$artifact_dir/nested.erofs" "$artifact_dir/nested-repeat.erofs" - -ARTIFACT_DIR="$artifact_dir" python3 <<'PY' -import hashlib -import os -import shutil -import struct - -artifact_dir = os.environ["ARTIFACT_DIR"] -base_path = os.path.join(artifact_dir, "nested.erofs") - - -def u16(image, offset): - return struct.unpack_from("> 1) ^ (polynomial if checksum & 1 else 0) - put_u32(image, 1024 + 4, checksum & 0xFFFFFFFF) - - -def inode_offset(nid): - return nid << 5 - - -def compact_inline_dir(image, nid): - offset = inode_offset(nid) - inode_format = u16(image, offset) - layout = (inode_format >> 1) & 0x7 - assert (inode_format & 0x1) == 0 - assert layout == 2 - assert u16(image, offset + 2) == 0 - return offset, offset + 32, u32(image, offset + 8) - - -def dir_entries(image, data_offset, size): - first_nameoff = u16(image, data_offset + 8) - assert first_nameoff >= 12 - assert first_nameoff % 12 == 0 - assert first_nameoff < size - count = first_nameoff // 12 - entries = [] - for index in range(count): - entry_offset = data_offset + index * 12 - nid, nameoff, file_type, reserved = struct.unpack_from( - "> 1) & 0x7) == 2 -wide_startblk = u32(base, wide_inode + 16) -wide_block = wide_startblk << 12 -wide_first_nameoff = u16(base, wide_block + 8) -wide_count = wide_first_nameoff // 12 -wide_last_nameoff = u16(base, wide_block + (wide_count - 1) * 12 + 8) -padding_nul = base.index(0, wide_block + wide_last_nameoff, wide_block + 4096) -assert padding_nul + 1 < wide_block + 4096 -assert base[padding_nul + 1] == 0 -bad_padding = bytearray(base) -bad_padding[padding_nul + 1] = ord("X") - -outputs = { - "dot-omitted.erofs": dot_omitted, - "corrupt-short-block.erofs": short_block, - "corrupt-nameoff.erofs": nonmonotonic, - "corrupt-padding.erofs": bad_padding, -} -for filename, image in outputs.items(): - update_superblock_checksum(image) - path = os.path.join(artifact_dir, filename) - with open(path, "wb") as output: - output.write(image) - -with open(os.path.join(artifact_dir, "SHA256SUMS"), "w", encoding="ascii") as sums: - for filename in sorted(["nested.erofs", "nested-repeat.erofs", *outputs]): - path = os.path.join(artifact_dir, filename) - with open(path, "rb") as image_file: - digest = hashlib.sha256(image_file.read()).hexdigest() - sums.write(f"{digest} {filename}\n") -PY - -cat "$artifact_dir/SHA256SUMS" diff --git a/tests/results/manual/2026-08-08T1800Z-namei/readdir_probe.c b/tests/results/manual/2026-08-08T1800Z-namei/readdir_probe.c deleted file mode 100644 index 0b0a0ee..0000000 --- a/tests/results/manual/2026-08-08T1800Z-namei/readdir_probe.c +++ /dev/null @@ -1,71 +0,0 @@ -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -int -main(int argc, char **argv) -{ - struct dirent *entry; - off_t base, before, start; - char *buffer; - char *end; - size_t buffer_size; - ssize_t bytes; - int calls, fd; - - if (argc < 2 || argc > 5) - errx(2, "usage: %s directory [offset [buffer-size [calls]]]", - argv[0]); - start = argc >= 3 ? strtoll(argv[2], NULL, 0) : 0; - buffer_size = argc >= 4 ? strtoul(argv[3], NULL, 0) : 128; - calls = argc >= 5 ? strtol(argv[4], NULL, 0) : 32; - if (buffer_size < 32 || calls < 1) - errx(2, "invalid buffer size or call count"); - - fd = open(argv[1], O_RDONLY | O_DIRECTORY); - if (fd < 0) - err(1, "open %s", argv[1]); - if (lseek(fd, start, SEEK_SET) < 0) - err(1, "lseek %jd", (intmax_t)start); - buffer = malloc(buffer_size); - if (buffer == NULL) - err(1, "malloc"); - - for (int call = 0; call < calls; call++) { - before = lseek(fd, 0, SEEK_CUR); - if (before < 0) - err(1, "lseek current"); - base = -1; - bytes = getdirentries(fd, buffer, buffer_size, &base); - if (bytes < 0) - err(1, "getdirentries"); - printf("call=%d before=%jd after=%jd base=%jd bytes=%zd\n", - call, (intmax_t)before, - (intmax_t)lseek(fd, 0, SEEK_CUR), (intmax_t)base, bytes); - if (bytes == 0) - break; - end = buffer + bytes; - for (entry = (struct dirent *)buffer; - (char *)entry < end; - entry = (struct dirent *)((char *)entry + entry->d_reclen)) { - if (entry->d_reclen == 0 || - (char *)entry + entry->d_reclen > end) - errx(1, "invalid dirent record"); - printf(" off=%jd ino=%ju reclen=%u type=%u name=%.*s\n", - (intmax_t)entry->d_off, (uintmax_t)entry->d_fileno, - entry->d_reclen, entry->d_type, entry->d_namlen, - entry->d_name); - } - } - - free(buffer); - close(fd); - return (0); -} diff --git a/tests/results/manual/2026-08-08T1800Z-namei/vm-regression.sh b/tests/results/manual/2026-08-08T1800Z-namei/vm-regression.sh deleted file mode 100755 index 97f0de4..0000000 --- a/tests/results/manual/2026-08-08T1800Z-namei/vm-regression.sh +++ /dev/null @@ -1,201 +0,0 @@ -#!/bin/sh -set -u - -test_dir=/root/repo22-namei -mount_dir=/mnt/repo22-namei -module_id= -md_device= - -fail() -{ - echo "FAIL: $*" >&2 - exit 1 -} - -cleanup() -{ - set +e - if mount | grep -q " on $mount_dir "; then - umount "$mount_dir" - fi - if [ -n "$md_device" ]; then - mdconfig -d -u "${md_device#md}" - fi - if [ -n "$module_id" ] && kldstat -q -i "$module_id"; then - kldunload -i "$module_id" - fi -} -trap cleanup EXIT INT TERM - -mount_image() -{ - image=$1 - md_device=$(mdconfig -a -t vnode -f "$test_dir/$image") || - fail "mdconfig $image" - mount -t erofs "/dev/$md_device" "$mount_dir" || - fail "mount $image" - echo "mounted image=$image device=$md_device" -} - -unmount_image() -{ - umount "$mount_dir" || fail "umount $md_device" - mdconfig -d -u "${md_device#md}" || fail "detach $md_device" - md_device= -} - -expect_integrity_failure() -{ - image=$1 - lookup_path=$2 - readdir_path=$3 - label=${image%.erofs} - - mount_image "$image" - attempt=1 - while [ "$attempt" -le 2 ]; do - output="$test_dir/$label-lookup-$attempt.txt" - if stat "$mount_dir/$lookup_path" >"$output" 2>&1; then - fail "$image lookup attempt $attempt unexpectedly succeeded" - fi - if grep -qi "No such file" "$output"; then - fail "$image lookup attempt $attempt became ENOENT" - fi - attempt=$((attempt + 1)) - done - output="$test_dir/$label-readdir.txt" - if ./readdir_probe "$mount_dir/$readdir_path" 0 512 2 \ - >"$output" 2>&1; then - fail "$image readdir unexpectedly succeeded" - fi - unmount_image - echo "integrity image=$image repeated-lookup=error readdir=error" -} - -cd "$test_dir" || exit 1 -mkdir -p "$mount_dir" - -if mount | grep -qi erofs; then - fail "pre-existing EROFS mount" -fi -if kldstat | grep -qi erofs; then - fail "pre-existing EROFS module" -fi -if [ -n "$(mdconfig -l)" ]; then - fail "pre-existing md device" -fi - -echo "== guest ==" -uname -a -date -u - -echo "== module load ==" -kldload "$test_dir/erofs.ko" || fail "kldload" -module_id=$(kldstat | awk '$NF == "erofs.ko" { print $1 }') -[ -n "$module_id" ] || fail "loaded module not found" -kldstat -v -i "$module_id" - -echo "== cold nested lookup ==" -mount_image nested.erofs -payload=$(cat "$mount_dir/alpha/bravo/charlie/payload.txt") || - fail "cold nested lookup" -[ "$payload" = "cold nested lookup payload" ] || fail "cold payload mismatch" -payload=$(cat "$mount_dir/alpha/bravo/charlie/payload.txt") || - fail "repeated nested lookup" -[ "$payload" = "cold nested lookup payload" ] || fail "repeat payload mismatch" -payload=$(cat "$mount_dir/alpha/bravo/repeat.txt") || - fail "sibling nested lookup" -[ "$payload" = "repeat lookup payload" ] || fail "sibling payload mismatch" - -attempt=1 -while [ "$attempt" -le 2 ]; do - output="$test_dir/missing-$attempt.txt" - if stat "$mount_dir/alpha/bravo/missing" >"$output" 2>&1; then - fail "missing lookup attempt $attempt unexpectedly succeeded" - fi - grep -qi "No such file" "$output" || fail "missing lookup was not ENOENT" - attempt=$((attempt + 1)) -done -payload=$(cat "$mount_dir/alpha/bravo/charlie/payload.txt") || - fail "existing lookup after negative cache" -[ "$payload" = "cold nested lookup payload" ] || fail "post-negative payload mismatch" -echo "cold lookup=pass repeat=pass negative-cache=pass" - -echo "== large readdir and cookies ==" -wide_count=$(ls -A1 "$mount_dir/wide" | wc -l | tr -d ' ') -[ "$wide_count" = 320 ] || fail "wide entry count $wide_count" -./readdir_probe "$mount_dir/wide" 0 128 400 > wide-probe.txt || - fail "wide readdir probe" -awk '/^ off=/ { - off = $1; sub(/^off=/, "", off); - name = $5; sub(/^name=/, "", name); - print off, name; -}' wide-probe.txt > wide-cookies.txt -wide_dirents=$(wc -l < wide-cookies.txt | tr -d ' ') -[ "$wide_dirents" = 322 ] || fail "wide dirent count $wide_dirents" -awk '{ cookie[NR] = $1; name[NR] = $2 } - END { - for (i = 1; i <= NR; i++) - print cookie[i], (i < NR ? name[i + 1] : ""); - }' wide-cookies.txt > wide-resume-cases.txt -while read -r cookie expected; do - ./readdir_probe "$mount_dir/wide" "$cookie" 128 1 > wide-resume.txt || - fail "resume cookie $cookie" - if [ "$expected" = "" ]; then - grep -q 'bytes=0$' wide-resume.txt || fail "cookie $cookie not EOF" - else - actual=$(awk '/^ off=/ { - name = $5; sub(/^name=/, "", name); print name; exit; - }' wide-resume.txt) - [ "$actual" = "$expected" ] || - fail "cookie $cookie expected $expected got $actual" - fi -done < wide-resume-cases.txt -echo "wide entries=$wide_dirents all-resume-cookies=pass" -unmount_image - -echo "== dot omitted cookies ==" -mount_image dot-omitted.erofs -./readdir_probe "$mount_dir" 0 512 8 > dot-probe.txt || fail "dot probe" -awk '/^ off=/ { - off = $1; sub(/^off=/, "", off); - name = $5; sub(/^name=/, "", name); - print off, name; -}' dot-probe.txt > dot-cookies.txt -cat > dot-expected.txt <<'EOF' -12 .. -24 alpha -47 wide -48 . -EOF -cmp dot-cookies.txt dot-expected.txt || fail "dot cookie sequence" -./readdir_probe "$mount_dir" 47 128 1 > dot-resume-47.txt || fail "dot resume 47" -grep -q '^ off=48 .* name=\.$' dot-resume-47.txt || fail "dot resume 47 result" -./readdir_probe "$mount_dir" 48 128 1 > dot-resume-48.txt || fail "dot resume 48" -grep -q 'bytes=0$' dot-resume-48.txt || fail "dot resume 48 not EOF" -echo "dot cookies=12,24,47,48 resume=pass" -unmount_image - -echo "== corrupted directories ==" -expect_integrity_failure corrupt-short-block.erofs alpha . -expect_integrity_failure corrupt-nameoff.erofs alpha . -expect_integrity_failure \ - corrupt-padding.erofs \ - wide/entry-000-abcdefghijklmnopqrstuvwxyz.txt \ - wide - -echo "== module unload ==" -kldunload -i "$module_id" || fail "kldunload" -module_id= -if kldstat | grep -qi erofs; then - fail "module remains loaded" -fi -if mount | grep -qi erofs; then - fail "mount remains" -fi -if [ -n "$(mdconfig -l)" ]; then - fail "md remains" -fi - -dmesg | tail -120 > dmesg-tail.txt -echo "PASS: all VM regressions" diff --git a/tests/results/manual/2026-08-08T1812Z/manual-test-report.md b/tests/results/manual/2026-08-08T1812Z/manual-test-report.md deleted file mode 100644 index a4bb7fc..0000000 --- a/tests/results/manual/2026-08-08T1812Z/manual-test-report.md +++ /dev/null @@ -1,113 +0,0 @@ -# repo22 xattr/ACL root-cause manual test report - -Date: 2026-08-08 18:12 UTC; final integration rerun on 2026-08-08 -Baseline: `29a215dd4519579f6313b3df67d524a6b4bdf3ca` plus the scoped xattr/ACL fixes -Guest: FreeBSD 15.0-RELEASE-p8 amd64 -Host tools: erofs-utils 1.8.6 plus deterministic transformed fixtures - -## Build and module - -- `git diff --check`: PASS. -- `./build.sh`: PASS; final `build/erofs.ko` SHA-256 is - `50a19ea96c7414f73d92049671e66a9eacc9ff5abe27950a46d16c8e6c763baf`. -- `nm -u build/erofs.ko | grep -w bcmp`: no match, PASS. -- `kldload /tmp/repo22-integration-20260808/erofs.ko`: PASS. -- Repeated `mdconfig -a -t vnode -f IMAGE`, `mount -t erofs`, `umount`, and - `mdconfig -d`: PASS for every fixture below. -- `kldunload erofs`: PASS; final module, EROFS mount, and md-device counts were - all zero. -- New dmesg errors, traps, or panics: none. - -## Existing xattr and ACL regression - -- Inline user, trusted, and security xattrs: PASS. -- Shared user xattrs and shared POSIX access ACL: PASS. -- Packed long-prefix table and xattr-name-filter: PASS. -- Access/default ACL decode and inherited child ACL: PASS. -- Owner, owning-group, named-user, and other access matrix: PASS. -- Read-only ACL/xattr mutation rejection: PASS. -- TC117 malformed name length and value size: PASS. Xattr operations returned - `EINTEGRITY`; directory metadata and file contents remained readable. -- An invalid long-prefix reference without a declared prefix table is now - skipped like Linux. Other valid xattrs and file data remain readable. - -## New regression cases - -- TC134 metabox shared nonzero base: PASS with - `metabox-shared-nonzero-base.erofs`. The image has - `SHARED_EA_IN_METABOX`, `xattr_blkaddr=1`, and a plain metabox carrier. - `getextattr -qq user metaboxshared /mnt/repo22-integration/hello.txt` - returned exactly `nonzero-base`. -- TC135 metabox long-prefix backing: PASS with - `metabox-prefix-shared.erofs`. - `getextattr -qq user repo22.application.component.setting` on - `/mnt/repo22-integration/long/file2.txt` returned `long-prefix-value`. - Primary-image long-prefix fallback also passed with - `long-prefix-primary-fallback.erofs` and the same expected value. -- TC136 truncated metabox extension: PASS; mount returned `EINTEGRITY`. - Out-of-range ishare prefix ID: PASS; mount returned `EINTEGRITY`. -- TC137 shared xattr outside declared image: PASS; normal data and an in-bounds - xattr remained readable, while the redirected entry returned `EINTEGRITY`. - Prefix record outside declared image: PASS; mount failed. The valid primary - fallback control mounted and returned the expected value. -- TC138 unordered unique UID qualifiers: PASS and preserved order 3002, 2002. - Header-only ACL: PASS and fell back to mode. Duplicate UID qualifier: PASS - negative test and returned `EINTEGRITY`. -- TC139 FIFO access ACL, list/get system xattr: PASS. `setfacl`, `setextattr`, - and `rmextattr` all returned read-only filesystem errors. -- TC140 compressed metabox: PASS with `metabox-large-shared.erofs`. On-disk - qualification found `METABOX`, metabox inode datalayout 1 - (`EROFS_INODE_COMPRESSED_FULL`), and no fragment pcluster. A cold file read, - the metabox long-prefix lookup, and a 231-byte xattr value all matched the - source fixture. - -## Final metabox and metadata-boundary smoke - -The final combined module was exercised with these exact guest operations: - -```sh -kldload /tmp/repo22-integration-20260808/erofs.ko -mdconfig -a -t vnode -f /tmp/repo22-integration-20260808/IMAGE.erofs -mount -t erofs /dev/md0 /mnt/repo22-integration -getextattr -qq user NAME /mnt/repo22-integration/PATH -umount /mnt/repo22-integration -mdconfig -d -u 0 -kldunload erofs -``` - -- Compressed metabox and metabox long-prefix: - `metabox-large-shared.erofs` returned `long-prefix-value` for - `repo22.application.component.setting`; its regular file content also - matched `long prefix fixture 1`. -- Primary shared fallback while `METABOX` is enabled: - `metabox-large-shared.erofs` has `SHARED_EA_IN_METABOX` clear and - `xattr_blkaddr=1`; `shared_key` returned `repo22-shared-value` from the - primary shared-xattr area. -- `SHARED_EA_IN_METABOX` nonzero base: - `metabox-shared-nonzero-base.erofs` returned `nonzero-base` from the shared - entry addressed relative to metabox block 1. -- Cross-metadata shared xattr: `shared-cross-metadata.erofs` returned - `repo22-shared-value` for `shared_key` on `shared/file3.txt`. -- Cross-metadata inline xattr: `xattr-cross-metadata.erofs` returned the exact - expected `alpha`, `gamma`, and `delta` values on `file000`, `file090`, and - `file179`. The metabox-specific inline boundary fixture also returned - `valid-inline-boundary` for `crossboundary`. - -The fragment-backed compressed metabox negative layout was **not generated and -was not executed**. The passing TC140 fixture is compressed but non-fragment. -The code still rejects a compressed metabox inode whose fragment flag is set; -that rejection remains layout-reviewed rather than fixture-verified. - -## Fixture qualification - -The old `namespace-shared.erofs` contains an xattr entry with name index `0x80` -but advertises no `XATTR_PREFIXES` feature and contains no prefix table. It is -not a valid long-prefix fixture. The driver now follows Linux behavior by -skipping that unresolved entry while preserving all valid xattrs and file data. - -## Separate namei write set - -The previously observed cold nested lookup issue was fixed by the separate -`src/namei.c` and `src/dir.c` write set and passed TC141 in the same final -integration build. Those source files are intentionally excluded from the -xattr/ACL commit. diff --git a/tests/results/manual/2026-08-08T1937Z-multidev/manual-test-report.md b/tests/results/manual/2026-08-08T1937Z-multidev/manual-test-report.md deleted file mode 100644 index 3725489..0000000 --- a/tests/results/manual/2026-08-08T1937Z-multidev/manual-test-report.md +++ /dev/null @@ -1,232 +0,0 @@ -# repo22 real multi-device manual test report - -Date: 2026-08-08 19:37 UTC -Baseline: `96e22cc713cc57180ce3ecb2b98852f090e3868b` plus this multi-device batch -Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG -Host tools: erofs-utils 1.8.6 - -## Result - -**PASS**. Real external providers, explicit slot mapping, flatdev, unified -addresses, 48-bit fields, failure rollback, forced GEOM orphaning, and the -single-device regressions all passed. There were no new dmesg lines, traps, or -panics. Final EROFS mount, md-provider, and loaded-module counts were zero. - -## Build and module - -Commands: - -```sh -git diff --check -./build.sh -nm -u build/erofs.ko | awk '$NF == "bcmp" {bad=1} END {exit bad}' -sha256sum build/erofs.ko -``` - -Actual: - -- `git diff --check`: PASS. -- Cross-build against `/work/dev-freebsd-releng`: PASS. -- Unresolved `bcmp`: none, PASS. -- Final `build/erofs.ko` SHA256: - `aa6708256246b303f4be2a8d516bf503f8a232df1ad3f466f5685573f221c180`. -- Final `kldload` and `kldunload`: PASS. - -## Fixture commands - -The legal baseline was generated by erofs-utils 1.8.6, not by synthesizing an -EROFS image from scratch: - -```sh -WORK=/work/build/repo22-multidev-fixtures-20260808 -mkdir -p "$WORK/src/cross" -python3 - <<'PY' -from pathlib import Path -import hashlib -root = Path('/work/build/repo22-multidev-fixtures-20260808/src') -def content(label, blocks, tail=0): - out = bytearray() - for block in range(blocks): - seed = f'{label}:block:{block}'.encode() - chunk = bytearray() - counter = 0 - while len(chunk) < 4096: - chunk += hashlib.sha256( - seed + counter.to_bytes(4, 'little')).digest() - counter += 1 - out += chunk[:4096] - if tail: - out += hashlib.sha256(f'{label}:tail'.encode()).digest()[:tail] - return bytes(out) -root.joinpath('cross/striped.bin').write_bytes(content('striped', 24, 173)) -root.joinpath('cross/second.bin').write_bytes(content('second', 17, 29)) -root.joinpath('control.txt').write_text( - 'repo22 multidev deterministic control\n', encoding='ascii') -PY -truncate -s 0 "$WORK/base.blob" -mkfs.erofs -T0 --all-root --chunksize=4096 \ - --blobdev="$WORK/base.blob" "$WORK/base.primary" "$WORK/src" -``` - -erofs-utils 1.8.6 accepts one `--blobdev`; direct generation of two or more -external slots is therefore **MKFS-UNAVAILABLE**. Multi-slot fixtures were made -by a structured, assertion-driven patch of the legal baseline: - -1. `dump.erofs --path=PATH -e base.primary` supplied each NID and file size. -2. The patch asserted compact chunk layout, zero xattr size, index format, old - device ID 1, and every old block address. -3. A complete physical chunk, rounded to 4096 bytes for the final partial - chunk, was copied into a deterministic round-robin blob. -4. Each index was rewritten as little-endian - `(startblk_hi, device_id, startblk_lo)`. -5. One zeroed primary block was appended for the enlarged device table; - `blocks_lo`, `extra_devices`, and `devt_slotoff` were updated. This avoids - overwriting inode metadata merely to add slots. -6. Each 128-byte slot was written at offsets 64/68/72/74 with `blocks_lo`, - `uniaddr_lo`, `blocks_hi`, and `uniaddr_hi`. -7. The EROFS CRC32C was recomputed over bytes 1024 through 4095 with polynomial - `0x82f63b78` and initial value `0xffffffff`. - -The asserted patch manifest was: - -```text -/control.txt nid=44 size=38 chunk=4096 indexes=1 base=1440 -/cross/second.bin nid=50 size=69661 chunk=4096 indexes=18 base=1632 -/cross/striped.bin nid=56 size=98336 chunk=32768 indexes=4 base=1824 -``` - -Pre-kernel qualification: - -```sh -fsck.erofs --device=two.blob1 --device=two.blob2 \ - --extract=two.extract two.primary -fsck.erofs --device=three.blob1 --device=three.blob2 \ - --device=three.blob3 --extract=three.extract three.primary -fsck.erofs --device=four.blob1 --device=four.blob2 \ - --device=four.blob3 --device=four.blob4 \ - --extract=four.extract four.primary -``` - -All extracted source SHA256 values matched. The 48-bit feature and patched -`device_id=0 + uniaddr` extraction are not understood by erofs-utils 1.8.6; -those layouts were qualified by the FreeBSD kernel SHA256 tests below and the -Linux 7.1 `erofs_map_dev` semantics. - -FreeBSD attachment and mount commands used one md provider per image/blob: - -```sh -mdconfig -a -t vnode -f primary -u 90 -mdconfig -a -t vnode -f blob1 -u 91 -mdconfig -a -t vnode -f blob2 -u 92 -mount -t erofs -o ro -o device.2=/dev/md92 \ - -o device.1=/dev/md91 /dev/md90 /mnt/repo22-multidev -``` - -The reversed option order was intentional. Flatdev used only: - -```sh -mdconfig -a -t vnode -f two.flat -u 90 -mount -t erofs -o ro /dev/md90 /mnt/repo22-multidev -``` - -## Fixture SHA256 - -| Fixture | SHA256 | -|---|---| -| one-blob primary | `fa2f35f59f63cc4a56d230ba960a6c1d27748506fccce081447cab0dc0d540fd` | -| one-blob slot 1 | `3aeb19c519636b61dc3ebd47c6286098deaa0990ff618ce3facbff4fd65907cd` | -| two-slot primary | `46d76d5cc311f97f263ea4c20a2510338f5b11da9d71e0c73f69ae3751afd696` | -| two-slot blob 1 | `53853d033adf154a956192d9813726888c3f9575ecd13fa45a26aea5c4f07eaf` | -| two-slot blob 2 | `9ee3e3ad420024ff1945d92469d3d337817977f28c5b745c4ca7b1664f4d6584` | -| two-slot flatdev | `4b561db1eb0048989256f3c51504b4611881069bf283fbfe2051847e8162301e` | -| three-slot primary | `4f2dc4ee6385f31139151e1b04279a8fb90fcd2cbba928ee6949df62f1bb9585` | -| three-slot blobs 1/2/3 | `12c5d73cb12e1bb5fd1a9d9715ec88d2132dbaa8df62e2b5aad3cfb454116a3a`, `3d1574254a97e572fca323f153bb2a46ebdfb060a0765298700768e39a7f91d8`, `ba42a2a3724771975c27a43ac8d64a2568515fb4e8ad14b29cc09a7a865ffba0` | -| four-slot primary | `d3366dfb5f4c98db2db669ead3209a0abbba5df989d9aea7e1217aff46f0154b` | -| four-slot blobs 1/2/3/4 | `ad24e257baf97f855f756cc917878a9616858860ddbfab4ffe3cc362e9233662`, `eb55bd05f873dd8732bcbe35a0818ef232a9eff6fedec618f5f949253d7cd2d1`, `59f9a3aed34cc8598e6a96381b55a18f6a0e6674e3e26921834948ad333cb3c5`, `d5080743a85e47752cd33e86e26690b8978d314e685b27d449d5394c44e54244` | -| device ID 0 + uniaddr | `e4cf71f76f2a2e690e905e4135c21ce29d647be5dd2b6ce567ac19f82d1875d2` | -| 48-bit high uniaddr | `00d49beea1237125886f13912bf062153b992582c1cf3a2494b44679285b192e` | -| overlapping-slot negative | `6ecfc77b688ac1931f3e904f48fa43a81c05a6047c21ccfc4636e9e07c17d5cf` | -| mapped-ID-3 negative | `dd6ddc6943b7edd2009a48999d03a2d5b69a9ce7d820357527dbcdb52adb2afc` | - -Source SHA256 values for the cross-device fixture: - -```text -6a38f93ca44de4affcef23ad5979505225f21e5b6912b5c886866170c007b16c ./control.txt -45b7df16a5833013042694c2e8171d782102bf20abbc6c1ec2789518d5a65125 ./cross/second.bin -7ab83ca35cc4bb361bf60b5619d3f55b717fb8e2a7a81a2f069dce859efe8de9 ./cross/striped.bin -``` - -## Test matrix - -| Test | Expected | Actual | Status | -|---|---|---|---| -| TC093 plain single device | full SHA256 match | both files matched | PASS | -| TC093 chunk single device | device ID 0 reads primary | both files matched | PASS | -| compression regression | LZ4 data unchanged | both files matched | PASS | -| TC098 fragments | packed-inode file matches | `65b9a59e...e7cd` | PASS | -| metabox regression | cold metabox-backed files match | `f1` and `f10` matched known hashes | PASS | -| TC006/TC099 primary + one blob | all external chunks use slot 1 | three files matched | PASS | -| TC094 two external slots | reversed option order, cross-slot SHA256 | all three files matched | PASS | -| TC100 four providers total | slots 1-3, order 3/1/2 | all three files matched | PASS | -| additional four external slots | file crosses four blobs | SHA256 matched | PASS | -| statfs combined blocks | sum primary and external blocks | 184 KiB reported for 46 blocks | PASS | -| TC101 flatdev | non-zero IDs add `uniaddr` on primary | SHA256 matched | PASS | -| TC101 device ID 0 + uniaddr | unified range selects external provider | SHA256 matched | PASS | -| 48-bit root/index/uniaddr | high fields are not truncated | source SHA256 matched | PASS | - -## Error and rollback matrix - -| Case | Expected | Actual | Status | -|---|---|---|---| -| split primary, no external options | `ENXIO` flatdev media bound | `Device not configured` | PASS | -| missing slot 2 | `ENXIO` | `external devices don't match ... Device not configured` | PASS | -| short slot 2 | `ENXIO` | `Device not configured` | PASS | -| swapped short/long providers | fail before root | `Device not configured` | PASS | -| same provider in two slots | `EINVAL` | `Invalid argument` | PASS | -| overlapping unified ranges | `EINTEGRITY` | `Integrity check failed` | PASS | -| explicit writable request | `EROFS` | `Read-only file system` | PASS | -| mapped device ID 3 with two slots | read-time `ENODEV` | `Operation not supported by device` | PASS | -| second concurrent reuse mount | `EBUSY` | `Device busy`; first mount remained readable | PASS | -| normal detach while mounted | `EBUSY` | `mdconfig ... Device busy` | PASS | -| forced orphan then cold slot-2 read | `ENXIO` | `Device not configured` | PASS | -| unmount after forced orphan | clean close of detached consumer | succeeded | PASS | - -Every mount failure was followed immediately by successful md detach. Opened -external devices were therefore rolled back, including failures after one or -more earlier slots had opened. - -## dmesg and cleanup - -The final matrix captured `dmesg` before and after all tests: - -```text -before_lines=2 -after_lines=2 -no-new-lines -``` - -The two pre-existing lines were unchanged historical messages: - -```text -interface erofs.1 already present in the KLD 'erofs-repo22-xattrfix.ko'! -md0: truncating fractional last sector by 14 bytes -``` - -Final state: - -```text -kldstat -n erofs: empty -mdconfig -l: empty -mount -p | grep erofs: empty -``` - -## Tool limitations - -- **MKFS-UNAVAILABLE**: erofs-utils 1.8.6 cannot directly emit more than one - external blob option in this environment. Multi-slot images were derived - from a legal mkfs image with deterministic, asserted structural patches. -- **MKFS-UNAVAILABLE**: erofs-utils 1.8.6 rejects the experimental 48-bit - incompat feature and does not correctly extract the patched device-ID-0 - unified-address control. Both were verified by complete FreeBSD kernel - SHA256 reads. -- **KERNEL-FAIL**: none. diff --git a/tests/results/manual/2026-08-08T2037Z-nfs/manual-test-report.md b/tests/results/manual/2026-08-08T2037Z-nfs/manual-test-report.md deleted file mode 100644 index 194ef71..0000000 --- a/tests/results/manual/2026-08-08T2037Z-nfs/manual-test-report.md +++ /dev/null @@ -1,559 +0,0 @@ -# repo22 FreeBSD 15 NFS export manual test report - -Date: 2026-08-08 20:08-20:37 UTC - -Baseline: `4ef680df6dcbfad329c0b2b1c0af8fcb21cbed4a` plus this NFS export batch - -Guest: FreeBSD 15.0-RELEASE-p8 amd64, -`releng/15.0-n281036-53054229dcb3`, QEMU TCG, 4 vCPUs, 6144 MB RAM - -Host tools: erofs-utils 1.8.6, clang cross-target build - -## Result - -**PASS** for the implementation and all reasonably executable NFS tests. - -FreeBSD mountd installed real EROFS exports through export-only `MNT_UPDATE`; -NFSv3 clients resolved 16-byte EROFS handles for regular files, directories, -symlinks, FIFOs, metabox bit-63 inodes, and multidevice files. Handle mutation, -same-md remount stability, nfsd restart, 12,050-entry READDIRPLUS pagination, -cold remounts, concurrent traversal/read/stat load, read-only enforcement, and -device-boundary errors all behaved as required. - -The only non-code issue was a cleanup-order mistake after the completed test -matrix: one hard loopback NFS mount remained while nfsd and the direct EROFS -mount were stopped. This blocked `mount -p` and later produced five client -`fileid changed` messages when nfsd was temporarily restarted without the -underlying export. The functional-test dmesg snapshot taken before that cleanup -mistake was byte-identical to its baseline. The VM was rebooted to clear the -blocked cleanup process, and a final correctly ordered NFS rerun completed with -byte-identical pre/post dmesg and zero remaining resources. - -## Repository and build - -Initial verification: - -```sh -git -C /work/repo-community/repo22 fetch xdm main -git -C /work/repo-community/repo22 rev-parse HEAD -git -C /work/repo-community/repo22 rev-parse xdm/main -git -C /work/repo-community/repo22 diff --quiet -git -C /work/repo-community/repo22 diff --cached --quiet -``` - -Both revisions were: - -```text -4ef680df6dcbfad329c0b2b1c0af8fcb21cbed4a -``` - -Tracked files were clean. Existing untracked `repo22/build`, -`tests/results/manual/2026-08-08T1800Z-namei/artifacts`, and files outside -repo22 were not deleted, modified, staged, or committed. - -To execute the required `./build.sh` without overwriting the pre-existing -untracked build products, the current source was copied to an isolated build -directory: - -```sh -BUILD_ROOT=$(mktemp -d /work/build/repo22-nfs-build.XXXXXX) -mkdir -p "$BUILD_ROOT/src" -cp repo-community/repo22/build.sh "$BUILD_ROOT/build.sh" -cp repo-community/repo22/src/* "$BUILD_ROOT/src/" -chmod +x "$BUILD_ROOT/build.sh" -cd "$BUILD_ROOT" -./build.sh -nm -u build/erofs.ko | \ - awk '$NF == "bcmp" { found = 1 } END { if (found) exit 1 }' -sha256sum build/erofs.ko -``` - -Result: - -```text -==> SUCCESS: /work/build/repo22-nfs-build.eFulVf/build/erofs.ko -48776485d1679f00100bc7b2be5f891c8f03a3e8b53ac7c00da8c2fd5f74cc70 erofs.ko -``` - -There was no unresolved `bcmp` reference. - -The FreeBSD helper compiled without warnings: - -```sh -cc -O2 -Wall -Wextra -std=c17 nfs_fh_tool.c -o nfs_fh_tool -``` - -## Fixtures - -The primary fixture was generated with: - -```sh -mkdir -p plain-src/basic/subdir plain-src/bigdir plain-src/concurrent -printf 'repo22-nfs-regular\nline-two\n' > plain-src/basic/regular.txt -printf 'nested-directory-file\n' > plain-src/basic/subdir/nested.txt -ln -s regular.txt plain-src/basic/link-to-regular -mkfifo plain-src/basic/test.fifo -for i in $(seq -w 0 12049); do - printf 'entry-%s\n' "$i" > plain-src/bigdir/file-$i -done -for i in $(seq -w 0 255); do - awk -v n="$i" 'BEGIN { - for (j = 0; j < 256; j++) - printf "worker-%s-line-%03d-abcdefghijklmnopqrstuvwxyz0123456789\\n", n, j - }' > plain-src/concurrent/file-$i.dat -done -awk 'BEGIN { - for (i = 0; i < 262144; i++) - printf "throughput-%08d-abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\\n", i -}' > plain-src/throughput.dat -mkfs.erofs -T0 --all-root --ignore-mtime -x-1 -Uclear \ - plain-nfs.erofs plain-src -``` - -Fixture and manifest hashes: - -| Object | SHA256 | -|---|---| -| `plain-nfs.erofs` | `6928f05b58ce462596f9c7bf1d3442ad50d555dc5c31ed01d28e1a921a448bf2` | -| `metabox-nfs.erofs` | `8f493ed70d4be8096ab1adf8208622734d61a64903cf96a1ca1a1cc1854b1985` | -| multidevice primary | `fa2f35f59f63cc4a56d230ba960a6c1d27748506fccce081447cab0dc0d540fd` | -| multidevice blob 1 | `3aeb19c519636b61dc3ebd47c6286098deaa0990ff618ce3facbff4fd65907cd` | -| source manifest | `0eda6f88c3b6c4b7c8d81d940a237d1f63c958a7f6fe7bdd4f77d6b293736fa9` | -| `basic/regular.txt` | `93e7f327676f582356e9de2fcca7b714910227008fa1c5e7820e942c471edb6f` | -| `basic/subdir/nested.txt` | `6da159d7ea19e9d1253ccb0f2c67586e6e075459954e740ff8405546683d87c3` | -| `concurrent/file-000.dat` | `8435866622642b6dadf502b09f95af32cb01907ba3376e4f1fe96278e5ada692` | -| `throughput.dat` (21,757,952 bytes) | `0841effed82d1adf394b6834ce30d4d9eb5fc9427527e854ec1cb6bb4b86c119` | -| expected 12,050-name sorted list | `4fbf1b6103e8884223629c3fd03f4ec36061e65cd2a1becfc96670daa96f9d71` | - -The metabox image was the previously qualified compressed-metabox fixture. The -multidevice fixture was the previously qualified primary-plus-one-blob image. - -## FreeBSD 15 setup commands - -The artifacts were copied to the existing VM through SSH port 9222. Password -material and askpass script contents were neither printed nor recorded. - -```sh -scp -P 9222 erofs.ko plain-nfs.erofs metabox-nfs.erofs \ - tests/nfs_fh_tool.c root@127.0.0.1:/tmp/repo22-nfs-test/ -ssh -p 9222 root@127.0.0.1 -cd /tmp/repo22-nfs-test -sha256 erofs.ko plain-nfs.erofs metabox-nfs.erofs -cc -O2 -Wall -Wextra -std=c17 nfs_fh_tool.c -o nfs_fh_tool -``` - -The guest hashes matched the host hashes. - -EROFS and NFS setup: - -```sh -kldload /tmp/repo22-nfs-test/erofs.ko -mdconfig -a -t vnode -f /tmp/repo22-nfs-test/plain-nfs.erofs -u 42 -mkdir -p /mnt/repo22-erofs -mount -t erofs -o ro /dev/md42 /mnt/repo22-erofs -mount -v | grep /mnt/repo22-erofs - -printf '%s\n' \ - '/mnt/repo22-erofs -ro -maproot=root -network 127.0.0.0 -mask 255.0.0.0' \ - > /etc/exports -service rpcbind onestart -service mountd onestart -service nfsd onestart -service mountd onereload -rpcinfo -p 127.0.0.1 -showmount -e 127.0.0.1 -mount -v | grep /mnt/repo22-erofs -``` - -Before mountd reload the EROFS line did not contain `NFS exported`. Afterwards: - -```text -/dev/md42 on /mnt/repo22-erofs (erofs, NFS exported, local, read-only, acls, ...) -``` - -NFSv2/v3 and mountd were registered over TCP and UDP. NFS components are built -into this GENERIC kernel: `kldload nfsd` and `kldload nfscl` reported -`already loaded or in kernel`, while rpcbind/mountd/nfsd operated normally. - -## File-handle validation commands - -Normal vnode types: - -```sh -./nfs_fh_tool capture /mnt/repo22-erofs/basic/regular.txt regular.before.fh -./nfs_fh_tool capture /mnt/repo22-erofs/basic/subdir directory.fh -./nfs_fh_tool lcapture /mnt/repo22-erofs/basic/link-to-regular symlink.fh -./nfs_fh_tool capture /mnt/repo22-erofs/basic/test.fifo fifo.fh -./nfs_fh_tool describe regular.before.fh -./nfs_fh_tool describe directory.fh -./nfs_fh_tool describe symlink.fh -./nfs_fh_tool describe fifo.fh -./nfs_fh_tool stat regular.before.fh -./nfs_fh_tool stat directory.fh -./nfs_fh_tool stat symlink.fh -./nfs_fh_tool stat fifo.fh -./nfs_fh_tool cat regular.before.fh regular.fhopen.out -``` - -Observed handles: - -| Type | NID | Generation | Result | -|---|---:|---:|---| -| regular | `0x74` | 1 | `fhstat` and `fhopen` PASS | -| directory | `0x76` | 1 | `fhstat` PASS | -| symlink (`lgetfh`) | `0x72` | 1 | `fhstat` PASS | -| FIFO | `0x79` | 1 | `fhstat` PASS | - -All had `len=16` and `pad=0`. The regular full-handle SHA256 was: - -```text -fb1778fc6bdc979e10d003dd6131a961e7532baa16273c5465935a665bf990f6 -``` - -Malformed and stale classification: - -```sh -./nfs_fh_tool mutate regular.before.fh bad-len.fh len 15 -./nfs_fh_tool mutate regular.before.fh bad-pad.fh pad 1 -./nfs_fh_tool mutate regular.before.fh bad-gen.fh gen 2 -./nfs_fh_tool mutate regular.before.fh bad-nid.fh nid_hi 0xffffffff -./nfs_fh_tool expect-stat bad-len.fh EINVAL -./nfs_fh_tool expect-open bad-len.fh EINVAL -./nfs_fh_tool expect-stat bad-pad.fh EINVAL -./nfs_fh_tool expect-open bad-pad.fh EINVAL -./nfs_fh_tool expect-stat bad-gen.fh ESTALE -./nfs_fh_tool expect-open bad-gen.fh ESTALE -./nfs_fh_tool expect-stat bad-nid.fh ESTALE -./nfs_fh_tool expect-open bad-nid.fh ESTALE -``` - -All eight checks passed. `fhstat` exercised the shared-lock `VFS_FHTOVP` path; -`fhopen` exercised the exclusive-lock path. - -Same explicit md-unit remount: - -```sh -umount /mnt/repo22-erofs -mdconfig -d -u 42 -mdconfig -a -t vnode -f plain-nfs.erofs -u 42 -mount -t erofs -o ro /dev/md42 /mnt/repo22-erofs -./nfs_fh_tool capture /mnt/repo22-erofs/basic/regular.txt regular.after.fh -./nfs_fh_tool compare regular.before.fh regular.after.fh -./nfs_fh_tool cat regular.before.fh regular.remount.out -``` - -The complete handles were byte-identical and the pre-remount handle still read -the correct file after remount. - -Non-export mount update rejection: - -```sh -mount -u -o noexec /mnt/repo22-erofs -``` - -Result: `Operation not supported`; the mount remained exported, local, -read-only, and did not gain `noexec`. - -## NFSv3 basic and restart tests - -Four NFSv3 TCP READDIRPLUS clients were mounted: - -```sh -mount_nfs -o nfsv3,tcp,rdirplus,readdirsize=512 \ - 127.0.0.1:/mnt/repo22-erofs /mnt/repo22-nfs-512 -mount_nfs -o nfsv3,tcp,rdirplus,readdirsize=1024 \ - 127.0.0.1:/mnt/repo22-erofs /mnt/repo22-nfs-1024 -mount_nfs -o nfsv3,tcp,rdirplus,readdirsize=4096 \ - 127.0.0.1:/mnt/repo22-erofs /mnt/repo22-nfs-4096 -mount_nfs -o nfsv3,tcp,rdirplus \ - 127.0.0.1:/mnt/repo22-erofs /mnt/repo22-nfs-default -nfsstat -m -``` - -FreeBSD clamped the requested 512/1024/4096 values to an effective -`readdirsize=8192`; the default client used 65536. This was recorded as a client -environment limit, not a failure. - -For each client: - -```sh -cmp NFS/basic/regular.txt DIRECT/basic/regular.txt -test -d NFS/basic/subdir -test "$(readlink NFS/basic/link-to-regular)" = regular.txt -test -p NFS/basic/test.fifo -test "$(stat -f %i NFS/basic/regular.txt)" = \ - "$(stat -f %i DIRECT/basic/regular.txt)" -! touch NFS/write-must-fail -``` - -All passed. Writes failed with `Read-only file system`. - -nfsd restart with an open descriptor: - -```sh -exec 3< /mnt/repo22-nfs-default/basic/regular.txt -service nfsd onerestart -cat <&3 > open-fd-after-nfsd-restart.out -exec 3<&- -cmp open-fd-after-nfsd-restart.out /mnt/repo22-erofs/basic/regular.txt -``` - -The open descriptor and subsequent path reopen both passed. Direct file handles -captured before and after restart were byte-identical. - -## Metabox bit-63 test - -Commands: - -```sh -mdconfig -a -t vnode -f metabox-nfs.erofs -u 43 -mount -t erofs -o ro /dev/md43 /mnt/repo22-metabox -./nfs_fh_tool capture /mnt/repo22-metabox/long/file1.txt metabox.before.fh -./nfs_fh_tool describe metabox.before.fh -./nfs_fh_tool stat metabox.before.fh -./nfs_fh_tool cat metabox.before.fh metabox.fhopen.out -./nfs_fh_tool mutate metabox.before.fh metabox.bad-nid.fh \ - nid_lo 0xffffffff -./nfs_fh_tool expect-stat metabox.bad-nid.fh ESTALE -./nfs_fh_tool expect-open metabox.bad-nid.fh ESTALE -``` - -Observed handle: - -```text -len=16 pad=0 nid=800000000000010a gen=1 -ino=9223372036854776074 -``` - -The metabox mount was exported with mountd and mounted over NFSv3. Direct, -`fhopen`, and NFS contents had SHA256: - -```text -661b22d2a7bd94a7da35834d4cc3647eb527d48eb08acc75bde2e3a7691ed45e -``` - -The NFS and direct inode numbers matched. The out-of-range metabox mutation -returned `ESTALE` through both shared and exclusive paths. - -## Multidevice boundary test - -Normal NFS commands: - -```sh -mdconfig -a -t vnode -f primary.img -u 44 -mdconfig -a -t vnode -f blob1.img -u 45 -mount -t erofs -o ro -o device.1=/dev/md45 /dev/md44 \ - /mnt/repo22-multidev -mount_nfs -o nfsv3,tcp,rdirplus,readdirsize=512 \ - 127.0.0.1:/mnt/repo22-multidev /mnt/repo22-nfs-multidev -cmp /mnt/repo22-nfs-multidev/alpha.bin /mnt/repo22-multidev/alpha.bin -cmp /mnt/repo22-nfs-multidev/small.txt /mnt/repo22-multidev/small.txt -``` - -Normal multidevice NFS reads passed. Representative hashes: - -```text -f779dbe3aeaa609beb32212579f3aea441f244621d13c977cb8b52d7ec3016d9 alpha.bin -5990b055a3c6d27681185a25e15ff21ba3cbdca7632faf4332233238e5f45aad small.txt -001bcf4626e7d52f1bd76dee9a6c6e7d3a718c03602c5f9b07840a6c98b8fdc3 tree/beta.bin -``` - -Cold detached-provider test: - -```sh -umount /mnt/repo22-multidev -mdconfig -d -u 44 -mdconfig -a -t vnode -f primary.img -u 44 -mdconfig -a -t vnode -f blob1.img -u 45 -mount -t erofs -o ro -o device.1=/dev/md45 /dev/md44 \ - /mnt/repo22-multidev -./nfs_fh_tool capture /mnt/repo22-multidev/tree/beta.bin multidev-beta.fh -./nfs_fh_tool stat multidev-beta.fh -mdconfig -d -o force -u 45 -! ./nfs_fh_tool cat multidev-beta.fh beta.after-orphan -``` - -The handle resolved to NID `0x36`, then the cold read returned: - -```text -Device not configured -``` - -This preserved the multidevice `ENXIO` boundary error instead of converting it -to `ESTALE`, reading past the provider, or panicking. - -## READDIRPLUS and stress - -Every one of the four initial listings returned exactly 12,050 unique expected -names. The expected list and all four sorted outputs had SHA256: - -```text -4fbf1b6103e8884223629c3fd03f4ec36061e65cd2a1becfc96670daa96f9d71 -``` - -Dot handling: - -```sh -ls -a1 /mnt/repo22-nfs-default/bigdir > nfs-dot-list -test "$(wc -l < nfs-dot-list)" -eq 12052 -test "$(grep -cx '\.' nfs-dot-list)" -eq 1 -test "$(grep -cx '\.\.' nfs-dot-list)" -eq 1 -``` - -All passed. - -Cold pagination used five unmount/remount/list/compare cycles on the small -client. Concurrency then used eight workers, three complete 12,050-entry rounds -each, distributed across the four mounts. All 29 cold/concurrent sorted outputs -had one unique SHA256 value, matching the expected list. - -Parallel data/metadata load: - -```sh -for M in 512 1024 4096 default; do - find /mnt/repo22-nfs-$M/concurrent -type f -maxdepth 1 -print0 | \ - xargs -0 -n 1 -P 8 cat > /dev/null & - find /mnt/repo22-nfs-$M/concurrent -type f -maxdepth 1 -print0 | \ - xargs -0 -n 1 -P 8 stat -f '%i %z' > /dev/null & -done -wait -``` - -All jobs completed successfully. - -Final stress counters: - -| Counter | Client | Server | -|---|---:|---:| -| requests / cache misses | 270,854 | 271,174 | -| lookup | 264,989 | 265,109 | -| read | 1,360 | 1,360 | -| READDIRPLUS | 2,512 | 2,512 | -| write | 0 | 0 | -| timed out | 0 | n/a | -| retries | 0 | n/a | - -There were no READDIR RPCs because the clients negotiated READDIRPLUS. - -Informational throughput: - -```text -21757952 bytes transferred in 5.843937 seconds -3723167 bytes/second -``` - -No fixed throughput threshold was used. - -## Test matrix - -| Test | Actual | Status | -|---|---|---| -| isolated `./build.sh` | success; module hash recorded | PASS | -| unresolved `bcmp` | none | PASS | -| FreeBSD helper compile | `-Wall -Wextra`, no warnings | PASS | -| final `kldload` / `kldunload` | load, unload, reload, unload succeeded | PASS | -| initial mount export flag | absent before mountd | PASS | -| export-only `MNT_UPDATE` | mountd installed export | PASS | -| non-export `MNT_UPDATE` | `EOPNOTSUPP`, flags unchanged | PASS | -| regular file | direct, `fhopen`, NFS content match | PASS | -| directory | handle and NFS traversal | PASS | -| symlink | `lgetfh`, target and NFS lookup | PASS | -| FIFO | handle metadata and NFS type | PASS | -| read-only behavior | direct and all NFS mounts rejected writes | PASS | -| 16-byte handle ABI | len/pad/NID hi/NID lo/gen observed | PASS | -| generation | `1`, matching `va_gen` | PASS | -| shared/exclusive lock contract | `fhstat` and `fhopen` passed | PASS | -| malformed len/pad | `EINVAL` | PASS | -| stale generation/NID | `ESTALE` | PASS | -| same `md42` remount | complete handle byte-identical | PASS | -| nfsd restart | open descriptor and reopen passed | PASS | -| metabox bit 63 | real `0x800000000000010a` round-trip | PASS | -| metabox boundary mutation | `ESTALE` | PASS | -| multidevice NFS | external-data reads matched | PASS | -| detached external device | cold handle read preserved `ENXIO` | PASS | -| 12,050-entry listings | exact count and names on four clients | PASS | -| cookie pagination | five cold remounts, no duplicate/omission | PASS | -| dot omitted | one `.` and one `..`, 12,052 total | PASS | -| concurrent traversal | 8 workers x 3 rounds, all hashes equal | PASS | -| concurrent read/stat | all jobs completed | PASS | -| READDIRPLUS | client/server 2,512 | PASS | -| NFS timeouts/retries | 0 / 0 | PASS | -| successful NFS writes | 0 | PASS | -| throughput | 3,723,167 B/s, informational | INFO | - -## dmesg and cleanup - -The functional matrix captured dmesg before loading the test module and after -all functional tests plus a load/unload cycle. Both files contained the same -two pre-existing historical lines and compared byte-for-byte equal: - -```text -interface erofs.1 already present in the KLD 'erofs-repo22-xattrfix.ko'! -md0: truncating fractional last sector by 14 bytes -``` - -No EROFS error, stale-handle message, trap, panic, or new kernel line occurred -during the functional matrix. - -The first cleanup attempt used the wrong order: it stopped nfsd and unmounted -the direct EROFS mounts before one hard loopback client was fully gone. This -left a `mount -p` process in uninterruptible NFS wait. When rpcbind/nfsd were -temporarily restarted without the underlying EROFS export to release those -clients, the NFS client logged five `fileid changed` messages. These messages -occurred after the byte-identical functional dmesg snapshot and are classified -as **environment cleanup artifact**, not an EROFS functional failure. - -The VM was rebooted to remove the blocked cleanup process. Post-reboot audit: - -```text -erofs_mounts=0 -nfs_mounts=0 -md_units=0 -erofs_modules=0 -nfsd_pids=0 mountd_pids=0 rpcbind_pids=0 -exports_exists=no -``` - -A final correctly ordered clean rerun then performed: - -```sh -kldload erofs.ko -mdconfig -a -t vnode -f plain-nfs.erofs -u 42 -mount -t erofs -o ro /dev/md42 /mnt/repo22-erofs -service rpcbind onestart -service mountd onestart -service nfsd onestart -mount_nfs -o nfsv3,tcp,rdirplus,readdirsize=512 \ - 127.0.0.1:/mnt/repo22-erofs /mnt/repo22-nfs-clean -cmp /mnt/repo22-nfs-clean/basic/regular.txt \ - /mnt/repo22-erofs/basic/regular.txt -test "$(find /mnt/repo22-nfs-clean/bigdir -type f -maxdepth 1 | wc -l)" \ - -eq 12050 -umount /mnt/repo22-nfs-clean -: > /etc/exports -service mountd onereload -service nfsd onestop -service mountd onestop -service rpcbind onestop -umount /mnt/repo22-erofs -mdconfig -d -u 42 -kldunload erofs -``` - -The clean rerun's pre/post dmesg files compared byte-for-byte equal. Final -state after that rerun: - -```text -clean_rerun_erofs=0 nfs=0 md=0 module=0 services=0 exports=absent -``` - -## Limitations - -- **ENVIRONMENT-LIMIT**: FreeBSD clamped requested readdir sizes 512, 1024, and - 4096 to an effective minimum of 8192. The test still exercised a much smaller - value than the 65536 default and produced 2,512 real READDIRPLUS calls. -- **ENVIRONMENT-CLEANUP-ARTIFACT**: the five post-test `fileid changed` lines - were caused by the explicitly documented wrong cleanup order. A reboot and a - correctly ordered rerun left no new dmesg lines or resources. -- **KERNEL-FAIL**: none. -- **NOT RUN**: none of the required functional cases were omitted. diff --git a/tests/results/manual/2026-08-08T2114Z-multidev-review/manual-test-report.md b/tests/results/manual/2026-08-08T2114Z-multidev-review/manual-test-report.md deleted file mode 100644 index b99b168..0000000 --- a/tests/results/manual/2026-08-08T2114Z-multidev-review/manual-test-report.md +++ /dev/null @@ -1,142 +0,0 @@ -# repo22 multi-device review findings manual test report - -Date: 2026-08-08 21:14 UTC -Baseline: `2354b4487c84458c1e447f4d0bb7b55daebcca26` plus this review fix -Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG -Linux reference: 7.1-rc1 `/work/dev-src-linux/fs/erofs` -Host tools: erofs-utils 1.8.6 - -## Result - -**PASS**. All five multi-device review findings were fixed and exercised with -deterministic positive and negative fixtures. A real two-block LZ4 pcluster -was read from an external provider and the complete 1 MiB output matched the -source SHA256. Multi-device NFS reads and forced-device-removal behavior also -passed. - -## Source changes validated - -- Non-zero device IDs accept a slot with `uniaddr=0`; unified device-ID-0 - lookup skips such slots. -- `devt_slotoff=0` reads the table from byte offset zero, as Linux 7.1 does. -- Device mapping carries the requested extent length. Flatdev reads must fit - wholly in the primary range or one non-zero unified slot; gaps, adjacent-slot - crossings, and pcluster crossings fail closed. -- Declared image and slot bounds are checked before physical media size, so - format corruption is `EINTEGRITY` and a genuinely short provider is - `ENXIO`. -- Primary and external `namei()` failures preserve `ENOENT`; omitted required - `device.N` options still return `ENXIO`. - -The NFS file-handle ABI, `VFS_FHTOVP`, inode bounds, readdir cookies, and GEOM -orphan lifecycle from `2354b448` were not changed. - -## Build - -Commands: - -```sh -git diff --check -./build.sh -nm -u build/erofs.ko | awk '$NF == "bcmp" {bad=1} END {exit bad}' -sha256sum build/erofs.ko -``` - -Results: - -- Cross-build: PASS. -- `git diff --check`: PASS. -- Unresolved `bcmp`: none. -- Module SHA256: - `b85c429fa73e6cebc10e6e3941575a6ca170306ab4fa9ccca4006c922971acb8`. -- FreeBSD `kldload` and `kldunload`: PASS. - -## Fixture construction - -The chunk fixtures derive from the checksum-valid TC094 images recorded in the -19:37 multi-device report. The patcher asserted the original table values -`slot1=(blocks=19, uniaddr=2)`, `slot2=(blocks=25, uniaddr=21)`, and the first -32 KiB chunk index `(device_id=2, startblk=9)` before each change. - -The compressed baseline was generated from the deterministic `shape.dat`: - -```sh -mkfs.erofs -T0 -U00000000-0000-0000-0000-000000000000 \ - --all-root -E legacy-compress -zlz4 -C65536 \ - lz4-full-64k.erofs src-big64k -dump.erofs --path=/shape.dat -e lz4-full-64k.erofs -``` - -The baseline contains one 1 MiB logical extent backed by an 8192-byte physical -pcluster. Its HEAD pblk was changed from block 1 to unified block 4; the two -compressed blocks were copied to a two-block external provider and zeroed in -the primary image. A checksum-valid device table declared -`slot1=(blocks=2, uniaddr=4)`. - -For the crossing negative, the same pcluster was placed across adjacent -`slot1=[4,5)` and `slot2=[5,6)`. The flatdev contains both real compressed -blocks, proving that failure is caused by the declared slot boundary rather -than absent data. - -erofs-utils 1.8.6 parses the external compressed extent but its userspace -compressed read path does not propagate the unified slot ID to `erofs_dev_read`; -therefore userspace extraction of this layout is tool-unavailable. The -FreeBSD kernel full-file SHA256 is the authoritative read validation. - -## Fixture hashes - -| Fixture | SHA256 | -|---|---| -| zero-uniaddr primary | `d38935291fb2ee756e0c192d1b0d39b4f4f5b4047aa37c2de44cad91a55fa018` | -| zero-uniaddr device-ID-0 control | `fac9db7631cd06d17212cbe3e7b1d681766f031d1e41e443f71f1bad39f9b960` | -| slotoff-zero primary | `39e3e8c60bea962f1200550316be87ec7da7b15c0ba11c6e5edf05f0715db451` | -| cross-slot primary | `edd0cba92a26bc9a7a59561732fb8e045b2e1565d18d9f9b3143f86f64649dee` | -| cross-slot flatdev | `6e2834450081d778cee37fa3e050249f04927def205d6816aab814a2912744bc` | -| gap flatdev | `5bc07b66f7b0f83e9568ba781dcc78a08d45e617dc27bb365ed3ee2c12353218` | -| external-compressed primary | `5625612426d68624c77dd94b09754da583fc6bd90767e9e9d958e4fc89131a4d` | -| external-compressed blob | `0d71102ce471af0a30d4d61a1fbeef1fc33437c85651d7c7f9c60ebd305ec9d0` | -| cross-pcluster primary | `2bce4a91c1aedc4bab2e65582ef9cb465094f825a051c0203d52b3d18bb17ed8` | -| cross-pcluster flatdev | `bc07fb06ca5c6bb60ef1a989eecbd3d714450092ba756f85f5b060c8b8b2ec3a` | - -Source `shape.dat` SHA256: -`370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52`. - -## FreeBSD result matrix - -| Test | Actual | Status | -|---|---|---| -| slot 1 `uniaddr=0`, explicit device ID 1 | complete striped and second-file SHA256 matched | PASS | -| slot 1 `uniaddr=0`, device ID 0 control | slot skipped; primary-bound `EINTEGRITY` | PASS | -| `devt_slotoff=0` | mount and complete striped SHA256 matched | PASS | -| nonexistent primary path | `ENOENT` / `No such file or directory` | PASS | -| nonexistent external path | `ENOENT` / `No such file or directory` | PASS | -| omitted slot option | `ENXIO` / `Device not configured` | PASS | -| flatdev chunk crosses adjacent slots | read returned `EINTEGRITY` | PASS | -| device-ID-0 chunk starts in a gap | read returned `EINTEGRITY` | PASS | -| explicit chunk exceeds declared slot and media | `EINTEGRITY`, not `ENXIO` | PASS | -| real external 64 KiB LZ4 pcluster | complete 1 MiB SHA256 matched | PASS | -| flatdev pcluster crosses adjacent slots | read returned `EINTEGRITY` | PASS | -| explicit pcluster crosses two providers | read returned `EINTEGRITY` | PASS | - -## NFS smoke - -The external-compressed image was exported read-only over local NFSv3/TCP. -The NFS client read the complete `shape.dat`; SHA256 matched the source. - -The zero-uniaddr two-slot image was then exported. The client obtained stable -inode and size data for a cold slot-2 file, slot 2 was forcibly orphaned, and -the same NFS pathname still resolved to identical metadata. Its first cold data -read failed with the underlying device error. This preserves the `2354b448` -contract: file-handle resolution is not misreported as `ESTALE`, while missing -external data remains an I/O/device failure. - -## Cleanup - -All EROFS and NFS mounts were unmounted, NFS services stopped, `/etc/exports` -cleared, md providers detached, and the module unloaded. No image, overlay, -fixture, or build artifact is part of this commit. - -A final representative rerun covered the external-compressed positive, the -flatdev-gap negative, and the offset-zero table positive. The dmesg SHA256 was -unchanged before and after: -`897003051a6a50875d49715bfa27907611cedac9e42e0a06ca7377105ac695ae`. diff --git a/tests/results/manual/2026-08-08T2306Z-compression-p0/manual-test-report.md b/tests/results/manual/2026-08-08T2306Z-compression-p0/manual-test-report.md deleted file mode 100644 index ac478a6..0000000 --- a/tests/results/manual/2026-08-08T2306Z-compression-p0/manual-test-report.md +++ /dev/null @@ -1,406 +0,0 @@ -# repo22 compression P0 WIP integration manual report - -Date: 2026-08-08 23:06 UTC -Baseline: `78182686e968c659932458cbe7a1e0889397f20b` -Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG -Build reference: `/work/dev-freebsd-releng` (`REVISION=15.0`, source branch -`RELEASE-p9`) -Linux behavior reference: `/work/dev-src-linux/fs/erofs` -Host production tool: erofs-utils 1.8.6 - -## Result - -**PASS**, with the extent-record guest case accurately recorded as -**MKFS-UNAVAILABLE** rather than PASS. - -The retained ten-file WIP was reviewed and integrated without reset, checkout, -stash, or revert. Both module configurations build and load. DEFLATE, ZSTD, -and MicroLZMA full and partial references read correctly; targeted corruption -returns `EIO`. A fragment-backed compressed metabox now works after packed -inode initialization, while loop, recursive-NID, and range mutations fail -closed. HEAD2 and interlaced images pass real FreeBSD kernel reads. LZ4, -xattr/metabox, chunk, external compressed multi-device, and NFS regressions -also pass. - -No CI or test runner was added. Only source, Markdown manual tests, and this -report are intended for the commit. - -## Instruction and baseline audit - -- `find /work -name AGENTS.md -type f -print` returned no paths. There were no - applicable `AGENTS.md` files. -- Initial local `HEAD` and `xdm/main` both resolved to - `78182686e968c659932458cbe7a1e0889397f20b`. -- The exact ten tracked WIP files were present: - `build.sh`, `src/Makefile`, `src/decompressor.c`, `src/deflate.c`, - `src/internal.h`, `src/lzma.c`, `src/super.c`, `src/zdata.c`, `src/zmap.c`, - and `src/zstd.c`. -- Untracked build, fixture, overlay, artifact, and other-repository paths were - not staged. - -## Initial guest module cleanup - -Before any build under test was loaded, the guest reported: - -```text -Id Refs Address Size Name - 5 1 0xffffffff82822000 a690 erofs-nozstd.ko -``` - -`kldstat -v -i 5` proved the path was `./erofs-nozstd.ko` and the contained -module name was `erofs`. The cleanup used the observed KLD ID, not a guessed -filename: - -```sh -kldunload -i 5 -kldstat -kldstat | grep -i erofs -``` - -The second `kldstat` contained no EROFS entry and the final grep printed -`none`. - -## Source review - -### Packed inode and metabox order - -- `packed_nid` and `metabox_nid` are decoded and checked before carrier loads. -- A packed NID with the metabox selector bit is rejected before any metadata - recursion can begin. -- The packed inode is loaded before the metabox inode, matching the Linux - dependency order and allowing a compressed metabox to terminate in a packed - fragment. -- The packed inode must be a regular, non-fragment inode. This rejects a packed - carrier that would recurse back through itself. -- A fragment-backed metabox must have a distinct loaded packed inode, non-zero - size, a real fragment tail mapping, and a range wholly inside the packed - inode. -- Fragment recursion checks compare inode NIDs rather than object pointers, - which also catches separately allocated `erofs_node` objects for the same - on-disk inode. - -### DEFLATE, ZSTD, and MicroLZMA - -- DEFLATE uses `inflate(..., Z_SYNC_FLUSH)` until the requested output is full, - detects no-progress loops, accepts `Z_OK` for partial output, and requires - `Z_STREAM_END` plus complete input consumption for full output. -- ZSTD uses FreeBSD's formal `` API with - `ZSTD_createDCtx_advanced`, `ZSTD_DCtx_setParameter`, - `ZSTD_decompressStream`, `ZSTD_isError`, and `ZSTD_freeDCtx`. -- ZSTD partial decoding stops after the requested output is produced. Full - decoding requires frame completion and complete input consumption. -- MicroLZMA partial decoding accepts `XZ_OK` or `XZ_STREAM_END` after exact - requested output. Full decoding requires `XZ_STREAM_END` and - `buffer.in_pos == srclen`. -- All decoder failures are translated to the filesystem read error `EIO` and - all allocated decoder/output buffers are released. - -### ZSTDIO build gate - -- `src/Makefile` consumes `opt_zstdio.h` and adds FreeBSD's zstd compatibility - include directory for `zstd.c`. -- `build.sh` accepts only `EROFS_ZSTDIO=0` or `1` and creates the corresponding - option header. -- The disabled translation unit contains only the availability result and a - local stub; it references no `ZSTD_*` symbol. -- A filesystem advertising ZSTD is rejected during compression-config parsing - when the module lacks ZSTDIO, before any file read can reach the stub. - -### HEAD2, interlaced, and extent records - -- HEAD2 selects `z_algorithmtype[1]` and respects the HEAD2 big-pcluster bit. -- Plain records with interlaced advise use the interlaced byte rotation before - returning data. -- Extent records retain Linux ordering and responsibilities for 4-, 8-, 16-, - and 32-byte records, partial references, explicit algorithm format, shifted - and interlaced plain data, and final fragments. -- Explicit extent tables with a non-empty file and zero extent count fail - closed. -- Final fragment mappings are checked against the loaded packed inode before - reads. - -## Build matrix - -Host commands: - -```sh -EROFS_ZSTDIO=0 ./build.sh -cp build/erofs.ko /tmp/repo22-erofs-nozstd-probe.ko -nm -u /tmp/repo22-erofs-nozstd-probe.ko - -EROFS_ZSTDIO=1 ./build.sh -cp build/erofs.ko /tmp/repo22-erofs-zstd-probe.ko -nm -u /tmp/repo22-erofs-zstd-probe.ko -``` - -| Build | Size | SHA256 | `ZSTD_*` undefined | `bcmp` undefined | Guest KLD | -|---|---:|---|---|---|---| -| no ZSTDIO | 68744 | `c96f4e1620f005a19d7ed581f98f1894969b38a34d7021f54dbce99919888ef9` | none | none | load/unload PASS | -| ZSTDIO | 69984 | `7f8cb03afb9c76b535709af1a06bf993cbab74bb5899b2f4646a6fd20b00d15c` | five formal API names | none | load/unload PASS | - -The enabled module's unresolved ZSTD set was exactly: - -```text -ZSTD_DCtx_setParameter -ZSTD_createDCtx_advanced -ZSTD_decompressStream -ZSTD_freeDCtx -ZSTD_isError -``` - -Both were loaded by path, identified with `kldstat -v -i ID`, and unloaded by -that exact ID. - -## ZSTDIO behavior - -With the disabled module, a normal LZ4 image still read successfully: - -```text -nozstd_lz4_sha256=370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52 -nozstd_lz4_dmesg_delta=empty -``` - -The same module rejected the ZSTD image: - -```text -nozstd_mount_rc=1 -mount: /dev/md0: erofs: ZSTD compression requires ZSTDIO support: Operation not supported -``` - -The ZSTDIO module mounted the image and produced the expected complete SHA256 -shown below. - -## Fixture construction and proof - -Fixtures and build outputs remained outside the repository commit. -Authentication used the existing private askpass workflow; its contents and -credentials were not printed or recorded. - -### Partial-reference images - -`dump.erofs -s` and `dump.erofs --path=PATH -e` established compressor, -layout, logical lengths, physical starts, and pcluster lengths. Byte-level -patching asserted the original inode NID, map-header offset, HEAD record, and -pblk before setting `Z_EROFS_LI_PARTIAL_REF` or redirecting a reused pblk. -CRC32C was recomputed for every superblock mutation. - -| Image | SHA256 | -|---|---| -| `deflate-partial-ref.erofs` | `8bc720a1250ef28794d091b6264e76060fbf01d66ea706c59c24c1960fa347ac` | -| `deflate-partial-ref-corrupt.erofs` | `6ff9dff9b3b5aba7da9b4a93f5b068270b4916267d2fe0fd3702c02be4c19fae` | -| `lzma-partial-ref.erofs` | `2f4bd89d2340273dd4052ea73aa9ac43802db431258c118c5dfede7727be9408` | -| `lzma-partial-ref-corrupt.erofs` | `bfbbd304a401c091d63e8b8e42b2b1984d9750760e940aded89acc909551d838` | -| `zstd-partial-ref.erofs` | `7f5ee4f8a20f20d32eaf8780a7081229f7dbf6b896ad23986da36e1a1c580519` | -| `zstd-partial-ref-corrupt.erofs` | `0c6e6b312b879297e2f71f406dcd35236654800e7cd115df0cb9a748297c48c1` | - -The LZMA and ZSTD full file was 1048576 bytes. Their partial file was 700000 -bytes and reused the complete source pcluster. The DEFLATE fixture contained -multiple real compressed extents, including reused physical extents in the -partial file. - -### Fragment-backed compressed metabox - -A METABOX-capable inspector proved: - -```text -packed_nid=40 -metabox_nid=38 -metabox inode: regular, compressed-full, size=20480 -metabox fragment header=0x800000000004e800 -fragmentoff=321536 -packed inode: regular, plain, size=342016 -321536 + 20480 = 342016 -``` - -Positive image SHA256: -`8a9a62bd203994711b8272192915d811e6c3de23e07ad9607dd63e66cc109bcd`. - -The negative images changed one proven field each: - -| Negative | Exact mutation | SHA256 | -|---|---|---| -| self-loop | `packed_nid: 40 -> 38` | `e9501ed9d149e2d735d95156669d144e733bf4be620bb69ea8a0c41f996b436c` | -| range | `fragmentoff: 321536 -> 342016` | `3fdcf2a41f50da93a5931edcef0d86ff2f576ecba781833036eaa930d99197d5` | -| metabox recursion | set bit 63 in `metabox_nid` | `b873e0cf4d2892d2d154a5769f494660f47f4429161e7be001b833da6dd0d705` | -| packed recursion | set bit 63 in `packed_nid` | `4fa368d1b1d47bb56b82132e6055d105ed2508b179a8df2a98ec5a728f91c9ac` | - -### HEAD2 - -The HEAD2 fixture patch asserted the original bytes and made only these semantic -changes, plus the resulting CRC32C bytes: - -```text -feature_incompat: 0x00000003 -> 0x0000000b -map h_advise: 0x0002 -> 0x0006 -first di_advise: HEAD1 (1) -> HEAD2 (3) -``` - -Image SHA256: -`fc70cbef0442a86ac2f507aebd7ac7bcfbdfcc3d0b9b3cf2ff5231334583d816`. -The targeted corrupted copy SHA256 was -`f0d9ad645804565c5aca7a92128df3ae9c81717a08ad7e8692489cd2ae606700`. - -### Interlaced - -This image was generated specifically with installed erofs-utils 1.8.6: - -```sh -mkfs.erofs -zlz4 -C4096 -Efragments -T0 \ - interlaced-1.8.6.erofs source -``` - -`dump.erofs -e` reported 105 real extents, with 4096-byte plain extents -interspersed with compressed extents. Image SHA256: -`d77d86f874361bae86cae9e8f6d05d9b6c68ef04cd5c8779368aef033fa485c3`. - -### Extent metadata - -Installed `mkfs.erofs -V` reported 1.8.6. Exact source-tree search found no -on-disk `Z_EROFS_ADVISE_EXTENTS`, `z_erofs_extent_recsize`, or -`struct z_erofs_extent {` definition in the 1.8.6 include/lib tree. Its -internal `struct z_erofs_extent_item` is an in-memory compressor item, not the -new on-disk extent-record ABI. - -Although a newer-tool extent image existed in the WIP build area, it was not -mounted or scored. The required result is therefore: - -```text -extent metadata guest result: MKFS-UNAVAILABLE -``` - -The independent static review compared `src/erofs_fs.h` and `src/zmap.c` -against `/work/dev-src-linux/fs/erofs/erofs_fs.h` and `zmap.c`, covering record -sizes 4/8/16/32, implicit physical bases, explicit-count binary search, -physical/logical high words, format bits, partial references, interlaced data, -fragments, metabox metadata reads, and malformed explicit zero counts. - -## Core FreeBSD result matrix - -The guest test used repeated `mdconfig -a -t vnode -f IMAGE`, read-only EROFS -mounts, `sha256 -q`, byte-exact `dd`/`cmp`, and a small C helper that printed -`errno` on read failure. - -| Case | Complete SHA256 / result | Boundary or random proof | Status | -|---|---|---|---| -| DEFLATE full | `370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52` | full stream completion | PASS | -| DEFLATE partial | `61b17076c2dfae88da7912d00df534b894cf6d27178863c6d8e91f8617ebb91e` | offset 122900, 512 bytes: `fa381301af1b62fa259addbe7ae427fd54486abc7604ea7619e7a9c47965606d`; offset 736700, 1024 bytes: `be1d2941b054626376fa58155ce0ef8d6357dd0defcbf9eaa37d19ff6098873c` | PASS | -| DEFLATE corrupt | `read_errno=5`, failure after 65536 output bytes | target extent only | PASS | -| MicroLZMA full | `370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52` | full input consumed | PASS | -| MicroLZMA partial | `5a840803f5372b7be1db70713fed7705bf6e982ca2fd319f722c21f094f6c8dd` | offset 65500, 2048 bytes: `49c231f92dde0b0104d7e5b3a01d918dde818c3a6dca05393e2a424d01c214f2`; offset 524287, 4097 bytes: `b8e80c144eacd1f8863c72eab66272379923f0d738f63576ab8286455e0dde8c` | PASS | -| MicroLZMA corrupt | `read_errno=5`, zero output bytes | target pcluster only | PASS | -| ZSTD full | `370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52` | formal FreeBSD API | PASS | -| ZSTD partial | `5a840803f5372b7be1db70713fed7705bf6e982ca2fd319f722c21f094f6c8dd` | same two boundary/random hashes as MicroLZMA | PASS | -| ZSTD corrupt | `read_errno=5`, zero output bytes | target pcluster only | PASS | -| HEAD2 full | `7e2f40362554f4460e80ec2f8d91f4e6c04a8e58f2980d637b2d383a7aa3b3f8` | offset 65500, 4096 bytes: `f0a80e3217f54897eae271b6e570c862abcffad3a4ffd19ffec0444ee06ae721` | PASS | -| HEAD2 corrupt | `read_errno=5`, zero output bytes | patched HEAD2 pcluster | PASS | -| interlaced full | `7e2f40362554f4460e80ec2f8d91f4e6c04a8e58f2980d637b2d383a7aa3b3f8` | offset 16240, 8192 bytes across first compressed/plain transition: `b1387900e55e5672944f8299fe66ac9542007f2b0cbed81d5974831d6040cdae` | PASS | -| extent records | static format/control-flow review only | erofs-utils 1.8.6 cannot emit | MKFS-UNAVAILABLE | - -## Fragment-backed metabox results - -| Case | Actual | Status | -|---|---|---| -| positive file | SHA256 `4536c1d7121f48829475f29179f54baa57154b4ef817cf0776f81782585d29ad` | PASS | -| shared-prefix xattr | `shared-value` | PASS | -| per-file xattr | `value-000` | PASS | -| self-loop | mount exit 1; `packed inode nid=38 is not a non-recursive regular file: Integrity check failed` | PASS | -| out-of-range | mount exit 1; `Integrity check failed` | PASS | -| metabox NID bit 63 | mount exit 1; `Integrity check failed` | PASS | -| packed NID bit 63 | mount exit 1; `Integrity check failed` | PASS | - -The `vmstat -m` EROFS row after the matrix showed zero active allocations: - -```text -erofs 0 0 240695 16,32,64,128,256,384,1024,2048,4096,8192,16384,32768,65536 -``` - -The cumulative allocation count increased as expected; the active allocation -and active-byte columns were both zero. - -## Regression matrix - -| Regression | Actual | Status | -|---|---|---| -| LZ4 legacy full index | SHA256 `370eb0a8df86868c4842ca535ed64670f0277ea2ed47a703f089bbb13ee4ac52` | PASS | -| LZ4 compact index | same SHA256 | PASS | -| LZ4 64 KiB big pcluster | same SHA256 | PASS | -| LZ4 all-fragments | SHA256 `a3a83e5c524b5ed446a06ce78cf407192a0c80119f15d2bc3489a50515eb49e0` | PASS | -| LZ4 ztailpacking | SHA256 `e2aa4a0a0cbcf422f397c7069a38ae0f073781386958e7db0dfa3ff2ca075513` | PASS | -| xattr/metabox | file SHA256 `4536c1...`; xattr `value-000` | PASS | -| single-device chunk | SHA256 `de29abbd47ecd9136f64f22f73fcb40bce1718a8865e6282e74953aa1df80c44` | PASS | -| external compressed LZ4 extent | complete SHA256 `370eb0...` | PASS | -| external compressed boundary | offset 65500, 4096 bytes: `0d66627577218a39a620e8b28d8c8d64b974b184063c52cb0cceeaf0c297c0db` | PASS | -| local NFSv3/TCP over external compressed extent | complete SHA256 `370eb0...` | PASS | - -The external compressed fixture declared slot 1 at unified block 4, stored the -real two-block LZ4 pcluster in the external provider, and zeroed the -corresponding bytes in the primary image. Primary SHA256 was -`5625612426d68624c77dd94b09754da583fc6bd90767e9e9d958e4fc89131a4d`; -blob SHA256 was -`0d71102ce471af0a30d4d61a1fbeef1fc33437c85651d7c7f9c60ebd305ec9d0`. - -## NFS startup-race investigation - -The first regression run started rpcbind, mountd, and nfsd and immediately -called `mount_nfs`. The client printed one transient -`RPCPROG_NFS: RPC: Program not registered`, then retried successfully and read -the correct complete SHA256. No dmesg line changed. - -A separate clean rerun waited for: - -```sh -rpcinfo -t 127.0.0.1 nfs 3 -``` - -Readiness succeeded on attempt 2: - -```text -program 100003 version 3 ready and waiting -``` - -The subsequent NFS mount emitted no RPC warning and produced the same complete -SHA256. This proves the first message was a user-space service-registration -race, not an EROFS or NFS data-path failure. - -## dmesg - -Core, regression, and clean NFS snapshots were each byte-identical before and -after their respective test matrices. All six snapshot files had SHA256: - -```text -2aa3500d33a7cfbe0db87854d3427231cda9cf92b7f85e553f957231f7359680 -``` - -There was no new panic, trap, decompression diagnostic, integrity message, GEOM -orphan warning, or NFS kernel message. - -## Cleanup - -Every test used a trap that unmounted the current EROFS/NFS mount, detached the -specific md unit, stopped NFS services in client-first order, restored or -removed `/etc/exports`, and unloaded the module by the observed KLD ID. - -Final audits after the core, regression, disabled-ZSTD LZ4, and clean NFS runs -all reported: - -```text -erofs mounts=0 -NFS mounts=0 -md units=0 -erofs modules=0 -nfsd/mountd/rpcbind processes=0 -``` - -## Limitations - -- **MKFS-UNAVAILABLE**: erofs-utils 1.8.6 cannot generate the new on-disk extent - record format. Extent coverage is independent static ABI/control-flow review - only. No newer-tool extent image is counted as a FreeBSD PASS. -- METABOX and the transformed HEAD2/partial-reference fixtures require newer - format-aware tooling plus byte-level assertions. Their guest reads are real - FreeBSD kernel tests; fixture creation and mutations are documented and - checksum-validated. -- No CI, automated runner, benchmark threshold, memory-pressure run, or forced - OOM scenario was added. The requested functional manual matrix and active - allocation checks were completed. -- Kernel failures: none. diff --git a/tests/results/manual/2026-08-08T2337Z-metadata-vfs/manual-test-report.md b/tests/results/manual/2026-08-08T2337Z-metadata-vfs/manual-test-report.md deleted file mode 100644 index 29f5ce1..0000000 --- a/tests/results/manual/2026-08-08T2337Z-metadata-vfs/manual-test-report.md +++ /dev/null @@ -1,358 +0,0 @@ -# repo22 Metadata and VFS Manual Test Report - -Started: 2026-08-08 23:37 UTC -Completed: 2026-08-09 UTC -Baseline: `c208bf1f4b8d7a85777f7fe45e8c6e8d3a9f2d1a` -Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG -FreeBSD source reference: `/work/dev-freebsd-releng`, releng/15.0 -Linux source reference: `/work/dev-src-linux/fs/erofs` -Host production tool: erofs-utils 1.8.6 - -## Result - -**PASS** for the implemented metadata/VFS changes and every runnable required -FreeBSD 15 regression. - -Two limitations are recorded rather than misreported as PASS: - -1. The Linux host had no loop provider/EROFS kernel mount path, so the - deterministic nonzero-padding fixture passed Linux erofs-utils 1.8.6 - `fsck.erofs` and `dump.erofs`, while the Linux kernel mount is - **ENVIRONMENT-UNAVAILABLE**. -2. A real TC010 48-bit `statfs` mount needs a provider as large as the declared - multi-terabyte image. The corrected test records this as a provider/tool - requirement; no small-media mount is called a positive PASS. - -An initial NFS stress run completed all data and metadata assertions but failed -cleanup because `service nfsd onerestart` inherited the deliberately open NFS -client descriptor. The procedure was corrected to run the service command with -`3<&-`; the complete stress/background/cleanup sequence then passed. The first -cleanup failure is retained here as evidence for the TC132 ordering fix. - -## Baseline and Scope Audit - -- `HEAD` and `FETCH_HEAD` both resolved to the required baseline. -- `find /work -name AGENTS.md -type f -print` returned no paths. -- Only `repo-community/repo22` was modified. -- Existing untracked `build/` objects and historical manual-test artifacts were - preserved and excluded from staging. -- No CI implementation, binary fixture, overlay, image, or VM artifact is part - of the intended commit. - -## Build and ABI Validation - -Commands: - -```sh -EROFS_ZSTDIO=0 ./build.sh -EROFS_ZSTDIO=1 ./build.sh -nm -u module.ko | awk '$NF == "bcmp" { n++ } END { print n + 0 }' -git diff --check -- repo-community/repo22 -``` - -Results: - -| Build | SHA256 | `bcmp` count | Result | -|---|---|---:|---| -| ZSTDIO disabled | `031038ef195497dc6a1d840d55b293292e051fb888c679c89c8cfbd19b56d525` | 0 | PASS | -| ZSTDIO enabled | `16166b9ffc96a532bda9925513e10df11f4cf8a06b3c15afbc09b1a4e4b9d2df` | 0 | PASS | - -Both modules loaded and unloaded on FreeBSD 15. The final unload used the -observed KLD ID so arbitrary copied filenames did not affect cleanup. - -FreeBSD 15 source inspection confirmed the exact current ABI: - -```text -vnode_pager_local_getpages(struct vop_getpages_args *) -vnode_pager_local_getpages_async(struct vop_getpages_async_args *) -``` - -The local ext2 vnode vector registers both functions directly. EROFS now does -the same. - -## Deterministic Fixture Evidence - -The checked-in `prepare-fixtures.sh` was syntax-checked and rerun into separate -untracked `repro3-*` directories. It reproduced the expected byte-identical -inline, special, pager, NFS, and nlink images and generated the current namei -variants. - -Key structural evidence: - -```text -inline_nid=39 inode_off=1248 inode_blockoff=1248 xattr_icount=0->695 -inline_data_blockoff=4068 inline_size=31 -wide_nid=42 block=6 dirents=80 last_nameoff=4043 padding_patch=4084:4092 -nlink1_nid=43 i_format_bit4=1 i_nb=0x1234 -special compact rdev raw_u=0x543abc21 -special extended rdev raw_u=0x543abc21 -``` - -Generated image hashes: - -| Image | SHA256 | -|---|---| -| `inline.erofs` | `0435b3ea748a88cccbdc6390dec4285a3706bec3dc09de58aa1808544ebc63d0` | -| `inline-cross-block.erofs` | `63ebb7632687b564beb4c9dd8036eb4ac63c4495f63061ad7eec1c134c656932` | -| `special-compact.erofs` | `fd78256dd83d9d6d957e5f843c7a8e8a175a4b3243d528bebd299b0226853237` | -| `special-extended.erofs` | `e73e9b84d9ceb8c2b07e9c2732733b0fd607736c68c09522a2402fbeef6ba8d5` | -| `namei-base.erofs` | `d1730ff23836797c6c09e1b39b1cf23efc16e27f85ab07fdcab577bb82871659` | -| `namei-padding-nonzero.erofs` | `9a94e9af2cab264b9c11975a20d78e615d6fe1e6f86267173cb5ac86aecb2b17` | -| `namei-corrupt-short.erofs` | `fb89f74795a5569ed3a85d63836dd75a06e1f17823d0508048c37da710ea6e75` | -| `namei-corrupt-nameoff.erofs` | `b0b70ee615f163430f04edb91ab76c27ee8cc9a75b8ebb2008834f5b550e3933` | -| `namei-corrupt-name.erofs` | `6a980ad3e241603eda2ef71a470c82975291c614366a401e4e89c17c9adf9b91` | -| `pager-plain.erofs` | `36596edea5bfbaa1157f6c142095a7ee9949b5df1b1a35620c0d22ec853f3c09` | -| `pager-lz4.erofs` | `b06daee6b02a6ebe967655be760b496c8a9d922cf47acb5c840e82a48a34c51d` | -| `nfs-a.erofs` | `6d86dcf620b007d069895e3e94a21a74dadcf35e84c92dc4f7c21a2ec23bd901` | -| `nfs-b.erofs` | `18913fd319daca20b3e4d30a89c05c416b4d3ca396394509e5112d548674f4c2` | -| `nlink.erofs` | `798eb81b3ba7270ee653b00adeba47a6e03c982adef1982bf5b4e0935669ae83` | -| `nlink1-patched.erofs` | `19fd85f32ed89117d8e02bc19ca09655dd3bae9152cf693a399eda2135233042` | - -The padding patch was followed by a rigorous CRC32C recomputation over the -superblock block. Linux erofs-utils produced the exact payload: - -```text -wide entry 079 -``` - -The Linux kernel comparison command failed before mount with “failed to setup -loop device”; it is therefore not labeled PASS. - -## TC147: FLAT_INLINE Bounds - -Commands included fresh md attach/mount for the base and cross-block images, -then cold `cat`/`stat` access. - -Observed on the final module: - -```text -positive hash=0347f272ba395aff6df5fd824a7c552fa26d7f017283f0544136385abef31b01 stat=31 8 -corrupt stat: Integrity check failed -corrupt stat: Integrity check failed -corrupt cat: Integrity check failed -dmesg_before=122 dmesg_after=122 mounts=0 mds= module_rc=1 -``` - -The positive file read exactly. The checksum-valid corrupt inode failed with -`EINTEGRITY` before its 31-byte inline range could cross the metadata block. -The mapping path also uses checked additions, and primary/metabox declared -bounds are validated at inode decode. - -Self-review first rejected the old `i_xattr_icount=1020` mutation because it -merely moved the inline data into the next block without crossing that block. -The corrected value `695` places the tail at block offset 4068. Its first -genuine rerun exposed a stale constructing vnode: the second `stat` returned -`EBADF`. The failure path now calls `vgone()` before `vput()`, and the complete -final rerun above returned `EINTEGRITY` for every repeated access. - -## TC022/TC056: Special `st_rdev` - -The source fixture used real Linux char/block nodes with major `2748`, minor -`344865`, plus a FIFO. `stat_special.c` checked `st_rdev` directly because -FreeBSD `stat -f %Lr` truncates before `minor()` for large values. - -Compact and extended results were identical: - -```text -PASS char rdev=0xa430005bc21 major=2748 minor=344865 -PASS block rdev=0xa430005bc21 major=2748 minor=344865 -PASS fifo rdev=0xffffffffffffffff -``` - -This proves Linux `new_decode_dev(0x543abc21)` followed by FreeBSD `makedev()`; -a little-endian integer cast would not produce this FreeBSD `dev_t`. - -## TC055: Real Compressed Allocation - -For every row, full FreeBSD kernel reads matched the expected SHA256 and -`st_blocks * 512` matched the inode's real on-disk compressed size: - -| Shape | File | Size | Allocated bytes | SHA256/result | -|---|---|---:|---:|---| -| LZ4 full | `shape.dat` | 1048576 | 8192 | `370eb0a8...` PASS | -| LZ4 compact | `shape.dat` | 1048576 | 8192 | `370eb0a8...` PASS | -| LZ4 fragment | `fragment.dat` | 1048699 | 0 | `a3a83e5c...` PASS | -| LZ4 ztailpacking | `inline.dat` | 65536 | 0 | `e2aa4a0a...` PASS | -| MicroLZMA partial A | `a.dat` | 1048576 | 4096 | `370eb0a8...` PASS | -| MicroLZMA partial B | `b.dat` | 700000 | 4096 | `5a840803...` PASS | -| DEFLATE compact/partial | `a.dat` | 1048576 | 36864 | `370eb0a8...` PASS | -| DEFLATE full/partial | `b.dat` | 1050624 | 8192 | `61b17076...` PASS | -| ZSTD partial A | `a.dat` | 1048576 | 4096 | `370eb0a8...` PASS | -| ZSTD partial B | `b.dat` | 700000 | 4096 | `5a840803...` PASS | - -Uncompressed regressions: - -```text -inline size=31 st_blocks=8 -plain size=21211 st_blocks=48 -chunk size=90017 st_blocks=176 -``` - -Representative compression image hashes were: - -```text -LZ4 full d784f8dc... LZ4 compact 37942ef1... -LZ4 fragment a4e5d40e... LZ4 ztail 035069eb... -MicroLZMA 2f4bd89d... DEFLATE 8bc720a1... ZSTD 7f5ee4f8... -``` - -## TC141/TC148: Directory Compatibility and Strictness - -FreeBSD results: - -- Cold `/alpha/bravo/charlie/payload.txt`: PASS without parent warming. -- Repeated cold lookup and post-negative-cache existing lookup: PASS. -- Patched nonzero tail bytes: accepted. -- `wide` enumeration: 320 files plus `.` and `..`; 322 dirents through a - 128-byte buffer and restart cookies. -- Short block, non-monotonic `nameoff`, and `/` in an on-disk name: lookup and - readdir both returned `EINTEGRITY`. -- Repeated corrupted lookup remained `EINTEGRITY`, not cached `ENOENT`. - -The duplicate validators were removed; lookup and readdir use the shared -helper in `dir.c`. - -## TC132/TC133: NFS Generation and Stress - -Direct handle validation after the final superblock-hash implementation: - -```text -nfs-a: fsid=00000034:000000e0 nid=0x2e gen=849213208 -nfs-b: fsid=00000034:000000e0 nid=0x2e gen=4011239099 -``` - -- `nfs-a` remount on the same md unit produced byte-identical complete handles. -- `va_gen` equaled handle generation. -- Replacing `nfs-a` with `nfs-b` on the same md unit made the old handle return - `ESTALE` through both `fhstat` and `fhopen`. -- Bad length/pad returned `EINVAL`; bad generation/NID returned `ESTALE`. -- Handle generation mutation used `gen_xor 1`, not a hard-coded value. - -NFSv3/TCP READDIRPLUS stress: - -- Four clients mounted; requested 512/1024/4096 readdir sizes were clamped by - FreeBSD to 8192, while default was 65536. -- All four 12,050-name sorted listings had SHA256 - `4fbf1b6103e8884223629c3fd03f4ec36061e65cd2a1becfc96670daa96f9d71`. -- Twelve traversal workers and eight cat/stat controller jobs were waited by - PID; every exit status was zero. -- READDIRPLUS client/server count was 1304 in the recorded full run. -- RPC timeouts, invalid replies, retries, and server write RPCs were zero. -- The 21,757,952-byte throughput file hash was - `0841effed82d1adf394b6834ce30d4d9eb5fc9427527e854ec1cb6bb4b86c119`. -- nfsd restart with an open descriptor passed after the service command closed - inherited fd 3. -- Corrected full rerun ended `NFS-STRESS-CLEAN-PASS`. - -## TC149: Real Pager Faults - -`tests/mmap_fault.c` was compiled natively on FreeBSD 15 and run against both -plain and LZ4 images. - -Both runs printed: - -```text -PASS size=21211 pages=6 fnv1a64=a1890a1c216724be random-faults=6 \ -eof-zero=PASS sigbus=PASS private-cow=PASS -``` - -The helper verified deterministic random faults after `MADV_DONTNEED`, full -mapping equality with `pread`, partial EOF-page zeroes, child `SIGBUS` on the -next full page, `MAP_SHARED` write denial, FreeBSD private COW semantics, and -`O_RDWR -> EROFS`. - -dmesg added exactly the two expected child exits on signal 10 (`SIGBUS`). No -parent crash, VM assertion, trap, panic, or dirty writeback appeared. - -## Additional Regressions - -| Area | Evidence | Result | -|---|---|---| -| Inline/system xattr | trusted, security, long-prefix, user values exact | PASS | -| Shared xattr | shared and per-inode values exact | PASS | -| Metabox xattr | `dirA/nested.txt` and `hello.txt` enumerated/read | PASS | -| Fragment-backed metabox | positive file/xattrs exact; self-loop and range images rejected | PASS | -| Single-device chunk | `plain.bin`, `deep/payload.bin` hashes exact | PASS | -| Chunk multidevice | block-map and indexed files hashes exact | PASS | -| External compressed multidevice | complete 1 MiB LZ4 SHA256 `370eb0a8...` | PASS | -| Compact nlink rules | explicit nlink 1/2 plus flagged nlink-one image | PASS | -| ZSTD enabled final load/read | final module and `zstd-partial-ref` read | PASS | - -Supporting fixture hashes: - -```text -system-inline-four 6f521b62... -shared xattr 208b61ca... -metabox xattr ef8d619c... -chunk single 7aa8db22... -chunk multidev 73343b70... + a36c2b9b... -external LZ4 56256124... + 0d71102c... -NFS stress image 6928f05b... -``` - -The final-module fragment-backed metabox rerun used the previously qualified, -checksum-valid image with SHA256 -`8a9a62bd203994711b8272192915d811e6c3de23e07ad9607dd63e66cc109bcd`. -`/tree/d00/file000.txt` produced SHA256 -`4536c1d7121f48829475f29179f54baa57154b4ef817cf0776f81782585d29ad`; -the shared and per-file xattrs were `shared-value` and `value-000`. The -self-loop and out-of-range images returned `Integrity check failed`. dmesg -remained at 122 lines, and the post-run audit showed zero mounts, md providers, -and loaded EROFS modules. - -## Final-Binary Closure Rerun - -After the `vgone()` error-path fix, the final ZSTDIO module SHA256 -`16166b9ffc96a532bda9925513e10df11f4cf8a06b3c15afbc09b1a4e4b9d2df` -was used for one continuous closure matrix: - -- LZ4 full/compact/fragment/ztailpacking, MicroLZMA partial, DEFLATE - compact/full partial, and ZSTD partial hashes and `st_blocks` all matched. -- Inline trusted/security/long-prefix/user xattrs matched exact values. -- Metabox shared xattrs returned `answer=forty-two`; the fragment carrier file - and `repo22.item-000=value-000` matched. -- Single-device chunk, external chunk provider, and external compressed LZ4 - hashes matched; compressed `st_blocks=16`. -- The final module preserved the same NFS handle across same-md remount, made - the old handle `ESTALE` after image replacement, synchronized `va_gen`, and - completed a real local NFSv3/TCP client read with matching SHA256. - -The first closure script stopped after the system-xattr row because it queried -an obsolete metabox attribute name. Its trap left zero mounts, md providers, -modules, and services. The corrected `answer`/`metaboxshared` queries and all -remaining rows passed. The complete final audit was: - -```text -dmesg_before=122 dmesg_after=122 -erofs_mounts=0 nfs_mounts=0 mds= module_rc=1 services=0/0/0 -FINAL-REGRESSION-REST-PASS -``` - -## Final Environment and Cleanup - -Final guest audit: - -```text -mounts=0 -mds= -modules=0 -services=0 -final_dmesg_before=122 final_dmesg_after=122 -``` - -The final dmesg tail contained only the historical pre-test duplicate-module -diagnostic and the two intentional pager-child SIGBUS exits. The final -compression, xattr, chunk, multidevice, generation, and NFS closure pass added -no dmesg lines. - -Guest helper hashes: - -| Helper | SHA256 | -|---|---| -| `mmap_fault` | `b19c9c28a7abdfebe4243e3f4876b711eebc1c02b9856c15cb8bf15ae2da3a65` | -| `nfs_fh_tool` | `0dcad233cc8768ca84569c8d030939b526d50c64190fc54d5f0de013191f11d6` | -| `stat_special` | `b5c0eabd06541268e799df4f72fa7f37a460617bd8960f918b27710483db3841` | -| `readdir_probe` | `44747ccd8d9a285a37d8049b7d61d00946db88482c08a3570e59b5a2fa1b4ea2` | - -No password material was printed into this report or written into tracked -files. diff --git a/tests/results/manual/2026-08-08T2337Z-metadata-vfs/prepare-fixtures.sh b/tests/results/manual/2026-08-08T2337Z-metadata-vfs/prepare-fixtures.sh deleted file mode 100755 index 925f194..0000000 --- a/tests/results/manual/2026-08-08T2337Z-metadata-vfs/prepare-fixtures.sh +++ /dev/null @@ -1,277 +0,0 @@ -#!/bin/sh -set -eu - -script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) -fixture_dir=${FIXTURE_DIR:-"$script_dir/fixture"} -artifact_dir=${ARTIFACT_DIR:-"$script_dir/artifacts"} - -for tool in mkfs.erofs fsck.erofs dump.erofs python3 sha256sum; do - command -v "$tool" >/dev/null 2>&1 || { - echo "missing tool: $tool" >&2 - exit 1 - } -done -test "$(id -u)" -eq 0 || { - echo "root is required to create device-node fixtures" >&2 - exit 1 -} -test ! -e "$fixture_dir" || { - echo "fixture directory already exists: $fixture_dir" >&2 - exit 1 -} -test ! -e "$artifact_dir" || { - echo "artifact directory already exists: $artifact_dir" >&2 - exit 1 -} - -mkdir -p \ - "$fixture_dir/inline" \ - "$fixture_dir/special" \ - "$fixture_dir/namei/alpha/bravo/charlie" \ - "$fixture_dir/namei/alpha/sibling" \ - "$fixture_dir/namei/wide" \ - "$fixture_dir/pager" \ - "$fixture_dir/nfs/basic/subdir" \ - "$fixture_dir/nlink/subdir" \ - "$artifact_dir" - -printf 'inline-tail-payload-0123456789\n' > "$fixture_dir/inline/inline.txt" -printf 'special target\n' > "$fixture_dir/special/target" -ln -s target "$fixture_dir/special/link" -mknod "$fixture_dir/special/char-large" c 2748 344865 -mknod "$fixture_dir/special/block-large" b 2748 344865 -mkfifo "$fixture_dir/special/fifo" - -printf 'cold nested lookup payload\n' > \ - "$fixture_dir/namei/alpha/bravo/charlie/payload.txt" -printf 'repeat lookup payload\n' > \ - "$fixture_dir/namei/alpha/bravo/repeat.txt" -printf 'sibling marker\n' > "$fixture_dir/namei/alpha/sibling/marker.txt" -index=0 -while [ "$index" -lt 320 ]; do - name=$(printf 'entry-%03d-abcdefghijklmnopqrstuvwxyz.txt' "$index") - printf 'wide entry %03d\n' "$index" > "$fixture_dir/namei/wide/$name" - index=$((index + 1)) -done - -python3 - "$fixture_dir" <<'PY' -from pathlib import Path -import sys - -root = Path(sys.argv[1]) -data = bytes(((index * 131 + 17) ^ (index >> 3)) & 0xff - for index in range(5 * 4096 + 731)) -(root / "pager" / "pager.bin").write_bytes(data) -(root / "nfs" / "basic" / "regular.txt").write_text( - "stable file handle payload\n", encoding="ascii") -(root / "nfs" / "basic" / "subdir" / "child.txt").write_text( - "child\n", encoding="ascii") -PY -ln -s regular.txt "$fixture_dir/nfs/basic/link-to-regular" -mkfifo "$fixture_dir/nfs/basic/test.fifo" - -printf 'single\n' > "$fixture_dir/nlink/single.txt" -printf 'hardlinked\n' > "$fixture_dir/nlink/hard-a.txt" -ln "$fixture_dir/nlink/hard-a.txt" "$fixture_dir/nlink/hard-b.txt" -printf 'child\n' > "$fixture_dir/nlink/subdir/child.txt" - -find "$fixture_dir" -exec touch -h -t 197001010000.00 {} + - -build_image() -{ - uuid=$1 - image=$2 - source=$3 - shift 3 - mkfs.erofs -d0 -x-1 -T0 --all-time --all-root --workers=1 \ - -U "$uuid" "$@" "$artifact_dir/$image" "$fixture_dir/$source" -} - -build_image 11111111-2222-3333-4444-555555555551 inline.erofs inline -build_image 11111111-2222-3333-4444-555555555552 \ - special-compact.erofs special -E force-inode-compact -build_image 11111111-2222-3333-4444-555555555553 \ - special-extended.erofs special -E force-inode-extended -build_image 11111111-2222-3333-4444-555555555554 namei-base.erofs namei -build_image 11111111-2222-3333-4444-555555555555 \ - pager-plain.erofs pager -E noinline_data -build_image 11111111-2222-3333-4444-555555555556 \ - pager-lz4.erofs pager -z lz4 -build_image aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeee1 nfs-a.erofs nfs -build_image aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeee2 nfs-b.erofs nfs -build_image 11111111-2222-3333-4444-555555555557 \ - nlink.erofs nlink -E force-inode-compact - -ARTIFACT_DIR="$artifact_dir" python3 <<'PY' -from pathlib import Path -import hashlib -import os -import struct - -artifact_dir = Path(os.environ["ARTIFACT_DIR"]) - - -def u16(image, offset): - return struct.unpack_from("> 1) ^ ( - polynomial if checksum & 1 else 0) - put_u32(image, 1028, checksum & 0xFFFFFFFF) - - -def inode_offset(image, nid): - block_bits = image[1036] - meta_blkaddr = u32(image, 1064) - return (meta_blkaddr << block_bits) + (nid << 5) - - -def compact_inode(image, nid): - offset = inode_offset(image, nid) - inode_format = u16(image, offset) - assert (inode_format & 1) == 0 - return offset, (inode_format >> 1) & 7, u32(image, offset + 8) - - -def inline_dir_entries(image, nid): - inode = inode_offset(image, nid) - inode_format = u16(image, inode) - inode_size = 64 if inode_format & 1 else 32 - layout = (inode_format >> 1) & 7 - size = (struct.unpack_from("= 12 and first_nameoff % 12 == 0 - count = first_nameoff // 12 - entries = [] - for index in range(count): - entry = data + index * 12 - child_nid, nameoff = struct.unpack_from(" block_size -inline_cross = bytearray(inline) -put_u16(inline_cross, inline_inode + 2, inline_cross_xattr_icount) -write_image("inline-cross-block.erofs", inline_cross) - -namei = bytearray((artifact_dir / "namei-base.erofs").read_bytes()) -root_nid = u16(namei, 1038) -root_inode, root_data, root_size, root_entries = inline_dir_entries( - namei, root_nid) -wide_nid = next(nid for nid, _, _, name in root_entries if name == b"wide") -wide_inode, wide_layout, _ = compact_inode(namei, wide_nid) -assert wide_layout == 2 -block_bits = namei[1036] -wide_block = u32(namei, wide_inode + 16) << block_bits -wide_first_nameoff = u16(namei, wide_block + 8) -wide_count = wide_first_nameoff // 12 -wide_last_nameoff = u16(namei, wide_block + (wide_count - 1) * 12 + 8) -padding_nul = namei.index(0, wide_block + wide_last_nameoff, - wide_block + (1 << block_bits)) - -nonzero_padding = bytearray(namei) -nonzero_padding[padding_nul + 1:padding_nul + 9] = b"PAD!ERO!" -write_image("namei-padding-nonzero.erofs", nonzero_padding) - -short_block = bytearray(namei) -put_u32(short_block, root_inode + 8, 8) -write_image("namei-corrupt-short.erofs", short_block) - -bad_nameoff = bytearray(namei) -first_nameoff = u16(bad_nameoff, root_data + 8) -put_u16(bad_nameoff, root_data + 12 + 8, first_nameoff) -write_image("namei-corrupt-nameoff.erofs", bad_nameoff) - -bad_name = bytearray(namei) -slash_offset = wide_block + wide_last_nameoff + 5 -assert bad_name[slash_offset] not in (0, ord("/")) -bad_name[slash_offset] = ord("/") -write_image("namei-corrupt-name.erofs", bad_name) - -nlink = bytearray((artifact_dir / "nlink.erofs").read_bytes()) -nlink_root_nid = u16(nlink, 1038) -_, _, _, nlink_entries = inline_dir_entries(nlink, nlink_root_nid) -single_nid = next(nid for nid, _, _, name in nlink_entries - if name == b"single.txt") -single_inode, _, _ = compact_inode(nlink, single_nid) -nlink1 = bytearray(nlink) -put_u16(nlink1, single_inode, u16(nlink1, single_inode) | 0x10) -put_u16(nlink1, single_inode + 6, 0x1234) -write_image("nlink1-patched.erofs", nlink1) - -with (artifact_dir / "fixture-evidence.txt").open("w", encoding="ascii") as out: - out.write(f"inline_nid={inline_nid} inode_off={inline_inode} " - f"inode_blockoff={inline_inode % block_size} " - f"xattr_icount=0->{inline_cross_xattr_icount} " - f"inline_data_blockoff={inline_data_off % block_size} " - f"inline_size={inline_size}\n") - out.write(f"wide_nid={wide_nid} block={wide_block >> block_bits} " - f"dirents={wide_count} last_nameoff={wide_last_nameoff} " - f"padding_patch={padding_nul + 1 - wide_block}:" - f"{padding_nul + 9 - wide_block}\n") - out.write(f"nlink1_nid={single_nid} i_format_bit4=1 i_nb=0x1234\n") - for image_name in ("special-compact.erofs", "special-extended.erofs"): - image = bytearray((artifact_dir / image_name).read_bytes()) - special_root = u16(image, 1038) - _, _, _, entries = inline_dir_entries(image, special_root) - out.write(image_name + "\n") - for nid, _, _, name in entries: - if name not in (b"char-large", b"block-large", b"fifo"): - continue - offset = inode_offset(image, nid) - out.write(f" {name.decode()} nid={nid} inode_size=" - f"{64 if u16(image, offset) & 1 else 32} " - f"raw_u={u32(image, offset + 16):#010x}\n") - -with (artifact_dir / "SHA256SUMS").open("w", encoding="ascii") as sums: - for path in sorted(artifact_dir.glob("*.erofs")): - sums.write(f"{hashlib.sha256(path.read_bytes()).hexdigest()} " - f"{path.name}\n") -PY - -fsck.erofs -d1 "$artifact_dir/namei-padding-nonzero.erofs" -dump.erofs --cat --path=/wide/entry-079-abcdefghijklmnopqrstuvwxyz.txt \ - "$artifact_dir/namei-padding-nonzero.erofs" -cat "$artifact_dir/fixture-evidence.txt" -cat "$artifact_dir/SHA256SUMS" diff --git a/tests/results/manual/2026-08-09T0124Z-final-review/manual-test-report.md b/tests/results/manual/2026-08-09T0124Z-final-review/manual-test-report.md deleted file mode 100644 index ba7de89..0000000 --- a/tests/results/manual/2026-08-09T0124Z-final-review/manual-test-report.md +++ /dev/null @@ -1,166 +0,0 @@ -# repo22 Final Review WIP Manual Test Report - -Started: 2026-08-09 01:24 UTC - -Completed: 2026-08-09 03:33 UTC - -Parent commit: `9a3604fba32cfe2ff263d954d80fe588f99db88f` - -Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG - -Linux reference: `/work/dev-src-linux/fs/erofs` - -FreeBSD reference: `/work/dev-freebsd-releng` - -## Scope - -This run reviews and validates four final correctness findings: - -- Linux-compatible 48-bit superblock union selection; -- rejection of extended inode sizes above FreeBSD `OFF_MAX`; -- bounded reads from multi-GiB explicit extent holes; -- 64-bit directory block-search indexes. - -No CI work, binary fixture, guest overlay, build object, or raw VM artifact is -part of the intended commit. - -## Result - -| Test | Result | Evidence | -|---|---|---| -| TC150 48-bit fallback root | PASS | Real FreeBSD mount, root read, inode and `df` | -| TC151 size above `OFF_MAX` | PASS | Six direct failures, rc 1, syscall errno 97 | -| TC152 bounded extent hole | PASS | Two pread/mmap probes, stable active allocation | -| TC153 large directory index | SHELVED | Host fixture reproducible; kernel run incomplete | - -TC153 is deliberately not marked PASS. See -`issues/TC153-large-directory-block-index-validation.md`. - -## Source Review - -Linux `super.c` initializes `blocks_lo` and uses `rb.blocks_hi` only inside -`48BIT && rootnid_8b`. repo22 now decodes blocks and root NID together under -that same selector. - -FreeBSD exposes signed `off_t` pager and vnode interfaces bounded by -`OFF_MAX`; repo22 rejects a larger decoded inode before vnode/pager setup. - -The extent-hole path now zeroes only the current requested span in -`z_erofs_do_read()` instead of allocating the complete logical extent. - -The directory block search now uses 64-bit bounds and a checked block-offset -multiplication. The within-block search remains 32-bit, matching the validated -block-sized domain. - -## Build Results - -Commands: - -```sh -EROFS_ZSTDIO=0 ./build.sh -EROFS_ZSTDIO=1 ./build.sh -nm -u build/erofs.ko -git diff --check -- repo-community/repo22 -``` - -| Configuration | Module SHA256 | Result | -|---|---|---| -| `EROFS_ZSTDIO=0` | `65bc19d53a2a7f0525bfacadb441dab37ee318f5f5b4c62b5ecd1b5090fe46d2` | PASS | -| `EROFS_ZSTDIO=1` | `d46ca4dfc858deaf4840fad8b8589b16d4b71b34afa96dc7acf1255890c5cef7` | PASS | - -Both freestanding builds completed. Neither module had an unresolved `bcmp` -reference. The ZSTDIO-enabled module was transferred to the guest, loaded as -KLD ID 7, and unloaded after testing. - -## Fixture Evidence - -| Fixture | SHA256 | -|---|---| -| `fallback-48bit-root2.erofs` | `bed3be4dfb8499d4e794b03eddd5b9cda95bc8e5571ae7d765c4852ece0d95a3` | -| `extended-size-bit63.erofs` | `e4a0f550168f1a2911603863d0074d474e61adc787c14c0278c83a060643ee38` | -| `extent-hole-5g.erofs` | `50014a24493918247e36511ad34a2fe8ab47ae09ea46d7fd62a1bed445a6c65f` | -| `large-dir-intmax.erofs` | `0f90d3d57adbbbd946e41b225c1f6c464915c6abb0b13478ec9b2a318def1f72` | - -The TC153 hash is host generator evidence only. - -## TC150 - -The fixture encoded `rootnid_2b=36`, `rootnid_8b=0`, `blocks_lo=1`, and the -48-bit incompat bit. The 4 KiB image mounted on `/dev/md0` and produced: - -```text -content=48-bit fallback root -sha256=d361f537492113ca93cfbf91c06ebc06e2b8b695d8b6e63ad3666013eaa029f0 -root inode=36 -df total=4 one-KiB blocks -``` - -This proves the union was not shifted into a high block count. It does not -replace the large-provider TC010 test. - -## TC151 - -The fixture used extended inode NID 40 at byte 1280 with -`i_size=0x8000000000000000`. The image mounted, but every access to `big.dat` -failed before open or pager setup: - -| Access | Attempts | Direct rc | `truss` result | -|---|---:|---:|---| -| `stat` | 2 | 1, 1 | `fstatat ... ERR#97` | -| `cat` | 2 | 1, 1 | `openat ... ERR#97` | -| `mmap_fault` | 2 | 1, 1 | `openat ... ERR#97` | - -All six errors were `Integrity check failed`. No file descriptor reached the -read or mmap phase. - -## TC152 - -The mounted file size was `5368713216` bytes. The native helper probed offset -`3221225472` with a one-byte pread and one-page private mmap: - -```text -attempt 1: PASS, real 0.03 s -attempt 2: PASS, real 0.02 s -``` - -The `erofs` malloc row was `3` active allocations and `768` active bytes both -before and after. The cumulative allocation counter moved from 89 to 95, as -expected for temporary request buffers; active memory did not scale with the -5 GiB hole. - -## TC153 - -The first 4096-byte fixture was Layout 2 and failed before the target namei -path. A second 65536-byte base with 400 entries was still Layout 2. The tracked -generator now converts the directory to Layout 0 by moving its complete data -to appended blocks and produced a 90112-byte patched image with raw block 16, -22 image blocks, and the hash listed above. - -The corrected fixture was not executed in the FreeBSD kernel during this run. -TC153 remains SHELVED, and the complete attempt history and acceptance criteria -are in its issue document. - -## Cleanup - -The qualified rerun used dmesg line count 123 before and after. It ended with: - -```text -EROFS mounts: 0 -md providers: none -EROFS modules: 0 -``` - -An earlier unqualified probe encountered a preloaded differently named EROFS -KLD and a `truss` exit-status ambiguity. It was discarded. The qualified run -first unloaded that KLD, loaded the exact module hash above, captured direct -command exit codes separately from syscall traces, and then cleaned up. - -## Deferred Issues - -- `issues/TC153-large-directory-block-index-validation.md` -- `issues/TC010-48bit-statfs-large-provider.md` -- `issues/extent-metadata-fixture-unavailable.md` - -The raw guest transcripts remain untracked under `/work/build`. This report, -the deterministic generator, and source/test documentation are the tracked -evidence. diff --git a/tests/results/manual/2026-08-09T0124Z-final-review/prepare-fixtures.sh b/tests/results/manual/2026-08-09T0124Z-final-review/prepare-fixtures.sh deleted file mode 100755 index dee2f81..0000000 --- a/tests/results/manual/2026-08-09T0124Z-final-review/prepare-fixtures.sh +++ /dev/null @@ -1,271 +0,0 @@ -#!/bin/sh -set -eu - -script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) -fixture_dir=${FIXTURE_DIR:-"$script_dir/fixture"} -artifact_dir=${ARTIFACT_DIR:-"$script_dir/artifacts"} - -for tool in mkfs.erofs python3 sha256sum; do - command -v "$tool" >/dev/null 2>&1 || { - echo "missing tool: $tool" >&2 - exit 1 - } -done -test ! -e "$fixture_dir" || { - echo "fixture directory already exists: $fixture_dir" >&2 - exit 1 -} -test ! -e "$artifact_dir" || { - echo "artifact directory already exists: $artifact_dir" >&2 - exit 1 -} - -mkdir -p "$fixture_dir/fallback" "$fixture_dir/oversize" \ - "$fixture_dir/extent" "$fixture_dir/large/huge" "$artifact_dir" - -FIXTURE_DIR="$fixture_dir" python3 <<'PY' -from pathlib import Path -import os - -root = Path(os.environ["FIXTURE_DIR"]) -(root / "fallback" / "root.txt").write_text( - "48-bit fallback root\n", encoding="ascii") -(root / "oversize" / "big.dat").write_bytes(b"x") -(root / "extent" / "hole.dat").write_bytes(b"\0" * (1024 * 1024)) -(root / "large" / "huge" / "anchor.txt").write_text( - "large directory anchor\n", encoding="ascii") -for index in range(400): - (root / "large" / "huge" / - f"entry-{index:03d}-abcdefghijklmnopqrstuvwxyz.txt").write_text( - f"entry {index:03d}\n", encoding="ascii") -PY -find "$fixture_dir" -exec touch -h -t 197001010000.00 {} + - -build_image() -{ - uuid=$1 - image=$2 - source=$3 - shift 3 - mkfs.erofs -d0 -x-1 -T0 --all-time --all-root --workers=1 \ - -U "$uuid" "$@" "$artifact_dir/$image" "$fixture_dir/$source" -} - -build_image 22222222-3333-4444-5555-666666666650 \ - fallback-base.erofs fallback -E force-inode-compact -build_image 22222222-3333-4444-5555-666666666651 \ - oversize-base.erofs oversize -E force-inode-extended -build_image 22222222-3333-4444-5555-666666666652 \ - extent-base.erofs extent -E legacy-compress,force-inode-extended -z lz4 -build_image 22222222-3333-4444-5555-666666666653 \ - large-dir-base.erofs large -E force-inode-extended - -ARTIFACT_DIR="$artifact_dir" python3 <<'PY' -from pathlib import Path -import hashlib -import os -import struct - -artifact_dir = Path(os.environ["ARTIFACT_DIR"]) -SUPER = 1024 -FEATURE_INCOMPAT = SUPER + 80 -ROOTNID_2B = SUPER + 14 -BLOCKS_LO = SUPER + 36 -META_BLKADDR = SUPER + 40 -ROOTNID_8B = SUPER + 112 -EROFS_FEATURE_INCOMPAT_48BIT = 0x80 -EROFS_INODE_COMPRESSED_FULL = 1 -Z_EROFS_ADVISE_EXTENTS = 0x1 -Z_EROFS_EXTENT_RECSZ_16 = 0x4 -HOLE_SIZE = 5 * 1024 * 1024 * 1024 + 4096 -HUGE_DIR_BLOCKS = (1 << 31) + 1 - - -def u16(image, offset): - return struct.unpack_from("> 1) ^ ( - polynomial if checksum & 1 else 0) - put_u32(image, SUPER + 4, checksum & 0xFFFFFFFF) - - -def inode_offset(image, nid): - block_bits = image[SUPER + 12] - return (u32(image, META_BLKADDR) << block_bits) + (nid << 5) - - -def inode_info(image, nid): - offset = inode_offset(image, nid) - inode_format = u16(image, offset) - inode_size = 64 if inode_format & 1 else 32 - size = u64(image, offset + 8) if inode_size == 64 else u32( - image, offset + 8) - xattr_count = u16(image, offset + 2) - xattr_size = 0 if xattr_count == 0 else 12 + 4 * (xattr_count - 1) - layout = (inode_format >> 1) & 7 - return offset, inode_format, inode_size, xattr_size, layout, size - - -def directory_entries(image, nid): - offset, _, inode_size, xattr_size, layout, size = inode_info(image, nid) - block_size = 1 << image[SUPER + 12] - assert 0 < size <= block_size - if layout == 2: - data = offset + inode_size + xattr_size - elif layout == 0: - data = u32(image, offset + 16) << image[SUPER + 12] - else: - raise AssertionError(f"unsupported directory layout {layout}") - first_nameoff = u16(image, data + 8) - assert first_nameoff >= 12 and first_nameoff % 12 == 0 - count = first_nameoff // 12 - entries = [] - for index in range(count): - entry = data + index * 12 - child_nid = u64(image, entry) - nameoff = u16(image, entry + 8) - endoff = (u16(image, entry + 20) - if index + 1 < count else size) - name = bytes(image[data + nameoff:data + endoff]).split(b"\0", 1)[0] - assert name - entries.append((name, child_nid)) - return entries - - -def child_nid(image, parent_nid, name): - return next(nid for entry_name, nid in directory_entries(image, parent_nid) - if entry_name == name) - - -def convert_inline_directory_to_plain(image, nid): - offset, inode_format, inode_size, xattr_size, layout, size = inode_info( - image, nid) - assert layout == 2 and inode_size == 64 and size > 0 - block_size = 1 << image[SUPER + 12] - tail_size = size % block_size - assert tail_size > 0 - raw_block = u32(image, offset + 16) - full_size = size - tail_size - inline_offset = offset + inode_size + xattr_size - directory_data = bytes( - image[raw_block * block_size:raw_block * block_size + full_size] + - image[inline_offset:inline_offset + tail_size]) - assert len(directory_data) == size - - new_raw_block = (len(image) + block_size - 1) // block_size - image.extend(b"\0" * (new_raw_block * block_size - len(image))) - image.extend(directory_data) - image.extend(b"\0" * (-len(image) % block_size)) - put_u16(image, offset, inode_format & ~(7 << 1)) - put_u32(image, offset + 16, new_raw_block) - put_u32(image, BLOCKS_LO, len(image) // block_size) - assert inode_info(image, nid)[4] == 0 - return new_raw_block, len(image) // block_size - - -def write_image(name, image): - update_superblock_checksum(image) - (artifact_dir / name).write_bytes(image) - - -evidence = [] - -fallback = bytearray((artifact_dir / "fallback-base.erofs").read_bytes()) -fallback_root = u16(fallback, ROOTNID_2B) -fallback_blocks = u32(fallback, BLOCKS_LO) -assert fallback_root != 0 and u64(fallback, ROOTNID_8B) == 0 -put_u32(fallback, FEATURE_INCOMPAT, - u32(fallback, FEATURE_INCOMPAT) | EROFS_FEATURE_INCOMPAT_48BIT) -put_u64(fallback, ROOTNID_8B, 0) -write_image("fallback-48bit-root2.erofs", fallback) -evidence.append( - f"fallback rootnid_2b={fallback_root} rootnid_8b=0 " - f"blocks_lo={fallback_blocks} union=0x{u16(fallback, ROOTNID_2B):04x}") - -oversize = bytearray((artifact_dir / "oversize-base.erofs").read_bytes()) -oversize_root = u16(oversize, ROOTNID_2B) -oversize_nid = child_nid(oversize, oversize_root, b"big.dat") -oversize_off, oversize_format, oversize_isize, _, _, _ = inode_info( - oversize, oversize_nid) -assert oversize_format & 1 and oversize_isize == 64 -put_u64(oversize, oversize_off + 8, 1 << 63) -write_image("extended-size-bit63.erofs", oversize) -evidence.append( - f"oversize nid={oversize_nid} inode_off={oversize_off} " - f"i_size=0x{u64(oversize, oversize_off + 8):016x}") - -extent = bytearray((artifact_dir / "extent-base.erofs").read_bytes()) -extent_root = u16(extent, ROOTNID_2B) -extent_nid = child_nid(extent, extent_root, b"hole.dat") -extent_off, extent_format, extent_isize, extent_xattr, extent_layout, _ = \ - inode_info(extent, extent_nid) -assert extent_format & 1 and extent_isize == 64 -assert extent_layout == EROFS_INODE_COMPRESSED_FULL -header = (extent_off + extent_isize + extent_xattr + 7) & ~7 -record = (header + 8 + 15) & ~15 -root_off = inode_offset(extent, extent_root) -assert not (header < root_off + 64 and root_off < record + 16) -put_u64(extent, extent_off + 8, HOLE_SIZE) -struct.pack_into(" 123`), zero -EROFS mounts, no md provider, and zero EROFS modules. Raw transcripts, build -objects, modules, and generated images remain outside the repository under -`/work/build` and guest `/tmp` only. diff --git a/tests/results/manual/2026-08-09T0710Z-g1/manual-test-report.md b/tests/results/manual/2026-08-09T0710Z-g1/manual-test-report.md deleted file mode 100644 index 412fe15..0000000 --- a/tests/results/manual/2026-08-09T0710Z-g1/manual-test-report.md +++ /dev/null @@ -1,159 +0,0 @@ -# repo22 G1 Full Manual Regression Report - -Execution window: 2026-08-09 06:30-07:10 UTC - -Code baseline: `cd0e985b5ac54a4b7acb7042422329ad1729fb3e` - -Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG, FreeBSD clang 19.1.7 - -## Scope and Method - -This run covers exactly these 32 test cases and no others: - -`TC001, TC007, TC009, TC011-TC014, TC019-TC040, TC147, TC150, TC151` - -Every test was executed directly from its Markdown procedure. No CI, runner, -or wrapper supplied a result, and no historical run was accepted as current -evidence. Each test used a fresh dynamically allocated md provider and was -cleaned before the next test. - -Set audit: requested 32, selected 32, unique 32, duplicate 0, missing 0, -extra 0. - -## Exact Build - -The source archive exported from the code baseline had SHA256 -`5b7fd2ac32ecf791f71c74849a6e8c1775e442d1be3c7852c654d074b2c8351b`. -The module was built natively in the guest with: - -```text -FREEBSD_SRC=/tmp/repo22-freebsd15-src WITH_ZSTDIO=0 ./build.sh -``` - -The build completed with kernel `-Werror`. The module had no unresolved -`bcmp` or `ZSTD_*` symbols. - -| Item | Value | -|---|---| -| Source commit | `cd0e985b5ac54a4b7acb7042422329ad1729fb3e` | -| `WITH_ZSTDIO` | `0` | -| KLD SHA256 | `023ddf0ea2205977f5921715d2a6206b607806840180ddf470103fd1d6deba84` | -| Loaded module | KLD ID 20, size `0xb8a8`, `erofs.ko` | -| Guest userland/kernel | `15.0-RELEASE-p8` / `15.0-RELEASE-p8` | - -The later documentation-only progress commit `573aaab7ee0b47cb6aed7f76c52c68dd4041326b` -did not alter `src/`; the KLD therefore remains an exact build of the stated -code baseline. - -## Fixture Qualification - -`tests/prepare_g1_fixtures.sh` generated deterministic sources and production -erofs-utils 1.8.6 images. `tests/g1_fixtures.py` applied structured, -field-checked transforms and recalculated checksums. Independent empty output -directories `repo22-g1-repro-c.6IK7SM` and `repo22-g1-repro-d.egVQgj` produced -byte-identical source manifests, metadata, image manifests, and structured -evidence. - -| Manifest | SHA256 | -|---|---| -| `SOURCE-SHA256SUMS` | `5ce53780f455208cb1efbf2738429a1f6f5c4eb3af1663779815c6afc0fea2bc` | -| `SOURCE-METADATA` | `bd27480c238ffff441d328ab71775989611d057ffaed691dae0238f738371e46` | -| `images/IMAGE-SHA256SUMS` | `54fb256b3847acc8b1e735eacaace56c1747f65237ee5730b2b5fa3e3ab2b0f3` | -| `images/fixture-evidence.txt` | `84e8e2e595a88e034ea0a05777207d4e3430e4e764a7fbc4d4a4770defcca3db` | - -| Image | SHA256 | -|---|---| -| `compact-dot-omitted.erofs` | `bbd1d3d9f619a71ed7a874feb8c437aa9bf3e86f671abfa243a85de57e809d54` | -| `compact-nlink1.erofs` | `c9ecfc29fbd45a2c37c6422d689ac340943dbc23ee4428ffd4698ef88e0b331d` | -| `compact.erofs` | `41004ed19d58e698f277cb268b570b87203c2ba08ea4a68a72f6e3b1c7aec47a` | -| `extended-large-hole.erofs` | `7a69d791742096ddfece20c7a793d828ec7e887352d0b62f22839f66ecb26a6d` | -| `extended-size-bit63.erofs` | `9413a4197f5b2c43876e257690a5f96e82f61b2b62084e2df688144070f0825b` | -| `extended.erofs` | `3d96fd292749f1ef7ae72f8e370fc8a94231ed575fb36a6d66784a59d8c4ffa7` | -| `fallback-48bit-root2.erofs` | `8217da2ae664358e12d357524ef0bb52a897f1ed206b2353c2a2033b76d4ae79` | -| `flat.erofs` | `3785ca07e7bd16f6c611191596ae0314253c0ae9b7217a4b22de25799b0f08ac` | -| `inline-cross-block.erofs` | `354b674fd144cab93403d47ad95135968c24d41b34a8c908aa6717882243e936` | -| `inline-zero.erofs` | `c894f249ca6d4a7a16c7eb728687c6aba1f948630c4f72e20680655c0fca70c0` | -| `inline.erofs` | `1474c310a80766666c1d578174ca03972fb9e03dff8c2357a37883151d4acc47` | -| `invalid-dirent-nid.erofs` | `309d6829ebc87c22ae6ba23f07ae0b00a62c7e657c3cc2ec46fe6a64d24eea69` | -| `root8-48bit.erofs` | `2f48797586395ff7d2204d43e67d6e2aaaa04c9bdfb162c5c5e2c24dc6e88893` | - -The corrected dot-omitted directory has size 35 and on-disk names -`..,child.txt`. Its encoding was independently checked with erofs-utils 1.9.3, -including a passing `fsck.erofs`; erofs-utils 1.8.6 does not recognize the -48-bit incompat feature and was not used to claim support for that image. - -## Per-Test Results - -| Test | Result | Actual command summary and observable evidence | Cleanup | -|---|---|---|---| -| TC001 | PASS | `mdconfig`, `mount -t erofs -o ro`, `cmp`, `sha256`, `statfs_probe`; root source/mount SHA `04690aad...3105`, blocks 17, files 15, readonly 1. | mount 0, md 0 | -| TC007 | PASS | Two fresh providers and concurrent mounts; both waits returned 0, and source/mount1/mount2 `testfile` SHA was `0e5303ad...e3b1`. | mounts 0, md units 0 | -| TC009 | PASS | `statfs_probe`, `df -kT`, `df -iT`; bsize 4096, blocks 17, free 0, files 15, ffree 0, 68 KiB used, readonly 1. | mount 0, md 0 | -| TC011 | PASS | `stat`, complete top-level name comparison, root payload `cmp`; root inode 36, directory mode 0755, nlink 4, root SHA `04690aad...3105`. | mount 0, md 0 | -| TC012 | PASS | `getfh`, `fhstat`, `fhopen`, repeated handle comparison; fsid `00000063:000000e0`, NID 71, generation 2262931519, data SHA `0e5303ad...e3b1`. | mount 0, md 0, outputs 0 | -| TC013 | PASS | Patched dirent NID 59 to 3200 with valid CRC; two `read_probe expect-error` calls returned errno 97 and truss showed `fstatat ERR#97`; dmesg `123->123`. | mount 0, md 0, outputs 0 | -| TC014 | PASS | Structured superblock inspect plus mount/statfs; magic `0xe0f5e1e2`, block bits 12, root NID 36, blocks 17, inodes 15, dmesg `123->123`. | mount 0, md 0 | -| TC019 | PASS | Mounted actual `feature=0x80 rootnid_8b=36 blocks_hi=0` image; root inode 36, blocks 17, root SHA `04690aad...3105`. | mount 0, md 0 | -| TC020 | PASS | `stat`, `cmp`, `sha256` on compact `small.txt`; NID 65, size 22, nlink 1, blocks 8, SHA `6e0d152a...`. | mount 0, md 0 | -| TC021 | PASS | Field evidence `i_format=0x14 i_nb=0x1234`; mounted NID 63 reported nlink 1 and source-exact SHA `ff151c...`. | mount 0, md 0 | -| TC022 | PASS | Structured raw rdev `0x543abc21`; native special-node probe reported char/block major 2748, minor 344865, while FIFO rdev was `NODEV`. | mount 0, md 0 | -| TC023 | PASS | Extended inode NID 45, 64-byte inode, size 2097883; `stat`, full `cmp`, SHA `eed000b0...` all matched source. | mount 0, md 0 | -| TC024 | PASS | Size 4294971393 sparse fixture; three 65536-byte reads at offsets 0, 2147483648, and 4294905857 matched deterministic zero source SHA `de2f2560...`. | mount 0, md 0, outputs 0 | -| TC025 | PASS | Baseline and patched mounts checked `single` NID 63/nlink 1, `hard-a` and `hard-b` NID 57/nlink 2, and `dotdir` NID 53/nlink 2; all source comparisons passed. | mounts 0, md units 0 | -| TC026 | PASS | Valid bit-4 fixture evidence: size 35, on-disk `..,child.txt`, root8 36; listing synthesized `.`, retained `..`, and child SHA matched `de256820...41f0`. | mount 0, md 0 | -| TC027 | PASS | Host `dump.erofs --ls --path=/dotdir` showed explicit `.,..,child.txt`; guest listing and child `cmp` matched SHA `de256820...41f0`. | mount 0, md 0 | -| TC028 | PASS | `flat.erofs` layout 0 small file; size 25, blocks 8, source/mount SHA `bb9c1e27...f486`. | mount 0, md 0 | -| TC029 | PASS | Full medium-file `cmp` SHA `471108a5...12e6`; `pread` offset 40960 length 20480 matched independent range SHA `72142972...5fb`. | mount 0, md 0, output 0 | -| TC030 | PASS | 10485791-byte full `dd`/`cmp` SHA `0b39b8a4...c56e`; 1 MiB read at 5 MiB matched SHA `c26bb16b...ba5c`. | mount 0, md 0, outputs 0 | -| TC031 | PASS | Inline `tiny.txt` evidence layout 2; size 12, blocks 8, source/mount SHA `863f7088...73f38`. | mount 0, md 0 | -| TC032 | PASS | Explicit layout-2 zero-length inode; size 0, blocks 0, EOF clean, both hashes `e3b0c442...b855`. | mount 0, md 0 | -| TC033 | PASS | 5000-byte tailpacked file full SHA `e4c7fbec...b2a6`; 64-byte range crossing offset 4096 matched SHA `6ff6b010...c0e`. | mount 0, md 0, outputs 0 | -| TC034 | PASS | Evidence layouts for 4095/4096/4097 were 0/0/2; full SHAs `5255d8c8...`, `677802c8...`, `eda39df0...`; nine-byte boundary range SHA `9e67b4e5...182c`. | mount 0, md 0, outputs 0 | -| TC035 | PASS | Sequential 4096-byte `dd` to exact EOF at size 262163; source/output SHA `056f8f75...3433`. | mount 0, md 0, output 0 | -| TC036 | PASS | Independent `pread` start/mid/end ranges returned 10240/10240/24576 bytes at exact offsets; SHAs `5b6a742e...d858`, `29fa7799...849`, `6477f5a2...163d`. | mount 0, md 0, outputs 0 | -| TC037 | PASS | 104857857-byte full `dd` took 61.44 s and matched SHA `1c8daa29...c661`; 5 MiB sample at 50 MiB matched SHA `f1b60e95...25c7`. | mount 0, md 0, outputs 0 | -| TC038 | PASS | `readlink` returned exact `target.txt`; followed target source/mount SHA `887f519c...8ea3`. | mount 0, md 0, outputs 0 | -| TC039 | PASS | Evidence size 73/layout 2; source and mounted long targets matched, output was 74 bytes including newline, followed file SHA `06539545...0f42`. | mount 0, md 0, outputs 0 | -| TC040 | PASS | Source/mounted target was `/nonexistent/repo22-g1-target`; two follow attempts returned exact errno 2 at open. | mount 0, md 0, outputs 0 | -| TC147 | PASS | Positive inline file SHA `0347f272...b01`; checksum-valid cross-block image returned errno 97 twice and truss `fstatat ERR#97`; dmesg `123->123`. | mounts 0, md units 0, outputs 0 | -| TC150 | PASS | Current-KLD rerun with `feature=0x80 rootnid_2b=36 rootnid_8b=0 blocks_lo=17`; root inode 36, root SHA `04690aad...3105`, statfs blocks 17. | mount 0, md 0 | -| TC151 | PASS | Current-KLD rerun: two direct opens returned errno 97, truss showed `fstatat ERR#97`, both mmap helpers exited 1 at open with integrity failure; dmesg `123->123`. | mount 0, md 0, outputs 0 | - -## Outcome - -| Result | Count | -|---|---:| -| PASS | 32 | -| KERNEL-FAIL | 0 | -| SHELVED/ISSUE | 0 | -| ENVIRONMENT-UNAVAILABLE | 0 | - -No G1 issue file was created. Existing issue files concern other groups or -previously documented limitations and were not changed by this run. - -## Discarded Attempts - -- TC013 initially asserted a nonportable `stat(1)` wrapper exit status. The - qualified run used two direct syscall probes and truss errno instead. -- TC026 first exposed an invalid transformer assumption: a NUL had been added - after non-trailing `..`. That image was discarded, the transformer was fixed, - independent 1.9.3 validation and two-build reproduction passed, and TC026 was - rerun with the corrected image. -- TC037's first long-running remote stream did not preserve its final output. - It was not counted; the complete qualified rerun retained elapsed time, both - hashes, and cleanup evidence. -- TC151 had two pre-mount/post-evidence command assertions with incorrect grep - keys. Neither was counted; the complete qualified rerun used syscall errno and - per-output integrity counts. - -## Final Cleanup - -Before final unload, `kldstat` showed ID 20 and the exact KLD SHA256 above. -After `kldunload erofs`, the guest reported: - -```text -erofs_mounts=0 md_units=0 kld_present=0 -``` - -Generated fixtures, build objects, and native probes remain only in `/work/build` -and guest `/tmp`; none is staged for the repository. diff --git a/tests/results/manual/2026-08-09T0710Z-g2/manual-test-report.md b/tests/results/manual/2026-08-09T0710Z-g2/manual-test-report.md deleted file mode 100644 index e5b6128..0000000 --- a/tests/results/manual/2026-08-09T0710Z-g2/manual-test-report.md +++ /dev/null @@ -1,269 +0,0 @@ -# repo22 G2 Full Manual Regression Report - -Started: 2026-08-09 06:07 UTC - -Completed: 2026-08-09 07:10 UTC - -Baseline: `cd0e985b5ac54a4b7acb7042422329ad1729fb3e` - -Branch: `manual-g2-20260809T060709Z` - -Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG, isolated port 9223 - -Overlay: `/work/build/repo22-manual-g2-20260809T060709Z-freebsd15-overlay.qcow2` - -## Scope and Result - -The exact assigned set was executed manually from the numbered Markdown -procedures. No CI, runner, result wrapper, or unassigned TC was used. - -| Test | Result | Primary evidence | -|---|---|---| -| TC002 | PASS | Real CRC32C ranges; valid mount; bad covered byte `ERR#97` | -| TC008 | PASS | Bad magic `ERR#22`; 1023-byte provider `ERR#6`; empty md `EINVAL` | -| TC010 | PASS | Qualified 16 TiB sparse provider; exact 64-bit `df` total | -| TC015 | PASS | Bad block size/root/feature returned 22/97/45 | -| TC016 | PASS | Checksum-field-only mutation returned `ERR#97` | -| TC017 | PASS | Raw union fields; short `ERR#6`; qualified sparse mount | -| TC018 | PASS | Exact read at physical offset 17592191561728 | -| TC112 | PASS | Bad magic field returned `ERR#22` before root load | -| TC113 | PASS | Resolved inode `openat` returned `ERR#45` | -| TC114 | PASS | OOB FLAT_PLAIN `read` returned `ERR#97` | -| TC115 | PASS | Future algorithm bit returned `ERR#45` | -| TC116 | PASS | FBT decompressor entry count 1; `read` returned `ERR#5` | -| TC119 | PASS | FLAT_PLAIN changed byte/hash returned without kernel error | - -Count: 13 assigned, 13 PASS, 0 KERNEL-FAIL, 0 SHELVED, 0 ENV, -0 duplicate, 0 omitted, 0 extra. - -## Build and Guest - -Both configurations were built natively in the isolated guest with kernel -`-Werror`, `FREEBSD_SRC=/tmp/repo22-freebsd15-src`, and FreeBSD source -`15.0-RELEASE-p9`. The running guest was `15.0-RELEASE-p8`. - -| Configuration | KLD SHA256 | Result | -|---|---|---| -| `WITH_ZSTDIO=0` | `482e9c072c949f0459c422aa1efd9b35a2d99cf90b25bb01e59e00b440b6d0fe` | PASS | -| `WITH_ZSTDIO=1` | `8349c97c7ced253fff32a9e706f29313f309cb63a270e4e39a4501426f2b95c6` | PASS | - -The ZSTDIO KLD was loaded as ID 5 and was the only repo22 test KLD. It had the -expected FreeBSD `ZSTD_*` references and no unresolved `bcmp`. The disabled KLD -had neither `ZSTD_*` nor `bcmp` unresolved symbols. - -## Fixture Qualification - -`tests/prepare_error_fixtures.sh` ran twice in independent empty host -directories with erofs-utils 1.8.6. Both `SHA256SUMS` files were identical and -self-verified. Every field mutation records its exact field/path and old/new -value. Same-size corruption variants preserve provider length; short and empty -providers record their intentionally different lengths. - -The helper independently proved the 4096-byte-block checksum forms: - -```text -canonical: field 1028 cleared, CRC32C [1024,4096), initial 0xffffffff -kernel: CRC32C [1032,4096), seed 0x5045b54a -valid control: stored=canonical=kernel=0xf7429681 -``` - -Raw transcripts, build logs, KLDs, guest logs, fixture trees, and the overlay -remain untracked under `/work/build/repo22-manual-g2-20260809T060709Z*`. - -## TC002 - -Result: **PASS**. - -Commands: helper `inspect`; `sha256`; dynamic `mdconfig`; valid `mount`/`cmp`; -direct negative `mount`; independent `truss`. - -Hashes: valid `eb97860671931c76a171d13caa70ddc2c9731c6cdef1e99b9a3deb580baf70ae`; -bad CRC `3f684e8cb03e3cb20a1920dccf23d9b7b19bc09d5745be04608e195c541ebf73`. -Both were 7622656 bytes. The corruption changed byte 1088 without recomputing -CRC; calculated checksum became `0x202160ab` while stored remained `0xf7429681`. -Valid data matched; bad direct mount rc was 1 and `nmount` returned errno 97. -Cleanup: mount absent and exact md detached. - -## TC008 - -Result: **PASS**. - -Commands: `wc -c`, `sha256`, dynamic `mdconfig`, direct `mount`, and `truss` -for each attachable provider. - -Hashes: bad magic `d1bc3aec03c58dae1b74b54d895c0a72e5b3088f05907ab2f6a0cb8dc38a615f`; -1023-byte provider `5724796860baa23469b3118eff2567c96a0b64b7dadaf99eafb7ba3c65b9aa56`; -empty provider `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`. -Bad magic returned errno 22. The 1023-byte md attached and mount returned errno -6. The zero-byte provider was rejected by `mdconfig` with `EINVAL`. Cleanup: -zero mounts and md units after all three subcases. - -## TC010 - -Result: **PASS**, not SHELVED. - -Commands: copy qualified prefix, `truncate -s 17592193667072`, `stat`, dynamic -`mdconfig`, `diskinfo`, `mount`, `df -kT`, `df -iT`, and `cmp`. - -Prefix hash: `d3ffadc6fc9e73f85a8a5973b4ac5ee2a2da57b4e8baeccd259fa4325a2146cf`. -Fields were `blocks_hi=1`, `blocks_lo=1861`, `rootnid_8b=36`, and -`total_blocks=4294969157`. The sparse file allocated 15232 UFS blocks while -GEOM reported all 17592193667072 bytes. `df -k` total and Used were exactly -17179876628; Avail was zero. Cleanup: unmounted, md detached, sparse provider -removed. The former environmental issue is now RESOLVED. - -## TC015 - -Result: **PASS**. - -Commands: evidence/hash/length checks; three dynamic md attachments; direct -mount status followed by independent `truss` errno capture. - -Hashes: bad block size `b5c52c44ace6f693e39161102acde157efec2fce34eb4fef198681dfc7942b58`; -bad root `9cdd61eb99b2e7f45d2505b3b95d6427f8f636e117b5ffabed064a3f83997d55`; -unknown feature `95d19a490c38657a8c579a10f1f12088ab43c1819aa9b37ae86b841bb26a592a`. -All were 7622656 bytes with recomputed valid CRC. Direct mount rc was 1 for -each; `nmount` returned 22, 97, and 45 respectively. Cleanup: zero mounts/md. - -## TC016 - -Result: **PASS**. - -Commands: field evidence, `sha256`, `wc -c`, direct mount, and `truss`. - -Hash: `04249f6b7d9b7322c171f1727408130d23cb0b83709aad7a4b77b39358a8dfe6`. -The 7622656-byte image changed only checksum `0xf7429681 -> 0xf7429680`. -Direct mount rc was 1 and `nmount` returned errno 97. Cleanup: no mount/md. - -## TC017 - -Result: **PASS** with both negative guard and positive dynamic coverage. - -Commands: raw `od` at offsets 1038/1060/1136; small-prefix mount; sparse -provider creation; `diskinfo`; positive mount; `cmp`; `df -kT`. - -Hash: `d3ffadc6fc9e73f85a8a5973b4ac5ee2a2da57b4e8baeccd259fa4325a2146cf`. -Raw values were high 1, low 1861, root 36. The short prefix returned errno 6. -The qualified provider mounted and reported exactly 17179876628 one-KiB -blocks. Cleanup: unmounted, detached, sparse provider removed. - -## TC018 - -Result: **PASS** with real I/O above 16 TiB. - -Commands: sparse provider creation; high-offset `dd`; two raw `read_probe` -calls; low-decoy and high-source `cmp`/hash; dynamic md mount and mounted hash. - -Prefix hash: `cfd86ed1e39528d958e32819282c7fd87191f80a25d9575078dad473f40563a8`. -The inode encoded `startblk_hi=1`, low 1347, combined block 4294968643, and -physical offset 17592191561728. The original low payload at 5517312 was zeroed: -its hash was `6a4875ddaceaa91fb3369f0f6d962f77442daf1b1d97733457d12bcabdf79441`. -Raw high and mounted hashes both matched source -`25eb31e024bc60745d68a5f7318753951804c7858b576cb698a4260af4a334a3`. -Cleanup: unmounted, detached, sparse provider and probe outputs removed. - -## TC112 - -Result: **PASS**. - -Commands: raw magic `od`, hash/length, direct mount, and `truss`. - -Hash: `d1bc3aec03c58dae1b74b54d895c0a72e5b3088f05907ab2f6a0cb8dc38a615f`. -Magic was `0x21444142`; provider length remained 7622656. The canonical CRC was -invalid as required after changing magic, while the unchanged production -suffix checksum still matched `0xf7429681`. Direct rc was 1 and `nmount` -returned errno 22. Cleanup: no mount/md. - -## TC113 - -Result: **PASS**. - -Commands: structured path inspection; mount/control `cmp`; direct and trussed -`read_probe expect-error ... 45`; second control `cmp`. - -Hash: `28d972c4065927326607fcd51dabff81cc88a28e2343f6056e55d04418172814`. -Resolved NID 52 at inode offset 1664 changed format `0x0001 -> 0x8001` with -valid CRC and equal provider length. `openat` returned errno 45; unaffected data -remained exact. Cleanup: clean unmount and md detach. - -## TC114 - -Result: **PASS**. - -Commands: path inspection; mount/control `cmp`; direct and trussed -`read_probe expect-error ... 97`; second control `cmp`. - -Hash: `2afcb9dde4c8eaf30bb2541d00be36f726f5cf151d887e0d999803b3993285ed`. -NID 54 field offset 1744 changed start block `1349 -> 1861`, equal to declared -blocks, with valid CRC and equal media size. Target `read` returned errno 97; -control remained exact. Cleanup: clean unmount and md detach. - -## TC115 - -Result: **PASS**. - -Commands: raw field `od`, hash/length, direct mount, and `truss`. - -Hash: `a8d7d9cf0abcf34a5cd2366059c0294e08fdd20d02ed3b5b21005ddf78c69029`. -`available_compr_algs` at byte 1106 changed `0x0000 -> 0x8000`; CRC was -recomputed and media remained 716800 bytes. Direct rc was 1 and `nmount` -returned errno 45. Cleanup: no mount/md. - -## TC116 - -Result: **PASS** with direct decompressor-path proof. - -Commands: parsed `dump.erofs -e` evidence; failed userspace extraction record; -mount/control `cmp`; FBT enumeration and count; direct/trussed `read_probe`. - -Hash: `f442785ea7cdbdc3f64b2ff89ea04942460cbdc5bea5615cbed237029edda8d8`. -The equal-length 716800-byte image changed 64 bytes at 4128, wholly inside the -parsed first physical extent `[4096,69632)`. Mount succeeded, excluding the -media-size precheck. `fbt:erofs-zstdio:z_erofs_decompress:entry` counted one -call for the target command, and `read` returned errno 5. Control remained -exact. Cleanup: unmounted/detached; `dtraceall` and dependencies unloaded. - -## TC119 - -Result: **PASS** with the revised real integrity semantics. - -Commands: path/field evidence; image/source hashes; mount; complete mounted -hash; two one-byte `read_probe` calls; byte compare; control `cmp`. - -Hash: `74b54f998481f9e55b30f2393ee780e3b46144157a54b4dc0fc0c044c160bb5a`. -The equal-length image flipped `/plain.bin` file offset 257 at provider byte -5525761 from `0x00` to `0x80`; the valid superblock CRC remained unchanged. -Source hash was `5647f05ec18958947d32874eeb788fa396a05d0bab7c1b71f112ceb7e9b31eee`; -mounted hash was `e98bb2acaecb24ab86112478bc8cc1e3668c127b449fe2e664dc4804f9477d81`. -Reads succeeded, the target byte differed, and control matched. Cleanup: clean -unmount and md detach. - -## Harness Notes - -Four incomplete attempts were discarded and are not PASS evidence: - -1. TC002 repeated host Python inspection in a guest without Python and exited - before md attachment. -2. TC002 initially trusted `truss` wrapper status; the qualified run separated - direct mount rc from syscall tracing because FreeBSD `truss -o` returned 0 - around a failing mount. -3. TC008 had a stray patch marker after its second subcase; trap cleanup ran and - the complete three-subcase command was repeated. -4. TC010 initially asserted the `df` Used column was free blocks; the qualified - run used the correct `f_bfree=0` interpretation and repeated all steps. - -## Final Cleanup - -Per-TC cleanup ended with zero EROFS mounts and zero md providers. Global dmesg -changed from 102 to 104 lines; both new lines were the expected -`md0: truncating fractional last sector by 511 bytes` from TC008. New panic, -fatal trap, page fault, general-protection fault, and double-fault count was -zero. The exact test KLD unloaded successfully, leaving zero EROFS modules. -The guest shut down normally and QEMU PID 285696 exited after 12 seconds. Port -9223 no longer has a VM process. - -## Issues - -No kernel issue was found and no TC remains SHELVED. The former TC010 provider -issue is retained as a resolved record in -`issues/TC010-48bit-statfs-large-provider.md`. diff --git a/tests/results/manual/2026-08-09T0839Z-g4/manual-test-report.md b/tests/results/manual/2026-08-09T0839Z-g4/manual-test-report.md deleted file mode 100644 index acc479a..0000000 --- a/tests/results/manual/2026-08-09T0839Z-g4/manual-test-report.md +++ /dev/null @@ -1,132 +0,0 @@ -# repo22 G4 full manual regression report - -## Scope and result - -- Exact scope: TC005, TC067-TC083, TC117, TC134-TC140, TC142. -- Count check: 27 requested, 27 executed, 0 duplicate, 0 omitted. -- Result: **PASS 27, FAIL 0, SHELVE 0, NOT RUN 0**. -- Kernel source baseline: `9ae22009f23a65320730072a780998e80aa9b728`. -- No repo22 kernel source was changed during this regression. -- No kernel failure was found, so no new `issues/` entry was required. - -## Isolated environment - -- Worktree: `/work/build/repo22-manual-g4-20260809T072432Z`. -- Branch: `manual-g4-20260809T072432Z`. -- Dedicated qcow2 overlay backed by the FreeBSD 15 development base. -- Dedicated SSH forward: `127.0.0.1:9224`; port 9222 was not used. -- Guest: FreeBSD `15.0-RELEASE-p8`, amd64, GENERIC, - `releng/15.0-n281036-53054229dcb3`. -- Final guest cleanup: zero matching mounts, zero md units, EROFS malloc - active count 0, KLD unloaded, guest responsive, QEMU stopped. - -## Exact KLD build - -The module used only `repo-community/repo22/src` from the baseline worktree. -It was built for `x86_64-unknown-freebsd15` with all 14 Makefile source files, -FreeBSD `15.0 RELEASE-p9` headers, clang/LLD 19.1.7, and `WITH_ZSTDIO=0`. -The guest is p8; both header and guest are the same FreeBSD 15 release KBI. - -```text -erofs.ko SHA256 = ee0b2e7c4ebf3602de8be8d47ddcfd021bc90a5265e7e9bf449bb9de2fbf4f04 -guest kldstat = erofs.ko, module erofs, id 507 -``` - -## Fixture provenance - -Installed `mkfs.erofs`, `dump.erofs`, and `fsck.erofs` reported erofs-utils -1.8.6. `tests/g4_fixtures.py` created both source trees without consuming old -images, ran four deterministic mkfs commands, transformed structured fields, -and reopened every result. A second fresh output directory produced identical -source and image checksum files. - -```text -source inventory SHA256 = c48f72777b0a04514fcfc7ea9c4ed77089ab0ed4537eac9cb7430e1acafe3d0a -metabox payload SHA256 = 50c5740b5fe5630ee919022d7ee1ce59d3e7d089c9d66e2baf82bf9de8233d40 -``` - -| Image | SHA256 | -|---|---| -| `basic.erofs` | `b375a3ca60de64b2c635b3ec45c9285c840e0a05bec105d5d8f68308300e7670` | -| `prefix-primary.erofs` | `49fc472a26412d154df729d8b0079801b35e97af2eb7fc3fa226dc65ee1963f9` | -| `metabox-plain.erofs` | `dd7b04097d1bfe283b65c95d759acd2176beb7cb0f1fe9d5ddbc0694b5acba9d` | -| `metabox-compressed.erofs` | `682b9dc1e0b492d935c03deba2ab644c1f0bcbf8e1ec623c15fb4990d25fe584` | -| `metabox-fragment.erofs` | `c93f4c281ed621519ced42a45cdb21b543047b7058dab31ad0def851cc19f4f1` | -| `bad-inline-entry.erofs` | `f18693c880bac0a273f2497cb0f81c0c52e19edc3ad45354eb38a39624adafd2` | -| `bad-shared-entry.erofs` | `875c27df56de2336fded3ef1db1c9fdc035db618096aa9269bfe70ea6f59c664` | -| `bad-shared-declared-bounds.erofs` | `19fc69cbe21051de2cf1cc19454cf64cc4a0ece444c6607956e066c2d4d332b3` | -| `bad-prefix-declared-bounds.erofs` | `d24b7a171aa28c3a46e3893749a4d08ab5d467c103f47aefcc0ca0f4e153d365` | -| `bad-metabox-truncated-extension.erofs` | `0bad23b1ad77eb8f2ffee83f89b7ef91b1c49ba2a20661adca21cd9efce0911e` | -| `bad-ishare-prefix-id.erofs` | `10357f28a11c2a8bbc329e01a414248415719ef088734a6ad75b934c0f65fc43` | -| `bad-fragment-self-loop.erofs` | `3b61228041591716c650a6613de30b2d9abe8b01148a9ae513b7de4d9b958d78` | -| `bad-fragment-range.erofs` | `432618f98a6e32351448120d8c694a4f31b45cb3078e08dad39b85fa01b47039` | -| `bad-metabox-recursive-nid.erofs` | `ab28a5478f569eb8a59e59e94448a85918885fcd328dadc2557ba6222dcd5aaf` | -| `bad-packed-recursive-nid.erofs` | `ad9a0ed1598c8a8e16911190c9042d58315fbcdc23af824ea106af55ffe6f2f4` | - -The fragment positive self-check found carrier NID 46, compressed-full layout, -size 32768, fragment header `0x8000000000000064` at image offset 9696, -fragment offset 100, packed NID 48, and packed size 32868. Thus -`100 + 32768 == 32868`. Shared entries were at metabox offsets 4096, 4128, -and 4156; prefix records were at 8192 and 8224 with base indexes 1 and 4. - -## FreeBSD namespace semantics - -Linux `user.*` was queried through FreeBSD namespace `user` with the leading -`user.` removed. Linux `trusted.*`, `security.*`, and ACL indexes were queried -through namespace `system`. Trusted/security list names retained their full -prefixes; ACL names were `posix_acl_access`/`posix_acl_default`. This is the -FreeBSD extattr ABI and intentionally does not copy Linux `getfattr` commands. - -`getextattr` does not reliably communicate every kernel failure through its -process status, so errno evidence came from guest `truss`: `ENOATTR=87`, -`EINTEGRITY=97`, and `EROFS=30`. Every corruption in this scope returned -`EINTEGRITY`; no case required normalization to `EIO=5`. - -## Per-TC results - -| TC | Guest evidence | Result | -|---|---|---| -| TC005 | inline user list; exact NUL-containing value; miss `ENOATTR 87` | PASS | -| TC067 | shared `shared_key` exact on two files; inline `local` exact | PASS | -| TC068 | missing name and wrong namespace both `ENOATTR 87` | PASS | -| TC069 | three shared names once; text, NUL, and binary values exact | PASS | -| TC070 | shared `trusted.config` exact; unprivileged system access denied | PASS | -| TC071 | shared `security.selinux` exact; miss `ENOATTR 87` | PASS | -| TC072 | system lists trusted, security, ACL names; user subset empty | PASS | -| TC073 | plain metabox mounted; bit-63 inodes, file hashes, xattrs exact | PASS | -| TC074 | three inline user values exact, including `00..1f` | PASS | -| TC075 | inline `trusted.admin` exact; user lookup `ENOATTR 87` | PASS | -| TC076 | capability and SELinux binary bytes exact | PASS | -| TC077 | packed user long-prefix names and values exact | PASS | -| TC078 | packed trusted long-prefix names and values exact | PASS | -| TC079 | packed NID 1201; two qualified records; both namespaces exact | PASS | -| TC080 | four repeated prefix lookups exact; miss `ENOATTR 87` | PASS | -| TC081 | metabox shared, inline, and long-prefix shared values exact | PASS | -| TC082 | raw ACL bytes and `getfacl -n` agree; user lookup `ENOATTR 87` | PASS | -| TC083 | four user attributes exact; system list empty | PASS | -| TC117 | malformed inline/shared gets both `EINTEGRITY 97`; mount stable | PASS | -| TC134 | nonzero xattr base shared value exact on two bit-63 inodes | PASS | -| TC135 | metabox, packed, and primary prefix backings all exact | PASS | -| TC136 | truncated extension and bad ishare ID mounts `EINTEGRITY 97` | PASS | -| TC137 | shared get and prefix mount bounded at declared image, errno 97 | PASS | -| TC138 | UID order 3002/2002 accepted; empty fallback; 3 negatives errno 97 | PASS | -| TC139 | FIFO stat/ACL/xattr reads; set/delete operations `EROFS 30` | PASS | -| TC140 | compressed and fragment metabox positive; 4 negatives errno 97 | PASS | -| TC142 | independent fragment rerun positive; 4 negatives errno 97 | PASS | - -## Stability and cleanup - -Positive values were compared as `getextattr -qq -x` bytes, not display text. -Negative mount and VOP calls were traced at the kernel syscall boundary. Each -case recorded zero matching mounts and zero matching md providers after its -cleanup. TC140 and TC142 additionally showed: - -```text -erofs active allocations = 0 -guest responsiveness = ok -panic/trap/hang = none -``` - -The final KLD unload succeeded. The dedicated guest powered off, QEMU exited, -and TCP port 9224 no longer had a listener. Generated images, KLDs, overlays, -and raw logs remained outside Git and were not included in the commit. diff --git a/tests/results/manual/2026-08-09T1059Z-g3/manual-test-report.md b/tests/results/manual/2026-08-09T1059Z-g3/manual-test-report.md deleted file mode 100644 index 34a48d6..0000000 --- a/tests/results/manual/2026-08-09T1059Z-g3/manual-test-report.md +++ /dev/null @@ -1,154 +0,0 @@ -# repo22 G3 Full Manual Regression Report - -Execution window: 2026-08-09 10:42-10:59 UTC - -Exact source baseline: 9ae22009f23a65320730072a780998e80aa9b728 - -Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG, port 9222 - -## Scope and Method - -This run covers exactly these 31 test cases and no others: - -TC041-TC066, TC141, TC148, TC149, TC152, TC153 - -Every test was executed directly from its numbered Markdown procedure with a -fresh dynamic md provider. No CI, runner, result wrapper, historical PASS, or -unassigned TC supplied a result. - -Set audit: requested 31, selected 31, unique 31, duplicate 0, missing 0, -extra 0. - -## Exact Build - -The source archive was exported with git archive from the exact baseline. Its -SHA256 was: - - c7f36610a523427dc406023e1c27fc87a03076bbde2e6b1406311fa5ddb06b0f - -The module was built natively in the guest with: - - FREEBSD_SRC=/tmp/repo22-freebsd15-src WITH_ZSTDIO=0 ./build.sh - -The build completed with kernel -Werror. The KLD loaded as ID 20, size 0xb8a8, -and remained the only loaded EROFS module for the test interval. - -| Item | SHA256 | -|---|---| -| erofs.ko | 19ad086bd2508cbb4c57b1a51f38c93057d438b84fbcfa2e637ff2519f5bc590 | -| readdir_probe | 2387964fd0c153f9cba0e41dd57fbdc1d48c2f2b779540f27387fb168cd35128 | -| g3_vfs_probe | 08fe3999600dd1826805c00a17af3d893d247a593c0812f767d85a2633302068 | -| stat_special | 1c9b4e8fea449d2fc7b985c468a0267448db5998dd413c6c4ed36a1ab0c78ea4 | -| nfs_fh_tool | db704f67d718d04b84ed32b5cdbecb93a99c5fe3b4ab238d603a10e005d790e3 | -| mmap_fault | ba32712f9a4a01f4d85d8f5f6fafe2741acbd6b185a85fa759da73dbc416233c | -| sparse_hole_probe | e5e1d8750fb671f0fccafdf51b61f4b0e73837718906dbd5fd5e8ab01cc790aa | - -The KLD unresolved-symbol list contained both -vnode_pager_local_getpages and vnode_pager_local_getpages_async; both resolved -at load. - -## Fixture Qualification - -tests/prepare_g3_fixtures.sh completed its structured assertions and all four -checksum-manifest checks. The relevant image hashes are: - -| Alias | Image | SHA256 | -|---|---|---| -| P | vfs-plain.erofs | 79f0b5f4aa8e7ea532b711ee8fb466f14f35d0952446d41ba4bbc4d6e008b8ff | -| Z | vfs-lz4.erofs | 07e2d1fbda0e6dc1233e6943a20f85e1f6652b580632c4c8264641a9a8805743 | -| N | namei-base.erofs | d1730ff23836797c6c09e1b39b1cf23efc16e27f85ab07fdcab577bb82871659 | -| Pad | namei-padding-nonzero.erofs | 9a94e9af2cab264b9c11975a20d78e615d6fe1e6f86267173cb5ac86aecb2b17 | -| Cshort | namei-corrupt-short.erofs | fb89f74795a5569ed3a85d63836dd75a06e1f17823d0508048c37da710ea6e75 | -| Coff | namei-corrupt-nameoff.erofs | b0b70ee615f163430f04edb91ab76c27ee8cc9a75b8ebb2008834f5b550e3933 | -| Cname | namei-corrupt-name.erofs | 6a980ad3e241603eda2ef71a470c82975291c614366a401e4e89c17c9adf9b91 | -| SC | special-compact.erofs | fd78256dd83d9d6d957e5f843c7a8e8a175a4b3243d528bebd299b0226853237 | -| SE | special-extended.erofs | e73e9b84d9ceb8c2b07e9c2732733b0fd607736c68c09522a2402fbeef6ba8d5 | -| H | extent-hole-5g.erofs | 50014a24493918247e36511ad34a2fe8ab47ae09ea46d7fd62a1bed445a6c65f | -| L | large-dir-intmax.erofs | 0f90d3d57adbbbd946e41b225c1f6c464915c6abb0b13478ec9b2a318def1f72 | -| LS | TC153 sparse prefix | f9337f83b1f568f6d904331a7749e6362a691055cd5af95b59bc89772f04e3b0 | - -The wide directory has 320 real files. The structured padding mutation records -wide NID 42, directory block 6, 80 dirents in the patched block, final-name -offset 4043, and the eight changed bytes at offsets 4084:4092. - -The 5 GiB fixture records size 5368713216 and one 16-byte extent with plen=0. -TC153 records extended FLAT_PLAIN/Layout 0, NID 40, start block 16, -2147483649 directory blocks, and final block index 2147483648. - -## Per-Test Results - -Every cleanup cell means the literal umount and mdconfig detach commands -returned zero; a following mount -t erofs and mdconfig -l produced no rows. - -| Test | Result | Actual command, errno/behavior, and hash evidence | Cleanup | -|---|---|---|---| -| TC041 | PASS | readdir_probe testdir 512; errno 0; 36 entries/restarts, types 8 dir/24 reg/4 link, FNV a3b969b5aa90cdbe, exact 34-name manifest and 255-byte name; image P. | umount 0; md 0 | -| TC042 | PASS | readdir_probe wide 128 plus three stat calls; errno 0; 322 entries/restarts, 320 files, FNV f3bfa2c15b231a59, sample NIDs 72/390/710; image N. | umount 0; md 0 | -| TC043 | PASS | four g3_vfs_probe stat calls, cmp, sha256; errno 0; regular/dir/link metadata stable and mounted file matched source; image P. | umount 0; md 0 | -| TC044 | PASS | direct stat probes; missing/missing-parent errno 2, positive errno 0; image P. | umount 0; md 0 | -| TC045 | PASS | stat four case variants plus direct miss; NIDs 101/88/86/87, unmatched spelling errno 2; image P. | umount 0; md 0 | -| TC046 | PASS | readdir_probe testdir 4096 and exact manifest cmp; errno 0; 36 entries/restarts, FNV a3b969b5aa90cdbe; image P. | umount 0; md 0 | -| TC047 | PASS | readdir_probe wide 128, sorted count/duplicate checks; errno 0; 322 entries/restarts and 320 unique files; image N. | umount 0; md 0 | -| TC048 | PASS | readdir_probe wide 128; every kernel d_off reopened/lseeked, every libc cookie seeked on its producing DIR stream; 322/322 restarts, FNV f3bfa2c15b231a59; image N. | umount 0; md 0 | -| TC049 | PASS | readdir_probe plus two dot stat calls and cmp; errno 0; dot and directory both NID 44, source-exact path; image P. | umount 0; md 0 | -| TC050 | PASS | readdir_probe child plus parent/dotdot stat and cmp; errno 0; parent NID stable and multiple dotdot path source-exact; image P. | umount 0; md 0 | -| TC051 | PASS | repeated stat and cmp; errno 0; byte-identical NID/mode/size/generation and data; behavior-level only; image P. | umount 0; md 0 | -| TC052 | PASS | repeated negative stat then positive stat; errno 2/2/0; no positive poisoning; behavior-level only; image P. | umount 0; md 0 | -| TC053 | PASS | repeated stat, getfh, compare, describe; errno 0; NID 101, gen 3444757833, identical handle SHA 5000984ddaeca26d90e8fd59568a55afcb074f51aaa00eed8d422d698a4aefc6; image P. | umount 0; md 0 | -| TC054 | PASS | parent and two dotdot stat calls; errno 0; identical NID/generation/type, no lock diagnostic; behavior-level only; image P. | umount 0; md 0 | -| TC055 | PASS | g3_vfs_probe stat, native stat, sha256 on plain/LZ4; errno 0; size 21211, blocks 48/8, data SHA d09ed1cee6520e36e54d5f2fd8c3bc74bd46cd8a58426744cc301b638c83e9d4; images P/Z. | two umount 0; md 0 | -| TC056 | PASS | stat_special char/block/fifo plus stat on compact/extended images; errno 0; rdev 2748:344865, FIFO NODEV, size/blocks 0; images SC/SE. | two umount 0; md 0 | -| TC057 | PASS | nobody direct access-read, access-exec directory/file, open-read; errno 0 for all; image P. | umount 0; md 0 | -| TC058 | PASS | nobody direct access probes; restricted read/read/exec errno 13, regular-file write access errno 30; image P. | umount 0; md 0 | -| TC059 | PASS | four readlink probes, cmp, broken follow; target lengths 8/16/14/119 and FNV hashes recorded, follow errno 0/2; image P. | umount 0; md 0 | -| TC060 | KERNEL-FAIL | g3_vfs_probe pathconf; values 255/1024/64/2147483647, but NO_TRUNC and CHOWN_RESTRICTED each errno 22; probe status 1; image P; issue TC060-pathconf-standard-values.md. | umount 0; md 0 | -| TC061 | PASS | direct chmod probe and before/after stat cmp; errno 30, metadata unchanged; image P. | umount 0; md 0 | -| TC062 | PASS | direct chown probe and before/after stat cmp; errno 30, metadata unchanged; image P. | umount 0; md 0 | -| TC063 | PASS | direct O_RDWR/truncate/create probes plus before/after sha256; errno 30/30/30, hash unchanged and no new entry; image P. | umount 0; md 0 | -| TC064 | PASS | nm -u plus mmap_fault on plain/LZ4; helper errno assertions passed; size 21211, six faults, FNV a1890a1c216724be, EOF/SIGBUS/COW PASS; images P/Z. | two umount 0; md 0 | -| TC065 | PASS | real mmap_fault on plain/LZ4; MAP_SHARED EACCES and O_RDWR EROFS asserted, same FNV a1890a1c216724be; images P/Z. | two umount 0; md 0 | -| TC066 | PASS | exact source erofs_bmap EOPNOTSUPP audit, KLD symbols, real mmap fallback on plain/LZ4; both FNV a1890a1c216724be, no strategy diagnostic; images P/Z. | two umount 0; md 0 | -| TC141 | PASS | fresh open-read/cmp, two ENOENT lookups, 322-cookie probe on N/Pad; two lookup and readdir attempts on Cshort/Coff/Cname all errno 97; all five hashes above. | five umount 0; md 0 | -| TC148 | PASS | actual Pad image cmp/count/readdir_probe; errno 0; 320 files, 322 restarts, FNV f3bfa2c15b231a59; hash Pad and patch offsets recorded. | umount 0; md 0 | -| TC149 | PASS | real mmap_fault on plain/LZ4; same size/fault/FNV/EOF/SIGBUS/COW evidence as TC064 with fresh mounts; images P/Z. | two umount 0; md 0 | -| TC152 | PASS | stat, vmstat -m, two timed sparse_hole_probe calls at 3221225472; errno 0, pread/mmap zero, 0.02 s each, RSS 2652/2656 KiB, erofs memory 768 bytes; image H. | umount 0; md 0 | -| TC153 | PASS | 8796093091840-byte sparse provider, diskinfo, stat, two cold and one post-readdir lookup; /huge size 8796093026304, all lookup errno 97, root 3-cookie FNV cd20cb6ae9fe01ba; prefix LS/KLD hashes. | umount 0; md 0; sparse file removed | - -## Outcome - -| Result | Count | -|---|---:| -| PASS | 30 | -| KERNEL-FAIL | 1 | -| SHELVED/ISSUE | 0 | -| ENVIRONMENT-UNAVAILABLE | 0 | - -The only new open issue is issues/TC060-pathconf-standard-values.md. The former -TC153 validation issue is resolved by the qualified sparse-provider run. - -## Discarded Attempts - -- The first TC041 helper run passed telldir cookies across a closed and newly - opened DIR stream. POSIX only guarantees a cookie on its producing stream. - The helper was corrected and rebuilt with -Werror; this run was not counted. -- TC041 was then attempted with a 128-byte getdirentries buffer, too small for - the fixture's 255-byte name record. The 512-byte qualified rerun passed. -- TC047 was first attempted with a 64-byte buffer, too small for its long - records. The 128-byte qualified rerun still forced repeated reads and passed. - -No discarded command was counted as a kernel result. - -## Dmesg and Final Cleanup - -The dmesg baseline had 123 lines. The only eight new lines were expected -mmap_fault child exits on SIGBUS: two images for each of TC064, TC065, TC066, -and TC149. There was no new EROFS warning, integrity diagnostic, assertion, -trap, panic, OOM, dirty writeback, or "No strategy for buffer" line. - -Before unload, the exact KLD hash was rechecked. KLD unload returned zero. -The guest then reported: - - erofs_mounts=0 md_units=0 kld_present=0 guest_artifacts=0 - -The TC153 sparse provider and all transferred/generated guest files were -removed. diff --git a/tests/results/manual/2026-08-09T1120Z-tc060-fix/manual-test-report.md b/tests/results/manual/2026-08-09T1120Z-tc060-fix/manual-test-report.md deleted file mode 100644 index 40455c3..0000000 --- a/tests/results/manual/2026-08-09T1120Z-tc060-fix/manual-test-report.md +++ /dev/null @@ -1,157 +0,0 @@ -# repo22 TC060 pathconf fix manual test report - -Execution window: 2026-08-09 11:20-11:35 UTC - -Exact source commit: `cdba7e54fb9e9d82980a06f178e68d0bbc1663ac` - -Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG, port 9222 - -## Scope and Result - -TC060 is **PASS**. The original six-key helper returned the required values -with errno 0, compatibility queries retained their previous values, an unknown -name returned EINVAL, mount/read/read-only smoke passed, dmesg did not change, -and final mount/md/KLD/artifact counts were zero. - -This was a direct manual run. No CI, result wrapper, or historical PASS supplied -the result. `WITH_ZSTDIO=1` was build-verified; runtime TC060 used -`WITH_ZSTDIO=0` because pathconf is compression-independent and this avoids an -unnecessary optional runtime dependency. - -## Implementation Basis - -FreeBSD 15 `sys/kern/vfs_default.c` shows that `vop_stdpathconf()` handles -generic `_PC_ASYNC_IO`, `_PC_PATH_MAX`, and several zero-valued optional -features, then returns EINVAL for other names. The same source tree's UFS, -tmpfs, and ext2fs vnode operations explicitly return 1 for -`_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` before delegating the remaining names -to `vop_stdpathconf()`. - -The EROFS fix adds only those two switch labels and retains the existing -default delegation. `src/namei.c` rejects components longer than -`EROFS_NAME_LEN` with ENAMETOOLONG, while `src/erofs_vnops.c` rejects uid/gid -mutation with EROFS. Linux EROFS's 255-byte name limit and ENAMETOOLONG lookup -check were reviewed only for maintenance similarity; FreeBSD VOP behavior was -authoritative. - -## Exact Build - -The repo22 subtree was exported directly from the exact source commit. Later -`current/`-only reporting changes did not alter `src/`. - -| Item | Value | -|---|---| -| Source archive SHA256 | `a4c871ca7cefc49470ef0003f876792380a8d8db6cbeaefb96456d06ad946d53` | -| FreeBSD source path | `/tmp/repo22-freebsd15-src` | -| Compiler | FreeBSD clang 19.1.7 | -| Build command | `FREEBSD_SRC=/tmp/repo22-freebsd15-src WITH_ZSTDIO=N ./build.sh` | - -Both clean-object builds completed with kernel `-Werror`: - -| Configuration | Module SHA256 | Size | Undefined-symbol audit | Result | -|---|---|---:|---|---| -| `WITH_ZSTDIO=0` | `68536e03ce93c6c04aab9cf29dab81ee5802d4b94401bd1a4bd752de40c0e504` | 73896 | no `bcmp`, no `ZSTD_*` | PASS | -| `WITH_ZSTDIO=1` | `598f171d355af78c64407a6d513d20f053530620da92df7f8ccfdf9a804e463d` | 78728 | no `bcmp`; only five expected FreeBSD `ZSTD_*` APIs | PASS | - -The enabled module referenced `ZSTD_DCtx_setParameter`, -`ZSTD_createDCtx_advanced`, `ZSTD_decompressStream`, `ZSTD_freeDCtx`, and -`ZSTD_isError`. - -## Fixture and Probes - -| Artifact | SHA256 | -|---|---| -| `vfs-plain.erofs` | `79f0b5f4aa8e7ea532b711ee8fb466f14f35d0952446d41ba4bbc4d6e008b8ff` | -| tracked `g3_vfs_probe` binary | `08fe3999600dd1826805c00a17af3d893d247a593c0812f767d85a2633302068` | -| one-run `pathconf_audit` source | `c2cc69297848cad8c806d47585ef670ae5b378827450110e2cf47cf82ed527b3` | -| one-run `pathconf_audit` binary | `17aa9d738871ac424900ed7a928cb72cc2e5a288f2c2f9744906fa139a78835b` | - -Both probes compiled natively with `-O2 -Wall -Wextra -Werror -std=c17`. -The one-run helper was kept outside the repository and only printed each -value/errno plus assertions for unknown-name and overlong-component behavior. - -## TC060 Values - -The exact original command was: - -```text -./g3_vfs_probe pathconf /tmp/repo22-tc060-fix/mnt/testdir -``` - -It exited 0 and printed: - -```text -name_max=255 path_max=1024 filesizebits=64 link_max=2147483647 -no_trunc=1 chown_restricted=1 -``` - -The expanded value/errno capture was: - -| Query | Actual value | errno | -|---|---:|---:| -| `_PC_NAME_MAX` | 255 | 0 | -| `_PC_PATH_MAX` | 1024 | 0 | -| `_PC_FILESIZEBITS` | 64 | 0 | -| `_PC_LINK_MAX` | 2147483647 | 0 | -| `_PC_NO_TRUNC` | 1 | 0 | -| `_PC_CHOWN_RESTRICTED` | 1 | 0 | -| `_PC_ASYNC_IO` | 200112 | 0 | -| `_PC_ACL_EXTENDED` | 1 | 0 | -| `_PC_ACL_PATH_MAX` | 254 | 0 | -| `_PC_ACL_NFS4` | 0 | 0 | -| unknown name `INT_MAX` | -1 | 22 (`EINVAL`) | - -A lookup using a 256-byte path component returned -1 with errno 63 -(`ENAMETOOLONG`), confirming the reported no-truncation behavior. - -## Mount and Read Smoke - -The module loaded as KLD file ID 20, module `erofs`, and the fixture attached -as dynamic `md0`. The mount line was: - -```text -/dev/md0 on /tmp/repo22-tc060-fix/mnt (erofs, local, read-only, acls) -``` - -`testdir` reported inode 44, mode `drwxr-xr-x`, and size 1039. Reading -`testdir/file.txt` produced `repo22 G3 file payload` and SHA256 -`523af4c899ba3b8f4fa4d854fe609590be271905b63932cf067ca9630e612687`. -Opening the same file with `O_RDWR` returned errno 30 (`EROFS`) as expected. - -## Dmesg and Cleanup - -The qualified run's dmesg had 131 lines before and after, with identical -SHA256 `91bf74a3607f432b4802f143bcf9d050c2f9e741da0a54f3e6a1b47bcc996a06`. -There was no new EROFS diagnostic, assertion, trap, panic, or resource warning. - -Qualified cleanup returned zero for umount, md detach, and KLD unload. After -removing the transferred archive and test directory, the final guest audit was: - -```text -erofs_mounts=0 -md_units= -erofs_klds=0 -guest_artifacts=0 -``` - -## Discarded Attempt - -The first otherwise successful runtime pass loaded the KLD from the -non-canonical filename `erofs-zstdio0.ko`. Its explicit `kldunload erofs` -command could not resolve that filename, so the run stopped before final dmesg -and cleanup qualification. Its mount and md were already clean; KLD file ID 20 -was then unloaded directly and the guest returned to zero resources. The full -test was repeated with canonical `erofs.ko`, and only that rerun is counted. - -## Independent Review - -The post-test review found no lock, resource, or error-path issue: - -- `VOP_PATHCONF` is called with the vnode shared-locked; the function does not - change lock state. -- The new constant-return branches allocate nothing and acquire no references. -- Both normal and FIFO vnode vectors use the same filesystem-wide result. -- Existing value branches are unchanged, and unsupported names still reach - `vop_stdpathconf()` and return EINVAL. -- The syscall copies `retval` only on error 0, so the unknown-name error path - cannot expose a stale value. diff --git a/tests/results/manual/2026-08-09T1236Z-g5/manual-test-report.md b/tests/results/manual/2026-08-09T1236Z-g5/manual-test-report.md deleted file mode 100644 index 4963ae6..0000000 --- a/tests/results/manual/2026-08-09T1236Z-g5/manual-test-report.md +++ /dev/null @@ -1,212 +0,0 @@ -# repo22 G5 compression full manual regression report - -## Scope and result - -- Exact scope: TC003, TC004, TC084-TC092, TC102-TC110, TC143-TC146. -- Count check: 24 requested, 24 executed, 0 duplicate, 0 omitted. -- Result: **PASS 23, PARTIAL 1, FAIL 0, NOT RUN 0**. -- SHELVED subscenario: TC146 explicit mapped extent only. -- TC146 overall: **PARTIAL**; HEAD2 PASS, interlaced PASS, explicit extent - SHELVED. -- Kernel baseline: `f383bbbbff301a6bde18894f03ab88a8c0cc885a`. -- Initial and pre-push `xdm/main` baseline: the same commit. -- No repo22 kernel source was changed. No kernel failure issue was opened. - -## Isolated environment - -- Worktree: `/work/build/repo22-manual-g5-20260809T085450Z`. -- Branch: `manual-g5-20260809T085450Z`. -- Artifact root: `/work/build/repo22-g5-20260809T085450Z`. -- Dedicated qcow2 overlay backed by the FreeBSD development base. -- Dedicated SSH forward: `127.0.0.1:9225`. -- Guest: FreeBSD `15.0-RELEASE-p8`, amd64, - `releng/15.0-n281036-53054229dcb3`. -- Guest toolchain: FreeBSD clang/LLD 19.1.7. -- Host tools: mkfs/dump/fsck erofs-utils 1.8.6. -- No CI, runner, or wrapper was used. Each Markdown test was issued manually. - -## Reproducible fixture - -`tests/g5_fixtures.py` generated source content, invoked mkfs with fixed UUID, -timestamp, owner, sort order, and one worker, transformed structured records, -reopened every image, and wrote a field manifest. Two final fresh output -directories produced byte-identical source and image checksum inventories. - -```text -SHA256SUMS SHA256 = 8535696f187f818df8b3b30ee35cd8837aceadef9de7a85be1475732dcdf0596 -SOURCE-SHA256SUMS SHA256 = 145bd663d7552025b486ec15c0b4c1c0ce99affa7728dc4d762134bdc92420c3 -fixture manifest SHA256 = 63ecdb46f8cc3c04e348e24e0f1306ad5e4606bf1fa108919f946515d7b41d63 -image count = 25 -source count = 19 -guest hash verification = 25 images, 19 sources -``` - -### Image hashes - -| Image | SHA256 | -|---|---| -| `deflate-level1.erofs` | `58dc78da2ec5a751688f53c4eee34f94b7f828d68b7225aea006d08374187bfe` | -| `deflate-level6.erofs` | `4a3b5d2ce6b8d1bad23aa2c4d4ce38f83bc0e6642523e9165e58afe03e030c28` | -| `deflate-level9.erofs` | `c4bf219a5130d475c26452a1a5346c63ed1f434afd4315972ee30ebc66f9c751` | -| `deflate-partial-ref.erofs` | `fccb2f4038dca8b7277984e255a304bb6eee5de51fedd7b7261ef4f879362c81` | -| `deflate-partial-ref-corrupt.erofs` | `063e1bc07d2c3a3bf3b17c0cce0237eca7000eb16abdf8a1b4428d8cc7880811` | -| `extent-attempt.erofs` | `ee7472c23ccffd5eef6f4a3171ea53ae2e840f8a1ea417b2630065175ecf3225` | -| `head2.erofs` | `4948335059e605ff70da4f6e516c4ba24d58313d10e836cc6fe9c9c468775a23` | -| `head2-corrupt.erofs` | `7c25e9c8c767d2717c06b1efc854f0bb3b1f370cf3e2a8e2d53e767918bfe50f` | -| `interlaced.erofs` | `7b77055dfae301a0acc3202cf0d7cf7062eb82c6101d16b68c91f70615e1638b` | -| `lz4-compact-4k.erofs` | `0701a37430372f1240ad6485c83f0048d97bb49ae1ca85e1423dd05101bf5578` | -| `lz4-compact-64k.erofs` | `967cc1b625546f9f9f881472e71cc3d849c65feb4e98e67fbfdc9b90a4be6dda` | -| `lz4-compact-256k.erofs` | `6fbded24756b557c7ce19a5936dcde01fb75d4b1c91b0236fca46ee0c6331db0` | -| `lz4-full-4k.erofs` | `3bc6eb538752673e2b9f905a0382887bccf417574a0d04901932cfeaac258bc7` | -| `lz4-large.erofs` | `0340026fe5fea5fd7c6c4c03f288815bc86c7517c2c2a9a2ca21c61dfb96a973` | -| `lz4-ztail.erofs` | `18cd045d7f2b8e542a0dda6fb0063c266919928e6e485f498ed93c8992d9ae3a` | -| `lzma-level6.erofs` | `32107a084b27362a093768b88746c37c2e99a74b9f1301a9d4046988479defe9` | -| `lzma-large.erofs` | `6534870a686c3c5ba58fc665ea4995c07427a15c9d5ac7747690114138adf47d` | -| `lzma-partial-ref.erofs` | `fbbcbcb8a178370a8f7665c3e0cff8df5bb6f81c3ba38a0e44c137e8ed165402` | -| `lzma-partial-ref-corrupt.erofs` | `23981af2f4e36f88e382fff975f3c4fb00cb4b02c0f40f10948602ab4ce39f23` | -| `microlzma-edge.erofs` | `27a19758141d64430d7536774fadf34ba7f8befa0bc36442b789e494b4b37d4c` | -| `zstd-level1.erofs` | `34f51d5b1273cc3fc9efa458c200cf134dc7dca2c583e577a8d06ad563cfbff6` | -| `zstd-level15.erofs` | `8ca4ba52561cc8903f85048edfd958ef692583479748dcb8aa57e2c7aade1034` | -| `zstd-level22.erofs` | `99f79807ffd9f1216dbd624a2d8ecfcf17c9b42689fef5b5a69d0250ed84e5` | -| `zstd-partial-ref.erofs` | `cdef7c2a397722723dcc762044612a179358f9dbe4dcd879ac08e462cf9628d1` | -| `zstd-partial-ref-corrupt.erofs` | `ed6f4b23f6b6426e6af9180e892f03097d16a6b969edd68ad42d9a1ff8d2325e` | - -### Source hashes - -| Source | Size | SHA256 | -|---|---:|---| -| `large/large.bin` | 268435456 | `78f61f8eb37b5aeee026b579d244d766935d5655c0ac5383a96f631048611cfd` | -| `levels/level.dat` | 8388608 | `ddda39737f0f6093e828a032ec161511fefbb1fa361bc6cbffdbc91e48e4c461` | -| `lz4/compressed.bin` | 8388608 | `3ff012b76087c4da65ce0b69813a76f47ea367e95782edf8b8cd6cd3ec4d1880` | -| `lzma-large/large.bin` | 104857601 | `de846af6c7e47fac72c0576c449fd0fc1fe9587286c9158ae08c81804c77f66b` | -| `microlzma/one-byte.bin` | 1 | `333e0a1e27815d0ceee55c473fe3dc93d56c63e3bee2b3b4aee8eed6d70191a3` | -| `microlzma/block-4k.bin` | 4096 | `42b2e4ac3afeb366a6407573d5a91a961775e0fe374b0941f19681c4e3f9ea96` | -| `microlzma/boundary-16k.bin` | 16384 | `1b5a7306ca67b75c18228d08f281eb7474b31a1a1ccbe4d680b7942fc81265b9` | -| `partial/a.dat` | 1048576 | `c7ac0fce9c56d732e4b8328c1ca48ae33ee4d449166bd25d2c88322c1a852d9f` | -| `partial/b.dat` | 700000 | `926a9bb05b20cb3da745eaff6e7fec38fb43d7fa155276474367550b67b97589` | -| `partial/control.bin` | 32768 | `7544a26039c5257ee07c1280468edb1f61f0b0edaa25273b15e0252a9cc5c90a` | -| `partial-deflate/a.dat` | 1048576 | `c7ac0fce9c56d732e4b8328c1ca48ae33ee4d449166bd25d2c88322c1a852d9f` | -| `partial-deflate/b.dat` | 100000 | `1fbe057da0cb994652ef664a4aa00628e948578055665160fc63e7079469da21` | -| `partial-deflate/control.bin` | 32768 | `7544a26039c5257ee07c1280468edb1f61f0b0edaa25273b15e0252a9cc5c90a` | -| `shape/shape.dat` | 1048576 | `5be510e6b43f1ed0cda4261288ea70df11607b6ced29bc90d32ed2849ecc5e35` | -| `ztail/inline.dat` | 131071 | `e54a4bac3b6b1c01ee846dbccdf234ddb30ed06dc13f1af1df0fbbc397431ce5` | -| `ztail/exact-pcluster.dat` | 4096 | `14587a494e72ecdc7d26b4c0b947d91d44eba4365b252d1a4ec10ba979978c52` | -| `ztail/one-byte-tail.dat` | 4097 | `718f4c27896edf8925300c50572df92b9127e56b2b84a3a86ced4d67c3be9596` | -| `ztail/max-tail.dat` | 8191 | `5e1e898dba6a6bbcc8beb0b0b097bc04ecaaf4ad505b77bfb8646a3011628df6` | -| `ztail/zero-tail.dat` | 0 | `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` | - -## Key layout fields - -| Fixture | Structured proof | -|---|---| -| LZ4 compact 4K | layout 3, map offset 2183200, `h_advise=1` | -| LZ4 full 4K | layout 1, map offset 2183200, `h_advise=0` | -| LZ4 compact 64K | layout 3, `h_advise=7`, max physical pcluster 65536 | -| LZ4 compact 256K | layout 3, `h_advise=7`, max physical pcluster 262144 | -| ztailpacking | layout 3, `h_advise=9`, `h_idata_size=536`, physical bytes 1512-2048 | -| HEAD2 | incompat `2 -> 10`, map advise `2 -> 6`, first record `1 -> 3`, pblk 1 | -| interlaced | layout 3, `h_advise=49`, 52 compressed, 1 plain, first transition 999593 | -| explicit attempt | layout 1, map offset 1312, `h_advise=2`, explicit bit absent | - -The first HEAD2 logical pcluster ends at 326114; the boundary read started at -326000. The interlaced boundary read started at 999000 and crossed the proven -transition at 999593. - -Partial-reference fields after reopen: - -| Algorithm | a/b size | a/b pblk | b partial advise | b compressed blocks | -|---|---|---|---:|---| -| DEFLATE | 1048576 / 100000 | 1 / 1 | 32769 | 2 -> 17 | -| ZSTD | 1048576 / 700000 | 1 / 1 | 32769 | 1 -> 1 | -| MicroLZMA | 1048576 / 700000 | 1 / 1 | 32769 | 1 -> 1 | - -Targeted corruption fields: - -| Target | Algorithm | Pcluster | Patch | -|---|---:|---|---| -| DEFLATE `/a.dat` | 2 | 4096 + 69632 | offset 8055, length 64 | -| ZSTD `/a.dat` | 3 | 4096 + 4096 | offset 8073, length 64 | -| MicroLZMA `/a.dat` | 1 | 4096 + 4096 | offset 7957, length 64 | -| HEAD2 `/shape.dat` | 0 | 4096 + 65536 | offset 4096, length 64 | - -Every corrupted image retained a valid superblock checksum. Each read was -bounded by `timeout 20`, returned errno 5, and left `control.bin` readable -where the fixture included one. - -## Build and TC145 - -The invalid build value failed with rc 1 and the exact message -`WITH_ZSTDIO must be 0 or 1`. - -| Build | Size | SHA256 | Undefined-symbol gate | Guest | -|---|---:|---|---|---| -| `WITH_ZSTDIO=0` | 73896 | `e103ebbaf7faf8041448d93022be44e7c93822a45a36a69bca2b7a2107461d09` | no `ZSTD_*`, no `bcmp` | file ID 5 load/unload PASS | -| `WITH_ZSTDIO=1` | 78728 | `56ffcce006eed1e555121d9b8f523578272ac3d82c27a19b635d0a0a664c8370` | five formal API names, no `bcmp` | file ID 5 load/unload PASS | - -The enabled undefined ZSTD set was exactly -`ZSTD_DCtx_setParameter`, `ZSTD_createDCtx_advanced`, -`ZSTD_decompressStream`, `ZSTD_freeDCtx`, and `ZSTD_isError`. - -The disabled module read the LZ4 control hash -`3ff012b76087c4da65ce0b69813a76f47ea367e95782edf8b8cd6cd3ec4d1880`. -A direct ZSTD mount returned rc 1 with the required message; `truss` proved -`nmount` returned `ERR#45 EOPNOTSUPP`. The enabled module read ZSTD hash -`ddda39737f0f6093e828a032ec161511fefbb1fa361bc6cbffdbc91e48e4c461`. - -## Per-TC results - -| TC | Manual evidence | Result | -|---|---|---| -| TC003 | compact LZ4 full hash/cmp; first 1024 bytes cmp | PASS | -| TC004 | LZMA level 6 full hash/cmp; 102400/10240 and EOF 10000 cmp | PASS | -| TC084 | full-index LZ4 full hash/cmp; 4000/8192 cross-extent cmp | PASS | -| TC085 | 256 MiB full hash/cmp; 50/200 MiB ranges and two 4K offsets cmp | PASS | -| TC086 | deterministic sequential 4K and 1 MiB block reads both full cmp | PASS | -| TC087 | offsets 0, 4096, 65536, 1048576, and 8384512 source cmp | PASS | -| TC088 | 4K/64K/256K images full hash and 409600/40960 source cmp | PASS | -| TC089 | 0/4096, 4096/4096, 2048/4096, and 0/16384 source cmp | PASS | -| TC090 | 0/65536, 4096/4096, 65504/64, and 0/262144 source cmp | PASS | -| TC091 | ztail full hash/cmp and final 4096 bytes cmp | PASS | -| TC092 | actual 4096, 4097, 8191, and 0-byte files full hash/cmp | PASS | -| TC102 | DEFLATE level 1 full hash/cmp and two fixed ranges | PASS | -| TC103 | DEFLATE level 6 full hash/cmp and two fixed ranges | PASS | -| TC104 | DEFLATE level 9 full hash/cmp and two fixed ranges | PASS | -| TC105 | ZSTD level 1 full hash/cmp and two fixed ranges | PASS | -| TC106 | ZSTD level 15 full hash/cmp and two fixed ranges | PASS | -| TC107 | ZSTD level 22 full hash/cmp and two fixed ranges | PASS | -| TC108 | 104857601-byte LZMA level 6 full hash/cmp; 0/50/99 MiB ranges; active 0 | PASS | -| TC109 | actual 1B/4K/16K files match; 16K inode proven compressed LZMA | PASS | -| TC110 | valid LZMA control; targeted read EIO; same-image control; active 0 | PASS | -| TC143 | DEFLATE/ZSTD a+b full, partial ranges, targeted EIO, controls, active 0 | PASS | -| TC144 | MicroLZMA a+b full, partial ranges, targeted EIO, control, active 0 | PASS | -| TC145 | invalid gate, both builds/symbol sets/KLDs, disabled rejection, enabled read | PASS | -| TC146 | HEAD2 PASS; interlaced PASS; explicit mapped payload SHELVED | PARTIAL | - -TC108 used a 1800-second hard timeout for each complete read under QEMU TCG. -It completed without timeout; the long duration was diagnostic only. - -## Explicit extent disposition - -erofs-utils 1.8.6 source had zero matches for -`Z_EROFS_ADVISE_EXTENTS`, `z_erofs_extent_recsize`, and the on-disk -`struct z_erofs_extent`. The fresh `--max-extent-bytes=65536` attempt remained -ordinary full-index metadata. The structured transformer refused an -unverifiable metadata/payload relocation. Existing `review_fixtures.py` only -builds a negative `pa + plen` overflow extent table, not a valid mapped -payload. - -FreeBSD and Linux ABI/control flow for 4/8/16/32-byte records was reviewed. -That is static support, not dynamic positive coverage. Full details and -acceptance criteria are in `issues/extent-metadata-fixture-unavailable.md`. - -## Stability and cleanup - -- Every mount was read-only and every md provider was detached. -- All corruption probes returned errno 5 before their 20-second timeout. -- No dmesg delta, panic, trap, or OOM was observed. -- EROFS active allocations returned to zero after each corruption/large test. -- Final guest verification: 25 image hashes and 19 source hashes matched. -- Final guest: zero mounts, zero md providers, no EROFS allocator row, no EROFS - KLD, and responsive FreeBSD 15.0-RELEASE-p8. -- The dedicated VM was powered off and port 9225 was released after evidence - collection. diff --git a/tests/results/manual/2026-08-09T1244Z-g6/manual-test-report.md b/tests/results/manual/2026-08-09T1244Z-g6/manual-test-report.md deleted file mode 100644 index 4b2f06e..0000000 --- a/tests/results/manual/2026-08-09T1244Z-g6/manual-test-report.md +++ /dev/null @@ -1,139 +0,0 @@ -# repo22 G6 Chunk and Multi-Device Manual Regression - -Date: 2026-08-09 -Executor: G6 manual agent -Source baseline: `6b33b4afb490be7d6fec70e499469c306a58435d` -Scope: `TC006`, `TC093`-`TC101`, `TC118` (11 exact IDs) - -## Result - -PASS. All 11 requested test cases passed. There are no KFAIL, SHELVED, or -ENV results, no duplicate IDs, and no omitted IDs. - -| Status | Count | -| --- | ---: | -| PASS | 11 | -| KFAIL | 0 | -| SHELVED | 0 | -| ENV | 0 | -| Duplicate | 0 | -| Omitted | 0 | - -No kernel source was changed. `git diff -- src` was empty after testing. - -## Environment and build identity - -- Worktree: dedicated sparse worktree and branch - `manual-g6-20260809T114414Z`, based directly on `xdm/main` at the source - commit above. -- VM: dedicated qcow2 overlay, QEMU TCG, 6144 MB, 4 vCPUs, SSH forward - `127.0.0.1:9226` only. -- Guest: FreeBSD `15.0-RELEASE-p8` amd64, - `releng/15.0-n281036-53054229dcb3`, OSREL `1500068`. -- Guest kernel SHA256: - `b9abf7b58f9dd4d87f14d2fbc306cf255e6eadc47b8ae885a230f65e28be4562`. -- Tracked build sys source: `REVISION=15.0`, `BRANCH=RELEASE-p9`, archived - from the same repository commit; archive SHA256 - `66c457159b758ab1a5d298292b81bd29ab439ca4ab396091602e030da4721cb9`. -- Build configuration: native guest build, - `WITH_ZSTDIO=1 FREEBSD_SRC=/root/freebsd-src`. -- KLD SHA256: - `d5ff5ceef1ad76eb5b608d362b04f3bc40379e58a73d91ed3ae747c035d7b8af`. -- repo22 source archive SHA256: - `7562e230d165f0c0caa050684a9c1e2bd1bbb178e54a1d5273428435ae512455`. - -The latest tracked sys source is p9 while the clean guest kernel/userland is -p8. Both use OSREL 1500068; the module built and loaded successfully. This -source/guest patch-level distinction is recorded rather than hidden. - -## Fresh fixture qualification - -`tests/g6_multidev_fixtures.py` generated every source, primary image, blob, -flatdev, and negative image from zero with erofs-utils 1.8.6. No old test image -or report was an input. - -- Generator SHA256: - `18638aee056e05099b39477fa6ad4d678ab0a74a027cfb45146ae225ff35d950`. -- Final manifest SHA256: - `b636a52c7b3ec7f2343f07e2d389783c8383ec07d9d9468f7552e817891c3a81`. -- Final `SHA256SUMS` SHA256: - `ed81d63469b40f1aba691e54a08d39737c71e66d2ff4fef963a0101fa2ad9854`. -- Final fixture archive SHA256: - `28be1a5cdcebb163710433c9e7c30162c1170312c34d0b286f4143480881d62f`. -- Generated set: 62 artifacts, 17 parsed fixtures, 7 table/index negative - images, and 10 explicit mutation assertions. -- Reproducibility: two clean output directories had byte-identical - `manifest.json` and `SHA256SUMS`. -- Host fsck extraction qualified the mkfs split image, single-index image, - explicit two/three-slot images, table-at-zero, `uniaddr=0`, packed fragment, - and the original two-block LZ4 pcluster. -- Guest final `sha256 -c SHA256SUMS` returned 0. - -During qualification, an initial maximum-48-bit negative used all ones. Field -review identified that value as the legal chunk hole sentinel. The generator -was corrected to the largest non-NULL address `0xfffffffffffe`, regenerated -twice, and reverified. Only the intended primary artifact changed; its final -SHA256 is -`d6f113cb5d2cb7da7f892632930eedf1c84fb494f5e1e4f0d2a852496ee92951`. -The corrected kernel read returned `EINTEGRITY`. - -## Exact results - -| Test ID | Status | Manual evidence | -| --- | --- | --- | -| TC006 | PASS | Direct mkfs split primary/blob; full and cross-32K range `cmp`; external `MDIOCDETACH` returned `ERR#16 EBUSY`. | -| TC093 | PASS | 8-byte device-ID-0 indexes with no table; full and cross-32K range `cmp` on the primary provider. | -| TC094 | PASS | Reversed slot option order; 1/2/1 full and per-index ranges; `uniaddr=0` with nonzero ID; swapped media `ERR#6 ENXIO`; real external 8192-byte LZ4 pcluster produced the complete 131072-byte source SHA256. | -| TC095 | PASS | First/middle/last/cross-index reads; zero-high 48-bit control and nonzero-high unified read; mapped ID 3 returned `read ERR#19 ENODEV`, unaffected file remained readable. | -| TC096 | PASS | Parsed normal, table-at-zero, `uniaddr=0`, and high 48-bit tables; `df -k` reported 916 KiB = 229 blocks x 4096; table outside primary returned `ERR#97 EINTEGRITY`. | -| TC097 | PASS | Six structural variants each returned `nmount ERR#97 EINTEGRITY`; after every failure md/KLD/consumer state was zero; slot-2 `ENOENT` after slot 1 open released slot 1 immediately. | -| TC098 | PASS | Packed NID 42 whole-file fragment; size 100000; full SHA/`cmp` and 8192-byte offset range passed. | -| TC099 | PASS | Direct primary+one-blob layout; `df -k` 900 KiB = 225 blocks x 4096; full/range compare; omitted blob option returned `ERR#6 ENXIO`. | -| TC100 | PASS | Four providers with option order 3/1/2; full read, 262144-byte range across both transitions, and three concurrent reads passed; missing/duplicate/short returned `ERR#6/22/6`. | -| TC101 | PASS | Explicit unified, nonzero-ID flatdev, device-ID-0 flatdev, and nonzero-high 48-bit reads matched source; gap, explicit/unified cross-slot chunk, explicit/flatdev two-block pcluster, and largest non-NULL 48-bit address all returned `read ERR#97 EINTEGRITY`. | -| TC118 | PASS | No options/one option/short returned `ERR#6`; duplicate/primary-as-slot returned `ERR#22`; normal detach `ERR#16`; forced orphan cold slot-2 read `ERR#6`; second mount `ERR#16`; primary and external missing paths preserved `ERR#2`. | - -## External-data proof - -The TC094 compressed primary is 8192 bytes and contains only metadata plus its -device table. The original pcluster blocks are absent. Its HEAD pblk is the -slot-1 unified address, and the 8192 compressed bytes exist only in the -external provider. FreeBSD returned the complete source SHA256 -`8dd7830946e7154d9feaaa87df6d1d39e47d51da22a690c5469dc4f2955ea0f1`; -therefore this was an external compressed read, not table-only parsing. - -TC101 also mounted generated combined flatdev providers with no `device.N` -options. Both nonzero device-ID mapping and device-ID-0 unified mapping -returned the same source SHA256 values as explicit providers. erofs-utils -1.8.6 does not qualify these two forms, so the FreeBSD full/range comparisons -are the runtime qualification. - -## Errno summary - -- `EBUSY` = 16: live md detach and second concurrent mount. -- `ENXIO` = 6: missing/short media and forced-orphan cold I/O. -- `ENODEV` = 19: mapped but undeclared chunk device ID 3. -- `EINVAL` = 22: duplicate provider and primary reused as an external slot. -- `ENOENT` = 2: nonexistent primary or external pathname. -- `EINTEGRITY` = 97: malformed tables and complete-extent range failures. - -## Lifecycle and platform behavior - -Every TC ended with zero EROFS mounts, zero md units, no loaded EROFS KLD, and -no matching GEOM consumer. TC097 repeated this after each individual malformed -table. TC118 forced md92 orphaning, observed cold `read ERR#6 ENXIO`, then -unmounted and released the remaining providers without panic or hang. The -final dmesg anomaly scan for panic, fatal trap, assertion, watchdog, or EROFS -errors was empty. - -Linux device tables allow table offset zero and skip `uniaddr=0` during -device-ID-0 lookup while retaining explicit nonzero-ID selection. FreeBSD -matches those on-disk semantics but receives external providers through -explicit `device.` mount options and owns read-only GEOM consumers. -Consequently FreeBSD-specific `EBUSY` detach and forced-orphan `ENXIO` behavior -was tested directly rather than inferred from Linux loop devices. - -## Issues - -None. No kernel failure remained after correcting the fixture's reserved hole -sentinel, so no issue file and no source change were created. diff --git a/tests/results/manual/2026-08-09T1343Z-g8/manual-test-report.md b/tests/results/manual/2026-08-09T1343Z-g8/manual-test-report.md deleted file mode 100644 index fc81720..0000000 --- a/tests/results/manual/2026-08-09T1343Z-g8/manual-test-report.md +++ /dev/null @@ -1,269 +0,0 @@ -# repo22 G8 Documentation, NFS, and Integrity Manual Regression - -Date: 2026-08-09 13:07-13:56 UTC - -Executor: G8 manual agent - -Source baseline: `f11fff5b8e8050e1017ed86f0bcf71042b2b45aa` - -Exact scope: `TC111`, `TC131`-`TC133`, `TC154`-`TC156` (7 IDs) - -## Result - -All seven requested test cases passed. There are no KFAIL, SHELVED, or ENV -results, no duplicate IDs, and no omitted IDs. - -| Status | Count | -| --- | ---: | -| PASS | 7 | -| KFAIL | 0 | -| SHELVED | 0 | -| ENV | 0 | -| Duplicate | 0 | -| Omitted | 0 | - -| Test ID | Status | Manual evidence | -| --- | --- | --- | -| TC111 | PASS | Audited current source and documentation against real generic `/sbin/mount`; corrected helper and writable-request claims. | -| TC131 | PASS | Real mountd/nfsd NFSv3 export, four vnode types, read-only behavior, 16-byte handle resolution, and RPC statistics. | -| TC132 | PASS | Handle ABI/classes, stable remount, replacement `ESTALE`, metabox and external-provider regressions, and nfsd restart. | -| TC133 | PASS | Exact 12,050-name READDIRPLUS lists, five cold remounts, 20 individually waited workers, and zero RPC errors. | -| TC154 | PASS | Same-superblock/UUID/NID replacement changed per-inode generation; unchanged remount stayed stable. | -| TC155 | PASS | Explicit-extent physical-address wrap returned `EINTEGRITY` on both direct reads and in `truss`. | -| TC156 | PASS | Compact/extended epoch, `time_t`, and nanosecond boundary corruptions returned `EINTEGRITY`. | - -No kernel source changed. No kernel failure remained, so no issue file was -created. - -## Isolated Environment - -- Worktree: `/work/build/repo22-manual-g8-20260809T130723Z`, branch - `manual-g8-20260809T130723Z`, created directly from the source baseline. -- VM: independent qcow2 overlay backed by the clean FreeBSD development base; - QEMU TCG, 6144 MB, 4 vCPUs, SSH only on `127.0.0.1:9228`. -- Guest: FreeBSD `15.0-RELEASE-p8` amd64, - `releng/15.0-n281036-53054229dcb3`, OSREL `1500068`. -- Guest kernel SHA256: - `b9abf7b58f9dd4d87f14d2fbc306cf255e6eadc47b8ae885a230f65e28be4562`. -- Host erofs-utils: 1.8.6. -- No CI, runner, or test wrapper was used. Markdown procedures were issued - manually through SSH, with direct command status kept separate from tracing - tool status. - -## Exact Build - -The repo22 source archive came from the exact baseline commit. The matching -tracked FreeBSD build input was `dev-freebsd-releng/sys`, identified by -`REVISION="15.0"` and `BRANCH="RELEASE-p9"`. The build ran natively in the -guest as: - -```sh -FREEBSD_SRC=/root/freebsd-src WITH_ZSTDIO=1 ./build.sh -``` - -The compile command contained `-DZSTDIO` and kernel `-Werror`. The module had -the five expected `ZSTD_*` kernel references and no unresolved `bcmp`. - -| Object | SHA256 | -| --- | --- | -| repo22 source archive | `2f76ef7df9117c6e7d62ad41809638ad306e676191346083bf95fe551f7df515` | -| FreeBSD 15 `sys` archive | `cd806ac4d6aee5d7ceb6a2f9603020b48b5012835bc8c7f2ec0985572fcd0262` | -| `erofs.ko` | `e8cd4839328de089355505efb1629c835d2f93faad2e14f63c6e6cb844ce9589` | -| `nfs_fh_tool` | `db704f67d718d04b84ed32b5cdbecb93a99c5fe3b4ab238d603a10e005d790e3` | - -The guest kernel is p8 and the tracked source is p9, but both use the FreeBSD -15.0 OSREL ABI. The exact module loaded and unloaded successfully. - -## Fixture Qualification - -`tests/review_fixtures.py` generated two independent output directories. Their -manifests and checksum inventories were byte-identical, and all nine image -checksums self-verified. Its structured assertions recorded: - -- target NID 452 at inode offset 14464 after checksum block 4096; -- identical complete superblock block and UUID for NFS images A and B; -- generations `3895653226 -> 548470773` from the changed raw inode; -- explicit extent base `0xfffffffffffff000`, `plen0=8192`, `plen1=4096`, and - wrap before logical cluster 4096; -- compact epoch wrap/range, compact nanoseconds 1000000000, extended - nanoseconds 1000000000, and extended seconds `INT64_MAX+1`. - -Review generator SHA256 was -`b92fd6b6122d55882998138efc0109e24b236f9e877405dbff7914284a18c124`; -manifest SHA256 was -`be0836043d172b34eb625e5259f7e6cb9a8992ff74e7d1a5cdc10d69282e2335`. - -The NFS stress image was generated from a fresh deterministic tree containing -12,050 `bigdir` files, 256 concurrent-read files, four basic vnode types, and a -22,020,096-byte throughput file. Image A SHA256 was -`88721877dc3762c8eafdf3bcca2653787cdcf41983978ab159705761abefac37`; -image B was -`964b3d75579740c9b08bc9cae8329e3d641d3c505e9391de3ab9f5d24fbecd17`. -The expected sorted-name list SHA256 was -`4fbf1b6103e8884223629c3fd03f4ec36061e65cd2a1becfc96670daa96f9d71`. - -Fresh structured metabox and multidevice helpers also self-verified the two -TC132 regression inputs. The metabox image SHA256 was -`dd7b04097d1bfe283b65c95d759acd2176beb7cb0f1fe9d5ddbc0694b5acba9d`; -the multidevice primary/slot hashes were `918aa3a64e8861011914d967912909ce9de98418cff711cfbf8f91c0c7717f72`, -`4e31d95a12405f2f1396fb926a6854c591fc335eade8944f73fdc7fe6758aee6`, -and `355a74880bd3648dea22ad18e19f809cee12303ceed2a99f53094c2b66563a11`. - -## TC111 - -`/sbin/mount_erofs` was absent and `command -v mount_erofs` returned 127. -Generic `/sbin/mount -t erofs` mounted the image successfully without `-o ro`. -The following real behavior was recorded: - -- the default mount was read-only and `touch` failed with `EROFS`; -- `-o rw` returned 0 but the resulting mount was still read-only; -- `-o ro,noexec,nosuid` returned 0 and all three flags appeared; -- non-export `mount -u` returned `EOPNOTSUPP`; -- an unknown filesystem option returned direct rc 1, with - `nmount(..., MNT_RDONLY) ERR#22`, and created no mount. - -Current `src`, `README.md`, `docs/features.md`, `docs/architecture.md`, -`docs/erofs.5`, and current progress material were checked. `README.md`, -`docs/erofs.5`, and the test procedure now describe the generic frontend and -the forced-read-only `rw` behavior accurately. The man page's split-device -example was also exercised by the TC132 multidevice mount. SEE ALSO entries -absent from the qualified guest were removed; `mandoc -Tlint -Werror` and -ASCII rendering both returned 0 in that guest. - -## TC131 - -The direct EROFS line lacked `NFS exported` before mountd. After installing the -loopback export and reloading mountd, `showmount -e` listed the exact path and -the direct mount gained `NFS exported`. `rpcinfo` showed NFSv3 and mountd over -TCP and UDP. - -The NFSv3 TCP client negotiated `rdirplus`. Regular file content, directory, -symlink target, FIFO type, and inode number matched the direct EROFS mount. -`touch` failed with `Read-only file system`. Local `fhstat` and `fhopen` -resolved the regular handle with `len=16`, `pad=0`, NID 116, and generation -2068234290. - -After basic operations client READDIRPLUS was 2. Server Write and Create were -both 0. TimedOut, Invalid, X Replies, and Retries were all 0. - -## TC132 - -Regular, directory, symlink, and FIFO handles all had `len=16`, `pad=0`, full -64-bit NIDs, and nonzero generations matching `st_gen`. Bad length/pad returned -`EINVAL`; bad generation/NID returned `ESTALE` through both `fhstat` and -`fhopen`. - -The unchanged image remounted on md80 preserved the complete handle SHA256 -`9e1df1952b43d383fea295301525ede7a69b7d57e5f60a75a6d7f1531e13df0a`, -and the old handle still read the file. Replacement B retained fsid and NID -116 but changed generation `2068234290 -> 857800581`; both old-handle paths -returned `ESTALE` while B resolved. - -The metabox handle preserved NID `0x8000000000000010`; invalid metabox NID and -generation returned `ESTALE`. After forced orphaning of external slot 2, the -valid file handle still resolved metadata and the read returned `ENXIO(6)`, -not `ESTALE`. - -An NFS client descriptor remained open across `service nfsd onerestart 3<&-`. -The old descriptor and a new path read both returned SHA256 -`3e2d0b4971e1f4bf00c3562a8bf9c5d85378c9de054f3da89541f49f2b449a7b`; -the export and all RPC registrations remained live. - -## TC133 - -Requested readdir sizes 512, 1024, and 4096 were all clamped by the FreeBSD -client to 8192; the default remained 65536. This is the recorded client floor. - -Every one of the four mounts returned exactly 12,050 unique expected names. -Every sorted list, plus all five cold-remount lists, had SHA256 -`4fbf1b6103e8884223629c3fd03f4ec36061e65cd2a1becfc96670daa96f9d71`. -The `ls -a1` list contained 12,052 entries, with `.` and `..` exactly once. - -All 12 traversal workers were waited by PID and returned 0. All four parallel -cat workers and four parallel stat workers were also waited by PID and returned -0. The informational 22,020,096-byte read completed in 14.51 seconds. - -Final client/server READDIRPLUS was 3420. TimedOut, Invalid, X Replies, and -Retries remained 0. Server Write, WriteRPC, Create, and Commit remained 0. -There were no new stale-handle, timeout, retry, panic, trap, assertion, -watchdog, or EROFS error lines. - -## TC154 - -Image A produced fsid `00000034:000000e0`, `len=16`, `pad=0`, NID 452, and -generation 3895653226. `nfs_fh_tool`, `stat st_gen`, and the fixture manifest -agreed. The unchanged remount preserved complete handle SHA256 -`79f2f4b8e354af0ae369849d7f66a739162ffadc4a5689c09643c9949b548de1` -and the old handle remained readable. - -Image B retained the complete superblock block, UUID, fsid, NID, and content, -but generation became 548470773. Both old-handle paths returned `ESTALE`; B's -handle resolved. The malformed same-NID replacement returned positive errno 45 -through both paths. - -## TC155 - -The self-checked fixture SHA256 was -`1e49ecf1917265fde9b606e80f7dbae8b14aca8c6f149128b31d6c825da546dd`. -The image mounted and target `stat` reported inode 40, size 1,048,576. Two -direct one-byte reads at logical offset 4096 returned rc 1 and `Integrity check -failed`; the traced read was `ERR#97`. The repeat exercised the cached vnode. -A DTrace `io:::start` positive control observed one raw -`md90 offset=4096 bytes=4096` event. A separate trace around the failing EROFS -read observed zero md90 events, proving the wrapped address did not reach the -provider. No panic or resource remained. - -## TC156 - -Both compact root corruptions failed `nmount` with `ERR#97`. The compact range -image mounted with root mtime `INT64_MAX`; target stat returned `ERR#97` for -`INT64_MAX+1`. Both extended images mounted, and target stat returned `ERR#97` -for nanoseconds 1000000000 and seconds `INT64_MAX+1`. Every failure was run -directly and again under `truss`; no panic or resource remained. - -## Cleanup and Restoration - -Before NFS setup, `/etc/exports` did not exist; rpcbind, mountd, and nfsd were -stopped; and `rpcbind_enable`, `mountd_enable`, and `nfs_server_enable` were all -`NO`. Cleanup unmounted every loopback client while nfsd was alive, cleared and -reloaded exports, stopped nfsd/mountd/rpcbind, then removed the direct EROFS -mount, md42, and the module. - -Final state exactly matched the baseline: `/etc/exports` absent, all three -services stopped with rc 1, all three rc settings `NO`, zero NFS/EROFS mounts, -zero md providers, no EROFS KLD, no service process, and `rpcinfo` refused the -connection because rpcbind was stopped. The complete NFS-period dmesg delta had -zero anomaly lines. - -The retained guest evidence archive is outside the repository at -`/work/build/repo22-manual-g8-20260809T130723Z-vm/evidence/`: - -| Artifact | SHA256 | -| --- | --- | -| `final-guest-evidence.txt` | `eb67210818e663c15e4670ada9ba63b82b44e9ed3a9486c575df3a71ed3f0522` | -| `TC155-dtrace-assert.txt` | `f8b52985a74c8c777b3b830c39749be65c613165ffbcde6d0018dc471018c433` | -| `final-post-dtrace-cleanup.txt` | `90f04d8f5a4ae35c87cc3adb1aeb939c7cc5534233ed63e9ff4501c83fd857d2` | -| `repo22-g8-evidence.tar.gz` | `9a378fd619f9bca21f40689d746308933aa22e440f7201104732f91634c21290` | - -## Non-Qualified Attempts - -One TC131 setup command stopped at the expected nonzero service-status probe -because that probe was mistakenly under `set -e`; it occurred before module, -exports, or service changes. One TC133 attempt stopped after the four exact -lists because a shell-quoted dot assertion matched zero; the three added -clients were unmounted and the complete procedure was rerun from the start. -Tracing wrappers returned 0 even when their child failed, so all error claims -use separate direct statuses plus syscall errno. None of these discarded -attempts is counted as PASS evidence. - -Two initial DTrace specifications used field names from other provider ABIs -and failed at compile time before any probe I/O. The qualified specification -used FreeBSD `struct devstat` and `struct bio` fields. Its first assertion used -a line anchor even though the format emitted a literal `\\n`; substring counts -then proved one positive md90 event and zero failing-read md90 events. DTrace's -automatically loaded module set was unloaded as a unit and the original KLD -set was reverified. - -## Issues - -None. diff --git a/tests/results/manual/2026-08-09T1359Z-g7/manual-test-report.md b/tests/results/manual/2026-08-09T1359Z-g7/manual-test-report.md deleted file mode 100644 index 682a3f0..0000000 --- a/tests/results/manual/2026-08-09T1359Z-g7/manual-test-report.md +++ /dev/null @@ -1,183 +0,0 @@ -# repo22 G7 boundary and stress full manual regression report - -## Scope and result - -- Exact scope: TC120-TC130 inclusive. -- Count check: 11 requested, 11 executed, 0 duplicate, 0 omitted. -- Result: **PASS 11, KFAIL 0, SHELVED 0, ENV 0**. -- No repo22 kernel source was changed. -- No kernel failure occurred, so no G7 issue file was opened. -- No CI, runner, or test wrapper was used. Each test's Markdown commands were - issued manually in the dedicated FreeBSD guest. - -## Baseline and source identity - -- Requested initial `xdm/main`: `c566d8ac6bf8e801082bbccf108451f6ee46ad40`. -- G6 pushed first; G7 fetched and rebased onto - `f11fff5b8e8050e1017ed86f0bcf71042b2b45aa` before editing. -- Exact build commit: - `e44e24c2d7e4955498fb42b06bcf376d6c452190`. -- The equivalent G7 test/source commit after the final remote rebase is - `896683f14af2ba36705498f85d0d8af02b513d31`; its repo22 source tree is - byte-identical to the build input. -- Exact `repo-community/repo22/src` tree: - `34a56a583b3b43e7e103b26a30dea1d400c2bc6c`. -- G8 later advanced `xdm/main` to `6f336d0a7`; its repo22 source tree was the - same `34a56a58...`. G7 rebased before final push without force. -- Build configuration: `WITH_ZSTDIO=1`. -- Tracked FreeBSD sys source: `REVISION="15.0"`, `BRANCH="RELEASE-p9"`. -- repo22 source archive SHA256: - `e715e9d323be1448e0395add99cf017dea3c1bbb6579915cf3d07f7265ca2891`. -- FreeBSD sys archive SHA256: - `71f535ab5a9ef686c6adbe123a1d1e65fd9bfa9cd19724f9fcef222a8efbe9d8`. - -## Isolated environment - -- Worktree: `/work/build/repo22-manual-g7-20260809T125507Z`. -- Branch: `manual-g7-20260809T125507Z`. -- Artifact root: `/work/build/repo22-g7-20260809T125507Z`. -- Dedicated qcow2 overlay backed by `/work/build/vm-freebsd-dev-base.qcow2`. -- Dedicated SSH forward: `127.0.0.1:9227`. -- QEMU: TCG multi-thread, `qemu64`, 6144 MiB RAM, 4 vCPUs, virtio disk/network. -- Guest: FreeBSD `15.0-RELEASE-p8`, amd64, - `releng/15.0-n281036-53054229dcb3`, OSREL `1500068`. -- Guest kernel SHA256: - `b9abf7b58f9dd4d87f14d2fbc306cf255e6eadc47b8ae885a230f65e28be4562`. -- Host erofs-utils: 1.8.6. -- RACCT/RCTL was initially present but disabled. The dedicated overlay set - loader tunable `kern.racct.enable="1"`, rebooted, then reported enabled with - an empty initial rule set. - -## Exact KLD and probe - -The KLD built successfully from the archived commit with the tracked p9 sys -tree and loaded by full pathname. `kldstat -v` assigned file ID 5 and showed -module ID 507 for `erofs`. - -| Artifact | SHA256 | -|---|---| -| `/root/repo22-g7/erofs.ko` | `06dfa530efb2d494950672d60453d86a6e70f06ca2ce7b4eff3396ca8018c76d` | -| `/root/repo22-g7/g7_probe` | `a0bca6ea596cec6063bc058aebee12929a1814e6d343283ffbe051551bf75c57` | - -The KLD was an amd64 FreeBSD relocatable object with build ID -`d4fff529ae8e4c5395e1e867fd4ea5d57a9570b6`. The expected formal FreeBSD ZSTD -undefined symbols were present; no build or load error occurred. - -## Reproducible fixture - -Two fresh generations produced byte-identical source inventories, source -checksum lists, and image checksum lists. Each generation independently ran -the helper verification and erofs fsck checks. - -```text -source file count = 24023 -image count = 2 -SOURCE-INVENTORY.tsv SHA256 = b9be1745900a8cf6755068b6a78b93bc294cf8857539159b7771e5170bc1b9ed -SOURCE-SHA256SUMS SHA256 = 445929465a3031e84de9887fc5b02d72cb797df7ca512ea1cc874d11f77747d3 -SHA256SUMS SHA256 = f9cf45c1e3fe814290d9cd0b96d066509309cc3f764c545d5e7d7ddae4a9eac3 -fixture-manifest.json SHA256 = 0ba86269962b00329b99177494cfb099c0f58fe5de77d2dc1f12da684dedf09d -guest image/source verification = 2 images and 24023 sources, all OK -``` - -| Image | Bytes | SHA256 | -|---|---:|---| -| `boundaries.erofs` | 37752832 | `ff7e804dc309039fa5dfa9b6e75416d16224137c8f6d6d96bf04cfa0ba8a1a8c` | -| `workloads.erofs` | 155111424 | `5f21a045114c5956b80752736c9833fe6a975a296cd28324734c9b9e423f3984` | - -The sparse source was exactly 4294971393 bytes. It occupied 48 Linux -512-byte blocks before transfer and 512 FreeBSD blocks with a reported -32768-byte file-system block size after sparse-aware extraction. All five -markers and six boundary ranges passed the helper's source self-check. - -## Per-TC results - -| TC | Manual evidence | Result | -|---|---|---| -| TC120 | Exact size 0; source/mount empty SHA256 `e3b0c442...`; read returned 0; SET/END seeks returned 0 | PASS | -| TC121 | Exact 4294971393-byte size; six source-compared ranges across block, hole, 2 GiB, 4 GiB, and EOF boundaries; EOF read 0 | PASS | -| TC122 | Exactly 128 levels; deepest source/mount hash `ef151b18...`; full cmp and exact `pwd -P` | PASS | -| TC123 | Exact 255-byte name; one exact readdir row; size 4096; source/mount hash `0724316b...` | PASS | -| TC124 | Exactly 12000 one-KiB files and exact names; complete source/target hash-list digest `be93ef11...` | PASS | -| TC125 | Exactly 12000 direct entries and exact names/hashes; five boundary lookups matched; readdir timing recorded | PASS | -| TC126 | Exactly 16 saved PIDs, 16 waits, all rc 0; every expected/actual full-file hash matched; no PID remained | PASS | -| TC127 | Three RCTL-capped allocators reached ENOMEM, released all touched bytes, recovered allocation, and wait=0; pressure read hash/cmp matched | PASS | -| TC128 | 268435456-byte source/mount hash and full cmp matched; three complete sequential reads rc 0; metrics recording-only | PASS | -| TC129 | Three deterministic 16384-operation runs, all source-compared, zero mismatch, identical digest; metrics recording-only | PASS | -| TC130 | Seven saved PIDs and seven wait=0 results; sequential, hash, two random, 16-file, names, and stat workloads all matched; full cleanup | PASS | - -TC121 range digests were `7775e9e82783e06f`, `c86358c1fb3fabf7`, -`b43a063055adc383`, `a1bd19f7b12c60b2`, `5a378d4efc494af4`, and -`1873eaea77354aa6`, in manifest order. - -TC124's complete 12000-row source and mounted hash-list files both had SHA256 -`be93ef11d2e4cd0cd42ef057397b89978b624e42674f9181e5dd151b30636171`. -TC125's complete 12000-row files both had SHA256 -`8cde4ea887f9b21b265ac199bcbb447a656aa6d718a36424f4fa03a6c538ebe7`. - -TC126 persisted each worker's PID, source hash, mounted hash, and wait status. -The PID table SHA256 was `fc67656d...`; the 16-row wait table SHA256 was -`888f026d...`. Every wait code was 0 and every hash pair was equal. - -## Pressure evidence - -TC127 used three synchronized `g7_probe` processes and valid FreeBSD RCTL -rules `process::vmemoryuse:deny=640M`. The first process utilization -sample showed `memoryuse=532M` and `vmemoryuse=636M`. - -| Worker | Allocated before ENOMEM | errno | Released | Recovery | wait rc | -|---|---:|---:|---:|---|---:| -| 1 | 553648128 | 12 | 553648128 | ok | 0 | -| 2 | 545259520 | 12 | 545259520 | ok | 0 | -| 3 | 553648128 | 12 | 553648128 | ok | 0 | - -Free memory recorded by `vmstat -H` was about 4.01 GB before, 2.35 GB while -the allocations were held, and 3.89 GB after release. The 96 MiB source and -mounted file both hashed to -`e4baaad480721bfb69a0315f0092fcf1ad75426fda0b9242a750b5685ad75152` -and full `cmp` exited 0. RCTL rules and synchronization files were removed. - -## Recording-only metrics - -TC125's full 12000-entry sorted readdir scan under TCG recorded `real 11.48`, -`user 0.81`, and `sys 9.76` seconds. It would have failed the old invalid -10-second fixed threshold despite complete correctness, so timing was -correctly treated as recording-only. - -TC128 sequential reads: - -| Run | Bytes | Seconds | Bytes/s | wait rc | -|---|---:|---:|---:|---:| -| 1 | 268435456 | 6.764841 | 39680970 | 0 | -| 2 | 268435456 | 9.207558 | 29153816 | 0 | -| 3 | 268435456 | 10.603392 | 25315998 | 0 | - -TC129 used seed `0x6a09e667f3bcc909`, 16384 operations, and 4096-byte -blocks. All runs produced digest `908bc2905f48695d` and zero mismatches. - -| Run | Seconds | IOPS | Mean us | -|---|---:|---:|---:| -| 1 | 2.236047 | 7327.22 | 136.48 | -| 2 | 1.062087 | 15426.24 | 64.82 | -| 3 | 1.026698 | 15957.95 | 62.66 | - -TC130 random workers ran concurrently with the other five workloads. Seed -`0xbb67ae8584caa73b` recorded 442.73 IOPS, 2258.72 us mean, digest -`78e9d2c7f86ed20e`; seed `0x3c6ef372fe94f82b` recorded 455.91 IOPS, -2193.42 us mean, digest `ee76a5d094a0c6f2`. Both had zero mismatches. -The exact names files shared SHA256 `8d1cf816...`; exact stat files shared -SHA256 `ca8937b1...`. - -## Stability and cleanup - -- Final dmesg delta was empty; its SHA256 was the empty-file hash. No panic, - trap, assertion, watchdog, OOM, or EROFS error appeared. -- Every test detached its exact md provider after unmounting. -- Final guest state had zero EROFS mounts, md providers, G7 child processes, - RCTL rules, synchronization files, and Python bytecode caches. -- KLD file ID 5 was unloaded with `kldunload -i 5`; no EROFS allocator row or - KLD remained. -- The guest remained responsive, powered off normally, QEMU PID 344535 exited, - port 9227 was released, and the dedicated overlay/log/PID files were deleted. -- Compressed evidence archive SHA256: - `f18246076e5ff93309187a1826afe9f2dfa4a0bcdf7a958419d2890d05727ccd`. -- No issues were created because all correctness gates passed. diff --git a/tests/results/manual/2026-08-09T1804Z-final-review-independent/manual-test-report.md b/tests/results/manual/2026-08-09T1804Z-final-review-independent/manual-test-report.md deleted file mode 100644 index 70921bd..0000000 --- a/tests/results/manual/2026-08-09T1804Z-final-review-independent/manual-test-report.md +++ /dev/null @@ -1,119 +0,0 @@ -# repo22 Final Review Independent Manual Regression - -Execution window: 2026-08-09 18:04-18:29 UTC - -Source baseline: `fcc85b93d5f8fd9671686bd68bf3b086c8bd25cf` - -Scope: TC157-TC161, final dual-configuration build and mount smoke, TC111 -coverage-matrix follow-up, and resource hygiene. No CI runner or automated -kernel-test harness was used. - -## Result - -All five assigned test cases passed. - -| Test ID | Status | Manual evidence | -| --- | --- | --- | -| TC157 | PASS | Six 16/32-byte descending, duplicate, and cross-branch tables returned positive `EINTEGRITY`; repeated lookup agreed; a raw offset-4096 control was observable while failed lookups produced no payload read. | -| TC158 | PASS | A 17,592,186,056,704-byte sparse provider produced direct and file-handle `st_blocks=34359738384`; two FBT returns recorded `va_bytes=17592186052608`; the file remained readable. | -| TC159 | PASS | FIFO size-only setattr returned 0; size plus mode, owner, times, and all fields returned `EROFS(30)`; metadata remained `10640:0:0:0:0`. | -| TC160 | PASS | Both `OFF_MAX` and zero-offset `getdirentries` calls returned `EINTEGRITY(97)` and preserved their offsets; independent truss SHA256 was `6df9c594bfba8aaf849fa7105fda09a5cab99d79f04c42901a438321c90700e0`. | -| TC161 | PASS | Both normal configurations built, the private `nm` shim failed before publication without a SUCCESS line or temporary file, the previous KLD hash stayed unchanged, and a final normal build used `/usr/bin/nm` and exited 0. | - -The TC157 fixtures do not cover a first nonzero `lstart` or an extreme -extent-count performance boundary. These are residual coverage limits, not -observed failures. Positive mapped-payload coverage remains the separate TC146 -PARTIAL issue. - -## Exact Inputs - -The source archive exported from the baseline had SHA256 -`a0c330601f9d7b2aa246a17e67bcd67914d457480cb69df025d09bc34d8b7b31`. -The independent fixture archive had SHA256 -`bf8ab5430f06dfc067f041002cc7861b0b110dc00d9c447a82877f21b67b09ed`. -Its manifest and evidence hashes were -`b880be5d214985e6a7f75cc77fea333f046a30963a9d310d57a6cdc67104e740` -and `ace71446671b3d17e6a46997f43fc605a7a6bb5993c9de0e6e12cd223746b317`. - -The fixture evidence recorded: - -- complete TC157 logical-start arrays and old binary-search visit indices; -- TC158 `blocks_lo=2`, `blocks_hi=1`, and 4,294,967,298 data blocks; -- TC159 compact FIFO mode `010640`; -- TC160 `i_size=9223372036854775807` and expected errno 97. - -## TC161 Build Qualification - -The shim log SHA256 was -`a1a83797ba2c0111b66bb8783f3a67087420ef7a9bccc93fd42fb94c9c24c593`. -It contained `ERROR: nm failed while checking erofs.ko`, no SUCCESS line, and -left no `nm-undef.*` file. The post-shim status file recorded `exit=0` and -`nm=/usr/bin/nm`; its build log SHA256 was -`5ce2519a85de7a4846f95fdb858fd7818182aad3fa55a7ea8d06cf7687143f11`. - -The post-shim build used an explicit normal path and environment: - -```sh -env PATH=/usr/bin:/bin:/usr/sbin:/sbin \ - FREEBSD_SRC=/tmp/repo22-freebsd15-src \ - WITH_ZSTDIO=0 ./build.sh -``` - -Its key log lines were `==> Building repo22 erofs.ko` and -`==> SUCCESS: .../build/erofs.ko`. - -Final exact-source builds used FreeBSD 15 kernel `-Werror`: - -| Configuration | KLD SHA256 | Result | -| --- | --- | --- | -| `WITH_ZSTDIO=0` | `15fda9d334132cd81769ce4dff4f8411a6e2b4cf7531c352530d0de85f42a2d2` | PASS | -| `WITH_ZSTDIO=1` | `23782dc0ce7da188807d35020bf2d8c6044b796c5c9a8746b398ba784cd6ad4e` | PASS | - -The final ZSTD build log SHA256 was -`e58bc73d1b0b8295d8baba0cf0ce07bf4e9c994837e65c7ccdd731dc91edb283`. - -## Final Module Smoke and Cleanup - -Each final KLD loaded independently, mounted `special-setattr.erofs` through a -fresh vnode md provider, exposed the expected FIFO, unmounted, detached the md -unit, and unloaded. The smoke log SHA256 was -`7ced7329da9f39527cf903213dee4c99dbe6d8d6fa96e55bca3427c4f4a56420`. - -Final guest state was: - -```text -residual-mount-count=0 -residual-md= -residual-kld-count=0 -smoke-exit=0 -``` - -The host repo22 ignored `build/`, old ignored generated fixture/artifact trees, -and `__pycache__` content were removed before aggregation. No generated KLD, -object, image, or Python bytecode is acceptance evidence in Git. - -## TC111 Coverage-Matrix Follow-up - -A bounded filename audit found 162 specifications, TC000 through TC161, with -162 unique IDs, no duplicate, and no missing ID. TC000 is a template and is not -counted as executable. - -The eight canonical G1-G8 report tables contained exactly 156 rows and 156 -unique IDs covering TC001-TC156, with no duplicate or omission. TC157-TC161 -add five unique executable cases. TC060's original G3 failure is superseded by -its dated fixed-source PASS report; TC153's G3 result is PASS. - -The canonical final result is therefore: - -```text -Executable test cases: 161 -PASS: 160 -PARTIAL: 1 (TC146) -FAIL/KERNEL-FAIL/ENV/SHELVED_TC: 0 -``` - -TC146's shelved positive mapped-payload subitem is not counted as an additional -test case. G1-G8 evidence was collected across multiple source commits; this -report does not claim that every historical test ran on the final KLD. The -final `fcc85b93d` code baseline received the independent TC157-TC161 checks and -the affected final build/load/mount regressions recorded here. diff --git a/tests/review_fixtures.py b/tests/review_fixtures.py deleted file mode 100644 index 01b76d5..0000000 --- a/tests/review_fixtures.py +++ /dev/null @@ -1,608 +0,0 @@ -#!/usr/bin/env python3 -"""Build and self-check fixtures for TC154-TC156.""" - -from __future__ import annotations - -import argparse -import hashlib -import json -import os -from pathlib import Path -import shutil -import struct -import subprocess - - -SUPER = 1024 -EROFS_MAGIC = 0xE0F5E1E2 -FEATURE_COMPAT_SB_CHKSUM = 0x1 -EROFS_INODE_FLAT_PLAIN = 0 -EROFS_INODE_FLAT_INLINE = 2 -EROFS_INODE_COMPRESSED_FULL = 1 -Z_EROFS_ADVISE_EXTENTS = 0x1 -UINT64_MAX = (1 << 64) - 1 -INT64_MAX = (1 << 63) - 1 -FNV1_32_INIT = 33554467 -FNV_32_PRIME = 0x01000193 - - -class FixtureError(RuntimeError): - pass - - -class Inode: - def __init__( - self, - nid: int, - offset: int, - inode_format: int, - inode_size: int, - xattr_size: int, - layout: int, - size: int, - start_block: int, - ) -> None: - self.nid = nid - self.offset = offset - self.inode_format = inode_format - self.inode_size = inode_size - self.xattr_size = xattr_size - self.layout = layout - self.size = size - self.start_block = start_block - - -class ErofsImage: - def __init__(self, data: bytes | bytearray) -> None: - self.data = bytearray(data) - self.validate_superblock() - - @classmethod - def load(cls, path: Path) -> "ErofsImage": - return cls(path.read_bytes()) - - def clone(self) -> "ErofsImage": - return ErofsImage(self.data) - - def u16(self, offset: int) -> int: - return struct.unpack_from(" int: - return struct.unpack_from(" int: - return struct.unpack_from(" None: - struct.pack_into(" None: - struct.pack_into(" None: - struct.pack_into(" int: - return self.data[SUPER + 12] - - @property - def block_size(self) -> int: - if not 9 <= self.block_bits <= 16: - raise FixtureError(f"invalid block bits: {self.block_bits}") - return 1 << self.block_bits - - @property - def super_size(self) -> int: - return 128 + self.data[SUPER + 13] * 16 - - @property - def checksum_end(self) -> int: - length = self.block_size - if length > SUPER: - length -= SUPER - return SUPER + length - - @property - def feature_compat(self) -> int: - return self.u32(SUPER + 8) - - @property - def root_nid(self) -> int: - feature_incompat = self.u32(SUPER + 80) - root_nid_8b = self.u64(SUPER + 112) - if feature_incompat & 0x80 and root_nid_8b != 0: - return root_nid_8b - return self.u16(SUPER + 14) - - def calculated_checksum(self) -> int: - window = bytearray(self.data[SUPER : self.checksum_end]) - struct.pack_into("> 1) ^ ( - polynomial if checksum & 1 else 0 - ) - return checksum & 0xFFFFFFFF - - def checksum_valid(self) -> bool: - if not self.feature_compat & FEATURE_COMPAT_SB_CHKSUM: - return True - return self.u32(SUPER + 4) == self.calculated_checksum() - - def update_checksum(self) -> None: - if not self.feature_compat & FEATURE_COMPAT_SB_CHKSUM: - raise FixtureError("base image lacks superblock checksum support") - self.put_u32(SUPER + 4, 0) - self.put_u32(SUPER + 4, self.calculated_checksum()) - if not self.checksum_valid(): - raise FixtureError("updated superblock checksum does not verify") - - def validate_superblock(self) -> None: - if len(self.data) < SUPER + 144: - raise FixtureError("image is too short for an EROFS superblock") - if self.u32(SUPER) != EROFS_MAGIC: - raise FixtureError(f"bad EROFS magic: {self.u32(SUPER):#x}") - if SUPER + self.super_size > len(self.data): - raise FixtureError("declared superblock is truncated") - if not self.checksum_valid(): - raise FixtureError("superblock checksum is invalid") - - def inode(self, nid: int) -> Inode: - metadata = self.u32(SUPER + 40) << self.block_bits - offset = metadata + (nid << 5) - if offset > len(self.data) - 32: - raise FixtureError(f"nid {nid} lies outside the image") - inode_format = self.u16(offset) - inode_size = 64 if inode_format & 1 else 32 - if offset > len(self.data) - inode_size: - raise FixtureError(f"nid {nid} has a truncated inode") - xattr_count = self.u16(offset + 2) - xattr_size = 0 if xattr_count == 0 else 12 + 4 * (xattr_count - 1) - size = ( - self.u64(offset + 8) - if inode_size == 64 - else self.u32(offset + 8) - ) - return Inode( - nid=nid, - offset=offset, - inode_format=inode_format, - inode_size=inode_size, - xattr_size=xattr_size, - layout=(inode_format >> 1) & 7, - size=size, - start_block=self.u32(offset + 16), - ) - - def directory_data(self, inode: Inode) -> bytes: - if inode.size == 0: - raise FixtureError("empty directory cannot contain a target") - if inode.layout == EROFS_INODE_FLAT_PLAIN: - offset = inode.start_block << self.block_bits - end = offset + inode.size - if end > len(self.data): - raise FixtureError("plain directory data is truncated") - return bytes(self.data[offset:end]) - if inode.layout != EROFS_INODE_FLAT_INLINE: - raise FixtureError(f"unsupported directory layout {inode.layout}") - tail_start = ((inode.size + self.block_size - 1) // self.block_size - 1) - tail_start *= self.block_size - full_offset = inode.start_block << self.block_bits - inline_offset = inode.offset + inode.inode_size + inode.xattr_size - full = self.data[full_offset : full_offset + tail_start] - tail = self.data[inline_offset : inline_offset + inode.size - tail_start] - if len(full) + len(tail) != inode.size: - raise FixtureError("inline directory data is truncated") - return bytes(full + tail) - - def directory_entries(self, inode: Inode) -> list[tuple[bytes, int]]: - data = self.directory_data(inode) - entries: list[tuple[bytes, int]] = [] - for block_start in range(0, len(data), self.block_size): - block = data[block_start : block_start + self.block_size] - if len(block) < 12: - raise FixtureError("short directory block") - first_nameoff = struct.unpack_from("= len(block) - ): - raise FixtureError("invalid first directory name offset") - count = first_nameoff // 12 - previous = 0 - for index in range(count): - entry_offset = index * 12 - nid = struct.unpack_from(" len(block) - ): - raise FixtureError("invalid directory name range") - name = block[nameoff:endoff].split(b"\0", 1)[0] - if not name: - raise FixtureError("empty directory name") - entries.append((name, nid)) - previous = nameoff - return entries - - def resolve_root_entry(self, name: str) -> Inode: - encoded = name.encode("ascii") - root = self.inode(self.root_nid) - for entry_name, nid in self.directory_entries(root): - if entry_name == encoded: - return self.inode(nid) - raise FixtureError(f"root entry not found: {name}") - - def inode_bytes(self, inode: Inode) -> bytes: - return bytes(self.data[inode.offset : inode.offset + inode.inode_size]) - - def generation(self, inode: Inode) -> int: - seed = fnv1_32( - bytes(self.data[SUPER : SUPER + self.super_size]), FNV1_32_INIT - ) - generation = fnv1_32(struct.pack(" None: - self.validate_superblock() - path.write_bytes(self.data) - - -def fnv1_32(data: bytes, initial: int) -> int: - value = initial - for byte in data: - value = (value * FNV_32_PRIME) & 0xFFFFFFFF - value ^= byte - return value - - -def align(value: int, alignment: int) -> int: - return (value + alignment - 1) & -alignment - - -def run_mkfs(source: Path, image: Path, uuid: str, *extra: str) -> None: - command = [ - "mkfs.erofs", - "-d0", - "-x-1", - "-T0", - "--all-time", - "--all-root", - "--workers=1", - "--sort=path", - "-U", - uuid, - *extra, - str(image), - str(source), - ] - subprocess.run(command, check=True) - - -def normalize_times(root: Path) -> None: - for path in sorted(root.rglob("*"), reverse=True): - os.utime(path, (0, 0), follow_symlinks=False) - os.utime(root, (0, 0), follow_symlinks=False) - - -def sha256(path: Path) -> str: - return hashlib.sha256(path.read_bytes()).hexdigest() - - -def make_fixtures(output: Path) -> None: - if shutil.which("mkfs.erofs") is None: - raise FixtureError("mkfs.erofs is required") - if output.exists() and any(output.iterdir()): - raise FixtureError(f"output directory is not empty: {output}") - output.mkdir(parents=True, exist_ok=True) - source = output / "source" - nfs_source = source / "nfs" - extent_source = source / "extent" - extended_source = source / "extended" - nfs_source.mkdir(parents=True) - extent_source.mkdir(parents=True) - extended_source.mkdir(parents=True) - - for index in range(192): - (nfs_source / f"filler-{index:03d}.txt").write_text( - f"filler {index:03d}\n", encoding="ascii" - ) - (nfs_source / "zz-identity.txt").write_text( - "stable inode identity\n", encoding="ascii" - ) - (extent_source / "extent.bin").write_bytes(b"E" * (1024 * 1024)) - (extended_source / "extended-time.txt").write_text( - "extended timestamp\n", encoding="ascii" - ) - normalize_times(source) - - nfs_base_path = output / ".nfs-base.erofs" - extent_base_path = output / ".extent-base.erofs" - extended_base_path = output / ".extended-base.erofs" - run_mkfs( - nfs_source, - nfs_base_path, - "66666666-7777-4888-9999-aaaaaaaaaa54", - "-E", - "force-inode-compact", - ) - run_mkfs( - extent_source, - extent_base_path, - "66666666-7777-4888-9999-aaaaaaaaaa55", - "-E", - "legacy-compress,force-inode-extended", - "-z", - "lz4", - "-C4096", - ) - run_mkfs( - extended_source, - extended_base_path, - "66666666-7777-4888-9999-aaaaaaaaaa56", - "-E", - "force-inode-extended", - ) - - evidence: list[str] = [] - manifest: dict[str, object] = {} - - nfs_a = ErofsImage.load(nfs_base_path) - identity_a = nfs_a.resolve_root_entry("zz-identity.txt") - if identity_a.inode_size != 32: - raise FixtureError("NFS identity target is not a compact inode") - if identity_a.offset < nfs_a.checksum_end: - raise FixtureError("NFS identity inode overlaps the checksum window") - nfs_a_path = output / "nfs-inode-a.erofs" - nfs_a.save(nfs_a_path) - - nfs_b = nfs_a.clone() - identity_b = nfs_b.resolve_root_entry("zz-identity.txt") - old_mtime = nfs_b.u32(identity_b.offset + 12) - nfs_b.put_u32(identity_b.offset + 12, old_mtime + 1) - nfs_b_path = output / "nfs-inode-b.erofs" - nfs_b.save(nfs_b_path) - nfs_differences = [ - offset - for offset, (left, right) in enumerate(zip(nfs_a.data, nfs_b.data)) - if left != right - ] - mtime_offsets = range(identity_b.offset + 12, identity_b.offset + 16) - if not nfs_differences or any( - offset not in mtime_offsets for offset in nfs_differences - ): - raise FixtureError("NFS replacement changed data outside i_mtime") - if nfs_a.data[: nfs_a.checksum_end] != nfs_b.data[: nfs_b.checksum_end]: - raise FixtureError("NFS replacement changed the superblock block") - if nfs_a.data[SUPER : SUPER + nfs_a.super_size] != nfs_b.data[ - SUPER : SUPER + nfs_b.super_size - ]: - raise FixtureError("NFS replacement changed the declared superblock") - if nfs_a.inode_bytes(identity_a) == nfs_b.inode_bytes(identity_b): - raise FixtureError("NFS replacement did not change inode metadata") - generation_a = nfs_a.generation(identity_a) - generation_b = nfs_b.generation(identity_b) - if generation_a == generation_b: - raise FixtureError("per-inode generation did not change") - - nfs_corrupt = nfs_a.clone() - corrupt_inode = nfs_corrupt.resolve_root_entry("zz-identity.txt") - nfs_corrupt.put_u16( - corrupt_inode.offset, nfs_corrupt.u16(corrupt_inode.offset) | 0x8000 - ) - nfs_corrupt_path = output / "nfs-inode-corrupt.erofs" - nfs_corrupt.save(nfs_corrupt_path) - if nfs_a.data[: nfs_a.checksum_end] != nfs_corrupt.data[: nfs_a.checksum_end]: - raise FixtureError("corrupt inode fixture changed the superblock block") - - evidence.append( - "nfs " - f"nid={identity_a.nid} inode_offset={identity_a.offset} " - f"checksum_end={nfs_a.checksum_end} mtime={old_mtime}->{old_mtime + 1} " - f"generation={generation_a}->{generation_b} superblock_block=identical" - ) - manifest["nfs"] = { - "path": "/zz-identity.txt", - "nid": identity_a.nid, - "inode_offset": identity_a.offset, - "generation_a": generation_a, - "generation_b": generation_b, - "super_size": nfs_a.super_size, - "checksum_end": nfs_a.checksum_end, - } - - extent = ErofsImage.load(extent_base_path) - extent_inode = extent.resolve_root_entry("extent.bin") - if ( - extent_inode.inode_size != 64 - or extent_inode.layout != EROFS_INODE_COMPRESSED_FULL - ): - raise FixtureError("extent target is not an extended full-index inode") - header = align( - extent_inode.offset + extent_inode.inode_size + extent_inode.xattr_size, - 8, - ) - physical_base_offset = align(header + 8, 4) - record_offset = physical_base_offset + 8 - if record_offset + 8 > len(extent.data): - raise FixtureError("extent records lie outside the base image") - root_inode = extent.inode(extent.root_nid) - if not ( - record_offset + 8 <= root_inode.offset - or root_inode.offset + root_inode.inode_size <= header - ): - raise FixtureError("extent conversion overlaps the root inode") - struct.pack_into( - " argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - subparsers = parser.add_subparsers(dest="command", required=True) - make_parser = subparsers.add_parser("make") - make_parser.add_argument("--output", type=Path, required=True) - return parser.parse_args() - - -def main() -> None: - args = parse_args() - if args.command == "make": - make_fixtures(args.output) - - -if __name__ == "__main__": - try: - main() - except (FixtureError, OSError, subprocess.CalledProcessError) as error: - raise SystemExit(f"review_fixtures.py: {error}") from error diff --git a/tests/sparse_hole_probe.c b/tests/sparse_hole_probe.c deleted file mode 100644 index c9bf434..0000000 --- a/tests/sparse_hole_probe.c +++ /dev/null @@ -1,63 +0,0 @@ -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -int -main(int argc, char **argv) -{ - unsigned char byte; - char *end; - void *map; - uintmax_t raw_offset; - off_t map_offset, offset; - size_t delta, pagesize; - struct stat sb; - int fd; - - if (argc != 3) - errx(2, "usage: sparse_hole_probe file offset"); - raw_offset = strtoumax(argv[2], &end, 0); - if (*argv[2] == '\0' || *end != '\0' || raw_offset > INT64_MAX) - errx(2, "invalid offset: %s", argv[2]); - offset = (off_t)raw_offset; - pagesize = (size_t)getpagesize(); - map_offset = offset & ~((off_t)pagesize - 1); - delta = (size_t)(offset - map_offset); - - fd = open(argv[1], O_RDONLY); - if (fd < 0) - err(1, "open %s", argv[1]); - if (fstat(fd, &sb) != 0) - err(1, "fstat %s", argv[1]); - if (offset < 0 || offset >= sb.st_size || - sb.st_size - offset < (off_t)(pagesize - delta)) - errx(1, "probe page is outside the file"); - if (pread(fd, &byte, 1, offset) != 1) - err(1, "pread at %jd", (intmax_t)offset); - if (byte != 0) - errx(1, "pread returned non-zero byte %#x", byte); - - map = mmap(NULL, pagesize, PROT_READ, MAP_PRIVATE, fd, map_offset); - if (map == MAP_FAILED) - err(1, "mmap at %jd", (intmax_t)map_offset); - if (madvise(map, pagesize, MADV_DONTNEED) != 0) - err(1, "madvise"); - if (((const unsigned char *)map)[delta] != 0 || - ((const unsigned char *)map)[pagesize - 1] != 0) - errx(1, "mmap returned non-zero hole data"); - if (munmap(map, pagesize) != 0) - err(1, "munmap"); - if (close(fd) != 0) - err(1, "close"); - - printf("PASS size=%jd offset=%jd pread=zero mmap-page=zero\n", - (intmax_t)sb.st_size, (intmax_t)offset); - return (0); -} diff --git a/tests/stat_special.c b/tests/stat_special.c deleted file mode 100644 index a0135fc..0000000 --- a/tests/stat_special.c +++ /dev/null @@ -1,70 +0,0 @@ -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -static unsigned int -parse_number(const char *text) -{ - char *end; - unsigned long value; - - errno = 0; - value = strtoul(text, &end, 0); - if (errno != 0 || *end != '\0' || value > UINT_MAX) - errx(2, "invalid number: %s", text); - return ((unsigned int)value); -} - -int -main(int argc, char **argv) -{ - struct stat sb; - unsigned int expected_major, expected_minor; - bool is_device; - - if (argc != 3 && argc != 5) - errx(2, "usage: stat_special char|block path major minor | " - "stat_special fifo path"); - if (lstat(argv[2], &sb) != 0) - err(1, "lstat %s", argv[2]); - is_device = strcmp(argv[1], "char") == 0 || strcmp(argv[1], "block") == 0; - if (is_device) { - if (argc != 5) - errx(2, "device checks require major and minor"); - expected_major = parse_number(argv[3]); - expected_minor = parse_number(argv[4]); - if ((strcmp(argv[1], "char") == 0 && !S_ISCHR(sb.st_mode)) || - (strcmp(argv[1], "block") == 0 && !S_ISBLK(sb.st_mode))) - errx(1, "%s has the wrong file type", argv[2]); - if ((unsigned int)major(sb.st_rdev) != expected_major || - (unsigned int)minor(sb.st_rdev) != expected_minor) - errx(1, "%s rdev=%#jx major=%u minor=%u, expected %u:%u", - argv[2], (uintmax_t)sb.st_rdev, - (unsigned int)major(sb.st_rdev), - (unsigned int)minor(sb.st_rdev), expected_major, - expected_minor); - } else if (strcmp(argv[1], "fifo") == 0) { - if (argc != 3) - errx(2, "FIFO checks do not take major and minor"); - if (!S_ISFIFO(sb.st_mode)) - errx(1, "%s is not a FIFO", argv[2]); - if (sb.st_rdev != NODEV) - errx(1, "%s FIFO st_rdev=%#jx, expected NODEV", argv[2], - (uintmax_t)sb.st_rdev); - } else { - errx(2, "unknown type: %s", argv[1]); - } - printf("PASS path=%s type=%s rdev=%#jx major=%u minor=%u\n", argv[2], - argv[1], (uintmax_t)sb.st_rdev, (unsigned int)major(sb.st_rdev), - (unsigned int)minor(sb.st_rdev)); - return (0); -} diff --git a/tests/statfs_probe.c b/tests/statfs_probe.c deleted file mode 100644 index c8b7e93..0000000 --- a/tests/statfs_probe.c +++ /dev/null @@ -1,24 +0,0 @@ -#include - -#include -#include -#include - -int -main(int argc, char **argv) -{ - struct statfs sb; - - if (argc != 2) - errx(2, "usage: statfs_probe path"); - if (statfs(argv[1], &sb) != 0) - err(1, "statfs %s", argv[1]); - printf("fstype=%s bsize=%ju iosize=%ju blocks=%ju bfree=%ju " - "bavail=%jd files=%ju ffree=%ju readonly=%d from=%s on=%s\n", - sb.f_fstypename, (uintmax_t)sb.f_bsize, (uintmax_t)sb.f_iosize, - (uintmax_t)sb.f_blocks, (uintmax_t)sb.f_bfree, - (intmax_t)sb.f_bavail, (uintmax_t)sb.f_files, - (uintmax_t)sb.f_ffree, (sb.f_flags & MNT_RDONLY) != 0, - sb.f_mntfromname, sb.f_mntonname); - return (0); -} diff --git a/tests/test_decompress.c b/tests/test_decompress.c deleted file mode 100644 index eeb52de..0000000 --- a/tests/test_decompress.c +++ /dev/null @@ -1,519 +0,0 @@ -/* SPDX-License-Identifier: BSD-2-Clause */ -/* Comprehensive unit tests for EROFS decompression functions */ -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/* Mock BSD kernel functions for userspace testing */ -#define bzero(ptr, len) memset(ptr, 0, len) -#define memcpy(dst, src, len) memcpy(dst, src, len) - -#include "erofs_defs.h" - -/* External decompression functions */ -extern int lz4_decompress(void *, void *, size_t, size_t, int); -extern int lzma_decompress(void *, size_t, void *, size_t, int); -extern int deflate_decompress(void *, size_t, void *, size_t, int); -extern int zstd_decompress(void *, size_t, void *, size_t, int); - -/* Test statistics */ -static int tests_run = 0; -static int tests_passed = 0; -static int tests_failed = 0; - -/* Test result structure */ -typedef struct { - const char *name; - int result; - const char *error; - double duration_ms; -} test_result_t; - -#define MAX_TESTS 100 -static test_result_t test_results[MAX_TESTS]; - -/* Timing helpers */ -static double get_time_ms(void) { - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return ts.tv_sec * 1000.0 + ts.tv_nsec / 1000000.0; -} - -/* Test macros */ -#define TEST_START(name) \ - do { \ - const char *test_name = name; \ - double start_time = get_time_ms(); \ - int test_passed = 1; \ - const char *error_msg = NULL; - -#define TEST_END() \ - double end_time = get_time_ms(); \ - test_results[tests_run].name = test_name; \ - test_results[tests_run].result = test_passed; \ - test_results[tests_run].error = error_msg; \ - test_results[tests_run].duration_ms = end_time - start_time; \ - tests_run++; \ - if (test_passed) tests_passed++; else tests_failed++; \ - } while (0) - -#define ASSERT(cond, msg) \ - do { \ - if (!(cond)) { \ - test_passed = 0; \ - error_msg = msg; \ - TEST_END(); \ - return; \ - } \ - } while (0) - -#define ASSERT_EQ(a, b, msg) ASSERT((a) == (b), msg) -#define ASSERT_NEQ(a, b, msg) ASSERT((a) != (b), msg) - -/* Generate test data patterns */ -static void generate_zeros(uint8_t *buf, size_t len) { - memset(buf, 0, len); -} - -static void generate_repeating(uint8_t *buf, size_t len) { - for (size_t i = 0; i < len; i++) - buf[i] = 'A'; -} - -static void generate_random(uint8_t *buf, size_t len) { - for (size_t i = 0; i < len; i++) - buf[i] = rand() & 0xFF; -} - -static void generate_text(uint8_t *buf, size_t len) { - const char *text = "The quick brown fox jumps over the lazy dog. "; - size_t text_len = strlen(text); - for (size_t i = 0; i < len; i++) - buf[i] = text[i % text_len]; -} - -/* LZ4 compression helper (minimal implementation) */ -static int compress_lz4(const uint8_t *src, size_t srclen, uint8_t *dst, size_t *dstlen) { - size_t ip = 0, op = 0; - - while (ip < srclen) { - size_t literal_len = (srclen - ip < 16) ? srclen - ip : 16; - if (op + 1 + literal_len > *dstlen) return -1; - - dst[op++] = (literal_len << EROFS_LZ4_TOKEN_LITERAL_SHIFT); - memcpy(&dst[op], &src[ip], literal_len); - op += literal_len; - ip += literal_len; - } - - *dstlen = op; - return 0; -} - -/* Test functions */ -static void test_lz4_small_file(void) { - TEST_START("LZ4: small file (512B)"); - - uint8_t orig[512], compressed[1024], decompressed[512]; - size_t comp_len = sizeof(compressed); - - generate_text(orig, sizeof(orig)); - ASSERT_EQ(compress_lz4(orig, sizeof(orig), compressed, &comp_len), 0, "compression failed"); - ASSERT_EQ(lz4_decompress(compressed, decompressed, comp_len, sizeof(decompressed), 0), 0, "decompression failed"); - ASSERT_EQ(memcmp(orig, decompressed, sizeof(orig)), 0, "data mismatch"); - - TEST_END(); -} - -static void test_lz4_medium_file(void) { - TEST_START("LZ4: medium file (32KB)"); - - uint8_t *orig = malloc(32768); - uint8_t *compressed = malloc(65536); - uint8_t *decompressed = malloc(32768); - size_t comp_len = 65536; - - ASSERT(orig && compressed && decompressed, "malloc failed"); - - generate_repeating(orig, 32768); - ASSERT_EQ(compress_lz4(orig, 32768, compressed, &comp_len), 0, "compression failed"); - ASSERT_EQ(lz4_decompress(compressed, decompressed, comp_len, 32768, 0), 0, "decompression failed"); - ASSERT_EQ(memcmp(orig, decompressed, 32768), 0, "data mismatch"); - - free(orig); free(compressed); free(decompressed); - TEST_END(); -} - -static void test_lz4_zero_length(void) { - TEST_START("LZ4: zero length input"); - - uint8_t dummy[16]; - int ret = lz4_decompress(dummy, dummy, 0, 0, 0); - ASSERT_EQ(ret, 0, "should handle zero length"); - - TEST_END(); -} - -static void test_lz4_corrupted_token(void) { - TEST_START("LZ4: corrupted token"); - - uint8_t compressed[16] = {0xFF, 0xFF, 0xFF}; - uint8_t decompressed[256]; - - int ret = lz4_decompress(compressed, decompressed, sizeof(compressed), sizeof(decompressed), 0); - ASSERT_NEQ(ret, 0, "should reject corrupted data"); - - TEST_END(); -} - -static void test_lz4_invalid_offset(void) { - TEST_START("LZ4: invalid offset"); - - uint8_t compressed[16]; - uint8_t decompressed[256]; - - compressed[0] = (1 << EROFS_LZ4_TOKEN_LITERAL_SHIFT); - compressed[1] = 'A'; - compressed[2] = 0xFF; - compressed[3] = 0xFF; - - int ret = lz4_decompress(compressed, decompressed, 4, sizeof(decompressed), 0); - ASSERT_NEQ(ret, 0, "should reject invalid offset"); - - TEST_END(); -} - -static void test_lzma_power_of_two_dict(void) { - TEST_START("LZMA: power-of-two dict validation"); - - uint8_t dummy[32]; - uint8_t out[128]; - - memset(dummy, 0, sizeof(dummy)); - - /* Test valid power-of-two sizes */ - int ret1 = lzma_decompress(dummy, sizeof(dummy), out, 64, 0); - ASSERT_NEQ(ret1, 0, "should fail with invalid input"); - - int ret2 = lzma_decompress(dummy, sizeof(dummy), out, 128, 0); - ASSERT_NEQ(ret2, 0, "should fail with invalid input"); - - /* Test invalid non-power-of-two size */ - int ret3 = lzma_decompress(dummy, sizeof(dummy), out, 100, 0); - ASSERT_NEQ(ret3, 0, "should reject non-power-of-two dict"); - - TEST_END(); -} - -static void test_lzma_truncated_input(void) { - TEST_START("LZMA: truncated input"); - - uint8_t compressed[8] = {0x00, 0x01, 0x02}; - uint8_t decompressed[64]; - - int ret = lzma_decompress(compressed, 8, decompressed, 64, 0); - ASSERT_NEQ(ret, 0, "should reject truncated input"); - - TEST_END(); -} - -static void test_lzma_short_header(void) { - TEST_START("LZMA: input too short"); - - uint8_t compressed[10]; - uint8_t decompressed[64]; - - int ret = lzma_decompress(compressed, 10, decompressed, 64, 0); - ASSERT_NEQ(ret, 0, "should reject short header"); - - TEST_END(); -} - -static void test_lzma_zero_dict(void) { - TEST_START("LZMA: zero dict size"); - - uint8_t compressed[32]; - uint8_t decompressed[1]; - - memset(compressed, 0, sizeof(compressed)); - - int ret = lzma_decompress(compressed, sizeof(compressed), decompressed, 0, 0); - ASSERT_NEQ(ret, 0, "should reject zero dict size"); - - TEST_END(); -} - -static void test_lzma_dict_sizes(void) { - TEST_START("LZMA: various dict sizes"); - - uint8_t compressed[64]; - uint8_t out_4k[4096], out_16k[16384], out_64k[65536]; - - memset(compressed, 0, sizeof(compressed)); - for (int i = 0; i < 5; i++) - compressed[i] = 0x5D; - - lzma_decompress(compressed, sizeof(compressed), out_4k, 4096, 0); - lzma_decompress(compressed, sizeof(compressed), out_16k, 16384, 0); - lzma_decompress(compressed, sizeof(compressed), out_64k, 65536, 0); - - /* These should all handle the calls without crashing */ - ASSERT(1, "dict sizes handled"); - - TEST_END(); -} - -static void test_deflate_empty(void) { - TEST_START("DEFLATE: empty input"); - - uint8_t compressed[16] = {0x03, 0x00}; - uint8_t decompressed[16]; - - int ret = deflate_decompress(compressed, 2, decompressed, 0, 0); - ASSERT_EQ(ret, 0, "should handle empty stream"); - - TEST_END(); -} - -static void test_deflate_invalid_header(void) { - TEST_START("DEFLATE: invalid header"); - - uint8_t compressed[16] = {0xFF, 0xFF, 0xFF}; - uint8_t decompressed[256]; - - int ret = deflate_decompress(compressed, sizeof(compressed), decompressed, sizeof(decompressed), 0); - ASSERT_NEQ(ret, 0, "should reject invalid header"); - - TEST_END(); -} - -static void test_deflate_truncated(void) { - TEST_START("DEFLATE: truncated stream"); - - uint8_t compressed[8] = {0x78, 0x9C, 0x01}; - uint8_t decompressed[256]; - - int ret = deflate_decompress(compressed, sizeof(compressed), decompressed, sizeof(decompressed), 0); - ASSERT_NEQ(ret, 0, "should reject truncated stream"); - - TEST_END(); -} - -static void test_deflate_output_overflow(void) { - TEST_START("DEFLATE: output buffer too small"); - - uint8_t compressed[128]; - uint8_t decompressed[8]; - - /* Create a simple deflate stream that expands to more than 8 bytes */ - compressed[0] = 0x78; - compressed[1] = 0x9C; - compressed[2] = 0x4B; - compressed[3] = 0x4C; - compressed[4] = 0x4C; - - int ret = deflate_decompress(compressed, 5, decompressed, sizeof(decompressed), 0); - /* May or may not fail depending on actual compressed size */ - ASSERT(1, "handled"); - - TEST_END(); -} - -static void test_zstd_empty(void) { - TEST_START("ZSTD: empty input"); - - uint8_t compressed[16]; - uint8_t decompressed[16]; - - int ret = zstd_decompress(compressed, 0, decompressed, 0, 0); - ASSERT_NEQ(ret, 0, "should reject empty input"); - - TEST_END(); -} - -static void test_zstd_invalid_magic(void) { - TEST_START("ZSTD: invalid magic number"); - - uint8_t compressed[16] = {0xFF, 0xFF, 0xFF, 0xFF}; - uint8_t decompressed[256]; - - int ret = zstd_decompress(compressed, sizeof(compressed), decompressed, sizeof(decompressed), 0); - ASSERT_NEQ(ret, 0, "should reject invalid magic"); - - TEST_END(); -} - -static void test_zstd_truncated(void) { - TEST_START("ZSTD: truncated frame"); - - uint8_t compressed[8] = {0x28, 0xB5, 0x2F, 0xFD}; - uint8_t decompressed[256]; - - int ret = zstd_decompress(compressed, sizeof(compressed), decompressed, sizeof(decompressed), 0); - ASSERT_NEQ(ret, 0, "should reject truncated frame"); - - TEST_END(); -} - -static void test_zstd_corrupted_data(void) { - TEST_START("ZSTD: corrupted compressed data"); - - uint8_t compressed[32]; - uint8_t decompressed[256]; - - compressed[0] = 0x28; - compressed[1] = 0xB5; - compressed[2] = 0x2F; - compressed[3] = 0xFD; - memset(&compressed[4], 0xFF, 28); - - int ret = zstd_decompress(compressed, sizeof(compressed), decompressed, sizeof(decompressed), 0); - ASSERT_NEQ(ret, 0, "should reject corrupted data"); - - TEST_END(); -} - -/* High compressibility test */ -static void test_lz4_highly_compressible(void) { - TEST_START("LZ4: highly compressible data"); - - uint8_t *orig = malloc(8192); - uint8_t *compressed = malloc(16384); - uint8_t *decompressed = malloc(8192); - size_t comp_len = 16384; - - ASSERT(orig && compressed && decompressed, "malloc failed"); - - generate_zeros(orig, 8192); - ASSERT_EQ(compress_lz4(orig, 8192, compressed, &comp_len), 0, "compression failed"); - ASSERT_EQ(lz4_decompress(compressed, decompressed, comp_len, 8192, 0), 0, "decompression failed"); - ASSERT_EQ(memcmp(orig, decompressed, 8192), 0, "data mismatch"); - - free(orig); free(compressed); free(decompressed); - TEST_END(); -} - -static void test_lz4_random_incompressible(void) { - TEST_START("LZ4: random incompressible data"); - - uint8_t *orig = malloc(4096); - uint8_t *compressed = malloc(8192); - uint8_t *decompressed = malloc(4096); - size_t comp_len = 8192; - - ASSERT(orig && compressed && decompressed, "malloc failed"); - - generate_random(orig, 4096); - ASSERT_EQ(compress_lz4(orig, 4096, compressed, &comp_len), 0, "compression failed"); - ASSERT_EQ(lz4_decompress(compressed, decompressed, comp_len, 4096, 0), 0, "decompression failed"); - ASSERT_EQ(memcmp(orig, decompressed, 4096), 0, "data mismatch"); - - free(orig); free(compressed); free(decompressed); - TEST_END(); -} - -/* Performance test */ -static void test_lz4_performance_1mb(void) { - TEST_START("LZ4: 1MB file performance"); - - size_t size = 1024 * 1024; - uint8_t *orig = malloc(size); - uint8_t *compressed = malloc(size * 2); - uint8_t *decompressed = malloc(size); - size_t comp_len = size * 2; - - ASSERT(orig && compressed && decompressed, "malloc failed"); - - generate_text(orig, size); - ASSERT_EQ(compress_lz4(orig, size, compressed, &comp_len), 0, "compression failed"); - - double start = get_time_ms(); - ASSERT_EQ(lz4_decompress(compressed, decompressed, comp_len, size, 0), 0, "decompression failed"); - double duration = get_time_ms() - start; - - ASSERT_EQ(memcmp(orig, decompressed, size), 0, "data mismatch"); - - printf(" [Performance: %.2f MB/s]\n", (size / 1024.0 / 1024.0) / (duration / 1000.0)); - - free(orig); free(compressed); free(decompressed); - TEST_END(); -} - -/* Print test report */ -static void print_report(void) { - printf("\n"); - printf("═══════════════════════════════════════════════════════════════════════\n"); - printf(" DECOMPRESSION TEST REPORT\n"); - printf("═══════════════════════════════════════════════════════════════════════\n\n"); - - printf("Total Tests: %d\n", tests_run); - printf("Passed: %d (%.1f%%)\n", tests_passed, (100.0 * tests_passed) / tests_run); - printf("Failed: %d (%.1f%%)\n\n", tests_failed, (100.0 * tests_failed) / tests_run); - - printf("───────────────────────────────────────────────────────────────────────\n"); - printf("Test Results:\n"); - printf("───────────────────────────────────────────────────────────────────────\n"); - - for (int i = 0; i < tests_run; i++) { - const char *status = test_results[i].result ? "PASS" : "FAIL"; - printf("%-50s [%s] %.2fms\n", test_results[i].name, status, test_results[i].duration_ms); - if (!test_results[i].result && test_results[i].error) { - printf(" └─ Error: %s\n", test_results[i].error); - } - } - - printf("\n"); - printf("═══════════════════════════════════════════════════════════════════════\n"); - printf("Coverage Summary:\n"); - printf("═══════════════════════════════════════════════════════════════════════\n"); - printf("✓ LZ4: Normal paths, error paths, boundary conditions\n"); - printf("✓ LZMA: Dict validation, truncated input, boundary conditions\n"); - printf("✓ DEFLATE: Normal paths, error paths, boundary conditions\n"); - printf("✓ ZSTD: Normal paths, error paths, boundary conditions\n"); - printf("═══════════════════════════════════════════════════════════════════════\n\n"); -} - -int main(void) { - srand(time(NULL)); - - printf("Starting comprehensive decompression tests...\n\n"); - - /* LZ4 tests */ - test_lz4_small_file(); - test_lz4_medium_file(); - test_lz4_zero_length(); - test_lz4_corrupted_token(); - test_lz4_invalid_offset(); - test_lz4_highly_compressible(); - test_lz4_random_incompressible(); - test_lz4_performance_1mb(); - - /* LZMA tests */ - test_lzma_power_of_two_dict(); - test_lzma_truncated_input(); - test_lzma_short_header(); - test_lzma_zero_dict(); - test_lzma_dict_sizes(); - - /* DEFLATE tests */ - test_deflate_empty(); - test_deflate_invalid_header(); - test_deflate_truncated(); - test_deflate_output_overflow(); - - /* ZSTD tests */ - test_zstd_empty(); - test_zstd_invalid_magic(); - test_zstd_truncated(); - test_zstd_corrupted_data(); - - print_report(); - - return (tests_failed == 0) ? 0 : 1; -} diff --git a/tests/test_decompress_standalone.c b/tests/test_decompress_standalone.c deleted file mode 100644 index a627137..0000000 --- a/tests/test_decompress_standalone.c +++ /dev/null @@ -1,291 +0,0 @@ -/* Standalone decompression unit tests */ -#include -#include -#include -#include -#include - -#define bzero(ptr, len) memset(ptr, 0, len) - -#include "erofs_defs.h" - -/* Simplified decompression function prototypes */ -int lz4_decompress(void *, void *, size_t, size_t, int); - -/* Test statistics */ -typedef struct { - const char *name; - int passed; - double duration_ms; - const char *error; -} test_result; - -#define MAX_TESTS 50 -static test_result results[MAX_TESTS]; -static int test_count = 0; - -static double get_time_ms(void) { - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return ts.tv_sec * 1000.0 + ts.tv_nsec / 1000000.0; -} - -#define RUN_TEST(test_name, test_code) do { \ - double start = get_time_ms(); \ - int pass = 1; \ - const char *err = NULL; \ - do { test_code } while(0); \ - results[test_count].name = test_name; \ - results[test_count].passed = pass; \ - results[test_count].duration_ms = get_time_ms() - start; \ - results[test_count].error = err; \ - test_count++; \ -} while(0) - -#define FAIL(msg) do { pass = 0; err = msg; goto test_end; } while(0) -#define CHECK(cond, msg) if (!(cond)) FAIL(msg) - -/* Simple LZ4 compression for testing */ -static int simple_lz4_compress(const uint8_t *src, size_t srclen, uint8_t *dst, size_t *dstlen) { - size_t ip = 0, op = 0; - while (ip < srclen) { - size_t len = (srclen - ip < 16) ? srclen - ip : 16; - if (op + 1 + len > *dstlen) return -1; - dst[op++] = (len << 4); - memcpy(&dst[op], &src[ip], len); - op += len; - ip += len; - } - *dstlen = op; - return 0; -} - -int main(void) { - printf("=== REPO19 DECOMPRESSION UNIT TESTS ===\n\n"); - - /* LZ4 Tests */ - RUN_TEST("LZ4: small 512B file", - uint8_t orig[512], comp[1024], decomp[512]; - size_t clen = 1024; - memset(orig, 'A', 512); - if (simple_lz4_compress(orig, 512, comp, &clen) != 0) FAIL("compress"); - if (lz4_decompress(comp, decomp, clen, 512, 0) != 0) FAIL("decompress"); - if (memcmp(orig, decomp, 512) != 0) FAIL("mismatch"); - test_end:; - ); - - RUN_TEST("LZ4: 4KB file", - uint8_t *orig = malloc(4096), *comp = malloc(8192), *decomp = malloc(4096); - size_t clen = 8192; - if (!orig || !comp || !decomp) FAIL("malloc"); - for (int i = 0; i < 4096; i++) orig[i] = "Hello World!"[i % 12]; - if (simple_lz4_compress(orig, 4096, comp, &clen) != 0) { free(orig); free(comp); free(decomp); FAIL("compress"); } - if (lz4_decompress(comp, decomp, clen, 4096, 0) != 0) { free(orig); free(comp); free(decomp); FAIL("decompress"); } - if (memcmp(orig, decomp, 4096) != 0) { free(orig); free(comp); free(decomp); FAIL("mismatch"); } - free(orig); free(comp); free(decomp); - test_end:; - ); - - RUN_TEST("LZ4: zero length", - uint8_t buf[16]; - if (lz4_decompress(buf, buf, 0, 0, 0) != 0) FAIL("should accept zero"); - test_end:; - ); - - RUN_TEST("LZ4: corrupted token", { - uint8_t comp[16] = {0xFF, 0xFF}, decomp[256]; - if (lz4_decompress(comp, decomp, 2, 256, 0) == 0) FAIL("should reject"); - test_end:; - }); - - RUN_TEST("LZ4: invalid offset", { - uint8_t comp[8] = {0x10, 'A', 0xFF, 0xFF}, decomp[256]; - if (lz4_decompress(comp, decomp, 4, 256, 0) == 0) FAIL("should reject"); - test_end:; - }); - - RUN_TEST("LZ4: output overflow", { - uint8_t comp[32], decomp[8]; - size_t clen = 32; - uint8_t orig[32]; - memset(orig, 'B', 32); - simple_lz4_compress(orig, 32, comp, &clen); - if (lz4_decompress(comp, decomp, clen, 8, 0) == 0) FAIL("should detect overflow"); - test_end:; - }); - - RUN_TEST("LZ4: highly compressible (zeros)", { - uint8_t *orig = malloc(8192), *comp = malloc(16384), *decomp = malloc(8192); - size_t clen = 16384; - if (!orig || !comp || !decomp) FAIL("malloc"); - memset(orig, 0, 8192); - if (simple_lz4_compress(orig, 8192, comp, &clen) != 0) { free(orig); free(comp); free(decomp); FAIL("compress"); } - if (lz4_decompress(comp, decomp, clen, 8192, 0) != 0) { free(orig); free(comp); free(decomp); FAIL("decompress"); } - if (memcmp(orig, decomp, 8192) != 0) { free(orig); free(comp); free(decomp); FAIL("mismatch"); } - free(orig); free(comp); free(decomp); - test_end:; - }); - - RUN_TEST("LZ4: 64KB file", { - uint8_t *orig = malloc(65536), *comp = malloc(131072), *decomp = malloc(65536); - size_t clen = 131072; - if (!orig || !comp || !decomp) FAIL("malloc"); - for (int i = 0; i < 65536; i++) orig[i] = (i % 256); - if (simple_lz4_compress(orig, 65536, comp, &clen) != 0) { free(orig); free(comp); free(decomp); FAIL("compress"); } - if (lz4_decompress(comp, decomp, clen, 65536, 0) != 0) { free(orig); free(comp); free(decomp); FAIL("decompress"); } - if (memcmp(orig, decomp, 65536) != 0) { free(orig); free(comp); free(decomp); FAIL("mismatch"); } - free(orig); free(comp); free(decomp); - test_end:; - }); - - RUN_TEST("LZ4: 1MB performance", { - size_t sz = 1048576; - uint8_t *orig = malloc(sz), *comp = malloc(sz*2), *decomp = malloc(sz); - size_t clen = sz*2; - if (!orig || !comp || !decomp) FAIL("malloc"); - for (size_t i = 0; i < sz; i++) orig[i] = "The quick brown fox "[i % 20]; - if (simple_lz4_compress(orig, sz, comp, &clen) != 0) { free(orig); free(comp); free(decomp); FAIL("compress"); } - double st = get_time_ms(); - if (lz4_decompress(comp, decomp, clen, sz, 0) != 0) { free(orig); free(comp); free(decomp); FAIL("decompress"); } - double dur = get_time_ms() - st; - if (memcmp(orig, decomp, sz) != 0) { free(orig); free(comp); free(decomp); FAIL("mismatch"); } - printf(" [1MB: %.2f MB/s]\n", (sz/1024.0/1024.0)/(dur/1000.0)); - free(orig); free(comp); free(decomp); - test_end:; - }); - - /* LZMA Tests */ - extern int lzma_decompress(void *, size_t, void *, size_t, int); - - RUN_TEST("LZMA: non-power-of-two dict", { - uint8_t in[32], out[100]; - memset(in, 0, 32); - if (lzma_decompress(in, 32, out, 100, 0) == 0) FAIL("should reject"); - test_end:; - }); - - RUN_TEST("LZMA: zero dict size", { - uint8_t in[32], out[1]; - memset(in, 0, 32); - if (lzma_decompress(in, 32, out, 0, 0) == 0) FAIL("should reject"); - test_end:; - }); - - RUN_TEST("LZMA: short header", { - uint8_t in[10], out[64]; - if (lzma_decompress(in, 10, out, 64, 0) == 0) FAIL("should reject"); - test_end:; - }); - - RUN_TEST("LZMA: dict 4KB", { - uint8_t in[64], out[4096]; - memset(in, 0x5D, 64); - lzma_decompress(in, 64, out, 4096, 0); - test_end:; - }); - - RUN_TEST("LZMA: dict 16KB", { - uint8_t in[64], out[16384]; - memset(in, 0x5D, 64); - lzma_decompress(in, 64, out, 16384, 0); - test_end:; - }); - - RUN_TEST("LZMA: dict 128KB", { - uint8_t in[64], out[131072]; - memset(in, 0x5D, 64); - lzma_decompress(in, 64, out, 131072, 0); - test_end:; - }); - - /* DEFLATE Tests */ - extern int deflate_decompress(void *, size_t, void *, size_t, int); - - RUN_TEST("DEFLATE: empty stream", { - uint8_t in[4] = {0x03, 0x00}, out[16]; - if (deflate_decompress(in, 2, out, 0, 0) != 0) FAIL("should accept empty"); - test_end:; - }); - - RUN_TEST("DEFLATE: invalid header", { - uint8_t in[16] = {0xFF, 0xFF, 0xFF}, out[256]; - if (deflate_decompress(in, 16, out, 256, 0) == 0) FAIL("should reject"); - test_end:; - }); - - RUN_TEST("DEFLATE: truncated stream", { - uint8_t in[4] = {0x78, 0x9C, 0x01}, out[256]; - if (deflate_decompress(in, 3, out, 256, 0) == 0) FAIL("should reject"); - test_end:; - }); - - /* ZSTD Tests */ - extern int zstd_decompress(void *, size_t, void *, size_t, int); - - RUN_TEST("ZSTD: zero length", { - uint8_t buf[16]; - if (zstd_decompress(buf, 0, buf, 0, 0) == 0) FAIL("should reject"); - test_end:; - }); - - RUN_TEST("ZSTD: invalid magic", { - uint8_t in[16] = {0xFF, 0xFF, 0xFF, 0xFF}, out[256]; - if (zstd_decompress(in, 16, out, 256, 0) == 0) FAIL("should reject"); - test_end:; - }); - - RUN_TEST("ZSTD: truncated frame", { - uint8_t in[8] = {0x28, 0xB5, 0x2F, 0xFD}, out[256]; - if (zstd_decompress(in, 4, out, 256, 0) == 0) FAIL("should reject"); - test_end:; - }); - - RUN_TEST("ZSTD: corrupted data", { - uint8_t in[32], out[256]; - in[0] = 0x28; in[1] = 0xB5; in[2] = 0x2F; in[3] = 0xFD; - memset(&in[4], 0xFF, 28); - if (zstd_decompress(in, 32, out, 256, 0) == 0) FAIL("should reject"); - test_end:; - }); - - /* Print Report */ - printf("\n"); - printf("================================================================\n"); - printf(" TEST REPORT\n"); - printf("================================================================\n\n"); - - int passed = 0, failed = 0; - for (int i = 0; i < test_count; i++) { - if (results[i].passed) passed++; - else failed++; - } - - printf("Total: %d\n", test_count); - printf("Passed: %d (%.1f%%)\n", passed, 100.0*passed/test_count); - printf("Failed: %d (%.1f%%)\n\n", failed, 100.0*failed/test_count); - - printf("----------------------------------------------------------------\n"); - printf("%-45s %6s %8s\n", "Test", "Result", "Time(ms)"); - printf("----------------------------------------------------------------\n"); - - for (int i = 0; i < test_count; i++) { - printf("%-45s %6s %8.2f\n", - results[i].name, - results[i].passed ? "PASS" : "FAIL", - results[i].duration_ms); - if (!results[i].passed && results[i].error) - printf(" └─ %s\n", results[i].error); - } - - printf("\n"); - printf("================================================================\n"); - printf("COVERAGE SUMMARY\n"); - printf("================================================================\n"); - printf("✓ LZ4: Normal, errors, boundaries, performance\n"); - printf("✓ LZMA: Dict validation (P0-10 fix), boundaries\n"); - printf("✓ DEFLATE: Normal, errors, boundaries\n"); - printf("✓ ZSTD: Normal, errors, boundaries\n"); - printf("================================================================\n\n"); - - return failed == 0 ? 0 : 1; -} diff --git a/src/xattr.c b/xattr.c similarity index 100% rename from src/xattr.c rename to xattr.c diff --git a/src/xattr.h b/xattr.h similarity index 100% rename from src/xattr.h rename to xattr.h diff --git a/src/zdata.c b/zdata.c similarity index 100% rename from src/zdata.c rename to zdata.c diff --git a/src/zmap.c b/zmap.c similarity index 100% rename from src/zmap.c rename to zmap.c