fix(dnfs2): update_note 成环检测(修复已知限制 L-1)

新增 _is_descendant 祖先走查:改 pid 时从新父节点沿 pid 链逐级向上,
命中自身即拒绝(自引用/移入自身子树均成环,子树不可达);深度上限 64
+ visited 集防存量脏数据死循环,db 错误 fail-closed 抛 500。
README 已知限制 L-1 移入安全修复表。
This commit is contained in:
qing committed 2026-08-13 04:10:35 +08:00
1 parent e9463a28fb
commit 9b6aa39b94
2 files changed
+54 -1

No files matched your search

+53
View File
@@ -13,6 +13,7 @@
* P1-8 update_note 改 pid≠0 时校验父节点存在
* P1-9 RpcClient 启用 retry_on_refused
* P1-11 note_id 显式 as_int64() + 类型校验
* L-1 update_note 成环检测(祖先走查,拒绝移入自身/自身子树)
*
* 依赖: libhttp (HttpServer), libmsg (RpcClient/AsyncQueue), boost::json,
* OpenSSL, libcurl
@@ -42,6 +43,7 @@
#include <string>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace json = boost::json;
@@ -661,6 +663,49 @@ private:
return std::string();
}
// L-1 修复:判定 candidate 是否位于 id 的子树内(即 id 是 candidate 的祖先)。
// db2 execute 不支持递归查询,从 candidate 沿 pid 链逐级向上走查,途中命中 id 即成立;
// 走到根(pid=0)即无环。深度上限 + visited 集防御存量脏数据(已含环)导致的死循环,
// 走查在写库之前,命中即拒绝,不会新增脏数据。
// db 业务错误抛 500(fail-closed,不误放行);candidate 不存在视为不在子树内
// (调用方已单独校验新父节点存在性)。
bool _is_descendant(int64_t id, int64_t candidate) {
constexpr int kMaxAncestorDepth = 64;
std::unordered_set<int64_t> visited;
int64_t cur = candidate;
for (int depth = 0; depth < kMaxAncestorDepth; ++depth) {
if (cur == id) return true;
if (cur == 0) return false;
if (!visited.insert(cur).second) return false; // 存量环,走不到根,按无环处理
json::object payload;
payload["action"] = "select";
payload["table"] = "Notes";
payload["columns"] = json::array{"pid"};
payload["where"] = json::object{{"id", cur}};
payload["limit"] = 1;
DbResult r = _call_db(payload);
if (!r.success) {
_log("ERROR", "dnfs2", "成环检测查询失败 cur=" + std::to_string(cur)
+ " err=" + r.error);
throw DnfsExp("查询节点失败", 500);
}
if (!r.data.is_array() || r.data.as_array().empty()) return false;
const auto& row = r.data.as_array()[0];
if (!row.is_object() || !row.as_object().contains("pid")
|| !row.as_object().at("pid").is_int64()) {
return false;
}
cur = row.as_object().at("pid").as_int64();
}
// 深度超限:保守拒绝,避免把潜在环写入
_log("WARNING", "dnfs2", "成环检测深度超限 id=" + std::to_string(id)
+ " candidate=" + std::to_string(candidate));
throw DnfsExp("节点层级过深,无法完成校验", 400);
}
// 观看人数自增(get_notes 打开节点 pid 时调用)。
// db2 的 update 仅支持字面量绑定(SET col = ?),无法表达 views = views + 1,
// 故采用「读当前值 → 写 +1」。读改写非原子,高并发下可能少计,作为浏览计数可接受。
@@ -1237,6 +1282,14 @@ private:
throw DnfsExp("父节点不存在", 400);
}
// L-1 修复:新父节点为自身或位于自身子树内会成环(子树不可达),拒绝。
// pid=0(挂到根)天然无环;pid==id 时走查首轮即命中
if (pid != 0 && _is_descendant(id, pid)) {
_log("WARNING", "dnfs2", "update_note 拒绝成环 id=" + std::to_string(id)
+ " pid=" + std::to_string(pid));
throw DnfsExp("不能将节点移动到自身或其子节点内", 400);
}
// 特殊节点校验:上方所有权校验已保证是节点拥有者或管理员,此处仅校验信封格式
json::value sp = parse_sp(content);
if (!sp.is_null()) {