Details
-
Bug
-
Status: Confirmed (View Workflow)
-
Critical
-
Resolution: Unresolved
-
10.11, 11.4, 11.8, 12.3
-
None
-
Unexpected results
Description
UUID version >= 8 variant validation is wrong for exactly two values of byte 8 (accepts invalid variant 0, rejects valid variant 10)
Created bug report on behalf of Marc-Etienne Barrut. This report was also sent to the developers mailing list.
We're trying to make use of UUIDv8 in our application (well I'm in the research phase), and I tried to use an UUID typed field.
For UUIDs with version >= 8, the validation of the variant field is wrong for exactly two values of byte 8.
One valid UUID is rejected, and one invalid UUID is accepted.
Quick reproduction script:
CREATE TABLE t1 (id UUID);
|
|
|
-- variant 0, should be rejected, but is accepted:
|
INSERT INTO t1 VALUES ('00000000-0000-8000-0000-000000000000');
|
|
|
-- variant 10 (valid per RFC 9562), should be accepted, but gives
|
-- ERROR 1292 (22007): Incorrect uuid value:
|
INSERT INTO t1 VALUES ('00000000-0000-8000-8000-000000000000');
|
|
|
SELECT * FROM t1;
|
DROP TABLE t1;
|
The expected result would be that the first insert fails and the second succeeds. Currently it's the other way round.
A quick reference table of things I tried just to be sure. Version nibble 8, varying byte 8:
byte 8 variant bits expected actual
|
0x00 00 reject ACCEPTED <- wrong
|
0x01 00 reject rejected
|
0x7f 01 reject rejected
|
0x80 10 accept REJECTED <- wrong
|
0x81 10 accept accepted
|
0xbf 10 accept accepted
|
0xff 11 accept accepted
|
If needed, I'm happy to put together a patch and an MTR test for mysql-test/main/type_uuid.test. Just let me know which branch it should be based on.
Full disclosure: From this part on, the analysis was done by AI (Claude Opus 5 if it's relevant).
Source checked: plugin/type_uuid/sql_type_uuid.h on branches 11.4, 11.8 and main, the expression below is byte-identical in all three.
if (m_buffer[6] & -m_buffer[8] & 0x80) |
goto err; // impossible combination: version >= 8, variant = 0 |
The first clause is correct: m_buffer[6] & 0x80 is set exactly when the version nibble is >= 8.
The second clause, -m_buffer[8] & 0x80, is meant to test that the top bit of byte 8 is clear, i.e. variant = 0.
That holds for byte 8 in 0x01..0x7f: ~x has bit 7 set, and the +1 of two's complement cannot carry into bit 7, because the low seven bits of ~x are never all ones in that range.
It fails at the two fixed points of negation:
byte 8 = 0x00 – -0 is 0, bit 7 clear, so the check does not fire. But 0x00 has variant bits 00, which is variant 0 and should be rejected.
byte 8 = 0x80 – -0x80 is 0xFFFFFF80 after integer promotion, bit 7 set, so the check fires. But 0x80 has variant bits 10, the RFC 9562 variant, and is perfectly valid.
The two cases are mirror images: the only byte that should be rejected is the one negation can't flag, and the byte that gets wrongly rejected is the one that negates to itself.
IMPACT
Only version >= 8 reaches this check, so v1 through v7 are unaffected, which is probably why it hasn't surfaced.
For a UUIDv8 layout with random bits in the low six bits of byte 8 – the natural layout, and what RFC 9562 leaves to the implementer – byte 8 is uniform over 0x80..0xbf, so roughly one insert in 64 (1.6%) is rejected at random.
That's frequent enough to hit production and rare enough to survive review and integration tests. It's how I ran into it.
SUGGESTED FIX
The parse expression should be:
- if (m_buffer[6] & -m_buffer[8] & 0x80)
|
+ if ((m_buffer[6] & 0x80) && !(m_buffer[8] & 0x80))
|
goto err; // impossible combination: version >= 8, variant = 0 |
but I don't think this can be applied on its own.
The same x & -y & 0x80 idiom appears in rec_need_swap() in the same header:
// s[6] & 0x80 && s[8] > 0: this means a swapped uuid
|
static bool rec_need_swap(const char *s) |
{ return s[6] & -s[8] & 0x80; } |
Lining the two up for version nibble 8:
byte 8 parse now parse fixed mem_need_swap rec_need_swap
|
0x00 accept reject 0 0
|
0x01 reject reject 0 1
|
0x7f reject reject 0 1
|
0x80 reject ACCEPT 0 1
|
0x81 accept accept 0 0
|
0xbf accept accept 0 0
|
0xff accept accept 0 0
|
mem_need_swap is 0 for every v8 value, which is correct, v8 is never byte-swapped on write.
But rec_need_swap returns 1 for byte 8 in 0x01..0x80, so a record written unswapped would be unswapped again on read.
That asymmetry is currently unreachable: every v8 value where rec_need_swap fires is rejected at parse time, and 0x00, the one invalid value that slips through, happens to be where the idiom returns 0.
Fixing the parse check alone makes 0x80 insertable, and 0x80 is exactly the value that trips rec_need_swap.
That would trade a wrongly rejected insert for a silently corrupted read, which seems worse. So the two probably need to move together.
I haven't derived the correct rec_need_swap, that needs the swapped-record byte layout and I'd be guessing.
Note also that this part is static reading of the source, not something I could test, since 0x80 can't currently be inserted.
Attachments
Issue Links
- relates to
-
MDEV-29959 UUID Sorting
-
- Closed
-
-
MDEV-32112 Random incorrect uuid errors since 10.11.5
-
- Closed
-