Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
12bae29afc fix(valkey): replace unreachable with explicit panics in SubscriptionCtx
Replaces `unreachable` statements in SubscriptionCtx with explicit
`@panic` calls that include debug warnings. This ensures that if
these code paths are reached (e.g., due to calling methods on
RedisClient.prototype), the error is handled deterministically
rather than causing undefined behavior.

The crash (ENG-24418) was reported by the fuzzer when calling
`Bun.RedisClient.prototype.subscribe()` followed by GC. While
the C++ code correctly guards against invalid `this` values,
replacing `unreachable` with `@panic` provides defense-in-depth
and clearer error messages if these paths are ever reached.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 10:40:33 +00:00
2 changed files with 68 additions and 5 deletions

View File

@@ -9,7 +9,12 @@ pub const SubscriptionCtx = struct {
pub fn init(valkey_parent: *JSValkeyClient) bun.JSError!Self {
const callback_map = jsc.JSMap.create(valkey_parent.globalObject);
const parent_this = valkey_parent.this_value.tryGet() orelse unreachable;
const parent_this = valkey_parent.this_value.tryGet() orelse {
// this_value should always be set by the time init is called from the constructor.
// If we get here, something is seriously wrong with how the client was created.
bun.Output.debugWarn("SubscriptionCtx.init called with uninitialized this_value", .{});
@panic("SubscriptionCtx.init called with uninitialized this_value");
};
ParentJS.gc.set(.subscriptionCallbackMap, parent_this, valkey_parent.globalObject, callback_map);
@@ -26,10 +31,22 @@ pub const SubscriptionCtx = struct {
}
fn subscriptionCallbackMap(this: *Self) *jsc.JSMap {
const parent_this = this.parent().this_value.tryGet() orelse unreachable;
const parent_this = this.parent().this_value.tryGet() orelse {
// this_value should always be set when subscriptionCallbackMap is called.
// If we get here, it means the client was not properly initialized.
bun.Output.debugWarn("subscriptionCallbackMap called with uninitialized this_value", .{});
@panic("subscriptionCallbackMap called with uninitialized this_value");
};
const value_js = ParentJS.gc.get(.subscriptionCallbackMap, parent_this).?;
return jsc.JSMap.fromJS(value_js).?;
const value_js = ParentJS.gc.get(.subscriptionCallbackMap, parent_this) orelse {
// subscriptionCallbackMap should always exist if we have a valid this_value.
bun.Output.debugWarn("subscriptionCallbackMap not found in gc values", .{});
@panic("subscriptionCallbackMap not found in gc values");
};
return jsc.JSMap.fromJS(value_js) orelse {
bun.Output.debugWarn("subscriptionCallbackMap is not a JSMap", .{});
@panic("subscriptionCallbackMap is not a JSMap");
};
}
/// Get the total number of channels that this subscription context is subscribed to.
@@ -139,7 +156,12 @@ pub const SubscriptionCtx = struct {
} else if (existing_handler_arr.isArray()) {
// Use the existing array
handlers_array = existing_handler_arr;
} else unreachable;
} else {
// The subscription callback map should only contain arrays.
// If we get here, the map was corrupted somehow.
bun.Output.debugWarn("subscriptionCallbackMap contained non-array value", .{});
@panic("subscriptionCallbackMap contained non-array value");
}
} else {
// No existing_handler_arr exists, create a new array
handlers_array = try jsc.JSArray.createEmpty(globalObject, 0);

View File

@@ -0,0 +1,41 @@
import { expect, test } from "bun:test";
// Regression test for ENG-24418
// Calling methods on RedisClient.prototype should throw an error
// but not crash during GC
test("calling RedisClient.prototype.subscribe should throw without crashing", () => {
const prototype = Bun.RedisClient.prototype;
// Should throw an error about invalid this value
expect(() => {
prototype.subscribe();
}).toThrow("Expected this to be instanceof RedisClient");
// Force GC to ensure no crash occurs due to improperly initialized objects
Bun.gc(true);
Bun.gc(true);
});
test("calling RedisClient.prototype methods should throw appropriate errors", () => {
const prototype = Bun.RedisClient.prototype;
// Test various methods on the prototype
expect(() => prototype.get("key")).toThrow("Expected this to be instanceof RedisClient");
expect(() => prototype.set("key", "value")).toThrow("Expected this to be instanceof RedisClient");
expect(() => prototype.subscribe("channel", () => {})).toThrow("Expected this to be instanceof RedisClient");
expect(() => prototype.unsubscribe("channel")).toThrow("Expected this to be instanceof RedisClient");
// Force GC after each to ensure no crash
Bun.gc(true);
});
test("RedisClient.prototype properties should throw or return undefined without crashing", () => {
const prototype = Bun.RedisClient.prototype;
// Properties that have custom getters will throw
expect(() => prototype.connected).toThrow();
expect(() => prototype.bufferedAmount).toThrow();
// Force GC to ensure no crash
Bun.gc(true);
});