From 0034da4293ae344fcfb96dc0a22e049a11c10636 Mon Sep 17 00:00:00 2001 From: "A.Shakhmatov" Date: Sun, 9 Aug 2026 15:00:06 +0300 Subject: [PATCH] wire: parse a logical session id out of a command body An accessor over the command envelope, next to `db_name` and the same shape: called from dispatch, never from `parse`, because a malformed session id is a command that gets an error reply, not a connection that gets torn down. It returns the 16 bytes or names what is wrong; the codes stay in the command layer, where they were measured. The tolerated fields were measured against mongod 8.3.7 rather than recalled, and the measurement contradicted the assumption this was designed on. The design said unknown fields inside `lsid` would be tolerated, on the reasoning that the server tolerates unknown fields everywhere and pinpoint strictness would be inconsistent. mongod answers IDLUnknownField (40415) -- it is strict here and the reasoning was simply wrong. It also accepts `uid`, the hash of the credentials owning the session, which a driver starts sending the moment authentication is on; rejecting that would have broken every command in M7, and the test says so where a future reader will meet it. `txnNumber` and `txnUUID` inside `lsid` are refused. They are not the retryable-write `txnNumber` that sits outside it: together they name an *internal* session, one that runs a transaction on another session's behalf. mongod refuses them on a standalone too. Also `bson.Value.type_name`, which is mongod's name for a type rather than Zig's -- a TypeMismatch message quotes it, and a driver that matches on the text is matching on these. 170/170 unit tests. --- src/bson.zig | 29 +++++++++ src/wire.zig | 163 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) diff --git a/src/bson.zig b/src/bson.zig index 5dfa35e..7532e4e 100644 --- a/src/bson.zig +++ b/src/bson.zig @@ -74,6 +74,35 @@ pub const Value = union(enum) { }; } + /// The name mongod uses for this type in a TypeMismatch message ("is the + /// wrong type 'int', expected type 'object'"). Its own names, not Zig's: + /// a driver that matches on the text is matching on these. + pub fn type_name(self: Value) []const u8 { + return switch (self) { + .double => "double", + .string => "string", + .doc => "object", + .array => "array", + .binary => "binData", + .object_id => "objectId", + .bool => "bool", + .datetime => "date", + .null => "null", + .regex => "regex", + .code => "javascript", + .symbol => "symbol", + .int32 => "int", + .timestamp => "timestamp", + .int64 => "long", + .decimal128 => "decimal", + .min_key => "minKey", + .max_key => "maxKey", + // An unparsed value keeps only its tag byte, and the tags this + // union does not name are the ones nothing here inspects. + .opaque_val => "unknown", + }; + } + pub fn is_number(self: Value) bool { return switch (self) { .double, .int32, .int64 => true, diff --git a/src/wire.zig b/src/wire.zig index df6cc9d..e87559a 100644 --- a/src/wire.zig +++ b/src/wire.zig @@ -185,6 +185,81 @@ pub const Message = struct { }; } + /// A logical session id: a UUID, so 16 bytes of binary subtype 4. + pub const SessionId = [16]u8; + + /// Every way an `lsid` can be malformed, named after what is wrong rather + /// than after the error the caller will send: the codes belong to the + /// command layer, which is where they were measured. + pub const LsidError = error{ + LsidNotDocument, + LsidUnknownField, + LsidIdMissing, + LsidIdNotBinary, + LsidIdNotUuid, + LsidIdWrongLength, + LsidInternalSession, + LsidTxnNumberWithoutTxnUuid, + }; + + /// The logical session id, or null when the command carries no `lsid`. + /// + /// An accessor over the command envelope, like `db_name`: called from + /// dispatch, never from `parse`, because a malformed session id is a + /// command that gets an error reply and not a connection that gets torn + /// down. + /// + /// The fields it tolerates were measured against mongod 8.3.7, not + /// recalled, and the measurement contradicted the assumption it was + /// written on. mongod does *not* ignore unknown fields inside `lsid` -- + /// it answers IDLUnknownField -- and it does accept `uid`, the hash of + /// the credentials that own the session, which a driver sends as soon as + /// authentication is on. Both matter to us: the first because tolerating + /// what the server rejects is the kind of divergence that only shows up + /// under a driver nobody tested, the second because M7 would otherwise + /// break every command. + pub fn lsid(self: *const Message) LsidError!?SessionId { + const v = bson.get_pair(self.body.pairs, "lsid") orelse return null; + const doc = switch (v) { + .doc => |d| d, + else => return error.LsidNotDocument, + }; + + var id: ?bson.Binary = null; + var has_txn_number = false; + var has_txn_uuid = false; + for (doc) |pair| { + if (std.mem.eql(u8, pair.key, "id")) { + id = switch (pair.value) { + .binary => |b| b, + else => return error.LsidIdNotBinary, + }; + } else if (std.mem.eql(u8, pair.key, "uid")) { + // Accepted and ignored: it identifies the user a session + // belongs to, and this server has exactly one. + } else if (std.mem.eql(u8, pair.key, "txnNumber")) { + has_txn_number = true; + } else if (std.mem.eql(u8, pair.key, "txnUUID")) { + has_txn_uuid = true; + } else { + return error.LsidUnknownField; + } + } + + // A `txnNumber` inside the session id is not the retryable-write one + // outside it: together with `txnUUID` the two name an *internal* + // session, which only exists to run a transaction on another + // session's behalf. Neither can mean anything here, and mongod + // refuses them on a standalone too. + if (has_txn_number and !has_txn_uuid) return error.LsidTxnNumberWithoutTxnUuid; + if (has_txn_uuid) return error.LsidInternalSession; + + const bin = id orelse return error.LsidIdMissing; + if (bin.subtype != 4) return error.LsidIdNotUuid; + if (bin.data.len != 16) return error.LsidIdWrongLength; + return bin.data[0..16].*; + } + /// Documents of a batch argument (`documents`, `updates`, `deletes`). /// Drivers send them either as an OP_MSG document sequence or as an array /// inside the command body; callers should not have to care which. The @@ -378,6 +453,94 @@ test "reply serializes to a parseable message" { try testing.expectEqual(@as(usize, 0), msg.seqs.len); } +/// An OP_MSG carrying one body section, for tests that care about the command +/// envelope rather than about framing. +fn fake_op_msg(gpa: std.mem.Allocator, pairs: []const bson.Pair) !Message { + var doc: std.ArrayListUnmanaged(u8) = .empty; + defer doc.deinit(gpa); + try bson.write_doc(pairs, gpa, &doc); + + var buf: std.ArrayListUnmanaged(u8) = .empty; + defer buf.deinit(gpa); + try buf.appendSlice(gpa, &[_]u8{ 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0xDD, 0x07, 0, 0, 0, 0, 0, 0 }); + try buf.append(gpa, 0x00); + try buf.appendSlice(gpa, doc.items); + std.mem.writeInt(u32, buf.items[0..4], @intCast(buf.items.len), .little); + return Message.parse(gpa, buf.items); +} + +test "a session id is read out of a command" { + const uuid = [_]u8{0xAB} ** 16; + var msg = try fake_op_msg(testing.allocator, &.{ + .{ .key = "ping", .value = .{ .int32 = 1 } }, + .{ .key = "lsid", .value = .{ .doc = &.{ + .{ .key = "id", .value = .{ .binary = .{ .subtype = 4, .data = &uuid } } }, + } } }, + }); + defer msg.deinit(); + try testing.expectEqual(uuid, (try msg.lsid()).?); +} + +test "a command with no lsid has no session" { + var msg = try fake_op_msg(testing.allocator, &.{.{ .key = "ping", .value = .{ .int32 = 1 } }}); + defer msg.deinit(); + try testing.expect((try msg.lsid()) == null); +} + +test "a session id carrying a user hash is still a session id" { + // `uid` arrives as soon as authentication is on (M7). Rejecting it as an + // unknown field would break every command the moment that lands, which is + // exactly the kind of divergence a measurement against a real server is + // for -- mongod 8.3.7 answers ok:1 to this. + const uuid = [_]u8{0x11} ** 16; + var msg = try fake_op_msg(testing.allocator, &.{ + .{ .key = "ping", .value = .{ .int32 = 1 } }, + .{ .key = "lsid", .value = .{ .doc = &.{ + .{ .key = "id", .value = .{ .binary = .{ .subtype = 4, .data = &uuid } } }, + .{ .key = "uid", .value = .{ .binary = .{ .subtype = 0, .data = &[_]u8{0} ** 32 } } }, + } } }, + }); + defer msg.deinit(); + try testing.expectEqual(uuid, (try msg.lsid()).?); +} + +test "every malformed session id is named" { + const uuid = [_]u8{0x22} ** 16; + const cases = [_]struct { want: anyerror, lsid: bson.Value }{ + .{ .want = error.LsidNotDocument, .lsid = .{ .int32 = 5 } }, + .{ .want = error.LsidIdMissing, .lsid = .{ .doc = &.{} } }, + .{ .want = error.LsidIdNotBinary, .lsid = .{ .doc = &.{ + .{ .key = "id", .value = .{ .string = "nope" } }, + } } }, + .{ .want = error.LsidIdNotUuid, .lsid = .{ .doc = &.{ + .{ .key = "id", .value = .{ .binary = .{ .subtype = 0, .data = &uuid } } }, + } } }, + .{ .want = error.LsidIdWrongLength, .lsid = .{ .doc = &.{ + .{ .key = "id", .value = .{ .binary = .{ .subtype = 4, .data = uuid[0..15] } } }, + } } }, + .{ .want = error.LsidUnknownField, .lsid = .{ .doc = &.{ + .{ .key = "id", .value = .{ .binary = .{ .subtype = 4, .data = &uuid } } }, + .{ .key = "bogus", .value = .{ .int32 = 1 } }, + } } }, + .{ .want = error.LsidTxnNumberWithoutTxnUuid, .lsid = .{ .doc = &.{ + .{ .key = "id", .value = .{ .binary = .{ .subtype = 4, .data = &uuid } } }, + .{ .key = "txnNumber", .value = .{ .int64 = 1 } }, + } } }, + .{ .want = error.LsidInternalSession, .lsid = .{ .doc = &.{ + .{ .key = "id", .value = .{ .binary = .{ .subtype = 4, .data = &uuid } } }, + .{ .key = "txnUUID", .value = .{ .binary = .{ .subtype = 4, .data = &uuid } } }, + } } }, + }; + for (cases) |c| { + var msg = try fake_op_msg(testing.allocator, &.{ + .{ .key = "ping", .value = .{ .int32 = 1 } }, + .{ .key = "lsid", .value = c.lsid }, + }); + defer msg.deinit(); + try testing.expectError(c.want, msg.lsid()); + } +} + test "reject non-OP_MSG non-OP_QUERY opcodes" { var buf: [20]u8 = undefined; std.mem.writeInt(u32, buf[0..4], 20, .little);