Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3525,13 +3525,15 @@ tables-wire-fuzz-negative-control: tables-wire-fuzz-length-negative-control tabl
# the string read's `room( len )`. THE ONE CONTENT RULE THE WIRE HAS
# (docs/SPEC-TABLES.md §3, §4) reads a kind `12` payload AS IT ARRIVES, over
# the whole of `L` and before the reader's own bound, so `room( len )` is what
# says those bytes are there at all: a sabotaged leg walks off the end of the
# buffer on a forged length and DIES on the mutant, where the oracle stops at
# the body and reports. A LENGTH IS A 64-BIT NUMBER (§3), so the check is
# unsigned: cast to int64 first, 0xFFFFFFFFFFFFFFFF reads as -1 and a negative
# length looks like room, which is the length this pass plants.
# says those bytes are there at all. A sabotaged leg believes a forged `L`: it
# takes the payload over bytes the mutant never carried and steps its cursor by
# a length the body never had, so the fields after it decode out of bytes that
# are not their own and the leg reports counters the oracle does not. A LENGTH
# IS A 64-BIT NUMBER (§3) and every reader of one takes it unsigned, so the
# length this pass plants is 0xFFFFFFFFFFFFFFFF, the largest the wire can spell
# and the one no buffer ever has room for.
tables-wire-fuzz-length-negative-control: build/conformance-harness
$(call wire_fuzz_control,length,internal/codegen/cpptable/codecs.go,s|if ( !r.getleb( len ) \|\| !r.room( len ) ) { r.report->malformed = true; return false; }|if ( !r.getleb( len ) ) { r.report->malformed = true; return false; } // NEGATIVE CONTROL: the fit check is gone|,the leg died on the mutant)
$(call wire_fuzz_control,length,internal/codegen/cpptable/codecs.go,s|if ( !r.getleb( len ) \|\| !r.room( len ) ) { r.report->malformed = true; return false; }|if ( !r.getleb( len ) ) { r.report->malformed = true; return false; } // NEGATIVE CONTROL: the fit check is gone|,the report differs)

# the numbering's `index - 1 >= map.count`: an index past the node table then
# reads a directory entry the region does not hold
Expand Down
28 changes: 19 additions & 9 deletions generated/bench/tables/cpp/BenchTableTable.h
Original file line number Diff line number Diff line change
Expand Up @@ -595,13 +595,17 @@ inline double TableWidenF32( uint32_t bits )
// and a code point past U+10FFFF, which is SPEC.md §4.7's rule in this wire's
// idiom: the field reads its declared default, one malformed counts, and the
// parent reads on past L.
inline bool TableUtf8Valid( const uint8_t * bytes, int64_t length )
//
// A LENGTH IS A 64-BIT NUMBER (§3), so it arrives as one: a payload length is
// whatever the wire spelled, and narrowing it to a signed count would read
// 0xFFFFFFFFFFFFFFFF as an empty payload.
inline bool TableUtf8Valid( const uint8_t * bytes, uint64_t length )
{
int64_t i = 0;
uint64_t i = 0;
while ( i < length )
{
const uint8_t lead = bytes[i];
int64_t continuations;
uint64_t continuations;
uint32_t code_point;
if ( lead == 0 ) { return false; }
if ( lead < 0x80 ) { i++; continue; }
Expand All @@ -610,7 +614,7 @@ inline bool TableUtf8Valid( const uint8_t * bytes, int64_t length )
else if ( ( lead & 0xF8 ) == 0xF0 ) { continuations = 3; code_point = lead & 0x07; }
else { return false; }
if ( i + continuations >= length ) { return false; }
for ( int64_t k = 1; k <= continuations; k++ )
for ( uint64_t k = 1; k <= continuations; k++ )
{
if ( ( bytes[i + k] & 0xC0 ) != 0x80 ) { return false; }
code_point = ( code_point << 6 ) | uint32_t( bytes[i + k] & 0x3F );
Expand All @@ -626,9 +630,15 @@ inline bool TableUtf8Valid( const uint8_t * bytes, int64_t length )
// A CLAMP CUTS AT A CODE POINT BOUNDARY (§3, §16.2): the last whole code point
// that fits within the bound, over a payload the check above already accepted,
// so a clamp can never invent ill-formed storage.
inline int64_t TableUtf8Clamp( const uint8_t * bytes, int64_t length, int64_t bound )
{
if ( length <= bound ) { return length; }
//
// THE ANSWER IS NEVER ABOVE THE BOUND. The length arrives as the wire's own
// 64-bit number and the caller turns the answer back into the size of a copy,
// so a length no reader could have bounded has to leave here bounded: taken as
// a signed count, 0xFFFFFFFFFFFFFFFF is -1, -1 is under every bound, and the
// copy would run at SIZE_MAX.
inline int64_t TableUtf8Clamp( const uint8_t * bytes, uint64_t length, int64_t bound )
{
if ( length <= (uint64_t) bound ) { return (int64_t) length; }
int64_t cut = bound;
while ( cut > 0 && ( bytes[cut] & 0xC0 ) == 0x80 ) { cut--; }
return cut;
Expand Down Expand Up @@ -5183,9 +5193,9 @@ BENCHTABLE_TABLE_INLINE bool TableMixedLoadBody( TableReader & r, TableMixed & v
if ( !r.getleb( len ) || !r.room( len ) ) { r.report->malformed = true; return false; }
// ILL-FORMED TEXT IS DAMAGE (§3, §4): the field reads its declared
// default, one malformed counts, and the parent reads on past L
if ( !TableUtf8Valid( r.buffer + r.offset, (int64_t) len ) ) { r.report->malformed = true; value.player_name[0] = 0; value.player_name_length = 0; r.offset += (int64_t) len; break; }
if ( !TableUtf8Valid( r.buffer + r.offset, len ) ) { r.report->malformed = true; value.player_name[0] = 0; value.player_name_length = 0; r.offset += (int64_t) len; break; }
uint64_t keep = len;
if ( keep > 15 ) { keep = (uint64_t) TableUtf8Clamp( r.buffer + r.offset, (int64_t) len, 15 ); r.report->clamped++; } // at a code point boundary (§3)
if ( keep > 15 ) { keep = (uint64_t) TableUtf8Clamp( r.buffer + r.offset, len, 15 ); r.report->clamped++; } // at a code point boundary (§3)
memcpy( value.player_name, r.buffer + r.offset, (size_t) keep );
value.player_name[keep] = 0;
value.player_name_length = (int32_t) keep;
Expand Down
4 changes: 2 additions & 2 deletions internal/codegen/cpptable/codecs.go
Original file line number Diff line number Diff line change
Expand Up @@ -1691,9 +1691,9 @@ func (g *tableGen) emitTableReadField(f *ir.Field, kind int) {
g.pf("%suint64_t len = 0;\n", ind)
g.pf("%sif ( !r.getleb( len ) || !r.room( len ) ) { r.report->malformed = true; return false; }\n", ind)
g.pf("%s// ILL-FORMED TEXT IS DAMAGE (§3, §4): the field reads its declared\n%s// default, one malformed counts, and the parent reads on past L\n", ind, ind)
g.pf("%sif ( !TableUtf8Valid( r.buffer + r.offset, (int64_t) len ) ) { r.report->malformed = true; value.%s[0] = 0; value.%s_length = 0; r.offset += (int64_t) len; break; }\n", ind, f.Name, f.Name)
g.pf("%sif ( !TableUtf8Valid( r.buffer + r.offset, len ) ) { r.report->malformed = true; value.%s[0] = 0; value.%s_length = 0; r.offset += (int64_t) len; break; }\n", ind, f.Name, f.Name)
g.pf("%suint64_t keep = len;\n", ind)
g.pf("%sif ( keep > %d ) { keep = (uint64_t) TableUtf8Clamp( r.buffer + r.offset, (int64_t) len, %d ); r.report->clamped++; } // at a code point boundary (§3)\n", ind, f.Type.Size, f.Type.Size)
g.pf("%sif ( keep > %d ) { keep = (uint64_t) TableUtf8Clamp( r.buffer + r.offset, len, %d ); r.report->clamped++; } // at a code point boundary (§3)\n", ind, f.Type.Size, f.Type.Size)
g.pf("%smemcpy( value.%s, r.buffer + r.offset, (size_t) keep );\n", ind, f.Name)
g.pf("%svalue.%s[keep] = 0;\n", ind, f.Name)
g.pf("%svalue.%s_length = (int32_t) keep;\n", ind, f.Name)
Expand Down
2 changes: 1 addition & 1 deletion internal/codegen/cpptable/maps.go
Original file line number Diff line number Diff line change
Expand Up @@ -1033,7 +1033,7 @@ func (g *tableGen) emitMapKeyReader(f *ir.Field) {
g.pf(" uint64_t key_len = 0;\n")
g.pf(" if ( !r.getleb( key_len ) || !r.room( key_len ) ) { out.malformed = true; return out; }\n")
g.pf(" // a key a string value would refuse as malformed makes the MAP malformed (§2.8, §3)\n")
g.pf(" if ( !TableUtf8Valid( r.buffer + r.offset, (int64_t) key_len ) ) { out.malformed = true; return out; }\n")
g.pf(" if ( !TableUtf8Valid( r.buffer + r.offset, key_len ) ) { out.malformed = true; return out; }\n")
g.pf(" out.key = (const char *) ( r.buffer + r.offset );\n")
g.pf(" out.length = (int32_t) key_len;\n")
g.pf(" out.over = key_len > %d; // KEYS NEVER CLAMP: the entry is dropped whole\n", key.Type.Size)
Expand Down
22 changes: 16 additions & 6 deletions internal/codegen/cpptable/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@ const tableTextRuntime = `
// and a code point past U+10FFFF, which is SPEC.md §4.7's rule in this wire's
// idiom: the field reads its declared default, one malformed counts, and the
// parent reads on past L.
inline bool TableUtf8Valid( const uint8_t * bytes, int64_t length )
//
// A LENGTH IS A 64-BIT NUMBER (§3), so it arrives as one: a payload length is
// whatever the wire spelled, and narrowing it to a signed count would read
// 0xFFFFFFFFFFFFFFFF as an empty payload.
inline bool TableUtf8Valid( const uint8_t * bytes, uint64_t length )
{
int64_t i = 0;
uint64_t i = 0;
while ( i < length )
{
const uint8_t lead = bytes[i];
int64_t continuations;
uint64_t continuations;
uint32_t code_point;
if ( lead == 0 ) { return false; }
if ( lead < 0x80 ) { i++; continue; }
Expand All @@ -31,7 +35,7 @@ inline bool TableUtf8Valid( const uint8_t * bytes, int64_t length )
else if ( ( lead & 0xF8 ) == 0xF0 ) { continuations = 3; code_point = lead & 0x07; }
else { return false; }
if ( i + continuations >= length ) { return false; }
for ( int64_t k = 1; k <= continuations; k++ )
for ( uint64_t k = 1; k <= continuations; k++ )
{
if ( ( bytes[i + k] & 0xC0 ) != 0x80 ) { return false; }
code_point = ( code_point << 6 ) | uint32_t( bytes[i + k] & 0x3F );
Expand All @@ -47,9 +51,15 @@ inline bool TableUtf8Valid( const uint8_t * bytes, int64_t length )
// A CLAMP CUTS AT A CODE POINT BOUNDARY (§3, §16.2): the last whole code point
// that fits within the bound, over a payload the check above already accepted,
// so a clamp can never invent ill-formed storage.
inline int64_t TableUtf8Clamp( const uint8_t * bytes, int64_t length, int64_t bound )
//
// THE ANSWER IS NEVER ABOVE THE BOUND. The length arrives as the wire's own
// 64-bit number and the caller turns the answer back into the size of a copy,
// so a length no reader could have bounded has to leave here bounded: taken as
// a signed count, 0xFFFFFFFFFFFFFFFF is -1, -1 is under every bound, and the
// copy would run at SIZE_MAX.
inline int64_t TableUtf8Clamp( const uint8_t * bytes, uint64_t length, int64_t bound )
{
if ( length <= bound ) { return length; }
if ( length <= (uint64_t) bound ) { return (int64_t) length; }
int64_t cut = bound;
while ( cut > 0 && ( bytes[cut] & 0xC0 ) == 0x80 ) { cut--; }
return cut;
Expand Down
26 changes: 26 additions & 0 deletions test/tables/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,31 @@ static void test_clamping()
CHECK( report4.clamped == 1 && out4.name_length == 32 && out4.name[32] == 0 );
}

// A LENGTH IS A 64-BIT NUMBER (docs/SPEC-TABLES.md §3), and the clamp takes it
// as one: the caller turns the answer straight back into the size of the
// field's copy, so the answer has to be within the bound for EVERY length the
// wire can spell, including the ones only a broken reader would still be
// holding. Read as a signed count, 0xFFFFFFFFFFFFFFFF is -1, -1 is under every
// bound, and the copy runs at SIZE_MAX.

static void test_text_clamp_is_bounded()
{
const uint8_t text[] = "golden-v1";

// a payload within the bound is kept whole, and one over it cuts at a code
// point boundary — "é" is two bytes, so a bound of 9 cuts back to 8
CHECK( tabledemo::TableUtf8Clamp( text, 9, 32 ) == 9 );
const uint8_t two_byte[] = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 0xC3, 0xA9 };
CHECK( tabledemo::TableUtf8Clamp( two_byte, 10, 9 ) == 8 );

// the field read's own three lines, over the length the wire fuzzer's
// length pass plants: the size that reaches memcpy is within the bound
const uint64_t forged = 0xFFFFFFFFFFFFFFFFull;
uint64_t keep = forged;
if ( keep > 32 ) { keep = (uint64_t) tabledemo::TableUtf8Clamp( text, forged, 32 ); }
CHECK( keep <= 32 );
}

// ---- malformed framing: decode stops, partial result kept, flag raised ----

static void test_malformed()
Expand Down Expand Up @@ -9047,6 +9072,7 @@ int main()
test_flags_are_positional();
test_wide_extents();
test_clamping();
test_text_clamp_is_bounded();
test_malformed();
test_reflection();
test_cross_file();
Expand Down
24 changes: 17 additions & 7 deletions testdata/golden/tables/arms/CarryTable.h
Original file line number Diff line number Diff line change
Expand Up @@ -653,13 +653,17 @@ inline double TableWidenF32( uint32_t bits )
// and a code point past U+10FFFF, which is SPEC.md §4.7's rule in this wire's
// idiom: the field reads its declared default, one malformed counts, and the
// parent reads on past L.
inline bool TableUtf8Valid( const uint8_t * bytes, int64_t length )
//
// A LENGTH IS A 64-BIT NUMBER (§3), so it arrives as one: a payload length is
// whatever the wire spelled, and narrowing it to a signed count would read
// 0xFFFFFFFFFFFFFFFF as an empty payload.
inline bool TableUtf8Valid( const uint8_t * bytes, uint64_t length )
{
int64_t i = 0;
uint64_t i = 0;
while ( i < length )
{
const uint8_t lead = bytes[i];
int64_t continuations;
uint64_t continuations;
uint32_t code_point;
if ( lead == 0 ) { return false; }
if ( lead < 0x80 ) { i++; continue; }
Expand All @@ -668,7 +672,7 @@ inline bool TableUtf8Valid( const uint8_t * bytes, int64_t length )
else if ( ( lead & 0xF8 ) == 0xF0 ) { continuations = 3; code_point = lead & 0x07; }
else { return false; }
if ( i + continuations >= length ) { return false; }
for ( int64_t k = 1; k <= continuations; k++ )
for ( uint64_t k = 1; k <= continuations; k++ )
{
if ( ( bytes[i + k] & 0xC0 ) != 0x80 ) { return false; }
code_point = ( code_point << 6 ) | uint32_t( bytes[i + k] & 0x3F );
Expand All @@ -684,9 +688,15 @@ inline bool TableUtf8Valid( const uint8_t * bytes, int64_t length )
// A CLAMP CUTS AT A CODE POINT BOUNDARY (§3, §16.2): the last whole code point
// that fits within the bound, over a payload the check above already accepted,
// so a clamp can never invent ill-formed storage.
inline int64_t TableUtf8Clamp( const uint8_t * bytes, int64_t length, int64_t bound )
{
if ( length <= bound ) { return length; }
//
// THE ANSWER IS NEVER ABOVE THE BOUND. The length arrives as the wire's own
// 64-bit number and the caller turns the answer back into the size of a copy,
// so a length no reader could have bounded has to leave here bounded: taken as
// a signed count, 0xFFFFFFFFFFFFFFFF is -1, -1 is under every bound, and the
// copy would run at SIZE_MAX.
inline int64_t TableUtf8Clamp( const uint8_t * bytes, uint64_t length, int64_t bound )
{
if ( length <= (uint64_t) bound ) { return (int64_t) length; }
int64_t cut = bound;
while ( cut > 0 && ( bytes[cut] & 0xC0 ) == 0x80 ) { cut--; }
return cut;
Expand Down
24 changes: 17 additions & 7 deletions testdata/golden/tables/arms/GateTable.h
Original file line number Diff line number Diff line change
Expand Up @@ -653,13 +653,17 @@ inline double TableWidenF32( uint32_t bits )
// and a code point past U+10FFFF, which is SPEC.md §4.7's rule in this wire's
// idiom: the field reads its declared default, one malformed counts, and the
// parent reads on past L.
inline bool TableUtf8Valid( const uint8_t * bytes, int64_t length )
//
// A LENGTH IS A 64-BIT NUMBER (§3), so it arrives as one: a payload length is
// whatever the wire spelled, and narrowing it to a signed count would read
// 0xFFFFFFFFFFFFFFFF as an empty payload.
inline bool TableUtf8Valid( const uint8_t * bytes, uint64_t length )
{
int64_t i = 0;
uint64_t i = 0;
while ( i < length )
{
const uint8_t lead = bytes[i];
int64_t continuations;
uint64_t continuations;
uint32_t code_point;
if ( lead == 0 ) { return false; }
if ( lead < 0x80 ) { i++; continue; }
Expand All @@ -668,7 +672,7 @@ inline bool TableUtf8Valid( const uint8_t * bytes, int64_t length )
else if ( ( lead & 0xF8 ) == 0xF0 ) { continuations = 3; code_point = lead & 0x07; }
else { return false; }
if ( i + continuations >= length ) { return false; }
for ( int64_t k = 1; k <= continuations; k++ )
for ( uint64_t k = 1; k <= continuations; k++ )
{
if ( ( bytes[i + k] & 0xC0 ) != 0x80 ) { return false; }
code_point = ( code_point << 6 ) | uint32_t( bytes[i + k] & 0x3F );
Expand All @@ -684,9 +688,15 @@ inline bool TableUtf8Valid( const uint8_t * bytes, int64_t length )
// A CLAMP CUTS AT A CODE POINT BOUNDARY (§3, §16.2): the last whole code point
// that fits within the bound, over a payload the check above already accepted,
// so a clamp can never invent ill-formed storage.
inline int64_t TableUtf8Clamp( const uint8_t * bytes, int64_t length, int64_t bound )
{
if ( length <= bound ) { return length; }
//
// THE ANSWER IS NEVER ABOVE THE BOUND. The length arrives as the wire's own
// 64-bit number and the caller turns the answer back into the size of a copy,
// so a length no reader could have bounded has to leave here bounded: taken as
// a signed count, 0xFFFFFFFFFFFFFFFF is -1, -1 is under every bound, and the
// copy would run at SIZE_MAX.
inline int64_t TableUtf8Clamp( const uint8_t * bytes, uint64_t length, int64_t bound )
{
if ( length <= (uint64_t) bound ) { return (int64_t) length; }
int64_t cut = bound;
while ( cut > 0 && ( bytes[cut] & 0xC0 ) == 0x80 ) { cut--; }
return cut;
Expand Down
24 changes: 17 additions & 7 deletions testdata/golden/tables/arms/NestTable.h
Original file line number Diff line number Diff line change
Expand Up @@ -654,13 +654,17 @@ inline double TableWidenF32( uint32_t bits )
// and a code point past U+10FFFF, which is SPEC.md §4.7's rule in this wire's
// idiom: the field reads its declared default, one malformed counts, and the
// parent reads on past L.
inline bool TableUtf8Valid( const uint8_t * bytes, int64_t length )
//
// A LENGTH IS A 64-BIT NUMBER (§3), so it arrives as one: a payload length is
// whatever the wire spelled, and narrowing it to a signed count would read
// 0xFFFFFFFFFFFFFFFF as an empty payload.
inline bool TableUtf8Valid( const uint8_t * bytes, uint64_t length )
{
int64_t i = 0;
uint64_t i = 0;
while ( i < length )
{
const uint8_t lead = bytes[i];
int64_t continuations;
uint64_t continuations;
uint32_t code_point;
if ( lead == 0 ) { return false; }
if ( lead < 0x80 ) { i++; continue; }
Expand All @@ -669,7 +673,7 @@ inline bool TableUtf8Valid( const uint8_t * bytes, int64_t length )
else if ( ( lead & 0xF8 ) == 0xF0 ) { continuations = 3; code_point = lead & 0x07; }
else { return false; }
if ( i + continuations >= length ) { return false; }
for ( int64_t k = 1; k <= continuations; k++ )
for ( uint64_t k = 1; k <= continuations; k++ )
{
if ( ( bytes[i + k] & 0xC0 ) != 0x80 ) { return false; }
code_point = ( code_point << 6 ) | uint32_t( bytes[i + k] & 0x3F );
Expand All @@ -685,9 +689,15 @@ inline bool TableUtf8Valid( const uint8_t * bytes, int64_t length )
// A CLAMP CUTS AT A CODE POINT BOUNDARY (§3, §16.2): the last whole code point
// that fits within the bound, over a payload the check above already accepted,
// so a clamp can never invent ill-formed storage.
inline int64_t TableUtf8Clamp( const uint8_t * bytes, int64_t length, int64_t bound )
{
if ( length <= bound ) { return length; }
//
// THE ANSWER IS NEVER ABOVE THE BOUND. The length arrives as the wire's own
// 64-bit number and the caller turns the answer back into the size of a copy,
// so a length no reader could have bounded has to leave here bounded: taken as
// a signed count, 0xFFFFFFFFFFFFFFFF is -1, -1 is under every bound, and the
// copy would run at SIZE_MAX.
inline int64_t TableUtf8Clamp( const uint8_t * bytes, uint64_t length, int64_t bound )
{
if ( length <= (uint64_t) bound ) { return (int64_t) length; }
int64_t cut = bound;
while ( cut > 0 && ( bytes[cut] & 0xC0 ) == 0x80 ) { cut--; }
return cut;
Expand Down
Loading
Loading