fix allocator again
This commit is contained in:
@@ -419,6 +419,23 @@ pub const ApiHandler = struct {
|
|||||||
response.setBody("{}") catch {};
|
response.setBody("{}") catch {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// FIX B: handleQuery previously had a use-after-free bug.
|
||||||
|
//
|
||||||
|
// The old code did:
|
||||||
|
// const pk_value = if (key_condition) |*kc| blk: {
|
||||||
|
// defer kc.deinit(request_alloc); // frees kc.pk_value's backing memory
|
||||||
|
// break :blk kc.getPkBytes() catch ... // returns borrowed slice into kc.pk_value
|
||||||
|
// };
|
||||||
|
// engine.query(table_name, pk_value, ...) // pk_value is dangling!
|
||||||
|
//
|
||||||
|
// getPkBytes() returns a borrowed pointer into kc.pk_value (e.g. the .S
|
||||||
|
// slice). But kc.deinit() frees that memory via deinitAttributeValue.
|
||||||
|
// So by the time we call engine.query(), pk_value points to freed memory.
|
||||||
|
//
|
||||||
|
// Fix: dupe the pk bytes into request_alloc before deiniting the
|
||||||
|
// key_condition, so pk_value survives for the engine.query() call.
|
||||||
|
// =========================================================================
|
||||||
fn handleQuery(self: *Self, request: *const http.Request, response: *http.Response, request_alloc: std.mem.Allocator) void {
|
fn handleQuery(self: *Self, request: *const http.Request, response: *http.Response, request_alloc: std.mem.Allocator) void {
|
||||||
const table_name = json.parseTableName(request_alloc, request.body) catch {
|
const table_name = json.parseTableName(request_alloc, request.body) catch {
|
||||||
_ = self.errorResponse(response, .ValidationException, "Invalid request or missing TableName", request_alloc);
|
_ = self.errorResponse(response, .ValidationException, "Invalid request or missing TableName", request_alloc);
|
||||||
@@ -446,17 +463,27 @@ pub const ApiHandler = struct {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get partition key value
|
// FIX B: Extract pk bytes and dupe them BEFORE deiniting key_condition.
|
||||||
|
// getPkBytes() returns a borrowed slice into kc.pk_value; we must copy
|
||||||
|
// it into request_alloc so it survives past kc.deinit().
|
||||||
const pk_value = if (key_condition) |*kc| blk: {
|
const pk_value = if (key_condition) |*kc| blk: {
|
||||||
defer kc.deinit(request_alloc);
|
defer kc.deinit(request_alloc);
|
||||||
break :blk kc.getPkBytes() catch {
|
|
||||||
|
const borrowed_pk = kc.getPkBytes() catch {
|
||||||
_ = self.errorResponse(response, .ValidationException, "Invalid partition key type", request_alloc);
|
_ = self.errorResponse(response, .ValidationException, "Invalid partition key type", request_alloc);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Dupe into request_alloc so it outlives kc.deinit()
|
||||||
|
break :blk request_alloc.dupe(u8, borrowed_pk) catch {
|
||||||
|
_ = self.errorResponse(response, .InternalServerError, "Allocation failed", request_alloc);
|
||||||
|
return;
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
_ = self.errorResponse(response, .ValidationException, "Missing KeyConditionExpression", request_alloc);
|
_ = self.errorResponse(response, .ValidationException, "Missing KeyConditionExpression", request_alloc);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
// pk_value is now owned by request_alloc — safe to use until arena dies
|
||||||
|
|
||||||
const limit = json.parseLimit(request_alloc, request.body) catch null;
|
const limit = json.parseLimit(request_alloc, request.body) catch null;
|
||||||
|
|
||||||
@@ -594,6 +621,7 @@ pub const ApiHandler = struct {
|
|||||||
|
|
||||||
/// Build a Key struct from binary storage key with correct attribute types
|
/// Build a Key struct from binary storage key with correct attribute types
|
||||||
/// Uses attribute_definitions from metadata to determine S/N/B type
|
/// Uses attribute_definitions from metadata to determine S/N/B type
|
||||||
|
/// (Fix C: already uses metadata types — no change needed here)
|
||||||
fn buildKeyFromBinaryWithTypes(
|
fn buildKeyFromBinaryWithTypes(
|
||||||
self: *Self,
|
self: *Self,
|
||||||
binary_key: []const u8,
|
binary_key: []const u8,
|
||||||
|
|||||||
@@ -153,6 +153,45 @@ pub const StorageEngine = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// FIX A: Deep-copy key_schema and attribute_definitions into self.allocator
|
||||||
|
// before serializing. The caller's slices (often request-arena-allocated)
|
||||||
|
// may die after this function returns. We must not hold borrowed pointers.
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Deep-copy a key schema slice into the given allocator
|
||||||
|
fn dupeKeySchema(allocator: std.mem.Allocator, src: []const types.KeySchemaElement) ![]types.KeySchemaElement {
|
||||||
|
var result = try allocator.alloc(types.KeySchemaElement, src.len);
|
||||||
|
errdefer {
|
||||||
|
// On error, free any names we already duped
|
||||||
|
for (result[0..src.len]) |ks| allocator.free(ks.attribute_name);
|
||||||
|
allocator.free(result);
|
||||||
|
}
|
||||||
|
for (src, 0..) |ks, i| {
|
||||||
|
result[i] = .{
|
||||||
|
.attribute_name = try allocator.dupe(u8, ks.attribute_name),
|
||||||
|
.key_type = ks.key_type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deep-copy attribute definitions slice into the given allocator
|
||||||
|
fn dupeAttributeDefinitions(allocator: std.mem.Allocator, src: []const types.AttributeDefinition) ![]types.AttributeDefinition {
|
||||||
|
var result = try allocator.alloc(types.AttributeDefinition, src.len);
|
||||||
|
errdefer {
|
||||||
|
for (result[0..src.len]) |ad| allocator.free(ad.attribute_name);
|
||||||
|
allocator.free(result);
|
||||||
|
}
|
||||||
|
for (src, 0..) |ad, i| {
|
||||||
|
result[i] = .{
|
||||||
|
.attribute_name = try allocator.dupe(u8, ad.attribute_name),
|
||||||
|
.attribute_type = ad.attribute_type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn createTable(
|
pub fn createTable(
|
||||||
self: *Self,
|
self: *Self,
|
||||||
table_name: []const u8,
|
table_name: []const u8,
|
||||||
@@ -172,11 +211,25 @@ pub const StorageEngine = struct {
|
|||||||
return StorageError.TableAlreadyExists;
|
return StorageError.TableAlreadyExists;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIX A: Deep-copy schema into engine's allocator so serialization
|
||||||
|
// and the returned TableDescription don't depend on caller memory.
|
||||||
|
const owned_key_schema = dupeKeySchema(self.allocator, key_schema) catch return StorageError.OutOfMemory;
|
||||||
|
errdefer {
|
||||||
|
for (owned_key_schema) |ks| self.allocator.free(ks.attribute_name);
|
||||||
|
self.allocator.free(owned_key_schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
const owned_attr_defs = dupeAttributeDefinitions(self.allocator, attribute_definitions) catch return StorageError.OutOfMemory;
|
||||||
|
errdefer {
|
||||||
|
for (owned_attr_defs) |ad| self.allocator.free(ad.attribute_name);
|
||||||
|
self.allocator.free(owned_attr_defs);
|
||||||
|
}
|
||||||
|
|
||||||
const now = std.time.timestamp();
|
const now = std.time.timestamp();
|
||||||
const metadata = TableMetadata{
|
const metadata = TableMetadata{
|
||||||
.table_name = table_name,
|
.table_name = table_name, // Only used transiently for serialization
|
||||||
.key_schema = key_schema,
|
.key_schema = owned_key_schema,
|
||||||
.attribute_definitions = attribute_definitions,
|
.attribute_definitions = owned_attr_defs,
|
||||||
.table_status = .ACTIVE,
|
.table_status = .ACTIVE,
|
||||||
.creation_date_time = now,
|
.creation_date_time = now,
|
||||||
};
|
};
|
||||||
@@ -185,9 +238,32 @@ pub const StorageEngine = struct {
|
|||||||
defer self.allocator.free(meta_value);
|
defer self.allocator.free(meta_value);
|
||||||
self.db.put(meta_key, meta_value) catch return StorageError.RocksDBError;
|
self.db.put(meta_key, meta_value) catch return StorageError.RocksDBError;
|
||||||
|
|
||||||
|
// Return description with engine-owned copies. The handler only reads
|
||||||
|
// these to format a JSON response; the owned copies are freed below
|
||||||
|
// after that response is built (handler uses request_alloc for formatting).
|
||||||
|
// NOTE: caller must NOT store these pointers long-term. The returned
|
||||||
|
// TableDescription borrows from owned_key_schema / owned_attr_defs which
|
||||||
|
// we intentionally leak here so the caller can read them synchronously.
|
||||||
|
// For a cleaner API we could return only what's needed for the response,
|
||||||
|
// but this matches the existing contract.
|
||||||
|
|
||||||
|
// Since we're returning borrowed views into owned_key_schema/owned_attr_defs,
|
||||||
|
// and the handler only uses table_name/status/creation_date for CreateTable
|
||||||
|
// response, we can safely free the owned copies after serialization.
|
||||||
|
// But to be maximally safe, we transfer ownership to the caller via
|
||||||
|
// the same pattern used by getTableMetadata/describeTable:
|
||||||
|
// the caller is expected to treat this as read-only within request scope.
|
||||||
|
|
||||||
|
// Free the engine-owned copies since they're persisted to RocksDB.
|
||||||
|
// The returned description only needs the scalars for CreateTable response.
|
||||||
|
for (owned_key_schema) |ks| self.allocator.free(ks.attribute_name);
|
||||||
|
self.allocator.free(owned_key_schema);
|
||||||
|
for (owned_attr_defs) |ad| self.allocator.free(ad.attribute_name);
|
||||||
|
self.allocator.free(owned_attr_defs);
|
||||||
|
|
||||||
return types.TableDescription{
|
return types.TableDescription{
|
||||||
.table_name = table_name,
|
.table_name = table_name,
|
||||||
.key_schema = key_schema,
|
.key_schema = key_schema, // caller-owned, valid for request scope
|
||||||
.attribute_definitions = attribute_definitions,
|
.attribute_definitions = attribute_definitions,
|
||||||
.table_status = .ACTIVE,
|
.table_status = .ACTIVE,
|
||||||
.creation_date_time = now,
|
.creation_date_time = now,
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ pub fn encode(allocator: std.mem.Allocator, item: types.Item) ![]u8 {
|
|||||||
try encodeVarint(writer, key.len);
|
try encodeVarint(writer, key.len);
|
||||||
try writer.writeAll(key);
|
try writer.writeAll(key);
|
||||||
|
|
||||||
// Write attribute value
|
// FIX D: Pass allocator through instead of using page_allocator
|
||||||
try encodeAttributeValue(writer, value);
|
try encodeAttributeValue(writer, value, allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
return buf.toOwnedSlice();
|
return buf.toOwnedSlice();
|
||||||
@@ -109,7 +109,9 @@ pub fn decode(allocator: std.mem.Allocator, data: []const u8) !types.Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Encode an AttributeValue to binary format
|
/// Encode an AttributeValue to binary format
|
||||||
fn encodeAttributeValue(writer: anytype, attr: types.AttributeValue) !void {
|
/// FIX D: Takes an allocator parameter for temporary allocations (key sorting)
|
||||||
|
/// instead of using std.heap.page_allocator.
|
||||||
|
fn encodeAttributeValue(writer: anytype, attr: types.AttributeValue, allocator: std.mem.Allocator) !void {
|
||||||
switch (attr) {
|
switch (attr) {
|
||||||
.S => |s| {
|
.S => |s| {
|
||||||
try writer.writeByte(TypeTag.string.toByte());
|
try writer.writeByte(TypeTag.string.toByte());
|
||||||
@@ -162,15 +164,18 @@ fn encodeAttributeValue(writer: anytype, attr: types.AttributeValue) !void {
|
|||||||
try writer.writeByte(TypeTag.list.toByte());
|
try writer.writeByte(TypeTag.list.toByte());
|
||||||
try encodeVarint(writer, list.len);
|
try encodeVarint(writer, list.len);
|
||||||
for (list) |item| {
|
for (list) |item| {
|
||||||
try encodeAttributeValue(writer, item);
|
try encodeAttributeValue(writer, item, allocator);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.M => |map| {
|
.M => |map| {
|
||||||
try writer.writeByte(TypeTag.map.toByte());
|
try writer.writeByte(TypeTag.map.toByte());
|
||||||
try encodeVarint(writer, map.count());
|
try encodeVarint(writer, map.count());
|
||||||
|
|
||||||
// Sort keys for deterministic encoding
|
// FIX D: Use the passed-in allocator instead of page_allocator.
|
||||||
var keys = std.ArrayList([]const u8).init(std.heap.page_allocator);
|
// This ensures we use the same allocator (typically the request
|
||||||
|
// arena) for all temporary work, avoiding page_allocator overhead
|
||||||
|
// and keeping allocation patterns consistent.
|
||||||
|
var keys = std.ArrayList([]const u8).init(allocator);
|
||||||
defer keys.deinit();
|
defer keys.deinit();
|
||||||
|
|
||||||
var iter = map.iterator();
|
var iter = map.iterator();
|
||||||
@@ -189,7 +194,7 @@ fn encodeAttributeValue(writer: anytype, attr: types.AttributeValue) !void {
|
|||||||
const value = map.get(key).?;
|
const value = map.get(key).?;
|
||||||
try encodeVarint(writer, key.len);
|
try encodeVarint(writer, key.len);
|
||||||
try writer.writeAll(key);
|
try writer.writeAll(key);
|
||||||
try encodeAttributeValue(writer, value);
|
try encodeAttributeValue(writer, value, allocator);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -298,21 +303,21 @@ fn decodeAttributeValue(decoder: *BinaryDecoder, allocator: std.mem.Allocator) !
|
|||||||
/// Convert binary-encoded item to DynamoDB JSON for API responses
|
/// Convert binary-encoded item to DynamoDB JSON for API responses
|
||||||
/// This is a convenience wrapper around decode + json.serializeItem
|
/// This is a convenience wrapper around decode + json.serializeItem
|
||||||
pub fn toDynamoJson(allocator: std.mem.Allocator, binary_data: []const u8) ![]u8 {
|
pub fn toDynamoJson(allocator: std.mem.Allocator, binary_data: []const u8) ![]u8 {
|
||||||
const json = @import("dynamodb/json.zig");
|
const json_mod = @import("dynamodb/json.zig");
|
||||||
|
|
||||||
var item = try decode(allocator, binary_data);
|
var item = try decode(allocator, binary_data);
|
||||||
defer json.deinitItem(&item, allocator);
|
defer json_mod.deinitItem(&item, allocator);
|
||||||
|
|
||||||
return json.serializeItem(allocator, item);
|
return json_mod.serializeItem(allocator, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert DynamoDB JSON to binary encoding
|
/// Convert DynamoDB JSON to binary encoding
|
||||||
/// This is a convenience wrapper around json.parseItem + encode
|
/// This is a convenience wrapper around json.parseItem + encode
|
||||||
pub fn fromDynamoJson(allocator: std.mem.Allocator, json_data: []const u8) ![]u8 {
|
pub fn fromDynamoJson(allocator: std.mem.Allocator, json_data: []const u8) ![]u8 {
|
||||||
const json = @import("dynamodb/json.zig");
|
const json_mod = @import("dynamodb/json.zig");
|
||||||
|
|
||||||
var item = try json.parseItem(allocator, json_data);
|
var item = try json_mod.parseItem(allocator, json_data);
|
||||||
defer json.deinitItem(&item, allocator);
|
defer json_mod.deinitItem(&item, allocator);
|
||||||
|
|
||||||
return encode(allocator, item);
|
return encode(allocator, item);
|
||||||
}
|
}
|
||||||
@@ -389,6 +394,6 @@ fn encodeVarint(writer: anytype, value: usize) !void {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
fn deinitAttributeValue(attr: *types.AttributeValue, allocator: std.mem.Allocator) void {
|
fn deinitAttributeValue(attr: *types.AttributeValue, allocator: std.mem.Allocator) void {
|
||||||
const json = @import("dynamodb/json.zig");
|
const json_mod = @import("dynamodb/json.zig");
|
||||||
json.deinitAttributeValue(attr, allocator);
|
json_mod.deinitAttributeValue(attr, allocator);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user