Skip to content

RedisCache::writeBuffer uses entry.front(): it keeps the oldest entry of a batch (stale value) and crashes on an empty batch #121

Open

Description

@bigsamich

RedisAdapter version or revision

main at ff2d5b8. RedisCache.hpp is unchanged through the #108 → #109 → #111 stack; #108 changes which inputs crash (details below).

Redis version and deployment mode

Redis 7.4.11 standalone (TCP).

Observed behavior

RedisCache<T>::writeBuffer takes its data and timestamp from entry.front() (RedisCache.hpp:33 and :42). That is wrong in two ways.

1. Stale value. A legacy list callback receives every entry newer than the reader's cursor, in ascending ID order: XREAD is issued without COUNT, and the cursor then advances to the batch's last ID. When two or more entries arrive in one reply, the cache keeps the oldest value and timestamp and discards the newer ones for good. It stays stale until some later write. Multi-entry batches are routine:

  • a producer writing faster than one XREAD round trip plus dispatch, especially over a network;
  • addLists() batches, which keep all n entries;
  • reader restarts, which happen on every reader registration or removal in the bucket;
  • reconnects, which deliver the outage backlog at once.

2. Crash on an empty batch. The legacy list-reader wrapper calls the user callback even when nothing in the batch decoded. writeBuffer then dereferences a null begin() and segfaults on the worker thread, taking the process down.

Expected: the cache holds the newest entry of every batch, and an empty or undecodable batch leaves the last good value in place.

Reproduction

  • Stale value: initialise a RedisCache<float> on {B}:x. Write value 1 and value 2 in one MULTI/EXEC, so that the blocked XREAD returns both. Result on main and at the stack head: Redis latest …-1 value=2, RedisCache time=…-0 value=1. The cache is stale, and the newValueAvailable flag is set.
  • Crash: create RedisCache<float>(adapter, "wave"), then write an entry with no _ field, an empty _ (on main), or a 6-byte _ (with Add owned stream subscriptions and safe payload decoding #108):
RedisCache.hpp:33:55: runtime error: member access within null pointer of type 'const struct value_type'
ERROR: AddressSanitizer: SEGV on unknown address 0x000000000010 ... in std::vector<float>::operator=   (worker thread)

Build and runtime environment

  • Ubuntu 24.04, gcc 13.3 with -fsanitize=address,undefined, -std=c++20
  • pinned redis-plus-plus 1.3.15 and hiredis 1.3.0
  • a private Redis 7.4.11 in a container

Suggested fix

  • In writeBuffer: if (entry.empty()) return; const auto& latest = entry.back();, then use latest.second for the buffer and latest.first for lastWrite.
  • In both legacy typed wrappers: invoke the user callback only if (!ret.empty()). This also protects other consumers that index ats[0], as this repo's own tests do (the Add owned stream subscriptions and safe payload decoding #108 review has the details).
  • Add a test that delivers two entries in one XREAD and asserts that the cache holds the second.

Related: #82, #119, #120, #108.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions