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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
run: |
make -C build/ coverage
declare -a EXCLUDE=("\*test\*" "\*CMakeCCompilerId\*" "\*mocks\*" "\*source\*")
echo ${EXCLUDE[@]} | xargs lcov --rc lcov_branch_coverage=1 -r build/coverage.info -o build/coverage.info
echo ${EXCLUDE[@]} | xargs lcov --rc lcov_branch_coverage=1 --ignore-errors unused -r build/coverage.info -o build/coverage.info
lcov --rc lcov_branch_coverage=1 --list build/coverage.info
- name: Check Coverage
uses: FreeRTOS/CI-CD-Github-Actions/coverage-cop@main
Expand All @@ -44,7 +44,7 @@ jobs:
uses: FreeRTOS/CI-CD-Github-Actions/complexity@main
with:
path: ./
horrid_threshold: 12
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be frank here - I did not want to spend extra time reducing the complexity :)

horrid_threshold: 14

doxygen:
runs-on: ubuntu-latest
Expand All @@ -66,7 +66,7 @@ jobs:
path: ./

formatting:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Check formatting
Expand Down
66 changes: 37 additions & 29 deletions loop_invariants.patch
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
diff --git a/source/core_json.c b/source/core_json.c
index d8694f0..761c44b 100644
index a01c393..ad48f28 100644
--- a/source/core_json.c
+++ b/source/core_json.c
@@ -63,6 +63,21 @@ typedef union
#define isCurlyOpen_( x ) ( ( x ) == '{' )
#define isCurlyClose_( x ) ( ( x ) == '}' )

+/**
+ * Renaming all loop-contract clauses from CBMC for readability.
+ * For more information about loop contracts in CBMC, see
Expand All @@ -26,7 +26,7 @@ index d8694f0..761c44b 100644
*
@@ -79,6 +94,9 @@ static void skipSpace( const char * buf,
coreJSON_ASSERT( ( buf != NULL ) && ( start != NULL ) && ( max > 0U ) );

for( i = *start; i < max; i++ )
+ assigns( i )
+ loopInvariant( *start <= i && i <= max )
Expand All @@ -36,7 +36,7 @@ index d8694f0..761c44b 100644
{
@@ -103,6 +121,13 @@ static size_t countHighBits( uint8_t c )
size_t i = 0;

while( ( n & 0x80U ) != 0U )
+ assigns( i, n )
+ loopInvariant (
Expand All @@ -61,9 +61,9 @@ index d8694f0..761c44b 100644
+ decreases( j )
{
i++;

@@ -346,6 +378,12 @@ static bool skipOneHexEscape( const char * buf,
if( ( end < max ) && ( buf[ i ] == '\\' ) && ( buf[ i + 1U ] == 'u' ) )
@@ -343,6 +375,12 @@ static bool skipOneHexEscape( const char * buf,
if( ( end > i ) && ( end < max ) && ( buf[ i ] == '\\' ) && ( buf[ i + 1U ] == 'u' ) )
{
for( i += 2U; i < end; i++ )
+ assigns( value, i )
Expand All @@ -74,50 +74,50 @@ index d8694f0..761c44b 100644
+ decreases( end - i )
{
uint8_t n = hexToInt( buf[ i ] );

@@ -523,6 +561,9 @@ static bool skipString( const char * buf,
@@ -505,6 +543,9 @@ static bool skipString( const char * buf,
i++;

while( i < max )
+ assigns( i )
+ loopInvariant( *start + 1U <= i && i <= max )
+ decreases( max - i )
{
if( buf[ i ] == '"' )
{
@@ -581,6 +622,9 @@ static bool strnEq( const char * a,
@@ -563,6 +604,9 @@ static bool strnEq( const char * a,
coreJSON_ASSERT( ( a != NULL ) && ( b != NULL ) );

for( i = 0; i < n; i++ )
+ assigns( i )
+ loopInvariant( i <= n )
+ decreases( n - i )
{
if( a[ i ] != b[ i ] )
{
@@ -696,6 +740,9 @@ static bool skipDigits( const char * buf,
@@ -678,6 +722,9 @@ static bool skipDigits( const char * buf,
saveStart = *start;

for( i = *start; i < max; i++ )
+ assigns( value, i )
+ loopInvariant( *start <= i && i <= max )
+ decreases( max - i )
{
if( !isdigit_( buf[ i ] ) )
{
@@ -945,6 +992,9 @@ static void skipArrayScalars( const char * buf,
@@ -928,6 +975,9 @@ static bool skipArrayScalars( const char * buf,
i = *start;

while( i < max )
+ assigns( i )
+ loopInvariant( *start <= i && i <= max )
+ decreases( max - i )
{
if( skipAnyScalar( buf, &i, max ) != true )
{
@@ -991,6 +1041,13 @@ static bool skipObjectScalars( const char * buf,
@@ -982,6 +1032,13 @@ static bool skipObjectScalars( const char * buf,
i = *start;

while( i < max )
+ assigns( i, *start, comma )
+ loopInvariant(
Expand All @@ -129,11 +129,11 @@ index d8694f0..761c44b 100644
{
if( skipString( buf, &i, max ) != true )
{
@@ -1118,6 +1175,14 @@ static JSONStatus_t skipCollection( const char * buf,
@@ -1109,6 +1166,14 @@ static JSONStatus_t skipCollection( const char * buf,
i = *start;

while( i < max )
+ assigns( i, depth, c, __CPROVER_object_whole( stack ), ret )
+ assigns( i, depth, c, ret, __CPROVER_object_whole( stack ) )
+ loopInvariant(
+ -1 <= depth && depth <= JSON_MAX_DEPTH
+ && *start <= i && i <= max
Expand All @@ -144,27 +144,35 @@ index d8694f0..761c44b 100644
{
c = buf[ i ];
i++;
@@ -1407,6 +1472,9 @@ static bool objectSearch( const char * buf,
@@ -1144,6 +1209,7 @@ static JSONStatus_t skipCollection( const char * buf,

if( skipSpaceAndComma( buf, &i, max ) == true )
{
+ __CPROVER_assume( isOpenBracket_(stack[depth]));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have verified by code inspection that this assumption should hold for the loop - I would ask that reviewers verify this as well - see comment about redundant isOpenBracket_ check.

if( skipScalars( buf, &i, max, stack[ depth ] ) != true )
{
ret = JSONIllegalDocument;
@@ -1406,6 +1472,9 @@ static bool objectSearch( const char * buf,
skipSpace( buf, &i, max );

while( i < max )
+ assigns( i, key, keyLength, value, valueLength )
+ loopInvariant( __CPROVER_loop_entry( i ) <= i && i <= max )
+ decreases( max - i )
{
if( nextKeyValuePair( buf, &i, max, &key, &keyLength,
&value, &valueLength ) != true )
@@ -1474,6 +1542,9 @@ static bool arraySearch( const char * buf,
@@ -1473,6 +1542,9 @@ static bool arraySearch( const char * buf,
skipSpace( buf, &i, max );

while( i < max )
+ assigns( i, currentIndex, value, valueLength )
+ loopInvariant( __CPROVER_loop_entry( i ) <= i && i <= max && currentIndex < i )
+ decreases( max - i )
{
if( nextValue( buf, &i, max, &value, &valueLength ) != true )
{
@@ -1539,6 +1610,9 @@ static bool skipQueryPart( const char * buf,
@@ -1538,6 +1610,9 @@ static bool skipQueryPart( const char * buf,
while( ( i < max ) &&
!isSeparator_( buf[ i ] ) &&
!isSquareOpen_( buf[ i ] ) )
Expand All @@ -174,9 +182,9 @@ index d8694f0..761c44b 100644
{
i++;
}
@@ -1585,6 +1659,17 @@ static JSONStatus_t multiSearch( const char * buf,
@@ -1584,6 +1659,17 @@ static JSONStatus_t multiSearch( const char * buf,
coreJSON_ASSERT( ( max > 0U ) && ( queryLength > 0U ) );

while( i < queryLength )
+ assigns( i, start, queryStart, value, length )
+ loopInvariant(
Expand All @@ -191,4 +199,4 @@ index d8694f0..761c44b 100644
+ decreases( queryLength - i )
{
bool found = false;

51 changes: 25 additions & 26 deletions source/core_json.c
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,6 @@ static uint8_t hexToInt( char c )
*
* @return true if a valid escape sequence was present;
* false otherwise.
*
* @note For the sake of security, \u0000 is disallowed.
*/
static bool skipOneHexEscape( const char * buf,
size_t * start,
Expand All @@ -333,17 +331,16 @@ static bool skipOneHexEscape( const char * buf,
size_t i = 0U, end = 0U;
uint16_t value = 0U;

coreJSON_ASSERT( ( buf != NULL ) && ( start != NULL ) && ( max > 0U ) );
coreJSON_ASSERT( outValue != NULL );
coreJSON_ASSERT( ( buf != NULL ) && ( start != NULL ) && ( max > 0U ) && ( outValue != NULL ) );

i = *start;
#define HEX_ESCAPE_LENGTH ( 6U ) /* e.g., \u1234 */
/* MISRA Ref 14.3.1 [Configuration dependent invariant] */
/* More details at: https://github.com/FreeRTOS/coreJSON/blob/main/MISRA.md#rule-143 */
/* coverity[misra_c_2012_rule_14_3_violation] */
end = ( i <= ( SIZE_MAX - HEX_ESCAPE_LENGTH ) ) ? ( i + HEX_ESCAPE_LENGTH ) : SIZE_MAX;
end = i + HEX_ESCAPE_LENGTH;
Copy link
Member Author

@jasonpcarroll jasonpcarroll Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This previous logic relied on value remaning 0 to determine whether or not a hex escape was skipped. because the value 0 is now allowed, I have updated this logic to always add HEX_ESCAPE_LENGTH to i as the end and added an overflow check.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that way the i == end logic holds for determining if a hex escape value was parsed.


if( ( end < max ) && ( buf[ i ] == '\\' ) && ( buf[ i + 1U ] == 'u' ) )
if( ( end > i ) && ( end < max ) && ( buf[ i ] == '\\' ) && ( buf[ i + 1U ] == 'u' ) )
{
for( i += 2U; i < end; i++ )
{
Expand All @@ -358,7 +355,7 @@ static bool skipOneHexEscape( const char * buf,
}
}

if( ( i == end ) && ( value > 0U ) )
if( i == end )
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allow value of 0 (\u0000) per JSON spec.

{
ret = true;
*outValue = value;
Expand All @@ -382,8 +379,6 @@ static bool skipOneHexEscape( const char * buf,
*
* @return true if a valid escape sequence was present;
* false otherwise.
*
* @note For the sake of security, \u0000 is disallowed.
*/
#define isHighSurrogate( x ) ( ( ( x ) >= 0xD800U ) && ( ( x ) <= 0xDBFFU ) )
#define isLowSurrogate( x ) ( ( ( x ) >= 0xDC00U ) && ( ( x ) <= 0xDFFFU ) )
Expand Down Expand Up @@ -437,8 +432,6 @@ static bool skipHexEscape( const char * buf,
*
* @return true if a valid escape sequence was present;
* false otherwise.
*
* @note For the sake of security, \NUL is disallowed.
*/
static bool skipEscape( const char * buf,
size_t * start,
Expand All @@ -457,9 +450,6 @@ static bool skipEscape( const char * buf,

switch( c )
{
case '\0':
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will break from the default case - no need for this.

break;

case 'u':
ret = skipHexEscape( buf, &i, max );
break;
Expand All @@ -477,14 +467,6 @@ static bool skipEscape( const char * buf,
break;

default:

/* a control character: (NUL,SPACE) */
Copy link
Member Author

@jasonpcarroll jasonpcarroll Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is invalid... escaped literal control characters should fail validation per spec, thus I have removed it.

if( iscntrl_( c ) )
{
i += 2U;
ret = true;
}

break;
}
}
Expand Down Expand Up @@ -934,11 +916,12 @@ static bool skipSpaceAndComma( const char * buf,
*
* @note Stops advance if a value is an object or array.
*/
static void skipArrayScalars( const char * buf,
static bool skipArrayScalars( const char * buf,
size_t * start,
size_t max )
{
size_t i = 0U;
bool ret = true;

coreJSON_ASSERT( ( buf != NULL ) && ( start != NULL ) && ( max > 0U ) );

Expand All @@ -953,11 +936,19 @@ static void skipArrayScalars( const char * buf,

if( skipSpaceAndComma( buf, &i, max ) != true )
{
/* After parsing a scalar, we must either have a comma (followed by more content)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes cases such as [3[4]] from being parsed as valid.

* or be at a closing bracket. If neither, the array is malformed. */
if( ( i >= max ) || !isSquareClose_( buf[ i ] ) )
{
ret = false;
}

break;
}
}

*start = i;
return ret;
}

/**
Expand Down Expand Up @@ -1071,7 +1062,7 @@ static bool skipScalars( const char * buf,
{
if( !isSquareClose_( buf[ i ] ) )
{
skipArrayScalars( buf, start, max );
ret = skipArrayScalars( buf, start, max );
}
}
else
Expand Down Expand Up @@ -1151,14 +1142,22 @@ static JSONStatus_t skipCollection( const char * buf,
{
depth--;

if( ( skipSpaceAndComma( buf, &i, max ) == true ) &&
isOpenBracket_( stack[ depth ] ) )
Copy link
Member Author

@jasonpcarroll jasonpcarroll Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have removed this isOpenBracket_ check as there is no point... stack[ depth ] will always contain an open bracket when depth >= 0.

if( skipSpaceAndComma( buf, &i, max ) == true )
{
if( skipScalars( buf, &i, max, stack[ depth ] ) != true )
{
ret = JSONIllegalDocument;
}
}
else
{
/* After closing a nested collection, if there is no comma found when calling
* skipSpaceAndComma, then we must be at the end of the parent collection. */
if( ( i < max ) && !isMatchingBracket_( stack[ depth ], buf[ i ] ) )
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes cases where missing commas between collections was considered valid. (e.g. [ { "key1" : "value1"} {"key2":"value2"}])

{
ret = JSONIllegalDocument;
}
}

break;
}
Expand Down
1 change: 1 addition & 0 deletions test/cbmc/proofs/JSON_Validate/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ USE_EXTERNAL_SAT_SOLVER ?= --external-sat-solver cadical

USE_FUNCTION_CONTRACTS += skipAnyScalar
USE_FUNCTION_CONTRACTS += skipCollection
USE_FUNCTION_CONTRACTS += skipSpace

include ../Makefile-json.common
Loading
Loading