Skip to content

Fix server crash on FUNCTION DELETE after case-only function name collision#3925

Merged
hpatro merged 1 commit into
valkey-io:unstablefrom
madolson:fix-function-case-collision-crash
Jun 8, 2026
Merged

Fix server crash on FUNCTION DELETE after case-only function name collision#3925
hpatro merged 1 commit into
valkey-io:unstablefrom
madolson:fix-function-case-collision-crash

Conversation

@madolson

@madolson madolson commented Jun 5, 2026

Copy link
Copy Markdown
Member

Problem

The per-library function dict (libraryFunctionDictType) compares function names case-sensitively, while the global function dict (functionDictType) compares them case-insensitively (dictCStrCaseHash + dictSdsKeyCaseCompare). This asymmetry lets a single library register two functions whose names differ only in case, leaving the two dicts inconsistent and crashing the server on the next unlink.

Reproducer (any client with the default SCRIPTING ACL category)

FUNCTION LOAD "#!lua name=poc
redis.register_function('aaa', function() return 1 end)
redis.register_function('AAA', function() return 2 end)"
FUNCTION DELETE poc

The server aborts with:

=== ASSERTION FAILED ===
==> functions.c:332 'ret == DICT_OK' is not true
0   valkey-server   ... libraryUnlink + ...

Mechanism

  1. FUNCTION LOADfunctionLibCreateFunction checks for duplicates in the per-library dict case-sensitively, so aaa and AAA are both stored there. libraryLink then inserts both into the global dict, but the second dictAdd silently returns DICT_ERR (return value ignored) because the global dict is case-insensitive. The load-path global-collision check iterates the per-library functions and looks each up in the global dict, but since AAA was never inserted there it finds no conflict and the load succeeds.
  2. FUNCTION DELETElibraryUnlink iterates the per-library dict (2 entries) and deletes each from the global dict. The first delete (aaa) succeeds; the second (AAA) resolves to the same case-insensitive slot, already removed, so dictDelete returns DICT_ERR and serverAssert(ret == DICT_OK) at functions.c:332 aborts the server.

FUNCTION LOAD REPLACE of an affected library hits the same path, since it calls libraryUnlink on the old library internally.

Requires only the SCRIPTING ACL category, granted to users by default. No admin, cluster, or special config needed.

Fix

Make libraryFunctionDictType case-insensitive (dictCStrCaseHash + dictSdsKeyCaseCompare) to match the global dict. Names differing only in case now collide at registration, and FUNCTION LOAD is rejected cleanly with Function already exists in the library. This is consistent with the long-standing case-insensitive FCALL contract (see the existing FUNCTION - test function case insensitive test).

Testing

  • Reproduced the crash on unstable before the fix; confirmed the server stays up and FUNCTION LOAD is rejected after.
  • Added two regression tests in tests/unit/functions.tcl. Verified they fail on pre-fix code (load wrongly succeeds, delete crashes the server) and pass after the fix.
  • unit/functions: 122 passed, 0 failed. unit/scripting: 683 passed, 0 failed.

…lision

The per-library function dict (libraryFunctionDictType) compared names
case-sensitively while the global function dict (functionDictType) compares
them case-insensitively. Registering two functions in one library whose
names differ only in case (e.g. "aaa" and "AAA") stored both in the
per-library dict, but the second insert into the global dict silently failed
as a duplicate.

On FUNCTION DELETE (and FUNCTION LOAD REPLACE, which unlinks the old library
internally), libraryUnlink iterates the per-library dict and deletes each
function from the global dict. The second delete resolved to the
already-removed case-insensitive slot, so dictDelete returned DICT_ERR and
serverAssert(ret == DICT_OK) aborted the server.

This is reachable by any client with the SCRIPTING ACL category, which is
granted by default, with no special privileges.

Make the per-library dict case-insensitive to match the global dict. Names
differing only in case now collide at registration and FUNCTION LOAD is
rejected cleanly with "Function already exists in the library", consistent
with the existing case-insensitive FCALL contract.

Signed-off-by: Madelyn Olson <madelyneolson@gmail.com>
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 514d11ee-dabc-4a44-9747-d1b0d4784be1

📥 Commits

Reviewing files that changed from the base of the PR and between c8c38e7 and 010adb2.

📒 Files selected for processing (2)
  • src/functions.c
  • tests/unit/functions.tcl

📝 Walkthrough

Walkthrough

The PR updates the per-library function dictionary to use case-insensitive hashing and key comparison, ensuring consistency with the global function dictionary's name collapsing. Two test cases verify the rejection of case-collision registrations and confirm server stability after deletion attempts.

Changes

Function name case-insensitivity consistency

Layer / File(s) Summary
Dictionary type configuration for case-insensitive lookups
src/functions.c
libraryFunctionDictType switches from case-sensitive (dictCStrHash, dictSdsKeyCompare) to case-insensitive (dictCStrCaseHash, dictSdsKeyCaseCompare) hash and comparison functions to prevent duplicate names differing only by case.
Test coverage for case-collision rejection and stability
tests/unit/functions.tcl
New test cases verify that registering function names differing only by case within the same library is rejected, and that library deletion after a rejected registration attempt does not crash the server.

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main fix: preventing a server crash on FUNCTION DELETE by addressing case-only function name collisions.
Description check ✅ Passed The description provides comprehensive context including the problem, reproducer, mechanism, fix, and testing approach, all directly related to the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jun 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.74%. Comparing base (c8c38e7) to head (010adb2).

Additional details and impacted files
@@             Coverage Diff              @@
##           unstable    #3925      +/-   ##
============================================
+ Coverage     76.63%   76.74%   +0.10%     
============================================
  Files           162      162              
  Lines         80735    80735              
============================================
+ Hits          61875    61958      +83     
+ Misses        18860    18777      -83     
Files with missing lines Coverage Δ
src/functions.c 96.64% <ø> (ø)

... and 23 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@hpatro hpatro left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good to me. Shall we backport it to all the existing versions?

@hpatro hpatro merged commit 2013e02 into valkey-io:unstable Jun 8, 2026
64 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.

5 participants