Skip to content

Fix remaining buffer size in vendored storage commands - #13

Merged
evaccaro merged 3 commits into
githubfrom
fix-storage-command-buffer-size
Oct 19, 2026
Merged

evaccaro merged 3 commits into
githubfrom
fix-storage-command-buffer-size

Conversation

@evaccaro

@evaccaro evaccaro commented Oct 19, 2026 •

Copy link
Copy Markdown

Summary

Update the two snprintf calls in the vendored libmemcached storage-command path to pass MEMCACHED_DEFAULT_COMMAND_SIZE - write_length.

Both calls write through buffer_ptr after it has advanced past the command and key. The size argument must therefore describe the remaining capacity rather than the entire buffer.

Add a length guard before the first memcpy to reject command, prefix, key, and separating-space combinations that fill or exceed the command buffer. Add three regression tests covering oversized prefix/key combinations, the exact buffer-capacity boundary, and delete with noreply.

Why

Builds using _FORTIFY_SOURCE=3 and LTO can abort during ordinary storage operations because the original size argument exceeds the space remaining at buffer_ptr. Passing the remaining capacity makes the formatting compatible with these compiler checks while preserving the existing command format and final length checks.

The pre-copy guard ensures that the command and key data fit before being copied and that the remaining-capacity subtraction cannot wrap around. Prefix and key lengths are otherwise validated independently, which does not guarantee their combination fits the command buffer.

This change covers both the delete-with-noreply path and the other non-CAS storage operations. The CAS path already formats from the beginning of the buffer and is unchanged.

Validation

  • Patch formatting checked with git diff --check.
  • The Ruby 3.4 Ubuntu CI job builds the native extension successfully. All three new regression tests pass; the suite reports 159 tests, 6,390 assertions, zero failures, and one error in the unchanged MemcachedExperimentalTest#test_touch test (Memcached::NotFound), matching the earlier PR runs.
  • The Ruby-head job still fails during dependency installation: locked minitest 5.26.0 requires Ruby below 4.0, while head is 4.1.0.dev.
  • The Ruby 3.4 CI build uses _FORTIFY_SOURCE=2; it does not exercise the exact level-3-plus-LTO configuration described above.
  • The local test attempt was blocked by the vendored configure scripts not recognizing ARM; no build-system changes are included.

Pass the remaining command buffer capacity to snprintf after advancing the buffer pointer. This keeps storage command formatting compatible with stricter compiler checks.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 605c1c5d-4b41-4ee4-8db3-a38d2ab61a30
Copilot AI max review requested due to automatic review settings October 19, 2026 17:57

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review any files in this pull request. Check if the Files changed in this pull request are included in default exclusions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 605c1c5d-4b41-4ee4-8db3-a38d2ab61a30
@baweaver

Copy link
Copy Markdown

The two changed snprintf calls are correct. The function also needs a length guard before its memcpy calls, or the new subtraction can wrap around.

Both calls write at buffer_ptr, which is already write_length bytes into the 350-byte buffer. That makes MEMCACHED_DEFAULT_COMMAND_SIZE - write_length the space that remains. The later write_length >= MEMCACHED_DEFAULT_COMMAND_SIZE check still catches truncation, because snprintf returns the length it needed. The compare-and-swap path formats from the start of buffer and is correct as written.

The command, prefix and key are copied into buffer before anything checks their combined length:

Part Largest allowed size
Command ("prepend ", "replace ") 8 bytes
Prefix (memcached_callback.c) 127 bytes
Key (memcached_validate_key_length) 250 bytes
Separating space 1 byte
Total 386 bytes

A 386-byte header overflows the 350-byte stack buffer inside the memcpy calls, before either snprintf runs. That overflow predates this pull request. With this change, any write_length above 350 makes MEMCACHED_DEFAULT_COMMAND_SIZE - write_length wrap around as a size_t. snprintf then gets a size far larger than the buffer. The multi-get path in memcached_get.c validates the prefix and key lengths together, while this path validates the key alone.

A guard ahead of the first memcpy closes both problems:

if (strlen(command) + strlen(ptr->prefix_key) + key_length + 1 >= MEMCACHED_DEFAULT_COMMAND_SIZE)
  return MEMCACHED_BAD_KEY_PROVIDED;

A test that sets a 250-character key under a 127-character prefix covers it. The overflow figures come from the limits in the source. No build has reproduced the crash yet.

ptr->prefix_key ? strlen(ptr->prefix_key) : 0 always takes the strlen branch, because prefix_key is a fixed array. Dropping the conditional is optional cleanup.

Reject command, prefix, and key combinations that leave no room in the command buffer before copying them. Cover oversized inputs, the exact capacity boundary, and delete with noreply.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 605c1c5d-4b41-4ee4-8db3-a38d2ab61a30
@evaccaro
evaccaro merged commit 9753e4b into github Oct 19, 2026
7 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants