commands: endSessions judges the array it is handed

It stays a no-op -- there is nothing to end -- but stops being a blind `ok`.
This is the one session command that arrives in normal operation: a driver
sends it on close for every session it handed out. Validating an argument that
is then discarded looks like ceremony, and is not: `ok: 1` to a malformed
`endSessions` says the server understood something it never read, which is the
same class of answer the previous commit removed for `txnNumber`.

Codes and messages measured against mongod 8.3.7, including the field path
`endSessions.endSessionsFromClient` -- an IDL artefact, since the command's own
field is `endSessions` and the parsed argument carries a different name. It is
reproduced rather than tidied: a path that differs from the real server's is
worse than an odd one.

With this the raw probe's replies are identical to mongod's for every lsid and
endSessions shape. What is left is five deliberate divergences, all recorded:
three because mongod knows per command whether `txnNumber` is even accepted and
answers Location50889 or 263 before reaching the standalone refusal, and two
because `startSession` and `refreshSessions` are not implemented -- a driver
calls neither, generating session ids locally, so CommandNotFound is the honest
answer. `commitTransaction` joins them, and it can only be reached by a client
whose write this server already refused.

175/175 unit tests.
This commit is contained in:
A.Shakhmatov
2026-08-09 15:08:54 +03:00
parent 54092f44be
commit f5c827e581

View File

@@ -559,10 +559,108 @@ fn reject_bad_session_fields(msg: *wire.Message, reply: *wire.Reply, cmd: []cons
return reject_txn_fields(msg, reply, cmd); return reject_txn_fields(msg, reply, cmd);
} }
fn cmd_end_sessions(_: *Context, _: *wire.Message, reply: *wire.Reply) !void { /// Still a no-op -- there is nothing to end -- but no longer a blind `ok`.
///
/// A driver sends this on close for every session it handed out, so it is the
/// one session command that arrives in normal operation. Validating an array
/// we then discard looks like ceremony; it is not. `ok: 1` to a malformed
/// `endSessions` is the same class of answer as `ok: 1` to a transactional
/// write: the client is told the server understood, and it did not. mongod's
/// field path is `endSessions.endSessionsFromClient`, an IDL artefact -- the
/// command's own field is `endSessions` and the parsed argument has a
/// different name -- and it is reproduced rather than tidied, because a name
/// that differs from the real server's is worse than an odd one.
fn cmd_end_sessions(_: *Context, msg: *wire.Message, reply: *wire.Reply) !void {
const arena = reply.arena_alloc();
const sessions = switch (msg.body.get("endSessions").?) {
.array => |a| a,
else => |v| {
const text = try std.fmt.allocPrint(
arena,
"BSON field 'endSessions.endSessions' is the wrong type '{s}', expected type 'array'",
.{v.type_name()},
);
return reply.put_error(@intFromEnum(ErrorCode.type_mismatch), "TypeMismatch", text);
},
};
for (sessions, 0..) |entry, i| {
if (try reject_bad_session_entry(entry, i, reply)) return;
}
try reply.put_ok(); try reply.put_ok();
} }
/// One element of `endSessions`. Answers whether an error reply was written.
fn reject_bad_session_entry(entry: bson.Value, i: usize, reply: *wire.Reply) !bool {
const arena = reply.arena_alloc();
const prefix = "BSON field 'endSessions.endSessionsFromClient";
const doc = switch (entry) {
.doc => |d| d,
else => {
const text = try std.fmt.allocPrint(
arena,
"{s}.{d}' is the wrong type '{s}', expected type 'object'",
.{ prefix, i, entry.type_name() },
);
try reply.put_error(@intFromEnum(ErrorCode.type_mismatch), "TypeMismatch", text);
return true;
},
};
var seen_id = false;
for (doc) |pair| {
if (std.mem.eql(u8, pair.key, "uid")) continue;
if (!std.mem.eql(u8, pair.key, "id")) {
const text = try std.fmt.allocPrint(
arena,
"{s}.{s}' is an unknown field.",
.{ prefix, pair.key },
);
try reply.put_error(@intFromEnum(ErrorCode.idl_unknown_field), "IDLUnknownField", text);
return true;
}
seen_id = true;
const bin = switch (pair.value) {
.binary => |b| b,
else => {
const text = try std.fmt.allocPrint(
arena,
"{s}.id' is the wrong type '{s}', expected type 'binData'",
.{ prefix, pair.value.type_name() },
);
try reply.put_error(@intFromEnum(ErrorCode.type_mismatch), "TypeMismatch", text);
return true;
},
};
if (bin.subtype != 4) {
const text = try std.fmt.allocPrint(
arena,
"{s}.id' is the wrong binData type '{s}', expected type 'UUID'",
.{ prefix, subtype_name(bin.subtype) },
);
try reply.put_error(@intFromEnum(ErrorCode.type_mismatch), "TypeMismatch", text);
return true;
}
if (bin.data.len != 16) {
try reply.put_error(
@intFromEnum(ErrorCode.invalid_uuid),
"InvalidUUID",
"uuid must be a 16-byte binary field with UUID (4) subtype",
);
return true;
}
}
if (!seen_id) {
try reply.put_error(
@intFromEnum(ErrorCode.idl_failed_to_parse),
"IDLFailedToParse",
"BSON field 'endSessions.endSessionsFromClient.id' is missing but a required field",
);
return true;
}
return false;
}
fn cmd_connection_status(_: *Context, _: *wire.Message, reply: *wire.Reply) !void { fn cmd_connection_status(_: *Context, _: *wire.Message, reply: *wire.Reply) !void {
const auth_info = try reply.arena_alloc().alloc(bson.Pair, 2); const auth_info = try reply.arena_alloc().alloc(bson.Pair, 2);
auth_info[0] = .{ .key = "authenticatedUsers", .value = .{ .array = &.{} } }; auth_info[0] = .{ .key = "authenticatedUsers", .value = .{ .array = &.{} } };
@@ -3194,6 +3292,47 @@ test "a transactional write is refused rather than applied" {
try testing.expectEqual(@as(i64, 0), try doc_count(&ctx, "txn")); try testing.expectEqual(@as(i64, 0), try doc_count(&ctx, "txn"));
} }
test "endSessions judges the array it is handed" {
// Still a no-op, and that is not what is being tested. The command arrives
// in normal operation -- a driver sends it on close for every session it
// handed out -- so a blind `ok` here tells a client the server understood
// something it never looked at. Codes measured against mongod 8.3.7.
var threaded: std.Io.Threaded = .init_single_threaded;
defer threaded.deinit();
const io = threaded.io();
var tdb = try TestDb.init(io);
defer tdb.deinit();
var ctx = tdb.ctx(io);
const uuid = [_]u8{0x6B} ** 16;
const good = bson.Value{ .binary = .{ .subtype = 4, .data = &uuid } };
const one = bson.Value{ .doc = &.{.{ .key = "id", .value = good }} };
try testing.expectEqual(@as(?i32, null), try run_for_code(&ctx, "endSessions", .{ .array = &.{} }, &.{}));
try testing.expectEqual(@as(?i32, null), try run_for_code(&ctx, "endSessions", .{ .array = &.{one} }, &.{}));
// `uid` rides along once authentication is on, exactly as it does in lsid.
try testing.expectEqual(@as(?i32, null), try run_for_code(&ctx, "endSessions", .{ .array = &.{.{ .doc = &.{
.{ .key = "id", .value = good },
.{ .key = "uid", .value = .{ .binary = .{ .subtype = 0, .data = &[_]u8{0} ** 32 } } },
} }} }, &.{}));
try testing.expectEqual(@as(i32, 14), (try run_for_code(&ctx, "endSessions", .{ .string = "nope" }, &.{})).?);
try testing.expectEqual(@as(i32, 14), (try run_for_code(&ctx, "endSessions", .{ .array = &.{.{ .int32 = 5 }} }, &.{})).?);
try testing.expectEqual(@as(i32, 14), (try run_for_code(&ctx, "endSessions", .{ .array = &.{.{ .doc = &.{
.{ .key = "id", .value = .{ .string = "nope" } },
} }} }, &.{})).?);
try testing.expectEqual(@as(i32, 207), (try run_for_code(&ctx, "endSessions", .{ .array = &.{.{ .doc = &.{
.{ .key = "id", .value = .{ .binary = .{ .subtype = 4, .data = uuid[0..15] } } },
} }} }, &.{})).?);
try testing.expectEqual(@as(i32, 40414), (try run_for_code(&ctx, "endSessions", .{ .array = &.{.{ .doc = &.{} }} }, &.{})).?);
try testing.expectEqual(@as(i32, 40415), (try run_for_code(&ctx, "endSessions", .{ .array = &.{.{ .doc = &.{
.{ .key = "id", .value = good },
.{ .key = "bogus", .value = .{ .int32 = 1 } },
} }} }, &.{})).?);
// A bad entry after a good one is still a bad entry.
try testing.expectEqual(@as(i32, 14), (try run_for_code(&ctx, "endSessions", .{ .array = &.{ one, .{ .int32 = 5 } } }, &.{})).?);
}
test "an unknown command is reported before its session id is judged" { test "an unknown command is reported before its session id is judged" {
// Measured: mongod answers CommandNotFound to `{nosuchcmd: 1, lsid: 5}`, // Measured: mongod answers CommandNotFound to `{nosuchcmd: 1, lsid: 5}`,
// so the lookup comes first and this is where the check belongs. // so the lookup comes first and this is where the check belongs.