test code v1
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
|||||||
|
# Kernel-module build output.
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Generated manual-test fixtures and captures.
|
||||||
|
/tests/results/manual/*/artifacts/
|
||||||
|
/tests/results/manual/*/fixture/
|
||||||
|
/tests/results/manual/*/repro-artifacts/
|
||||||
|
/tests/results/manual/*/repro-fixture/
|
||||||
|
/tests/results/manual/*/repro[0-9]-artifacts/
|
||||||
|
/tests/results/manual/*/repro[0-9]-fixture/
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# 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.<slot>=<provider>`:
|
||||||
|
|
||||||
|
```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.<slot>` 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.
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#!/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}"
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# 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/`。
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
# 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 全量结论。
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# repo22 当前状态索引
|
||||||
|
|
||||||
|
- [2026-08-09 总体进度](2026-08-09-overall-progress.md):基线
|
||||||
|
`cd0e985b5ac54a4b7acb7042422329ad1729fb3e` 上的实现、历史动态证据、八组
|
||||||
|
全量回归分工、未决 issue、风险和下一步。
|
||||||
|
|
||||||
|
此目录记录阶段性状态;测试结果会随后续 commit 更新。正式逐项证据位于
|
||||||
|
`tests/results/manual/`,未决验证缺口位于 `issues/`。
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# 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。
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# repo22 第二次状态报告索引
|
||||||
|
|
||||||
|
- [第二次总体进度报告](2026-08-09-overall-progress.md):以
|
||||||
|
`381349b3847209a67d669264ac54a50a3599915c` 为统计快照,汇总实现与审查、
|
||||||
|
八组回归、动态结果计数、issue、风险和后续工作。
|
||||||
|
|
||||||
|
本目录只记录阶段性总体状态。各 TC 的命令、hash、errno、dmesg 和清理证据以
|
||||||
|
`tests/results/manual/` 中的对应报告为准。
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
# 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`。
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# repo22 第三次状态报告索引
|
||||||
|
|
||||||
|
- [第三次总体进度报告](2026-08-09-overall-progress.md):以
|
||||||
|
`12351cbb0a593474f4fcef68ffc5218d9c99dd46` 为最终验证快照,汇总已完成
|
||||||
|
feature 验证、code/style review、最终构建、剩余风险和后续计划。
|
||||||
|
|
||||||
|
本目录只记录本轮收尾状态。逐项命令、hash、errno 和清理证据以
|
||||||
|
`tests/results/manual/` 为准,issue 的触发、分析、尝试和验收条件以
|
||||||
|
`issues/` 为准。report-1 和 report-2 保留为历史快照,不因本报告改写。
|
||||||
@@ -0,0 +1,726 @@
|
|||||||
|
# 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 环境中复现、修复和回归。
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# 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、来源和许可证审计作更精确的限定。
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
# 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_<module>_<action>`
|
||||||
|
- 静态函数:描述性名称,无固定前缀
|
||||||
|
- 宏:`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 控制面
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# 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.
|
||||||
+271
@@ -0,0 +1,271 @@
|
|||||||
|
.\" 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
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# 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`.
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# 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).
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
# 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`
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# 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`.
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
# 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`.
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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.
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"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}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# 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 一致。
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# 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;测试后无遗留挂载或模块。
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# 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`.
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
# 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: '^<sys/cdefs\.h>'
|
||||||
|
Priority: 2
|
||||||
|
SortPriority: 20
|
||||||
|
- Regex: '^<sys/types\.h>'
|
||||||
|
Priority: 2
|
||||||
|
SortPriority: 21
|
||||||
|
- Regex: '^<sys/param\.h>'
|
||||||
|
Priority: 2
|
||||||
|
SortPriority: 22
|
||||||
|
- Regex: '^<sys/systm\.h>'
|
||||||
|
Priority: 2
|
||||||
|
SortPriority: 23
|
||||||
|
- Regex: '^<sys.*/'
|
||||||
|
Priority: 2
|
||||||
|
SortPriority: 24
|
||||||
|
- Regex: '^<vm/vm\.h>'
|
||||||
|
Priority: 3
|
||||||
|
SortPriority: 30
|
||||||
|
- Regex: '^<vm/'
|
||||||
|
Priority: 3
|
||||||
|
SortPriority: 31
|
||||||
|
- Regex: '^<machine/'
|
||||||
|
Priority: 4
|
||||||
|
SortPriority: 40
|
||||||
|
- Regex: '^<(x86|amd64|i386|xen)/'
|
||||||
|
Priority: 5
|
||||||
|
SortPriority: 50
|
||||||
|
- Regex: '^<dev/'
|
||||||
|
Priority: 6
|
||||||
|
SortPriority: 60
|
||||||
|
- Regex: '^<net.*/'
|
||||||
|
Priority: 7
|
||||||
|
SortPriority: 70
|
||||||
|
- Regex: '^<protocols/'
|
||||||
|
Priority: 7
|
||||||
|
SortPriority: 71
|
||||||
|
- Regex: '^<(fs|nfs(|client|server)|ufs)/'
|
||||||
|
Priority: 8
|
||||||
|
SortPriority: 80
|
||||||
|
- Regex: '^<[^/].*\.h'
|
||||||
|
Priority: 9
|
||||||
|
SortPriority: 90
|
||||||
|
- Regex: '^\".*\.h\"'
|
||||||
|
Priority: 10
|
||||||
|
SortPriority: 100
|
||||||
|
# LLVM's header include ordering style is almost the exact opposite of ours.
|
||||||
|
# Unfortunately, they have hard-coded their preferences into clang-format.
|
||||||
|
# Clobbering this regular expression to avoid matching prevents non-system
|
||||||
|
# headers from being forcibly moved to the top of the include list.
|
||||||
|
# http://llvm.org/docs/CodingStandards.html#include-style
|
||||||
|
IncludeIsMainRegex: 'BLAH_DONT_MATCH_ANYTHING'
|
||||||
|
SortIncludes: true
|
||||||
|
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||||
|
TypenameMacros:
|
||||||
|
- ARB_ELMTYPE
|
||||||
|
- ARB_HEAD
|
||||||
|
- ARB8_HEAD
|
||||||
|
- ARB16_HEAD
|
||||||
|
- ARB32_HEAD
|
||||||
|
- ARB_ENTRY
|
||||||
|
- ARB8_ENTRY
|
||||||
|
- ARB16_ENTRY
|
||||||
|
- ARB32_ENTRY
|
||||||
|
- LIST_CLASS_ENTRY
|
||||||
|
- LIST_CLASS_HEAD
|
||||||
|
- LIST_ENTRY
|
||||||
|
- LIST_HEAD
|
||||||
|
- QUEUE_TYPEOF
|
||||||
|
- RB_ENTRY
|
||||||
|
- RB_HEAD
|
||||||
|
- SLIST_CLASS_HEAD
|
||||||
|
- SLIST_CLASS_ENTRY
|
||||||
|
- SLIST_HEAD
|
||||||
|
- SLIST_ENTRY
|
||||||
|
- SMR_POINTER
|
||||||
|
- SPLAY_ENTRY
|
||||||
|
- SPLAY_HEAD
|
||||||
|
- STAILQ_CLASS_ENTRY
|
||||||
|
- STAILQ_CLASS_HEAD
|
||||||
|
- STAILQ_ENTRY
|
||||||
|
- STAILQ_HEAD
|
||||||
|
- TAILQ_CLASS_ENTRY
|
||||||
|
- TAILQ_CLASS_HEAD
|
||||||
|
- TAILQ_ENTRY
|
||||||
|
- TAILQ_HEAD
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
i386
|
||||||
|
machine
|
||||||
|
x86
|
||||||
|
.cache
|
||||||
|
|
||||||
|
export_syms
|
||||||
|
|
||||||
|
*.o
|
||||||
|
*.ko
|
||||||
|
|
||||||
|
opt_global.h
|
||||||
|
|
||||||
|
vnode_if.h
|
||||||
|
vnode_if_newproto.h
|
||||||
|
vnode_if_typedef.h
|
||||||
|
|
||||||
|
compile_commands.json
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
KMOD= erofs
|
||||||
|
|
||||||
|
.if ${MACHINE_ARCH} != "amd64"
|
||||||
|
.error erofs supports only MACHINE_ARCH=amd64
|
||||||
|
.endif
|
||||||
|
|
||||||
|
WITH_ZSTDIO?= 0
|
||||||
|
.if empty(WITH_ZSTDIO:M0) && empty(WITH_ZSTDIO:M1)
|
||||||
|
.error WITH_ZSTDIO must be 0 or 1
|
||||||
|
.endif
|
||||||
|
|
||||||
|
SRCS= super.c \
|
||||||
|
inode.c \
|
||||||
|
data.c \
|
||||||
|
namei.c \
|
||||||
|
dir.c \
|
||||||
|
xattr.c \
|
||||||
|
erofs_vnops.c \
|
||||||
|
decompressor.c \
|
||||||
|
zmap.c \
|
||||||
|
zdata.c \
|
||||||
|
lz4.c \
|
||||||
|
decompressor_lzma.c \
|
||||||
|
decompressor_deflate.c \
|
||||||
|
decompressor_zstd.c \
|
||||||
|
vnode_if.h
|
||||||
|
|
||||||
|
CFLAGS.decompressor_zstd.c+= -I${SYSDIR}/contrib/zstd/lib/freebsd
|
||||||
|
.if ${WITH_ZSTDIO} == 1
|
||||||
|
CFLAGS.decompressor_zstd.c+= -DZSTDIO
|
||||||
|
.endif
|
||||||
|
|
||||||
|
.include <bsd.kmod.mk>
|
||||||
+542
@@ -0,0 +1,542 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017-2018 HUAWEI, Inc.
|
||||||
|
* https://www.huawei.com/
|
||||||
|
* Copyright (C) 2021, Alibaba Cloud
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <sys/param.h>
|
||||||
|
#include <sys/systm.h>
|
||||||
|
#include <sys/bio.h>
|
||||||
|
#include <sys/buf.h>
|
||||||
|
#include <sys/kernel.h>
|
||||||
|
#include <sys/libkern.h>
|
||||||
|
#include <sys/malloc.h>
|
||||||
|
#include <sys/mount.h>
|
||||||
|
#include <sys/vnode.h>
|
||||||
|
#include "internal.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* For flat inline files, compute the "inline tail" start offset.
|
||||||
|
* Linux EROFS semantics: the last logical block may be tailpacked
|
||||||
|
* into the inode metadata area.
|
||||||
|
*/
|
||||||
|
static uint64_t
|
||||||
|
erofs_inline_tail_start(const struct erofs_mount *em,
|
||||||
|
const struct erofs_node *en)
|
||||||
|
{
|
||||||
|
if (en->size == 0)
|
||||||
|
return (0);
|
||||||
|
return (roundup2(en->size, (uint64_t)em->block_size) - em->block_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_check_device_range(const struct erofs_mount *em,
|
||||||
|
const struct erofs_device_info *dif, uint64_t off, uint64_t len)
|
||||||
|
{
|
||||||
|
uint64_t end, limit;
|
||||||
|
|
||||||
|
if (dif->blocks > (UINT64_MAX >> em->block_bits) ||
|
||||||
|
__builtin_add_overflow(off, len, &end))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
limit = dif->blocks << em->block_bits;
|
||||||
|
return (end > limit ? EINTEGRITY : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
erofs_map_dev(struct erofs_mount *em, struct erofs_map_dev *map)
|
||||||
|
{
|
||||||
|
struct erofs_device_info *dif;
|
||||||
|
uint64_t start;
|
||||||
|
unsigned int id;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
map->m_em = em;
|
||||||
|
map->m_dif = &em->dif0;
|
||||||
|
if (map->m_deviceid != 0) {
|
||||||
|
if (map->m_deviceid > em->extra_devices || em->devs == NULL)
|
||||||
|
return (ENODEV);
|
||||||
|
dif = &em->devs[map->m_deviceid - 1];
|
||||||
|
error = erofs_check_device_range(em, dif, map->m_pa,
|
||||||
|
map->m_plen);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (em->flatdev) {
|
||||||
|
if (dif->uniaddr > (UINT64_MAX >> em->block_bits))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
start = dif->uniaddr << em->block_bits;
|
||||||
|
if (__builtin_add_overflow(map->m_pa, start, &map->m_pa))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
if (dif->devvp == NULL || dif->cp == NULL)
|
||||||
|
return (ENODEV);
|
||||||
|
map->m_dif = dif;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (em->extra_devices == 0)
|
||||||
|
return (0);
|
||||||
|
if (em->flatdev) {
|
||||||
|
error = erofs_check_device_range(em, &em->dif0, map->m_pa,
|
||||||
|
map->m_plen);
|
||||||
|
if (error == 0)
|
||||||
|
return (0);
|
||||||
|
for (id = 0; id < em->extra_devices; ++id) {
|
||||||
|
dif = &em->devs[id];
|
||||||
|
if (dif->uniaddr == 0 ||
|
||||||
|
dif->uniaddr > (UINT64_MAX >> em->block_bits))
|
||||||
|
continue;
|
||||||
|
start = dif->uniaddr << em->block_bits;
|
||||||
|
if (map->m_pa < start)
|
||||||
|
continue;
|
||||||
|
error = erofs_check_device_range(em, dif,
|
||||||
|
map->m_pa - start, map->m_plen);
|
||||||
|
if (error == 0)
|
||||||
|
return (0);
|
||||||
|
if (map->m_pa - start <
|
||||||
|
(dif->blocks << em->block_bits))
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
for (id = 0; id < em->extra_devices; ++id) {
|
||||||
|
dif = &em->devs[id];
|
||||||
|
if (dif->uniaddr == 0)
|
||||||
|
continue;
|
||||||
|
if (dif->uniaddr > (UINT64_MAX >> em->block_bits))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
start = dif->uniaddr << em->block_bits;
|
||||||
|
if (map->m_pa >= start &&
|
||||||
|
map->m_pa - start < (dif->blocks << em->block_bits)) {
|
||||||
|
error = erofs_check_device_range(em, dif,
|
||||||
|
map->m_pa - start, map->m_plen);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (dif->devvp == NULL || dif->cp == NULL)
|
||||||
|
return (ENODEV);
|
||||||
|
map->m_pa -= start;
|
||||||
|
map->m_dif = dif;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Map chunk-based file to physical extent */
|
||||||
|
static int
|
||||||
|
erofs_map_blocks_chunk(struct erofs_mount *em, struct erofs_node *en,
|
||||||
|
uint64_t loff, uint64_t *phys_off, unsigned int *device_id,
|
||||||
|
size_t *run_len, bool *hole)
|
||||||
|
{
|
||||||
|
struct erofs_inode_chunk_index *idx;
|
||||||
|
void *buf;
|
||||||
|
uint64_t chunk_idx, idx_off, chunk_size, entry_size, chunk_off;
|
||||||
|
uint64_t idx_base, image_size, addrmask;
|
||||||
|
uint64_t blkaddr;
|
||||||
|
uint16_t raw_device_id;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
chunk_size = 1ULL << en->chunkbits;
|
||||||
|
chunk_idx = loff >> en->chunkbits;
|
||||||
|
chunk_off = loff & (chunk_size - 1);
|
||||||
|
|
||||||
|
if ((en->chunkformat & EROFS_CHUNK_FORMAT_INDEXES) != 0)
|
||||||
|
entry_size = sizeof(struct erofs_inode_chunk_index);
|
||||||
|
else
|
||||||
|
entry_size = EROFS_BLOCK_MAP_ENTRY_SIZE;
|
||||||
|
if (en->inode_off > UINT64_MAX - en->inode_isize ||
|
||||||
|
en->inode_off + en->inode_isize > UINT64_MAX - en->xattr_isize)
|
||||||
|
return (EOVERFLOW);
|
||||||
|
idx_base = en->inode_off + en->inode_isize + en->xattr_isize;
|
||||||
|
if (idx_base > UINT64_MAX - (entry_size - 1))
|
||||||
|
return (EOVERFLOW);
|
||||||
|
idx_base = roundup2(idx_base, entry_size);
|
||||||
|
if (chunk_idx > (UINT64_MAX - idx_base) / entry_size)
|
||||||
|
return (EOVERFLOW);
|
||||||
|
idx_off = idx_base + chunk_idx * entry_size;
|
||||||
|
if (erofs_nid_in_metabox(en->nid)) {
|
||||||
|
if (em->metabox_en == NULL || idx_off > em->metabox_en->size ||
|
||||||
|
entry_size > em->metabox_en->size - idx_off)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
} else {
|
||||||
|
if (em->blocks > (UINT64_MAX >> em->block_bits))
|
||||||
|
return (EOVERFLOW);
|
||||||
|
image_size = em->blocks << em->block_bits;
|
||||||
|
if (idx_off > image_size || entry_size > image_size - idx_off)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
error = erofs_read_metadata(em, en->nid, idx_off, entry_size, &buf);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
|
||||||
|
idx = buf;
|
||||||
|
if ((en->chunkformat & EROFS_CHUNK_FORMAT_INDEXES) != 0) {
|
||||||
|
blkaddr = le32toh(idx->startblk_lo);
|
||||||
|
if ((en->chunkformat & EROFS_CHUNK_FORMAT_48BIT) != 0)
|
||||||
|
blkaddr |= (uint64_t)le16toh(idx->startblk_hi) << 32;
|
||||||
|
raw_device_id = le16toh(idx->device_id);
|
||||||
|
addrmask = (en->chunkformat & EROFS_CHUNK_FORMAT_48BIT) != 0 ?
|
||||||
|
((1ULL << 48) - 1) : UINT32_MAX;
|
||||||
|
} else {
|
||||||
|
blkaddr = le32toh(*(__le32 *)idx);
|
||||||
|
raw_device_id = 0;
|
||||||
|
addrmask = UINT32_MAX;
|
||||||
|
}
|
||||||
|
erofs_brelse(buf);
|
||||||
|
|
||||||
|
if (!((blkaddr ^ EROFS_NULL_ADDR) & addrmask)) {
|
||||||
|
*hole = true;
|
||||||
|
*phys_off = 0;
|
||||||
|
*run_len = MIN(chunk_size - chunk_off, en->size - loff);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
*run_len = MIN(chunk_size - chunk_off, en->size - loff);
|
||||||
|
*device_id = raw_device_id & em->device_id_mask;
|
||||||
|
if (blkaddr > (UINT64_MAX >> em->block_bits))
|
||||||
|
return (EOVERFLOW);
|
||||||
|
*phys_off = blkaddr << em->block_bits;
|
||||||
|
if (chunk_off > UINT64_MAX - *phys_off)
|
||||||
|
return (EOVERFLOW);
|
||||||
|
*phys_off += chunk_off;
|
||||||
|
*hole = false;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_bread_device(struct erofs_mount *em, struct erofs_device_info *dif,
|
||||||
|
erofs_blk_t blocks, uint64_t off, size_t len, void **bufp)
|
||||||
|
{
|
||||||
|
struct buf *bp;
|
||||||
|
uint64_t end, limit;
|
||||||
|
off_t blkoff, current;
|
||||||
|
size_t blklen, done, iosize;
|
||||||
|
char *out;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
if (bufp == NULL)
|
||||||
|
return (EINVAL);
|
||||||
|
*bufp = NULL;
|
||||||
|
if (len == 0) {
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
if (dif == NULL || dif->devvp == NULL || dif->cp == NULL)
|
||||||
|
return (ENODEV);
|
||||||
|
if (__builtin_add_overflow(off, (uint64_t)len, &end))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
if (blocks != 0) {
|
||||||
|
if (blocks > (UINT64_MAX >> em->block_bits))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
limit = blocks << em->block_bits;
|
||||||
|
if (end > limit)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
if (end > dif->mediasize)
|
||||||
|
return (ENXIO);
|
||||||
|
if (off > INT64_MAX || end > (uint64_t)INT64_MAX + 1)
|
||||||
|
return (EOVERFLOW);
|
||||||
|
|
||||||
|
iosize = em->block_size != 0 ? em->block_size : dif->sectorsize;
|
||||||
|
if (iosize == 0 || (iosize & (iosize - 1)) != 0)
|
||||||
|
return (EINVAL);
|
||||||
|
out = malloc(len, M_EROFS, M_WAITOK);
|
||||||
|
done = 0;
|
||||||
|
while (done < len) {
|
||||||
|
current = (off_t)(off + done);
|
||||||
|
blkoff = rounddown2(current, (off_t)iosize);
|
||||||
|
blklen = MIN(iosize - (size_t)(current - blkoff), len - done);
|
||||||
|
error = bread(dif->devvp, btodb(blkoff), iosize, NOCRED, &bp);
|
||||||
|
if (error != 0) {
|
||||||
|
free(out, M_EROFS);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
if (bp->b_data == NULL) {
|
||||||
|
brelse(bp);
|
||||||
|
free(out, M_EROFS);
|
||||||
|
return (EIO);
|
||||||
|
}
|
||||||
|
memcpy(out + done, (char *)bp->b_data + (current - blkoff),
|
||||||
|
blklen);
|
||||||
|
brelse(bp);
|
||||||
|
done += blklen;
|
||||||
|
}
|
||||||
|
*bufp = out;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
erofs_bread(struct erofs_mount *em, uint64_t off, size_t len, void **bufp)
|
||||||
|
{
|
||||||
|
return (erofs_bread_device(em, &em->dif0, em->dif0.blocks, off, len,
|
||||||
|
bufp));
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
erofs_read_physical(struct erofs_mount *em, unsigned int device_id,
|
||||||
|
uint64_t off, size_t len, void **bufp)
|
||||||
|
{
|
||||||
|
struct erofs_map_dev map;
|
||||||
|
erofs_blk_t blocks;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
map = (struct erofs_map_dev) {
|
||||||
|
.m_pa = off,
|
||||||
|
.m_plen = len,
|
||||||
|
.m_deviceid = device_id,
|
||||||
|
};
|
||||||
|
error = erofs_map_dev(em, &map);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
blocks = map.m_dif->blocks;
|
||||||
|
if (map.m_dif == &em->dif0 && em->flatdev)
|
||||||
|
blocks = em->flatdev_blocks;
|
||||||
|
return (erofs_bread_device(em, map.m_dif, blocks, map.m_pa, len, bufp));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Release a contiguous buffer returned by erofs_bread(). */
|
||||||
|
void
|
||||||
|
erofs_brelse(void *buf)
|
||||||
|
{
|
||||||
|
free(buf, M_EROFS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read inode metadata from either the primary image or the metabox file. */
|
||||||
|
int
|
||||||
|
erofs_read_metadata(struct erofs_mount *em, erofs_nid_t nid, uint64_t off,
|
||||||
|
size_t len, void **bufp)
|
||||||
|
{
|
||||||
|
if (!erofs_nid_in_metabox(nid)) {
|
||||||
|
if (off > INT64_MAX)
|
||||||
|
return (EOVERFLOW);
|
||||||
|
return (erofs_bread(em, (off_t)off, len, bufp));
|
||||||
|
}
|
||||||
|
if (!erofs_sb_has_metabox(em) || em->metabox_en == NULL)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
return (erofs_read_data(em, em->metabox_en, off, len, bufp));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Map a logical file offset to a physical position for an uncompressed
|
||||||
|
* plain/inline inode.
|
||||||
|
*
|
||||||
|
* Output:
|
||||||
|
* - phys_off: physical byte offset;
|
||||||
|
* - run_len: contiguous length readable from the current position;
|
||||||
|
* - hole: whether the current range maps to a zero-filled hole (NULL_ADDR).
|
||||||
|
*/
|
||||||
|
int
|
||||||
|
erofs_map_blocks(struct erofs_mount *em, struct erofs_node *en, uint64_t loff,
|
||||||
|
uint64_t *phys_off, unsigned int *device_id, size_t *run_len, bool *hole,
|
||||||
|
bool *metadata)
|
||||||
|
{
|
||||||
|
uint64_t tail_start, remain, block_rem;
|
||||||
|
|
||||||
|
*phys_off = 0;
|
||||||
|
*device_id = 0;
|
||||||
|
*run_len = 0;
|
||||||
|
*hole = false;
|
||||||
|
*metadata = false;
|
||||||
|
if (loff >= en->size)
|
||||||
|
return (0);
|
||||||
|
|
||||||
|
remain = en->size - loff;
|
||||||
|
switch (en->datalayout) {
|
||||||
|
case EROFS_INODE_CHUNK_BASED:
|
||||||
|
return (erofs_map_blocks_chunk(em, en, loff, phys_off, device_id,
|
||||||
|
run_len, hole));
|
||||||
|
case EROFS_INODE_FLAT_PLAIN:
|
||||||
|
block_rem = em->block_size - (loff & (em->block_size - 1));
|
||||||
|
*run_len = MIN(remain, block_rem);
|
||||||
|
if (en->startblk == EROFS_NULL_ADDR) {
|
||||||
|
*hole = true;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
if (en->startblk > (UINT64_MAX >> em->block_bits) ||
|
||||||
|
__builtin_add_overflow(en->startblk << em->block_bits, loff,
|
||||||
|
phys_off))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
return (0);
|
||||||
|
case EROFS_INODE_FLAT_INLINE:
|
||||||
|
tail_start = erofs_inline_tail_start(em, en);
|
||||||
|
if (loff < tail_start) {
|
||||||
|
block_rem = em->block_size -
|
||||||
|
(loff & (em->block_size - 1));
|
||||||
|
*run_len = MIN(MIN(remain, tail_start - loff),
|
||||||
|
block_rem);
|
||||||
|
if (en->startblk == EROFS_NULL_ADDR) {
|
||||||
|
*hole = true;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
if (en->startblk > (UINT64_MAX >> em->block_bits) ||
|
||||||
|
__builtin_add_overflow(en->startblk << em->block_bits,
|
||||||
|
loff, phys_off))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
block_rem = em->block_size -
|
||||||
|
((loff - tail_start) & (em->block_size - 1));
|
||||||
|
*run_len = MIN(remain, block_rem);
|
||||||
|
if (__builtin_add_overflow(en->inode_off, en->inode_isize,
|
||||||
|
phys_off) || __builtin_add_overflow(*phys_off, en->xattr_isize,
|
||||||
|
phys_off) || __builtin_add_overflow(*phys_off, loff - tail_start,
|
||||||
|
phys_off))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
*metadata = true;
|
||||||
|
return (0);
|
||||||
|
case EROFS_INODE_COMPRESSED_FULL:
|
||||||
|
case EROFS_INODE_COMPRESSED_COMPACT:
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
default:
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Read a small range at a logical file offset into a contiguous buffer.
|
||||||
|
* Primarily used for directory block reads, lookup, and symlink fragment
|
||||||
|
* parsing.
|
||||||
|
*/
|
||||||
|
int
|
||||||
|
erofs_read_data(struct erofs_mount *em, struct erofs_node *en, uint64_t loff,
|
||||||
|
size_t len, void **bufp)
|
||||||
|
{
|
||||||
|
char *out;
|
||||||
|
void *blk;
|
||||||
|
uint64_t phys_off;
|
||||||
|
unsigned int device_id;
|
||||||
|
size_t run_len, done, want;
|
||||||
|
bool hole, metadata;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
if (bufp == NULL)
|
||||||
|
return (EINVAL);
|
||||||
|
*bufp = NULL;
|
||||||
|
if (len == 0) {
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
if (loff > UINT64_MAX - (uint64_t)len)
|
||||||
|
return (EOVERFLOW);
|
||||||
|
if (loff > en->size || (uint64_t)len > en->size - loff)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
|
||||||
|
/* Compressed file path */
|
||||||
|
if (en->datalayout == EROFS_INODE_COMPRESSED_FULL ||
|
||||||
|
en->datalayout == EROFS_INODE_COMPRESSED_COMPACT)
|
||||||
|
return (z_erofs_read_data(em, en, loff, len, bufp));
|
||||||
|
|
||||||
|
/* Uncompressed file path */
|
||||||
|
out = malloc(len, M_EROFS, M_WAITOK);
|
||||||
|
done = 0;
|
||||||
|
while (done < len) {
|
||||||
|
error = erofs_map_blocks(em, en, loff + done, &phys_off, &device_id,
|
||||||
|
&run_len, &hole, &metadata);
|
||||||
|
if (error != 0) {
|
||||||
|
free(out, M_EROFS);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
if (run_len == 0) {
|
||||||
|
free(out, M_EROFS);
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
want = MIN(run_len, len - done);
|
||||||
|
if (hole) {
|
||||||
|
bzero(out + done, want);
|
||||||
|
} else {
|
||||||
|
if (metadata)
|
||||||
|
error = erofs_read_metadata(em, en->nid, phys_off,
|
||||||
|
want, &blk);
|
||||||
|
else
|
||||||
|
error = erofs_read_physical(em, device_id, phys_off, want,
|
||||||
|
&blk);
|
||||||
|
if (error != 0) {
|
||||||
|
free(out, M_EROFS);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
memcpy(out + done, blk, want);
|
||||||
|
erofs_brelse(blk);
|
||||||
|
}
|
||||||
|
done += want;
|
||||||
|
}
|
||||||
|
*bufp = out;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Transfer the logical content of an inode directly into a uio.
|
||||||
|
* Regular files and symlinks both use this read path.
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
erofs_read_uio(struct erofs_mount *em, struct erofs_node *en, struct uio *uio)
|
||||||
|
{
|
||||||
|
char zerobuf[PAGE_SIZE];
|
||||||
|
void *blk;
|
||||||
|
uint64_t phys_off;
|
||||||
|
unsigned int device_id;
|
||||||
|
size_t run_len, want, chunk;
|
||||||
|
bool hole, metadata;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
if (uio->uio_offset < 0)
|
||||||
|
return (EINVAL);
|
||||||
|
if ((uint64_t)uio->uio_offset >= en->size)
|
||||||
|
return (0);
|
||||||
|
|
||||||
|
/* Compressed file path */
|
||||||
|
if (en->datalayout == EROFS_INODE_COMPRESSED_FULL ||
|
||||||
|
en->datalayout == EROFS_INODE_COMPRESSED_COMPACT)
|
||||||
|
return (z_erofs_read_uio(em, en, uio));
|
||||||
|
|
||||||
|
/* Uncompressed file path */
|
||||||
|
bzero(zerobuf, sizeof(zerobuf));
|
||||||
|
while (uio->uio_resid > 0 && (uint64_t)uio->uio_offset < en->size) {
|
||||||
|
error = erofs_map_blocks(em, en, uio->uio_offset, &phys_off,
|
||||||
|
&device_id, &run_len, &hole, &metadata);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (run_len == 0)
|
||||||
|
break;
|
||||||
|
want = MIN(run_len, (size_t)uio->uio_resid);
|
||||||
|
if (hole) {
|
||||||
|
chunk = want;
|
||||||
|
while (chunk > 0) {
|
||||||
|
size_t zlen = MIN(chunk, sizeof(zerobuf));
|
||||||
|
|
||||||
|
error = uiomove(zerobuf, zlen, uio);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
chunk -= zlen;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (metadata)
|
||||||
|
error = erofs_read_metadata(em, en->nid, phys_off, want,
|
||||||
|
&blk);
|
||||||
|
else
|
||||||
|
error = erofs_read_physical(em, device_id, phys_off, want,
|
||||||
|
&blk);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
error = uiomove(blk, want, uio);
|
||||||
|
erofs_brelse(blk);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read symlink target string. */
|
||||||
|
int
|
||||||
|
erofs_readlink_target(struct vnode *vp, struct uio *uio)
|
||||||
|
{
|
||||||
|
return (erofs_read_uio(MTOE(vp->v_mount), VTOE(vp), uio));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read regular file data. */
|
||||||
|
int
|
||||||
|
erofs_read_file(struct vnode *vp, struct uio *uio, int ioflag)
|
||||||
|
{
|
||||||
|
(void)ioflag;
|
||||||
|
return (erofs_read_uio(MTOE(vp->v_mount), VTOE(vp), uio));
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2019 HUAWEI, Inc.
|
||||||
|
* https://www.huawei.com/
|
||||||
|
* Copyright (C) 2024 Alibaba Cloud
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <sys/param.h>
|
||||||
|
#include <sys/endian.h>
|
||||||
|
#include <sys/libkern.h>
|
||||||
|
#include <sys/malloc.h>
|
||||||
|
#include <sys/systm.h>
|
||||||
|
|
||||||
|
#include "internal.h"
|
||||||
|
|
||||||
|
static int
|
||||||
|
z_erofs_load_lz4_config(struct erofs_mount *em,
|
||||||
|
const struct erofs_super_block *dsb, const void *data, size_t size)
|
||||||
|
{
|
||||||
|
const struct z_erofs_lz4_cfgs *lz4;
|
||||||
|
uint32_t max_pclusterblks;
|
||||||
|
uint16_t distance;
|
||||||
|
|
||||||
|
if (data != NULL) {
|
||||||
|
if (size < sizeof(*lz4))
|
||||||
|
return (EINVAL);
|
||||||
|
lz4 = data;
|
||||||
|
distance = le16toh(lz4->max_distance);
|
||||||
|
max_pclusterblks = le16toh(lz4->max_pclusterblks);
|
||||||
|
if (max_pclusterblks == 0)
|
||||||
|
max_pclusterblks = 1;
|
||||||
|
else if (max_pclusterblks >
|
||||||
|
(Z_EROFS_PCLUSTER_MAX_SIZE >> em->block_bits))
|
||||||
|
return (EINVAL);
|
||||||
|
} else {
|
||||||
|
distance = le16toh(dsb->u1.lz4_max_distance);
|
||||||
|
if (distance == 0 && !erofs_sb_has_lz4_0padding(em))
|
||||||
|
return (0);
|
||||||
|
max_pclusterblks = 1;
|
||||||
|
em->available_compr_algs = 1U << Z_EROFS_COMPRESSION_LZ4;
|
||||||
|
}
|
||||||
|
em->lz4.max_pclusterblks = max_pclusterblks;
|
||||||
|
em->lz4.max_distance_pages = distance != 0 ?
|
||||||
|
howmany(distance, PAGE_SIZE) + 1 :
|
||||||
|
howmany(UINT16_MAX, PAGE_SIZE) + 1;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
z_erofs_read_cfg(struct erofs_mount *em, uint64_t *offset, void **bufp,
|
||||||
|
size_t *sizep)
|
||||||
|
{
|
||||||
|
uint8_t length_buf[2];
|
||||||
|
uint64_t aligned;
|
||||||
|
uint16_t length;
|
||||||
|
void *buf;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
aligned = roundup2(*offset, 4);
|
||||||
|
if (aligned > UINT64_MAX - sizeof(length_buf))
|
||||||
|
return (EOVERFLOW);
|
||||||
|
error = erofs_bread(em, aligned, sizeof(length_buf), &buf);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
memcpy(length_buf, buf, sizeof(length_buf));
|
||||||
|
erofs_brelse(buf);
|
||||||
|
length = le16dec(length_buf);
|
||||||
|
*sizep = length != 0 ? length : UINT16_MAX + 1U;
|
||||||
|
if (*sizep > 65536 || aligned + sizeof(length_buf) >
|
||||||
|
UINT64_MAX - *sizep)
|
||||||
|
return (EOVERFLOW);
|
||||||
|
*offset = aligned + sizeof(length_buf);
|
||||||
|
error = erofs_bread(em, *offset, *sizep, bufp);
|
||||||
|
if (error == 0)
|
||||||
|
*offset += *sizep;
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
z_erofs_parse_cfgs(struct erofs_mount *em,
|
||||||
|
const struct erofs_super_block *dsb)
|
||||||
|
{
|
||||||
|
uint64_t offset;
|
||||||
|
uint16_t algorithms;
|
||||||
|
void *data;
|
||||||
|
size_t size;
|
||||||
|
int algorithm, error;
|
||||||
|
|
||||||
|
if (!erofs_sb_has_compr_cfgs(em))
|
||||||
|
return (z_erofs_load_lz4_config(em, dsb, NULL, 0));
|
||||||
|
algorithms = le16toh(dsb->u1.available_compr_algs);
|
||||||
|
em->available_compr_algs = algorithms;
|
||||||
|
if ((algorithms & ~Z_EROFS_ALL_COMPR_ALGS) != 0)
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
offset = EROFS_SUPER_OFFSET + em->sb_size;
|
||||||
|
for (algorithm = 0; algorithm < Z_EROFS_COMPRESSION_MAX;
|
||||||
|
++algorithm) {
|
||||||
|
if ((algorithms & (1U << algorithm)) == 0)
|
||||||
|
continue;
|
||||||
|
error = z_erofs_read_cfg(em, &offset, &data, &size);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
switch (algorithm) {
|
||||||
|
case Z_EROFS_COMPRESSION_LZ4:
|
||||||
|
error = z_erofs_load_lz4_config(em, dsb, data, size);
|
||||||
|
break;
|
||||||
|
case Z_EROFS_COMPRESSION_LZMA:
|
||||||
|
error = z_erofs_load_lzma_config(em, data, size);
|
||||||
|
break;
|
||||||
|
case Z_EROFS_COMPRESSION_DEFLATE:
|
||||||
|
error = z_erofs_load_deflate_config(em, data, size);
|
||||||
|
break;
|
||||||
|
case Z_EROFS_COMPRESSION_ZSTD:
|
||||||
|
error = z_erofs_load_zstd_config(em, data, size);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
error = EOPNOTSUPP;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
erofs_brelse(data);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
z_erofs_transform_plain(struct erofs_mount *em,
|
||||||
|
const struct erofs_map_blocks *map, const uint8_t *src, size_t srclen,
|
||||||
|
uint8_t *dst, size_t dstlen)
|
||||||
|
{
|
||||||
|
size_t first, offset;
|
||||||
|
|
||||||
|
if (dstlen > srclen)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
if (map->m_algorithmformat == Z_EROFS_COMPRESSION_SHIFTED) {
|
||||||
|
memmove(dst, src, dstlen);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
first = MIN((size_t)(em->block_size -
|
||||||
|
(map->m_la & (em->block_size - 1))), dstlen);
|
||||||
|
offset = (srclen - first) & (em->block_size - 1);
|
||||||
|
if (offset > srclen || first > srclen - offset)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
memmove(dst, src + offset, first);
|
||||||
|
if (first < dstlen)
|
||||||
|
memmove(dst + first, src, dstlen - first);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
z_erofs_decompress(struct erofs_mount *em,
|
||||||
|
const struct erofs_map_blocks *map, const void *src0, size_t srclen,
|
||||||
|
void *dst, size_t dstlen, bool partial)
|
||||||
|
{
|
||||||
|
const uint8_t *src;
|
||||||
|
size_t padding, padding_limit;
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
if (map->m_algorithmformat == Z_EROFS_COMPRESSION_SHIFTED ||
|
||||||
|
map->m_algorithmformat == Z_EROFS_COMPRESSION_INTERLACED)
|
||||||
|
return (z_erofs_transform_plain(em, map, src0, srclen, dst,
|
||||||
|
dstlen));
|
||||||
|
if ((unsigned char)map->m_algorithmformat >= Z_EROFS_COMPRESSION_MAX)
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
|
||||||
|
src = src0;
|
||||||
|
if (map->m_algorithmformat != Z_EROFS_COMPRESSION_LZ4 ||
|
||||||
|
erofs_sb_has_lz4_0padding(em)) {
|
||||||
|
padding_limit = MIN(srclen, em->block_size -
|
||||||
|
(map->m_pa & (em->block_size - 1)));
|
||||||
|
for (padding = 0; padding < padding_limit && src[padding] == 0;
|
||||||
|
++padding)
|
||||||
|
;
|
||||||
|
if (padding == padding_limit)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
src += padding;
|
||||||
|
srclen -= padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (map->m_algorithmformat) {
|
||||||
|
case Z_EROFS_COMPRESSION_LZ4:
|
||||||
|
ret = lz4_decompress(__DECONST(void *, src), dst, srclen,
|
||||||
|
dstlen, partial);
|
||||||
|
break;
|
||||||
|
case Z_EROFS_COMPRESSION_LZMA:
|
||||||
|
if (em->lzma_dict_size == 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
ret = lzma_decompress(src, srclen, dst, dstlen,
|
||||||
|
em->lzma_dict_size, partial);
|
||||||
|
break;
|
||||||
|
case Z_EROFS_COMPRESSION_DEFLATE:
|
||||||
|
ret = deflate_decompress(__DECONST(void *, src), srclen, dst,
|
||||||
|
dstlen, em->deflate_windowbits, partial);
|
||||||
|
break;
|
||||||
|
case Z_EROFS_COMPRESSION_ZSTD:
|
||||||
|
ret = zstd_decompress(__DECONST(void *, src), srclen, dst,
|
||||||
|
dstlen, em->zstd_windowlog + 10, partial);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
}
|
||||||
|
return (ret == 0 ? 0 : EIO);
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/* SPDX-License-Identifier: BSD-2-Clause */
|
||||||
|
/* Minimal DEFLATE decompressor for EROFS FreeBSD */
|
||||||
|
#include <sys/param.h>
|
||||||
|
#include <sys/systm.h>
|
||||||
|
#include <sys/kernel.h>
|
||||||
|
#include <contrib/zlib/zlib.h>
|
||||||
|
|
||||||
|
#include "internal.h"
|
||||||
|
|
||||||
|
int
|
||||||
|
z_erofs_load_deflate_config(struct erofs_mount *em, const void *data,
|
||||||
|
size_t size)
|
||||||
|
{
|
||||||
|
const struct z_erofs_deflate_cfgs *deflate;
|
||||||
|
|
||||||
|
if (size < sizeof(*deflate))
|
||||||
|
return (EINVAL);
|
||||||
|
deflate = data;
|
||||||
|
if (deflate->windowbits < 8 || deflate->windowbits > 15)
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
em->deflate_windowbits = deflate->windowbits;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
deflate_decompress(void *src, size_t srclen, void *dst, size_t dstlen, int n,
|
||||||
|
bool partial)
|
||||||
|
{
|
||||||
|
z_stream strm;
|
||||||
|
uInt in_before, out_before;
|
||||||
|
int endret, ret;
|
||||||
|
|
||||||
|
if (n < 8 || n > MAX_WBITS || srclen > (size_t)(uInt)-1 ||
|
||||||
|
dstlen > (size_t)(uInt)-1 || dstlen == 0)
|
||||||
|
return (-1);
|
||||||
|
|
||||||
|
bzero(&strm, sizeof(strm));
|
||||||
|
strm.next_in = src;
|
||||||
|
strm.avail_in = srclen;
|
||||||
|
strm.next_out = dst;
|
||||||
|
strm.avail_out = dstlen;
|
||||||
|
|
||||||
|
ret = inflateInit2(&strm, -n);
|
||||||
|
if (ret != Z_OK)
|
||||||
|
return (-1);
|
||||||
|
|
||||||
|
ret = Z_OK;
|
||||||
|
while (strm.avail_out != 0) {
|
||||||
|
in_before = strm.avail_in;
|
||||||
|
out_before = strm.avail_out;
|
||||||
|
ret = inflate(&strm, Z_SYNC_FLUSH);
|
||||||
|
if (ret == Z_STREAM_END)
|
||||||
|
break;
|
||||||
|
if (ret != Z_OK ||
|
||||||
|
(strm.avail_in == in_before && strm.avail_out == out_before))
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
endret = inflateEnd(&strm);
|
||||||
|
if (endret != Z_OK || strm.avail_out != 0)
|
||||||
|
return (-1);
|
||||||
|
if (partial)
|
||||||
|
return (ret == Z_OK || ret == Z_STREAM_END ? 0 : -1);
|
||||||
|
if (ret != Z_STREAM_END || strm.avail_in != 0)
|
||||||
|
return (-1);
|
||||||
|
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
/*
|
||||||
|
* EROFS MicroLZMA wrapper around FreeBSD's bundled XZ Embedded decoder.
|
||||||
|
* The decoder source is compiled with private symbol names because the
|
||||||
|
* stock xz.ko does not enable its optional MicroLZMA entry points.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <sys/param.h>
|
||||||
|
#include <sys/malloc.h>
|
||||||
|
#include <sys/systm.h>
|
||||||
|
|
||||||
|
#include "internal.h"
|
||||||
|
|
||||||
|
#define XZ_DEC_MICROLZMA
|
||||||
|
#define xz_dec_lzma2_create erofs_xz_dec_lzma2_create
|
||||||
|
#define xz_dec_lzma2_reset erofs_xz_dec_lzma2_reset
|
||||||
|
#define xz_dec_lzma2_run erofs_xz_dec_lzma2_run
|
||||||
|
#define xz_dec_lzma2_end erofs_xz_dec_lzma2_end
|
||||||
|
#define xz_dec_microlzma_alloc erofs_xz_dec_microlzma_alloc
|
||||||
|
#define xz_dec_microlzma_reset erofs_xz_dec_microlzma_reset
|
||||||
|
#define xz_dec_microlzma_run erofs_xz_dec_microlzma_run
|
||||||
|
#define xz_dec_microlzma_end erofs_xz_dec_microlzma_end
|
||||||
|
#define xz_malloc erofs_xz_malloc
|
||||||
|
#define xz_free erofs_xz_free
|
||||||
|
|
||||||
|
static void *
|
||||||
|
erofs_xz_malloc(unsigned long size)
|
||||||
|
{
|
||||||
|
return (malloc(size, M_EROFS, M_WAITOK));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
erofs_xz_free(void *ptr)
|
||||||
|
{
|
||||||
|
free(ptr, M_EROFS);
|
||||||
|
}
|
||||||
|
|
||||||
|
#include <contrib/xz-embedded/linux/lib/xz/xz_dec_lzma2.c>
|
||||||
|
|
||||||
|
#undef bool
|
||||||
|
#undef false
|
||||||
|
#undef true
|
||||||
|
#undef min
|
||||||
|
|
||||||
|
int
|
||||||
|
z_erofs_load_lzma_config(struct erofs_mount *em, const void *data,
|
||||||
|
size_t size)
|
||||||
|
{
|
||||||
|
const struct z_erofs_lzma_cfgs *lzma;
|
||||||
|
uint32_t dict_size;
|
||||||
|
|
||||||
|
if (size < sizeof(*lzma))
|
||||||
|
return (EINVAL);
|
||||||
|
lzma = data;
|
||||||
|
if (le16toh(lzma->format) != 0)
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
dict_size = le32toh(lzma->dict_size);
|
||||||
|
if (dict_size < 4096 || dict_size > Z_EROFS_LZMA_MAX_DICT_SIZE)
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
em->lzma_dict_size = dict_size;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
lzma_decompress(const void *src, size_t srclen, void *dst, size_t dstlen,
|
||||||
|
uint32_t dict_size, bool partial)
|
||||||
|
{
|
||||||
|
struct xz_dec_microlzma *state;
|
||||||
|
struct xz_buf buffer;
|
||||||
|
enum xz_ret ret;
|
||||||
|
|
||||||
|
if (srclen > UINT32_MAX || dstlen > UINT32_MAX)
|
||||||
|
return (-1);
|
||||||
|
state = xz_dec_microlzma_alloc(XZ_SINGLE, dict_size);
|
||||||
|
if (state == NULL)
|
||||||
|
return (-1);
|
||||||
|
bzero(&buffer, sizeof(buffer));
|
||||||
|
buffer.in = src;
|
||||||
|
buffer.in_size = srclen;
|
||||||
|
buffer.out = dst;
|
||||||
|
buffer.out_size = dstlen;
|
||||||
|
xz_dec_microlzma_reset(state, (uint32_t)srclen, (uint32_t)dstlen,
|
||||||
|
!partial);
|
||||||
|
ret = xz_dec_microlzma_run(state, &buffer);
|
||||||
|
xz_dec_microlzma_end(state);
|
||||||
|
if (buffer.out_pos != dstlen)
|
||||||
|
return (-1);
|
||||||
|
if (partial)
|
||||||
|
return (ret == XZ_OK || ret == XZ_STREAM_END ? 0 : -1);
|
||||||
|
return (ret == XZ_STREAM_END && buffer.in_pos == srclen ? 0 : -1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
/* SPDX-License-Identifier: BSD-2-Clause */
|
||||||
|
/* Minimal zstd decompressor for EROFS FreeBSD */
|
||||||
|
#include <sys/param.h>
|
||||||
|
#include <sys/malloc.h>
|
||||||
|
#include <sys/systm.h>
|
||||||
|
#include <sys/kernel.h>
|
||||||
|
|
||||||
|
#include "internal.h"
|
||||||
|
|
||||||
|
#ifdef ZSTDIO
|
||||||
|
#define ZSTD_STATIC_LINKING_ONLY
|
||||||
|
#include <contrib/zstd/lib/zstd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool
|
||||||
|
erofs_zstd_available(void)
|
||||||
|
{
|
||||||
|
#ifdef ZSTDIO
|
||||||
|
return (true);
|
||||||
|
#else
|
||||||
|
return (false);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
z_erofs_load_zstd_config(struct erofs_mount *em, const void *data,
|
||||||
|
size_t size)
|
||||||
|
{
|
||||||
|
const struct z_erofs_zstd_cfgs *zstd;
|
||||||
|
|
||||||
|
if (!erofs_zstd_available()) {
|
||||||
|
vfs_mount_error(em->mnt,
|
||||||
|
"erofs: ZSTD compression requires ZSTDIO support");
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
}
|
||||||
|
if (size < sizeof(*zstd))
|
||||||
|
return (EINVAL);
|
||||||
|
zstd = data;
|
||||||
|
if (zstd->format != 0 || zstd->windowlog > 10)
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
em->zstd_windowlog = zstd->windowlog;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef ZSTDIO
|
||||||
|
static void *
|
||||||
|
zstd_alloc(void *opaque, size_t size)
|
||||||
|
{
|
||||||
|
return (malloc(size, opaque, M_WAITOK));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
zstd_free(void *opaque, void *address)
|
||||||
|
{
|
||||||
|
free(address, opaque);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const ZSTD_customMem zstd_erofs_alloc = {
|
||||||
|
.customAlloc = zstd_alloc,
|
||||||
|
.customFree = zstd_free,
|
||||||
|
.opaque = M_EROFS,
|
||||||
|
};
|
||||||
|
|
||||||
|
int
|
||||||
|
zstd_decompress(void *src, size_t srclen, void *dst, size_t dstlen, int n,
|
||||||
|
bool partial)
|
||||||
|
{
|
||||||
|
ZSTD_DCtx *dctx;
|
||||||
|
ZSTD_inBuffer input;
|
||||||
|
ZSTD_outBuffer output;
|
||||||
|
size_t in_before, out_before, ret;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
if (n < 10 || n > 20 || dstlen == 0)
|
||||||
|
return (-1);
|
||||||
|
dctx = ZSTD_createDCtx_advanced(zstd_erofs_alloc);
|
||||||
|
if (dctx == NULL)
|
||||||
|
return (-1);
|
||||||
|
ret = ZSTD_DCtx_setParameter(dctx, ZSTD_d_windowLogMax, n);
|
||||||
|
if (ZSTD_isError(ret)) {
|
||||||
|
ZSTD_freeDCtx(dctx);
|
||||||
|
return (-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
input = (ZSTD_inBuffer) {
|
||||||
|
.src = src,
|
||||||
|
.size = srclen,
|
||||||
|
};
|
||||||
|
output = (ZSTD_outBuffer) {
|
||||||
|
.dst = dst,
|
||||||
|
.size = dstlen,
|
||||||
|
};
|
||||||
|
ret = 1;
|
||||||
|
while (output.pos != output.size) {
|
||||||
|
in_before = input.pos;
|
||||||
|
out_before = output.pos;
|
||||||
|
ret = ZSTD_decompressStream(dctx, &output, &input);
|
||||||
|
if (ZSTD_isError(ret) ||
|
||||||
|
(input.pos == in_before && output.pos == out_before))
|
||||||
|
break;
|
||||||
|
if (ret == 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
error = 0;
|
||||||
|
if (ZSTD_isError(ret) || output.pos != output.size)
|
||||||
|
error = -1;
|
||||||
|
else if (!partial && (ret != 0 || input.pos != input.size))
|
||||||
|
error = -1;
|
||||||
|
ret = ZSTD_freeDCtx(dctx);
|
||||||
|
if (ZSTD_isError(ret))
|
||||||
|
error = -1;
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
int
|
||||||
|
zstd_decompress(void *src, size_t srclen, void *dst, size_t dstlen, int n,
|
||||||
|
bool partial)
|
||||||
|
{
|
||||||
|
(void)src;
|
||||||
|
(void)srclen;
|
||||||
|
(void)dst;
|
||||||
|
(void)dstlen;
|
||||||
|
(void)n;
|
||||||
|
(void)partial;
|
||||||
|
return (-1);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017-2018 HUAWEI, Inc.
|
||||||
|
* https://www.huawei.com/
|
||||||
|
* Copyright (C) 2022, Alibaba Cloud
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <sys/param.h>
|
||||||
|
#include <sys/systm.h>
|
||||||
|
#include <sys/dirent.h>
|
||||||
|
#include <sys/kernel.h>
|
||||||
|
#include <sys/libkern.h>
|
||||||
|
#include <sys/limits.h>
|
||||||
|
#include <sys/malloc.h>
|
||||||
|
#include <sys/mount.h>
|
||||||
|
#include <sys/vnode.h>
|
||||||
|
|
||||||
|
#include "internal.h"
|
||||||
|
#include "erofs_defs.h"
|
||||||
|
|
||||||
|
/* Map EROFS directory entry file type to FreeBSD dirent.d_type. */
|
||||||
|
static unsigned char
|
||||||
|
erofs_ftype_to_dtype(uint8_t ftype)
|
||||||
|
{
|
||||||
|
switch (ftype) {
|
||||||
|
case EROFS_FT_REG_FILE:
|
||||||
|
return (DT_REG);
|
||||||
|
case EROFS_FT_DIR:
|
||||||
|
return (DT_DIR);
|
||||||
|
case EROFS_FT_CHRDEV:
|
||||||
|
return (DT_CHR);
|
||||||
|
case EROFS_FT_BLKDEV:
|
||||||
|
return (DT_BLK);
|
||||||
|
case EROFS_FT_FIFO:
|
||||||
|
return (DT_FIFO);
|
||||||
|
case EROFS_FT_SOCK:
|
||||||
|
return (DT_SOCK);
|
||||||
|
case EROFS_FT_SYMLINK:
|
||||||
|
return (DT_LNK);
|
||||||
|
default:
|
||||||
|
return (DT_UNKNOWN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Validate one name slot and return its Linux-visible length. */
|
||||||
|
int
|
||||||
|
erofs_dirent_namelen(const char *blk, uint32_t nameoff, uint32_t endoff,
|
||||||
|
bool trailing, size_t *namelenp)
|
||||||
|
{
|
||||||
|
size_t namelen, span;
|
||||||
|
|
||||||
|
if (endoff <= nameoff)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
span = endoff - nameoff;
|
||||||
|
if (trailing) {
|
||||||
|
namelen = strnlen(blk + nameoff, span);
|
||||||
|
} else {
|
||||||
|
namelen = span;
|
||||||
|
if (memchr(blk + nameoff, '\0', span) != NULL)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
if (namelen == 0 || namelen > EROFS_NAME_LEN)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
for (size_t i = 0; i < namelen; i++) {
|
||||||
|
if (blk[nameoff + i] == '/')
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
*namelenp = namelen;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Validate the dirent array, name offsets, and names in a block. */
|
||||||
|
int
|
||||||
|
erofs_validate_dirblock(const char *blk, uint32_t blksz, uint32_t maxsize,
|
||||||
|
uint32_t *ndirentsp)
|
||||||
|
{
|
||||||
|
const struct erofs_dirent *de;
|
||||||
|
uint32_t endoff, first_nameoff, idx, nameoff, ndirents, prev_nameoff;
|
||||||
|
size_t namelen;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
if (blksz < EROFS_DIRENT_SIZE || maxsize < EROFS_DIRENT_SIZE ||
|
||||||
|
maxsize > blksz)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
de = (const struct erofs_dirent *)blk;
|
||||||
|
first_nameoff = le16toh(de[0].nameoff);
|
||||||
|
if (first_nameoff < EROFS_DIRENT_SIZE || first_nameoff >= maxsize ||
|
||||||
|
(first_nameoff % EROFS_DIRENT_SIZE) != 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
ndirents = first_nameoff / EROFS_DIRENT_SIZE;
|
||||||
|
prev_nameoff = 0;
|
||||||
|
for (idx = 0; idx < ndirents; idx++) {
|
||||||
|
nameoff = le16toh(de[idx].nameoff);
|
||||||
|
if ((idx == 0 && nameoff != first_nameoff) ||
|
||||||
|
(idx != 0 && nameoff <= prev_nameoff) ||
|
||||||
|
nameoff < first_nameoff || nameoff >= maxsize)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
endoff = idx + 1 < ndirents ?
|
||||||
|
le16toh(de[idx + 1].nameoff) : maxsize;
|
||||||
|
if (endoff <= nameoff || endoff > maxsize)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
error = erofs_dirent_namelen(blk, nameoff, endoff,
|
||||||
|
idx + 1 == ndirents, &namelen);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
prev_nameoff = nameoff;
|
||||||
|
}
|
||||||
|
*ndirentsp = ndirents;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extract name, nid, and type of the idx'th directory entry from a block. */
|
||||||
|
static int
|
||||||
|
erofs_dirent_name(const char *blk, uint32_t maxsize,
|
||||||
|
uint32_t idx, uint32_t ndirents, char *name, size_t namesz, uint64_t *nid,
|
||||||
|
uint8_t *ftype, size_t *namelenp)
|
||||||
|
{
|
||||||
|
const struct erofs_dirent *de;
|
||||||
|
uint32_t nameoff, endoff;
|
||||||
|
size_t namelen;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
de = (const struct erofs_dirent *)blk;
|
||||||
|
nameoff = le16toh(de[idx].nameoff);
|
||||||
|
if (idx + 1 < ndirents)
|
||||||
|
endoff = le16toh(de[idx + 1].nameoff);
|
||||||
|
else
|
||||||
|
endoff = maxsize;
|
||||||
|
error = erofs_dirent_namelen(blk, nameoff, endoff,
|
||||||
|
idx + 1 == ndirents, &namelen);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (namelen >= namesz)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
memcpy(name, blk + nameoff, namelen);
|
||||||
|
name[namelen] = '\0';
|
||||||
|
*nid = le64toh(de[idx].nid);
|
||||||
|
*ftype = de[idx].file_type;
|
||||||
|
*namelenp = namelen;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Per-call state for readdir dirent/cookie output. */
|
||||||
|
struct erofs_uiodir {
|
||||||
|
struct dirent *dirent;
|
||||||
|
uint64_t *cookies;
|
||||||
|
uint64_t last_cookie;
|
||||||
|
int ncookies;
|
||||||
|
int acookies;
|
||||||
|
int eofflag;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum erofs_uiodir_result {
|
||||||
|
EROFS_UIODIR_BUFFER_FULL = -1,
|
||||||
|
EROFS_UIODIR_OK = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Push a dirent and its cookie to the caller, modelled after UDF. */
|
||||||
|
static int
|
||||||
|
erofs_uiodir(struct erofs_uiodir *uiodir, int de_size, struct uio *uio,
|
||||||
|
uint64_t cookie)
|
||||||
|
{
|
||||||
|
int error;
|
||||||
|
|
||||||
|
if (cookie <= uiodir->last_cookie)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
if (uio->uio_resid < de_size ||
|
||||||
|
(uiodir->cookies != NULL &&
|
||||||
|
uiodir->acookies >= uiodir->ncookies)) {
|
||||||
|
return (EROFS_UIODIR_BUFFER_FULL);
|
||||||
|
}
|
||||||
|
error = uiomove(uiodir->dirent, de_size, uio);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
uiodir->last_cookie = cookie;
|
||||||
|
if (uiodir->cookies != NULL)
|
||||||
|
uiodir->cookies[uiodir->acookies++] = cookie;
|
||||||
|
return (EROFS_UIODIR_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Process directory entries within a single block and output them to uio.
|
||||||
|
* (Linux equivalent: erofs_fill_dentries in Linux's dir.c)
|
||||||
|
*
|
||||||
|
* Returns 0 on success (all entries consumed), -1 if uio is full, or a
|
||||||
|
* positive error code on corruption.
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
erofs_fill_dentries(struct erofs_uiodir *uiodir, struct uio *uio,
|
||||||
|
struct dirent *d, const char *blk, uint32_t maxsize,
|
||||||
|
uint32_t start_idx, uint32_t ndirents, uint64_t block_off,
|
||||||
|
uint64_t *logical_offp)
|
||||||
|
{
|
||||||
|
char name[EROFS_NAME_LEN + 1];
|
||||||
|
uint32_t idx;
|
||||||
|
uint64_t curpos, nextoff, nid;
|
||||||
|
size_t namelen;
|
||||||
|
uint8_t ftype;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
for (idx = start_idx; idx < ndirents; idx++) {
|
||||||
|
curpos = block_off + idx * EROFS_DIRENT_SIZE;
|
||||||
|
nextoff = (idx + 1 < ndirents) ?
|
||||||
|
(curpos + EROFS_DIRENT_SIZE) :
|
||||||
|
(block_off + maxsize);
|
||||||
|
error = erofs_dirent_name(blk, maxsize, idx, ndirents, name,
|
||||||
|
sizeof(name), &nid, &ftype, &namelen);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
bzero(d, sizeof(*d));
|
||||||
|
d->d_fileno = nid;
|
||||||
|
d->d_type = erofs_ftype_to_dtype(ftype);
|
||||||
|
d->d_namlen = namelen;
|
||||||
|
d->d_reclen = GENERIC_DIRSIZ(d);
|
||||||
|
d->d_off = nextoff;
|
||||||
|
strlcpy(d->d_name, name, sizeof(d->d_name));
|
||||||
|
error = erofs_uiodir(uiodir, d->d_reclen, uio, d->d_off);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
*logical_offp = nextoff;
|
||||||
|
uio->uio_offset = *logical_offp;
|
||||||
|
}
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Read directory contents and output a FreeBSD dirent stream to uio.
|
||||||
|
*
|
||||||
|
* Key points:
|
||||||
|
* - On-disk entries use their logical file offsets as cookies;
|
||||||
|
* - A dot_omitted directory appends a synthetic "." at i_size, matching
|
||||||
|
* Linux, so existing on-disk cookies are not shifted;
|
||||||
|
* - The dirent array occupies only the front portion of a block, so after
|
||||||
|
* scanning all entries offset must jump to maxsize (the block end),
|
||||||
|
* otherwise the loop would get stuck on the same block;
|
||||||
|
* - Supports a_ncookies / a_cookies for NFS and other callers that need
|
||||||
|
* resumable iteration.
|
||||||
|
*/
|
||||||
|
int
|
||||||
|
erofs_readdir_block(struct vnode *vp, struct uio *uio, int *eofflag,
|
||||||
|
int *ncookies, uint64_t **cookies)
|
||||||
|
{
|
||||||
|
struct erofs_node *dir;
|
||||||
|
struct erofs_mount *em;
|
||||||
|
struct erofs_uiodir uiodir;
|
||||||
|
struct dirent d;
|
||||||
|
uint64_t *cookiebuf;
|
||||||
|
char *blk;
|
||||||
|
uint64_t block_off, logical_off;
|
||||||
|
uint32_t block_pos, blksz, ndirents, start_idx, maxsize;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
dir = VTOE(vp);
|
||||||
|
em = MTOE(vp->v_mount);
|
||||||
|
blksz = em->block_size;
|
||||||
|
error = 0;
|
||||||
|
cookiebuf = NULL;
|
||||||
|
uiodir.eofflag = 0;
|
||||||
|
uiodir.acookies = 0;
|
||||||
|
uiodir.dirent = &d;
|
||||||
|
uiodir.cookies = NULL;
|
||||||
|
uiodir.ncookies = 0;
|
||||||
|
if (cookies != NULL && ncookies != NULL) {
|
||||||
|
*cookies = NULL;
|
||||||
|
*ncookies = 0;
|
||||||
|
uiodir.ncookies = MAX(1, uio->uio_resid / 8);
|
||||||
|
cookiebuf = malloc(sizeof(*uiodir.cookies) * uiodir.ncookies,
|
||||||
|
M_TEMP, M_WAITOK);
|
||||||
|
uiodir.cookies = cookiebuf;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uio->uio_offset < 0) {
|
||||||
|
error = EINVAL;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
if (dir->dot_omitted && dir->size == (uint64_t)OFF_MAX) {
|
||||||
|
error = EINTEGRITY;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
logical_off = uio->uio_offset;
|
||||||
|
uiodir.last_cookie = logical_off;
|
||||||
|
uio->uio_offset = logical_off;
|
||||||
|
|
||||||
|
while (logical_off < dir->size) {
|
||||||
|
block_off = rounddown2(logical_off, (uint64_t)blksz);
|
||||||
|
maxsize = MIN((uint64_t)blksz, dir->size - block_off);
|
||||||
|
block_pos = logical_off - block_off;
|
||||||
|
if ((block_pos % EROFS_DIRENT_SIZE) != 0) {
|
||||||
|
block_pos = roundup(block_pos, EROFS_DIRENT_SIZE);
|
||||||
|
logical_off = block_off + block_pos;
|
||||||
|
uio->uio_offset = logical_off;
|
||||||
|
}
|
||||||
|
error = erofs_read_data(em, dir, block_off, maxsize,
|
||||||
|
(void **)&blk);
|
||||||
|
if (error != 0)
|
||||||
|
goto out;
|
||||||
|
error = erofs_validate_dirblock(blk, blksz, maxsize, &ndirents);
|
||||||
|
if (error != 0) {
|
||||||
|
erofs_brelse(blk);
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
start_idx = block_pos / EROFS_DIRENT_SIZE;
|
||||||
|
if (start_idx >= ndirents) {
|
||||||
|
logical_off = block_off + maxsize;
|
||||||
|
uio->uio_offset = logical_off;
|
||||||
|
erofs_brelse(blk);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
error = erofs_fill_dentries(&uiodir, uio, &d, blk, maxsize,
|
||||||
|
start_idx, ndirents, block_off, &logical_off);
|
||||||
|
erofs_brelse(blk);
|
||||||
|
if (error != 0)
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
if (dir->dot_omitted && logical_off == dir->size) {
|
||||||
|
bzero(&d, sizeof(d));
|
||||||
|
d.d_fileno = dir->nid;
|
||||||
|
d.d_type = DT_DIR;
|
||||||
|
d.d_namlen = 1;
|
||||||
|
d.d_reclen = GENERIC_DIRSIZ(&d);
|
||||||
|
d.d_off = dir->size + 1;
|
||||||
|
d.d_name[0] = '.';
|
||||||
|
d.d_name[1] = '\0';
|
||||||
|
error = erofs_uiodir(&uiodir, d.d_reclen, uio, d.d_off);
|
||||||
|
if (error != 0)
|
||||||
|
goto out;
|
||||||
|
logical_off++;
|
||||||
|
uio->uio_offset = logical_off;
|
||||||
|
}
|
||||||
|
uiodir.eofflag = 1;
|
||||||
|
out:
|
||||||
|
if (error == EROFS_UIODIR_BUFFER_FULL)
|
||||||
|
error = 0;
|
||||||
|
if (eofflag != NULL && error == 0)
|
||||||
|
*eofflag = uiodir.eofflag;
|
||||||
|
if (cookies != NULL && ncookies != NULL) {
|
||||||
|
if (error != 0) {
|
||||||
|
free(cookiebuf, M_TEMP);
|
||||||
|
} else {
|
||||||
|
*ncookies = uiodir.acookies;
|
||||||
|
*cookies = cookiebuf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-2.0-only */
|
||||||
|
#ifndef __EROFS_DEFS_H
|
||||||
|
#define __EROFS_DEFS_H
|
||||||
|
|
||||||
|
/* CRC32C polynomial and seed */
|
||||||
|
#define EROFS_CRC32C_SEED 0x5045b54aU
|
||||||
|
|
||||||
|
/* Inode slot alignment */
|
||||||
|
|
||||||
|
/* Directory entry size */
|
||||||
|
#define EROFS_DIRENT_SIZE sizeof(struct erofs_dirent)
|
||||||
|
|
||||||
|
/* RC decoder constants */
|
||||||
|
|
||||||
|
/* LZ4 format constants */
|
||||||
|
#define EROFS_LZ4_TOKEN_LITERAL_SHIFT 4
|
||||||
|
#define EROFS_LZ4_TOKEN_MATCH_MASK 0x0F
|
||||||
|
#define EROFS_LZ4_MAX_RUN 15
|
||||||
|
#define EROFS_LZ4_EXT_SENTINEL 255
|
||||||
|
#define EROFS_LZ4_MIN_MATCH 4
|
||||||
|
#define EROFS_LZ4_OFFSET_BYTES 2
|
||||||
|
|
||||||
|
#endif /* __EROFS_DEFS_H */
|
||||||
+472
@@ -0,0 +1,472 @@
|
|||||||
|
/* SPDX-License-Identifier: MIT */
|
||||||
|
/*
|
||||||
|
* EROFS (Enhanced ROM File System) on-disk format definition
|
||||||
|
*
|
||||||
|
* Copyright (C) 2017-2018 HUAWEI, Inc.
|
||||||
|
* https://www.huawei.com/
|
||||||
|
* Copyright (C) 2021, Alibaba Cloud
|
||||||
|
*/
|
||||||
|
#ifndef __EROFS_FS_H
|
||||||
|
#define __EROFS_FS_H
|
||||||
|
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/endian.h>
|
||||||
|
|
||||||
|
/* FreeBSD compatibility - Linux-style little-endian types */
|
||||||
|
#ifndef __le16
|
||||||
|
typedef uint16_t __le16;
|
||||||
|
typedef uint32_t __le32;
|
||||||
|
typedef uint64_t __le64;
|
||||||
|
typedef uint8_t __u8;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* to allow for x86 boot sectors and other oddities. */
|
||||||
|
#define EROFS_SUPER_OFFSET 1024
|
||||||
|
|
||||||
|
#define EROFS_SUPER_MAGIC_V1 0xE0F5E1E2
|
||||||
|
|
||||||
|
#define EROFS_FEATURE_COMPAT_SB_CHKSUM 0x00000001
|
||||||
|
#define EROFS_FEATURE_COMPAT_MTIME 0x00000002
|
||||||
|
#define EROFS_FEATURE_COMPAT_XATTR_FILTER 0x00000004
|
||||||
|
#define EROFS_FEATURE_COMPAT_SHARED_EA_IN_METABOX 0x00000008
|
||||||
|
#define EROFS_FEATURE_COMPAT_PLAIN_XATTR_PFX 0x00000010
|
||||||
|
#define EROFS_FEATURE_COMPAT_ISHARE_XATTRS 0x00000020
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Any bits that aren't in EROFS_ALL_SUPPORTED_INCOMPAT should
|
||||||
|
* be incompatible with this kernel version.
|
||||||
|
*/
|
||||||
|
#define EROFS_FEATURE_INCOMPAT_LZ4_0PADDING 0x00000001
|
||||||
|
#define EROFS_FEATURE_INCOMPAT_COMPR_CFGS 0x00000002
|
||||||
|
#define EROFS_FEATURE_INCOMPAT_BIG_PCLUSTER 0x00000002
|
||||||
|
#define EROFS_FEATURE_INCOMPAT_CHUNKED_FILE 0x00000004
|
||||||
|
#define EROFS_FEATURE_INCOMPAT_DEVICE_TABLE 0x00000008
|
||||||
|
#define EROFS_FEATURE_INCOMPAT_COMPR_HEAD2 0x00000008
|
||||||
|
#define EROFS_FEATURE_INCOMPAT_ZTAILPACKING 0x00000010
|
||||||
|
#define EROFS_FEATURE_INCOMPAT_FRAGMENTS 0x00000020
|
||||||
|
#define EROFS_FEATURE_INCOMPAT_DEDUPE 0x00000020
|
||||||
|
#define EROFS_FEATURE_INCOMPAT_XATTR_PREFIXES 0x00000040
|
||||||
|
#define EROFS_FEATURE_INCOMPAT_48BIT 0x00000080
|
||||||
|
#define EROFS_FEATURE_INCOMPAT_METABOX 0x00000100
|
||||||
|
|
||||||
|
#define EROFS_DIRENT_NID_METABOX_BIT 63
|
||||||
|
#define EROFS_DIRENT_NID_METABOX (1ULL << EROFS_DIRENT_NID_METABOX_BIT)
|
||||||
|
#define EROFS_DIRENT_NID_MASK ((1ULL << EROFS_DIRENT_NID_METABOX_BIT) - 1)
|
||||||
|
|
||||||
|
#define EROFS_ALL_SUPPORTED_INCOMPAT \
|
||||||
|
(EROFS_FEATURE_INCOMPAT_LZ4_0PADDING | EROFS_FEATURE_INCOMPAT_48BIT | \
|
||||||
|
EROFS_FEATURE_INCOMPAT_COMPR_CFGS | \
|
||||||
|
EROFS_FEATURE_INCOMPAT_XATTR_PREFIXES | \
|
||||||
|
EROFS_FEATURE_INCOMPAT_ZTAILPACKING | \
|
||||||
|
EROFS_FEATURE_INCOMPAT_CHUNKED_FILE | \
|
||||||
|
EROFS_FEATURE_INCOMPAT_COMPR_HEAD2 | \
|
||||||
|
EROFS_FEATURE_INCOMPAT_FRAGMENTS | \
|
||||||
|
EROFS_FEATURE_INCOMPAT_METABOX)
|
||||||
|
|
||||||
|
#define EROFS_SB_EXTSLOT_SIZE 16
|
||||||
|
|
||||||
|
#define EROFS_NAME_LEN 255
|
||||||
|
|
||||||
|
/* EROFS inode datalayout (i_format in on-disk inode) */
|
||||||
|
enum {
|
||||||
|
EROFS_INODE_FLAT_PLAIN = 0,
|
||||||
|
EROFS_INODE_COMPRESSED_FULL = 1,
|
||||||
|
EROFS_INODE_FLAT_INLINE = 2,
|
||||||
|
EROFS_INODE_COMPRESSED_COMPACT = 3,
|
||||||
|
EROFS_INODE_CHUNK_BASED = 4,
|
||||||
|
EROFS_INODE_DATALAYOUT_MAX
|
||||||
|
};
|
||||||
|
|
||||||
|
/* bit definitions of inode i_format */
|
||||||
|
#define EROFS_I_VERSION_MASK 0x01
|
||||||
|
#define EROFS_I_DATALAYOUT_MASK 0x07
|
||||||
|
|
||||||
|
#define EROFS_I_VERSION_BIT 0
|
||||||
|
#define EROFS_I_DATALAYOUT_BIT 1
|
||||||
|
#define EROFS_I_NLINK_1_BIT 4 /* non-directory compact inodes only */
|
||||||
|
#define EROFS_I_DOT_OMITTED_BIT 4 /* (directories) omit the `.` dirent */
|
||||||
|
#define EROFS_I_ALL ((1 << (EROFS_I_NLINK_1_BIT + 1)) - 1)
|
||||||
|
|
||||||
|
/* file type definitions in directory entries */
|
||||||
|
#define EROFS_FT_UNKNOWN 0
|
||||||
|
#define EROFS_FT_REG_FILE 1
|
||||||
|
#define EROFS_FT_DIR 2
|
||||||
|
#define EROFS_FT_CHRDEV 3
|
||||||
|
#define EROFS_FT_BLKDEV 4
|
||||||
|
#define EROFS_FT_FIFO 5
|
||||||
|
#define EROFS_FT_SOCK 6
|
||||||
|
#define EROFS_FT_SYMLINK 7
|
||||||
|
|
||||||
|
/* represent a zeroed chunk (hole) */
|
||||||
|
#define EROFS_NULL_ADDR ((uint64_t)-1)
|
||||||
|
|
||||||
|
/* erofs on-disk super block (currently 144 bytes at maximum) */
|
||||||
|
struct erofs_super_block {
|
||||||
|
uint32_t magic;
|
||||||
|
uint32_t checksum;
|
||||||
|
uint32_t feature_compat;
|
||||||
|
uint8_t blkszbits;
|
||||||
|
uint8_t sb_extslots;
|
||||||
|
union {
|
||||||
|
uint16_t rootnid_2b;
|
||||||
|
uint16_t blocks_hi;
|
||||||
|
} __packed rb;
|
||||||
|
uint64_t inos;
|
||||||
|
uint64_t epoch;
|
||||||
|
uint32_t fixed_nsec;
|
||||||
|
uint32_t blocks_lo;
|
||||||
|
uint32_t meta_blkaddr;
|
||||||
|
uint32_t xattr_blkaddr;
|
||||||
|
uint8_t uuid[16];
|
||||||
|
uint8_t volume_name[16];
|
||||||
|
uint32_t feature_incompat;
|
||||||
|
union {
|
||||||
|
uint16_t available_compr_algs;
|
||||||
|
uint16_t lz4_max_distance;
|
||||||
|
} __packed u1;
|
||||||
|
uint16_t extra_devices;
|
||||||
|
uint16_t devt_slotoff;
|
||||||
|
uint8_t dirblkbits;
|
||||||
|
uint8_t xattr_prefix_count;
|
||||||
|
uint32_t xattr_prefix_start;
|
||||||
|
uint64_t packed_nid;
|
||||||
|
uint8_t xattr_filter_reserved;
|
||||||
|
uint8_t ishare_xattr_prefix_id;
|
||||||
|
uint8_t reserved[2];
|
||||||
|
uint32_t build_time;
|
||||||
|
uint64_t rootnid_8b;
|
||||||
|
uint64_t reserved2;
|
||||||
|
uint64_t metabox_nid;
|
||||||
|
uint64_t reserved3;
|
||||||
|
} __packed;
|
||||||
|
|
||||||
|
struct erofs_inode_chunk_info {
|
||||||
|
__le16 format;
|
||||||
|
__le16 reserved;
|
||||||
|
} __packed;
|
||||||
|
|
||||||
|
union erofs_inode_i_u {
|
||||||
|
__le32 blocks_lo;
|
||||||
|
__le32 startblk_lo;
|
||||||
|
__le32 rdev;
|
||||||
|
struct erofs_inode_chunk_info c;
|
||||||
|
};
|
||||||
|
|
||||||
|
union erofs_inode_i_nb {
|
||||||
|
uint16_t nlink; /* if EROFS_I_NLINK_1_BIT is unset */
|
||||||
|
uint16_t blocks_hi; /* total blocks count MSB */
|
||||||
|
uint16_t startblk_hi; /* starting block number MSB */
|
||||||
|
} __packed;
|
||||||
|
|
||||||
|
/* 32-byte reduced form of an ondisk inode */
|
||||||
|
struct erofs_inode_compact {
|
||||||
|
uint16_t i_format; /* inode format hints */
|
||||||
|
uint16_t i_xattr_icount;
|
||||||
|
uint16_t i_mode;
|
||||||
|
union erofs_inode_i_nb i_nb;
|
||||||
|
uint32_t i_size;
|
||||||
|
uint32_t i_mtime;
|
||||||
|
union erofs_inode_i_u i_u;
|
||||||
|
|
||||||
|
uint32_t i_ino; /* only used for 32-bit stat compatibility */
|
||||||
|
uint16_t i_uid;
|
||||||
|
uint16_t i_gid;
|
||||||
|
uint32_t i_reserved;
|
||||||
|
} __packed;
|
||||||
|
|
||||||
|
/* 64-byte complete form of an ondisk inode */
|
||||||
|
struct erofs_inode_extended {
|
||||||
|
uint16_t i_format; /* inode format hints */
|
||||||
|
uint16_t i_xattr_icount;
|
||||||
|
uint16_t i_mode;
|
||||||
|
union erofs_inode_i_nb i_nb;
|
||||||
|
uint64_t i_size;
|
||||||
|
union erofs_inode_i_u i_u;
|
||||||
|
|
||||||
|
uint32_t i_ino; /* only used for 32-bit stat compatibility */
|
||||||
|
uint32_t i_uid;
|
||||||
|
uint32_t i_gid;
|
||||||
|
uint64_t i_mtime;
|
||||||
|
uint32_t i_mtime_nsec;
|
||||||
|
uint32_t i_nlink;
|
||||||
|
uint8_t i_reserved2[16];
|
||||||
|
} __packed;
|
||||||
|
|
||||||
|
/* dirent sorts in alphabet order, thus we can do binary search */
|
||||||
|
struct erofs_dirent {
|
||||||
|
uint64_t nid;
|
||||||
|
uint16_t nameoff;
|
||||||
|
uint8_t file_type;
|
||||||
|
uint8_t reserved;
|
||||||
|
} __packed;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* inline xattrs (n == i_xattr_icount):
|
||||||
|
* erofs_xattr_ibody_header(1) + (n - 1) * 4 bytes
|
||||||
|
* 12 bytes / \
|
||||||
|
* / \
|
||||||
|
* /-----------------------\
|
||||||
|
* | erofs_xattr_entries+ |
|
||||||
|
* +-----------------------+
|
||||||
|
* inline xattrs must starts in erofs_xattr_ibody_header,
|
||||||
|
* for read-only fs, no need to introduce h_refcount
|
||||||
|
*/
|
||||||
|
struct erofs_xattr_ibody_header {
|
||||||
|
uint32_t h_name_filter; /* bit value 1 indicates not-present */
|
||||||
|
uint8_t h_shared_count;
|
||||||
|
uint8_t h_reserved2[7];
|
||||||
|
uint32_t h_shared_xattrs[0]; /* shared xattr id array */
|
||||||
|
} __packed;
|
||||||
|
|
||||||
|
/* Name indexes */
|
||||||
|
#define EROFS_XATTR_INDEX_USER 1
|
||||||
|
#define EROFS_XATTR_INDEX_POSIX_ACL_ACCESS 2
|
||||||
|
#define EROFS_XATTR_INDEX_POSIX_ACL_DEFAULT 3
|
||||||
|
#define EROFS_XATTR_INDEX_TRUSTED 4
|
||||||
|
#define EROFS_XATTR_INDEX_LUSTRE 5
|
||||||
|
#define EROFS_XATTR_INDEX_SECURITY 6
|
||||||
|
|
||||||
|
/*
|
||||||
|
* bit 7 of e_name_index is set when it refers to a long xattr name prefix,
|
||||||
|
* while the remained lower bits represent the index of the prefix.
|
||||||
|
*/
|
||||||
|
#define EROFS_XATTR_LONG_PREFIX 0x80
|
||||||
|
#define EROFS_XATTR_LONG_PREFIX_MASK 0x7f
|
||||||
|
|
||||||
|
/* long xattr name prefix */
|
||||||
|
struct erofs_xattr_long_prefix {
|
||||||
|
uint8_t base_index; /* short xattr name prefix index */
|
||||||
|
char infix[0]; /* infix apart from short prefix */
|
||||||
|
} __packed;
|
||||||
|
|
||||||
|
/* xattr entry (for both inline & shared xattrs) */
|
||||||
|
struct erofs_xattr_entry {
|
||||||
|
uint8_t e_name_len;
|
||||||
|
uint8_t e_name_index;
|
||||||
|
uint16_t e_value_size;
|
||||||
|
char e_name[]; /* attribute name */
|
||||||
|
} __packed;
|
||||||
|
|
||||||
|
#define EROFS_XATTR_ALIGN(size) \
|
||||||
|
(((size) + sizeof(struct erofs_xattr_entry) - 1) & \
|
||||||
|
~(sizeof(struct erofs_xattr_entry) - 1))
|
||||||
|
|
||||||
|
static inline unsigned int
|
||||||
|
erofs_xattr_entry_size(const struct erofs_xattr_entry *entry)
|
||||||
|
{
|
||||||
|
return (EROFS_XATTR_ALIGN(
|
||||||
|
sizeof(*entry) + entry->e_name_len + le16toh(entry->e_value_size)));
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline unsigned int
|
||||||
|
erofs_xattr_ibody_size(uint16_t i_xattr_icount)
|
||||||
|
{
|
||||||
|
if (!i_xattr_icount)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
/* 1 header + n-1 * 4 bytes inline xattr to keep continuity */
|
||||||
|
return (sizeof(struct erofs_xattr_ibody_header) +
|
||||||
|
sizeof(uint32_t) * (le16toh(i_xattr_icount) - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* compression algorithm types (for h_algorithmtype) */
|
||||||
|
enum {
|
||||||
|
Z_EROFS_COMPRESSION_LZ4 = 0,
|
||||||
|
Z_EROFS_COMPRESSION_LZMA = 1,
|
||||||
|
Z_EROFS_COMPRESSION_DEFLATE = 2,
|
||||||
|
Z_EROFS_COMPRESSION_ZSTD = 3,
|
||||||
|
Z_EROFS_COMPRESSION_MAX
|
||||||
|
};
|
||||||
|
#define Z_EROFS_ALL_COMPR_ALGS ((1 << Z_EROFS_COMPRESSION_MAX) - 1)
|
||||||
|
|
||||||
|
#define Z_EROFS_PCLUSTER_MAX_SIZE (1024 * 1024)
|
||||||
|
#define Z_EROFS_PCLUSTER_MAX_DSIZE (12 * 1024 * 1024)
|
||||||
|
|
||||||
|
/* 14 bytes (+ length field = 16 bytes) */
|
||||||
|
struct z_erofs_lz4_cfgs {
|
||||||
|
__le16 max_distance;
|
||||||
|
__le16 max_pclusterblks;
|
||||||
|
uint8_t reserved[10];
|
||||||
|
} __packed;
|
||||||
|
|
||||||
|
/* 14 bytes (+ length field = 16 bytes) */
|
||||||
|
struct z_erofs_lzma_cfgs {
|
||||||
|
__le32 dict_size;
|
||||||
|
__le16 format;
|
||||||
|
uint8_t reserved[8];
|
||||||
|
} __packed;
|
||||||
|
|
||||||
|
#define Z_EROFS_LZMA_MAX_DICT_SIZE (8 * Z_EROFS_PCLUSTER_MAX_SIZE)
|
||||||
|
|
||||||
|
/* 6 bytes (+ length field = 8 bytes) */
|
||||||
|
struct z_erofs_deflate_cfgs {
|
||||||
|
uint8_t windowbits;
|
||||||
|
uint8_t reserved[5];
|
||||||
|
} __packed;
|
||||||
|
|
||||||
|
/* 6 bytes (+ length field = 8 bytes) */
|
||||||
|
struct z_erofs_zstd_cfgs {
|
||||||
|
uint8_t format;
|
||||||
|
uint8_t windowlog;
|
||||||
|
uint8_t reserved[4];
|
||||||
|
} __packed;
|
||||||
|
|
||||||
|
#define Z_EROFS_ZSTD_MAX_DICT_SIZE Z_EROFS_PCLUSTER_MAX_SIZE
|
||||||
|
|
||||||
|
/* z_advise flags */
|
||||||
|
#define Z_EROFS_ADVISE_COMPACTED_2B 0x0001
|
||||||
|
#define Z_EROFS_ADVISE_EXTENTS 0x0001
|
||||||
|
#define Z_EROFS_ADVISE_BIG_PCLUSTER_1 0x0002
|
||||||
|
#define Z_EROFS_ADVISE_BIG_PCLUSTER_2 0x0004
|
||||||
|
#define Z_EROFS_ADVISE_INLINE_PCLUSTER 0x0008
|
||||||
|
#define Z_EROFS_ADVISE_INTERLACED_PCLUSTER 0x0010
|
||||||
|
#define Z_EROFS_ADVISE_FRAGMENT_PCLUSTER 0x0020
|
||||||
|
#define Z_EROFS_ADVISE_EXTRECSZ_BIT 1
|
||||||
|
#define Z_EROFS_ADVISE_EXTRECSZ_MASK 0x3
|
||||||
|
|
||||||
|
#define Z_EROFS_FRAGMENT_INODE_BIT 7
|
||||||
|
|
||||||
|
/* Logical cluster types */
|
||||||
|
enum {
|
||||||
|
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_LCLUSTER_TYPE_MAX
|
||||||
|
};
|
||||||
|
|
||||||
|
#define Z_EROFS_LI_LCLUSTER_TYPE_MASK (Z_EROFS_LCLUSTER_TYPE_MAX - 1)
|
||||||
|
#define Z_EROFS_LI_PARTIAL_REF (1 << 15)
|
||||||
|
#define Z_EROFS_LI_D0_CBLKCNT (1 << 11)
|
||||||
|
|
||||||
|
/* Compression extent index structures */
|
||||||
|
struct z_erofs_lcluster_index {
|
||||||
|
__le16 di_advise;
|
||||||
|
__le16 di_clusterofs;
|
||||||
|
union {
|
||||||
|
__le32 blkaddr;
|
||||||
|
__le16 delta[2];
|
||||||
|
} di_u;
|
||||||
|
} __packed;
|
||||||
|
|
||||||
|
struct z_erofs_map_header {
|
||||||
|
union {
|
||||||
|
__le32 h_fragmentoff;
|
||||||
|
struct {
|
||||||
|
__le16 h_reserved1;
|
||||||
|
__le16 h_idata_size;
|
||||||
|
};
|
||||||
|
__le32 h_extents_lo;
|
||||||
|
};
|
||||||
|
__le16 h_advise;
|
||||||
|
union {
|
||||||
|
struct {
|
||||||
|
uint8_t h_algorithmtype;
|
||||||
|
uint8_t h_clusterbits;
|
||||||
|
} __packed;
|
||||||
|
__le16 h_extents_hi;
|
||||||
|
} __packed;
|
||||||
|
} __packed;
|
||||||
|
|
||||||
|
#define Z_EROFS_MAP_HEADER_END(end) \
|
||||||
|
(roundup2((end), 8) + sizeof(struct z_erofs_map_header))
|
||||||
|
#define Z_EROFS_FULL_INDEX_START(end) (Z_EROFS_MAP_HEADER_END(end) + 8)
|
||||||
|
|
||||||
|
#define Z_EROFS_EXTENT_PLEN_PARTIAL (1U << 27)
|
||||||
|
#define Z_EROFS_EXTENT_PLEN_FMT_BIT 28
|
||||||
|
#define Z_EROFS_EXTENT_PLEN_MASK ((Z_EROFS_PCLUSTER_MAX_SIZE << 1) - 1)
|
||||||
|
struct z_erofs_extent {
|
||||||
|
__le32 plen;
|
||||||
|
__le32 pstart_lo;
|
||||||
|
__le32 pstart_hi;
|
||||||
|
__le32 lstart_lo;
|
||||||
|
__le32 lstart_hi;
|
||||||
|
uint8_t reserved[12];
|
||||||
|
} __packed;
|
||||||
|
|
||||||
|
static inline unsigned int
|
||||||
|
z_erofs_extent_recsize(unsigned int advise)
|
||||||
|
{
|
||||||
|
return (4U << ((advise >> Z_EROFS_ADVISE_EXTRECSZ_BIT) &
|
||||||
|
Z_EROFS_ADVISE_EXTRECSZ_MASK));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chunk-based file definitions */
|
||||||
|
#define EROFS_CHUNK_FORMAT_BLKBITS_MASK 0x001F
|
||||||
|
#define EROFS_CHUNK_FORMAT_INDEXES 0x0020
|
||||||
|
#define EROFS_CHUNK_FORMAT_48BIT 0x0040
|
||||||
|
#define EROFS_CHUNK_FORMAT_ALL ((EROFS_CHUNK_FORMAT_48BIT << 1) - 1)
|
||||||
|
#define EROFS_CHUNK_FORMAT_INDEXES_FLAG EROFS_CHUNK_FORMAT_INDEXES
|
||||||
|
#define EROFS_BLOCK_MAP_ENTRY_SIZE sizeof(__le32)
|
||||||
|
|
||||||
|
struct erofs_inode_chunk_index {
|
||||||
|
__le16 startblk_hi;
|
||||||
|
__le16 device_id;
|
||||||
|
__le32 startblk_lo;
|
||||||
|
} __packed;
|
||||||
|
|
||||||
|
/* Device table slot (128 bytes) */
|
||||||
|
#define EROFS_DEVT_SLOT_SIZE 128
|
||||||
|
struct erofs_deviceslot {
|
||||||
|
uint8_t tag[64];
|
||||||
|
__le32 blocks_lo;
|
||||||
|
__le32 uniaddr_lo;
|
||||||
|
__le16 blocks_hi;
|
||||||
|
__le16 uniaddr_hi;
|
||||||
|
uint8_t reserved[52];
|
||||||
|
} __packed;
|
||||||
|
|
||||||
|
_Static_assert(sizeof(struct erofs_super_block) == 144,
|
||||||
|
"EROFS super block ABI size");
|
||||||
|
_Static_assert(sizeof(struct erofs_inode_compact) == 32,
|
||||||
|
"EROFS compact inode ABI size");
|
||||||
|
_Static_assert(sizeof(struct erofs_inode_extended) == 64,
|
||||||
|
"EROFS extended inode ABI size");
|
||||||
|
_Static_assert(sizeof(struct erofs_xattr_ibody_header) == 12,
|
||||||
|
"EROFS xattr ibody header ABI size");
|
||||||
|
_Static_assert(sizeof(struct erofs_xattr_entry) == 4,
|
||||||
|
"EROFS xattr entry ABI size");
|
||||||
|
_Static_assert(sizeof(struct erofs_inode_chunk_info) == 4,
|
||||||
|
"EROFS chunk info ABI size");
|
||||||
|
_Static_assert(sizeof(struct erofs_inode_chunk_index) == 8,
|
||||||
|
"EROFS chunk index ABI size");
|
||||||
|
_Static_assert(sizeof(struct z_erofs_map_header) == 8,
|
||||||
|
"EROFS zmap header ABI size");
|
||||||
|
_Static_assert(sizeof(struct z_erofs_lcluster_index) == 8,
|
||||||
|
"EROFS lcluster index ABI size");
|
||||||
|
_Static_assert(sizeof(struct z_erofs_extent) == 32,
|
||||||
|
"EROFS compression extent ABI size");
|
||||||
|
_Static_assert(sizeof(struct erofs_dirent) == 12,
|
||||||
|
"EROFS dirent ABI size");
|
||||||
|
_Static_assert(sizeof(struct erofs_deviceslot) == EROFS_DEVT_SLOT_SIZE,
|
||||||
|
"EROFS device slot ABI size");
|
||||||
|
_Static_assert(__builtin_offsetof(struct erofs_super_block, extra_devices) == 86,
|
||||||
|
"EROFS extra device count ABI offset");
|
||||||
|
_Static_assert(__builtin_offsetof(struct erofs_super_block, devt_slotoff) == 88,
|
||||||
|
"EROFS device table slot offset ABI offset");
|
||||||
|
_Static_assert(__builtin_offsetof(struct erofs_super_block, rootnid_8b) == 112,
|
||||||
|
"EROFS 48-bit root nid ABI offset");
|
||||||
|
_Static_assert(__builtin_offsetof(struct erofs_super_block, metabox_nid) == 128,
|
||||||
|
"EROFS metabox nid ABI offset");
|
||||||
|
_Static_assert(__builtin_offsetof(struct erofs_inode_compact, i_u) == 16,
|
||||||
|
"EROFS compact inode union ABI offset");
|
||||||
|
_Static_assert(__builtin_offsetof(struct erofs_inode_extended, i_u) == 16,
|
||||||
|
"EROFS extended inode union ABI offset");
|
||||||
|
_Static_assert(__builtin_offsetof(struct erofs_inode_extended, i_reserved2) == 48,
|
||||||
|
"EROFS extended inode reserved ABI offset");
|
||||||
|
_Static_assert(__builtin_offsetof(struct erofs_inode_chunk_index, device_id) == 2,
|
||||||
|
"EROFS chunk device id ABI offset");
|
||||||
|
_Static_assert(__builtin_offsetof(struct erofs_inode_chunk_index, startblk_lo) == 4,
|
||||||
|
"EROFS chunk start block ABI offset");
|
||||||
|
_Static_assert(__builtin_offsetof(struct z_erofs_map_header, h_advise) == 4,
|
||||||
|
"EROFS zmap advise ABI offset");
|
||||||
|
_Static_assert(__builtin_offsetof(struct erofs_deviceslot, blocks_lo) == 64,
|
||||||
|
"EROFS device blocks ABI offset");
|
||||||
|
_Static_assert(__builtin_offsetof(struct erofs_deviceslot, uniaddr_lo) == 68,
|
||||||
|
"EROFS device unified address ABI offset");
|
||||||
|
_Static_assert(__builtin_offsetof(struct erofs_deviceslot, blocks_hi) == 72,
|
||||||
|
"EROFS device blocks high ABI offset");
|
||||||
|
_Static_assert(__builtin_offsetof(struct erofs_deviceslot, uniaddr_hi) == 74,
|
||||||
|
"EROFS device unified address high ABI offset");
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,468 @@
|
|||||||
|
/* SPDX-License-Identifier: BSD-2-Clause */
|
||||||
|
|
||||||
|
#include <sys/param.h>
|
||||||
|
#include <sys/systm.h>
|
||||||
|
#include <sys/dirent.h>
|
||||||
|
#include <sys/extattr.h>
|
||||||
|
#include <sys/kernel.h>
|
||||||
|
#include <sys/libkern.h>
|
||||||
|
#include <sys/limits.h>
|
||||||
|
#include <sys/malloc.h>
|
||||||
|
#include <sys/namei.h>
|
||||||
|
#include <sys/proc.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/unistd.h>
|
||||||
|
#include <sys/vnode.h>
|
||||||
|
#include <sys/acl.h>
|
||||||
|
|
||||||
|
#include <vm/vnode_pager.h>
|
||||||
|
|
||||||
|
#include "internal.h"
|
||||||
|
#include "xattr.h"
|
||||||
|
|
||||||
|
static vop_access_t erofs_access;
|
||||||
|
static vop_aclcheck_t erofs_aclcheck;
|
||||||
|
static vop_bmap_t erofs_bmap;
|
||||||
|
static vop_deleteextattr_t erofs_deleteextattr;
|
||||||
|
/* vop_fhtovp removed in FreeBSD 15.0 */
|
||||||
|
static vop_getacl_t erofs_vop_getacl;
|
||||||
|
static vop_getextattr_t erofs_getextattr;
|
||||||
|
static vop_getattr_t erofs_getattr;
|
||||||
|
static vop_inactive_t erofs_inactive;
|
||||||
|
static vop_listextattr_t erofs_listextattr;
|
||||||
|
static vop_open_t erofs_open;
|
||||||
|
static vop_pathconf_t erofs_pathconf;
|
||||||
|
static vop_read_t erofs_read;
|
||||||
|
static vop_readdir_t erofs_readdir;
|
||||||
|
static vop_readlink_t erofs_readlink;
|
||||||
|
static vop_reclaim_t erofs_reclaim;
|
||||||
|
static vop_setacl_t erofs_setacl;
|
||||||
|
static vop_setattr_t erofs_setattr;
|
||||||
|
static vop_setextattr_t erofs_setextattr;
|
||||||
|
static vop_vptofh_t erofs_vptofh;
|
||||||
|
|
||||||
|
/* Access check: data nodes are read-only, but device/FIFO nodes are not denied
|
||||||
|
* writes. */
|
||||||
|
static int
|
||||||
|
erofs_access(struct vop_access_args *ap)
|
||||||
|
{
|
||||||
|
struct vnode *vp;
|
||||||
|
struct erofs_node *en;
|
||||||
|
struct acl *acl;
|
||||||
|
accmode_t accmode;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
vp = ap->a_vp;
|
||||||
|
en = VTOE(vp);
|
||||||
|
accmode = ap->a_accmode;
|
||||||
|
if ((accmode & VMODIFY_PERMS) != 0) {
|
||||||
|
switch (vp->v_type) {
|
||||||
|
case VDIR:
|
||||||
|
case VLNK:
|
||||||
|
case VREG:
|
||||||
|
return (EROFS);
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error = vfs_unixify_accmode(&accmode);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if ((vp->v_mount->mnt_flag & MNT_ACLS) == 0)
|
||||||
|
return (vaccess(vp->v_type, en->mode & ALLPERMS, en->uid,
|
||||||
|
en->gid, accmode, ap->a_cred));
|
||||||
|
|
||||||
|
acl = acl_alloc(M_WAITOK);
|
||||||
|
error = erofs_get_acl(vp, ACL_TYPE_ACCESS, acl);
|
||||||
|
if (error == 0)
|
||||||
|
error = vaccess_acl_posix1e(vp->v_type, en->uid, en->gid, acl,
|
||||||
|
accmode, ap->a_cred);
|
||||||
|
acl_free(acl);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Tell the generic pager that EROFS does not provide block-level bmap.
|
||||||
|
*
|
||||||
|
* Returning EOPNOTSUPP prevents the pager from assuming a bufobj/strategy is
|
||||||
|
* available, avoiding “No strategy for buffer” errors. VM will correctly
|
||||||
|
* fall back to the VOP_READ-based page-in path.
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
erofs_bmap(struct vop_bmap_args *ap)
|
||||||
|
{
|
||||||
|
(void)ap;
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No dirty writeback on last ref release, so inactive is a no-op. */
|
||||||
|
static int
|
||||||
|
erofs_inactive(struct vop_inactive_args *ap)
|
||||||
|
{
|
||||||
|
(void)ap;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create VM object when opening a regular vnode.
|
||||||
|
*
|
||||||
|
* The FreeBSD local vnode pager services synchronous and asynchronous faults
|
||||||
|
* through VOP_READ without requiring a block strategy method.
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
erofs_open(struct vop_open_args *ap)
|
||||||
|
{
|
||||||
|
struct vnode *vp;
|
||||||
|
struct erofs_node *en;
|
||||||
|
|
||||||
|
vp = ap->a_vp;
|
||||||
|
en = VTOE(vp);
|
||||||
|
if (VN_ISDEV(vp))
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
if (vp->v_type == VREG) {
|
||||||
|
if (vnode_create_vobject(vp, en->size, ap->a_td) != 0)
|
||||||
|
return (ENOMEM);
|
||||||
|
}
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_getattr(struct vop_getattr_args *ap)
|
||||||
|
{
|
||||||
|
struct vnode *vp;
|
||||||
|
struct erofs_node *en;
|
||||||
|
struct erofs_mount *em;
|
||||||
|
struct vattr *vap;
|
||||||
|
|
||||||
|
vp = ap->a_vp;
|
||||||
|
en = VTOE(vp);
|
||||||
|
em = MTOE(vp->v_mount);
|
||||||
|
vap = ap->a_vap;
|
||||||
|
VATTR_NULL(vap);
|
||||||
|
vap->va_type = vp->v_type;
|
||||||
|
vap->va_mode = en->mode & ALLPERMS;
|
||||||
|
vap->va_nlink = en->nlink;
|
||||||
|
vap->va_uid = en->uid;
|
||||||
|
vap->va_gid = en->gid;
|
||||||
|
vap->va_fsid = vp->v_mount->mnt_stat.f_fsid.val[0];
|
||||||
|
vap->va_fileid = en->nid;
|
||||||
|
vap->va_size = en->size;
|
||||||
|
vap->va_blocksize = em->block_size;
|
||||||
|
vap->va_atime.tv_sec = en->mtime;
|
||||||
|
vap->va_mtime.tv_sec = en->mtime;
|
||||||
|
vap->va_ctime.tv_sec = en->mtime;
|
||||||
|
vap->va_atime.tv_nsec = en->mtime_nsec;
|
||||||
|
vap->va_mtime.tv_nsec = en->mtime_nsec;
|
||||||
|
vap->va_ctime.tv_nsec = en->mtime_nsec;
|
||||||
|
vap->va_gen = en->generation;
|
||||||
|
vap->va_flags = 0;
|
||||||
|
vap->va_rdev = VN_ISDEV(vp) ? en->rdev : NODEV;
|
||||||
|
if (en->data_blocks > (UINT64_MAX >> em->block_bits))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
vap->va_bytes = en->data_blocks << em->block_bits;
|
||||||
|
vap->va_filerev = 0;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Read-only xattr get entry point.
|
||||||
|
*
|
||||||
|
* Delegates to erofs_getxattr() for two namespaces:
|
||||||
|
* - EXTATTR_NAMESPACE_USER
|
||||||
|
* - EXTATTR_NAMESPACE_SYSTEM (trusted.* / security.*)
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
erofs_getextattr(struct vop_getextattr_args *ap)
|
||||||
|
{
|
||||||
|
int error;
|
||||||
|
|
||||||
|
if (VN_ISDEV(ap->a_vp))
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
error = extattr_check_cred(ap->a_vp, ap->a_attrnamespace, ap->a_cred,
|
||||||
|
ap->a_td, VREAD);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (ap->a_name == NULL || ap->a_name[0] == '\0')
|
||||||
|
return (EINVAL);
|
||||||
|
if (strlen(ap->a_name) > EXTATTR_MAXNAMELEN)
|
||||||
|
return (EINVAL);
|
||||||
|
|
||||||
|
switch (ap->a_attrnamespace) {
|
||||||
|
case EXTATTR_NAMESPACE_USER:
|
||||||
|
case EXTATTR_NAMESPACE_SYSTEM:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (erofs_getxattr(ap->a_vp, ap->a_attrnamespace, ap->a_name,
|
||||||
|
ap->a_uio, ap->a_size));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Read-only xattr list entry point.
|
||||||
|
*
|
||||||
|
* Delegates to erofs_listxattr() for two namespaces:
|
||||||
|
* - EXTATTR_NAMESPACE_USER
|
||||||
|
* - EXTATTR_NAMESPACE_SYSTEM (trusted.* / security.*)
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
erofs_listextattr(struct vop_listextattr_args *ap)
|
||||||
|
{
|
||||||
|
int error;
|
||||||
|
|
||||||
|
if (VN_ISDEV(ap->a_vp))
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
error = extattr_check_cred(ap->a_vp, ap->a_attrnamespace, ap->a_cred,
|
||||||
|
ap->a_td, VREAD);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
|
||||||
|
switch (ap->a_attrnamespace) {
|
||||||
|
case EXTATTR_NAMESPACE_USER:
|
||||||
|
case EXTATTR_NAMESPACE_SYSTEM:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (erofs_listxattr(ap->a_vp, ap->a_attrnamespace, ap->a_uio,
|
||||||
|
ap->a_size));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_deleteextattr(struct vop_deleteextattr_args *ap)
|
||||||
|
{
|
||||||
|
if (VN_ISDEV(ap->a_vp))
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
return (EROFS);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_setextattr(struct vop_setextattr_args *ap)
|
||||||
|
{
|
||||||
|
if (VN_ISDEV(ap->a_vp))
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
return (EROFS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* EROFS is read-only; mutations on regular files/dirs/symlinks are denied, size
|
||||||
|
* changes on special vnodes are treated as no-ops per read-only convention. */
|
||||||
|
static int
|
||||||
|
erofs_setattr(struct vop_setattr_args *ap)
|
||||||
|
{
|
||||||
|
struct vnode *vp;
|
||||||
|
struct vattr *vap;
|
||||||
|
|
||||||
|
vp = ap->a_vp;
|
||||||
|
vap = ap->a_vap;
|
||||||
|
if (vap->va_mode != (mode_t)VNOVAL || vap->va_uid != (uid_t)VNOVAL ||
|
||||||
|
vap->va_gid != (gid_t)VNOVAL || vap->va_atime.tv_sec != VNOVAL ||
|
||||||
|
vap->va_atime.tv_nsec != VNOVAL || vap->va_mtime.tv_sec != VNOVAL ||
|
||||||
|
vap->va_mtime.tv_nsec != VNOVAL || vap->va_flags != VNOVAL)
|
||||||
|
return (EROFS);
|
||||||
|
if (vap->va_size != VNOVAL) {
|
||||||
|
switch (vp->v_type) {
|
||||||
|
case VDIR:
|
||||||
|
return (EISDIR);
|
||||||
|
case VLNK:
|
||||||
|
case VREG:
|
||||||
|
return (EROFS);
|
||||||
|
case VCHR:
|
||||||
|
case VBLK:
|
||||||
|
case VSOCK:
|
||||||
|
case VFIFO:
|
||||||
|
case VNON:
|
||||||
|
case VBAD:
|
||||||
|
case VMARKER:
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_read(struct vop_read_args *ap)
|
||||||
|
{
|
||||||
|
switch (ap->a_vp->v_type) {
|
||||||
|
case VREG:
|
||||||
|
return (erofs_read_file(ap->a_vp, ap->a_uio, ap->a_ioflag));
|
||||||
|
case VDIR:
|
||||||
|
return (EISDIR);
|
||||||
|
default:
|
||||||
|
return (EINVAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_readdir(struct vop_readdir_args *ap)
|
||||||
|
{
|
||||||
|
if (ap->a_vp->v_type != VDIR)
|
||||||
|
return (ENOTDIR);
|
||||||
|
return (erofs_readdir_block(ap->a_vp, ap->a_uio, ap->a_eofflag,
|
||||||
|
ap->a_ncookies, ap->a_cookies));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_readlink(struct vop_readlink_args *ap)
|
||||||
|
{
|
||||||
|
if (ap->a_vp->v_type != VLNK)
|
||||||
|
return (EINVAL);
|
||||||
|
return (erofs_readlink_target(ap->a_vp, ap->a_uio));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_pathconf(struct vop_pathconf_args *ap)
|
||||||
|
{
|
||||||
|
switch (ap->a_name) {
|
||||||
|
case _PC_NAME_MAX:
|
||||||
|
*ap->a_retval = EROFS_NAME_LEN;
|
||||||
|
return (0);
|
||||||
|
case _PC_PATH_MAX:
|
||||||
|
*ap->a_retval = PATH_MAX;
|
||||||
|
return (0);
|
||||||
|
case _PC_FILESIZEBITS:
|
||||||
|
*ap->a_retval = 64;
|
||||||
|
return (0);
|
||||||
|
case _PC_LINK_MAX:
|
||||||
|
*ap->a_retval = INT_MAX;
|
||||||
|
return (0);
|
||||||
|
case _PC_CHOWN_RESTRICTED:
|
||||||
|
case _PC_NO_TRUNC:
|
||||||
|
*ap->a_retval = 1;
|
||||||
|
return (0);
|
||||||
|
case _PC_ACL_EXTENDED:
|
||||||
|
*ap->a_retval =
|
||||||
|
((ap->a_vp->v_mount->mnt_flag & MNT_ACLS) != 0) ? 1 : 0;
|
||||||
|
return (0);
|
||||||
|
case _PC_ACL_PATH_MAX:
|
||||||
|
*ap->a_retval =
|
||||||
|
((ap->a_vp->v_mount->mnt_flag & MNT_ACLS) != 0) ?
|
||||||
|
ACL_MAX_ENTRIES : 3;
|
||||||
|
return (0);
|
||||||
|
case _PC_ACL_NFS4:
|
||||||
|
*ap->a_retval = 0;
|
||||||
|
return (0);
|
||||||
|
default:
|
||||||
|
return (vop_stdpathconf(ap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_vop_getacl(struct vop_getacl_args *ap)
|
||||||
|
{
|
||||||
|
if (VN_ISDEV(ap->a_vp))
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
if ((ap->a_vp->v_mount->mnt_flag & MNT_ACLS) == 0)
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
return (erofs_get_acl(ap->a_vp, ap->a_type, ap->a_aclp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_aclcheck(struct vop_aclcheck_args *ap)
|
||||||
|
{
|
||||||
|
if (VN_ISDEV(ap->a_vp))
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
if ((ap->a_vp->v_mount->mnt_flag & MNT_ACLS) == 0)
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
if (ap->a_aclp == NULL)
|
||||||
|
return (EINVAL);
|
||||||
|
switch (ap->a_type) {
|
||||||
|
case ACL_TYPE_ACCESS:
|
||||||
|
break;
|
||||||
|
case ACL_TYPE_DEFAULT:
|
||||||
|
if (ap->a_vp->v_type != VDIR)
|
||||||
|
return (EINVAL);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return (EINVAL);
|
||||||
|
}
|
||||||
|
return (acl_posix1e_check(ap->a_aclp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_setacl(struct vop_setacl_args *ap)
|
||||||
|
{
|
||||||
|
if (VN_ISDEV(ap->a_vp))
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
return (EROFS);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_reclaim(struct vop_reclaim_args *ap)
|
||||||
|
{
|
||||||
|
struct vnode *vp;
|
||||||
|
struct erofs_node *en;
|
||||||
|
|
||||||
|
vp = ap->a_vp;
|
||||||
|
en = VTOE(vp);
|
||||||
|
if (en != NULL) {
|
||||||
|
vfs_hash_remove(vp);
|
||||||
|
free(en, M_EROFS);
|
||||||
|
vp->v_data = NULL;
|
||||||
|
}
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vnode pointer to persistent EROFS file handle. */
|
||||||
|
static int
|
||||||
|
erofs_vptofh(struct vop_vptofh_args *ap)
|
||||||
|
{
|
||||||
|
struct erofs_fid efid;
|
||||||
|
struct erofs_node *en;
|
||||||
|
|
||||||
|
en = VTOE(ap->a_vp);
|
||||||
|
bzero(&efid, sizeof(efid));
|
||||||
|
efid.len = sizeof(efid);
|
||||||
|
efid.nid_hi = en->nid >> 32;
|
||||||
|
efid.nid_lo = en->nid;
|
||||||
|
efid.gen = en->generation;
|
||||||
|
memcpy(ap->a_fhp, &efid, sizeof(efid));
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct vop_vector erofs_vnodeops = {
|
||||||
|
.vop_default = &default_vnodeops,
|
||||||
|
.vop_access = erofs_access,
|
||||||
|
.vop_aclcheck = erofs_aclcheck,
|
||||||
|
.vop_bmap = erofs_bmap,
|
||||||
|
.vop_cachedlookup = erofs_lookup,
|
||||||
|
.vop_deleteextattr = erofs_deleteextattr,
|
||||||
|
.vop_getacl = erofs_vop_getacl,
|
||||||
|
.vop_getextattr = erofs_getextattr,
|
||||||
|
.vop_getattr = erofs_getattr,
|
||||||
|
.vop_getpages = vnode_pager_local_getpages,
|
||||||
|
.vop_getpages_async = vnode_pager_local_getpages_async,
|
||||||
|
.vop_inactive = erofs_inactive,
|
||||||
|
.vop_listextattr = erofs_listextattr,
|
||||||
|
.vop_lookup = vfs_cache_lookup,
|
||||||
|
.vop_open = erofs_open,
|
||||||
|
.vop_pathconf = erofs_pathconf,
|
||||||
|
.vop_read = erofs_read,
|
||||||
|
.vop_readdir = erofs_readdir,
|
||||||
|
.vop_readlink = erofs_readlink,
|
||||||
|
.vop_reclaim = erofs_reclaim,
|
||||||
|
.vop_setacl = erofs_setacl,
|
||||||
|
.vop_setattr = erofs_setattr,
|
||||||
|
.vop_setextattr = erofs_setextattr,
|
||||||
|
.vop_vptofh = erofs_vptofh,
|
||||||
|
};
|
||||||
|
VFS_VOP_VECTOR_REGISTER(erofs_vnodeops);
|
||||||
|
|
||||||
|
struct vop_vector erofs_fifoops = {
|
||||||
|
.vop_default = &fifo_specops,
|
||||||
|
.vop_access = erofs_access,
|
||||||
|
.vop_aclcheck = erofs_aclcheck,
|
||||||
|
.vop_deleteextattr = erofs_deleteextattr,
|
||||||
|
.vop_getacl = erofs_vop_getacl,
|
||||||
|
.vop_getextattr = erofs_getextattr,
|
||||||
|
.vop_getattr = erofs_getattr,
|
||||||
|
.vop_listextattr = erofs_listextattr,
|
||||||
|
.vop_pathconf = erofs_pathconf,
|
||||||
|
.vop_reclaim = erofs_reclaim,
|
||||||
|
.vop_setacl = erofs_setacl,
|
||||||
|
.vop_setattr = erofs_setattr,
|
||||||
|
.vop_setextattr = erofs_setextattr,
|
||||||
|
.vop_vptofh = erofs_vptofh,
|
||||||
|
};
|
||||||
|
VFS_VOP_VECTOR_REGISTER(erofs_fifoops);
|
||||||
+481
@@ -0,0 +1,481 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017-2018 HUAWEI, Inc.
|
||||||
|
* https://www.huawei.com/
|
||||||
|
* Copyright (C) 2021, Alibaba Cloud
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <sys/param.h>
|
||||||
|
#include <sys/endian.h>
|
||||||
|
#include <sys/systm.h>
|
||||||
|
#include <sys/kernel.h>
|
||||||
|
#include <sys/libkern.h>
|
||||||
|
#include <sys/limits.h>
|
||||||
|
#include <sys/malloc.h>
|
||||||
|
#include <sys/mount.h>
|
||||||
|
#include <sys/fnv_hash.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/vnode.h>
|
||||||
|
|
||||||
|
#include "internal.h"
|
||||||
|
|
||||||
|
static bool
|
||||||
|
erofs_is_48bit(const struct erofs_mount *em)
|
||||||
|
{
|
||||||
|
return ((em->feature_incompat & EROFS_FEATURE_INCOMPAT_48BIT) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint64_t
|
||||||
|
erofs_addrmask(const struct erofs_mount *em)
|
||||||
|
{
|
||||||
|
if (erofs_is_48bit(em))
|
||||||
|
return ((1ULL << 48) - 1);
|
||||||
|
return (UINT32_MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
static dev_t
|
||||||
|
erofs_decode_dev(uint32_t dev)
|
||||||
|
{
|
||||||
|
unsigned int major, minor;
|
||||||
|
|
||||||
|
major = (dev & 0xfff00) >> 8;
|
||||||
|
minor = (dev & 0xff) | ((dev >> 12) & 0xfff00);
|
||||||
|
return (makedev(major, minor));
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t
|
||||||
|
erofs_inode_generation(const struct erofs_mount *em, uint64_t nid,
|
||||||
|
const void *inode, size_t inode_size)
|
||||||
|
{
|
||||||
|
uint8_t encoded_nid[sizeof(nid)];
|
||||||
|
uint32_t generation;
|
||||||
|
|
||||||
|
le64enc(encoded_nid, nid);
|
||||||
|
generation = fnv_32_buf(encoded_nid, sizeof(encoded_nid),
|
||||||
|
em->generation_seed);
|
||||||
|
generation = fnv_32_buf(inode, inode_size, generation);
|
||||||
|
return (generation != 0 ? generation : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_set_timestamp(struct erofs_node *en, uint64_t seconds,
|
||||||
|
uint32_t nanoseconds)
|
||||||
|
{
|
||||||
|
if (nanoseconds >= 1000000000 || seconds > (uint64_t)INT64_MAX)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
en->mtime = seconds;
|
||||||
|
en->mtime_nsec = nanoseconds;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_set_data_blocks(const struct erofs_mount *em, struct erofs_node *en,
|
||||||
|
uint64_t compressed_blocks)
|
||||||
|
{
|
||||||
|
if (en->datalayout == EROFS_INODE_COMPRESSED_FULL ||
|
||||||
|
en->datalayout == EROFS_INODE_COMPRESSED_COMPACT) {
|
||||||
|
en->data_blocks = compressed_blocks;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
if (en->size == 0) {
|
||||||
|
en->data_blocks = 0;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
if (en->size > UINT64_MAX - (em->block_size - 1))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
en->data_blocks = roundup2(en->size, (uint64_t)em->block_size) >>
|
||||||
|
em->block_bits;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_validate_inline_data(const struct erofs_mount *em,
|
||||||
|
const struct erofs_node *en)
|
||||||
|
{
|
||||||
|
uint64_t image_size, inline_end, inline_off, inline_size, tail_start;
|
||||||
|
|
||||||
|
if (en->datalayout != EROFS_INODE_FLAT_INLINE || en->size == 0)
|
||||||
|
return (0);
|
||||||
|
tail_start = roundup2(en->size, (uint64_t)em->block_size) -
|
||||||
|
em->block_size;
|
||||||
|
inline_size = en->size - tail_start;
|
||||||
|
if (__builtin_add_overflow(en->inode_off, en->inode_isize, &inline_off) ||
|
||||||
|
__builtin_add_overflow(inline_off, en->xattr_isize, &inline_off) ||
|
||||||
|
__builtin_add_overflow(inline_off, inline_size, &inline_end))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
if ((inline_off & (em->block_size - 1)) + inline_size > em->block_size)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
if (erofs_nid_in_metabox(en->nid)) {
|
||||||
|
if (em->metabox_en == NULL || inline_end > em->metabox_en->size)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
if (em->blocks > (UINT64_MAX >> em->block_bits))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
image_size = em->blocks << em->block_bits;
|
||||||
|
if (inline_end > image_size || inline_end > em->dif0.mediasize)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convert a logical nid to its inode-table byte offset. Normal NIDs are
|
||||||
|
* relative to the primary metadata area. For metabox NIDs, bit 63 selects
|
||||||
|
* the metabox backing inode and the remaining bits are relative to its data.
|
||||||
|
* EROFS_NULL_ADDR is returned when the address cannot be represented.
|
||||||
|
*/
|
||||||
|
uint64_t
|
||||||
|
erofs_iloc(struct erofs_mount *em, uint64_t nid)
|
||||||
|
{
|
||||||
|
uint64_t meta_offset, nid_lo, result;
|
||||||
|
bool in_metabox;
|
||||||
|
|
||||||
|
in_metabox = erofs_nid_in_metabox(nid);
|
||||||
|
if (in_metabox && !erofs_sb_has_metabox(em))
|
||||||
|
return (EROFS_NULL_ADDR);
|
||||||
|
nid_lo = nid & EROFS_DIRENT_NID_MASK;
|
||||||
|
if (nid_lo > (UINT64_MAX >> 5))
|
||||||
|
return (EROFS_NULL_ADDR);
|
||||||
|
result = nid_lo << 5;
|
||||||
|
if (in_metabox)
|
||||||
|
return (result);
|
||||||
|
|
||||||
|
if (em->block_bits > 58)
|
||||||
|
return (EROFS_NULL_ADDR);
|
||||||
|
meta_offset = (uint64_t)em->meta_blkaddr << em->block_bits;
|
||||||
|
if (result > UINT64_MAX - meta_offset)
|
||||||
|
return (EROFS_NULL_ADDR);
|
||||||
|
|
||||||
|
return (meta_offset + result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Check that a NID can address at least one compact inode slot without
|
||||||
|
* crossing the declared primary image or metabox backing-file boundary.
|
||||||
|
*/
|
||||||
|
bool
|
||||||
|
erofs_nid_is_valid(struct erofs_mount *em, uint64_t nid)
|
||||||
|
{
|
||||||
|
uint64_t image_size, off;
|
||||||
|
|
||||||
|
off = erofs_iloc(em, nid);
|
||||||
|
if (off == EROFS_NULL_ADDR)
|
||||||
|
return (false);
|
||||||
|
if (erofs_nid_in_metabox(nid)) {
|
||||||
|
if (em->metabox_en == NULL || off > em->metabox_en->size)
|
||||||
|
return (false);
|
||||||
|
return (sizeof(struct erofs_inode_compact) <=
|
||||||
|
em->metabox_en->size - off);
|
||||||
|
}
|
||||||
|
if (em->blocks > (UINT64_MAX >> em->block_bits))
|
||||||
|
return (false);
|
||||||
|
image_size = em->blocks << em->block_bits;
|
||||||
|
if (off > image_size || sizeof(struct erofs_inode_compact) >
|
||||||
|
image_size - off)
|
||||||
|
return (false);
|
||||||
|
if (off > em->dif0.mediasize || sizeof(struct erofs_inode_compact) >
|
||||||
|
em->dif0.mediasize - off)
|
||||||
|
return (false);
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Read and decode a disk inode.
|
||||||
|
*
|
||||||
|
* Currently supports:
|
||||||
|
* - compact / extended inode;
|
||||||
|
* - plain / inline uncompressed layouts;
|
||||||
|
* - basic 48-bit address parsing;
|
||||||
|
* - compact inode epoch/fixed_nsec timestamp semantics;
|
||||||
|
* - dot_omitted / nlink==1 i_format details.
|
||||||
|
*/
|
||||||
|
int
|
||||||
|
erofs_read_inode(struct erofs_mount *em, uint64_t nid, struct erofs_node *en)
|
||||||
|
{
|
||||||
|
struct erofs_inode_compact *dic;
|
||||||
|
struct erofs_inode_extended *die;
|
||||||
|
struct erofs_inode_chunk_info chunk_info;
|
||||||
|
union erofs_inode_i_nb inode_nb;
|
||||||
|
void *buf;
|
||||||
|
uint64_t addrmask, mtime, off, startblk;
|
||||||
|
uint64_t compressed_blocks;
|
||||||
|
uint32_t raw_rdev, startblk_lo;
|
||||||
|
uint16_t ifmt, startblk_hi;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
if (!erofs_nid_is_valid(em, nid))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
off = erofs_iloc(em, nid);
|
||||||
|
error = erofs_read_metadata(em, nid, off,
|
||||||
|
sizeof(struct erofs_inode_compact), &buf);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
|
||||||
|
bzero(&en->size, sizeof(*en) - offsetof(struct erofs_node, size));
|
||||||
|
en->nid = nid;
|
||||||
|
en->inode_off = off;
|
||||||
|
ifmt = le16toh(*(uint16_t *)buf);
|
||||||
|
if ((ifmt & ~EROFS_I_ALL) != 0) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
}
|
||||||
|
en->datalayout = erofs_inode_datalayout(ifmt);
|
||||||
|
if (en->datalayout >= EROFS_INODE_DATALAYOUT_MAX) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
}
|
||||||
|
en->compact_inode = (erofs_inode_version(ifmt) == 0);
|
||||||
|
if (!en->compact_inode) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
error = erofs_read_metadata(em, nid, off,
|
||||||
|
sizeof(struct erofs_inode_extended), &buf);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
addrmask = erofs_addrmask(em);
|
||||||
|
startblk = EROFS_NULL_ADDR;
|
||||||
|
startblk_lo = 0;
|
||||||
|
startblk_hi = 0;
|
||||||
|
compressed_blocks = 0;
|
||||||
|
raw_rdev = 0;
|
||||||
|
bzero(&inode_nb, sizeof(inode_nb));
|
||||||
|
dic = buf;
|
||||||
|
if (en->compact_inode) {
|
||||||
|
en->inode_isize = sizeof(struct erofs_inode_compact);
|
||||||
|
en->generation = erofs_inode_generation(em, nid, buf,
|
||||||
|
en->inode_isize);
|
||||||
|
en->mode = le16toh(dic->i_mode);
|
||||||
|
en->size = le32toh(dic->i_size);
|
||||||
|
en->ino = le32toh(dic->i_ino);
|
||||||
|
en->uid = le16toh(dic->i_uid);
|
||||||
|
en->gid = le16toh(dic->i_gid);
|
||||||
|
en->xattr_isize = erofs_xattr_ibody_size(dic->i_xattr_icount);
|
||||||
|
if (__builtin_add_overflow(em->epoch,
|
||||||
|
(uint64_t)le32toh(dic->i_mtime), &mtime)) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
error = erofs_set_timestamp(en, mtime, em->fixed_nsec);
|
||||||
|
if (error != 0) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
startblk_lo = le32toh(dic->i_u.startblk_lo);
|
||||||
|
compressed_blocks = le32toh(dic->i_u.blocks_lo);
|
||||||
|
raw_rdev = le32toh(dic->i_u.rdev);
|
||||||
|
if (!S_ISDIR(en->mode) &&
|
||||||
|
((ifmt >> EROFS_I_NLINK_1_BIT) & 0x1) != 0) {
|
||||||
|
en->nlink = 1;
|
||||||
|
inode_nb = dic->i_nb;
|
||||||
|
} else {
|
||||||
|
en->nlink = le16toh(dic->i_nb.nlink);
|
||||||
|
addrmask = UINT32_MAX;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
die = buf;
|
||||||
|
en->inode_isize = sizeof(struct erofs_inode_extended);
|
||||||
|
en->generation = erofs_inode_generation(em, nid, buf,
|
||||||
|
en->inode_isize);
|
||||||
|
en->mode = le16toh(die->i_mode);
|
||||||
|
en->size = le64toh(die->i_size);
|
||||||
|
en->ino = le32toh(die->i_ino);
|
||||||
|
en->uid = le32toh(die->i_uid);
|
||||||
|
en->gid = le32toh(die->i_gid);
|
||||||
|
en->nlink = le32toh(die->i_nlink);
|
||||||
|
inode_nb = die->i_nb;
|
||||||
|
en->xattr_isize = erofs_xattr_ibody_size(die->i_xattr_icount);
|
||||||
|
error = erofs_set_timestamp(en, le64toh(die->i_mtime),
|
||||||
|
le32toh(die->i_mtime_nsec));
|
||||||
|
if (error != 0) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
startblk_lo = le32toh(die->i_u.startblk_lo);
|
||||||
|
compressed_blocks = le32toh(die->i_u.blocks_lo);
|
||||||
|
raw_rdev = le32toh(die->i_u.rdev);
|
||||||
|
}
|
||||||
|
startblk_hi = le16toh(inode_nb.startblk_hi);
|
||||||
|
compressed_blocks |= (uint64_t)le16toh(inode_nb.blocks_hi) << 32;
|
||||||
|
if (en->size > (uint64_t)OFF_MAX) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
en->vtype = IFTOVT(en->mode);
|
||||||
|
if (en->mode != 0 && en->vtype == VNON) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
en->inline_data = (en->datalayout == EROFS_INODE_FLAT_INLINE);
|
||||||
|
en->dot_omitted = (en->vtype == VDIR) &&
|
||||||
|
(((ifmt >> EROFS_I_DOT_OMITTED_BIT) & 0x1) != 0);
|
||||||
|
|
||||||
|
if (en->datalayout == EROFS_INODE_COMPRESSED_FULL ||
|
||||||
|
en->datalayout == EROFS_INODE_COMPRESSED_COMPACT) {
|
||||||
|
error = z_erofs_fill_inode(em, en);
|
||||||
|
if (error != 0) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
} else if (en->datalayout == EROFS_INODE_CHUNK_BASED) {
|
||||||
|
if (!erofs_sb_has_chunked_file(em) || en->vtype != VREG) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
if (en->compact_inode)
|
||||||
|
chunk_info = dic->i_u.c;
|
||||||
|
else
|
||||||
|
chunk_info = die->i_u.c;
|
||||||
|
if (le16toh(chunk_info.reserved) != 0) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
en->chunkformat = le16toh(chunk_info.format);
|
||||||
|
if (en->chunkformat & ~EROFS_CHUNK_FORMAT_ALL) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
}
|
||||||
|
if ((en->chunkformat & EROFS_CHUNK_FORMAT_48BIT) != 0 &&
|
||||||
|
(en->chunkformat & EROFS_CHUNK_FORMAT_INDEXES) == 0) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
en->chunkbits = em->block_bits +
|
||||||
|
(en->chunkformat & EROFS_CHUNK_FORMAT_BLKBITS_MASK);
|
||||||
|
if (en->chunkbits >= 64) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
} else if (en->datalayout != EROFS_INODE_FLAT_PLAIN &&
|
||||||
|
en->datalayout != EROFS_INODE_FLAT_INLINE) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (en->vtype) {
|
||||||
|
case VREG:
|
||||||
|
case VDIR:
|
||||||
|
case VLNK:
|
||||||
|
if (en->datalayout == EROFS_INODE_CHUNK_BASED) {
|
||||||
|
en->startblk = EROFS_NULL_ADDR;
|
||||||
|
en->rdev = NODEV;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
startblk = startblk_lo | ((uint64_t)startblk_hi << 32);
|
||||||
|
if (en->datalayout == EROFS_INODE_FLAT_PLAIN &&
|
||||||
|
((startblk ^ EROFS_NULL_ADDR) & addrmask) == 0)
|
||||||
|
startblk = EROFS_NULL_ADDR;
|
||||||
|
en->startblk = startblk;
|
||||||
|
en->rdev = NODEV;
|
||||||
|
break;
|
||||||
|
case VCHR:
|
||||||
|
case VBLK:
|
||||||
|
en->startblk = EROFS_NULL_ADDR;
|
||||||
|
en->rdev = erofs_decode_dev(raw_rdev);
|
||||||
|
break;
|
||||||
|
case VFIFO:
|
||||||
|
case VSOCK:
|
||||||
|
en->startblk = EROFS_NULL_ADDR;
|
||||||
|
en->rdev = NODEV;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
error = erofs_set_data_blocks(em, en, compressed_blocks);
|
||||||
|
if (error == 0)
|
||||||
|
error = erofs_validate_inline_data(em, en);
|
||||||
|
if (error != 0) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static u_int
|
||||||
|
erofs_vfs_hash(uint64_t nid)
|
||||||
|
{
|
||||||
|
|
||||||
|
return (fnv_32_buf(&nid, sizeof(nid), FNV1_32_INIT));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_vfs_hash_cmp(struct vnode *vp, void *pnid)
|
||||||
|
{
|
||||||
|
struct erofs_node *en;
|
||||||
|
|
||||||
|
en = VTOE(vp);
|
||||||
|
return (en == NULL || en->nid != *(uint64_t *)pnid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Get vnode by raw on-disk nid. The raw nid is also the FreeBSD fileid and
|
||||||
|
* hash identity, so the metabox selector bit remains collision-free.
|
||||||
|
* Uses the standard FreeBSD vfs_hash API.
|
||||||
|
* (Linux equivalent: erofs_iget in Linux's inode.c)
|
||||||
|
*/
|
||||||
|
int
|
||||||
|
erofs_vget(struct mount *mp, ino_t ino, int flags, struct vnode **vpp)
|
||||||
|
{
|
||||||
|
struct erofs_mount *em;
|
||||||
|
struct erofs_node *en;
|
||||||
|
struct thread *td;
|
||||||
|
struct vnode *vp;
|
||||||
|
uint64_t nid;
|
||||||
|
u_int hash;
|
||||||
|
bool shared;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
td = curthread;
|
||||||
|
nid = (uint64_t)ino;
|
||||||
|
shared = (flags & LK_TYPE_MASK) == LK_SHARED;
|
||||||
|
hash = erofs_vfs_hash(nid);
|
||||||
|
error = vfs_hash_get(mp, hash, flags, td, vpp, erofs_vfs_hash_cmp,
|
||||||
|
&nid);
|
||||||
|
if (error != 0 || *vpp != NULL)
|
||||||
|
return (error);
|
||||||
|
|
||||||
|
em = MTOE(mp);
|
||||||
|
en = malloc(sizeof(*en), M_EROFS, M_WAITOK | M_ZERO);
|
||||||
|
error = getnewvnode("erofs", mp, &erofs_vnodeops, &vp);
|
||||||
|
if (error != 0) {
|
||||||
|
free(en, M_EROFS);
|
||||||
|
*vpp = NULL;
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
vp->v_data = en;
|
||||||
|
en->vnode = vp;
|
||||||
|
en->nid = nid;
|
||||||
|
lockmgr(vp->v_vnlock, LK_EXCLUSIVE, NULL);
|
||||||
|
error = insmntque(vp, mp);
|
||||||
|
if (error != 0) {
|
||||||
|
free(en, M_EROFS);
|
||||||
|
vp->v_data = NULL;
|
||||||
|
*vpp = NULL;
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
error = vfs_hash_insert(vp, hash, flags, td, vpp, erofs_vfs_hash_cmp,
|
||||||
|
&nid);
|
||||||
|
if (error != 0 || *vpp != NULL)
|
||||||
|
return (error);
|
||||||
|
|
||||||
|
error = erofs_read_inode(em, nid, en);
|
||||||
|
if (error != 0) {
|
||||||
|
*vpp = NULL;
|
||||||
|
vgone(vp);
|
||||||
|
vput(vp);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
vp->v_type = en->vtype;
|
||||||
|
if (vp->v_type == VFIFO)
|
||||||
|
vp->v_op = &erofs_fifoops;
|
||||||
|
if ((uint64_t)ino == em->root_nid)
|
||||||
|
vp->v_vflag |= VV_ROOT;
|
||||||
|
vn_set_state(vp, VSTATE_CONSTRUCTED);
|
||||||
|
if (shared)
|
||||||
|
VOP_LOCK(vp, LK_DOWNGRADE);
|
||||||
|
*vpp = vp;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
+351
@@ -0,0 +1,351 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-2.0-only */
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017-2018 HUAWEI, Inc.
|
||||||
|
* https://www.huawei.com/
|
||||||
|
* Copyright (C) 2021, Alibaba Cloud
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef __EROFS_INTERNAL_H
|
||||||
|
#define __EROFS_INTERNAL_H
|
||||||
|
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/param.h> // MUST FIRST
|
||||||
|
#include <sys/systm.h>
|
||||||
|
#include <sys/malloc.h>
|
||||||
|
#include <sys/mount.h>
|
||||||
|
#include <sys/mutex.h>
|
||||||
|
#include <sys/vnode.h>
|
||||||
|
|
||||||
|
#include "erofs_fs.h"
|
||||||
|
|
||||||
|
MALLOC_DECLARE(M_EROFS);
|
||||||
|
|
||||||
|
struct cdev;
|
||||||
|
struct g_consumer;
|
||||||
|
struct erofs_device_info;
|
||||||
|
|
||||||
|
/* EROFS_SUPER_MAGIC_V1 to represent the whole file system */
|
||||||
|
#define EROFS_SUPER_MAGIC EROFS_SUPER_MAGIC_V1
|
||||||
|
|
||||||
|
typedef uint64_t erofs_nid_t;
|
||||||
|
typedef uint64_t erofs_off_t;
|
||||||
|
typedef uint64_t erofs_blk_t;
|
||||||
|
#define EROFS_FEATURE_FUNCS(name, compat, feature) \
|
||||||
|
static inline bool erofs_sb_has_##name(struct erofs_mount *em) \
|
||||||
|
{ \
|
||||||
|
return ( \
|
||||||
|
(em->feature_##compat & EROFS_FEATURE_##feature) != 0); \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define EROFS_MOUNT_XATTR_USER 0x00000010
|
||||||
|
#define EROFS_MOUNT_POSIX_ACL 0x00000020
|
||||||
|
|
||||||
|
#define clear_opt(opt, option) ((opt)->mount_opt &= ~EROFS_MOUNT_##option)
|
||||||
|
#define set_opt(opt, option) ((opt)->mount_opt |= EROFS_MOUNT_##option)
|
||||||
|
#define test_opt(opt, option) ((opt)->mount_opt & EROFS_MOUNT_##option)
|
||||||
|
|
||||||
|
struct erofs_mount_opts {
|
||||||
|
unsigned int mount_opt;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum {
|
||||||
|
EROFS_SYNC_DECOMPRESS_AUTO,
|
||||||
|
EROFS_SYNC_DECOMPRESS_FORCE_ON,
|
||||||
|
EROFS_SYNC_DECOMPRESS_FORCE_OFF
|
||||||
|
};
|
||||||
|
|
||||||
|
enum {
|
||||||
|
EROFS_ZIP_CACHE_DISABLED,
|
||||||
|
EROFS_ZIP_CACHE_READAHEAD,
|
||||||
|
EROFS_ZIP_CACHE_READAROUND
|
||||||
|
};
|
||||||
|
|
||||||
|
struct erofs_sb_lz4_info {
|
||||||
|
uint16_t max_distance_pages;
|
||||||
|
uint16_t max_pclusterblks;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct erofs_buf {
|
||||||
|
void *base;
|
||||||
|
erofs_off_t off;
|
||||||
|
};
|
||||||
|
#define __EROFS_BUF_INITIALIZER ((struct erofs_buf) { .base = NULL })
|
||||||
|
|
||||||
|
#define EROFS_MAP_MAPPED 0x0001
|
||||||
|
#define EROFS_MAP_META 0x0002
|
||||||
|
#define EROFS_MAP_PARTIAL_MAPPED 0x0004
|
||||||
|
#define EROFS_MAP_PARTIAL_REF 0x0008
|
||||||
|
#define EROFS_MAP_FRAGMENT 0x0010
|
||||||
|
#define EROFS_MAP_FULL(f) \
|
||||||
|
(!((f) & (EROFS_MAP_PARTIAL_MAPPED | EROFS_MAP_PARTIAL_REF)))
|
||||||
|
|
||||||
|
struct erofs_map_blocks {
|
||||||
|
struct erofs_buf buf;
|
||||||
|
|
||||||
|
erofs_off_t m_pa, m_la;
|
||||||
|
uint64_t m_plen, m_llen;
|
||||||
|
|
||||||
|
unsigned short m_deviceid;
|
||||||
|
char m_algorithmformat;
|
||||||
|
unsigned int m_flags;
|
||||||
|
};
|
||||||
|
|
||||||
|
#define EROFS_GET_BLOCKS_FIEMAP 0x0001
|
||||||
|
#define EROFS_GET_BLOCKS_READMORE 0x0002
|
||||||
|
#define EROFS_GET_BLOCKS_FINDTAIL 0x0004
|
||||||
|
|
||||||
|
enum {
|
||||||
|
Z_EROFS_COMPRESSION_SHIFTED = Z_EROFS_COMPRESSION_MAX,
|
||||||
|
Z_EROFS_COMPRESSION_INTERLACED,
|
||||||
|
Z_EROFS_COMPRESSION_RUNTIME_MAX
|
||||||
|
};
|
||||||
|
|
||||||
|
struct erofs_map_dev {
|
||||||
|
struct erofs_mount *m_em;
|
||||||
|
struct erofs_device_info *m_dif;
|
||||||
|
erofs_off_t m_pa;
|
||||||
|
uint64_t m_plen;
|
||||||
|
unsigned int m_deviceid;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct erofs_device_info {
|
||||||
|
struct vnode *devvp;
|
||||||
|
struct cdev *dev;
|
||||||
|
struct g_consumer *cp;
|
||||||
|
erofs_blk_t blocks;
|
||||||
|
erofs_blk_t uniaddr;
|
||||||
|
uint64_t mediasize;
|
||||||
|
uint32_t sectorsize;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct erofs_xattr_prefix_item {
|
||||||
|
uint8_t base_index;
|
||||||
|
uint8_t infix_len;
|
||||||
|
char *infix;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct erofs_mount {
|
||||||
|
struct mount *mnt;
|
||||||
|
struct erofs_device_info dif0;
|
||||||
|
|
||||||
|
uint32_t block_size;
|
||||||
|
uint32_t sb_size;
|
||||||
|
uint8_t block_bits;
|
||||||
|
uint32_t meta_blkaddr;
|
||||||
|
uint32_t xattr_blkaddr;
|
||||||
|
uint32_t xattr_prefix_start;
|
||||||
|
uint8_t xattr_prefix_count;
|
||||||
|
uint64_t packed_nid;
|
||||||
|
uint64_t metabox_nid;
|
||||||
|
struct erofs_node *metabox_en;
|
||||||
|
struct erofs_node *packed_inode;
|
||||||
|
struct erofs_xattr_prefix_item *xattr_prefixes;
|
||||||
|
uint64_t blocks;
|
||||||
|
uint64_t inos;
|
||||||
|
uint64_t root_nid;
|
||||||
|
uint64_t epoch;
|
||||||
|
uint32_t fixed_nsec;
|
||||||
|
uint32_t generation_seed;
|
||||||
|
uint32_t feature_compat;
|
||||||
|
uint32_t feature_incompat;
|
||||||
|
char volume_name[17];
|
||||||
|
|
||||||
|
struct erofs_mount_opts opt;
|
||||||
|
struct erofs_sb_lz4_info lz4;
|
||||||
|
uint16_t available_compr_algs;
|
||||||
|
uint32_t lzma_dict_size;
|
||||||
|
uint8_t deflate_windowbits;
|
||||||
|
uint8_t zstd_windowlog;
|
||||||
|
|
||||||
|
/* Device table */
|
||||||
|
uint16_t extra_devices;
|
||||||
|
uint16_t device_id_mask;
|
||||||
|
bool flatdev;
|
||||||
|
erofs_blk_t total_blocks;
|
||||||
|
erofs_blk_t flatdev_blocks;
|
||||||
|
struct erofs_device_info *devs;
|
||||||
|
struct mtx z_extent_cache_lock;
|
||||||
|
struct erofs_zextent_cache z_extent_cache;
|
||||||
|
bool z_extent_cache_initialized;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct erofs_node {
|
||||||
|
struct vnode *vnode;
|
||||||
|
uint64_t nid;
|
||||||
|
uint64_t size;
|
||||||
|
uint64_t data_blocks;
|
||||||
|
/* Absolute device offset, or metabox-file offset when bit 63 is set. */
|
||||||
|
uint64_t inode_off;
|
||||||
|
uint64_t startblk;
|
||||||
|
uint32_t ino;
|
||||||
|
uint32_t generation;
|
||||||
|
uint32_t nlink;
|
||||||
|
uid_t uid;
|
||||||
|
gid_t gid;
|
||||||
|
mode_t mode;
|
||||||
|
__enum_uint8(vtype) vtype;
|
||||||
|
dev_t rdev;
|
||||||
|
uint64_t mtime;
|
||||||
|
uint32_t mtime_nsec;
|
||||||
|
uint8_t datalayout;
|
||||||
|
uint8_t inode_isize;
|
||||||
|
uint32_t xattr_isize;
|
||||||
|
bool inline_data;
|
||||||
|
bool compact_inode;
|
||||||
|
bool dot_omitted;
|
||||||
|
/* Compression fields */
|
||||||
|
uint16_t z_advise;
|
||||||
|
uint8_t z_algorithmtype[2];
|
||||||
|
uint8_t z_lclusterbits;
|
||||||
|
uint16_t z_idata_size;
|
||||||
|
uint64_t z_fragmentoff;
|
||||||
|
uint64_t z_tailextent_headlcn;
|
||||||
|
uint64_t z_extents;
|
||||||
|
bool z_initialized;
|
||||||
|
/* Chunk-based fields */
|
||||||
|
uint16_t chunkformat;
|
||||||
|
uint8_t chunkbits;
|
||||||
|
/* Fragment fields */
|
||||||
|
bool fragment;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct erofs_fid {
|
||||||
|
uint16_t len;
|
||||||
|
uint16_t pad;
|
||||||
|
uint32_t nid_hi;
|
||||||
|
uint32_t nid_lo;
|
||||||
|
uint32_t gen;
|
||||||
|
};
|
||||||
|
|
||||||
|
_Static_assert(sizeof(struct erofs_fid) == 16,
|
||||||
|
"EROFS file handle ABI must be 16 bytes");
|
||||||
|
_Static_assert(sizeof(struct erofs_fid) <= sizeof(struct fid),
|
||||||
|
"struct erofs_fid must fit within struct fid");
|
||||||
|
|
||||||
|
#define VTOE(vp) ((struct erofs_node *)(vp)->v_data)
|
||||||
|
#define MTOE(mp) ((struct erofs_mount *)(mp)->mnt_data)
|
||||||
|
|
||||||
|
EROFS_FEATURE_FUNCS(lz4_0padding, incompat, INCOMPAT_LZ4_0PADDING)
|
||||||
|
EROFS_FEATURE_FUNCS(compr_cfgs, incompat, INCOMPAT_COMPR_CFGS)
|
||||||
|
EROFS_FEATURE_FUNCS(big_pcluster, incompat, INCOMPAT_BIG_PCLUSTER)
|
||||||
|
EROFS_FEATURE_FUNCS(chunked_file, incompat, INCOMPAT_CHUNKED_FILE)
|
||||||
|
EROFS_FEATURE_FUNCS(device_table, incompat, INCOMPAT_DEVICE_TABLE)
|
||||||
|
EROFS_FEATURE_FUNCS(compr_head2, incompat, INCOMPAT_COMPR_HEAD2)
|
||||||
|
EROFS_FEATURE_FUNCS(ztailpacking, incompat, INCOMPAT_ZTAILPACKING)
|
||||||
|
EROFS_FEATURE_FUNCS(fragments, incompat, INCOMPAT_FRAGMENTS)
|
||||||
|
EROFS_FEATURE_FUNCS(dedupe, incompat, INCOMPAT_DEDUPE)
|
||||||
|
EROFS_FEATURE_FUNCS(xattr_prefixes, incompat, INCOMPAT_XATTR_PREFIXES)
|
||||||
|
EROFS_FEATURE_FUNCS(48bit, incompat, INCOMPAT_48BIT)
|
||||||
|
EROFS_FEATURE_FUNCS(metabox, incompat, INCOMPAT_METABOX)
|
||||||
|
EROFS_FEATURE_FUNCS(sb_chksum, compat, COMPAT_SB_CHKSUM)
|
||||||
|
EROFS_FEATURE_FUNCS(xattr_filter, compat, COMPAT_XATTR_FILTER)
|
||||||
|
EROFS_FEATURE_FUNCS(shared_ea_in_metabox, compat, COMPAT_SHARED_EA_IN_METABOX)
|
||||||
|
EROFS_FEATURE_FUNCS(plain_xattr_pfx, compat, COMPAT_PLAIN_XATTR_PFX)
|
||||||
|
EROFS_FEATURE_FUNCS(ishare_xattrs, compat, COMPAT_ISHARE_XATTRS)
|
||||||
|
EROFS_FEATURE_FUNCS(mtime, compat, COMPAT_MTIME)
|
||||||
|
|
||||||
|
static inline bool
|
||||||
|
erofs_is_fileio_mode(struct erofs_mount *em __unused)
|
||||||
|
{
|
||||||
|
return (false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline unsigned int
|
||||||
|
erofs_inode_version(unsigned int ifmt)
|
||||||
|
{
|
||||||
|
return ((ifmt >> EROFS_I_VERSION_BIT) & EROFS_I_VERSION_MASK);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline unsigned int
|
||||||
|
erofs_inode_datalayout(unsigned int ifmt)
|
||||||
|
{
|
||||||
|
return ((ifmt >> EROFS_I_DATALAYOUT_BIT) & EROFS_I_DATALAYOUT_MASK);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline bool
|
||||||
|
erofs_nid_in_metabox(erofs_nid_t nid)
|
||||||
|
{
|
||||||
|
return ((nid & EROFS_DIRENT_NID_METABOX) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int erofs_bread(struct erofs_mount *em, uint64_t off, size_t len, void **bufp);
|
||||||
|
int erofs_read_physical(struct erofs_mount *em, unsigned int device_id,
|
||||||
|
uint64_t off, size_t len, void **bufp);
|
||||||
|
void erofs_brelse(void *buf);
|
||||||
|
int erofs_read_metadata(struct erofs_mount *em, erofs_nid_t nid,
|
||||||
|
uint64_t off, size_t len, void **bufp);
|
||||||
|
|
||||||
|
int erofs_read_inode(struct erofs_mount *em, uint64_t nid,
|
||||||
|
struct erofs_node *en);
|
||||||
|
|
||||||
|
int erofs_vget(struct mount *mp, ino_t ino, int flags, struct vnode **vpp);
|
||||||
|
|
||||||
|
int erofs_read_data(struct erofs_mount *em, struct erofs_node *en,
|
||||||
|
uint64_t loff, size_t len, void **bufp);
|
||||||
|
int erofs_read_file(struct vnode *vp, struct uio *uio, int ioflag);
|
||||||
|
|
||||||
|
int erofs_readdir_block(struct vnode *vp, struct uio *uio, int *eofflag,
|
||||||
|
int *ncookies, uint64_t **cookies);
|
||||||
|
|
||||||
|
int erofs_dirent_namelen(const char *blk, uint32_t nameoff, uint32_t endoff,
|
||||||
|
bool trailing, size_t *namelenp);
|
||||||
|
int erofs_validate_dirblock(const char *blk, uint32_t blksz, uint32_t maxsize,
|
||||||
|
uint32_t *ndirentsp);
|
||||||
|
|
||||||
|
int erofs_readlink_target(struct vnode *vp, struct uio *uio);
|
||||||
|
|
||||||
|
int erofs_lookup(struct vop_cachedlookup_args *ap);
|
||||||
|
|
||||||
|
uint64_t erofs_iloc(struct erofs_mount *em, uint64_t nid);
|
||||||
|
bool erofs_nid_is_valid(struct erofs_mount *em, uint64_t nid);
|
||||||
|
|
||||||
|
int erofs_map_blocks(struct erofs_mount *em, struct erofs_node *en,
|
||||||
|
uint64_t loff, uint64_t *phys_off, unsigned int *device_id,
|
||||||
|
size_t *run_len, bool *hole, bool *metadata);
|
||||||
|
int erofs_map_dev(struct erofs_mount *em, struct erofs_map_dev *map);
|
||||||
|
int z_erofs_fill_inode(struct erofs_mount *em, struct erofs_node *en);
|
||||||
|
int z_erofs_map_blocks_iter(struct erofs_mount *em, struct erofs_node *en,
|
||||||
|
struct erofs_map_blocks *map, int flags);
|
||||||
|
int z_erofs_read_data(struct erofs_mount *em, struct erofs_node *en,
|
||||||
|
uint64_t loff, size_t len, void **bufp);
|
||||||
|
int z_erofs_read_uio(struct erofs_mount *em, struct erofs_node *en,
|
||||||
|
struct uio *uio);
|
||||||
|
void z_erofs_extent_cache_init(struct erofs_mount *em);
|
||||||
|
void z_erofs_extent_cache_fini(struct erofs_mount *em);
|
||||||
|
int z_erofs_decompress(struct erofs_mount *em,
|
||||||
|
const struct erofs_map_blocks *map, const void *src, size_t srclen,
|
||||||
|
void *dst, size_t dstlen, bool partial);
|
||||||
|
int z_erofs_parse_cfgs(struct erofs_mount *em,
|
||||||
|
const struct erofs_super_block *dsb);
|
||||||
|
int lz4_decompress(void *src, void *dst, size_t srclen, size_t dstlen,
|
||||||
|
int partial);
|
||||||
|
int z_erofs_load_lzma_config(struct erofs_mount *em, const void *data,
|
||||||
|
size_t size);
|
||||||
|
int lzma_decompress(const void *src, size_t srclen, void *dst, size_t dstlen,
|
||||||
|
uint32_t dict_size, bool partial);
|
||||||
|
int z_erofs_load_deflate_config(struct erofs_mount *em, const void *data,
|
||||||
|
size_t size);
|
||||||
|
int deflate_decompress(void *src, size_t srclen, void *dst, size_t dstlen,
|
||||||
|
int windowbits, bool partial);
|
||||||
|
bool erofs_zstd_available(void);
|
||||||
|
int z_erofs_load_zstd_config(struct erofs_mount *em, const void *data,
|
||||||
|
size_t size);
|
||||||
|
int zstd_decompress(void *src, size_t srclen, void *dst, size_t dstlen,
|
||||||
|
int windowlog, bool partial);
|
||||||
|
|
||||||
|
extern struct vop_vector erofs_vnodeops;
|
||||||
|
extern struct vop_vector erofs_fifoops;
|
||||||
|
|
||||||
|
#endif /* __EROFS_INTERNAL_H */
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/* SPDX-License-Identifier: BSD-2-Clause */
|
||||||
|
/* Minimal LZ4 decompressor for EROFS FreeBSD */
|
||||||
|
#include <sys/param.h>
|
||||||
|
#include <sys/systm.h>
|
||||||
|
|
||||||
|
#include "erofs_defs.h"
|
||||||
|
#include "internal.h"
|
||||||
|
|
||||||
|
static int
|
||||||
|
lz4_finish(const uint8_t *ip, const uint8_t *iend, int partial)
|
||||||
|
{
|
||||||
|
if (partial)
|
||||||
|
return (0);
|
||||||
|
while (ip < iend) {
|
||||||
|
if (*ip++ != 0)
|
||||||
|
return (-1);
|
||||||
|
}
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
lz4_decompress(void *src, void *dst, size_t srclen, size_t dstlen, int partial)
|
||||||
|
{
|
||||||
|
const uint8_t *ip, *iend;
|
||||||
|
uint8_t *op, *oend;
|
||||||
|
unsigned int token;
|
||||||
|
size_t length, copylen;
|
||||||
|
size_t offset;
|
||||||
|
|
||||||
|
ip = src;
|
||||||
|
iend = ip + srclen;
|
||||||
|
op = dst;
|
||||||
|
oend = op + dstlen;
|
||||||
|
if (iend < ip || oend < op)
|
||||||
|
return (-1);
|
||||||
|
|
||||||
|
while (ip < iend) {
|
||||||
|
token = *ip++;
|
||||||
|
length = token >> EROFS_LZ4_TOKEN_LITERAL_SHIFT;
|
||||||
|
if (length == EROFS_LZ4_MAX_RUN) {
|
||||||
|
unsigned int value;
|
||||||
|
do {
|
||||||
|
if (ip >= iend)
|
||||||
|
return (-1);
|
||||||
|
value = *ip++;
|
||||||
|
if (length > SIZE_MAX - value)
|
||||||
|
return (-1);
|
||||||
|
length += value;
|
||||||
|
} while (value == EROFS_LZ4_EXT_SENTINEL);
|
||||||
|
}
|
||||||
|
if (length > (size_t)(iend - ip))
|
||||||
|
return (-1);
|
||||||
|
if (!partial && length > (size_t)(oend - op))
|
||||||
|
return (-1);
|
||||||
|
copylen = MIN(length, (size_t)(oend - op));
|
||||||
|
memcpy(op, ip, copylen);
|
||||||
|
ip += length;
|
||||||
|
op += copylen;
|
||||||
|
if (op == oend)
|
||||||
|
return (lz4_finish(ip, iend, partial));
|
||||||
|
if (ip >= iend)
|
||||||
|
break;
|
||||||
|
if (ip + EROFS_LZ4_OFFSET_BYTES > iend)
|
||||||
|
return (-1);
|
||||||
|
offset = ip[0] | (ip[1] << 8);
|
||||||
|
ip += EROFS_LZ4_OFFSET_BYTES;
|
||||||
|
if (offset == 0 || offset > (size_t)(op - (uint8_t *)dst))
|
||||||
|
return (-1);
|
||||||
|
length = token & EROFS_LZ4_TOKEN_MATCH_MASK;
|
||||||
|
if (length == EROFS_LZ4_MAX_RUN) {
|
||||||
|
unsigned int value;
|
||||||
|
do {
|
||||||
|
if (ip >= iend)
|
||||||
|
return (-1);
|
||||||
|
value = *ip++;
|
||||||
|
if (length > SIZE_MAX - value)
|
||||||
|
return (-1);
|
||||||
|
length += value;
|
||||||
|
} while (value == EROFS_LZ4_EXT_SENTINEL);
|
||||||
|
}
|
||||||
|
if (length > SIZE_MAX - EROFS_LZ4_MIN_MATCH)
|
||||||
|
return (-1);
|
||||||
|
length += EROFS_LZ4_MIN_MATCH;
|
||||||
|
if (!partial && length > (size_t)(oend - op))
|
||||||
|
return (-1);
|
||||||
|
copylen = MIN(length, (size_t)(oend - op));
|
||||||
|
while (copylen-- != 0) {
|
||||||
|
*op = *(op - offset);
|
||||||
|
++op;
|
||||||
|
}
|
||||||
|
if (op == oend)
|
||||||
|
return (lz4_finish(ip, iend, partial));
|
||||||
|
}
|
||||||
|
return (op == oend ? lz4_finish(ip, iend, partial) : -1);
|
||||||
|
}
|
||||||
+310
@@ -0,0 +1,310 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017-2018 HUAWEI, Inc.
|
||||||
|
* https://www.huawei.com/
|
||||||
|
* Copyright (C) 2022, Alibaba Cloud
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <sys/param.h>
|
||||||
|
#include <sys/systm.h>
|
||||||
|
#include <sys/dirent.h>
|
||||||
|
#include <sys/kernel.h>
|
||||||
|
#include <sys/libkern.h>
|
||||||
|
#include <sys/malloc.h>
|
||||||
|
#include <sys/namei.h>
|
||||||
|
#include <sys/proc.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/vnode.h>
|
||||||
|
|
||||||
|
#include "internal.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Compare two directory entry names using an already-matched prefix.
|
||||||
|
* (Linux equivalent: erofs_dirnamecmp in Linux's namei.c)
|
||||||
|
*
|
||||||
|
* qn_name / qn_len: search key (not necessarily null-terminated).
|
||||||
|
* qd_name / qd_end: on-disk name range (may not be null-terminated).
|
||||||
|
* matched: in/out count of prefix characters already known to match.
|
||||||
|
*
|
||||||
|
* Returns 0 if equal, 1 if qn > qd, -1 if qn < qd.
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
erofs_dirnamecmp(const char *qn_name, size_t qn_len, const char *qd_name,
|
||||||
|
const char *qd_end, unsigned int *matched)
|
||||||
|
{
|
||||||
|
size_t dname_span;
|
||||||
|
unsigned int i;
|
||||||
|
|
||||||
|
dname_span = qd_end - qd_name;
|
||||||
|
i = MIN(*matched, qn_len);
|
||||||
|
i = MIN(i, dname_span);
|
||||||
|
while (i < qn_len && i < dname_span && qd_name[i] != '\0') {
|
||||||
|
if ((unsigned char)qn_name[i] != (unsigned char)qd_name[i]) {
|
||||||
|
*matched = i;
|
||||||
|
return ((unsigned char)qn_name[i] >
|
||||||
|
(unsigned char)qd_name[i] ? 1 : -1);
|
||||||
|
}
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
*matched = i;
|
||||||
|
if (i == qn_len)
|
||||||
|
return (i == dname_span || qd_name[i] == '\0' ? 0 : -1);
|
||||||
|
return (1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Binary search within a directory block for the target name.
|
||||||
|
*
|
||||||
|
* Returns a pointer to the matching dirent, or NULL on miss.
|
||||||
|
*/
|
||||||
|
static struct erofs_dirent *
|
||||||
|
find_target_dirent(const char *name, size_t namelen, char *data,
|
||||||
|
uint32_t datasize, uint32_t ndirents)
|
||||||
|
{
|
||||||
|
uint32_t head, back;
|
||||||
|
unsigned int startprfx, endprfx;
|
||||||
|
struct erofs_dirent *const de = (struct erofs_dirent *)data;
|
||||||
|
|
||||||
|
/* The 1st dirent has already been evaluated by the caller. */
|
||||||
|
head = 1;
|
||||||
|
back = ndirents - 1;
|
||||||
|
startprfx = endprfx = 0;
|
||||||
|
|
||||||
|
while (head <= back) {
|
||||||
|
const uint32_t mid = head + (back - head) / 2;
|
||||||
|
const uint32_t nameoff = le16toh(de[mid].nameoff);
|
||||||
|
unsigned int matched = MIN(startprfx, endprfx);
|
||||||
|
const char *dname_start = data + nameoff;
|
||||||
|
const char *dname_end;
|
||||||
|
|
||||||
|
if (mid >= ndirents - 1)
|
||||||
|
dname_end = data + datasize;
|
||||||
|
else
|
||||||
|
dname_end = data + le16toh(de[mid + 1].nameoff);
|
||||||
|
|
||||||
|
/* String comparison without already matched prefix */
|
||||||
|
int ret = erofs_dirnamecmp(name, namelen, dname_start,
|
||||||
|
dname_end, &matched);
|
||||||
|
|
||||||
|
if (ret == 0)
|
||||||
|
return (de + mid);
|
||||||
|
else if (ret > 0) {
|
||||||
|
head = mid + 1;
|
||||||
|
startprfx = matched;
|
||||||
|
} else {
|
||||||
|
back = mid - 1;
|
||||||
|
endprfx = matched;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Find the directory block most likely to contain the target name.
|
||||||
|
*
|
||||||
|
* Uses two-level binary search: first across blocks, then within the
|
||||||
|
* candidate block via find_target_dirent().
|
||||||
|
*
|
||||||
|
* Returns the block buffer on success (caller must erofs_brelse),
|
||||||
|
* or NULL on error. *_ndirents is set to the number of dirents in
|
||||||
|
* the returned block (0 means the first entry is the match).
|
||||||
|
* On error, *errorp is set to a positive errno.
|
||||||
|
*/
|
||||||
|
static char *
|
||||||
|
erofs_find_target_block(struct erofs_mount *em, struct erofs_node *dir,
|
||||||
|
const char *name, size_t namelen, uint32_t *_ndirents, uint32_t *_datasize,
|
||||||
|
int *errorp)
|
||||||
|
{
|
||||||
|
uint32_t bsz = em->block_size;
|
||||||
|
uint64_t head, back;
|
||||||
|
unsigned int startprfx = 0, endprfx = 0;
|
||||||
|
char *candidate = NULL;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
*errorp = 0;
|
||||||
|
*_ndirents = 0;
|
||||||
|
*_datasize = 0;
|
||||||
|
|
||||||
|
if (dir->size == 0)
|
||||||
|
return (NULL);
|
||||||
|
|
||||||
|
head = 0;
|
||||||
|
back = (dir->size - 1) / bsz;
|
||||||
|
|
||||||
|
while (head <= back) {
|
||||||
|
const uint64_t mid = head + (back - head) / 2;
|
||||||
|
uint64_t block_off;
|
||||||
|
uint32_t maxsize;
|
||||||
|
const struct erofs_dirent *de;
|
||||||
|
char *blk;
|
||||||
|
int diff;
|
||||||
|
uint32_t ndirents;
|
||||||
|
uint32_t nameoff;
|
||||||
|
unsigned int matched;
|
||||||
|
const char *dname_start, *dname_end;
|
||||||
|
|
||||||
|
if (__builtin_mul_overflow(mid, (uint64_t)bsz, &block_off) ||
|
||||||
|
block_off >= dir->size) {
|
||||||
|
*errorp = EINTEGRITY;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
maxsize = MIN((uint64_t)bsz, dir->size - block_off);
|
||||||
|
error = erofs_read_data(em, dir, block_off, maxsize,
|
||||||
|
(void **)&blk);
|
||||||
|
if (error != 0) {
|
||||||
|
*errorp = error;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
error = erofs_validate_dirblock(blk, bsz, maxsize, &ndirents);
|
||||||
|
if (error != 0) {
|
||||||
|
erofs_brelse(blk);
|
||||||
|
*errorp = error;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
de = (const struct erofs_dirent *)blk;
|
||||||
|
nameoff = le16toh(de[0].nameoff);
|
||||||
|
|
||||||
|
matched = MIN(startprfx, endprfx);
|
||||||
|
dname_start = blk + nameoff;
|
||||||
|
if (ndirents == 1)
|
||||||
|
dname_end = blk + maxsize;
|
||||||
|
else
|
||||||
|
dname_end = blk + le16toh(de[1].nameoff);
|
||||||
|
|
||||||
|
/* String comparison without already matched prefix */
|
||||||
|
diff = erofs_dirnamecmp(name, namelen, dname_start, dname_end,
|
||||||
|
&matched);
|
||||||
|
|
||||||
|
if (diff < 0) {
|
||||||
|
erofs_brelse(blk);
|
||||||
|
if (mid == 0)
|
||||||
|
break;
|
||||||
|
back = mid - 1;
|
||||||
|
endprfx = matched;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* diff >= 0: this block is a candidate. */
|
||||||
|
if (candidate != NULL)
|
||||||
|
erofs_brelse(candidate);
|
||||||
|
candidate = blk;
|
||||||
|
if (diff == 0) {
|
||||||
|
*_ndirents = 0;
|
||||||
|
*_datasize = maxsize;
|
||||||
|
return (candidate);
|
||||||
|
}
|
||||||
|
head = mid + 1;
|
||||||
|
startprfx = matched;
|
||||||
|
*_ndirents = ndirents;
|
||||||
|
*_datasize = maxsize;
|
||||||
|
}
|
||||||
|
return (candidate);
|
||||||
|
out:
|
||||||
|
if (candidate != NULL)
|
||||||
|
erofs_brelse(candidate);
|
||||||
|
return (NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Look up a name in a directory and return its nid and d_type.
|
||||||
|
* (Linux equivalent: erofs_namei in Linux's namei.c)
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
erofs_namei(struct erofs_mount *em, struct erofs_node *dir, const char *name,
|
||||||
|
size_t namelen, uint64_t *nid, uint8_t *d_type)
|
||||||
|
{
|
||||||
|
int error;
|
||||||
|
uint32_t ndirents;
|
||||||
|
uint32_t datasize;
|
||||||
|
char *blk;
|
||||||
|
struct erofs_dirent *de;
|
||||||
|
|
||||||
|
if (dir->size == 0)
|
||||||
|
return (ENOENT);
|
||||||
|
|
||||||
|
blk = erofs_find_target_block(em, dir, name, namelen, &ndirents,
|
||||||
|
&datasize, &error);
|
||||||
|
if (blk == NULL)
|
||||||
|
return (error != 0 ? error : ENOENT);
|
||||||
|
|
||||||
|
de = (struct erofs_dirent *)blk;
|
||||||
|
if (ndirents > 0)
|
||||||
|
de = find_target_dirent(name, namelen, blk, datasize,
|
||||||
|
ndirents);
|
||||||
|
|
||||||
|
if (de != NULL) {
|
||||||
|
*nid = le64toh(de->nid);
|
||||||
|
*d_type = de->file_type;
|
||||||
|
}
|
||||||
|
erofs_brelse(blk);
|
||||||
|
return (de != NULL ? 0 : ENOENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Directory name lookup (VOP_CACHEDLOOKUP entry point).
|
||||||
|
*
|
||||||
|
* FreeBSD-side API requirements:
|
||||||
|
* - "." must be returned under the caller's requested lock mode;
|
||||||
|
* - ".." must go through vn_vget_ino() to avoid holding a child lock while
|
||||||
|
* acquiring the parent directory lock in reverse;
|
||||||
|
* - Both hit and miss must correctly update the namecache.
|
||||||
|
*/
|
||||||
|
int
|
||||||
|
erofs_lookup(struct vop_cachedlookup_args *ap)
|
||||||
|
{
|
||||||
|
struct vnode *dvp, *vp;
|
||||||
|
struct erofs_node *dir;
|
||||||
|
struct erofs_mount *em;
|
||||||
|
struct componentname *cnp;
|
||||||
|
uint64_t nid;
|
||||||
|
uint8_t dtype;
|
||||||
|
int error, ltype;
|
||||||
|
|
||||||
|
dvp = ap->a_dvp;
|
||||||
|
cnp = ap->a_cnp;
|
||||||
|
*ap->a_vpp = NULL;
|
||||||
|
if ((cnp->cn_flags & ISLASTCN) != 0 &&
|
||||||
|
(cnp->cn_nameiop == DELETE || cnp->cn_nameiop == RENAME))
|
||||||
|
return (EROFS);
|
||||||
|
if (cnp->cn_namelen < 0)
|
||||||
|
return (EINVAL);
|
||||||
|
if (cnp->cn_namelen > EROFS_NAME_LEN)
|
||||||
|
return (ENAMETOOLONG);
|
||||||
|
if (cnp->cn_namelen == 1 && cnp->cn_nameptr[0] == '.') {
|
||||||
|
vref(dvp);
|
||||||
|
ltype = cnp->cn_lkflags & LK_TYPE_MASK;
|
||||||
|
if (ltype != VOP_ISLOCKED(dvp)) {
|
||||||
|
if (ltype == LK_EXCLUSIVE)
|
||||||
|
vn_lock(dvp, LK_UPGRADE | LK_RETRY);
|
||||||
|
else if (ltype == LK_SHARED)
|
||||||
|
vn_lock(dvp, LK_DOWNGRADE | LK_RETRY);
|
||||||
|
}
|
||||||
|
*ap->a_vpp = dvp;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
dir = VTOE(dvp);
|
||||||
|
em = MTOE(dvp->v_mount);
|
||||||
|
error = erofs_namei(em, dir, cnp->cn_nameptr, cnp->cn_namelen, &nid,
|
||||||
|
&dtype);
|
||||||
|
if (error != 0) {
|
||||||
|
if (error == ENOENT && (cnp->cn_flags & MAKEENTRY) != 0)
|
||||||
|
cache_enter(dvp, NULL, cnp);
|
||||||
|
if (error == ENOENT && (cnp->cn_flags & ISLASTCN) != 0 &&
|
||||||
|
(cnp->cn_nameiop == CREATE || cnp->cn_nameiop == RENAME))
|
||||||
|
return (EROFS);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((cnp->cn_flags & ISDOTDOT) != 0)
|
||||||
|
error = vn_vget_ino(dvp, nid, cnp->cn_lkflags, &vp);
|
||||||
|
else
|
||||||
|
error = erofs_vget(dvp->v_mount, nid, cnp->cn_lkflags, &vp);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
*ap->a_vpp = vp;
|
||||||
|
if ((cnp->cn_flags & MAKEENTRY) != 0)
|
||||||
|
cache_enter(dvp, vp, cnp);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
+884
@@ -0,0 +1,884 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017-2018 HUAWEI, Inc.
|
||||||
|
* https://www.huawei.com/
|
||||||
|
* Copyright (C) 2021, Alibaba Cloud
|
||||||
|
*/
|
||||||
|
#include <sys/param.h>
|
||||||
|
#include <sys/systm.h>
|
||||||
|
#include <sys/_maxphys.h>
|
||||||
|
#include <sys/bio.h>
|
||||||
|
#include <sys/buf.h>
|
||||||
|
#include <sys/conf.h>
|
||||||
|
#include <sys/fcntl.h>
|
||||||
|
#include <sys/fnv_hash.h>
|
||||||
|
#include <sys/gsb_crc32.h>
|
||||||
|
#include <sys/kernel.h>
|
||||||
|
#include <sys/libkern.h>
|
||||||
|
#include <sys/malloc.h>
|
||||||
|
#include <sys/module.h>
|
||||||
|
#include <sys/mount.h>
|
||||||
|
#include <sys/namei.h>
|
||||||
|
#include <sys/priv.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/vnode.h>
|
||||||
|
|
||||||
|
#include <geom/geom.h>
|
||||||
|
#include <geom/geom_vfs.h>
|
||||||
|
|
||||||
|
#include "internal.h"
|
||||||
|
#include "xattr.h"
|
||||||
|
#include "erofs_defs.h"
|
||||||
|
|
||||||
|
MALLOC_DEFINE(M_EROFS, "erofs", "EROFS filesystem");
|
||||||
|
|
||||||
|
static const char *erofs_opts[] = {
|
||||||
|
"export",
|
||||||
|
"from",
|
||||||
|
NULL,
|
||||||
|
};
|
||||||
|
|
||||||
|
static vfs_mount_t erofs_mount;
|
||||||
|
static vfs_root_t erofs_root;
|
||||||
|
static vfs_statfs_t erofs_statfs;
|
||||||
|
static vfs_unmount_t erofs_unmount;
|
||||||
|
static vfs_vget_t erofs_vgetf;
|
||||||
|
static vfs_fhtovp_t erofs_fhtovp;
|
||||||
|
|
||||||
|
#define EROFS_DEVICE_OPT_PREFIX "device."
|
||||||
|
|
||||||
|
struct erofs_device_arg {
|
||||||
|
uint16_t slot;
|
||||||
|
char *path;
|
||||||
|
};
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_load_generation_seed(struct erofs_mount *em, uint32_t sb_size,
|
||||||
|
uint32_t *seedp)
|
||||||
|
{
|
||||||
|
uint32_t seed;
|
||||||
|
void *buf;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
error = erofs_bread(em, EROFS_SUPER_OFFSET, sb_size, &buf);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
seed = fnv_32_buf(buf, sb_size, FNV1_32_INIT);
|
||||||
|
erofs_brelse(buf);
|
||||||
|
*seedp = seed != 0 ? seed : 1;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
erofs_free_device_args(struct erofs_device_arg *args, unsigned int count)
|
||||||
|
{
|
||||||
|
unsigned int i;
|
||||||
|
|
||||||
|
if (args == NULL)
|
||||||
|
return;
|
||||||
|
for (i = 0; i < count; ++i)
|
||||||
|
free(args[i].path, M_EROFS);
|
||||||
|
free(args, M_EROFS);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_parse_device_slot(const char *name, uint16_t *slotp)
|
||||||
|
{
|
||||||
|
const char *p;
|
||||||
|
unsigned int slot;
|
||||||
|
|
||||||
|
if (strncmp(name, EROFS_DEVICE_OPT_PREFIX,
|
||||||
|
sizeof(EROFS_DEVICE_OPT_PREFIX) - 1) != 0)
|
||||||
|
return (ENOENT);
|
||||||
|
p = name + sizeof(EROFS_DEVICE_OPT_PREFIX) - 1;
|
||||||
|
if (*p < '1' || *p > '9')
|
||||||
|
return (EINVAL);
|
||||||
|
slot = 0;
|
||||||
|
for (; *p != '\0'; ++p) {
|
||||||
|
if (*p < '0' || *p > '9' || slot > (UINT16_MAX - (*p - '0')) / 10)
|
||||||
|
return (EINVAL);
|
||||||
|
slot = slot * 10 + (*p - '0');
|
||||||
|
}
|
||||||
|
if (slot == 0 || slot > UINT16_MAX)
|
||||||
|
return (EINVAL);
|
||||||
|
*slotp = slot;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_parse_device_options(struct mount *mp, struct erofs_device_arg **argsp,
|
||||||
|
unsigned int *countp)
|
||||||
|
{
|
||||||
|
struct erofs_device_arg *args;
|
||||||
|
struct vfsopt *opt;
|
||||||
|
char name[32];
|
||||||
|
unsigned int count, i;
|
||||||
|
uint16_t slot;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
*argsp = NULL;
|
||||||
|
*countp = 0;
|
||||||
|
count = 0;
|
||||||
|
TAILQ_FOREACH(opt, mp->mnt_optnew, link) {
|
||||||
|
error = erofs_parse_device_slot(opt->name, &slot);
|
||||||
|
if (error == ENOENT)
|
||||||
|
continue;
|
||||||
|
if (error != 0 || opt->value == NULL || opt->len <= 1 ||
|
||||||
|
((char *)opt->value)[opt->len - 1] != '\0') {
|
||||||
|
vfs_mount_error(mp, "erofs: invalid external device option %s",
|
||||||
|
opt->name);
|
||||||
|
return (EINVAL);
|
||||||
|
}
|
||||||
|
if (count == UINT16_MAX)
|
||||||
|
return (E2BIG);
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
if (count == 0)
|
||||||
|
return (0);
|
||||||
|
|
||||||
|
args = mallocarray(count, sizeof(*args), M_EROFS, M_WAITOK | M_ZERO);
|
||||||
|
i = 0;
|
||||||
|
TAILQ_FOREACH(opt, mp->mnt_optnew, link) {
|
||||||
|
error = erofs_parse_device_slot(opt->name, &slot);
|
||||||
|
if (error == ENOENT)
|
||||||
|
continue;
|
||||||
|
KASSERT(error == 0, ("validated EROFS device option changed"));
|
||||||
|
args[i].slot = slot;
|
||||||
|
args[i].path = malloc(opt->len, M_EROFS, M_WAITOK);
|
||||||
|
memcpy(args[i].path, opt->value, opt->len);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
for (i = 0; i < count; ++i) {
|
||||||
|
snprintf(name, sizeof(name), EROFS_DEVICE_OPT_PREFIX "%u",
|
||||||
|
args[i].slot);
|
||||||
|
vfs_deleteopt(mp->mnt_optnew, name);
|
||||||
|
}
|
||||||
|
*argsp = args;
|
||||||
|
*countp = count;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
erofs_release_device_info(struct erofs_device_info *dif)
|
||||||
|
{
|
||||||
|
if (dif->cp != NULL) {
|
||||||
|
g_topology_lock();
|
||||||
|
g_vfs_close(dif->cp);
|
||||||
|
g_topology_unlock();
|
||||||
|
dif->cp = NULL;
|
||||||
|
}
|
||||||
|
if (dif->devvp != NULL) {
|
||||||
|
vrele(dif->devvp);
|
||||||
|
dif->devvp = NULL;
|
||||||
|
}
|
||||||
|
if (dif->dev != NULL) {
|
||||||
|
dev_rel(dif->dev);
|
||||||
|
dif->dev = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
erofs_provider_is_duplicate(struct erofs_mount *em, struct g_provider *pp)
|
||||||
|
{
|
||||||
|
unsigned int i;
|
||||||
|
|
||||||
|
if (em == NULL)
|
||||||
|
return (false);
|
||||||
|
if (em->dif0.cp != NULL && em->dif0.cp->provider == pp)
|
||||||
|
return (true);
|
||||||
|
for (i = 0; i < em->extra_devices; ++i) {
|
||||||
|
if (em->devs[i].cp != NULL && em->devs[i].cp->provider == pp)
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
return (false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_open_device(struct erofs_mount *em, const char *path,
|
||||||
|
struct erofs_device_info *dif)
|
||||||
|
{
|
||||||
|
struct g_provider *pp;
|
||||||
|
struct nameidata nd;
|
||||||
|
struct vnode *devvp;
|
||||||
|
struct cdev *dev;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
bzero(dif, sizeof(*dif));
|
||||||
|
NDINIT(&nd, LOOKUP, FOLLOW | LOCKLEAF, UIO_SYSSPACE, path);
|
||||||
|
error = namei(&nd);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
devvp = nd.ni_vp;
|
||||||
|
NDFREE_PNBUF(&nd);
|
||||||
|
if (!vn_isdisk_error(devvp, &error)) {
|
||||||
|
vput(devvp);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
error = VOP_ACCESS(devvp, VREAD, curthread->td_ucred, curthread);
|
||||||
|
if (error != 0)
|
||||||
|
error = priv_check(curthread, PRIV_VFS_MOUNT_PERM);
|
||||||
|
if (error != 0) {
|
||||||
|
vput(devvp);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
dev = devvp->v_rdev;
|
||||||
|
dev_ref(dev);
|
||||||
|
g_topology_lock();
|
||||||
|
pp = g_dev_getprovider(dev);
|
||||||
|
if (pp == NULL)
|
||||||
|
error = ENXIO;
|
||||||
|
else if (erofs_provider_is_duplicate(em, pp))
|
||||||
|
error = EINVAL;
|
||||||
|
else
|
||||||
|
error = g_vfs_open(devvp, &dif->cp, "erofs", 0);
|
||||||
|
if (error == 0) {
|
||||||
|
dif->mediasize = dif->cp->provider->mediasize;
|
||||||
|
dif->sectorsize = dif->cp->provider->sectorsize;
|
||||||
|
}
|
||||||
|
g_topology_unlock();
|
||||||
|
VOP_UNLOCK(devvp);
|
||||||
|
if (error != 0) {
|
||||||
|
dev_rel(dev);
|
||||||
|
vrele(devvp);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
dif->devvp = devvp;
|
||||||
|
dif->dev = dev;
|
||||||
|
if (dif->sectorsize == 0 ||
|
||||||
|
(dif->sectorsize & (dif->sectorsize - 1)) != 0) {
|
||||||
|
erofs_release_device_info(dif);
|
||||||
|
return (EINVAL);
|
||||||
|
}
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
erofs_update_iosize_max(struct mount *mp, const struct erofs_device_info *dif)
|
||||||
|
{
|
||||||
|
u_long iosize;
|
||||||
|
|
||||||
|
iosize = dif->dev != NULL && dif->dev->si_iosize_max != 0 ?
|
||||||
|
dif->dev->si_iosize_max : MAXPHYS;
|
||||||
|
mp->mnt_iosize_max = MIN(mp->mnt_iosize_max, MIN(iosize, (u_long)MAXPHYS));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
erofs_sb_free(struct erofs_mount *em)
|
||||||
|
{
|
||||||
|
if (em == NULL)
|
||||||
|
return;
|
||||||
|
z_erofs_extent_cache_fini(em);
|
||||||
|
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 int
|
||||||
|
erofs_superblock_csum_verify(struct erofs_mount *em,
|
||||||
|
const struct erofs_super_block *dsb)
|
||||||
|
{
|
||||||
|
uint32_t expected, crc;
|
||||||
|
size_t len;
|
||||||
|
void *buf;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
if ((le32toh(dsb->feature_compat) & EROFS_FEATURE_COMPAT_SB_CHKSUM) == 0)
|
||||||
|
return (0);
|
||||||
|
|
||||||
|
len = 1u << dsb->blkszbits;
|
||||||
|
if (len > EROFS_SUPER_OFFSET)
|
||||||
|
len -= EROFS_SUPER_OFFSET;
|
||||||
|
|
||||||
|
buf = NULL;
|
||||||
|
error = erofs_bread(em, EROFS_SUPER_OFFSET, len, &buf);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
|
||||||
|
crc = calculate_crc32c(EROFS_CRC32C_SEED,
|
||||||
|
(const uint8_t *)buf + offsetof(struct erofs_super_block, checksum) +
|
||||||
|
sizeof(dsb->checksum),
|
||||||
|
len - offsetof(struct erofs_super_block, checksum) -
|
||||||
|
sizeof(dsb->checksum));
|
||||||
|
expected = le32toh(dsb->checksum);
|
||||||
|
erofs_brelse(buf);
|
||||||
|
|
||||||
|
if (crc != expected) {
|
||||||
|
vfs_mount_error(em->mnt,
|
||||||
|
"erofs: invalid superblock checksum 0x%08x, "
|
||||||
|
"0x%08x expected", crc, expected);
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
erofs_sb_blocks_root(const struct erofs_super_block *dsb, uint32_t incompat,
|
||||||
|
uint64_t *blocks, uint64_t *root_nid)
|
||||||
|
{
|
||||||
|
*blocks = le32toh(dsb->blocks_lo);
|
||||||
|
if ((incompat & EROFS_FEATURE_INCOMPAT_48BIT) != 0 &&
|
||||||
|
dsb->rootnid_8b != 0) {
|
||||||
|
*blocks |= (uint64_t)le16toh(dsb->rb.blocks_hi) << 32;
|
||||||
|
*root_nid = le64toh(dsb->rootnid_8b);
|
||||||
|
} else {
|
||||||
|
*root_nid = le16toh(dsb->rb.rootnid_2b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_validate_device_size(struct erofs_mount *em,
|
||||||
|
struct erofs_device_info *dif, erofs_blk_t blocks)
|
||||||
|
{
|
||||||
|
uint64_t bytes;
|
||||||
|
|
||||||
|
if (blocks == 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
if (em->block_size < dif->sectorsize ||
|
||||||
|
em->block_size % dif->sectorsize != 0)
|
||||||
|
return (EINVAL);
|
||||||
|
if (blocks > (UINT64_MAX >> em->block_bits))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
bytes = blocks << em->block_bits;
|
||||||
|
if (bytes > dif->mediasize)
|
||||||
|
return (ENXIO);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_init_device(struct erofs_mount *em, struct erofs_device_info *dif,
|
||||||
|
const char *path)
|
||||||
|
{
|
||||||
|
struct erofs_device_info opened;
|
||||||
|
erofs_blk_t blocks, uniaddr;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
blocks = dif->blocks;
|
||||||
|
uniaddr = dif->uniaddr;
|
||||||
|
error = erofs_open_device(em, path, &opened);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
erofs_update_iosize_max(em->mnt, &opened);
|
||||||
|
opened.blocks = blocks;
|
||||||
|
opened.uniaddr = uniaddr;
|
||||||
|
*dif = opened;
|
||||||
|
return (erofs_validate_device_size(em, dif, dif->blocks));
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *
|
||||||
|
erofs_device_arg_path(const struct erofs_device_arg *args, unsigned int count,
|
||||||
|
unsigned int slot)
|
||||||
|
{
|
||||||
|
unsigned int i;
|
||||||
|
|
||||||
|
for (i = 0; i < count; ++i) {
|
||||||
|
if (args[i].slot == slot)
|
||||||
|
return (args[i].path);
|
||||||
|
}
|
||||||
|
return (NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_scan_devices(struct erofs_mount *em, const struct erofs_super_block *dsb,
|
||||||
|
const struct erofs_device_arg *args, unsigned int arg_count)
|
||||||
|
{
|
||||||
|
struct erofs_deviceslot *slots;
|
||||||
|
struct erofs_device_info *dif;
|
||||||
|
const char *path;
|
||||||
|
uint64_t devt_off, devt_size, image_size, end, other_end;
|
||||||
|
erofs_blk_t maxend;
|
||||||
|
unsigned int i, j, mask;
|
||||||
|
void *buf;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
em->total_blocks = em->dif0.blocks;
|
||||||
|
em->flatdev_blocks = em->dif0.blocks;
|
||||||
|
if (em->extra_devices == 0) {
|
||||||
|
if (arg_count != 0) {
|
||||||
|
vfs_mount_error(em->mnt,
|
||||||
|
"erofs: external devices given without a device table");
|
||||||
|
return (EINVAL);
|
||||||
|
}
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
devt_off = (uint64_t)le16toh(dsb->devt_slotoff) * EROFS_DEVT_SLOT_SIZE;
|
||||||
|
devt_size = (uint64_t)em->extra_devices * EROFS_DEVT_SLOT_SIZE;
|
||||||
|
if (em->dif0.blocks > (UINT64_MAX >> em->block_bits))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
image_size = em->dif0.blocks << em->block_bits;
|
||||||
|
if (devt_off > image_size || devt_size > image_size - devt_off ||
|
||||||
|
devt_size > SIZE_MAX)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
error = erofs_bread(em, devt_off, (size_t)devt_size, &buf);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
slots = buf;
|
||||||
|
em->devs = mallocarray(em->extra_devices, sizeof(*em->devs), M_EROFS,
|
||||||
|
M_WAITOK | M_ZERO);
|
||||||
|
maxend = em->dif0.blocks;
|
||||||
|
for (i = 0; i < em->extra_devices; ++i) {
|
||||||
|
dif = &em->devs[i];
|
||||||
|
dif->blocks = le32toh(slots[i].blocks_lo);
|
||||||
|
dif->uniaddr = le32toh(slots[i].uniaddr_lo);
|
||||||
|
if (erofs_sb_has_48bit(em)) {
|
||||||
|
dif->blocks |= (uint64_t)le16toh(slots[i].blocks_hi) << 32;
|
||||||
|
dif->uniaddr |= (uint64_t)le16toh(slots[i].uniaddr_hi) << 32;
|
||||||
|
}
|
||||||
|
if (dif->blocks == 0 ||
|
||||||
|
__builtin_add_overflow(dif->uniaddr, dif->blocks, &end)) {
|
||||||
|
error = EINTEGRITY;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
if (end >
|
||||||
|
(erofs_sb_has_48bit(em) ? (1ULL << 48) : (1ULL << 32))) {
|
||||||
|
error = EINTEGRITY;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
if (dif->uniaddr != 0 && dif->uniaddr < em->dif0.blocks) {
|
||||||
|
error = EINTEGRITY;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
for (j = 0; j < i; ++j) {
|
||||||
|
if (dif->uniaddr == 0 || em->devs[j].uniaddr == 0)
|
||||||
|
continue;
|
||||||
|
if (__builtin_add_overflow(em->devs[j].uniaddr,
|
||||||
|
em->devs[j].blocks, &other_end)) {
|
||||||
|
error = EINTEGRITY;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
if (dif->uniaddr < other_end && em->devs[j].uniaddr < end) {
|
||||||
|
error = EINTEGRITY;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (__builtin_add_overflow(em->total_blocks, dif->blocks,
|
||||||
|
&em->total_blocks)) {
|
||||||
|
error = EOVERFLOW;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
maxend = MAX(maxend, (erofs_blk_t)end);
|
||||||
|
}
|
||||||
|
erofs_brelse(buf);
|
||||||
|
buf = NULL;
|
||||||
|
em->flatdev_blocks = maxend;
|
||||||
|
mask = 1;
|
||||||
|
while (mask < (unsigned int)em->extra_devices + 1)
|
||||||
|
mask <<= 1;
|
||||||
|
em->device_id_mask = mask - 1;
|
||||||
|
em->flatdev = arg_count == 0;
|
||||||
|
if (em->flatdev)
|
||||||
|
return (erofs_validate_device_size(em, &em->dif0,
|
||||||
|
em->flatdev_blocks));
|
||||||
|
if (arg_count != em->extra_devices) {
|
||||||
|
vfs_mount_error(em->mnt,
|
||||||
|
"erofs: external devices don't match (ondisk %u, given %u)",
|
||||||
|
em->extra_devices, arg_count);
|
||||||
|
return (arg_count < em->extra_devices ? ENXIO : EINVAL);
|
||||||
|
}
|
||||||
|
for (i = 0; i < arg_count; ++i) {
|
||||||
|
if (args[i].slot == 0 || args[i].slot > em->extra_devices)
|
||||||
|
return (EINVAL);
|
||||||
|
}
|
||||||
|
for (i = 0; i < em->extra_devices; ++i) {
|
||||||
|
path = erofs_device_arg_path(args, arg_count, i + 1);
|
||||||
|
if (path == NULL)
|
||||||
|
return (ENXIO);
|
||||||
|
error = erofs_init_device(em, &em->devs[i], path);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
return (0);
|
||||||
|
out:
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_init_packed_inode(struct erofs_mount *em)
|
||||||
|
{
|
||||||
|
int error;
|
||||||
|
|
||||||
|
/* Load the packed carrier before any fragment-backed metabox inode. */
|
||||||
|
if ((em->feature_incompat & EROFS_FEATURE_INCOMPAT_FRAGMENTS) != 0 &&
|
||||||
|
em->packed_nid > 0) {
|
||||||
|
em->packed_inode = malloc(sizeof(*em->packed_inode), M_EROFS,
|
||||||
|
M_WAITOK | M_ZERO);
|
||||||
|
if (em->packed_inode == NULL)
|
||||||
|
return (ENOMEM);
|
||||||
|
error = erofs_read_inode(em, em->packed_nid, em->packed_inode);
|
||||||
|
if (error != 0) {
|
||||||
|
free(em->packed_inode, M_EROFS);
|
||||||
|
em->packed_inode = NULL;
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
if (em->packed_inode->vtype != VREG || em->packed_inode->fragment) {
|
||||||
|
vfs_mount_error(em->mnt,
|
||||||
|
"erofs: packed inode nid=%ju is not a non-recursive regular file",
|
||||||
|
(uintmax_t)em->packed_nid);
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_init_metabox_inode(struct erofs_mount *em)
|
||||||
|
{
|
||||||
|
int error;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* METABOX NIDs address inode slots in this backing inode's data. The
|
||||||
|
* packed carrier is ready first so a compressed metabox may legally end in
|
||||||
|
* a fragment pcluster without reading an uninitialized dependency.
|
||||||
|
*/
|
||||||
|
if (erofs_sb_has_metabox(em)) {
|
||||||
|
struct erofs_map_blocks map;
|
||||||
|
|
||||||
|
em->metabox_en = malloc(sizeof(*em->metabox_en), M_EROFS,
|
||||||
|
M_WAITOK | M_ZERO);
|
||||||
|
if (em->metabox_en == NULL)
|
||||||
|
return (ENOMEM);
|
||||||
|
error = erofs_read_inode(em, em->metabox_nid, em->metabox_en);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (em->metabox_en->vtype != VREG) {
|
||||||
|
vfs_mount_error(em->mnt,
|
||||||
|
"erofs: metabox inode nid=%ju is not a regular file",
|
||||||
|
(uintmax_t)em->metabox_nid);
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
if (em->metabox_en->fragment) {
|
||||||
|
if (em->packed_inode == NULL ||
|
||||||
|
em->packed_inode->nid == em->metabox_en->nid ||
|
||||||
|
em->metabox_en->size == 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
bzero(&map, sizeof(map));
|
||||||
|
map.m_la = em->metabox_en->size - 1;
|
||||||
|
error = z_erofs_map_blocks_iter(em, em->metabox_en, &map,
|
||||||
|
EROFS_GET_BLOCKS_FIEMAP);
|
||||||
|
if (error != 0 || (map.m_flags & EROFS_MAP_FRAGMENT) == 0)
|
||||||
|
return (error != 0 ? error : EINTEGRITY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_mountfs(struct erofs_device_info *primary, struct mount *mp,
|
||||||
|
const struct erofs_device_arg *args, unsigned int arg_count)
|
||||||
|
{
|
||||||
|
struct erofs_mount *em;
|
||||||
|
struct erofs_super_block *dsb;
|
||||||
|
uint32_t unsupported;
|
||||||
|
void *buf;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
em = malloc(sizeof(*em), M_EROFS, M_WAITOK | M_ZERO);
|
||||||
|
em->mnt = mp;
|
||||||
|
z_erofs_extent_cache_init(em);
|
||||||
|
em->dif0 = *primary;
|
||||||
|
bzero(primary, sizeof(*primary));
|
||||||
|
buf = NULL;
|
||||||
|
|
||||||
|
error = erofs_bread(em, EROFS_SUPER_OFFSET, sizeof(*dsb), &buf);
|
||||||
|
if (error != 0)
|
||||||
|
goto fail;
|
||||||
|
dsb = buf;
|
||||||
|
if (le32toh(dsb->magic) != EROFS_SUPER_MAGIC_V1) {
|
||||||
|
error = EINVAL;
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
if (dsb->blkszbits < 9 || dsb->blkszbits > PAGE_SHIFT) {
|
||||||
|
error = EINVAL;
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
if (dsb->dirblkbits != 0) {
|
||||||
|
error = EOPNOTSUPP;
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
em->feature_compat = le32toh(dsb->feature_compat);
|
||||||
|
em->feature_incompat = le32toh(dsb->feature_incompat);
|
||||||
|
em->packed_nid = le64toh(dsb->packed_nid);
|
||||||
|
em->extra_devices = erofs_sb_has_device_table(em) ?
|
||||||
|
le16toh(dsb->extra_devices) : 0;
|
||||||
|
unsupported = em->feature_incompat & ~EROFS_ALL_SUPPORTED_INCOMPAT;
|
||||||
|
/*
|
||||||
|
* Narrowly allow one extra combination: long xattr prefixes enabled
|
||||||
|
* with non-plain prefix table stored in a packed inode, which adds
|
||||||
|
* the FRAGMENTS (0x20) incompat bit. This is NOT a declaration of
|
||||||
|
* general fragments support; per-inode data layout is still gated
|
||||||
|
* by plain/inline checks in erofs_read_inode().
|
||||||
|
*/
|
||||||
|
if (unsupported != 0) {
|
||||||
|
if (unsupported != EROFS_FEATURE_INCOMPAT_FRAGMENTS ||
|
||||||
|
(em->feature_incompat &
|
||||||
|
EROFS_FEATURE_INCOMPAT_XATTR_PREFIXES) == 0 ||
|
||||||
|
(em->feature_compat &
|
||||||
|
EROFS_FEATURE_COMPAT_PLAIN_XATTR_PFX) != 0 ||
|
||||||
|
em->packed_nid == 0) {
|
||||||
|
error = EOPNOTSUPP;
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
em->block_bits = dsb->blkszbits;
|
||||||
|
em->block_size = 1u << em->block_bits;
|
||||||
|
em->sb_size = 128 + dsb->sb_extslots * EROFS_SB_EXTSLOT_SIZE;
|
||||||
|
if (em->sb_size > PAGE_SIZE - EROFS_SUPER_OFFSET) {
|
||||||
|
error = EINVAL;
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
em->meta_blkaddr = le32toh(dsb->meta_blkaddr);
|
||||||
|
em->xattr_blkaddr = le32toh(dsb->xattr_blkaddr);
|
||||||
|
em->xattr_prefix_start = le32toh(dsb->xattr_prefix_start);
|
||||||
|
em->xattr_prefix_count = dsb->xattr_prefix_count;
|
||||||
|
if (erofs_sb_has_ishare_xattrs(em) &&
|
||||||
|
dsb->ishare_xattr_prefix_id >= em->xattr_prefix_count) {
|
||||||
|
error = EINTEGRITY;
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
/* A non-zero reserved value disables the current name-filter format. */
|
||||||
|
if (erofs_sb_has_xattr_filter(em) && dsb->xattr_filter_reserved != 0)
|
||||||
|
em->feature_compat &= ~EROFS_FEATURE_COMPAT_XATTR_FILTER;
|
||||||
|
erofs_sb_blocks_root(dsb, em->feature_incompat, &em->blocks,
|
||||||
|
&em->root_nid);
|
||||||
|
em->dif0.blocks = em->blocks;
|
||||||
|
error = erofs_validate_device_size(em, &em->dif0, em->dif0.blocks);
|
||||||
|
if (error != 0)
|
||||||
|
goto fail;
|
||||||
|
error = erofs_superblock_csum_verify(em, dsb);
|
||||||
|
if (error != 0)
|
||||||
|
goto fail;
|
||||||
|
em->inos = le64toh(dsb->inos);
|
||||||
|
em->epoch = le64toh(dsb->epoch);
|
||||||
|
em->fixed_nsec = le32toh(dsb->fixed_nsec);
|
||||||
|
if (em->fixed_nsec >= 1000000000) {
|
||||||
|
error = EINTEGRITY;
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
error = erofs_load_generation_seed(em, em->sb_size,
|
||||||
|
&em->generation_seed);
|
||||||
|
if (error != 0)
|
||||||
|
goto fail;
|
||||||
|
if (em->packed_nid != 0 && erofs_nid_in_metabox(em->packed_nid)) {
|
||||||
|
error = EINTEGRITY;
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
if (erofs_sb_has_metabox(em)) {
|
||||||
|
if (em->sb_size <= offsetof(struct erofs_super_block, metabox_nid)) {
|
||||||
|
error = EINTEGRITY;
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
em->metabox_nid = le64toh(dsb->metabox_nid);
|
||||||
|
if (erofs_nid_in_metabox(em->metabox_nid)) {
|
||||||
|
error = EINTEGRITY;
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error = z_erofs_parse_cfgs(em, dsb);
|
||||||
|
if (error != 0)
|
||||||
|
goto fail;
|
||||||
|
error = erofs_scan_devices(em, dsb, args, arg_count);
|
||||||
|
if (error != 0)
|
||||||
|
goto fail;
|
||||||
|
|
||||||
|
if (erofs_sb_has_shared_ea_in_metabox(em) &&
|
||||||
|
!erofs_sb_has_metabox(em)) {
|
||||||
|
error = EINTEGRITY;
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = erofs_init_packed_inode(em);
|
||||||
|
if (error != 0)
|
||||||
|
goto fail;
|
||||||
|
error = erofs_init_metabox_inode(em);
|
||||||
|
if (error != 0)
|
||||||
|
goto fail;
|
||||||
|
error = erofs_xattr_prefixes_init(em);
|
||||||
|
if (error != 0)
|
||||||
|
goto fail;
|
||||||
|
set_opt(&em->opt, POSIX_ACL);
|
||||||
|
memcpy(em->volume_name, dsb->volume_name, 16);
|
||||||
|
em->volume_name[16] = '\0';
|
||||||
|
|
||||||
|
erofs_brelse(buf);
|
||||||
|
buf = NULL;
|
||||||
|
mp->mnt_data = em;
|
||||||
|
mp->mnt_stat.f_fsid.val[0] = dev2udev(em->dif0.devvp->v_rdev);
|
||||||
|
mp->mnt_stat.f_fsid.val[1] = mp->mnt_vfc->vfc_typenum;
|
||||||
|
MNT_ILOCK(mp);
|
||||||
|
mp->mnt_flag |= MNT_LOCAL | MNT_RDONLY | MNT_ACLS;
|
||||||
|
mp->mnt_kern_flag |= MNTK_LOOKUP_SHARED | MNTK_EXTENDED_SHARED |
|
||||||
|
MNTK_USES_BCACHE;
|
||||||
|
MNT_IUNLOCK(mp);
|
||||||
|
return (0);
|
||||||
|
fail:
|
||||||
|
if (buf != NULL)
|
||||||
|
erofs_brelse(buf);
|
||||||
|
erofs_sb_free(em);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_mount(struct mount *mp)
|
||||||
|
{
|
||||||
|
struct erofs_device_arg *args;
|
||||||
|
struct erofs_device_info primary;
|
||||||
|
char *fspec;
|
||||||
|
unsigned int arg_count;
|
||||||
|
int error, len;
|
||||||
|
|
||||||
|
MNT_ILOCK(mp);
|
||||||
|
mp->mnt_flag |= MNT_RDONLY;
|
||||||
|
MNT_IUNLOCK(mp);
|
||||||
|
if (mp->mnt_flag & MNT_UPDATE) {
|
||||||
|
if (vfs_flagopt(mp->mnt_optnew, "export", NULL, 0))
|
||||||
|
return (0);
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
}
|
||||||
|
args = NULL;
|
||||||
|
arg_count = 0;
|
||||||
|
error = erofs_parse_device_options(mp, &args, &arg_count);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (vfs_filteropt(mp->mnt_optnew, erofs_opts) != 0) {
|
||||||
|
erofs_free_device_args(args, arg_count);
|
||||||
|
return (EINVAL);
|
||||||
|
}
|
||||||
|
fspec = NULL;
|
||||||
|
error = vfs_getopt(mp->mnt_optnew, "from", (void **)&fspec, &len);
|
||||||
|
if (error != 0 || fspec == NULL || len == 0 ||
|
||||||
|
fspec[len - 1] != '\0') {
|
||||||
|
erofs_free_device_args(args, arg_count);
|
||||||
|
return (EINVAL);
|
||||||
|
}
|
||||||
|
mp->mnt_iosize_max = MAXPHYS;
|
||||||
|
error = erofs_open_device(NULL, fspec, &primary);
|
||||||
|
if (error != 0) {
|
||||||
|
erofs_free_device_args(args, arg_count);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
erofs_update_iosize_max(mp, &primary);
|
||||||
|
error = erofs_mountfs(&primary, mp, args, arg_count);
|
||||||
|
erofs_free_device_args(args, arg_count);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
vfs_mountedfrom(mp, fspec);
|
||||||
|
return (erofs_statfs(mp, &mp->mnt_stat));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_root(struct mount *mp, int flags, struct vnode **vpp)
|
||||||
|
{
|
||||||
|
int error;
|
||||||
|
|
||||||
|
error = erofs_vget(mp, MTOE(mp)->root_nid, flags, vpp);
|
||||||
|
if (error != 0)
|
||||||
|
vfs_mount_error(mp, "erofs: failed to load root nid %ju: error %d",
|
||||||
|
(uintmax_t)MTOE(mp)->root_nid, error);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_statfs(struct mount *mp, struct statfs *sbp)
|
||||||
|
{
|
||||||
|
struct erofs_mount *em;
|
||||||
|
|
||||||
|
em = MTOE(mp);
|
||||||
|
sbp->f_bsize = em->block_size;
|
||||||
|
sbp->f_iosize = em->block_size;
|
||||||
|
sbp->f_blocks = em->total_blocks;
|
||||||
|
sbp->f_bfree = 0;
|
||||||
|
sbp->f_bavail = 0;
|
||||||
|
sbp->f_files = em->inos;
|
||||||
|
sbp->f_ffree = 0;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_unmount(struct mount *mp, int mntflags)
|
||||||
|
{
|
||||||
|
struct erofs_mount *em;
|
||||||
|
int error, flags;
|
||||||
|
|
||||||
|
flags = ((mntflags & MNT_FORCE) != 0) ? FORCECLOSE : 0;
|
||||||
|
error = vflush(mp, 0, flags, curthread);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
em = MTOE(mp);
|
||||||
|
mp->mnt_data = NULL;
|
||||||
|
erofs_sb_free(em);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_vgetf(struct mount *mp, ino_t ino, int flags, struct vnode **vpp)
|
||||||
|
{
|
||||||
|
return (erofs_vget(mp, ino, flags, vpp));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Persistent EROFS file handle to locked vnode. */
|
||||||
|
static int
|
||||||
|
erofs_fhtovp(struct mount *mp, struct fid *fhp, int flags, struct vnode **vpp)
|
||||||
|
{
|
||||||
|
struct erofs_fid efid;
|
||||||
|
struct erofs_node *en;
|
||||||
|
struct vnode *vp;
|
||||||
|
uint64_t nid;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
*vpp = NULLVP;
|
||||||
|
bzero(&efid, sizeof(efid));
|
||||||
|
memcpy(&efid, fhp, sizeof(efid));
|
||||||
|
if (efid.len != sizeof(efid) || efid.pad != 0)
|
||||||
|
return (EINVAL);
|
||||||
|
nid = ((uint64_t)efid.nid_hi << 32) | efid.nid_lo;
|
||||||
|
if (!erofs_nid_is_valid(MTOE(mp), nid))
|
||||||
|
return (ESTALE);
|
||||||
|
error = VFS_VGET(mp, (ino_t)nid, flags, &vp);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
en = VTOE(vp);
|
||||||
|
if (en->mode == 0 || en->nlink == 0 || en->nid != nid ||
|
||||||
|
en->generation != efid.gen) {
|
||||||
|
vput(vp);
|
||||||
|
return (ESTALE);
|
||||||
|
}
|
||||||
|
*vpp = vp;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct vfsops erofs_vfsops = {
|
||||||
|
.vfs_fhtovp = erofs_fhtovp,
|
||||||
|
.vfs_mount = erofs_mount,
|
||||||
|
.vfs_root = erofs_root,
|
||||||
|
.vfs_statfs = erofs_statfs,
|
||||||
|
.vfs_unmount = erofs_unmount,
|
||||||
|
.vfs_vget = erofs_vgetf,
|
||||||
|
};
|
||||||
|
VFS_SET(erofs_vfsops, erofs, VFCF_READONLY);
|
||||||
|
MODULE_DEPEND(erofs, acl_posix1e, 1, 1, 1);
|
||||||
|
MODULE_DEPEND(erofs, zlib, 1, 1, 1);
|
||||||
|
MODULE_VERSION(erofs, 1);
|
||||||
+842
@@ -0,0 +1,842 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-2.0-only */
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017-2018 HUAWEI, Inc.
|
||||||
|
* https://www.huawei.com/
|
||||||
|
* Copyright (C) 2021-2022, Alibaba Cloud
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <sys/param.h>
|
||||||
|
#include <sys/systm.h>
|
||||||
|
#include <sys/dirent.h>
|
||||||
|
#include <sys/extattr.h>
|
||||||
|
#include <sys/kernel.h>
|
||||||
|
#include <sys/libkern.h>
|
||||||
|
#include <sys/malloc.h>
|
||||||
|
#include <sys/mount.h>
|
||||||
|
#include <sys/vnode.h>
|
||||||
|
#include <sys/acl.h>
|
||||||
|
|
||||||
|
#include "internal.h"
|
||||||
|
#include "xattr.h"
|
||||||
|
|
||||||
|
struct posix_acl_xattr_entry {
|
||||||
|
uint16_t e_tag;
|
||||||
|
uint16_t e_perm;
|
||||||
|
uint32_t e_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct posix_acl_xattr_header {
|
||||||
|
uint32_t a_version;
|
||||||
|
};
|
||||||
|
|
||||||
|
#define POSIX_ACL_XATTR_VERSION 0x0002
|
||||||
|
#define EROFS_XATTR_FILTER_POSIX_ACL \
|
||||||
|
((1U << 21) | (1U << 30))
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_xattr_backing_size(struct erofs_mount *em, struct erofs_node *backing_en,
|
||||||
|
uint64_t *sizep)
|
||||||
|
{
|
||||||
|
if (backing_en != NULL) {
|
||||||
|
*sizep = backing_en->size;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
if (em->blocks > (UINT64_MAX >> em->block_bits))
|
||||||
|
return (EOVERFLOW);
|
||||||
|
*sizep = em->blocks << em->block_bits;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_xattr_read_backing(struct erofs_mount *em,
|
||||||
|
struct erofs_node *backing_en, uint64_t off, size_t len, void **bufp)
|
||||||
|
{
|
||||||
|
uint64_t backing_size;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
error = erofs_xattr_backing_size(em, backing_en, &backing_size);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (off > backing_size || (uint64_t)len > backing_size - off)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
if (backing_en != NULL)
|
||||||
|
return (erofs_read_data(em, backing_en, off, len, bufp));
|
||||||
|
if (off > INT64_MAX)
|
||||||
|
return (EOVERFLOW);
|
||||||
|
return (erofs_bread(em, (off_t)off, len, bufp));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Read one prefix table metadata record.
|
||||||
|
*
|
||||||
|
* When backing_en == NULL the record is in the physical metadata area;
|
||||||
|
* otherwise it lives in the selected metadata carrier's logical data stream.
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
erofs_xattr_read_metadata(struct erofs_mount *em, struct erofs_node *backing_en,
|
||||||
|
uint64_t *offp, void **bufp, size_t *lenp)
|
||||||
|
{
|
||||||
|
uint16_t raw_len;
|
||||||
|
void *buf, *hdrbuf;
|
||||||
|
uint64_t off;
|
||||||
|
size_t len;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
if (*offp > UINT64_MAX - (sizeof(struct erofs_xattr_entry) - 1))
|
||||||
|
return (EOVERFLOW);
|
||||||
|
off = roundup2(*offp, sizeof(struct erofs_xattr_entry));
|
||||||
|
error = erofs_xattr_read_backing(em, backing_en, off, sizeof(raw_len),
|
||||||
|
&hdrbuf);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
raw_len = le16toh(*(uint16_t *)hdrbuf);
|
||||||
|
erofs_brelse(hdrbuf);
|
||||||
|
len = (raw_len == 0) ? (size_t)UINT16_MAX + 1 : raw_len;
|
||||||
|
if (len < sizeof(struct erofs_xattr_long_prefix) ||
|
||||||
|
len > EROFS_NAME_LEN + sizeof(struct erofs_xattr_long_prefix))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
if (off > UINT64_MAX - sizeof(raw_len))
|
||||||
|
return (EOVERFLOW);
|
||||||
|
error = erofs_xattr_read_backing(em, backing_en,
|
||||||
|
off + sizeof(raw_len), len, &buf);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
*offp = off + sizeof(raw_len) + len;
|
||||||
|
*bufp = buf;
|
||||||
|
*lenp = len;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
erofs_xattr_prefixes_cleanup(struct erofs_mount *em)
|
||||||
|
{
|
||||||
|
if (em->xattr_prefixes == NULL)
|
||||||
|
return;
|
||||||
|
for (uint8_t i = 0; i < em->xattr_prefix_count; i++)
|
||||||
|
free(em->xattr_prefixes[i].infix, M_EROFS);
|
||||||
|
free(em->xattr_prefixes, M_EROFS);
|
||||||
|
em->xattr_prefixes = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
erofs_xattr_prefixes_init(struct erofs_mount *em)
|
||||||
|
{
|
||||||
|
struct erofs_xattr_long_prefix *prefix = NULL;
|
||||||
|
struct erofs_node packed_en, *prefix_en;
|
||||||
|
uint64_t off;
|
||||||
|
size_t infix_len, len;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
if ((em->feature_incompat & EROFS_FEATURE_INCOMPAT_XATTR_PREFIXES) ==
|
||||||
|
0 ||
|
||||||
|
em->xattr_prefix_count == 0)
|
||||||
|
return (0);
|
||||||
|
prefix_en = NULL;
|
||||||
|
if ((em->feature_compat & EROFS_FEATURE_COMPAT_PLAIN_XATTR_PFX) == 0) {
|
||||||
|
if (erofs_sb_has_metabox(em)) {
|
||||||
|
if (em->metabox_en == NULL)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
prefix_en = em->metabox_en;
|
||||||
|
} else if (em->packed_inode != NULL) {
|
||||||
|
prefix_en = em->packed_inode;
|
||||||
|
} else if (em->packed_nid != 0) {
|
||||||
|
error = erofs_read_inode(em, em->packed_nid, &packed_en);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (packed_en.vtype != VREG)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
prefix_en = &packed_en;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
em->xattr_prefixes = malloc(sizeof(*em->xattr_prefixes) *
|
||||||
|
em->xattr_prefix_count,
|
||||||
|
M_EROFS, M_WAITOK | M_ZERO);
|
||||||
|
off = (uint64_t)em->xattr_prefix_start << 2;
|
||||||
|
for (uint8_t i = 0; i < em->xattr_prefix_count; i++) {
|
||||||
|
error = erofs_xattr_read_metadata(em, prefix_en, &off,
|
||||||
|
(void **)&prefix, &len);
|
||||||
|
if (error != 0)
|
||||||
|
goto fail;
|
||||||
|
infix_len = len - sizeof(*prefix);
|
||||||
|
em->xattr_prefixes[i].base_index = prefix->base_index;
|
||||||
|
em->xattr_prefixes[i].infix_len = infix_len;
|
||||||
|
em->xattr_prefixes[i].infix = malloc(infix_len + 1, M_EROFS,
|
||||||
|
M_WAITOK);
|
||||||
|
memcpy(em->xattr_prefixes[i].infix, prefix->infix, infix_len);
|
||||||
|
em->xattr_prefixes[i].infix[infix_len] = '\0';
|
||||||
|
erofs_brelse(prefix);
|
||||||
|
prefix = NULL;
|
||||||
|
}
|
||||||
|
return (0);
|
||||||
|
fail:
|
||||||
|
if (prefix != NULL)
|
||||||
|
erofs_brelse(prefix);
|
||||||
|
erofs_xattr_prefixes_cleanup(em);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_xattr_move(void *value, size_t value_size, struct uio *uio, size_t *sizep)
|
||||||
|
{
|
||||||
|
if (sizep != NULL)
|
||||||
|
*sizep = value_size;
|
||||||
|
if (uio == NULL || value_size == 0)
|
||||||
|
return (0);
|
||||||
|
return (uiomove(value, value_size, uio));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_xattr_load_body(struct erofs_mount *em, struct erofs_node *en,
|
||||||
|
char **bodyp, struct erofs_xattr_ibody_header **ihp, size_t *header_sizep)
|
||||||
|
{
|
||||||
|
struct erofs_xattr_ibody_header *ih;
|
||||||
|
char *body;
|
||||||
|
uint64_t body_off;
|
||||||
|
size_t header_size;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
if (en->xattr_isize < sizeof(*ih))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
if (en->inode_off > UINT64_MAX - en->inode_isize)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
body_off = en->inode_off + en->inode_isize;
|
||||||
|
error = erofs_xattr_read_backing(em,
|
||||||
|
erofs_nid_in_metabox(en->nid) ? em->metabox_en : NULL, body_off,
|
||||||
|
en->xattr_isize, (void **)&body);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
ih = (struct erofs_xattr_ibody_header *)body;
|
||||||
|
if (en->xattr_isize == sizeof(*ih)) {
|
||||||
|
error = EOPNOTSUPP;
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
header_size = sizeof(*ih) + sizeof(uint32_t) * ih->h_shared_count;
|
||||||
|
if (header_size > en->xattr_isize) {
|
||||||
|
error = EINTEGRITY;
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
*bodyp = body;
|
||||||
|
*ihp = ih;
|
||||||
|
*header_sizep = header_size;
|
||||||
|
return (0);
|
||||||
|
fail:
|
||||||
|
erofs_brelse(body);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_xattr_validate_entry(struct erofs_xattr_entry *entry, size_t remaining,
|
||||||
|
size_t *entry_sizep, size_t *value_sizep)
|
||||||
|
{
|
||||||
|
size_t entry_size, min_size, value_size;
|
||||||
|
|
||||||
|
if (remaining < sizeof(*entry))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
value_size = le16toh(entry->e_value_size);
|
||||||
|
min_size = sizeof(*entry) + entry->e_name_len + value_size;
|
||||||
|
if (min_size > remaining)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
entry_size = erofs_xattr_entry_size(entry);
|
||||||
|
if (entry_size > remaining)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
if (entry_sizep != NULL)
|
||||||
|
*entry_sizep = entry_size;
|
||||||
|
if (value_sizep != NULL)
|
||||||
|
*value_sizep = value_size;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
erofs_xattr_prefix(uint8_t base_index, int *namespacep,
|
||||||
|
const char **prefixp, size_t *prefix_lenp)
|
||||||
|
{
|
||||||
|
switch (base_index) {
|
||||||
|
case EROFS_XATTR_INDEX_USER:
|
||||||
|
*namespacep = EXTATTR_NAMESPACE_USER;
|
||||||
|
*prefixp = NULL;
|
||||||
|
*prefix_lenp = 0;
|
||||||
|
return (true);
|
||||||
|
case EROFS_XATTR_INDEX_POSIX_ACL_ACCESS:
|
||||||
|
*namespacep = EXTATTR_NAMESPACE_SYSTEM;
|
||||||
|
*prefixp = "posix_acl_access";
|
||||||
|
*prefix_lenp = sizeof("posix_acl_access") - 1;
|
||||||
|
return (true);
|
||||||
|
case EROFS_XATTR_INDEX_POSIX_ACL_DEFAULT:
|
||||||
|
*namespacep = EXTATTR_NAMESPACE_SYSTEM;
|
||||||
|
*prefixp = "posix_acl_default";
|
||||||
|
*prefix_lenp = sizeof("posix_acl_default") - 1;
|
||||||
|
return (true);
|
||||||
|
case EROFS_XATTR_INDEX_TRUSTED:
|
||||||
|
*namespacep = EXTATTR_NAMESPACE_SYSTEM;
|
||||||
|
*prefixp = "trusted.";
|
||||||
|
*prefix_lenp = sizeof("trusted.") - 1;
|
||||||
|
return (true);
|
||||||
|
case EROFS_XATTR_INDEX_SECURITY:
|
||||||
|
*namespacep = EXTATTR_NAMESPACE_SYSTEM;
|
||||||
|
*prefixp = "security.";
|
||||||
|
*prefix_lenp = sizeof("security.") - 1;
|
||||||
|
return (true);
|
||||||
|
case EROFS_XATTR_INDEX_LUSTRE:
|
||||||
|
default:
|
||||||
|
return (false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_xattr_namespace_prefix(int attrnamespace, uint8_t base_index,
|
||||||
|
const char **prefixp, size_t *prefix_lenp)
|
||||||
|
{
|
||||||
|
int mapped_namespace;
|
||||||
|
|
||||||
|
if (attrnamespace != EXTATTR_NAMESPACE_USER &&
|
||||||
|
attrnamespace != EXTATTR_NAMESPACE_SYSTEM)
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
if (!erofs_xattr_prefix(base_index, &mapped_namespace, prefixp,
|
||||||
|
prefix_lenp))
|
||||||
|
return (ENOATTR);
|
||||||
|
if (mapped_namespace != attrnamespace)
|
||||||
|
return (ENOATTR);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_xattr_list_move(const char *namespace_prefix, size_t namespace_prefix_len,
|
||||||
|
const char *infix, size_t infix_len, const char *name, uint8_t name_len,
|
||||||
|
struct uio *uio, size_t *sizep)
|
||||||
|
{
|
||||||
|
uint8_t total_name_len;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
if (namespace_prefix_len + infix_len + name_len > EROFS_NAME_LEN)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
total_name_len = namespace_prefix_len + infix_len + name_len;
|
||||||
|
if (sizep != NULL) {
|
||||||
|
*sizep += total_name_len + 1;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
if (uio == NULL)
|
||||||
|
return (0);
|
||||||
|
error = uiomove(__DECONST(void *, &total_name_len), 1, uio);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (namespace_prefix_len != 0) {
|
||||||
|
error = uiomove(__DECONST(void *, namespace_prefix),
|
||||||
|
namespace_prefix_len, uio);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
if (infix_len != 0) {
|
||||||
|
error = uiomove(__DECONST(void *, infix), infix_len, uio);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
return (uiomove(__DECONST(void *, name), name_len, uio));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_xattr_resolve_name(struct erofs_mount *em,
|
||||||
|
const struct erofs_xattr_entry *entry, uint8_t *base_indexp,
|
||||||
|
const char **infixp, size_t *infix_lenp)
|
||||||
|
{
|
||||||
|
struct erofs_xattr_prefix_item *prefix;
|
||||||
|
uint8_t prefix_id;
|
||||||
|
|
||||||
|
if ((entry->e_name_index & EROFS_XATTR_LONG_PREFIX) == 0) {
|
||||||
|
*base_indexp = entry->e_name_index;
|
||||||
|
*infixp = NULL;
|
||||||
|
*infix_lenp = 0;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
if (em->xattr_prefixes == NULL)
|
||||||
|
return (ENOATTR);
|
||||||
|
prefix_id = entry->e_name_index & EROFS_XATTR_LONG_PREFIX_MASK;
|
||||||
|
if (prefix_id >= em->xattr_prefix_count)
|
||||||
|
return (ENOATTR);
|
||||||
|
prefix = &em->xattr_prefixes[prefix_id];
|
||||||
|
*base_indexp = prefix->base_index;
|
||||||
|
*infixp = prefix->infix;
|
||||||
|
*infix_lenp = prefix->infix_len;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
erofs_xattr_name_match(const char *namespace_prefix,
|
||||||
|
size_t namespace_prefix_len, const char *infix, size_t infix_len,
|
||||||
|
const struct erofs_xattr_entry *entry, const char *name, size_t name_len)
|
||||||
|
{
|
||||||
|
if (name_len != namespace_prefix_len + infix_len + entry->e_name_len)
|
||||||
|
return (false);
|
||||||
|
if (namespace_prefix_len != 0 &&
|
||||||
|
memcmp(name, namespace_prefix, namespace_prefix_len) != 0)
|
||||||
|
return (false);
|
||||||
|
if (infix_len != 0 &&
|
||||||
|
memcmp(name + namespace_prefix_len, infix, infix_len) != 0)
|
||||||
|
return (false);
|
||||||
|
return (memcmp(name + namespace_prefix_len + infix_len, entry->e_name,
|
||||||
|
entry->e_name_len) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_xattr_shared_entry_offset(struct erofs_mount *em, uint32_t shared_id,
|
||||||
|
uint64_t *phys_offp)
|
||||||
|
{
|
||||||
|
uint64_t base, relative;
|
||||||
|
|
||||||
|
if (em->xattr_blkaddr > (UINT64_MAX >> em->block_bits))
|
||||||
|
return (EOVERFLOW);
|
||||||
|
base = (uint64_t)em->xattr_blkaddr << em->block_bits;
|
||||||
|
relative = (uint64_t)shared_id * sizeof(uint32_t);
|
||||||
|
if (relative > UINT64_MAX - base)
|
||||||
|
return (EOVERFLOW);
|
||||||
|
*phys_offp = base + relative;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_xattr_load_shared_entry(struct erofs_mount *em, uint32_t shared_id,
|
||||||
|
struct erofs_xattr_entry **entryp, size_t *entry_sizep, size_t *value_sizep)
|
||||||
|
{
|
||||||
|
struct erofs_xattr_entry *entry;
|
||||||
|
struct erofs_node *backing_en;
|
||||||
|
void *hdrbuf;
|
||||||
|
uint64_t off;
|
||||||
|
size_t entry_size, value_size;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
backing_en = erofs_sb_has_shared_ea_in_metabox(em) ? em->metabox_en : NULL;
|
||||||
|
if (erofs_sb_has_shared_ea_in_metabox(em) && backing_en == NULL)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
|
||||||
|
error = erofs_xattr_shared_entry_offset(em, shared_id, &off);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
|
||||||
|
error = erofs_xattr_read_backing(em, backing_en, off, sizeof(*entry),
|
||||||
|
&hdrbuf);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
entry = hdrbuf;
|
||||||
|
value_size = le16toh(entry->e_value_size);
|
||||||
|
entry_size = erofs_xattr_entry_size(entry);
|
||||||
|
|
||||||
|
erofs_brelse(hdrbuf);
|
||||||
|
|
||||||
|
error = erofs_xattr_read_backing(em, backing_en, off, entry_size,
|
||||||
|
(void **)entryp);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (entry_sizep != NULL)
|
||||||
|
*entry_sizep = entry_size;
|
||||||
|
if (value_sizep != NULL)
|
||||||
|
*value_sizep = value_size;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_inode_has_noacl(struct erofs_mount *em, struct erofs_node *en,
|
||||||
|
bool *noaclp)
|
||||||
|
{
|
||||||
|
struct erofs_xattr_ibody_header *ih;
|
||||||
|
struct erofs_node *backing_en;
|
||||||
|
uint64_t body_off;
|
||||||
|
uint32_t name_filter;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
*noaclp = false;
|
||||||
|
if (en->xattr_isize < sizeof(*ih)) {
|
||||||
|
*noaclp = true;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
if (!erofs_sb_has_xattr_filter(em))
|
||||||
|
return (0);
|
||||||
|
if (en->inode_off > UINT64_MAX - en->inode_isize)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
body_off = en->inode_off + en->inode_isize;
|
||||||
|
backing_en = erofs_nid_in_metabox(en->nid) ? em->metabox_en : NULL;
|
||||||
|
error = erofs_xattr_read_backing(em, backing_en, body_off, sizeof(*ih),
|
||||||
|
(void **)&ih);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
name_filter = le32toh(ih->h_name_filter);
|
||||||
|
erofs_brelse(ih);
|
||||||
|
*noaclp = (name_filter & EROFS_XATTR_FILTER_POSIX_ACL) ==
|
||||||
|
EROFS_XATTR_FILTER_POSIX_ACL;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
erofs_acl_from_mode(struct erofs_node *en, acl_type_t type, struct acl *aclp)
|
||||||
|
{
|
||||||
|
if (type == ACL_TYPE_DEFAULT) {
|
||||||
|
aclp->acl_cnt = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
aclp->acl_cnt = 3;
|
||||||
|
aclp->acl_entry[0].ae_tag = ACL_USER_OBJ;
|
||||||
|
aclp->acl_entry[0].ae_id = ACL_UNDEFINED_ID;
|
||||||
|
aclp->acl_entry[0].ae_perm = (en->mode >> 6) & ACL_PERM_BITS;
|
||||||
|
aclp->acl_entry[1].ae_tag = ACL_GROUP_OBJ;
|
||||||
|
aclp->acl_entry[1].ae_id = ACL_UNDEFINED_ID;
|
||||||
|
aclp->acl_entry[1].ae_perm = (en->mode >> 3) & ACL_PERM_BITS;
|
||||||
|
aclp->acl_entry[2].ae_tag = ACL_OTHER;
|
||||||
|
aclp->acl_entry[2].ae_id = ACL_UNDEFINED_ID;
|
||||||
|
aclp->acl_entry[2].ae_perm = en->mode & ACL_PERM_BITS;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct erofs_xattr_iter {
|
||||||
|
struct erofs_mount *em;
|
||||||
|
struct erofs_node *en;
|
||||||
|
int attrnamespace;
|
||||||
|
const char *name;
|
||||||
|
size_t name_len;
|
||||||
|
struct uio *uio;
|
||||||
|
size_t *sizep;
|
||||||
|
};
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_getxattr_foreach(struct erofs_xattr_iter *it,
|
||||||
|
struct erofs_xattr_entry *entry, size_t value_size)
|
||||||
|
{
|
||||||
|
const char *infix, *namespace_prefix;
|
||||||
|
size_t infix_len, namespace_prefix_len;
|
||||||
|
uint8_t base_index;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
error = erofs_xattr_resolve_name(it->em, entry, &base_index, &infix,
|
||||||
|
&infix_len);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
error = erofs_xattr_namespace_prefix(it->attrnamespace, base_index,
|
||||||
|
&namespace_prefix, &namespace_prefix_len);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (!erofs_xattr_name_match(namespace_prefix, namespace_prefix_len,
|
||||||
|
infix, infix_len, entry, it->name, it->name_len))
|
||||||
|
return (ENOATTR);
|
||||||
|
return (erofs_xattr_move(entry->e_name + entry->e_name_len, value_size,
|
||||||
|
it->uio, it->sizep));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_listxattr_foreach(struct erofs_xattr_iter *it,
|
||||||
|
struct erofs_xattr_entry *entry)
|
||||||
|
{
|
||||||
|
const char *infix, *namespace_prefix;
|
||||||
|
size_t infix_len, namespace_prefix_len;
|
||||||
|
uint8_t base_index;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
error = erofs_xattr_resolve_name(it->em, entry, &base_index, &infix,
|
||||||
|
&infix_len);
|
||||||
|
if (error == ENOATTR)
|
||||||
|
return (0);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
error = erofs_xattr_namespace_prefix(it->attrnamespace, base_index,
|
||||||
|
&namespace_prefix, &namespace_prefix_len);
|
||||||
|
if (error == ENOATTR)
|
||||||
|
return (0);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
return (erofs_xattr_list_move(namespace_prefix, namespace_prefix_len,
|
||||||
|
infix, infix_len, entry->e_name, entry->e_name_len, it->uio,
|
||||||
|
it->sizep));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_xattr_iter_inline(struct erofs_xattr_iter *it, char *body,
|
||||||
|
size_t header_size, bool get)
|
||||||
|
{
|
||||||
|
struct erofs_xattr_entry *entry;
|
||||||
|
char *cursor;
|
||||||
|
size_t entry_size, remaining, value_size;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
remaining = it->en->xattr_isize - header_size;
|
||||||
|
cursor = body + header_size;
|
||||||
|
while (remaining != 0) {
|
||||||
|
entry = (struct erofs_xattr_entry *)cursor;
|
||||||
|
error = erofs_xattr_validate_entry(entry, remaining,
|
||||||
|
&entry_size, get ? &value_size : NULL);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (get)
|
||||||
|
error = erofs_getxattr_foreach(it, entry, value_size);
|
||||||
|
else
|
||||||
|
error = erofs_listxattr_foreach(it, entry);
|
||||||
|
if (get) {
|
||||||
|
if (error != ENOATTR)
|
||||||
|
return (error);
|
||||||
|
} else if (error != 0) {
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
cursor += entry_size;
|
||||||
|
remaining -= entry_size;
|
||||||
|
}
|
||||||
|
return (get ? ENOATTR : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
erofs_xattr_iter_shared(struct erofs_xattr_iter *it,
|
||||||
|
struct erofs_xattr_ibody_header *ih, bool get)
|
||||||
|
{
|
||||||
|
struct erofs_xattr_entry *entry;
|
||||||
|
uint32_t shared_id;
|
||||||
|
size_t value_size;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < ih->h_shared_count; i++) {
|
||||||
|
shared_id = le32toh(ih->h_shared_xattrs[i]);
|
||||||
|
error = erofs_xattr_load_shared_entry(it->em, shared_id, &entry,
|
||||||
|
NULL, get ? &value_size : NULL);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (get)
|
||||||
|
error = erofs_getxattr_foreach(it, entry, value_size);
|
||||||
|
else
|
||||||
|
error = erofs_listxattr_foreach(it, entry);
|
||||||
|
erofs_brelse(entry);
|
||||||
|
if (get) {
|
||||||
|
if (error != ENOATTR)
|
||||||
|
return (error);
|
||||||
|
} else if (error != 0) {
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (get ? ENOATTR : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Look up one inline/shared xattr by name.
|
||||||
|
*
|
||||||
|
* Name exposure rules:
|
||||||
|
* - user namespace: bare name, no "user." prefix;
|
||||||
|
* - system namespace: exposes full "trusted.*" / "security.*" names.
|
||||||
|
*/
|
||||||
|
int
|
||||||
|
erofs_getxattr(struct vnode *vp, int attrnamespace, const char *name,
|
||||||
|
struct uio *uio, size_t *sizep)
|
||||||
|
{
|
||||||
|
struct erofs_mount *em;
|
||||||
|
struct erofs_node *en;
|
||||||
|
struct erofs_xattr_ibody_header *ih;
|
||||||
|
struct erofs_xattr_iter it;
|
||||||
|
char *body;
|
||||||
|
size_t header_size, name_len;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
em = MTOE(vp->v_mount);
|
||||||
|
en = VTOE(vp);
|
||||||
|
if (name == NULL || name[0] == '\0')
|
||||||
|
return (EINVAL);
|
||||||
|
name_len = strlen(name);
|
||||||
|
if (name_len > EROFS_NAME_LEN)
|
||||||
|
return (EINVAL);
|
||||||
|
if (en->xattr_isize == 0)
|
||||||
|
return (ENOATTR);
|
||||||
|
error = erofs_xattr_load_body(em, en, &body, &ih, &header_size);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
it.em = em;
|
||||||
|
it.en = en;
|
||||||
|
it.attrnamespace = attrnamespace;
|
||||||
|
it.name = name;
|
||||||
|
it.name_len = name_len;
|
||||||
|
it.uio = uio;
|
||||||
|
it.sizep = sizep;
|
||||||
|
error = erofs_xattr_iter_inline(&it, body, header_size, true);
|
||||||
|
if (error == ENOATTR)
|
||||||
|
error = erofs_xattr_iter_shared(&it, ih, true);
|
||||||
|
erofs_brelse(body);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enumerate inline/shared xattr names for a given namespace.
|
||||||
|
*
|
||||||
|
* Return format: 1-byte name length followed by non-NUL-terminated name bytes.
|
||||||
|
*/
|
||||||
|
int
|
||||||
|
erofs_listxattr(struct vnode *vp, int attrnamespace, struct uio *uio,
|
||||||
|
size_t *sizep)
|
||||||
|
{
|
||||||
|
struct erofs_mount *em;
|
||||||
|
struct erofs_node *en;
|
||||||
|
struct erofs_xattr_ibody_header *ih;
|
||||||
|
struct erofs_xattr_iter it;
|
||||||
|
char *body;
|
||||||
|
size_t header_size;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
em = MTOE(vp->v_mount);
|
||||||
|
en = VTOE(vp);
|
||||||
|
if (sizep != NULL)
|
||||||
|
*sizep = 0;
|
||||||
|
if (en->xattr_isize == 0)
|
||||||
|
return (0);
|
||||||
|
error = erofs_xattr_load_body(em, en, &body, &ih, &header_size);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
it.em = em;
|
||||||
|
it.en = en;
|
||||||
|
it.attrnamespace = attrnamespace;
|
||||||
|
it.name = NULL;
|
||||||
|
it.name_len = 0;
|
||||||
|
it.uio = uio;
|
||||||
|
it.sizep = sizep;
|
||||||
|
error = erofs_xattr_iter_inline(&it, body, header_size, false);
|
||||||
|
if (error == 0)
|
||||||
|
error = erofs_xattr_iter_shared(&it, ih, false);
|
||||||
|
erofs_brelse(body);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
erofs_get_acl(struct vnode *vp, acl_type_t type, struct acl *aclp)
|
||||||
|
{
|
||||||
|
struct erofs_mount *em;
|
||||||
|
struct erofs_node *en;
|
||||||
|
const char *xattr_name;
|
||||||
|
struct uio auio;
|
||||||
|
struct iovec aiov;
|
||||||
|
struct posix_acl_xattr_header hdr;
|
||||||
|
struct posix_acl_xattr_entry entry;
|
||||||
|
uint8_t buf[sizeof(hdr) + sizeof(entry) * ACL_MAX_ENTRIES];
|
||||||
|
size_t size;
|
||||||
|
uint32_t id;
|
||||||
|
bool noacl;
|
||||||
|
int error, count, i, j, phase;
|
||||||
|
|
||||||
|
em = MTOE(vp->v_mount);
|
||||||
|
if (!test_opt(&em->opt, POSIX_ACL))
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
|
||||||
|
en = VTOE(vp);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case ACL_TYPE_ACCESS:
|
||||||
|
xattr_name = "posix_acl_access";
|
||||||
|
break;
|
||||||
|
case ACL_TYPE_DEFAULT:
|
||||||
|
if (vp->v_type != VDIR)
|
||||||
|
return (EINVAL);
|
||||||
|
xattr_name = "posix_acl_default";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return (EINVAL);
|
||||||
|
}
|
||||||
|
error = erofs_inode_has_noacl(em, en, &noacl);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (noacl) {
|
||||||
|
erofs_acl_from_mode(en, type, aclp);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
error = erofs_getxattr(vp, EXTATTR_NAMESPACE_SYSTEM, xattr_name, NULL,
|
||||||
|
&size);
|
||||||
|
if (error == ENOATTR) {
|
||||||
|
erofs_acl_from_mode(en, type, aclp);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (size > sizeof(buf))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
|
||||||
|
aiov.iov_base = buf;
|
||||||
|
aiov.iov_len = size;
|
||||||
|
auio.uio_iov = &aiov;
|
||||||
|
auio.uio_iovcnt = 1;
|
||||||
|
auio.uio_offset = 0;
|
||||||
|
auio.uio_resid = size;
|
||||||
|
auio.uio_segflg = UIO_SYSSPACE;
|
||||||
|
auio.uio_rw = UIO_READ;
|
||||||
|
auio.uio_td = curthread;
|
||||||
|
error = erofs_getxattr(vp, EXTATTR_NAMESPACE_SYSTEM, xattr_name, &auio,
|
||||||
|
NULL);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (auio.uio_resid != 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
|
||||||
|
if (size < sizeof(hdr) || (size - sizeof(hdr)) % sizeof(entry) != 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
|
||||||
|
memcpy(&hdr, buf, sizeof(hdr));
|
||||||
|
if (le32toh(hdr.a_version) != POSIX_ACL_XATTR_VERSION)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
|
||||||
|
count = (size - sizeof(hdr)) / sizeof(entry);
|
||||||
|
if (count > ACL_MAX_ENTRIES)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
if (count == 0) {
|
||||||
|
erofs_acl_from_mode(en, type, aclp);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
aclp->acl_cnt = count;
|
||||||
|
phase = 0;
|
||||||
|
for (i = 0; i < count; i++) {
|
||||||
|
uint16_t tag, perm;
|
||||||
|
|
||||||
|
memcpy(&entry, buf + sizeof(hdr) + i * sizeof(entry),
|
||||||
|
sizeof(entry));
|
||||||
|
tag = le16toh(entry.e_tag);
|
||||||
|
perm = le16toh(entry.e_perm);
|
||||||
|
|
||||||
|
id = le32toh(entry.e_id);
|
||||||
|
if ((perm & ~ACL_PERM_BITS) != 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
switch (tag) {
|
||||||
|
case ACL_USER_OBJ:
|
||||||
|
if (phase != 0 || id != UINT32_MAX)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
phase = 1;
|
||||||
|
break;
|
||||||
|
case ACL_USER:
|
||||||
|
if ((phase != 1 && phase != 2) || id == UINT32_MAX)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
phase = 2;
|
||||||
|
break;
|
||||||
|
case ACL_GROUP_OBJ:
|
||||||
|
if ((phase != 1 && phase != 2) || id != UINT32_MAX)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
phase = 3;
|
||||||
|
break;
|
||||||
|
case ACL_GROUP:
|
||||||
|
if ((phase != 3 && phase != 4) || id == UINT32_MAX)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
phase = 4;
|
||||||
|
break;
|
||||||
|
case ACL_MASK:
|
||||||
|
if ((phase != 3 && phase != 4) || id != UINT32_MAX)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
phase = 5;
|
||||||
|
break;
|
||||||
|
case ACL_OTHER:
|
||||||
|
if ((phase != 3 && phase != 4 && phase != 5) ||
|
||||||
|
id != UINT32_MAX)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
phase = 6;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
if (tag == ACL_USER || tag == ACL_GROUP) {
|
||||||
|
for (j = 0; j < i; j++) {
|
||||||
|
if (aclp->acl_entry[j].ae_tag == tag &&
|
||||||
|
aclp->acl_entry[j].ae_id == id)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aclp->acl_entry[i].ae_tag = tag;
|
||||||
|
aclp->acl_entry[i].ae_perm = perm;
|
||||||
|
aclp->acl_entry[i].ae_id = (id == UINT32_MAX) ? ACL_UNDEFINED_ID : id;
|
||||||
|
}
|
||||||
|
if (phase != 6 || acl_posix1e_check(aclp) != 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-2.0-only */
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017-2018 HUAWEI, Inc.
|
||||||
|
* https://www.huawei.com/
|
||||||
|
*/
|
||||||
|
#ifndef __EROFS_XATTR_H
|
||||||
|
#define __EROFS_XATTR_H
|
||||||
|
|
||||||
|
#include "internal.h"
|
||||||
|
|
||||||
|
int erofs_xattr_prefixes_init(struct erofs_mount *em);
|
||||||
|
void erofs_xattr_prefixes_cleanup(struct erofs_mount *em);
|
||||||
|
int erofs_getxattr(struct vnode *vp, int attrnamespace, const char *name,
|
||||||
|
struct uio *uio, size_t *sizep);
|
||||||
|
int erofs_listxattr(struct vnode *vp, int attrnamespace, struct uio *uio,
|
||||||
|
size_t *sizep);
|
||||||
|
int erofs_get_acl(struct vnode *vp, acl_type_t type, struct acl *aclp);
|
||||||
|
#endif
|
||||||
+287
@@ -0,0 +1,287 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2018-2019 HUAWEI, Inc.
|
||||||
|
* https://www.huawei.com/
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <sys/param.h>
|
||||||
|
#include <sys/_maxphys.h>
|
||||||
|
#include <sys/libkern.h>
|
||||||
|
#include <sys/malloc.h>
|
||||||
|
#include <sys/systm.h>
|
||||||
|
#include <sys/uio.h>
|
||||||
|
|
||||||
|
#include "internal.h"
|
||||||
|
|
||||||
|
static bool
|
||||||
|
z_erofs_extent_cache_match(const struct erofs_zextent_cache *cache,
|
||||||
|
const struct erofs_node *en, const struct erofs_map_blocks *map)
|
||||||
|
{
|
||||||
|
|
||||||
|
return (cache->data != NULL && cache->m_nid == en->nid &&
|
||||||
|
cache->m_pa == map->m_pa &&
|
||||||
|
cache->m_la == map->m_la && cache->m_plen == map->m_plen &&
|
||||||
|
cache->m_llen == map->m_llen &&
|
||||||
|
cache->m_deviceid == map->m_deviceid &&
|
||||||
|
cache->m_flags == map->m_flags &&
|
||||||
|
cache->m_algorithmformat ==
|
||||||
|
(unsigned char)map->m_algorithmformat);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
z_erofs_extent_cache_copy(struct erofs_mount *em, struct erofs_node *en,
|
||||||
|
struct erofs_map_blocks *map, uint64_t mapoff, size_t len, void *dst)
|
||||||
|
{
|
||||||
|
bool matched;
|
||||||
|
|
||||||
|
KASSERT(len <= MAXPHYS, ("erofs extent cache copy exceeds MAXPHYS"));
|
||||||
|
if (!em->z_extent_cache_initialized)
|
||||||
|
return (false);
|
||||||
|
mtx_lock(&em->z_extent_cache_lock);
|
||||||
|
matched = z_erofs_extent_cache_match(&em->z_extent_cache, en, map);
|
||||||
|
if (matched)
|
||||||
|
memcpy(dst, (char *)em->z_extent_cache.data + (size_t)mapoff, len);
|
||||||
|
mtx_unlock(&em->z_extent_cache_lock);
|
||||||
|
return (matched);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
z_erofs_extent_cache_publish(struct erofs_mount *em, struct erofs_node *en,
|
||||||
|
struct erofs_map_blocks *map, uint64_t mapoff, size_t len, void *decoded,
|
||||||
|
void *dst)
|
||||||
|
{
|
||||||
|
void *old;
|
||||||
|
|
||||||
|
KASSERT(len <= MAXPHYS, ("erofs extent cache publish exceeds MAXPHYS"));
|
||||||
|
mtx_lock(&em->z_extent_cache_lock);
|
||||||
|
if (z_erofs_extent_cache_match(&em->z_extent_cache, en, map)) {
|
||||||
|
memcpy(dst, (char *)em->z_extent_cache.data + (size_t)mapoff, len);
|
||||||
|
mtx_unlock(&em->z_extent_cache_lock);
|
||||||
|
free(decoded, M_EROFS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
old = em->z_extent_cache.data;
|
||||||
|
em->z_extent_cache.data = decoded;
|
||||||
|
em->z_extent_cache.m_nid = en->nid;
|
||||||
|
em->z_extent_cache.m_pa = map->m_pa;
|
||||||
|
em->z_extent_cache.m_la = map->m_la;
|
||||||
|
em->z_extent_cache.m_plen = map->m_plen;
|
||||||
|
em->z_extent_cache.m_llen = map->m_llen;
|
||||||
|
em->z_extent_cache.m_deviceid = map->m_deviceid;
|
||||||
|
em->z_extent_cache.m_flags = map->m_flags;
|
||||||
|
em->z_extent_cache.m_algorithmformat =
|
||||||
|
(unsigned char)map->m_algorithmformat;
|
||||||
|
memcpy(dst, (char *)decoded + (size_t)mapoff, len);
|
||||||
|
mtx_unlock(&em->z_extent_cache_lock);
|
||||||
|
free(old, M_EROFS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
z_erofs_extent_cache_init(struct erofs_mount *em)
|
||||||
|
{
|
||||||
|
|
||||||
|
mtx_init(&em->z_extent_cache_lock, "erofs zextent", NULL, MTX_DEF);
|
||||||
|
em->z_extent_cache_initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
z_erofs_extent_cache_fini(struct erofs_mount *em)
|
||||||
|
{
|
||||||
|
void *data;
|
||||||
|
|
||||||
|
if (!em->z_extent_cache_initialized)
|
||||||
|
return;
|
||||||
|
mtx_lock(&em->z_extent_cache_lock);
|
||||||
|
data = em->z_extent_cache.data;
|
||||||
|
em->z_extent_cache.data = NULL;
|
||||||
|
mtx_unlock(&em->z_extent_cache_lock);
|
||||||
|
free(data, M_EROFS);
|
||||||
|
mtx_destroy(&em->z_extent_cache_lock);
|
||||||
|
em->z_extent_cache_initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
z_erofs_extent_cache_eligible(const struct erofs_mount *em,
|
||||||
|
const struct erofs_node *en, const struct erofs_map_blocks *map,
|
||||||
|
size_t len)
|
||||||
|
{
|
||||||
|
|
||||||
|
return (len <= MAXPHYS && (map->m_flags & (EROFS_MAP_META |
|
||||||
|
EROFS_MAP_PARTIAL_MAPPED | EROFS_MAP_PARTIAL_REF |
|
||||||
|
EROFS_MAP_FRAGMENT)) == 0 &&
|
||||||
|
map->m_algorithmformat == Z_EROFS_COMPRESSION_LZMA &&
|
||||||
|
em->z_extent_cache_initialized && en != em->packed_inode &&
|
||||||
|
en != em->metabox_en);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
z_erofs_read_extent(struct erofs_mount *em, struct erofs_node *en,
|
||||||
|
struct erofs_map_blocks *map, size_t decoded_len, void **bufp)
|
||||||
|
{
|
||||||
|
void *compressed, *decoded;
|
||||||
|
bool partial;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
*bufp = NULL;
|
||||||
|
if ((map->m_flags & EROFS_MAP_FRAGMENT) != 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
if ((map->m_flags & EROFS_MAP_MAPPED) == 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
#if SIZE_MAX < UINT64_MAX
|
||||||
|
if (map->m_plen > SIZE_MAX || map->m_llen > SIZE_MAX)
|
||||||
|
return (EOVERFLOW);
|
||||||
|
#endif
|
||||||
|
if (decoded_len == 0 || decoded_len > map->m_llen)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
partial = (map->m_flags & EROFS_MAP_PARTIAL_REF) != 0;
|
||||||
|
if (!partial && decoded_len != map->m_llen)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
|
||||||
|
if ((map->m_flags & EROFS_MAP_META) != 0)
|
||||||
|
error = erofs_read_metadata(em, en->nid, map->m_pa,
|
||||||
|
(size_t)map->m_plen, &compressed);
|
||||||
|
else
|
||||||
|
error = erofs_read_physical(em, map->m_deviceid, map->m_pa,
|
||||||
|
(size_t)map->m_plen, &compressed);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
|
||||||
|
decoded = malloc(decoded_len, M_EROFS, M_WAITOK | M_ZERO);
|
||||||
|
error = z_erofs_decompress(em, map, compressed, (size_t)map->m_plen,
|
||||||
|
decoded, decoded_len, partial);
|
||||||
|
erofs_brelse(compressed);
|
||||||
|
if (error != 0) {
|
||||||
|
free(decoded, M_EROFS);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
*bufp = decoded;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
z_erofs_do_read(struct erofs_mount *em, struct erofs_node *en,
|
||||||
|
uint64_t loff, size_t len, char *out)
|
||||||
|
{
|
||||||
|
struct erofs_map_blocks map;
|
||||||
|
void *decoded, *fragment;
|
||||||
|
uint64_t mapoff;
|
||||||
|
size_t decoded_len, done, want;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
done = 0;
|
||||||
|
while (done < len) {
|
||||||
|
bzero(&map, sizeof(map));
|
||||||
|
map.m_la = loff + done;
|
||||||
|
error = z_erofs_map_blocks_iter(em, en, &map,
|
||||||
|
EROFS_GET_BLOCKS_FIEMAP);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (map.m_llen == 0 || map.m_la > loff + done ||
|
||||||
|
loff + done - map.m_la >= map.m_llen)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
mapoff = loff + done - map.m_la;
|
||||||
|
#if SIZE_MAX < UINT64_MAX
|
||||||
|
if (mapoff > SIZE_MAX)
|
||||||
|
return (EOVERFLOW);
|
||||||
|
if (map.m_llen - mapoff > SIZE_MAX)
|
||||||
|
return (EOVERFLOW);
|
||||||
|
#endif
|
||||||
|
want = MIN((size_t)(map.m_llen - mapoff), len - done);
|
||||||
|
if (want == 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
|
||||||
|
if ((map.m_flags & EROFS_MAP_FRAGMENT) != 0) {
|
||||||
|
if (em->packed_inode == NULL ||
|
||||||
|
em->packed_inode->nid == en->nid ||
|
||||||
|
en->z_fragmentoff > UINT64_MAX - mapoff)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
error = erofs_read_data(em, em->packed_inode,
|
||||||
|
en->z_fragmentoff + mapoff, want, &fragment);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
memcpy(out + done, fragment, want);
|
||||||
|
erofs_brelse(fragment);
|
||||||
|
} else if ((map.m_flags & EROFS_MAP_MAPPED) == 0) {
|
||||||
|
bzero(out + done, want);
|
||||||
|
} else {
|
||||||
|
decoded_len = (size_t)map.m_llen;
|
||||||
|
if ((map.m_flags & EROFS_MAP_PARTIAL_REF) != 0) {
|
||||||
|
if (mapoff > SIZE_MAX - want)
|
||||||
|
return (EOVERFLOW);
|
||||||
|
decoded_len = (size_t)mapoff + want;
|
||||||
|
}
|
||||||
|
if (z_erofs_extent_cache_eligible(em, en, &map, want) &&
|
||||||
|
z_erofs_extent_cache_copy(em, en, &map, mapoff, want,
|
||||||
|
out + done)) {
|
||||||
|
done += want;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
error = z_erofs_read_extent(em, en, &map, decoded_len,
|
||||||
|
&decoded);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (z_erofs_extent_cache_eligible(em, en, &map, want))
|
||||||
|
z_erofs_extent_cache_publish(em, en, &map, mapoff, want,
|
||||||
|
decoded, out + done);
|
||||||
|
else {
|
||||||
|
memcpy(out + done, (char *)decoded + mapoff, want);
|
||||||
|
free(decoded, M_EROFS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
done += want;
|
||||||
|
}
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
z_erofs_read_data(struct erofs_mount *em, struct erofs_node *en,
|
||||||
|
uint64_t loff, size_t len, void **bufp)
|
||||||
|
{
|
||||||
|
char *out;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
if (bufp == NULL)
|
||||||
|
return (EINVAL);
|
||||||
|
*bufp = NULL;
|
||||||
|
if (len == 0)
|
||||||
|
return (0);
|
||||||
|
if (loff > UINT64_MAX - len)
|
||||||
|
return (EOVERFLOW);
|
||||||
|
if (loff > en->size || len > en->size - loff)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
|
||||||
|
out = malloc(len, M_EROFS, M_WAITOK);
|
||||||
|
error = z_erofs_do_read(em, en, loff, len, out);
|
||||||
|
if (error != 0) {
|
||||||
|
free(out, M_EROFS);
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
*bufp = out;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
z_erofs_read_uio(struct erofs_mount *em, struct erofs_node *en,
|
||||||
|
struct uio *uio)
|
||||||
|
{
|
||||||
|
char *buf;
|
||||||
|
size_t want;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
if (uio->uio_offset < 0)
|
||||||
|
return (EINVAL);
|
||||||
|
while (uio->uio_resid > 0 && (uint64_t)uio->uio_offset < en->size) {
|
||||||
|
want = MIN((size_t)uio->uio_resid,
|
||||||
|
(size_t)MIN((uint64_t)MAXPHYS,
|
||||||
|
en->size - (uint64_t)uio->uio_offset));
|
||||||
|
buf = malloc(want, M_EROFS, M_WAITOK);
|
||||||
|
error = z_erofs_do_read(em, en, (uint64_t)uio->uio_offset,
|
||||||
|
want, buf);
|
||||||
|
if (error == 0)
|
||||||
|
error = uiomove(buf, want, uio);
|
||||||
|
free(buf, M_EROFS);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
+928
@@ -0,0 +1,928 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2018-2019 HUAWEI, Inc.
|
||||||
|
* https://www.huawei.com/
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <sys/param.h>
|
||||||
|
#include <sys/endian.h>
|
||||||
|
#include <sys/libkern.h>
|
||||||
|
#include <sys/malloc.h>
|
||||||
|
#include <sys/systm.h>
|
||||||
|
|
||||||
|
#include "internal.h"
|
||||||
|
|
||||||
|
struct z_erofs_maprecorder {
|
||||||
|
struct erofs_mount *em;
|
||||||
|
struct erofs_node *en;
|
||||||
|
struct erofs_map_blocks *map;
|
||||||
|
uint64_t lcn;
|
||||||
|
uint8_t type;
|
||||||
|
uint8_t headtype;
|
||||||
|
unsigned int clusterofs;
|
||||||
|
uint16_t delta[2];
|
||||||
|
erofs_blk_t pblk;
|
||||||
|
erofs_blk_t compressedblks;
|
||||||
|
erofs_off_t nextpackoff;
|
||||||
|
bool partialref;
|
||||||
|
};
|
||||||
|
|
||||||
|
static int
|
||||||
|
z_erofs_read_index(struct z_erofs_maprecorder *m, uint64_t pos, size_t len,
|
||||||
|
void **bufp)
|
||||||
|
{
|
||||||
|
return (erofs_read_metadata(m->em, m->en->nid, pos, len, bufp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
z_erofs_load_full_lcluster(struct z_erofs_maprecorder *m, uint64_t lcn)
|
||||||
|
{
|
||||||
|
struct erofs_node *en;
|
||||||
|
struct z_erofs_lcluster_index *di;
|
||||||
|
uint64_t base, pos;
|
||||||
|
unsigned int advise;
|
||||||
|
void *buf;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
en = m->en;
|
||||||
|
base = en->inode_off + en->inode_isize + en->xattr_isize;
|
||||||
|
if (base < en->inode_off)
|
||||||
|
return (EOVERFLOW);
|
||||||
|
base = Z_EROFS_FULL_INDEX_START(base);
|
||||||
|
if (lcn > (UINT64_MAX - base) / sizeof(*di))
|
||||||
|
return (EOVERFLOW);
|
||||||
|
pos = base + lcn * sizeof(*di);
|
||||||
|
error = z_erofs_read_index(m, pos, sizeof(*di), &buf);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
|
||||||
|
di = buf;
|
||||||
|
m->lcn = lcn;
|
||||||
|
m->nextpackoff = pos + sizeof(*di);
|
||||||
|
advise = le16toh(di->di_advise);
|
||||||
|
m->type = advise & Z_EROFS_LI_LCLUSTER_TYPE_MASK;
|
||||||
|
if (m->type == Z_EROFS_LCLUSTER_TYPE_NONHEAD) {
|
||||||
|
m->clusterofs = 1U << en->z_lclusterbits;
|
||||||
|
m->delta[0] = le16toh(di->di_u.delta[0]);
|
||||||
|
if ((m->delta[0] & Z_EROFS_LI_D0_CBLKCNT) != 0) {
|
||||||
|
if ((en->z_advise & (Z_EROFS_ADVISE_BIG_PCLUSTER_1 |
|
||||||
|
Z_EROFS_ADVISE_BIG_PCLUSTER_2)) == 0) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
m->compressedblks =
|
||||||
|
m->delta[0] & ~Z_EROFS_LI_D0_CBLKCNT;
|
||||||
|
m->delta[0] = 1;
|
||||||
|
}
|
||||||
|
m->delta[1] = le16toh(di->di_u.delta[1]);
|
||||||
|
} else {
|
||||||
|
m->partialref = (advise & Z_EROFS_LI_PARTIAL_REF) != 0;
|
||||||
|
m->clusterofs = le16toh(di->di_clusterofs);
|
||||||
|
m->pblk = le32toh(di->di_u.blkaddr);
|
||||||
|
}
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static unsigned int
|
||||||
|
decode_compactedbits(unsigned int lobits, const uint8_t *in,
|
||||||
|
unsigned int pos, uint8_t *type)
|
||||||
|
{
|
||||||
|
uint32_t value;
|
||||||
|
unsigned int lo;
|
||||||
|
|
||||||
|
value = le32dec(in + pos / 8) >> (pos & 7);
|
||||||
|
lo = value & ((1U << lobits) - 1);
|
||||||
|
*type = (value >> lobits) & 3;
|
||||||
|
return (lo);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
get_compacted_la_distance(unsigned int lobits, unsigned int encodebits,
|
||||||
|
unsigned int vcnt, const uint8_t *in, int i)
|
||||||
|
{
|
||||||
|
unsigned int lo, distance;
|
||||||
|
uint8_t type;
|
||||||
|
|
||||||
|
distance = 0;
|
||||||
|
do {
|
||||||
|
lo = decode_compactedbits(lobits, in, encodebits * i, &type);
|
||||||
|
if (type != Z_EROFS_LCLUSTER_TYPE_NONHEAD)
|
||||||
|
return (distance);
|
||||||
|
++distance;
|
||||||
|
} while (++i < (int)vcnt);
|
||||||
|
|
||||||
|
if ((lo & Z_EROFS_LI_D0_CBLKCNT) == 0) {
|
||||||
|
if (lo == 0)
|
||||||
|
return (-1);
|
||||||
|
distance += lo - 1;
|
||||||
|
}
|
||||||
|
return ((int)distance);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
z_erofs_load_compact_lcluster(struct z_erofs_maprecorder *m, uint64_t lcn,
|
||||||
|
bool lookahead)
|
||||||
|
{
|
||||||
|
struct erofs_node *en;
|
||||||
|
uint64_t ebase, pos, totalidx, original_lcn;
|
||||||
|
unsigned int compacted_4b_initial, compacted_2b, amortizedshift;
|
||||||
|
unsigned int vcnt, lo, lobits, encodebits, nblk, bytes, packsize;
|
||||||
|
bool big_pcluster;
|
||||||
|
uint8_t *in, type;
|
||||||
|
void *buf;
|
||||||
|
int distance, error, i;
|
||||||
|
|
||||||
|
en = m->en;
|
||||||
|
ebase = Z_EROFS_MAP_HEADER_END(en->inode_off + en->inode_isize +
|
||||||
|
en->xattr_isize);
|
||||||
|
totalidx = roundup2(en->size, 1ULL << en->z_lclusterbits) >>
|
||||||
|
en->z_lclusterbits;
|
||||||
|
if (lcn >= totalidx || en->z_lclusterbits > 14)
|
||||||
|
return (EINVAL);
|
||||||
|
|
||||||
|
original_lcn = lcn;
|
||||||
|
m->lcn = lcn;
|
||||||
|
compacted_4b_initial = ((32 - ebase % 32) / 4) & 7;
|
||||||
|
compacted_2b = 0;
|
||||||
|
if ((en->z_advise & Z_EROFS_ADVISE_COMPACTED_2B) != 0 &&
|
||||||
|
compacted_4b_initial < totalidx)
|
||||||
|
compacted_2b = rounddown2(totalidx - compacted_4b_initial, 16);
|
||||||
|
|
||||||
|
pos = ebase;
|
||||||
|
amortizedshift = 2;
|
||||||
|
if (lcn >= compacted_4b_initial) {
|
||||||
|
pos += compacted_4b_initial * 4;
|
||||||
|
lcn -= compacted_4b_initial;
|
||||||
|
if (lcn < compacted_2b) {
|
||||||
|
amortizedshift = 1;
|
||||||
|
} else {
|
||||||
|
pos += compacted_2b * 2;
|
||||||
|
lcn -= compacted_2b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pos += lcn << amortizedshift;
|
||||||
|
|
||||||
|
if (amortizedshift == 2 && en->z_lclusterbits <= 14)
|
||||||
|
vcnt = 2;
|
||||||
|
else if (amortizedshift == 1 && en->z_lclusterbits <= 12)
|
||||||
|
vcnt = 16;
|
||||||
|
else
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
|
||||||
|
packsize = vcnt << amortizedshift;
|
||||||
|
bytes = pos & (packsize - 1);
|
||||||
|
pos -= bytes;
|
||||||
|
error = z_erofs_read_index(m, pos, packsize, &buf);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
in = buf;
|
||||||
|
m->nextpackoff = pos + packsize;
|
||||||
|
lobits = MAX(en->z_lclusterbits, fls(Z_EROFS_LI_D0_CBLKCNT));
|
||||||
|
encodebits = (packsize - sizeof(uint32_t)) * 8 / vcnt;
|
||||||
|
i = bytes >> amortizedshift;
|
||||||
|
|
||||||
|
lo = decode_compactedbits(lobits, in, encodebits * i, &type);
|
||||||
|
m->type = type;
|
||||||
|
if (type == Z_EROFS_LCLUSTER_TYPE_NONHEAD) {
|
||||||
|
m->clusterofs = 1U << en->z_lclusterbits;
|
||||||
|
if (lookahead) {
|
||||||
|
distance = get_compacted_la_distance(lobits, encodebits,
|
||||||
|
vcnt, in, i);
|
||||||
|
if (distance < 0) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
m->delta[1] = distance;
|
||||||
|
}
|
||||||
|
big_pcluster =
|
||||||
|
(en->z_advise & Z_EROFS_ADVISE_BIG_PCLUSTER_1) != 0;
|
||||||
|
if ((lo & Z_EROFS_LI_D0_CBLKCNT) != 0) {
|
||||||
|
if (!big_pcluster) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
m->compressedblks = lo & ~Z_EROFS_LI_D0_CBLKCNT;
|
||||||
|
m->delta[0] = 1;
|
||||||
|
} else if (i + 1 != (int)vcnt) {
|
||||||
|
m->delta[0] = lo;
|
||||||
|
} else {
|
||||||
|
if (i == 0) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
lo = decode_compactedbits(lobits, in,
|
||||||
|
encodebits * (i - 1), &type);
|
||||||
|
if (type != Z_EROFS_LCLUSTER_TYPE_NONHEAD)
|
||||||
|
lo = 0;
|
||||||
|
else if ((lo & Z_EROFS_LI_D0_CBLKCNT) != 0)
|
||||||
|
lo = 1;
|
||||||
|
m->delta[0] = lo + 1;
|
||||||
|
}
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (m->delta[0] == 0 ? EINTEGRITY : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
m->clusterofs = lo;
|
||||||
|
m->delta[0] = 0;
|
||||||
|
big_pcluster =
|
||||||
|
(en->z_advise & Z_EROFS_ADVISE_BIG_PCLUSTER_1) != 0;
|
||||||
|
if (!big_pcluster) {
|
||||||
|
nblk = 1;
|
||||||
|
while (i > 0) {
|
||||||
|
--i;
|
||||||
|
lo = decode_compactedbits(lobits, in,
|
||||||
|
encodebits * i, &type);
|
||||||
|
if (type == Z_EROFS_LCLUSTER_TYPE_NONHEAD)
|
||||||
|
i -= lo;
|
||||||
|
if (i >= 0)
|
||||||
|
++nblk;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nblk = 0;
|
||||||
|
while (i > 0) {
|
||||||
|
--i;
|
||||||
|
lo = decode_compactedbits(lobits, in,
|
||||||
|
encodebits * i, &type);
|
||||||
|
if (type == Z_EROFS_LCLUSTER_TYPE_NONHEAD) {
|
||||||
|
if ((lo & Z_EROFS_LI_D0_CBLKCNT) != 0) {
|
||||||
|
if (i == 0) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
--i;
|
||||||
|
nblk += lo & ~Z_EROFS_LI_D0_CBLKCNT;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (lo <= 1) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
i -= lo - 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++nblk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m->pblk = le32dec(in + packsize - sizeof(uint32_t)) + nblk;
|
||||||
|
erofs_brelse(buf);
|
||||||
|
m->lcn = original_lcn;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
z_erofs_load_lcluster_from_disk(struct z_erofs_maprecorder *m, uint64_t lcn,
|
||||||
|
bool lookahead)
|
||||||
|
{
|
||||||
|
int error;
|
||||||
|
|
||||||
|
if (m->en->datalayout == EROFS_INODE_COMPRESSED_COMPACT)
|
||||||
|
error = z_erofs_load_compact_lcluster(m, lcn, lookahead);
|
||||||
|
else if (m->en->datalayout == EROFS_INODE_COMPRESSED_FULL)
|
||||||
|
error = z_erofs_load_full_lcluster(m, lcn);
|
||||||
|
else
|
||||||
|
return (EINTEGRITY);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (m->type >= Z_EROFS_LCLUSTER_TYPE_MAX)
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
if (m->type != Z_EROFS_LCLUSTER_TYPE_NONHEAD &&
|
||||||
|
m->clusterofs >= (1U << m->en->z_lclusterbits))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
z_erofs_extent_lookback(struct z_erofs_maprecorder *m,
|
||||||
|
unsigned int lookback_distance)
|
||||||
|
{
|
||||||
|
uint64_t lcn;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
while (lookback_distance != 0 && m->lcn >= lookback_distance) {
|
||||||
|
lcn = m->lcn - lookback_distance;
|
||||||
|
error = z_erofs_load_lcluster_from_disk(m, lcn, false);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (m->type == Z_EROFS_LCLUSTER_TYPE_NONHEAD) {
|
||||||
|
lookback_distance = m->delta[0];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
m->headtype = m->type;
|
||||||
|
m->map->m_la = (lcn << m->en->z_lclusterbits) |
|
||||||
|
m->clusterofs;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
z_erofs_get_extent_compressedlen(struct z_erofs_maprecorder *m,
|
||||||
|
uint64_t initial_lcn)
|
||||||
|
{
|
||||||
|
struct erofs_node *en;
|
||||||
|
bool bigpcl1, bigpcl2;
|
||||||
|
uint64_t lcn;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
en = m->en;
|
||||||
|
bigpcl1 = (en->z_advise & Z_EROFS_ADVISE_BIG_PCLUSTER_1) != 0;
|
||||||
|
bigpcl2 = (en->z_advise & Z_EROFS_ADVISE_BIG_PCLUSTER_2) != 0;
|
||||||
|
lcn = m->lcn + 1;
|
||||||
|
if ((m->headtype == Z_EROFS_LCLUSTER_TYPE_HEAD1 && !bigpcl1) ||
|
||||||
|
((m->headtype == Z_EROFS_LCLUSTER_TYPE_PLAIN ||
|
||||||
|
m->headtype == Z_EROFS_LCLUSTER_TYPE_HEAD2) && !bigpcl2) ||
|
||||||
|
(lcn << en->z_lclusterbits) >= en->size)
|
||||||
|
m->compressedblks = 1;
|
||||||
|
if (m->compressedblks == 0) {
|
||||||
|
error = z_erofs_load_lcluster_from_disk(m, lcn, false);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (m->type == Z_EROFS_LCLUSTER_TYPE_NONHEAD &&
|
||||||
|
m->delta[0] != 1)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
if (m->type != Z_EROFS_LCLUSTER_TYPE_NONHEAD ||
|
||||||
|
m->compressedblks == 0)
|
||||||
|
m->compressedblks = 1;
|
||||||
|
}
|
||||||
|
if (m->compressedblks > (UINT64_MAX >> m->em->block_bits))
|
||||||
|
return (EOVERFLOW);
|
||||||
|
m->map->m_plen = m->compressedblks << m->em->block_bits;
|
||||||
|
(void)initial_lcn;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
z_erofs_get_extent_decompressedlen(struct z_erofs_maprecorder *m)
|
||||||
|
{
|
||||||
|
struct erofs_node *en;
|
||||||
|
struct erofs_map_blocks *map;
|
||||||
|
uint64_t lcn, headlcn;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
en = m->en;
|
||||||
|
map = m->map;
|
||||||
|
lcn = m->lcn;
|
||||||
|
headlcn = map->m_la >> en->z_lclusterbits;
|
||||||
|
for (;;) {
|
||||||
|
if ((lcn << en->z_lclusterbits) >= en->size) {
|
||||||
|
map->m_llen = en->size - map->m_la;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
error = z_erofs_load_lcluster_from_disk(m, lcn, true);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (m->type == Z_EROFS_LCLUSTER_TYPE_NONHEAD) {
|
||||||
|
if (m->delta[1] == 0)
|
||||||
|
m->delta[1] = 1;
|
||||||
|
} else {
|
||||||
|
if (lcn != headlcn)
|
||||||
|
break;
|
||||||
|
m->delta[1] = 1;
|
||||||
|
}
|
||||||
|
if (lcn > UINT64_MAX - m->delta[1])
|
||||||
|
return (EOVERFLOW);
|
||||||
|
lcn += m->delta[1];
|
||||||
|
}
|
||||||
|
map->m_llen = (lcn << en->z_lclusterbits) + m->clusterofs -
|
||||||
|
map->m_la;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
z_erofs_map_blocks_fo(struct erofs_mount *em, struct erofs_node *en,
|
||||||
|
struct erofs_map_blocks *map, int flags)
|
||||||
|
{
|
||||||
|
bool fragment, ztailpacking;
|
||||||
|
struct z_erofs_maprecorder m;
|
||||||
|
uint64_t initial_lcn, ofs, end;
|
||||||
|
unsigned int endoff;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
fragment = (en->z_advise & Z_EROFS_ADVISE_FRAGMENT_PCLUSTER) != 0;
|
||||||
|
ztailpacking = en->z_idata_size != 0;
|
||||||
|
bzero(&m, sizeof(m));
|
||||||
|
m.em = em;
|
||||||
|
m.en = en;
|
||||||
|
m.map = map;
|
||||||
|
|
||||||
|
if (en->size == 0) {
|
||||||
|
map->m_la = 0;
|
||||||
|
map->m_llen = 0;
|
||||||
|
map->m_flags = 0;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
ofs = (flags & EROFS_GET_BLOCKS_FINDTAIL) != 0 ?
|
||||||
|
en->size - 1 : map->m_la;
|
||||||
|
if (fragment && (flags & EROFS_GET_BLOCKS_FINDTAIL) == 0 &&
|
||||||
|
en->z_tailextent_headlcn == 0) {
|
||||||
|
map->m_la = 0;
|
||||||
|
map->m_llen = en->size;
|
||||||
|
map->m_flags = EROFS_MAP_FRAGMENT;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
initial_lcn = ofs >> en->z_lclusterbits;
|
||||||
|
endoff = ofs & ((1U << en->z_lclusterbits) - 1);
|
||||||
|
error = z_erofs_load_lcluster_from_disk(&m, initial_lcn, false);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if ((flags & EROFS_GET_BLOCKS_FINDTAIL) != 0 && ztailpacking)
|
||||||
|
en->z_fragmentoff = m.nextpackoff;
|
||||||
|
|
||||||
|
map->m_flags = EROFS_MAP_MAPPED | EROFS_MAP_PARTIAL_MAPPED;
|
||||||
|
end = (m.lcn + 1) << en->z_lclusterbits;
|
||||||
|
if (m.type != Z_EROFS_LCLUSTER_TYPE_NONHEAD &&
|
||||||
|
endoff >= m.clusterofs) {
|
||||||
|
m.headtype = m.type;
|
||||||
|
map->m_la = (m.lcn << en->z_lclusterbits) | m.clusterofs;
|
||||||
|
if (ztailpacking && end > en->size)
|
||||||
|
end = en->size;
|
||||||
|
} else {
|
||||||
|
if (m.type != Z_EROFS_LCLUSTER_TYPE_NONHEAD) {
|
||||||
|
end = (m.lcn << en->z_lclusterbits) | m.clusterofs;
|
||||||
|
map->m_flags &= ~EROFS_MAP_PARTIAL_MAPPED;
|
||||||
|
m.delta[0] = 1;
|
||||||
|
}
|
||||||
|
error = z_erofs_extent_lookback(&m, m.delta[0]);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
if (m.partialref)
|
||||||
|
map->m_flags |= EROFS_MAP_PARTIAL_REF;
|
||||||
|
if (end < map->m_la)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
map->m_llen = end - map->m_la;
|
||||||
|
|
||||||
|
if ((flags & EROFS_GET_BLOCKS_FINDTAIL) != 0) {
|
||||||
|
en->z_tailextent_headlcn = m.lcn;
|
||||||
|
if (fragment &&
|
||||||
|
en->datalayout == EROFS_INODE_COMPRESSED_FULL)
|
||||||
|
en->z_fragmentoff |= m.pblk << 32;
|
||||||
|
}
|
||||||
|
if (ztailpacking && m.lcn == en->z_tailextent_headlcn) {
|
||||||
|
map->m_flags |= EROFS_MAP_META;
|
||||||
|
map->m_pa = en->z_fragmentoff;
|
||||||
|
map->m_plen = en->z_idata_size;
|
||||||
|
if ((map->m_pa & (em->block_size - 1)) + map->m_plen >
|
||||||
|
em->block_size)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
} else if (fragment && m.lcn == en->z_tailextent_headlcn) {
|
||||||
|
map->m_flags = EROFS_MAP_FRAGMENT;
|
||||||
|
} else {
|
||||||
|
if (m.pblk > (UINT64_MAX >> em->block_bits))
|
||||||
|
return (EOVERFLOW);
|
||||||
|
map->m_pa = m.pblk << em->block_bits;
|
||||||
|
error = z_erofs_get_extent_compressedlen(&m, initial_lcn);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m.headtype == Z_EROFS_LCLUSTER_TYPE_PLAIN) {
|
||||||
|
map->m_algorithmformat =
|
||||||
|
(en->z_advise & Z_EROFS_ADVISE_INTERLACED_PCLUSTER) != 0 ?
|
||||||
|
Z_EROFS_COMPRESSION_INTERLACED :
|
||||||
|
Z_EROFS_COMPRESSION_SHIFTED;
|
||||||
|
} else if (m.headtype == Z_EROFS_LCLUSTER_TYPE_HEAD2) {
|
||||||
|
map->m_algorithmformat = en->z_algorithmtype[1];
|
||||||
|
} else {
|
||||||
|
map->m_algorithmformat = en->z_algorithmtype[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((flags & EROFS_GET_BLOCKS_FIEMAP) != 0 ||
|
||||||
|
((flags & EROFS_GET_BLOCKS_READMORE) != 0 &&
|
||||||
|
(map->m_algorithmformat == Z_EROFS_COMPRESSION_LZMA ||
|
||||||
|
map->m_algorithmformat == Z_EROFS_COMPRESSION_DEFLATE ||
|
||||||
|
map->m_algorithmformat == Z_EROFS_COMPRESSION_ZSTD) &&
|
||||||
|
map->m_llen >= em->block_size)) {
|
||||||
|
error = z_erofs_get_extent_decompressedlen(&m);
|
||||||
|
if (error == 0)
|
||||||
|
map->m_flags &= ~EROFS_MAP_PARTIAL_MAPPED;
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
z_erofs_read_extent(struct erofs_mount *em, struct erofs_node *en,
|
||||||
|
uint64_t pos, unsigned int recsz, struct z_erofs_extent *ext)
|
||||||
|
{
|
||||||
|
void *buf;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
bzero(ext, sizeof(*ext));
|
||||||
|
error = erofs_read_metadata(em, en->nid, pos, recsz, &buf);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
memcpy(ext, buf, recsz);
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
z_erofs_extent_add(uint64_t left, uint64_t right, uint64_t *result)
|
||||||
|
{
|
||||||
|
if (__builtin_add_overflow(left, right, result))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
z_erofs_extent_roundup(uint64_t value, unsigned int alignment,
|
||||||
|
uint64_t *result)
|
||||||
|
{
|
||||||
|
uint64_t rounded;
|
||||||
|
|
||||||
|
if (z_erofs_extent_add(value, alignment - 1, &rounded) != 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
*result = rounddown2(rounded, alignment);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
z_erofs_extent_table_pos(const struct erofs_node *en, unsigned int recsz,
|
||||||
|
uint64_t *result)
|
||||||
|
{
|
||||||
|
uint64_t pos;
|
||||||
|
|
||||||
|
if (z_erofs_extent_add(en->inode_off, en->inode_isize, &pos) != 0 ||
|
||||||
|
z_erofs_extent_add(pos, en->xattr_isize, &pos) != 0 ||
|
||||||
|
z_erofs_extent_roundup(pos, 8, &pos) != 0 ||
|
||||||
|
z_erofs_extent_add(pos, sizeof(struct z_erofs_map_header), &pos) != 0 ||
|
||||||
|
z_erofs_extent_roundup(pos, recsz, result) != 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
z_erofs_extent_record_pos(const struct erofs_node *en, uint64_t table_pos,
|
||||||
|
unsigned int recsz, uint64_t index, uint64_t *result)
|
||||||
|
{
|
||||||
|
uint64_t offset;
|
||||||
|
|
||||||
|
if (index >= en->z_extents ||
|
||||||
|
__builtin_mul_overflow(index, recsz, &offset) ||
|
||||||
|
z_erofs_extent_add(table_pos, offset, result) != 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint64_t
|
||||||
|
z_erofs_extent_lstart(const struct z_erofs_extent *ext, unsigned int recsz)
|
||||||
|
{
|
||||||
|
uint64_t lstart;
|
||||||
|
|
||||||
|
lstart = le32toh(ext->lstart_lo);
|
||||||
|
if (recsz > offsetof(struct z_erofs_extent, lstart_hi))
|
||||||
|
lstart |= (uint64_t)le32toh(ext->lstart_hi) << 32;
|
||||||
|
return (lstart);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
z_erofs_validate_extent_table(struct erofs_mount *em, struct erofs_node *en,
|
||||||
|
unsigned int recsz)
|
||||||
|
{
|
||||||
|
struct z_erofs_extent ext;
|
||||||
|
uint64_t extent_pos, index, last_pos, lstart, previous;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
if (en->z_extents == 0)
|
||||||
|
return (en->size == 0 ? 0 : EINTEGRITY);
|
||||||
|
error = z_erofs_extent_table_pos(en, recsz, &extent_pos);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
if (recsz <= offsetof(struct z_erofs_extent, pstart_lo) &&
|
||||||
|
z_erofs_extent_add(extent_pos, sizeof(uint64_t), &extent_pos) != 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
error = z_erofs_extent_record_pos(en, extent_pos, recsz,
|
||||||
|
en->z_extents - 1, &last_pos);
|
||||||
|
if (error != 0 || z_erofs_extent_add(last_pos, recsz, &last_pos) != 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
if (recsz <= offsetof(struct z_erofs_extent, pstart_hi))
|
||||||
|
return (0);
|
||||||
|
if (en->size == 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
|
||||||
|
previous = 0;
|
||||||
|
for (index = 0; index < en->z_extents; index++) {
|
||||||
|
error = z_erofs_extent_record_pos(en, extent_pos, recsz, index,
|
||||||
|
&last_pos);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
error = z_erofs_read_extent(em, en, last_pos, recsz, &ext);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
lstart = z_erofs_extent_lstart(&ext, recsz);
|
||||||
|
if (lstart >= en->size || (index != 0 && lstart <= previous))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
previous = lstart;
|
||||||
|
}
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
z_erofs_map_blocks_ext(struct erofs_mount *em, struct erofs_node *en,
|
||||||
|
struct erofs_map_blocks *map, int flags)
|
||||||
|
{
|
||||||
|
struct z_erofs_extent ext;
|
||||||
|
unsigned int recsz, bmask, fmt;
|
||||||
|
uint64_t cluster_size, extent_idx, extent_pos, next, pos, rounded_lend;
|
||||||
|
uint64_t lend, l, r, mid, pa, la, lstart, table_pos;
|
||||||
|
bool interlaced, last;
|
||||||
|
void *buf;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
(void)flags;
|
||||||
|
interlaced =
|
||||||
|
(en->z_advise & Z_EROFS_ADVISE_INTERLACED_PCLUSTER) != 0;
|
||||||
|
recsz = z_erofs_extent_recsize(en->z_advise);
|
||||||
|
error = z_erofs_extent_table_pos(en, recsz, &table_pos);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
pos = table_pos;
|
||||||
|
bmask = em->block_size - 1;
|
||||||
|
lend = en->size;
|
||||||
|
cluster_size = 1ULL << en->z_lclusterbits;
|
||||||
|
map->m_flags = 0;
|
||||||
|
|
||||||
|
if (recsz <= offsetof(struct z_erofs_extent, pstart_hi)) {
|
||||||
|
if (recsz <= offsetof(struct z_erofs_extent, pstart_lo)) {
|
||||||
|
error = erofs_read_metadata(em, en->nid, pos,
|
||||||
|
sizeof(uint64_t), &buf);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
pa = le64dec(buf);
|
||||||
|
erofs_brelse(buf);
|
||||||
|
if (z_erofs_extent_add(pos, sizeof(uint64_t), &pos) != 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
lstart = 0;
|
||||||
|
extent_idx = 0;
|
||||||
|
} else {
|
||||||
|
lstart = rounddown2(map->m_la, cluster_size);
|
||||||
|
extent_idx = lstart >> en->z_lclusterbits;
|
||||||
|
pa = EROFS_NULL_ADDR;
|
||||||
|
}
|
||||||
|
for (;;) {
|
||||||
|
error = z_erofs_extent_record_pos(en, pos, recsz,
|
||||||
|
extent_idx, &extent_pos);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
error = z_erofs_read_extent(em, en, extent_pos, recsz, &ext);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
map->m_plen = le32toh(ext.plen);
|
||||||
|
if (pa != EROFS_NULL_ADDR) {
|
||||||
|
map->m_pa = pa;
|
||||||
|
if (z_erofs_extent_add(pa,
|
||||||
|
map->m_plen & Z_EROFS_EXTENT_PLEN_MASK,
|
||||||
|
&next) != 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
pa = next;
|
||||||
|
} else {
|
||||||
|
map->m_pa = le32toh(ext.pstart_lo);
|
||||||
|
}
|
||||||
|
if (extent_idx == UINT64_MAX)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
extent_idx++;
|
||||||
|
if (z_erofs_extent_add(lstart, cluster_size, &next) != 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
lstart = next;
|
||||||
|
if (lstart > map->m_la)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (z_erofs_extent_roundup(lend, cluster_size, &rounded_lend) != 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
last = lstart >= rounded_lend;
|
||||||
|
lend = MIN(lstart, lend);
|
||||||
|
lstart -= cluster_size;
|
||||||
|
} else {
|
||||||
|
lstart = lend;
|
||||||
|
for (l = 0, r = en->z_extents; l < r;) {
|
||||||
|
mid = l + (r - l) / 2;
|
||||||
|
error = z_erofs_extent_record_pos(en, table_pos, recsz, mid,
|
||||||
|
&extent_pos);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
error = z_erofs_read_extent(em, en, extent_pos,
|
||||||
|
recsz, &ext);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
la = z_erofs_extent_lstart(&ext, recsz);
|
||||||
|
pa = le32toh(ext.pstart_lo) |
|
||||||
|
((uint64_t)le32toh(ext.pstart_hi) << 32);
|
||||||
|
if (la > map->m_la) {
|
||||||
|
r = mid;
|
||||||
|
if (la > lend)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
lend = la;
|
||||||
|
} else {
|
||||||
|
l = mid + 1;
|
||||||
|
if (map->m_la == la)
|
||||||
|
r = MIN(l + 1, r);
|
||||||
|
lstart = la;
|
||||||
|
map->m_plen = le32toh(ext.plen);
|
||||||
|
map->m_pa = pa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last = l >= en->z_extents;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lstart < lend) {
|
||||||
|
map->m_la = lstart;
|
||||||
|
if (last &&
|
||||||
|
(en->z_advise & Z_EROFS_ADVISE_FRAGMENT_PCLUSTER) != 0) {
|
||||||
|
map->m_flags = EROFS_MAP_FRAGMENT;
|
||||||
|
en->z_fragmentoff = map->m_plen;
|
||||||
|
if (recsz > offsetof(struct z_erofs_extent, pstart_lo))
|
||||||
|
en->z_fragmentoff |= map->m_pa << 32;
|
||||||
|
} else if ((map->m_plen & Z_EROFS_EXTENT_PLEN_MASK) != 0) {
|
||||||
|
map->m_flags = EROFS_MAP_MAPPED;
|
||||||
|
fmt = map->m_plen >> Z_EROFS_EXTENT_PLEN_FMT_BIT;
|
||||||
|
if ((map->m_plen & Z_EROFS_EXTENT_PLEN_PARTIAL) != 0)
|
||||||
|
map->m_flags |= EROFS_MAP_PARTIAL_REF;
|
||||||
|
map->m_plen &= Z_EROFS_EXTENT_PLEN_MASK;
|
||||||
|
if (fmt != 0)
|
||||||
|
map->m_algorithmformat = fmt - 1;
|
||||||
|
else if (interlaced &&
|
||||||
|
((map->m_pa | map->m_plen) & bmask) == 0)
|
||||||
|
map->m_algorithmformat =
|
||||||
|
Z_EROFS_COMPRESSION_INTERLACED;
|
||||||
|
else
|
||||||
|
map->m_algorithmformat =
|
||||||
|
Z_EROFS_COMPRESSION_SHIFTED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map->m_llen = lend - map->m_la;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
z_erofs_fill_inode(struct erofs_mount *em, struct erofs_node *en)
|
||||||
|
{
|
||||||
|
struct z_erofs_map_header *h;
|
||||||
|
struct erofs_map_blocks map;
|
||||||
|
uint64_t cluster_size, raw, pos, rounded_size;
|
||||||
|
unsigned int recsz;
|
||||||
|
void *buf;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
if (en->z_initialized)
|
||||||
|
return (0);
|
||||||
|
if (z_erofs_extent_add(en->inode_off, en->inode_isize, &pos) != 0 ||
|
||||||
|
z_erofs_extent_add(pos, en->xattr_isize, &pos) != 0 ||
|
||||||
|
z_erofs_extent_roundup(pos, 8, &pos) != 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
error = erofs_read_metadata(em, en->nid, pos, sizeof(*h), &buf);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
h = buf;
|
||||||
|
if ((h->h_clusterbits & (1U << Z_EROFS_FRAGMENT_INODE_BIT)) != 0) {
|
||||||
|
if (!erofs_sb_has_fragments(em) || em->packed_nid == 0) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
raw = le64dec(h);
|
||||||
|
en->z_advise = Z_EROFS_ADVISE_FRAGMENT_PCLUSTER;
|
||||||
|
en->z_fragmentoff = raw ^ (1ULL << 63);
|
||||||
|
en->z_tailextent_headlcn = 0;
|
||||||
|
en->fragment = true;
|
||||||
|
erofs_brelse(buf);
|
||||||
|
en->z_initialized = true;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
en->z_advise = le16toh(h->h_advise);
|
||||||
|
en->z_lclusterbits = em->block_bits + (h->h_clusterbits & 15);
|
||||||
|
if (en->z_lclusterbits >= 31) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
if (en->datalayout == EROFS_INODE_COMPRESSED_FULL &&
|
||||||
|
(en->z_advise & Z_EROFS_ADVISE_EXTENTS) != 0) {
|
||||||
|
recsz = z_erofs_extent_recsize(en->z_advise);
|
||||||
|
if (recsz <= offsetof(struct z_erofs_extent, pstart_hi)) {
|
||||||
|
cluster_size = 1ULL << en->z_lclusterbits;
|
||||||
|
if (z_erofs_extent_roundup(en->size, cluster_size,
|
||||||
|
&rounded_size) != 0) {
|
||||||
|
erofs_brelse(buf);
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
en->z_extents = rounded_size >> en->z_lclusterbits;
|
||||||
|
} else {
|
||||||
|
en->z_extents = le32toh(h->h_extents_lo) |
|
||||||
|
((uint64_t)le16toh(h->h_extents_hi) << 32);
|
||||||
|
}
|
||||||
|
en->fragment =
|
||||||
|
(en->z_advise & Z_EROFS_ADVISE_FRAGMENT_PCLUSTER) != 0;
|
||||||
|
erofs_brelse(buf);
|
||||||
|
if (en->fragment &&
|
||||||
|
(!erofs_sb_has_fragments(em) || em->packed_nid == 0))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
if (recsz > offsetof(struct z_erofs_extent, pstart_hi) &&
|
||||||
|
en->z_extents == 0 && en->size != 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
error = z_erofs_validate_extent_table(em, en, recsz);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
en->z_initialized = true;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
en->z_algorithmtype[0] = h->h_algorithmtype & 15;
|
||||||
|
en->z_algorithmtype[1] = h->h_algorithmtype >> 4;
|
||||||
|
if ((en->z_advise & Z_EROFS_ADVISE_FRAGMENT_PCLUSTER) != 0)
|
||||||
|
en->z_fragmentoff = le32toh(h->h_fragmentoff);
|
||||||
|
else if ((en->z_advise & Z_EROFS_ADVISE_INLINE_PCLUSTER) != 0)
|
||||||
|
en->z_idata_size = le16toh(h->h_idata_size);
|
||||||
|
erofs_brelse(buf);
|
||||||
|
|
||||||
|
if (!erofs_sb_has_big_pcluster(em) &&
|
||||||
|
(en->z_advise & (Z_EROFS_ADVISE_BIG_PCLUSTER_1 |
|
||||||
|
Z_EROFS_ADVISE_BIG_PCLUSTER_2)) != 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
if (en->datalayout == EROFS_INODE_COMPRESSED_COMPACT &&
|
||||||
|
(((en->z_advise & Z_EROFS_ADVISE_BIG_PCLUSTER_1) != 0) !=
|
||||||
|
((en->z_advise & Z_EROFS_ADVISE_BIG_PCLUSTER_2) != 0)))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
if (en->z_idata_size != 0 ||
|
||||||
|
(en->z_advise & Z_EROFS_ADVISE_FRAGMENT_PCLUSTER) != 0) {
|
||||||
|
if ((en->z_advise & Z_EROFS_ADVISE_FRAGMENT_PCLUSTER) != 0 &&
|
||||||
|
(!erofs_sb_has_fragments(em) || em->packed_nid == 0))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
bzero(&map, sizeof(map));
|
||||||
|
error = z_erofs_map_blocks_fo(em, en, &map,
|
||||||
|
EROFS_GET_BLOCKS_FINDTAIL);
|
||||||
|
if (error != 0)
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
|
en->fragment =
|
||||||
|
(en->z_advise & Z_EROFS_ADVISE_FRAGMENT_PCLUSTER) != 0;
|
||||||
|
en->z_initialized = true;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
z_erofs_map_sanity_check(struct erofs_mount *em, struct erofs_node *en,
|
||||||
|
struct erofs_map_blocks *map)
|
||||||
|
{
|
||||||
|
uint64_t pend;
|
||||||
|
|
||||||
|
if ((map->m_flags & EROFS_MAP_FRAGMENT) != 0) {
|
||||||
|
if ((map->m_flags & (EROFS_MAP_MAPPED | EROFS_MAP_META)) != 0 ||
|
||||||
|
em->packed_inode == NULL || em->packed_inode->nid == en->nid ||
|
||||||
|
en->z_fragmentoff > em->packed_inode->size ||
|
||||||
|
map->m_llen > em->packed_inode->size - en->z_fragmentoff)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
if ((map->m_flags & EROFS_MAP_MAPPED) == 0)
|
||||||
|
return (0);
|
||||||
|
if ((unsigned char)map->m_algorithmformat >=
|
||||||
|
Z_EROFS_COMPRESSION_RUNTIME_MAX)
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
if (map->m_algorithmformat < Z_EROFS_COMPRESSION_MAX) {
|
||||||
|
if ((em->available_compr_algs &
|
||||||
|
(1U << map->m_algorithmformat)) == 0)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
if (EROFS_MAP_FULL(map->m_flags) && map->m_llen < map->m_plen)
|
||||||
|
return (EINTEGRITY);
|
||||||
|
} else if (map->m_llen > map->m_plen) {
|
||||||
|
return (EINTEGRITY);
|
||||||
|
}
|
||||||
|
if (map->m_plen > Z_EROFS_PCLUSTER_MAX_SIZE ||
|
||||||
|
map->m_llen > Z_EROFS_PCLUSTER_MAX_DSIZE)
|
||||||
|
return (EOPNOTSUPP);
|
||||||
|
if ((map->m_flags & EROFS_MAP_META) != 0)
|
||||||
|
return (0);
|
||||||
|
if (__builtin_add_overflow(map->m_pa, map->m_plen, &pend))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
if ((pend >> em->block_bits) >= (1ULL << 48))
|
||||||
|
return (EINTEGRITY);
|
||||||
|
(void)en;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
z_erofs_map_blocks_iter(struct erofs_mount *em, struct erofs_node *en,
|
||||||
|
struct erofs_map_blocks *map, int flags)
|
||||||
|
{
|
||||||
|
int error;
|
||||||
|
|
||||||
|
if (map->m_la >= en->size) {
|
||||||
|
map->m_llen = map->m_la + 1 - en->size;
|
||||||
|
map->m_la = en->size;
|
||||||
|
map->m_flags = 0;
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
error = z_erofs_fill_inode(em, en);
|
||||||
|
if (error == 0) {
|
||||||
|
if (en->datalayout == EROFS_INODE_COMPRESSED_FULL &&
|
||||||
|
(en->z_advise & Z_EROFS_ADVISE_EXTENTS) != 0)
|
||||||
|
error = z_erofs_map_blocks_ext(em, en, map, flags);
|
||||||
|
else
|
||||||
|
error = z_erofs_map_blocks_fo(em, en, map, flags);
|
||||||
|
}
|
||||||
|
if (error == 0)
|
||||||
|
error = z_erofs_map_sanity_check(em, en, map);
|
||||||
|
if (error != 0)
|
||||||
|
map->m_llen = 0;
|
||||||
|
return (error);
|
||||||
|
}
|
||||||
Executable
+462
@@ -0,0 +1,462 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Comprehensive decompression unit tests for repo19
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
TESTDIR="/tmp/repo19_decompress_tests"
|
||||||
|
SRCDIR="/work/repo-community/repo19/src"
|
||||||
|
RESULTS_FILE="/tmp/test_results.txt"
|
||||||
|
|
||||||
|
mkdir -p "$TESTDIR"
|
||||||
|
cd "$TESTDIR"
|
||||||
|
|
||||||
|
echo "==================================================================="
|
||||||
|
echo " REPO19 DECOMPRESSION COMPREHENSIVE UNIT TESTS"
|
||||||
|
echo "==================================================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test counters
|
||||||
|
TOTAL=0
|
||||||
|
PASSED=0
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
# Track results
|
||||||
|
> "$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)
|
||||||
Executable
+190
@@ -0,0 +1,190 @@
|
|||||||
|
#!/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!"
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
# 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.<slot>=/dev/<provider>`
|
||||||
|
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.
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# 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)
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# 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.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user