CVE-2024-27454 (Other, published 2024-02-26T16:28Z)
Description: value: orjson.loads in orjson before 3.9.15 does not limit recursion for deeply nested JSON documents.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/ijl/orjson
Files changed:
README.md
include/yyjson-recursion-limit.patch
include/yyjson/yyjson.c
include/yyjson/yyjson.h
script/vendor-yyjson
test/test_api.py
Commit message: yyjson 0eca326, recursion limit


Vulnerable code:


 * This is a simple memory allocator that uses linked list memory chunk.
 * The following code will be executed only when the library user creates
 * this allocator manually.
/** chunk header */
    usize size; /* chunk memory size (include chunk header) */
    struct pool_chunk *next;
/** ctx header */
    usize size; /* total memory size (include ctx header) */
    pool_chunk *free_list;
    if (unlikely(size == 0 || size >= ctx->size)) return NULL;
    size = size_align_up(size, sizeof(pool_chunk)) + sizeof(pool_chunk);
    usize free_size;
    void *new_ptr;
    
    if (unlikely(size == 0 || size >= ctx->size)) return NULL;
    size = size_align_up(size, sizeof(pool_chunk)) + sizeof(pool_chunk);
    /* reduce size */
    if (unlikely(size <= cur->size)) {
        free_size = cur->size - size;
        if (free_size >= sizeof(pool_chunk) * 2) {
            tmp = (pool_chunk *)(void *)((u8 *)cur + cur->size - free_size);
            tmp->size = free_size;
            pool_free(ctx_ptr, (void *)(tmp + 1));
            cur->size -= free_size;
        }
        return ptr;
    }
    /* merge to higher chunk if they are contiguous */
    if ((u8 *)cur + cur->size == (u8 *)next &&
        cur->size + next->size >= size) {
        free_size = cur->size + next->size - size;
    
    /* fallback to malloc and memcpy */
    new_ptr = pool_malloc(ctx_ptr, size - sizeof(pool_chunk));
    if (new_ptr) {
        memcpy(new_ptr, ptr, cur->size - sizeof(pool_chunk));
        pool_free(ctx_ptr, ptr);
    }
    return new_ptr;
    
    
    if (lt == rt)
        return luni->u64 == runi->u64;
    if (lt == YYJSON_SUBTYPE_SINT && rt == YYJSON_SUBTYPE_UINT)
    if (lt == YYJSON_SUBTYPE_UINT && rt == YYJSON_SUBTYPE_SINT)
                    if (!rhs || !unsafe_yyjson_equals(lhs + 1, rhs))
                        return false;
                    if (!rhs || !unsafe_yyjson_mut_equals(lhs->next, rhs))
                        return false;
#if !YYJSON_DISABLE_READER

    
    
            incr_len(str_len + 1);
            incr_len(str_len * 6 + 4);
            incr_len(32);
            incr_len(4);
            incr_len(4);
#define YYJSON_VERSION_MINOR  7
#define YYJSON_VERSION_HEX    0x000700
#define YYJSON_VERSION_STRING "0.7.0"
 This is not a general-purpose allocator. If used to read multiple JSON 
 documents and only some of them are released, it may cause memory
 fragmentation, leading to performance degradation and memory waste. Therefore, 
 it is recommended to use this allocator only for reading or writing a single 
 JSON document.
static const yyjson_read_flag YYJSON_READ_NOFLAG                = 0 << 0;
static const yyjson_write_flag YYJSON_WRITE_NOFLAG                  = 0 << 0;
    @warning The key string are not copied, you should keep the string
    @warning The key string are not copied, you should keep the string
    @warning The key string are not copied, you should keep the string
    @warning The key string are not copied, you should keep the string
    @warning The key string are not copied, you should keep the string
    @warning The key string are not copied, you should keep the string
    @warning The key string are not copied, you should keep the string
    @warning The key string are not copied, you should keep the string
    @warning The key/value string are not copied, you should keep these strings
    @warning The key/value string are not copied, you should keep these strings
    @warning The key string are not copied, you should keep the string
    @warning The key/value string are not copied, you should keep these strings
    @warning The key string are not copied, you should keep the string
 Set provided `value` if the JSON Pointer (RFC 6901) exists and is type uint.
 Returns true if value at `ptr` exists and is the correct type, otherwise false.
    if (value && yyjson_is_uint(val)) {
        *value = unsafe_yyjson_get_uint(val);
        return true;
    } else {
        return false;
 Set provided `value` if the JSON Pointer (RFC 6901) exists and is type sint.
 Returns true if value at `ptr` exists and is the correct type, otherwise false.
    if (value && yyjson_is_sint(val)) {
        *value = unsafe_yyjson_get_sint(val);
        return true;
    } else {
        return false;
yyjson_version="5e3b26d2659287d31e2f8e10f95f95feb7e5ab3a"
    def test_loads_recursion(self):
        loads() recursion limit

Fixed code:
It raises `JSONDecodeError` if a combination of array or object recurses
1024 levels deep.

diff --git a/include/yyjson/yyjson.c b/include/yyjson/yyjson.c
index e76f538..4bac033 100644
--- a/include/yyjson/yyjson.c
@@ -329,8 +329,9 @@ uint32_t yyjson_version(void) {
 #ifndef YYJSON_DISABLE_UTF8_VALIDATION
 #define YYJSON_DISABLE_UTF8_VALIDATION 0
 #endif
-
-
+#ifndef YYJSON_READER_CONTAINER_RECURSION_LIMIT
+#define YYJSON_READER_CONTAINER_RECURSION_LIMIT 1024
+#endif
 
 /*==============================================================================
  * Macros
@@ -5798,6 +5799,8 @@ fail_character:
     return_err(cur, UNEXPECTED_CHARACTER, "unexpected character");
 fail_garbage:
     return_err(cur, UNEXPECTED_CONTENT, "unexpected content after document");
+fail_recursion:
+    return_err(cur, RECURSION_DEPTH, "array and object recursion depth exceeded");
     
 #undef return_err
 }
@@ -5854,7 +5857,8 @@ static_inline yyjson_doc *read_root_minify(u8 *hdr,
     yyjson_val *ctn_parent; /* parent of current container */
     yyjson_doc *doc; /* the JSON document, equals to val_hdr */
     const char *msg; /* error message */
-    
+
+    u32 container_depth = 0; /* limit on number of open array and map */
     bool raw; /* read number as raw */
     bool inv; /* allow invalid unicode */
     u8 *raw_end; /* raw end for null-terminator */
@@ -5889,6 +5893,11 @@ static_inline yyjson_doc *read_root_minify(u8 *hdr,
     }
     
 arr_begin:
+    container_depth++;
+    if (unlikely(container_depth >= YYJSON_READER_CONTAINER_RECURSION_LIMIT)) {
+        goto fail_recursion;
+    }
+
     /* save current container */
     ctn->tag = (((u64)ctn_len + 1) << YYJSON_TAG_BIT) |
                (ctn->tag & YYJSON_TAG_MASK);
@@ -5988,6 +5997,8 @@ arr_val_end:
     goto fail_character;
     
 arr_end:
+    container_depth--;
+
     /* get parent container */
     ctn_parent = (yyjson_val *)(void *)((u8 *)ctn - ctn->uni.ofs);
     
@@ -6006,6 +6017,11 @@ arr_end:
     }
     
 obj_begin:
+    container_depth++;
+    if (unlikely(container_depth >= YYJSON_READER_CONTAINER_RECURSION_LIMIT)) {
+        goto fail_recursion;
+    }
+
     /* push container */
     ctn->tag = (((u64)ctn_len + 1) << YYJSON_TAG_BIT) |
                (ctn->tag & YYJSON_TAG_MASK);
@@ -6134,6 +6150,8 @@ obj_val_end:
     goto fail_character;
     
 obj_end:
+    container_depth--;
+
     /* pop container */
     ctn_parent = (yyjson_val *)(void *)((u8 *)ctn - ctn->uni.ofs);
     /* point to the next value */
@@ -6185,6 +6203,8 @@ fail_character:
     return_err(cur, UNEXPECTED_CHARACTER, "unexpected character");
 fail_garbage:
     return_err(cur, UNEXPECTED_CONTENT, "unexpected content after document");
+fail_recursion:
+    return_err(cur, RECURSION_DEPTH, "array and object recursion depth exceeded");
     
 #undef val_incr
 #undef return_err
@@ -6242,7 +6262,8 @@ static_inline yyjson_doc *read_root_pretty(u8 *hdr,
     yyjson_val *ctn_parent; /* parent of current container */
     yyjson_doc *doc; /* the JSON document, equals to val_hdr */
     const char *msg; /* error message */
-    
+
+    u32 container_depth = 0; /* limit on number of open array and map */
     bool raw; /* read number as raw */
     bool inv; /* allow invalid unicode */
     u8 *raw_end; /* raw end for null-terminator */
@@ -6279,6 +6300,11 @@ static_inline yyjson_doc *read_root_pretty(u8 *hdr,
     }
     
 arr_begin:
+    container_depth++;
+    if (unlikely(container_depth >= YYJSON_READER_CONTAINER_RECURSION_LIMIT)) {
+        goto fail_recursion;
+    }
+
     /* save current container */
     ctn->tag = (((u64)ctn_len + 1) << YYJSON_TAG_BIT) |
                (ctn->tag & YYJSON_TAG_MASK);
@@ -6395,6 +6421,8 @@ arr_val_end:
     goto fail_character;
     
 arr_end:
+    container_depth--;
+
     /* get parent container */
     ctn_parent = (yyjson_val *)(void *)((u8 *)ctn - ctn->uni.ofs);
     
@@ -6414,6 +6442,11 @@ arr_end:
     }
     
 obj_begin:
+    container_depth++;
+    if (unlikely(container_depth >= YYJSON_READER_CONTAINER_RECURSION_LIMIT)) {
+        goto fail_recursion;
+    }
+
     /* push container */
     ctn->tag = (((u64)ctn_len + 1) << YYJSON_TAG_BIT) |
                (ctn->tag & YYJSON_TAG_MASK);
@@ -6562,6 +6595,8 @@ obj_val_end:
     goto fail_character;
     
 obj_end:
+    container_depth--;
+
     /* pop container */
     ctn_parent = (yyjson_val *)(void *)((u8 *)ctn - ctn->uni.ofs);
     /* point to the next value */
@@ -6614,6 +6649,8 @@ fail_character:
     return_err(cur, UNEXPECTED_CHARACTER, "unexpected character");
 fail_garbage:
     return_err(cur, UNEXPECTED_CONTENT, "unexpected content after document");
+fail_recursion:
+    return_err(cur, RECURSION_DEPTH, "array and object recursion depth exceeded");
     
 #undef val_incr
 #undef return_err
diff --git a/include/yyjson/yyjson.h b/include/yyjson/yyjson.h
index c393408..bc688e0 100644
--- a/include/yyjson/yyjson.h
@@ -831,6 +831,9 @@ static const yyjson_read_code YYJSON_READ_ERROR_FILE_OPEN               = 12;
 /** Failed to read a file. */
 static const yyjson_read_code YYJSON_READ_ERROR_FILE_READ               = 13;
 
+/** Document exceeded YYJSON_READER_CONTAINER_RECURSION_LIMIT.  */
+static const yyjson_read_code YYJSON_READ_ERROR_RECURSION_DEPTH         = 14;
+
 /** Error information for JSON reader. */
 typedef struct yyjson_read_err {
     /** Error code, see `yyjson_read_code` for all possible values. */
/* The minimum size of the dynamic allocator's chunk. */
#define YYJSON_ALC_DYN_MIN_SIZE             0x1000

#ifndef YYJSON_READER_CONTAINER_RECURSION_LIMIT
#define YYJSON_READER_CONTAINER_RECURSION_LIMIT 1024
#endif


/*==============================================================================
 * Null Memory Allocator
 *
 * This allocator is just a placeholder to ensure that the internal
 * malloc/realloc/free function pointers are not null.
 *============================================================================*/

 *
 * This allocator is initialized with a fixed-size buffer.
 * The buffer is split into multiple memory chunks for memory allocation.
/** memory chunk header */
    usize size; /* chunk memory size, include chunk header */
    struct pool_chunk *next; /* linked list, nullable */
    /* char mem[]; flexible array member */
/** allocator ctx header */
    usize size; /* total memory size, include ctx header */
    pool_chunk *free_list; /* linked list, nullable */
    /* pool_chunk chunks[]; flexible array member */
/** align up the input size to chunk size */
static_inline void pool_size_align(usize *size) {
    *size = size_align_up(*size, sizeof(pool_chunk)) + sizeof(pool_chunk);
}

    /* assert(size != 0) */
    if (unlikely(size >= ctx->size)) return NULL;
    pool_size_align(&size);
    /* assert(ptr != NULL) */
    /* assert(ptr != NULL && size != 0 && old_size < size) */
    /* check size */
    if (unlikely(size >= ctx->size)) return NULL;
    pool_size_align(&old_size);
    pool_size_align(&size);
    if (unlikely(old_size == size)) return ptr;
    if ((u8 *)cur + cur->size == (u8 *)next && cur->size + next->size >= size) {
        /* merge to higher chunk if they are contiguous */
        usize free_size = cur->size + next->size - size;
    } else {
        /* fallback to malloc and memcpy */
        void *new_ptr = pool_malloc(ctx_ptr, size - sizeof(pool_chunk));
        if (new_ptr) {
            memcpy(new_ptr, ptr, cur->size - sizeof(pool_chunk));
            pool_free(ctx_ptr, ptr);
        }
        return new_ptr;
/*==============================================================================
 * Dynamic Memory Allocator
 *
 * This allocator allocates memory on demand and does not immediately release
 * unused memory. Instead, it places the unused memory into a freelist for
 * potential reuse in the future. It is only when the entire allocator is
 * destroyed that all previously allocated memory is released at once.
 *============================================================================*/

/** memory chunk header */
typedef struct dyn_chunk {
    usize size; /* chunk size, include header */
    struct dyn_chunk *next;
    /* char mem[]; flexible array member */
} dyn_chunk;

/** allocator ctx header */
typedef struct {
    dyn_chunk free_list; /* dummy header, sorted from small to large */
    dyn_chunk used_list; /* dummy header */
} dyn_ctx;

/** align up the input size to chunk size */
static_inline bool dyn_size_align(usize *size) {
    usize alc_size = *size + sizeof(dyn_chunk);
    alc_size = size_align_up(alc_size, YYJSON_ALC_DYN_MIN_SIZE);
    if (unlikely(alc_size < *size)) return false; /* overflow */
    *size = alc_size;
    return true;
}

/** remove a chunk from list (the chunk must already be in the list) */
static_inline void dyn_chunk_list_remove(dyn_chunk *list, dyn_chunk *chunk) {
    dyn_chunk *prev = list, *cur;
    for (cur = prev->next; cur; cur = cur->next) {
        if (cur == chunk) {
            prev->next = cur->next;
            cur->next = NULL;
            return;
        }
        prev = cur;
    }
}

/** add a chunk to list header (the chunk must not be in the list) */
static_inline void dyn_chunk_list_add(dyn_chunk *list, dyn_chunk *chunk) {
    chunk->next = list->next;
    list->next = chunk;
}

static void *dyn_malloc(void *ctx_ptr, usize size) {
    /* assert(size != 0) */
    const yyjson_alc def = YYJSON_DEFAULT_ALC;
    dyn_ctx *ctx = (dyn_ctx *)ctx_ptr;
    dyn_chunk *chunk, *prev, *next;
    if (unlikely(!dyn_size_align(&size))) return NULL;
    
    /* freelist is empty, create new chunk */
    if (!ctx->free_list.next) {
        chunk = (dyn_chunk *)def.malloc(def.ctx, size);
        if (unlikely(!chunk)) return NULL;
        chunk->size = size;
        chunk->next = NULL;
        dyn_chunk_list_add(&ctx->used_list, chunk);
        return (void *)(chunk + 1);
    }
    
    /* find a large enough chunk, or resize the largest chunk */
    prev = &ctx->free_list;
    while (true) {
        chunk = prev->next;
        if (chunk->size >= size) { /* enough size, reuse this chunk */
            prev->next = chunk->next;
            dyn_chunk_list_add(&ctx->used_list, chunk);
            return (void *)(chunk + 1);
        }
        if (!chunk->next) { /* resize the largest chunk */
            chunk = (dyn_chunk *)def.realloc(def.ctx, chunk, chunk->size, size);
            if (unlikely(!chunk)) return NULL;
            prev->next = NULL;
            chunk->size = size;
            dyn_chunk_list_add(&ctx->used_list, chunk);
            return (void *)(chunk + 1);
        }
        prev = chunk;
    }
}

static void *dyn_realloc(void *ctx_ptr, void *ptr,
                          usize old_size, usize size) {
    /* assert(ptr != NULL && size != 0 && old_size < size) */
    const yyjson_alc def = YYJSON_DEFAULT_ALC;
    dyn_ctx *ctx = (dyn_ctx *)ctx_ptr;
    dyn_chunk *prev, *next, *new_chunk;
    dyn_chunk *chunk = (dyn_chunk *)ptr - 1;
    if (unlikely(!dyn_size_align(&size))) return NULL;
    if (chunk->size >= size) return ptr;
    
    dyn_chunk_list_remove(&ctx->used_list, chunk);
    new_chunk = (dyn_chunk *)def.realloc(def.ctx, chunk, chunk->size, size);
    if (likely(new_chunk)) {
        new_chunk->size = size;
        chunk = new_chunk;
    }
    dyn_chunk_list_add(&ctx->used_list, chunk);
    return new_chunk ? (void *)(new_chunk + 1) : NULL;
}

static void dyn_free(void *ctx_ptr, void *ptr) {
    /* assert(ptr != NULL) */
    dyn_ctx *ctx = (dyn_ctx *)ctx_ptr;
    dyn_chunk *chunk = (dyn_chunk *)ptr - 1, *prev;
    
    dyn_chunk_list_remove(&ctx->used_list, chunk);
    for (prev = &ctx->free_list; prev; prev = prev->next) {
        if (!prev->next || prev->next->size >= chunk->size) {
            chunk->next = prev->next;
            prev->next = chunk;
            break;
        }
    }
}

yyjson_alc *yyjson_alc_dyn_new(void) {
    const yyjson_alc def = YYJSON_DEFAULT_ALC;
    usize hdr_len = sizeof(yyjson_alc) + sizeof(dyn_ctx);
    yyjson_alc *alc = (yyjson_alc *)def.malloc(def.ctx, hdr_len);
    dyn_ctx *ctx = (dyn_ctx *)(void *)(alc + 1);
    if (unlikely(!alc)) return NULL;
    alc->malloc = dyn_malloc;
    alc->realloc = dyn_realloc;
    alc->free = dyn_free;
    alc->ctx = alc + 1;
    memset(ctx, 0, sizeof(*ctx));
    return alc;
}

void yyjson_alc_dyn_free(yyjson_alc *alc) {
    const yyjson_alc def = YYJSON_DEFAULT_ALC;
    dyn_ctx *ctx = (dyn_ctx *)(void *)(alc + 1);
    dyn_chunk *chunk, *next;
    if (unlikely(!alc)) return;
    for (chunk = ctx->free_list.next; chunk; chunk = next) {
        next = chunk->next;
        def.free(def.ctx, chunk);
    }
    for (chunk = ctx->used_list.next; chunk; chunk = next) {
        next = chunk->next;
        def.free(def.ctx, chunk);
    }
    def.free(def.ctx, alc);
}



        memset(&doc->alc, 0, sizeof(alc));
    if (lt == rt) return luni->u64 == runi->u64;
    if (lt == YYJSON_SUBTYPE_SINT && rt == YYJSON_SUBTYPE_UINT) {
    }
    if (lt == YYJSON_SUBTYPE_UINT && rt == YYJSON_SUBTYPE_SINT) {
    }
                    if (!rhs) return false;
                    if (!unsafe_yyjson_equals(lhs + 1, rhs)) return false;
                    if (!rhs) return false;
                    if (!unsafe_yyjson_mut_equals(lhs->next, rhs)) return false;
    memset(&local_orig, 0, sizeof(local_orig));
    memset(&local_orig, 0, sizeof(local_orig));
#if !YYJSON_DISABLE_READER

fail_recursion:
    return_err(cur, RECURSION_DEPTH, "array and object recursion depth exceeded");

    u32 container_depth = 0; /* limit on number of open array and map */
    container_depth++;
    if (unlikely(container_depth >= YYJSON_READER_CONTAINER_RECURSION_LIMIT)) {
        goto fail_recursion;
    }

    container_depth--;

    container_depth++;
    if (unlikely(container_depth >= YYJSON_READER_CONTAINER_RECURSION_LIMIT)) {
        goto fail_recursion;
    }

    container_depth--;

fail_recursion:
    return_err(cur, RECURSION_DEPTH, "array and object recursion depth exceeded");

    u32 container_depth = 0; /* limit on number of open array and map */
    container_depth++;
    if (unlikely(container_depth >= YYJSON_READER_CONTAINER_RECURSION_LIMIT)) {
        goto fail_recursion;
    }

    container_depth--;

    container_depth++;
    if (unlikely(container_depth >= YYJSON_READER_CONTAINER_RECURSION_LIMIT)) {
        goto fail_recursion;
    }

    container_depth--;

fail_recursion:
    return_err(cur, RECURSION_DEPTH, "array and object recursion depth exceeded");
    bool newline = has_write_flag(NEWLINE_AT_END) != 0;
    const usize end_len = 2; /* '\n' and '\0' */
            incr_len(str_len + end_len);
            incr_len(str_len * 6 + 2 + end_len);
            incr_len(32 + end_len);
            incr_len(2 + end_len);
            incr_len(2 + end_len);
    if (newline) *cur++ = '\n';
    bool newline = has_write_flag(NEWLINE_AT_END) != 0;
    if (newline) {
        incr_len(2);
        *(cur - 1) = '\n';
        cur++;
    }
    bool newline = has_write_flag(NEWLINE_AT_END) != 0;
    if (newline) {
        incr_len(2);
        *cur++ = '\n';
    }
    bool newline = has_write_flag(NEWLINE_AT_END) != 0;
    if (newline) {
        incr_len(2);
        *(cur - 1) = '\n';
        cur++;
    }
    bool newline = has_write_flag(NEWLINE_AT_END) != 0;
    if (newline) {
        incr_len(2);
        *cur++ = '\n';
    }
#define YYJSON_VERSION_MINOR  8
#define YYJSON_VERSION_HEX    0x000800
#define YYJSON_VERSION_STRING "0.8.0"
 This is not a general-purpose allocator. It is designed to handle a single JSON
 data at a time. If it is used for overly complex memory tasks, such as parsing
 multiple JSON documents using the same allocator but releasing only a few of
 them, it may cause memory fragmentation, resulting in performance degradation
 and memory waste.
 
 @warning This Allocator is not thread-safe.
/**
 A dynamic allocator.
 
 This allocator has a similar usage to the pool allocator above. However, when
 there is not enough memory, this allocator will dynamically request more memory
 using libc's `malloc` function, and frees it all at once when it is destroyed.
 
 @return A new dynamic allocator, or NULL if memory allocation failed.
 @note The returned value should be freed with `yyjson_alc_dyn_free()`.
 
 @warning This Allocator is not thread-safe.
 */
yyjson_api yyjson_alc *yyjson_alc_dyn_new(void);

/**
 Free a dynamic allocator which is created by `yyjson_alc_dyn_new()`.
 @param alc The dynamic allocator to be destroyed.
 */
yyjson_api void yyjson_alc_dyn_free(yyjson_alc *alc);

static const yyjson_read_flag YYJSON_READ_NOFLAG                = 0;
/** Document exceeded YYJSON_READER_CONTAINER_RECURSION_LIMIT.  */
static const yyjson_read_code YYJSON_READ_ERROR_RECURSION_DEPTH         = 14;

static const yyjson_write_flag YYJSON_WRITE_NOFLAG                  = 0;
/** Adds a newline character `\n` at the end of the JSON.
    This can be helpful for text editors or NDJSON. */
static const yyjson_write_flag YYJSON_WRITE_NEWLINE_AT_END          = 1 << 7;

    @warning The key string is not copied, you should keep the string
    @warning The key string is not copied, you should keep the string
    @warning The key string is not copied, you should keep the string
    @warning The key string is not copied, you should keep the string
    @warning The key string is not copied, you should keep the string
    @warning The key string is not copied, you should keep the string
    @warning The key string is not copied, you should keep the string
    @warning The key string is not copied, you should keep the string
    @warning The key/value strings are not copied, you should keep these strings
    @warning The key/value strings are not copied, you should keep these strings
    @warning The key string is not copied, you should keep the string
    @warning The key/value strings are not copied, you should keep these strings
/**
 Creates and adds a new array to the target object.
 The `key` should be a null-terminated UTF-8 string.
 This function allows duplicated key in one object.
 
 @warning The key string is not copied, you should keep these strings
          unmodified for the lifetime of this JSON document.
 @return The new array, or NULL on error.
 */
yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_add_arr(yyjson_mut_doc *doc,
                                                         yyjson_mut_val *obj,
                                                         const char *key);

/**
 Creates and adds a new object to the target object.
 The `key` should be a null-terminated UTF-8 string.
 This function allows duplicated key in one object.
 
 @warning The key string is not copied, you should keep these strings
          unmodified for the lifetime of this JSON document.
 @return The new object, or NULL on error.
 */
yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_add_obj(yyjson_mut_doc *doc,
                                                         yyjson_mut_val *obj,
                                                         const char *key);

    @warning The key string is not copied, you should keep the string
        memset(&doc->alc, 0, sizeof(alc));
yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_add_arr(yyjson_mut_doc *doc,
                                                         yyjson_mut_val *obj,
                                                         const char *_key) {
    yyjson_mut_val *key = yyjson_mut_str(doc, _key);
    yyjson_mut_val *val = yyjson_mut_arr(doc);
    return yyjson_mut_obj_add(obj, key, val) ? val : NULL;
}

yyjson_api_inline yyjson_mut_val *yyjson_mut_obj_add_obj(yyjson_mut_doc *doc,
                                                         yyjson_mut_val *obj,
                                                         const char *_key) {
    yyjson_mut_val *key = yyjson_mut_str(doc, _key);
    yyjson_mut_val *val = yyjson_mut_obj(doc);
    return yyjson_mut_obj_add(obj, key, val) ? val : NULL;
}

 Set provided `value` if the JSON Pointer (RFC 6901) exists and is an integer
 that fits in `uint64_t`. Returns true if successful, otherwise false.
    if (value && val) {
        uint64_t ret = val->uni.u64;
        if (unsafe_yyjson_is_uint(val) ||
            (unsafe_yyjson_is_sint(val) && !(ret >> 63))) {
            *value = ret;
            return true;
        }
    return false;
 Set provided `value` if the JSON Pointer (RFC 6901) exists and is an integer
 that fits in `int64_t`. Returns true if successful, otherwise false.
    if (value && val) {
        int64_t ret = val->uni.i64;
        if (unsafe_yyjson_is_sint(val) ||
            (unsafe_yyjson_is_uint(val) && ret >= 0)) {
            *value = ret;
            return true;
        }
    return false;
yyjson_version="0eca326fe57aeeb866e6f04c9ef9ea9f8343157e"

git apply include/yyjson-recursion-limit.patch
LOADS_RECURSION_LIMIT = 1024

    def test_loads_recursion_partial(self):
        loads() recursion limit partial
    def test_loads_recursion_valid_limit_array(self):
        """
        loads() recursion limit at limit array
        """
        n = LOADS_RECURSION_LIMIT + 1
        value = b"[" * n + b"]" * n
        pytest.raises(orjson.JSONDecodeError, orjson.loads, value)

    def test_loads_recursion_valid_limit_object(self):
        """
        loads() recursion limit at limit object
        """
        n = LOADS_RECURSION_LIMIT
        value = b'{"key":' * n + b'{"key":true}' + b"}" * n
        pytest.raises(orjson.JSONDecodeError, orjson.loads, value)

    def test_loads_recursion_valid_limit_mixed(self):
        """
        loads() recursion limit at limit mixed
        """
        n = LOADS_RECURSION_LIMIT
        value = b"[" b'{"key":' * n + b'{"key":true}' + b"}" * n + b"]"
        pytest.raises(orjson.JSONDecodeError, orjson.loads, value)

    def test_loads_recursion_valid_excessive_array(self):
        """
        loads() recursion limit excessively high value
        """
        n = 10000000
        value = b"[" * n + b"]" * n
        pytest.raises(orjson.JSONDecodeError, orjson.loads, value)

    def test_loads_recursion_valid_limit_array_pretty(self):
        """
        loads() recursion limit at limit array pretty
        """
        n = LOADS_RECURSION_LIMIT + 1
        value = b"[\n  " * n + b"]" * n
        pytest.raises(orjson.JSONDecodeError, orjson.loads, value)

    def test_loads_recursion_valid_limit_object_pretty(self):
        """
        loads() recursion limit at limit object pretty
        """
        n = LOADS_RECURSION_LIMIT
        value = b'{\n  "key":' * n + b'{"key":true}' + b"}" * n
        pytest.raises(orjson.JSONDecodeError, orjson.loads, value)

    def test_loads_recursion_valid_limit_mixed_pretty(self):
        """
        loads() recursion limit at limit mixed pretty
        """
        n = LOADS_RECURSION_LIMIT
        value = b"[\n  " b'{"key":' * n + b'{"key":true}' + b"}" * n + b"]"
        pytest.raises(orjson.JSONDecodeError, orjson.loads, value)

    def test_loads_recursion_valid_excessive_array_pretty(self):
        """
        loads() recursion limit excessively high value pretty
        """
        n = 10000000
        value = b"[\n  " * n + b"]" * n
        pytest.raises(orjson.JSONDecodeError, orjson.loads, value)