Skip to content

Commit b260188

Browse files
AlexStocksOmX
andcommitted
fix(security): bound RESP processing and pipeline queues
Constraint: Bound unauthenticated RESP parsing and optional pipeline resources without changing Redis command, storage, or Raft semantics. Confidence: High. Parser budgets, every active network consumer, legacy queue draining, and bounded pipeline admission have direct regression coverage. Scope-risk: Oversized or excessively expensive RESP frames now fail closed; queue admission shares the existing 30-second timeout. Red: Baseline parsing trusted attacker-controlled lengths, replayed incomplete aggregates without a cumulative budget, retained legacy command copies, and used an unbounded pipeline channel. Green: Resource-limit, parser-consumer, pipeline, and Linux TCP isolation regressions pass. Tested: Windows MSVC cargo test -p resp --all-features --locked; cargo test -p net --lib --all-features --locked; strict resp/net Clippy; fmt check; diff check. WSL/Linux exact oversized unauthenticated request and protocol-error isolation tests. Not-tested: Full WSL workspace suite. The earlier Windows workspace run reached all unit suites but retained the repository baseline of 19 TCP startup failures. Co-authored-by: OmX <omx@oh-my-codex.dev>
1 parent 94694d8 commit b260188

13 files changed

Lines changed: 835 additions & 194 deletions

.planning/KANBAN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
| ID | 工作项 | Requirement / Gate 关系 | 当前状态 |
1616
|---|---|---|---|
17-
| `RESP-LIMITS-001` | 限制未认证 RESP Array/Map/Set/Push 声明长度、初始预分配和嵌套深度 | `REQ-COMPAT-002``REQ-COMPAT-006``REQ-WORK-003` | PR `#404` 已创建;独立 review 缺口已修复,二次 review 无 finding,最终 Head/checks 以 GitHub 为准 |
17+
| `RESP-LIMITS-001` | 有界 RESP 解析、未认证连接 buffer、parser 历史副本和 optional pipeline 背压 | `REQ-COMPAT-002``REQ-COMPAT-006``REQ-STABILITY-002``REQ-STABILITY-003``REQ-WORK-003` | PR `#404` 已创建;本地 Windows/WSL 定向验证完成,待发布并复检最终 Head CI |
1818

1919
## Ready for a separate implementation task
2020

.planning/STATE.md

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,29 @@
88
>
99
> 实现基线:`main` at `cbc28958f261ae049d67a8b4a9d904d794b37726`
1010
>
11-
> 状态:PR 已创建;独立 review 的嵌套深度缺口已修复,二次 review 无 finding,最终 Head/checks 以 GitHub 实时查询为准
11+
> 状态:PR 已创建;嵌套深度及广义资源边界缺口已在本地修复并验证,待发布后复检最终 Head/checks
1212
>
13-
> 当前范围:拒绝超出 Redis 8.8.1 整数边界的 RESP 聚合长度,限制 Array/Map/Set/Push 的初始预分配和递归嵌套深度
13+
> 当前范围:限制 RESP 首行、payload、buffer、递归、解码节点和重复解析工作;限制未认证连接 buffer;清理 parser 历史副本;为 optional pipeline 建立真实背压
1414
>
1515
> Requirement 边界:`REQ-COMPAT-002``REQ-COMPAT-006``REQ-WORK-003`
1616
1717
## 当前目标
1818

19-
本 task 修复 Issue #395 B1 中已经由源码确认的未认证 RESP 聚合类型无界预分配问题。客户端声明长度不得直接控制 `Vec` 的初始容量;超出 Redis 8.8.1 `INT_MAX` 边界的声明返回协议错误,合法声明的初始容量最多为 1024,聚合递归最多为 128 层
19+
本 task 修复 Issue #395 B1 中已经由源码确认的未认证 RESP 资源耗尽路径,并处理 Issue #398 的 optional pipeline 无界 channel。客户端声明长度不得直接控制分配;解析器和未认证连接具有内存、深度、对象数与累计工作预算;`command_queue_size` 形成真实背压
2020

21-
本 task 不处理实际流入的超大 bulk 或连接累计 buffer 限额,不修改 PR #402 的文档,不处理 Issue #395 的其他条目,也不实现 Redis Oracle provenance 或 Embedded Redis Hot Tier。
21+
经调用链复核,Issue #395`DEL``MSETNX` 和 expiration P0 描述与当前 executor gate、单写者 Raft apply 及 no-op compaction 不符。本 PR 不添加可能与 Raft apply 死锁的无效 record lock,也不修改 PR #402 的文档、Redis Oracle provenance 或 Embedded Redis Hot Tier。
2222

2323
## 当前授权边界
2424

2525
允许:
2626

27-
- 修改 `src/resp/src/parse.rs`、本 task 的设计/计划、`.planning/STATE.md``.planning/KANBAN.md`
28-
- 运行 resp crate 单测、Clippy、格式检查、Git diff 检查及与 changed surface 对应的验证
29-
- commitpush 并创建以 `main` 为 base 的独立 PR;发布后实时查询 checks 和 review threads。
27+
- 修改 `src/resp/src/parse.rs``src/net` 下 parser consumer、pipeline 和定向测试、本 task 的设计/计划、`.planning/STATE.md``.planning/KANBAN.md`
28+
- 运行 resp/net 定向测试、workspace 测试、Clippy、格式检查、Git diff 检查和 WSL/Linux TCP 验证
29+
- commit 并 fast-forward push 到现有 PR #404 的 head 分支;发布后实时查询 checks 和 review threads。
3030

3131
禁止:
3232

33-
- 修改 Cargo、网络/认证逻辑、其他生产源码、PR #402 文档、Oracle provenance 或 Hot Tier 实现。
33+
- 修改 Cargo、storage/Raft/命令语义、PR #402 文档、Oracle provenance 或 Hot Tier 实现。
3434
- 在旧 `redis-8.8.1-stability-foundation` worktree 继续、暂存、提交、push、清理或回退六文件实现草稿。
3535
- 把旧草稿的绿色测试、审查或真实构建准备表述为方案 A 已实现。
3636
- 扩大到 Embedded Redis Hot Tier、Redis fork、动态库、loader、Cache ON 或组合发行实现。
@@ -41,6 +41,9 @@
4141
- PR `#383` 已于 2026-07-28 合并:final Head `42c16bef899385bd2e1b1e16e2e0202d4a614590`,merge commit `58030e1331655546ea4547a9a94efc493534ef7d`;它只完成 Oracle 方案 A 的规划闭环。
4242
- PR `#388` 已于 2026-07-30 合并;本 task 是从最新 `main` 创建的独立 implementation task。
4343
- RESP 聚合长度上限采用 Redis 8.8.1 的 `INT_MAX`,初始预分配上限采用 1024。
44+
- 所有 RESP 首行上限为 64 KiB,bulk payload 上限为 512 MiB,通用 parser buffer 上限为 1 GiB,未认证连接 buffer 上限为 1 MiB。
45+
- 单 frame 最多物化 65,536 个 RESP 节点;分片 aggregate 重放最多累计 1,000,000 次节点访问。
46+
- optional pipeline 使用容量至少为 1 的 bounded Tokio channel,queue admission 纳入现有 30 秒 timeout。
4447
- `D011`:Redis Oracle required provenance 采用 verifier fresh-checkout independent rebuild 和 exact binary hash equality。
4548
- `D012`:规划 task 与实施 task 分离;规划批准不授权源码实现,提前产生的实现草稿冻结。
4649
- Redis 8.8.1 tag `8.8.1` / commit `77b6c308396c9700672390a210143a8496fb4b10` 是唯一兼容和 Oracle 基线。
@@ -114,13 +117,18 @@ D:\test\github\kiwi\.worktrees\redis-8.8.1-stability-foundation\.codex\recovery\
114117
- 最终工作区 Windows 与 WSL `cargo clippy -p resp --all-targets -- -D warnings -D clippy::unwrap_used`:通过。
115118
- 最终工作区 `cargo fmt --all -- --check``git diff --check`:通过。
116119
- 二次独立 review 未发现 Critical、Important 或 Minor;确认 128/129 深度边界、四类聚合错误传播和容量 helper 测试与设计一致。
120+
- 后续广义审计新增 3 个 Important:非 inline 首行绕过、分片 aggregate O(N²) 重放、协议 count 与实际对象预算混淆;均已通过统一首行、65,536 节点和 1,000,000 node-visits 预算修复。
121+
- 最终 consolidation worktree Windows `cargo test -p resp --all-features --locked`:71 unit + 20 integration + doc tests 全部通过。
122+
- 最终 consolidation worktree Windows `cargo test -p net --lib --all-features --locked`:32/32 通过;严格 resp/net Clippy 以 `-D warnings -D clippy::unwrap_used` 通过。
123+
- WSL/Linux RESP 结果同为 71 + 20 + doc tests;新增未认证超限 TCP 与既有 protocol-error 控制用例均为 1/1 通过。
124+
- Windows workspace 已运行;到达的 unit suites 全部通过,19 个 TCP 用例统一复现仓库基线 `server did not become connectable`,不作为本分支回归。
117125
- checks、review threads 和 PR 状态不在本文件中缓存;任何当前结论必须重新查询 GitHub。
118126

119127
PR `#383` 的结果只证明 Oracle 规划闭环,不证明方案 A 已实现;PR `#388` 也不改变该结论。
120128

121129
## 下一条安全动作
122130

123-
1. 提交并 push PR #404 review 修复后,重新查询最终 Head 的 checks、评论和 review threads;不得把旧 Head 的 CI 结果作为最终状态。
131+
1. 提交并 fast-forward push PR #404 的 consolidation 修复后,重新查询最终 Head 的 checks、评论和 review threads;不得把旧 Head 的 CI 结果作为最终状态。
124132
2. 若最终 Head checks 未完成,只报告 pending,不给可 Merge 结论。
125133
3. 不 Resolve 或回复 #402/#404 review thread,不 merge PR。
126134
4. PR `#383` 的规划历史保持不变,旧六文件 Oracle 草稿继续冻结。
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Bounded Request Processing Implementation Plan
2+
3+
> **For AI workers:** Required sub-skill: use superpowers:subagent-driven-development to implement this plan task by task. Track steps with checkboxes.
4+
5+
**Goal:** Bound unauthenticated RESP parsing and optional pipeline queuing, and stop active connections from retaining duplicate command history.
6+
7+
**Architecture:** Keep the existing parser and network APIs, add explicit parser limits with depth-aware recursive parsing, enforce the unauthenticated limit at the active network boundary, drain the legacy command queue, and replace the optional pipeline's unbounded channel with a bounded sender. Each behavior is introduced through a failing regression test before production code changes.
8+
9+
**Tech Stack:** Rust 1.97.1, nom 8, Tokio mpsc, Cargo tests, Windows MSVC and WSL/Linux validation.
10+
11+
---
12+
13+
### Task 1: Bound RESP lengths, allocation, buffering, and nesting
14+
15+
**Files:**
16+
- Modify: `src/resp/src/parse.rs`
17+
- Modify: `src/resp/src/error.rs` only if a structured limit variant is required
18+
19+
- [x] Add parser tests using small private limits for bulk length, aggregate length, exact boundaries, nesting depth, chunked buffer growth, and reset-after-error.
20+
- [x] Run `cargo +1.97.1-x86_64-pc-windows-msvc test -p resp --lib parse::tests --locked` and confirm the new assertions fail because over-limit frames are incomplete or complete.
21+
- [x] Add immutable parser limits, pre-append buffer checks, depth-aware aggregate parsing, bounded initial capacity, and fallible incremental reservation.
22+
- [x] Add uniform first-line limits, a decoded-node budget, and a cumulative incomplete-frame parse-work budget after code-quality review.
23+
- [x] Re-run the parser tests and all `resp` tests; require zero failures.
24+
- [x] Commit `fix(resp): bound request parsing resources`.
25+
26+
### Task 2: Enforce pre-authentication limits and drain parser history
27+
28+
**Files:**
29+
- Modify: `src/net/src/network_handle.rs`
30+
- Modify: `src/net/src/optimized_handler.rs`
31+
- Modify: `src/net/tests/storage_command_e2e_tests.rs`
32+
- Modify: `src/resp/src/parse.rs` only for read-only buffer/queue observability
33+
34+
- [x] Add a TCP regression test that sends an incomplete request beyond 1 MiB before `AUTH`, observes connection closure, then authenticates and pings through a healthy connection.
35+
- [x] Add a parser/network regression proving complete frames do not accumulate undrained legacy commands in active consumers.
36+
- [x] Run the two new tests and confirm timeout or retained-command failures on the baseline implementation.
37+
- [x] Check `buffered_len + read_len` before unauthenticated parsing and drain one legacy command result after every complete frame in all consumers.
38+
- [x] Re-run the new tests plus the existing bad-connection protocol test; require zero failures under WSL/Linux.
39+
- [x] Commit `fix(net): cap unauthenticated request buffering`.
40+
41+
### Task 3: Enforce bounded pipeline admission
42+
43+
**Files:**
44+
- Modify: `src/net/src/pipeline.rs`
45+
46+
- [x] Add tests proving zero capacity normalizes to one and that the production channel helper rejects immediate admission after its configured capacity is occupied.
47+
- [x] Run `cargo +1.97.1-x86_64-pc-windows-msvc test -p net pipeline::tests --locked` and confirm the bounded-capacity assertions fail.
48+
- [x] Replace `UnboundedSender/UnboundedReceiver` with bounded Tokio mpsc, await admission under the existing timeout, and report actual normalized capacity.
49+
- [x] Re-run pipeline and net tests; require zero failures.
50+
- [x] Commit `fix(net): enforce pipeline queue capacity`.
51+
52+
### Task 4: Integrate, review, and publish one PR
53+
54+
**Files:**
55+
- Modify: this plan's checkboxes as each gate completes
56+
57+
- [x] Run `cargo fmt --all -- --check`.
58+
- [x] Run strict Clippy for `resp` and `net` with all targets/features and `-D warnings -D clippy::unwrap_used`.
59+
- [x] Run `cargo test --workspace --all-features --locked` with `RUST_TEST_THREADS=1`; all reached unit suites pass, while the 19 Windows TCP tests reproduce the repository baseline inability to start the server.
60+
- [x] Run the targeted RESP and network tests under WSL/Linux with an independent target directory.
61+
- [x] Run `git diff --check` and verify only planned files changed.
62+
- [x] Complete specification compliance review, then code-quality review, and resolve every important finding.
63+
- [ ] Fast-forward the existing PR #404 head `codex/fix-resp-parser-limits` with the consolidated fix; do not merge it.

docs/superpowers/plans/2026-07-31-resp-parser-resource-limits.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# RESP 聚合类型资源限额实现计划
22

3+
> 最终 PR 范围已扩展,本文件作为聚合预分配与 nesting 阶段计划保留;现行计划见
4+
> `2026-07-31-bounded-request-processing.md`
5+
36
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
47
58
**目标:** 阻止未认证客户端通过 RESP 聚合类型声明长度或递归嵌套触发无界初始分配、capacity-overflow panic 或栈耗尽。
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Bounded Request Processing Design
2+
3+
## Goal
4+
5+
Prevent unauthenticated or slow clients from causing unbounded memory growth in
6+
the active RESP parsing path and the optional command pipeline, while preserving
7+
Redis-compatible request limits and existing command behavior.
8+
9+
## Scope
10+
11+
The change covers three proven ownership problems:
12+
13+
1. RESP length headers can trigger oversized aggregate reservations before
14+
authentication, bulk frames can grow the parser buffer without a declared
15+
length check, and recursive aggregates have no nesting limit.
16+
2. The active network path returns parsed `RespData` but leaves a second
17+
`RespCommand` copy in `RespParse`, retaining command history for the lifetime
18+
of a connection.
19+
3. `PipelineConfig::command_queue_size` is reported but ignored because the
20+
optional pipeline uses an unbounded channel.
21+
22+
The storage findings from issue #395 are not implementation scope. Normal
23+
client `DEL` and `MSETNX` calls already hold the executor's
24+
`STORAGE_EXCLUSIVE` gate, Raft apply is single-writer, and expiration compaction
25+
is currently a no-op. Those facts invalidate their current P0 data-loss
26+
descriptions; adding local locks would not repair the claimed Raft interleaving.
27+
28+
## RESP Limits
29+
30+
`RespParse` owns immutable limits. Production constructors use:
31+
32+
- 64 KiB maximum first-line length for inline commands and every RESP type or
33+
aggregate header.
34+
- 512 MiB maximum bulk, bulk-error, and verbatim-string payload length.
35+
- 1 GiB maximum buffered frame length for authenticated/general parsing.
36+
- `i32::MAX` maximum aggregate item or pair count, matching Redis' multibulk
37+
header boundary.
38+
- 128 maximum aggregate nesting depth.
39+
- At most 1024 elements of initial aggregate reservation, followed by fallible
40+
incremental reservation.
41+
- At most 65,536 decoded `RespData` nodes in one frame. The `i32::MAX` wire
42+
count remains accepted as a protocol boundary, but it cannot force that many
43+
objects to be materialized.
44+
- At most 1,000,000 cumulative node visits while an incomplete frame is
45+
reparsed. This bounds repeated aggregate-prefix work without changing the
46+
public parser API.
47+
48+
Tests use a private constructor with small limits, so boundaries are exercised
49+
without allocating large buffers. Limits are checked before extending the
50+
buffer or reserving aggregate memory. Limit violations are terminal parser
51+
errors rather than incomplete frames.
52+
53+
The active network path also enforces a 1 MiB buffer limit before authentication.
54+
It checks the existing incomplete-frame size plus the next read before passing
55+
bytes to the parser. A violation closes only that connection through the
56+
existing protocol-error path.
57+
58+
## Parser Ownership
59+
60+
The public parser API remains compatible in this PR. All network-side parser
61+
consumers route through one helper that drains the corresponding legacy
62+
`next_command()` entry after each complete frame. This keeps the internal queue
63+
bounded without combining an API removal with the security fix. Removing the
64+
duplicate command representation is a follow-up API cleanup.
65+
66+
## Pipeline Backpressure
67+
68+
The optional pipeline uses `tokio::sync::mpsc::channel` with a minimum capacity
69+
of one. `submit_command` awaits bounded-channel capacity and applies the existing
70+
30-second timeout to queue admission as well as response delivery. Channel
71+
closure remains a distinct error. Statistics report the normalized, real queue
72+
capacity.
73+
74+
## Error Handling
75+
76+
- Malformed or over-limit RESP frames become `RespParseResult::Error`.
77+
- An over-limit unauthenticated connection is closed without affecting other
78+
clients or the server process.
79+
- Queue admission timeout returns `PipelineError::Timeout`.
80+
- No production path uses `unwrap`, `expect`, or infallible allocation for
81+
attacker-controlled aggregate capacity.
82+
83+
## Verification
84+
85+
The implementation must prove red-green behavior for oversized first lines and
86+
length headers, nesting, decoded-node amplification, cumulative incomplete
87+
aggregate work, chunked buffer growth, parser reset, legacy queue draining, and
88+
bounded pipeline capacity. Final gates are targeted tests, workspace tests,
89+
strict Clippy, formatting, `git diff --check`, Linux/WSL verification, and
90+
current PR Head/check reconciliation.

docs/superpowers/specs/2026-07-31-resp-parser-resource-limits-design.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# RESP 聚合类型资源限额设计
22

3+
> 最终 PR 范围已扩展,本文件作为聚合预分配与 nesting 阶段记录保留;现行合同见
4+
> `2026-07-31-bounded-request-processing-design.md`
5+
36
> 状态:已批准
47
> 日期:2026-07-31
58
> 基线:`main` at `cbc28958f261ae049d67a8b4a9d904d794b37726`

src/net/src/async_resp_parser.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ impl AsyncRespParser {
7272

7373
// Parse as many complete commands as possible
7474
loop {
75-
let parse_result = self.parser.parse(self.buffer.split().freeze());
75+
let parse_result = crate::network_handle::parse_request(
76+
&mut self.parser,
77+
self.buffer.split().freeze(),
78+
);
7679

7780
match parse_result {
7881
RespParseResult::Complete(command_data) => {
@@ -230,6 +233,18 @@ mod tests {
230233
assert_eq!(parser.buffered_bytes(), 0);
231234
}
232235

236+
#[test]
237+
fn parse_data_discards_underlying_legacy_command_copy() {
238+
let mut parser = AsyncRespParser::new(RespVersion::RESP2);
239+
240+
let commands = parser
241+
.parse_data(Bytes::from_static(b"*1\r\n$4\r\nPING\r\n"))
242+
.expect("valid command should parse");
243+
244+
assert_eq!(commands.len(), 1);
245+
assert!(parser.parser.next_command().is_none());
246+
}
247+
233248
#[test]
234249
fn test_command_batch_priority() {
235250
let ping_cmd = RespData::Array(Some(vec![RespData::BulkString(Some(Bytes::from("PING")))]));

src/net/src/handle.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ use cmd::table::CmdTable;
2424
use executor::{CmdExecution, CmdExecutor};
2525
use log::error;
2626
use resp::encode::RespEncoder;
27-
use resp::{Parse, RespData, RespEncode, RespParseResult};
27+
use resp::{RespData, RespEncode, RespParseResult};
2828
use storage::storage::Storage;
2929
use tokio::select;
3030
use tokio_util::sync::CancellationToken;
3131

32+
use crate::network_handle::parse_client_request;
3233
use crate::storage_client::StorageClient;
3334

3435
pub async fn process_connection(
@@ -47,8 +48,11 @@ pub async fn process_connection(
4748
Ok(n) => {
4849
if n == 0 { return Ok(()); }
4950

50-
let parse_result =
51-
resp_parser.parse(Bytes::copy_from_slice(&buf[..n]));
51+
let parse_result = parse_client_request(
52+
&mut resp_parser,
53+
client.is_authenticated(),
54+
Bytes::copy_from_slice(&buf[..n]),
55+
)?;
5256
match parse_result {
5357
RespParseResult::Complete(data) => {
5458
if let RespData::Array(Some(params)) = data {

0 commit comments

Comments
 (0)