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.
This commit is contained in:
A.Shakhmatov
2026-08-09 15:00:06 +03:00
parent e65da2740e
commit 0034da4293
2 changed files with 192 additions and 0 deletions

View File

@@ -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 { pub fn is_number(self: Value) bool {
return switch (self) { return switch (self) {
.double, .int32, .int64 => true, .double, .int32, .int64 => true,

View File

@@ -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`). /// Documents of a batch argument (`documents`, `updates`, `deletes`).
/// Drivers send them either as an OP_MSG document sequence or as an array /// 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 /// 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); 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" { test "reject non-OP_MSG non-OP_QUERY opcodes" {
var buf: [20]u8 = undefined; var buf: [20]u8 = undefined;
std.mem.writeInt(u32, buf[0..4], 20, .little); std.mem.writeInt(u32, buf[0..4], 20, .little);