{"repro_id":"REPRO-2026-00141","version":8,"title":"rsync: off-by-one out-of-bounds stack write in establish_proxy_connection","repro_type":"security","status":"published","severity":"low","description":"`rsync` can connect to a remote daemon through an HTTP proxy. The function\n`establish_proxy_connection()` in `socket.c` performs an **off-by-one\nout-of-bounds write** on a **stack buffer** while handling the proxy server's\n`CONNECT` response. A **malicious proxy server** that returns an overlong\n`CONNECT` response corrupts memory adjacent to that stack buffer.\n\nThe write is a single byte past the buffer end, so impact is limited (NVD rates\nthe issue **Low**), but it is still out-of-bounds stack corruption driven by\nattacker-controlled input.","root_cause":"# RCA Report: CVE-2026-45232\n\n## Summary\n\nCVE-2026-45232 is an off-by-one out-of-bounds stack write vulnerability in the `rsync` utility. The bug resides in `establish_proxy_connection()` within `socket.c`. When rsync connects through an HTTP proxy (configured via `RSYNC_PROXY`), it reads the proxy's `CONNECT` response into a 1024-byte stack buffer. If the proxy returns an overlong response (1023+ bytes without a newline terminator), post-loop null-termination logic writes one byte past the buffer's end, corrupting adjacent stack memory.\n\n## Impact\n\n- **Package**: `rsync` (C CLI tool)\n- **Affected versions**: `< 3.4.2`\n- **Fixed version**: `3.4.3`\n- **Risk level**: Low (NVD CVSS 3.1 base 3.1)\n- **Consequences**: A malicious or MITM'd HTTP proxy can trigger a single-byte stack overwrite, potentially leading to a crash or undefined behavior. The overwritten byte is always a null terminator (`\\0`), and the attacker does not control the offset, limiting exploitability.\n\n## Root Cause\n\nIn `socket.c`, `establish_proxy_connection()` declares a stack buffer `char buffer[1024]` and reads the proxy response one byte at a time into `buffer[0]` through `buffer[1022]`. The loop condition is `cp < &buffer[sizeof buffer - 1]`.\n\nIf no newline arrives before the buffer is filled, `cp` is left at `&buffer[1023]` (the last valid index). The post-loop code then executes:\n\n```c\nif (*cp != '\\n')\n    cp++;\n*cp-- = '\\0';\n```\n\nBecause `*cp` was never written by the loop (it contains stale stack data), the `if` is usually true, so `cp++` advances past the array to `&buffer[1024]`. The subsequent `*cp = '\\0'` writes one byte out of bounds.\n\n**Fix commit**: `a5fc5ebe7a8ef1aa72f6e344599f97fd4427ecba` (\"socket: reject over-long proxy response line\")\n\nThe fix:\n1. Increases the buffer size to `PROXY_BUF_SIZE + 1` (1025 bytes) for safe null-termination\n2. Adds an explicit check: if `cp == &buffer[PROXY_BUF_SIZE - 1]`, reject with `\"proxy response line too long\"` instead of writing past the buffer\n3. Correctly null-terminates within bounds for shorter responses\n\n## Reproduction Steps\n\n1. `repro/reproduction_steps.sh` clones the rsync repository, checks out `v3.4.2` (vulnerable) and `v3.4.3` (fixed), and builds both with AddressSanitizer (`-fsanitize=address`).\n2. It starts a minimal Python TCP mock proxy that binds to a local ephemeral port, accepts one connection, reads the `CONNECT` request headers, and sends exactly 1023 bytes of `'X'` with no newline terminator.\n3. It runs `rsync` via `RSYNC_PROXY=127.0.0.1:<port>` against each build.\n4. Expected behavior:\n   - **v3.4.2 (vulnerable)**: AddressSanitizer reports `stack-buffer-overflow` at `establish_proxy_connection` (`socket.c:95`), and rsync aborts.\n   - **v3.4.3 (fixed)**: rsync prints `\"proxy response line too long\"` and exits with error code 10 (non-zero but no crash).\n\n## Evidence\n\n- **Log files**:\n  - `logs/rsync_v3.4.2_proxy.log` — ASan output for the vulnerable build\n  - `logs/rsync_v3.4.3_proxy.log` — stderr for the fixed build\n- **ASan excerpt** (vulnerable build):\n  ```\n  ==7440==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7f747c600940\n  WRITE of size 1 at 0x7f747c600940 thread T0\n      #0 establish_proxy_connection /tmp/.../rsync/socket.c:95\n      ...\n  Memory access at offset 2368 overflows this variable\n      [1344, 2368) 'buffer' (line 55)\n  ```\n- **Fixed build excerpt**:\n  ```\n  proxy response line too long\n  rsync error: error in socket IO (code 10) at clientserver.c(141) [Receiver=3.4.3]\n  ```\n- **Runtime manifest**: `repro/runtime_manifest.json` records build versions, exit codes, ASan excerpt, payload size (1023 bytes), and confirmation flags.\n\n## Recommendations / Next Steps\n\n1. **Upgrade** to rsync `>= 3.4.3` to obtain the bounds-check fix.\n2. **Network controls**: Restrict proxy configurations (`RSYNC_PROXY`) to trusted infrastructure to reduce exposure to malicious proxies.\n3. **Testing**: The upstream test `testsuite/proxy-response-line-too-long.test` should be included in CI to prevent regression.\n4. **Build hardening**: Compile with `-fstack-protector-strong` and ASan in QA pipelines to detect similar stack overwrites early.\n\n## Additional Notes\n\n- **Idempotency**: The reproduction script was run twice consecutively; both runs produced identical ASan crashes on `v3.4.2` and clean rejection on `v3.4.3`.\n- **Payload specifics**: 1023 bytes of `'X'` without a newline is the exact boundary that triggers the off-by-one. A shorter payload does not trigger the bug; a newline-terminated payload exits the read loop early.\n- **Build environment**: Tested on `x86_64-pc-linux-gnu` with GCC and AddressSanitizer. The bug is architecture-agnostic, but ASan makes the one-byte overwrite reliably observable.\n","cve_id":"CVE-2026-45232","cwe_id":"CWE-193 (Off-by-one Error) / CWE-787 (Out-of-Bounds Write)","source_url":"https://github.com/RsyncProject/rsync.git","package":{"name":"rsync","ecosystem":"c","affected_versions":"< 3.4.3","fixed_version":"3.4.3"},"reproduced_at":"2026-05-22T10:23:58.910186+00:00","duration_secs":1769.496844291687,"tool_calls":144,"turns":117,"handoffs":2,"total_cost_usd":1.0184361199999998,"agent_costs":{"repro":0.31145339999999994,"support":0.03346132,"vuln_variant":0.6735213999999998},"cost_breakdown":{"repro":{"accounts/fireworks/models/kimi-k2p6":0.31145339999999994},"support":{"accounts/fireworks/models/kimi-k2p6":0.03346132},"vuln_variant":{"accounts/fireworks/models/kimi-k2p6":0.6735213999999998}},"quality":{"idempotent_verified":false,"community_verifications":0},"published_at":"2026-05-22T10:24:00.749614+00:00","retracted":false,"artifacts":[{"path":"repro/rca_report.md","filename":"rca_report.md","size":4745,"category":"analysis"},{"path":"repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":5211,"category":"reproduction_script"},{"path":"vuln_variant/rca_report.md","filename":"rca_report.md","size":7568,"category":"analysis"},{"path":"vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":8468,"category":"reproduction_script"},{"path":"bundle/context.json","filename":"context.json","size":2982,"category":"other"},{"path":"bundle/metadata.json","filename":"metadata.json","size":589,"category":"other"},{"path":"bundle/ticket.md","filename":"ticket.md","size":3563,"category":"ticket"},{"path":"repro/runtime_manifest.json","filename":"runtime_manifest.json","size":914,"category":"other"},{"path":"repro/validation_verdict.json","filename":"validation_verdict.json","size":1330,"category":"other"},{"path":"vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":1174,"category":"other"},{"path":"vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":3959,"category":"documentation"},{"path":"vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":2342,"category":"other"},{"path":"vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":645,"category":"other"},{"path":"vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":2031,"category":"other"},{"path":"vuln_variant/source_identity.json","filename":"source_identity.json","size":703,"category":"other"},{"path":"logs/variant1_vuln.log","filename":"variant1_vuln.log","size":567,"category":"log"},{"path":"logs/variant2_fixed.log","filename":"variant2_fixed.log","size":521,"category":"log"},{"path":"logs/variant1_fixed.log","filename":"variant1_fixed.log","size":567,"category":"log"},{"path":"logs/variant_run1.log","filename":"variant_run1.log","size":1110,"category":"log"},{"path":"logs/variant3_fixed.log","filename":"variant3_fixed.log","size":567,"category":"log"},{"path":"logs/variant_run3.log","filename":"variant_run3.log","size":1040,"category":"log"},{"path":"logs/variant_run2.log","filename":"variant_run2.log","size":1041,"category":"log"},{"path":"logs/rsync_v3.4.3_proxy.log","filename":"rsync_v3.4.3_proxy.log","size":526,"category":"log"},{"path":"logs/rsync_v3.4.2_proxy.log","filename":"rsync_v3.4.2_proxy.log","size":3330,"category":"log"},{"path":"logs/variant2_vuln.log","filename":"variant2_vuln.log","size":3278,"category":"log"},{"path":"logs/variant3_vuln.log","filename":"variant3_vuln.log","size":567,"category":"log"}]}