mirror of https://github.com/openssl/openssl.git
				
				
				
			
		
			
				
	
	
		
			566 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			C
		
	
	
	
			
		
		
	
	
			566 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			C
		
	
	
	
| /*
 | |
|  * Copyright 2023-2024 The OpenSSL Project Authors. All Rights Reserved.
 | |
|  *
 | |
|  * Licensed under the Apache License 2.0 (the "License").  You may not use
 | |
|  * this file except in compliance with the License.  You can obtain a copy
 | |
|  * in the file LICENSE in the source distribution or at
 | |
|  * https://www.openssl.org/source/license.html
 | |
|  */
 | |
| 
 | |
| #include "internal/quic_srtm.h"
 | |
| #include "internal/common.h"
 | |
| #include <openssl/lhash.h>
 | |
| #include <openssl/core_names.h>
 | |
| #include <openssl/rand.h>
 | |
| 
 | |
| /*
 | |
|  * QUIC Stateless Reset Token Manager
 | |
|  * ==================================
 | |
|  */
 | |
| typedef struct srtm_item_st SRTM_ITEM;
 | |
| 
 | |
| #define BLINDED_SRT_LEN     16
 | |
| 
 | |
| DEFINE_LHASH_OF_EX(SRTM_ITEM);
 | |
| 
 | |
| /*
 | |
|  * The SRTM is implemented using two LHASH instances, one matching opaque pointers to
 | |
|  * an item structure, and another matching a SRT-derived value to an item
 | |
|  * structure. Multiple items with different seq_num values under a given opaque,
 | |
|  * and duplicate SRTs, are handled using sorted singly-linked lists.
 | |
|  *
 | |
|  * The O(n) insert and lookup performance is tolerated on the basis that the
 | |
|  * total number of entries for a given opaque (total number of extant CIDs for a
 | |
|  * connection) should be quite small, and the QUIC protocol allows us to place a
 | |
|  * hard limit on this via the active_connection_id_limit TPARAM. Thus there is
 | |
|  * no risk of a large number of SRTs needing to be registered under a given
 | |
|  * opaque.
 | |
|  *
 | |
|  * It is expected one SRTM will exist per QUIC_PORT and track all SRTs across
 | |
|  * all connections for that QUIC_PORT.
 | |
|  */
 | |
| struct srtm_item_st {
 | |
|     SRTM_ITEM                   *next_by_srt_blinded; /* SORT BY opaque  DESC */
 | |
|     SRTM_ITEM                   *next_by_seq_num;     /* SORT BY seq_num DESC */
 | |
|     void                        *opaque; /* \__ unique identity for item */
 | |
|     uint64_t                    seq_num; /* /                            */
 | |
|     QUIC_STATELESS_RESET_TOKEN  srt;
 | |
|     unsigned char               srt_blinded[BLINDED_SRT_LEN]; /* H(srt) */
 | |
| 
 | |
| #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
 | |
|     uint32_t                    debug_token;
 | |
| #endif
 | |
| };
 | |
| 
 | |
| struct quic_srtm_st {
 | |
|     /* Crypto context used to calculate blinded SRTs H(srt). */
 | |
|     EVP_CIPHER_CTX              *blind_ctx; /* kept with key */
 | |
| 
 | |
|     LHASH_OF(SRTM_ITEM)         *items_fwd; /* (opaque)  -> SRTM_ITEM */
 | |
|     LHASH_OF(SRTM_ITEM)         *items_rev; /* (H(srt))  -> SRTM_ITEM */
 | |
| 
 | |
|     /*
 | |
|      * Monotonically transitions to 1 in event of allocation failure. The only
 | |
|      * valid operation on such an object is to free it.
 | |
|      */
 | |
|     unsigned int                alloc_failed : 1;
 | |
| };
 | |
| 
 | |
| static unsigned long items_fwd_hash(const SRTM_ITEM *item)
 | |
| {
 | |
|     return (unsigned long)(uintptr_t)item->opaque;
 | |
| }
 | |
| 
 | |
| static int items_fwd_cmp(const SRTM_ITEM *a, const SRTM_ITEM *b)
 | |
| {
 | |
|     return a->opaque != b->opaque;
 | |
| }
 | |
| 
 | |
| static unsigned long items_rev_hash(const SRTM_ITEM *item)
 | |
| {
 | |
|     /*
 | |
|      * srt_blinded has already been through a crypto-grade hash function, so we
 | |
|      * can just use bits from that.
 | |
|      */
 | |
|     unsigned long l;
 | |
| 
 | |
|     memcpy(&l, item->srt_blinded, sizeof(l));
 | |
|     return l;
 | |
| }
 | |
| 
 | |
| static int items_rev_cmp(const SRTM_ITEM *a, const SRTM_ITEM *b)
 | |
| {
 | |
|     /*
 | |
|      * We don't need to use CRYPTO_memcmp here as the relationship of
 | |
|      * srt_blinded to srt is already cryptographically obfuscated.
 | |
|      */
 | |
|     return memcmp(a->srt_blinded, b->srt_blinded, sizeof(a->srt_blinded));
 | |
| }
 | |
| 
 | |
| static int srtm_check_lh(QUIC_SRTM *srtm, LHASH_OF(SRTM_ITEM) *lh)
 | |
| {
 | |
|     if (lh_SRTM_ITEM_error(lh)) {
 | |
|         srtm->alloc_failed = 1;
 | |
|         return 0;
 | |
|     }
 | |
| 
 | |
|     return 1;
 | |
| }
 | |
| 
 | |
| QUIC_SRTM *ossl_quic_srtm_new(OSSL_LIB_CTX *libctx, const char *propq)
 | |
| {
 | |
|     QUIC_SRTM *srtm = NULL;
 | |
|     unsigned char key[16];
 | |
|     EVP_CIPHER *ecb = NULL;
 | |
| 
 | |
|     if (RAND_priv_bytes_ex(libctx, key, sizeof(key), sizeof(key) * 8) != 1)
 | |
|         goto err;
 | |
| 
 | |
|     if ((srtm = OPENSSL_zalloc(sizeof(*srtm))) == NULL)
 | |
|         return NULL;
 | |
| 
 | |
|     /* Use AES-128-ECB as a permutation over 128-bit SRTs. */
 | |
|     if ((ecb = EVP_CIPHER_fetch(libctx, "AES-128-ECB", propq)) == NULL)
 | |
|         goto err;
 | |
| 
 | |
|     if ((srtm->blind_ctx = EVP_CIPHER_CTX_new()) == NULL)
 | |
|         goto err;
 | |
| 
 | |
|     if (!EVP_EncryptInit_ex2(srtm->blind_ctx, ecb, key, NULL, NULL))
 | |
|         goto err;
 | |
| 
 | |
|     EVP_CIPHER_free(ecb);
 | |
|     ecb = NULL;
 | |
| 
 | |
|     /* Create mappings. */
 | |
|     if ((srtm->items_fwd = lh_SRTM_ITEM_new(items_fwd_hash, items_fwd_cmp)) == NULL
 | |
|         || (srtm->items_rev = lh_SRTM_ITEM_new(items_rev_hash, items_rev_cmp)) == NULL)
 | |
|         goto err;
 | |
| 
 | |
|     return srtm;
 | |
| 
 | |
| err:
 | |
|     /*
 | |
|      * No cleansing of key needed as blinding exists only for side channel
 | |
|      * mitigation.
 | |
|      */
 | |
|     ossl_quic_srtm_free(srtm);
 | |
|     EVP_CIPHER_free(ecb);
 | |
|     return NULL;
 | |
| }
 | |
| 
 | |
| static void srtm_free_each(SRTM_ITEM *ihead)
 | |
| {
 | |
|     SRTM_ITEM *inext, *item = ihead;
 | |
| 
 | |
|     for (item = item->next_by_seq_num; item != NULL; item = inext) {
 | |
|         inext = item->next_by_seq_num;
 | |
|         OPENSSL_free(item);
 | |
|     }
 | |
| 
 | |
|     OPENSSL_free(ihead);
 | |
| }
 | |
| 
 | |
| void ossl_quic_srtm_free(QUIC_SRTM *srtm)
 | |
| {
 | |
|     if (srtm == NULL)
 | |
|         return;
 | |
| 
 | |
|     lh_SRTM_ITEM_free(srtm->items_rev);
 | |
|     if (srtm->items_fwd != NULL) {
 | |
|         lh_SRTM_ITEM_doall(srtm->items_fwd, srtm_free_each);
 | |
|         lh_SRTM_ITEM_free(srtm->items_fwd);
 | |
|     }
 | |
| 
 | |
|     EVP_CIPHER_CTX_free(srtm->blind_ctx);
 | |
|     OPENSSL_free(srtm);
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * Find a SRTM_ITEM by (opaque, seq_num). Returns NULL if no match.
 | |
|  * If head is non-NULL, writes the head of the relevant opaque list to *head if
 | |
|  * there is one.
 | |
|  * If prev is non-NULL, writes the previous node to *prev or NULL if it is
 | |
|  * the first item.
 | |
|  */
 | |
| static SRTM_ITEM *srtm_find(QUIC_SRTM *srtm, void *opaque, uint64_t seq_num,
 | |
|                             SRTM_ITEM **head_p, SRTM_ITEM **prev_p)
 | |
| {
 | |
|     SRTM_ITEM key, *item = NULL, *prev = NULL;
 | |
| 
 | |
|     key.opaque  = opaque;
 | |
| 
 | |
|     item = lh_SRTM_ITEM_retrieve(srtm->items_fwd, &key);
 | |
|     if (head_p != NULL)
 | |
|         *head_p = item;
 | |
| 
 | |
|     for (; item != NULL; prev = item, item = item->next_by_seq_num)
 | |
|         if (item->seq_num == seq_num) {
 | |
|             break;
 | |
|         } else if (item->seq_num < seq_num) {
 | |
|             /*
 | |
|              * List is sorted in descending order so there can't be any match
 | |
|              * after this.
 | |
|              */
 | |
|             item = NULL;
 | |
|             break;
 | |
|         }
 | |
| 
 | |
|     if (prev_p != NULL)
 | |
|         *prev_p = prev;
 | |
| 
 | |
|     return item;
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * Inserts a SRTM_ITEM into the singly-linked by-sequence-number linked list.
 | |
|  * The new head pointer is written to *new_head (which may or may not be
 | |
|  * unchanged).
 | |
|  */
 | |
| static void sorted_insert_seq_num(SRTM_ITEM *head, SRTM_ITEM *item, SRTM_ITEM **new_head)
 | |
| {
 | |
|     uint64_t seq_num = item->seq_num;
 | |
|     SRTM_ITEM *cur = head, **fixup = new_head;
 | |
| 
 | |
|     *new_head = head;
 | |
| 
 | |
|     while (cur != NULL && cur->seq_num > seq_num) {
 | |
|         fixup = &cur->next_by_seq_num;
 | |
|         cur = cur->next_by_seq_num;
 | |
|     }
 | |
| 
 | |
|     item->next_by_seq_num = *fixup;
 | |
|     *fixup = item;
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * Inserts a SRTM_ITEM into the singly-linked by-SRT list.
 | |
|  * The new head pointer is written to *new_head (which may or may not be
 | |
|  * unchanged).
 | |
|  */
 | |
| static void sorted_insert_srt(SRTM_ITEM *head, SRTM_ITEM *item, SRTM_ITEM **new_head)
 | |
| {
 | |
|     uintptr_t opaque = (uintptr_t)item->opaque;
 | |
|     SRTM_ITEM *cur = head, **fixup = new_head;
 | |
| 
 | |
|     *new_head = head;
 | |
| 
 | |
|     while (cur != NULL && (uintptr_t)cur->opaque > opaque) {
 | |
|         fixup = &cur->next_by_srt_blinded;
 | |
|         cur = cur->next_by_srt_blinded;
 | |
|     }
 | |
| 
 | |
|     item->next_by_srt_blinded = *fixup;
 | |
|     *fixup = item;
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * Computes the blinded SRT value used for internal lookup for side channel
 | |
|  * mitigation purposes. We compute this once as a cached value when an SRTM_ITEM
 | |
|  * is formed.
 | |
|  */
 | |
| static int srtm_compute_blinded(QUIC_SRTM *srtm, SRTM_ITEM *item,
 | |
|                                 const QUIC_STATELESS_RESET_TOKEN *token)
 | |
| {
 | |
|     int outl = 0;
 | |
| 
 | |
|     /*
 | |
|      * We use AES-128-ECB as a permutation using a random key to facilitate
 | |
|      * blinding for side-channel purposes. Encrypt the token as a single AES
 | |
|      * block.
 | |
|      */
 | |
|     if (!EVP_EncryptUpdate(srtm->blind_ctx, item->srt_blinded, &outl,
 | |
|                            (const unsigned char *)token, sizeof(*token)))
 | |
|         return 0;
 | |
| 
 | |
|     if (!ossl_assert(outl == sizeof(*token)))
 | |
|         return 0;
 | |
| 
 | |
|     return 1;
 | |
| }
 | |
| 
 | |
| int ossl_quic_srtm_add(QUIC_SRTM *srtm, void *opaque, uint64_t seq_num,
 | |
|                        const QUIC_STATELESS_RESET_TOKEN *token)
 | |
| {
 | |
|     SRTM_ITEM *item = NULL, *head = NULL, *new_head, *r_item;
 | |
| 
 | |
|     if (srtm->alloc_failed)
 | |
|         return 0;
 | |
| 
 | |
|     /* (opaque, seq_num) duplicates not allowed */
 | |
|     if ((item = srtm_find(srtm, opaque, seq_num, &head, NULL)) != NULL)
 | |
|         return 0;
 | |
| 
 | |
|     if ((item = OPENSSL_zalloc(sizeof(*item))) == NULL)
 | |
|         return 0;
 | |
| 
 | |
|     item->opaque    = opaque;
 | |
|     item->seq_num   = seq_num;
 | |
|     item->srt       = *token;
 | |
|     if (!srtm_compute_blinded(srtm, item, &item->srt)) {
 | |
|         OPENSSL_free(item);
 | |
|         return 0;
 | |
|     }
 | |
| 
 | |
|     /* Add to forward mapping. */
 | |
|     if (head == NULL) {
 | |
|         /* First item under this opaque */
 | |
|         lh_SRTM_ITEM_insert(srtm->items_fwd, item);
 | |
|         if (!srtm_check_lh(srtm, srtm->items_fwd)) {
 | |
|             OPENSSL_free(item);
 | |
|             return 0;
 | |
|         }
 | |
|     } else {
 | |
|         sorted_insert_seq_num(head, item, &new_head);
 | |
|         if (new_head != head) { /* head changed, update in lhash */
 | |
|             lh_SRTM_ITEM_insert(srtm->items_fwd, new_head);
 | |
|             if (!srtm_check_lh(srtm, srtm->items_fwd)) {
 | |
|                 OPENSSL_free(item);
 | |
|                 return 0;
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /* Add to reverse mapping. */
 | |
|     r_item = lh_SRTM_ITEM_retrieve(srtm->items_rev, item);
 | |
|     if (r_item == NULL) {
 | |
|         /* First item under this blinded SRT */
 | |
|         lh_SRTM_ITEM_insert(srtm->items_rev, item);
 | |
|         if (!srtm_check_lh(srtm, srtm->items_rev))
 | |
|             /*
 | |
|              * Can't free the item now as we would have to undo the insertion
 | |
|              * into the forward mapping which would require an insert operation
 | |
|              * to restore the previous value. which might also fail. However,
 | |
|              * the item will be freed OK when we free the entire SRTM.
 | |
|              */
 | |
|             return 0;
 | |
|     } else {
 | |
|         sorted_insert_srt(r_item, item, &new_head);
 | |
|         if (new_head != r_item) { /* head changed, update in lhash */
 | |
|             lh_SRTM_ITEM_insert(srtm->items_rev, new_head);
 | |
|             if (!srtm_check_lh(srtm, srtm->items_rev))
 | |
|                 /* As above. */
 | |
|                 return 0;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     return 1;
 | |
| }
 | |
| 
 | |
| /* Remove item from reverse mapping. */
 | |
| static int srtm_remove_from_rev(QUIC_SRTM *srtm, SRTM_ITEM *item)
 | |
| {
 | |
|     SRTM_ITEM *rh_item;
 | |
| 
 | |
|     rh_item = lh_SRTM_ITEM_retrieve(srtm->items_rev, item);
 | |
|     assert(rh_item != NULL);
 | |
|     if (rh_item == item) {
 | |
|         /*
 | |
|          * Change lhash to point to item after this one, or remove the entry if
 | |
|          * this is the last one.
 | |
|          */
 | |
|         if (item->next_by_srt_blinded != NULL) {
 | |
|             lh_SRTM_ITEM_insert(srtm->items_rev, item->next_by_srt_blinded);
 | |
|             if (!srtm_check_lh(srtm, srtm->items_rev))
 | |
|                 return 0;
 | |
|         } else {
 | |
|             lh_SRTM_ITEM_delete(srtm->items_rev, item);
 | |
|         }
 | |
|     } else {
 | |
|         /* Find our entry in the SRT list */
 | |
|         for (; rh_item->next_by_srt_blinded != item;
 | |
|                rh_item = rh_item->next_by_srt_blinded);
 | |
|         rh_item->next_by_srt_blinded = item->next_by_srt_blinded;
 | |
|     }
 | |
| 
 | |
|     return 1;
 | |
| }
 | |
| 
 | |
| int ossl_quic_srtm_remove(QUIC_SRTM *srtm, void *opaque, uint64_t seq_num)
 | |
| {
 | |
|     SRTM_ITEM *item, *prev = NULL;
 | |
| 
 | |
|     if (srtm->alloc_failed)
 | |
|         return 0;
 | |
| 
 | |
|     if ((item = srtm_find(srtm, opaque, seq_num, NULL, &prev)) == NULL)
 | |
|         /* No match */
 | |
|         return 0;
 | |
| 
 | |
|     /* Remove from forward mapping. */
 | |
|     if (prev == NULL) {
 | |
|         /*
 | |
|          * Change lhash to point to item after this one, or remove the entry if
 | |
|          * this is the last one.
 | |
|          */
 | |
|         if (item->next_by_seq_num != NULL) {
 | |
|             lh_SRTM_ITEM_insert(srtm->items_fwd, item->next_by_seq_num);
 | |
|             if (!srtm_check_lh(srtm, srtm->items_fwd))
 | |
|                 return 0;
 | |
|         } else {
 | |
|             lh_SRTM_ITEM_delete(srtm->items_fwd, item);
 | |
|         }
 | |
|     } else {
 | |
|         prev->next_by_seq_num = item->next_by_seq_num;
 | |
|     }
 | |
| 
 | |
|     /* Remove from reverse mapping. */
 | |
|     if (!srtm_remove_from_rev(srtm, item))
 | |
|         return 0;
 | |
| 
 | |
|     OPENSSL_free(item);
 | |
|     return 1;
 | |
| }
 | |
| 
 | |
| int ossl_quic_srtm_cull(QUIC_SRTM *srtm, void *opaque)
 | |
| {
 | |
|     SRTM_ITEM key, *item = NULL, *inext, *ihead;
 | |
| 
 | |
|     key.opaque = opaque;
 | |
| 
 | |
|     if (srtm->alloc_failed)
 | |
|         return 0;
 | |
| 
 | |
|     if ((ihead = lh_SRTM_ITEM_retrieve(srtm->items_fwd, &key)) == NULL)
 | |
|         return 1; /* nothing removed is a success condition */
 | |
| 
 | |
|     for (item = ihead; item != NULL; item = inext) {
 | |
|         inext = item->next_by_seq_num;
 | |
|         if (item != ihead) {
 | |
|             srtm_remove_from_rev(srtm, item);
 | |
|             OPENSSL_free(item);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     lh_SRTM_ITEM_delete(srtm->items_fwd, ihead);
 | |
|     srtm_remove_from_rev(srtm, ihead);
 | |
|     OPENSSL_free(ihead);
 | |
|     return 1;
 | |
| }
 | |
| 
 | |
| int ossl_quic_srtm_lookup(QUIC_SRTM *srtm,
 | |
|                           const QUIC_STATELESS_RESET_TOKEN *token,
 | |
|                           size_t idx,
 | |
|                           void **opaque, uint64_t *seq_num)
 | |
| {
 | |
|     SRTM_ITEM key, *item;
 | |
| 
 | |
|     if (srtm->alloc_failed)
 | |
|         return 0;
 | |
| 
 | |
|     if (!srtm_compute_blinded(srtm, &key, token))
 | |
|         return 0;
 | |
| 
 | |
|     item = lh_SRTM_ITEM_retrieve(srtm->items_rev, &key);
 | |
|     for (; idx > 0 && item != NULL; --idx, item = item->next_by_srt_blinded);
 | |
|     if (item == NULL)
 | |
|         return 0;
 | |
| 
 | |
|     if (opaque != NULL)
 | |
|         *opaque     = item->opaque;
 | |
|     if (seq_num != NULL)
 | |
|         *seq_num    = item->seq_num;
 | |
| 
 | |
|     return 1;
 | |
| }
 | |
| 
 | |
| #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
 | |
| 
 | |
| static uint32_t token_next = 0x5eadbeef;
 | |
| static size_t tokens_seen;
 | |
| 
 | |
| struct check_args {
 | |
|     uint32_t token;
 | |
|     int      mode;
 | |
| };
 | |
| 
 | |
| static void check_mark(SRTM_ITEM *item, void *arg)
 | |
| {
 | |
|     struct check_args *arg_ = arg;
 | |
|     uint32_t token = arg_->token;
 | |
|     uint64_t prev_seq_num = 0;
 | |
|     void *prev_opaque = NULL;
 | |
|     int have_prev = 0;
 | |
| 
 | |
|     assert(item != NULL);
 | |
| 
 | |
|     while (item != NULL) {
 | |
|         if (have_prev) {
 | |
|             assert(!(item->opaque == prev_opaque && item->seq_num == prev_seq_num));
 | |
|             if (!arg_->mode)
 | |
|                 assert(item->opaque != prev_opaque || item->seq_num < prev_seq_num);
 | |
|         }
 | |
| 
 | |
|         ++tokens_seen;
 | |
|         item->debug_token = token;
 | |
|         prev_opaque  = item->opaque;
 | |
|         prev_seq_num = item->seq_num;
 | |
|         have_prev = 1;
 | |
| 
 | |
|         if (arg_->mode)
 | |
|             item = item->next_by_srt_blinded;
 | |
|         else
 | |
|             item = item->next_by_seq_num;
 | |
|     }
 | |
| }
 | |
| 
 | |
| static void check_count(SRTM_ITEM *item, void *arg)
 | |
| {
 | |
|     struct check_args *arg_ = arg;
 | |
|     uint32_t token = arg_->token;
 | |
| 
 | |
|     assert(item != NULL);
 | |
| 
 | |
|     while (item != NULL) {
 | |
|         ++tokens_seen;
 | |
|         assert(item->debug_token == token);
 | |
| 
 | |
|         if (arg_->mode)
 | |
|             item = item->next_by_seq_num;
 | |
|         else
 | |
|             item = item->next_by_srt_blinded;
 | |
|     }
 | |
| }
 | |
| 
 | |
| #endif
 | |
| 
 | |
| void ossl_quic_srtm_check(const QUIC_SRTM *srtm)
 | |
| {
 | |
| #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
 | |
|     struct check_args args = {0};
 | |
|     size_t tokens_expected, tokens_expected_old;
 | |
| 
 | |
|     args.token = token_next;
 | |
|     ++token_next;
 | |
| 
 | |
|     assert(srtm != NULL);
 | |
|     assert(srtm->blind_ctx != NULL);
 | |
|     assert(srtm->items_fwd != NULL);
 | |
|     assert(srtm->items_rev != NULL);
 | |
| 
 | |
|     tokens_seen = 0;
 | |
|     lh_SRTM_ITEM_doall_arg(srtm->items_fwd, check_mark, &args);
 | |
| 
 | |
|     tokens_expected = tokens_seen;
 | |
|     tokens_seen = 0;
 | |
|     lh_SRTM_ITEM_doall_arg(srtm->items_rev, check_count, &args);
 | |
| 
 | |
|     assert(tokens_seen == tokens_expected);
 | |
|     tokens_expected_old = tokens_expected;
 | |
| 
 | |
|     args.token = token_next;
 | |
|     ++token_next;
 | |
| 
 | |
|     args.mode = 1;
 | |
|     tokens_seen = 0;
 | |
|     lh_SRTM_ITEM_doall_arg(srtm->items_rev, check_mark, &args);
 | |
| 
 | |
|     tokens_expected = tokens_seen;
 | |
|     tokens_seen = 0;
 | |
|     lh_SRTM_ITEM_doall_arg(srtm->items_fwd, check_count, &args);
 | |
| 
 | |
|     assert(tokens_seen == tokens_expected);
 | |
|     assert(tokens_seen == tokens_expected_old);
 | |
| #endif
 | |
| }
 |