本章导读
文件下章讲目录的三个能力——从单文件操作升到"文件的容器与边界"。目录枚举(xrtDirOpen/Next):三态逐项枚举,且一次系统调用带出类型与尺寸——传统 readdir 每项补一次 stat 的 2N 次系统调用在这里是 N 次,大目录枚举性能差一个数量级。递归遍历:目录树的深度优先行走(dir_tour 范例展开)。沙箱根(xrtRootOpen):目录句柄作为安全边界——根内操作只收相对名、穿越与绝对路径系统级拒绝,是第 44 章词法校验(IsSafeEntry)之外的第二道防线(纵深防御的系统级一环)(词法可被绕过时,系统级语义兜底)。
47.1引入
解压用户上传的压缩包:包内条目名是不可信输入——../../evil.so 的穿越条目若被直接落盘就写到了沙箱外。第 44 章的词法校验(IsSafeEntry)能拦住绝大多数,但词法层有个根本弱点:它判断的是字符串,符号链接、平台特例、规范化差异都可能让词法判断与文件系统实际行为脱节。
沙箱根从系统级解决:xrtRootOpen 打开目录得到句柄,此后一切操作(建文件、开文件、建目录)都以句柄为基准、只收相对名——穿越在操作系统层面就无处遁形(不是"检查后放行"而是"根本没有逃出路径")。根句柄还有个附带福利:目录被改名/移动后依然有效(句柄不依赖路径字符串)——日志轮转、目录重组不打断进行中的操作。
47.2概念
目录枚举:三态接口与零额外 stat 的性能红利
"零额外 stat"的性能含义:传统 readdir 拿到名字后,想知道"是文件还是目录"要再调一次 stat——N 个条目 2N 次系统调用;XRT 的目录 API 直接带出类型与尺寸(Windows/Linux 的原生目录信息本就包含)——N 次调用,大目录枚举快一个数量级。条目里的名字是借用视图,时效止于下一次 Next(第 18 章迭代纪律)。
递归遍历(目录树的深度优先行走)
目录树的深度优先行走:枚举当前层 → 遇子目录递归进入 → 完成后回溯。递归深度受第 3 章资源边界保护(防恶意深目录树);遍历中目录被并发增删是真实风险——枚举期间内容被外部修改——遍历器语义按"快照尽力"处理,需要严格一致时先收集再处理(第 18 章坑 2 的目录版)。
沙箱根:句柄即边界(系统级而非词法级)
xrtRootOpen / xrtRootOpenIn(嵌套子根)打开目录为根;xrtRootFileOpen(根内开文件)、xrtRootDirCreate/Remove(根内建删目录)全部只收相对名——../ 与绝对路径被拒绝。与第 44 章词法校验的配合关系:词法层做第一道过滤(快速拒绝明显的恶意形态,零成本),系统级根做最终保障(语义正确,覆盖词法盲区)——两层防线不是冗余,是纵深防御——工程安全的标准姿势:便宜的挡量、结实的兜底。
临时文件与目录的配合(批量暂存的标准容器)
xrtDirTemp(第 46 章提到)在指定目录创建唯一命名的临时目录——"一批临时文件需要一个独立家"的场景(解压暂存、构建中间产物):建临时目录 → 里面随便放 → 整目录删除收尾。比散落的临时文件干净一个量级——"家"的语义让清理变成一次调用。
47.3示例
完整程序:零额外 stat 枚举
来自仓库范例 examples/file/directory/main.c:
#include <stdio.h> #include <xrt.h> */ /* 展示零额外 stat 的目录枚举。 */ int main(void) { xdir Dir = xrtDirOpen(".", 0u); xdirentry Entry; xdirnext Next; if ( Dir == NULL ) { return 1; } while ( (Next = xrtDirNext(Dir, &Entry)) == XDIR_NEXT_ITEM ) { printf("%.*s\n", (int)Entry.Name.Size, Entry.Name.Data); } if ( Next == XDIR_NEXT_ERROR ) { (void)xrtDirClose(Dir); return 1; } return xrtDirClose(Dir) ? 0 : 1; }
刚才发生了什么。 ① xrtDirOpen(".", 0) 打开当前目录——返回目录流句柄(不是文件句柄,两种句柄不可混用)。② xrtDirNext 三态循环:ITEM 打印条目名、END 自然结束、ERROR 中止并报告——与第 41 章行读取器的三态(LINE/END/失败)同构,XRT 流式接口的统一形态在目录上延续。③ 每个条目一次调用就带出名字与类型尺寸——内容随当前目录自然变化(输出清单因仓库而异)——本例只打印名字,但 Entry 里的类型字段(文件/目录)随手可用,不需要再 stat。④ 名字是借用视图(第 3 章约定)——%.*s 打印后即弃,需要保留自己拷贝(坑 1 的 good 形态正是"先拷贝再收集")。
完整程序:沙箱根操作
来自 examples/file/root/main.c——不可信相对名的安全存储:
#include <stdio.h> #include <string.h> #include <xrt.h> */ /* 使用目录能力安全处理不可信的相对文件名。 */ int main(int argc, char** argv) { cstr sName = argc > 1 ? argv[1] : "message.txt"; const cstr sDirectory = ".xrt-root-example-data"; xfileoptions Options; xfile File; xroot Parent; xroot Root; Parent = xrtRootOpen("."); if ( Parent == NULL ) { fprintf(stderr, "%s\n", xrtErrorMessage(xrtGetError())); return 1; } if ( !xrtRootDirCreate(Parent, sDirectory, 0700u) ) { fprintf(stderr, "%s\n", xrtErrorMessage(xrtGetError())); (void)xrtRootClose(Parent); return 1; } Root = xrtRootOpenIn(Parent, sDirectory); if ( Root == NULL ) { fprintf(stderr, "%s\n", xrtErrorMessage(xrtGetError())); (void)xrtRootRemove(Parent, sDirectory); (void)xrtRootClose(Parent); return 1; } xrtFileOptionsInit(&Options); Options.Flags = XFILE_WRITE | XFILE_CREATE | XFILE_TRUNCATE; File = xrtRootFileOpen(Root, sName, &Options); if ( File == NULL ) { fprintf(stderr, "%s\n", xrtErrorMessage(xrtGetError())); (void)xrtRootClose(Root); (void)xrtRootRemove(Parent, sDirectory); (void)xrtRootClose(Parent); return 1; } if ( !xrtWriteFull(File, "stored inside root\n", 19u, NULL) || !xrtClose(File) || !xrtRootRemove(Root, sName) || !xrtRootClose(Root) || !xrtRootRemove(Parent, sDirectory) || !xrtRootClose(Parent) ) { fprintf(stderr, "%s\n", xrtErrorMessage(xrtGetError())); return 1; } printf("stored and removed %s/%s\n", sDirectory, sName); return 0; }
刚才发生了什么。 ① xrtRootOpenIn 在父目录下开根——嵌套形态允许"每个上传一个根"。② xrtRootDirCreate 在根内建目录、xrtRootFileOpen 在根内开文件——全部相对名:传 ../escape 或绝对路径会被系统级拒绝(不是词法检查,是句柄语义的自然结果——操作就发生在根内,没有"根外"这个选项)。③ 存储完成、清理后 xrtRootClose 关根——根内资源随根的生命周期统一管理。④ 输出的路径带根前缀(审计可读),但操作从未依赖这个字符串——句柄不依赖路径,改名移动不打断。把 dir_tour 与 root_tour 留作进阶阅读:递归遍历与根操作的完整接口面各成一组断言输出。
47.4契约
- 三态枚举:ITEM/END/ERROR 三态与全库流式接口同构;名字视图借用不出迭代期。
- 零额外 stat:类型尺寸随条目带出——大目录枚举 N 次调用不是 2N。
- 遍历边界:深度受资源边界保护;遍历中增删按"快照尽力",严格一致先收集再处理。
- 沙箱语义:根内只收相对名;穿越与绝对路径系统级拒绝;根句柄不依赖路径字符串(改名移动后仍有效)。
- 纵深防线:词法校验(第 44 章)快速过滤 + 系统级根语义兜底——两层配合不是冗余。
- 临时目录:
xrtDirTemp唯一命名整目录管理——批量暂存的标准容器。
从示例到工程:目录能力的三个宿主
上传与解压(沙箱根的主场):每个上传一个根(RootOpenIn 嵌套)——穿越条目系统级拒收、正常条目根内还原、处理完整根删除;第 44 章词法校验做前置快速过滤,根句柄做最终保障。资源目录访问(枚举+根的组合):插件资源、静态文件目录——根句柄固定后目录可被运维改名重组(句柄不依赖路径),服务不中断。批量构建暂存(临时目录的主场):构建/转换任务的中间产物整目录管理——DirTemp 建家、任务完删除,比散落临时文件干净一个量级(第 46 章原子交付的批量版)。
遍历的工程纪律
递归遍历看似简单,工程化要补四条纪律。深度上限:恶意深目录树(或符号链接环)能打爆朴素递归——资源边界在枚举入口设闸。符号链接策略:跟随(默认可能成环)或不跟随(按类型字段识别后跳过)——策略显式声明并写进注释。错误恢复:单目录枚举失败(权限)是中止还是跳过——按遍历目的定(审计中止、清理跳过)。顺序假设:枚举顺序由文件系统决定(无序)——需要排序的清单在收集后自行排序(第 14 章)。四条纪律合起来:遍历器是框架代码不是循环样板——写之前先答四个问题。
与文件族前两章的合流
至此文件三章合流成完整地图:第 45 章的句柄与读写(单文件生命周期)、第 46 章的映射锁原子交付(并发与性能)、本章的目录与沙箱(组织与安全)。三者的典型组合贯穿全书后续:卷七静态文件服务(根句柄 + 枚举 + 映射)、卷十二构建工具(目录遍历 + 临时目录 + 原子交付)、解压器(沙箱根 + 三态枚举 + Full 读写)。文件能力是系统服务的地基——地基铺完,下一章的异步与信号补上最后两块砖。
常见错误
症状:遍历偶发漏项或 ERROR——并发清理脚本正在删遍历目标里的文件;复现依赖时序。
原因:目录枚举是"尽力快照"——底层目录流的语义不保证看到遍历期间的所有变更。
while ( (Next = xrtDirNext(Dir, &Entry)) == XDIR_NEXT_ITEM ) { if ( IsOld(Entry.Name) ) { DeleteByName(DirPath, Entry.Name); /* 边遍历边删——底层目录流受扰 */ } }
xarray tOld; xrtArrayInit(&tOld, sizeof(str)); while ( (Next = xrtDirNext(Dir, &Entry)) == XDIR_NEXT_ITEM ) { if ( IsOld(Entry.Name) ) { xrtArrayPush(&tOld, &(str){ xrtStrDupView(Entry.Name) }); /* 先收集名字 */ } } xrtDirClose(Dir); for ( size_t i = 0; i < tOld.Count; i++ ) { DeleteByName(DirPath, *(str*)xrtArrayGet(&tOld, i)); /* 遍历结束后再删 */ }
症状:渗透测试绕过了 IsSafeEntry——符号链接、大小写混淆或平台路径规范化差异让"词法上安全"的路径实际指向根外——字符串判断与文件系统解析在边界处脱节。
原因:词法判断的是字符串,文件系统解析的是真实路径——两者在符号链接等场景脱节。
if ( xrtPathIsSafeEntry(Name, false) ) { str sFull = xrtPathJoin(BaseDir, NameBuf); xrtFileOpen(sFull, ...); /* 词法过了就开——符号链接可能指向根外 */ }
xroot Root = xrtRootOpen(BaseDir, 0); xfile File = xrtRootFileOpen(Root, NameBuf, ...); /* 系统级边界:穿越无处发生 */ /* 词法校验仍可前置做快速过滤——但根句柄才是最终保障 */
编程练习
枚举一个目录,按类型(文件/目录)分别计数并输出总尺寸——零额外 stat 的一次调用数据全部用上。
实现 list_tree(路径, 深度上限):缩进输出目录树(深度优先)、每层计数文件与子目录、超限目录标注跳过——递归遍历与资源边界的组合。
用沙箱根实现压缩包落盘:根内创建目录结构(RootDirCreate 逐级)、文件落盘(RootFileOpen+Write)、恶意条目(穿越/绝对/设备名)全部拒收并列清单。验收标准:构造含 5 个恶意条目的包(穿越/绝对路径/设备名/符号链接环/超深嵌套各一),全部拒收且根外零文件;正常条目完整还原;整根删除后零残留——五类攻击的处置各写一行结论。
小结
| 知识点 | 速查 |
|---|---|
| 三态枚举 | DirOpen/Next/Close:ITEM=取到/END=结束/ERROR=失败,与全库流式同构 |
| 零额外 stat | 类型与尺寸随条目一次带出——N 次调用而非 2N,大目录快一个数量级 |
| 递归遍历 | 深度优先行走 + 资源边界深度闸;遍历中增删按快照尽力语义 |
| 沙箱根 | RootOpen/RootOpenIn/RootFileOpen/RootDirCreate:只收相对名,系统级拒绝穿越 |
| 纵深防线 | 词法校验(第 44 章)快速过滤 + 根句柄语义兜底——便宜的挡量、结实的兜底 |
| 临时目录 | xrtDirTemp 唯一命名——批量暂存整目录管理 |
| 三宿主 | 上传解压(根主场)/ 资源目录(枚举+根)/ 批量暂存(临时目录) |
| 遍历四纪律 | 深度上限 / 符号链接策略 / 错误恢复 / 顺序假设——框架不是样板 |
📖 本章涉及的 API 参考手册:file(132 个条目)path(41 个条目)
点击查看完整签名、参数约束、返回值与错误说明