md_analyze_line: Avoid reading 1 byte beyond the input size.

Fixes #155.

md_analyze_line(MD_CTX* ctx, OFF beg, OFF* p_end,
                const MD_LINE_ANALYSIS* pivot_line, MD_LINE_ANALYSIS* line)
{
    unsigned total_indent = 0;
    int n_parents = 0;
    int n_brothers = 0;
    int n_children = 0;
    MD_CONTAINER container = { 0 };
    int prev_line_has_list_loosening_effect = ctx->last_line_has_list_loosening_effect;
    OFF off = beg;
    OFF hr_killer = 0;
    int ret = 0;

    line->indent = md_line_indentation(ctx, total_indent, off, &off);
    total_indent += line->indent;
    line->beg = off;

    /* Given the indentation and block quote marks '>', determine how many of
     * the current containers are our parents. */
    while(n_parents < ctx->n_containers) {
        MD_CONTAINER* c = &ctx->containers[n_parents];

        if(c->ch == _T('>')  &&  line->indent < ctx->code_indent_offset  &&
            off < ctx->size  &&  CH(off) == _T('>'))
        {
            /* Block quote mark. */
            off++;
            total_indent++;
            line->indent = md_line_indentation(ctx, total_indent, off, &off);
            total_indent += line->indent;

            /* The optional 1st space after '>' is part of the block quote mark. */
            if(line->indent > 0)
                line->indent--;

            line->beg = off;

        } else if(c->ch != _T('>')  &&  line->indent >= c->contents_indent) {
            /* List. */
            line->indent -= c->contents_indent;
        } else {
            break;
        }

        n_parents++;
    }

    if(off >= ctx->size  ||  ISNEWLINE(off)) {
        /* Blank line does not need any real indentation to be nested inside
         * a list. */
        if(n_brothers + n_children == 0) {
            while(n_parents < ctx->n_containers  &&  ctx->containers[n_parents].ch != _T('>'))
                n_parents++;
        }
    }

    while(TRUE) {
        /* Check whether we are fenced code continuation. */
        if(pivot_line->type == MD_LINE_FENCEDCODE) {
            line->beg = off;

            /* We are another MD_LINE_FENCEDCODE unless we are closing fence
             * which we transform into MD_LINE_BLANK. */
            if(line->indent < ctx->code_indent_offset) {
                if(md_is_closing_code_fence(ctx, CH(pivot_line->beg), off, &off)) {
                    line->type = MD_LINE_BLANK;
                    ctx->last_line_has_list_loosening_effect = FALSE;
                    break;
                }
            }

            /* Change indentation accordingly to the initial code fence. */
            if(n_parents == ctx->n_containers) {
                if(line->indent > pivot_line->indent)
                    line->indent -= pivot_line->indent;
                else
                    line->indent = 0;

                line->type = MD_LINE_FENCEDCODE;
                break;
            }
        }

        /* Check whether we are HTML block continuation. */
        if(pivot_line->type == MD_LINE_HTML  &&  ctx->html_block_type > 0) {
            if(n_parents < ctx->n_containers) {
                /* HTML block is implicitly ended if the enclosing container
                 * block ends. */
                ctx->html_block_type = 0;
            } else {
                int html_block_type;

                html_block_type = md_is_html_block_end_condition(ctx, off, &off);
                if(html_block_type > 0) {
                    MD_ASSERT(html_block_type == ctx->html_block_type);

                    /* Make sure this is the last line of the block. */
                    ctx->html_block_type = 0;

                    /* Some end conditions serve as blank lines at the same time. */
                    if(html_block_type == 6 || html_block_type == 7) {
                        line->type = MD_LINE_BLANK;
                        line->indent = 0;
                        break;
                    }
                }

                line->type = MD_LINE_HTML;
                n_parents = ctx->n_containers;
                break;
            }
        }

        /* Check for blank line. */
        if(off >= ctx->size  ||  ISNEWLINE(off)) {
            if(pivot_line->type == MD_LINE_INDENTEDCODE  &&  n_parents == ctx->n_containers) {
                line->type = MD_LINE_INDENTEDCODE;
                if(line->indent > ctx->code_indent_offset)
                    line->indent -= ctx->code_indent_offset;
                else
                    line->indent = 0;
                ctx->last_line_has_list_loosening_effect = FALSE;
            } else {
                line->type = MD_LINE_BLANK;
                ctx->last_line_has_list_loosening_effect = (n_parents > 0  &&
                        n_brothers + n_children == 0  &&
                        ctx->containers[n_parents-1].ch != _T('>'));

    #if 1
                /* See https://github.com/mity/md4c/issues/6
                 *
                 * This ugly checking tests we are in (yet empty) list item but
                 * not its very first line (i.e. not the line with the list
                 * item mark).
                 *
                 * If we are such a blank line, then any following non-blank
                 * line which would be part of the list item actually has to
                 * end the list because according to the specification, "a list
                 * item can begin with at most one blank line."
                 */
                if(n_parents > 0  &&  ctx->containers[n_parents-1].ch != _T('>')  &&
                   n_brothers + n_children == 0  &&  ctx->current_block == NULL  &&
                   ctx->n_block_bytes > (int) sizeof(MD_BLOCK))
                {
                    MD_BLOCK* top_block = (MD_BLOCK*) ((char*)ctx->block_bytes + ctx->n_block_bytes - sizeof(MD_BLOCK));
                    if(top_block->type == MD_BLOCK_LI)
                        ctx->last_list_item_starts_with_two_blank_lines = TRUE;
                }
    #endif
            }
            break;
        } else {
    #if 1
            /* This is the 2nd half of the hack. If the flag is set (i.e. there
             * was a 2nd blank line at the beginning of the list item) and if
             * we would otherwise still belong to the list item, we enforce
             * the end of the list. */
            ctx->last_line_has_list_loosening_effect = FALSE;
            if(ctx->last_list_item_starts_with_two_blank_lines) {
                if(n_parents > 0  &&  ctx->containers[n_parents-1].ch != _T('>')  &&
                   n_brothers + n_children == 0  &&  ctx->current_block == NULL  &&
                   ctx->n_block_bytes > (int) sizeof(MD_BLOCK))
                {
                    MD_BLOCK* top_block = (MD_BLOCK*) ((char*)ctx->block_bytes + ctx->n_block_bytes - sizeof(MD_BLOCK));
                    if(top_block->type == MD_BLOCK_LI)
                        n_parents--;
                }

                ctx->last_list_item_starts_with_two_blank_lines = FALSE;
            }
    #endif
        }

        /* Check whether we are Setext underline. */
        if(line->indent < ctx->code_indent_offset  &&  pivot_line->type == MD_LINE_TEXT
            &&  (CH(off) == _T('=') || CH(off) == _T('-'))
            &&  (n_parents == ctx->n_containers))
        {
            unsigned level;

            if(md_is_setext_underline(ctx, off, &off, &level)) {
                line->type = MD_LINE_SETEXTUNDERLINE;
                line->data = level;
                break;
            }
        }

        /* Check for thematic break line. */
        if(line->indent < ctx->code_indent_offset  &&  ISANYOF(off, _T("-_*"))  &&  off >= hr_killer) {
            if(md_is_hr_line(ctx, off, &off, &hr_killer)) {
                line->type = MD_LINE_HR;
                break;
            }
        }

        /* Check for "brother" container. I.e. whether we are another list item
         * in already started list. */
        if(n_parents < ctx->n_containers  &&  n_brothers + n_children == 0) {
            OFF tmp;

            if(md_is_container_mark(ctx, line->indent, off, &tmp, &container)  &&
               md_is_container_compatible(&ctx->containers[n_parents], &container))
            {
                pivot_line = &md_dummy_blank_line;

                off = tmp;

                total_indent += container.contents_indent - container.mark_indent;
                line->indent = md_line_indentation(ctx, total_indent, off, &off);
                total_indent += line->indent;
                line->beg = off;

                /* Some of the following whitespace actually still belongs to the mark. */
                if(off >= ctx->size || ISNEWLINE(off)) {
                    container.contents_indent++;
                } else if(line->indent <= ctx->code_indent_offset) {
                    container.contents_indent += line->indent;
                    line->indent = 0;
                } else {
                    container.contents_indent += 1;
                    line->indent--;
                }

                ctx->containers[n_parents].mark_indent = container.mark_indent;
                ctx->containers[n_parents].contents_indent = container.contents_indent;

                n_brothers++;
                continue;
            }
        }

        /* Check for indented code.
         * Note indented code block cannot interrupt a paragraph. */
        if(line->indent >= ctx->code_indent_offset  &&
            (pivot_line->type == MD_LINE_BLANK || pivot_line->type == MD_LINE_INDENTEDCODE))
        {
            line->type = MD_LINE_INDENTEDCODE;
            MD_ASSERT(line->indent >= ctx->code_indent_offset);
            line->indent -= ctx->code_indent_offset;
            line->data = 0;
            break;
        }

        /* Check for start of a new container block. */
        if(line->indent < ctx->code_indent_offset  &&
           md_is_container_mark(ctx, line->indent, off, &off, &container))
        {
            if(pivot_line->type == MD_LINE_TEXT  &&  n_parents == ctx->n_containers  &&
                        (off >= ctx->size || ISNEWLINE(off))  &&  container.ch != _T('>'))
            {
                /* Noop. List mark followed by a blank line cannot interrupt a paragraph. */
            } else if(pivot_line->type == MD_LINE_TEXT  &&  n_parents == ctx->n_containers  &&
                        (container.ch == _T('.') || container.ch == _T(')'))  &&  container.start != 1)
            {
                /* Noop. Ordered list cannot interrupt a paragraph unless the start index is 1. */
            } else {
                total_indent += container.contents_indent - container.mark_indent;
                line->indent = md_line_indentation(ctx, total_indent, off, &off);
                total_indent += line->indent;

                line->beg = off;
                line->data = container.ch;

                /* Some of the following whitespace actually still belongs to the mark. */
                if(off >= ctx->size || ISNEWLINE(off)) {
                    container.contents_indent++;
                } else if(line->indent <= ctx->code_indent_offset) {
                    container.contents_indent += line->indent;
                    line->indent = 0;
                } else {
                    container.contents_indent += 1;
                    line->indent--;
                }

                if(n_brothers + n_children == 0)
                    pivot_line = &md_dummy_blank_line;

                if(n_children == 0)
                    MD_CHECK(md_leave_child_containers(ctx, n_parents + n_brothers));

                n_children++;
                MD_CHECK(md_push_container(ctx, &container));
                continue;
            }
        }

        /* Check whether we are table continuation. */
        if(pivot_line->type == MD_LINE_TABLE  &&  n_parents == ctx->n_containers) {
            line->type = MD_LINE_TABLE;
            break;
        }

        /* Check for ATX header. */
        if(line->indent < ctx->code_indent_offset  &&  CH(off) == _T('#')) {
            unsigned level;

            if(md_is_atxheader_line(ctx, off, &line->beg, &off, &level)) {
                line->type = MD_LINE_ATXHEADER;
                line->data = level;
                break;
            }
        }

        /* Check whether we are starting code fence. */
        if(CH(off) == _T('`') || CH(off) == _T('~')) {
            if(md_is_opening_code_fence(ctx, off, &off)) {
                line->type = MD_LINE_FENCEDCODE;
                line->data = 1;
                break;
            }
        }

        /* Check for start of raw HTML block. */
        if(CH(off) == _T('<')  &&  !(ctx->parser.flags & MD_FLAG_NOHTMLBLOCKS))
        {
            ctx->html_block_type = md_is_html_block_start_condition(ctx, off);

            /* HTML block type 7 cannot interrupt paragraph. */
            if(ctx->html_block_type == 7  &&  pivot_line->type == MD_LINE_TEXT)
                ctx->html_block_type = 0;

            if(ctx->html_block_type > 0) {
                /* The line itself also may immediately close the block. */
                if(md_is_html_block_end_condition(ctx, off, &off) == ctx->html_block_type) {
                    /* Make sure this is the last line of the block. */
                    ctx->html_block_type = 0;
                }

                line->type = MD_LINE_HTML;
                break;
            }
        }

        /* Check for table underline. */
        if((ctx->parser.flags & MD_FLAG_TABLES)  &&  pivot_line->type == MD_LINE_TEXT  &&
           (CH(off) == _T('|') || CH(off) == _T('-') || CH(off) == _T(':'))  &&
           n_parents == ctx->n_containers)
        {
            unsigned col_count;

            if(ctx->current_block != NULL  &&  ctx->current_block->n_lines == 1  &&
                md_is_table_underline(ctx, off, &off, &col_count))
            {
                line->data = col_count;
                line->type = MD_LINE_TABLEUNDERLINE;
                break;
            }
        }

        /* By default, we are normal text line. */
        line->type = MD_LINE_TEXT;
        if(pivot_line->type == MD_LINE_TEXT  &&  n_brothers + n_children == 0) {
            /* Lazy continuation. */
            n_parents = ctx->n_containers;
        }

        /* Check for task mark. */
        if((ctx->parser.flags & MD_FLAG_TASKLISTS)  &&  n_brothers + n_children > 0  &&
           ISANYOF_(ctx->containers[ctx->n_containers-1].ch, _T("-+*.)")))
        {
            OFF tmp = off;

            while(tmp < ctx->size  &&  tmp < off + 3  &&  ISBLANK(tmp))
                tmp++;
            if(tmp + 2 < ctx->size  &&  CH(tmp) == _T('[')  &&
               ISANYOF(tmp+1, _T("xX "))  &&  CH(tmp+2) == _T(']')  &&
               (tmp + 3 == ctx->size  ||  ISBLANK(tmp+3)  ||  ISNEWLINE(tmp+3)))
            {
                MD_CONTAINER* task_container = (n_children > 0 ? &ctx->containers[ctx->n_containers-1] : &container);
                task_container->is_task = TRUE;
                task_container->task_mark_off = tmp + 1;
                off = tmp + 3;
                while(ISWHITESPACE(off))
                    off++;
                line->beg = off;
            }
        }

        break;
    }

    /* Scan for end of the line.
     *
     * Note this is quite a bottleneck of the parsing as we here iterate almost
     * over compete document.
     */
#if defined __linux__ && !defined MD4C_USE_UTF16
    /* Recent glibc versions have superbly optimized strcspn(), even using
     * vectorization if available. */
    if(ctx->doc_ends_with_newline  &&  off < ctx->size) {
        while(TRUE) {
            off += (OFF) strcspn(STR(off), "\r\n");

            /* strcspn() can stop on zero terminator; but that can appear
             * anywhere in the Markfown input... */
            if(CH(off) == _T('\0'))
                off++;
            else
                break;
        }
    } else
#endif
    {
        /* Optimization: Use some loop unrolling. */
        while(off + 3 < ctx->size  &&  !ISNEWLINE(off+0)  &&  !ISNEWLINE(off+1)
                                   &&  !ISNEWLINE(off+2)  &&  !ISNEWLINE(off+3))
            off += 4;
        while(off < ctx->size  &&  !ISNEWLINE(off))
            off++;
    }

    /* Set end of the line. */
    line->end = off;

    /* But for ATX header, we should exclude the optional trailing mark. */
    if(line->type == MD_LINE_ATXHEADER) {
        OFF tmp = line->end;
        while(tmp > line->beg && CH(tmp-1) == _T(' '))
            tmp--;
        while(tmp > line->beg && CH(tmp-1) == _T('#'))
            tmp--;
        if(tmp == line->beg || CH(tmp-1) == _T(' ') || (ctx->parser.flags & MD_FLAG_PERMISSIVEATXHEADERS))
            line->end = tmp;
    }

    /* Trim trailing spaces. */
    if(line->type != MD_LINE_INDENTEDCODE  &&  line->type != MD_LINE_FENCEDCODE) {
        while(line->end > line->beg && CH(line->end-1) == _T(' '))
            line->end--;
    }

    /* Eat also the new line. */
    if(off < ctx->size && CH(off) == _T('\r'))
        off++;
    if(off < ctx->size && CH(off) == _T('\n'))
        off++;

    *p_end = off;

    /* If we belong to a list after seeing a blank line, the list is loose. */
    if(prev_line_has_list_loosening_effect  &&  line->type != MD_LINE_BLANK  &&  n_parents + n_brothers > 0) {
        MD_CONTAINER* c = &ctx->containers[n_parents + n_brothers - 1];
        if(c->ch != _T('>')) {
            MD_BLOCK* block = (MD_BLOCK*) (((char*)ctx->block_bytes) + c->block_byte_off);
            block->flags |= MD_BLOCK_LOOSE_LIST;
        }
    }

    /* Leave any containers we are not part of anymore. */
    if(n_children == 0  &&  n_parents + n_brothers < ctx->n_containers)
        MD_CHECK(md_leave_child_containers(ctx, n_parents + n_brothers));

    /* Enter any container we found a mark for. */
    if(n_brothers > 0) {
        MD_ASSERT(n_brothers == 1);
        MD_CHECK(md_push_container_bytes(ctx, MD_BLOCK_LI,
                    ctx->containers[n_parents].task_mark_off,
                    (ctx->containers[n_parents].is_task ? CH(ctx->containers[n_parents].task_mark_off) : 0),
                    MD_BLOCK_CONTAINER_CLOSER));
        MD_CHECK(md_push_container_bytes(ctx, MD_BLOCK_LI,
                    container.task_mark_off,
                    (container.is_task ? CH(container.task_mark_off) : 0),
                    MD_BLOCK_CONTAINER_OPENER));
        ctx->containers[n_parents].is_task = container.is_task;
        ctx->containers[n_parents].task_mark_off = container.task_mark_off;
    }

    if(n_children > 0)
        MD_CHECK(md_enter_child_containers(ctx, n_children));

abort:
    return ret;
}

md_analyze_line in md4c.c in md4c 0.4.7 allows attackers to trigger use of uninitialized memory, and cause a denial of service via a malformed Markdown document.


md_analyze_line(MD_CTX* ctx, OFF beg, OFF* p_end,
                const MD_LINE_ANALYSIS* pivot_line, MD_LINE_ANALYSIS* line)
{
    unsigned total_indent = 0;
    int n_parents = 0;
    int n_brothers = 0;
    int n_children = 0;
    MD_CONTAINER container = { 0 };
    int prev_line_has_list_loosening_effect = ctx->last_line_has_list_loosening_effect;
    OFF off = beg;
    OFF hr_killer = 0;
    int ret = 0;

    line->indent = md_line_indentation(ctx, total_indent, off, &off);
    total_indent += line->indent;
    line->beg = off;

    /* Given the indentation and block quote marks '>', determine how many of
     * the current containers are our parents. */
    while(n_parents < ctx->n_containers) {
        MD_CONTAINER* c = &ctx->containers[n_parents];

        if(c->ch == _T('>')  &&  line->indent < ctx->code_indent_offset  &&
            off < ctx->size  &&  CH(off) == _T('>'))
        {
            /* Block quote mark. */
            off++;
            total_indent++;
            line->indent = md_line_indentation(ctx, total_indent, off, &off);
            total_indent += line->indent;

            /* The optional 1st space after '>' is part of the block quote mark. */
            if(line->indent > 0)
                line->indent--;

            line->beg = off;

        } else if(c->ch != _T('>')  &&  line->indent >= c->contents_indent) {
            /* List. */
            line->indent -= c->contents_indent;
        } else {
            break;
        }

        n_parents++;
    }

    if(off >= ctx->size  ||  ISNEWLINE(off)) {
        /* Blank line does not need any real indentation to be nested inside
         * a list. */
        if(n_brothers + n_children == 0) {
            while(n_parents < ctx->n_containers  &&  ctx->containers[n_parents].ch != _T('>'))
                n_parents++;
        }
    }

    while(TRUE) {
        /* Check whether we are fenced code continuation. */
        if(pivot_line->type == MD_LINE_FENCEDCODE) {
            line->beg = off;

            /* We are another MD_LINE_FENCEDCODE unless we are closing fence
             * which we transform into MD_LINE_BLANK. */
            if(line->indent < ctx->code_indent_offset) {
                if(md_is_closing_code_fence(ctx, CH(pivot_line->beg), off, &off)) {
                    line->type = MD_LINE_BLANK;
                    ctx->last_line_has_list_loosening_effect = FALSE;
                    break;
                }
            }

            /* Change indentation accordingly to the initial code fence. */
            if(n_parents == ctx->n_containers) {
                if(line->indent > pivot_line->indent)
                    line->indent -= pivot_line->indent;
                else
                    line->indent = 0;

                line->type = MD_LINE_FENCEDCODE;
                break;
            }
        }

        /* Check whether we are HTML block continuation. */
        if(pivot_line->type == MD_LINE_HTML  &&  ctx->html_block_type > 0) {
            if(n_parents < ctx->n_containers) {
                /* HTML block is implicitly ended if the enclosing container
                 * block ends. */
                ctx->html_block_type = 0;
            } else {
                int html_block_type;

                html_block_type = md_is_html_block_end_condition(ctx, off, &off);
                if(html_block_type > 0) {
                    MD_ASSERT(html_block_type == ctx->html_block_type);

                    /* Make sure this is the last line of the block. */
                    ctx->html_block_type = 0;

                    /* Some end conditions serve as blank lines at the same time. */
                    if(html_block_type == 6 || html_block_type == 7) {
                        line->type = MD_LINE_BLANK;
                        line->indent = 0;
                        break;
                    }
                }

                line->type = MD_LINE_HTML;
                n_parents = ctx->n_containers;
                break;
            }
        }

        /* Check for blank line. */
        if(off >= ctx->size  ||  ISNEWLINE(off)) {
            if(pivot_line->type == MD_LINE_INDENTEDCODE  &&  n_parents == ctx->n_containers) {
                line->type = MD_LINE_INDENTEDCODE;
                if(line->indent > ctx->code_indent_offset)
                    line->indent -= ctx->code_indent_offset;
                else
                    line->indent = 0;
                ctx->last_line_has_list_loosening_effect = FALSE;
            } else {
                line->type = MD_LINE_BLANK;
                ctx->last_line_has_list_loosening_effect = (n_parents > 0  &&
                        n_brothers + n_children == 0  &&
                        ctx->containers[n_parents-1].ch != _T('>'));

    #if 1
                /* See https://github.com/mity/md4c/issues/6
                 *
                 * This ugly checking tests we are in (yet empty) list item but
                 * not its very first line (i.e. not the line with the list
                 * item mark).
                 *
                 * If we are such a blank line, then any following non-blank
                 * line which would be part of the list item actually has to
                 * end the list because according to the specification, "a list
                 * item can begin with at most one blank line."
                 */
                if(n_parents > 0  &&  ctx->containers[n_parents-1].ch != _T('>')  &&
                   n_brothers + n_children == 0  &&  ctx->current_block == NULL  &&
                   ctx->n_block_bytes > (int) sizeof(MD_BLOCK))
                {
                    MD_BLOCK* top_block = (MD_BLOCK*) ((char*)ctx->block_bytes + ctx->n_block_bytes - sizeof(MD_BLOCK));
                    if(top_block->type == MD_BLOCK_LI)
                        ctx->last_list_item_starts_with_two_blank_lines = TRUE;
                }
    #endif
            }
            break;
        } else {
    #if 1
            /* This is the 2nd half of the hack. If the flag is set (i.e. there
             * was a 2nd blank line at the beginning of the list item) and if
             * we would otherwise still belong to the list item, we enforce
             * the end of the list. */
            ctx->last_line_has_list_loosening_effect = FALSE;
            if(ctx->last_list_item_starts_with_two_blank_lines) {
                if(n_parents > 0  &&  ctx->containers[n_parents-1].ch != _T('>')  &&
                   n_brothers + n_children == 0  &&  ctx->current_block == NULL  &&
                   ctx->n_block_bytes > (int) sizeof(MD_BLOCK))
                {
                    MD_BLOCK* top_block = (MD_BLOCK*) ((char*)ctx->block_bytes + ctx->n_block_bytes - sizeof(MD_BLOCK));
                    if(top_block->type == MD_BLOCK_LI)
                        n_parents--;
                }

                ctx->last_list_item_starts_with_two_blank_lines = FALSE;
            }
    #endif
        }

        /* Check whether we are Setext underline. */
        if(line->indent < ctx->code_indent_offset  &&  pivot_line->type == MD_LINE_TEXT
            &&  off < ctx->size  &&  ISANYOF2(off, _T('='), _T('-'))
            &&  (n_parents == ctx->n_containers))
        {
            unsigned level;

            if(md_is_setext_underline(ctx, off, &off, &level)) {
                line->type = MD_LINE_SETEXTUNDERLINE;
                line->data = level;
                break;
            }
        }

        /* Check for thematic break line. */
        if(line->indent < ctx->code_indent_offset
            &&  off < ctx->size  &&  off >= hr_killer
            &&  ISANYOF(off, _T("-_*")))
        {
            if(md_is_hr_line(ctx, off, &off, &hr_killer)) {
                line->type = MD_LINE_HR;
                break;
            }
        }

        /* Check for "brother" container. I.e. whether we are another list item
         * in already started list. */
        if(n_parents < ctx->n_containers  &&  n_brothers + n_children == 0) {
            OFF tmp;

            if(md_is_container_mark(ctx, line->indent, off, &tmp, &container)  &&
               md_is_container_compatible(&ctx->containers[n_parents], &container))
            {
                pivot_line = &md_dummy_blank_line;

                off = tmp;

                total_indent += container.contents_indent - container.mark_indent;
                line->indent = md_line_indentation(ctx, total_indent, off, &off);
                total_indent += line->indent;
                line->beg = off;

                /* Some of the following whitespace actually still belongs to the mark. */
                if(off >= ctx->size || ISNEWLINE(off)) {
                    container.contents_indent++;
                } else if(line->indent <= ctx->code_indent_offset) {
                    container.contents_indent += line->indent;
                    line->indent = 0;
                } else {
                    container.contents_indent += 1;
                    line->indent--;
                }

                ctx->containers[n_parents].mark_indent = container.mark_indent;
                ctx->containers[n_parents].contents_indent = container.contents_indent;

                n_brothers++;
                continue;
            }
        }

        /* Check for indented code.
         * Note indented code block cannot interrupt a paragraph. */
        if(line->indent >= ctx->code_indent_offset  &&
            (pivot_line->type == MD_LINE_BLANK || pivot_line->type == MD_LINE_INDENTEDCODE))
        {
            line->type = MD_LINE_INDENTEDCODE;
            MD_ASSERT(line->indent >= ctx->code_indent_offset);
            line->indent -= ctx->code_indent_offset;
            line->data = 0;
            break;
        }

        /* Check for start of a new container block. */
        if(line->indent < ctx->code_indent_offset  &&
           md_is_container_mark(ctx, line->indent, off, &off, &container))
        {
            if(pivot_line->type == MD_LINE_TEXT  &&  n_parents == ctx->n_containers  &&
                        (off >= ctx->size || ISNEWLINE(off))  &&  container.ch != _T('>'))
            {
                /* Noop. List mark followed by a blank line cannot interrupt a paragraph. */
            } else if(pivot_line->type == MD_LINE_TEXT  &&  n_parents == ctx->n_containers  &&
                        ISANYOF2_(container.ch, _T('.'), _T(')'))  &&  container.start != 1)
            {
                /* Noop. Ordered list cannot interrupt a paragraph unless the start index is 1. */
            } else {
                total_indent += container.contents_indent - container.mark_indent;
                line->indent = md_line_indentation(ctx, total_indent, off, &off);
                total_indent += line->indent;

                line->beg = off;
                line->data = container.ch;

                /* Some of the following whitespace actually still belongs to the mark. */
                if(off >= ctx->size || ISNEWLINE(off)) {
                    container.contents_indent++;
                } else if(line->indent <= ctx->code_indent_offset) {
                    container.contents_indent += line->indent;
                    line->indent = 0;
                } else {
                    container.contents_indent += 1;
                    line->indent--;
                }

                if(n_brothers + n_children == 0)
                    pivot_line = &md_dummy_blank_line;

                if(n_children == 0)
                    MD_CHECK(md_leave_child_containers(ctx, n_parents + n_brothers));

                n_children++;
                MD_CHECK(md_push_container(ctx, &container));
                continue;
            }
        }

        /* Check whether we are table continuation. */
        if(pivot_line->type == MD_LINE_TABLE  &&  n_parents == ctx->n_containers) {
            line->type = MD_LINE_TABLE;
            break;
        }

        /* Check for ATX header. */
        if(line->indent < ctx->code_indent_offset  &&
                off < ctx->size  &&  CH(off) == _T('#'))
        {
            unsigned level;

            if(md_is_atxheader_line(ctx, off, &line->beg, &off, &level)) {
                line->type = MD_LINE_ATXHEADER;
                line->data = level;
                break;
            }
        }

        /* Check whether we are starting code fence. */
        if(off < ctx->size  &&  ISANYOF2(off, _T('`'), _T('~'))) {
            if(md_is_opening_code_fence(ctx, off, &off)) {
                line->type = MD_LINE_FENCEDCODE;
                line->data = 1;
                break;
            }
        }

        /* Check for start of raw HTML block. */
        if(off < ctx->size  &&  CH(off) == _T('<')
            &&  !(ctx->parser.flags & MD_FLAG_NOHTMLBLOCKS))
        {
            ctx->html_block_type = md_is_html_block_start_condition(ctx, off);

            /* HTML block type 7 cannot interrupt paragraph. */
            if(ctx->html_block_type == 7  &&  pivot_line->type == MD_LINE_TEXT)
                ctx->html_block_type = 0;

            if(ctx->html_block_type > 0) {
                /* The line itself also may immediately close the block. */
                if(md_is_html_block_end_condition(ctx, off, &off) == ctx->html_block_type) {
                    /* Make sure this is the last line of the block. */
                    ctx->html_block_type = 0;
                }

                line->type = MD_LINE_HTML;
                break;
            }
        }

        /* Check for table underline. */
        if((ctx->parser.flags & MD_FLAG_TABLES)  &&  pivot_line->type == MD_LINE_TEXT
            &&  off < ctx->size  &&  ISANYOF3(off, _T('|'), _T('-'), _T(':'))
            &&  n_parents == ctx->n_containers)
        {
            unsigned col_count;

            if(ctx->current_block != NULL  &&  ctx->current_block->n_lines == 1  &&
                md_is_table_underline(ctx, off, &off, &col_count))
            {
                line->data = col_count;
                line->type = MD_LINE_TABLEUNDERLINE;
                break;
            }
        }

        /* By default, we are normal text line. */
        line->type = MD_LINE_TEXT;
        if(pivot_line->type == MD_LINE_TEXT  &&  n_brothers + n_children == 0) {
            /* Lazy continuation. */
            n_parents = ctx->n_containers;
        }

        /* Check for task mark. */
        if((ctx->parser.flags & MD_FLAG_TASKLISTS)  &&  n_brothers + n_children > 0  &&
           ISANYOF_(ctx->containers[ctx->n_containers-1].ch, _T("-+*.)")))
        {
            OFF tmp = off;

            while(tmp < ctx->size  &&  tmp < off + 3  &&  ISBLANK(tmp))
                tmp++;
            if(tmp + 2 < ctx->size  &&  CH(tmp) == _T('[')  &&
               ISANYOF(tmp+1, _T("xX "))  &&  CH(tmp+2) == _T(']')  &&
               (tmp + 3 == ctx->size  ||  ISBLANK(tmp+3)  ||  ISNEWLINE(tmp+3)))
            {
                MD_CONTAINER* task_container = (n_children > 0 ? &ctx->containers[ctx->n_containers-1] : &container);
                task_container->is_task = TRUE;
                task_container->task_mark_off = tmp + 1;
                off = tmp + 3;
                while(ISWHITESPACE(off))
                    off++;
                line->beg = off;
            }
        }

        break;
    }

    /* Scan for end of the line.
     *
     * Note this is quite a bottleneck of the parsing as we here iterate almost
     * over compete document.
     */
#if defined __linux__ && !defined MD4C_USE_UTF16
    /* Recent glibc versions have superbly optimized strcspn(), even using
     * vectorization if available. */
    if(ctx->doc_ends_with_newline  &&  off < ctx->size) {
        while(TRUE) {
            off += (OFF) strcspn(STR(off), "\r\n");

            /* strcspn() can stop on zero terminator; but that can appear
             * anywhere in the Markfown input... */
            if(CH(off) == _T('\0'))
                off++;
            else
                break;
        }
    } else
#endif
    {
        /* Optimization: Use some loop unrolling. */
        while(off + 3 < ctx->size  &&  !ISNEWLINE(off+0)  &&  !ISNEWLINE(off+1)
                                   &&  !ISNEWLINE(off+2)  &&  !ISNEWLINE(off+3))
            off += 4;
        while(off < ctx->size  &&  !ISNEWLINE(off))
            off++;
    }

    /* Set end of the line. */
    line->end = off;

    /* But for ATX header, we should exclude the optional trailing mark. */
    if(line->type == MD_LINE_ATXHEADER) {
        OFF tmp = line->end;
        while(tmp > line->beg && CH(tmp-1) == _T(' '))
            tmp--;
        while(tmp > line->beg && CH(tmp-1) == _T('#'))
            tmp--;
        if(tmp == line->beg || CH(tmp-1) == _T(' ') || (ctx->parser.flags & MD_FLAG_PERMISSIVEATXHEADERS))
            line->end = tmp;
    }

    /* Trim trailing spaces. */
    if(line->type != MD_LINE_INDENTEDCODE  &&  line->type != MD_LINE_FENCEDCODE) {
        while(line->end > line->beg && CH(line->end-1) == _T(' '))
            line->end--;
    }

    /* Eat also the new line. */
    if(off < ctx->size && CH(off) == _T('\r'))
        off++;
    if(off < ctx->size && CH(off) == _T('\n'))
        off++;

    *p_end = off;

    /* If we belong to a list after seeing a blank line, the list is loose. */
    if(prev_line_has_list_loosening_effect  &&  line->type != MD_LINE_BLANK  &&  n_parents + n_brothers > 0) {
        MD_CONTAINER* c = &ctx->containers[n_parents + n_brothers - 1];
        if(c->ch != _T('>')) {
            MD_BLOCK* block = (MD_BLOCK*) (((char*)ctx->block_bytes) + c->block_byte_off);
            block->flags |= MD_BLOCK_LOOSE_LIST;
        }
    }

    /* Leave any containers we are not part of anymore. */
    if(n_children == 0  &&  n_parents + n_brothers < ctx->n_containers)
        MD_CHECK(md_leave_child_containers(ctx, n_parents + n_brothers));

    /* Enter any container we found a mark for. */
    if(n_brothers > 0) {
        MD_ASSERT(n_brothers == 1);
        MD_CHECK(md_push_container_bytes(ctx, MD_BLOCK_LI,
                    ctx->containers[n_parents].task_mark_off,
                    (ctx->containers[n_parents].is_task ? CH(ctx->containers[n_parents].task_mark_off) : 0),
                    MD_BLOCK_CONTAINER_CLOSER));
        MD_CHECK(md_push_container_bytes(ctx, MD_BLOCK_LI,
                    container.task_mark_off,
                    (container.is_task ? CH(container.task_mark_off) : 0),
                    MD_BLOCK_CONTAINER_OPENER));
        ctx->containers[n_parents].is_task = container.is_task;
        ctx->containers[n_parents].task_mark_off = container.task_mark_off;
    }

    if(n_children > 0)
        MD_CHECK(md_enter_child_containers(ctx, n_children));

abort:
    return ret;
}


ssize_t nghttp2_session_mem_recv(nghttp2_session *session, const uint8_t *in,
                                 size_t inlen) {
  const uint8_t *first = in, *last = in + inlen;
  nghttp2_inbound_frame *iframe = &session->iframe;
  size_t readlen;
  ssize_t padlen;
  int rv;
  int busy = 0;
  nghttp2_frame_hd cont_hd;
  nghttp2_stream *stream;
  size_t pri_fieldlen;
  nghttp2_mem *mem;

  DEBUGF("recv: connection recv_window_size=%d, local_window=%d\n",
         session->recv_window_size, session->local_window_size);

  mem = &session->mem;

  /* We may have idle streams more than we expect (e.g.,
     nghttp2_session_change_stream_priority() or
     nghttp2_session_create_idle_stream()).  Adjust them here. */
  rv = nghttp2_session_adjust_idle_stream(session);
  if (nghttp2_is_fatal(rv)) {
    return rv;
  }

  if (!nghttp2_session_want_read(session)) {
    return (ssize_t)inlen;
  }

  for (;;) {
    switch (iframe->state) {
    case NGHTTP2_IB_READ_CLIENT_MAGIC:
      readlen = nghttp2_min(inlen, iframe->payloadleft);

      if (memcmp(&NGHTTP2_CLIENT_MAGIC[NGHTTP2_CLIENT_MAGIC_LEN -
                                       iframe->payloadleft],
                 in, readlen) != 0) {
        return NGHTTP2_ERR_BAD_CLIENT_MAGIC;
      }

      iframe->payloadleft -= readlen;
      in += readlen;

      if (iframe->payloadleft == 0) {
        session_inbound_frame_reset(session);
        iframe->state = NGHTTP2_IB_READ_FIRST_SETTINGS;
      }

      break;
    case NGHTTP2_IB_READ_FIRST_SETTINGS:
      DEBUGF("recv: [IB_READ_FIRST_SETTINGS]\n");

      readlen = inbound_frame_buf_read(iframe, in, last);
      in += readlen;

      if (nghttp2_buf_mark_avail(&iframe->sbuf)) {
        return in - first;
      }

      if (iframe->sbuf.pos[3] != NGHTTP2_SETTINGS ||
          (iframe->sbuf.pos[4] & NGHTTP2_FLAG_ACK)) {
        rv = session_call_error_callback(
            session, NGHTTP2_ERR_SETTINGS_EXPECTED,
            "Remote peer returned unexpected data while we expected "
            "SETTINGS frame.  Perhaps, peer does not support HTTP/2 "
            "properly.");

        if (nghttp2_is_fatal(rv)) {
          return rv;
        }

        rv = nghttp2_session_terminate_session_with_reason(
            session, NGHTTP2_PROTOCOL_ERROR, "SETTINGS expected");

        if (nghttp2_is_fatal(rv)) {
          return rv;
        }

        return (ssize_t)inlen;
      }

      iframe->state = NGHTTP2_IB_READ_HEAD;

    /* Fall through */
    case NGHTTP2_IB_READ_HEAD: {
      int on_begin_frame_called = 0;

      DEBUGF("recv: [IB_READ_HEAD]\n");

      readlen = inbound_frame_buf_read(iframe, in, last);
      in += readlen;

      if (nghttp2_buf_mark_avail(&iframe->sbuf)) {
        return in - first;
      }

      nghttp2_frame_unpack_frame_hd(&iframe->frame.hd, iframe->sbuf.pos);
      iframe->payloadleft = iframe->frame.hd.length;

      DEBUGF("recv: payloadlen=%zu, type=%u, flags=0x%02x, stream_id=%d\n",
             iframe->frame.hd.length, iframe->frame.hd.type,
             iframe->frame.hd.flags, iframe->frame.hd.stream_id);

      if (iframe->frame.hd.length > session->local_settings.max_frame_size) {
        DEBUGF("recv: length is too large %zu > %u\n", iframe->frame.hd.length,
               session->local_settings.max_frame_size);

        rv = nghttp2_session_terminate_session_with_reason(
            session, NGHTTP2_FRAME_SIZE_ERROR, "too large frame size");

        if (nghttp2_is_fatal(rv)) {
          return rv;
        }

        return (ssize_t)inlen;
      }

      switch (iframe->frame.hd.type) {
      case NGHTTP2_DATA: {
        DEBUGF("recv: DATA\n");

        iframe->frame.hd.flags &=
            (NGHTTP2_FLAG_END_STREAM | NGHTTP2_FLAG_PADDED);
        /* Check stream is open. If it is not open or closing,
           ignore payload. */
        busy = 1;

        rv = session_on_data_received_fail_fast(session);
        if (iframe->state == NGHTTP2_IB_IGN_ALL) {
          return (ssize_t)inlen;
        }
        if (rv == NGHTTP2_ERR_IGN_PAYLOAD) {
          DEBUGF("recv: DATA not allowed stream_id=%d\n",
                 iframe->frame.hd.stream_id);
          iframe->state = NGHTTP2_IB_IGN_DATA;
          break;
        }

        if (nghttp2_is_fatal(rv)) {
          return rv;
        }

        rv = inbound_frame_handle_pad(iframe, &iframe->frame.hd);
        if (rv < 0) {
          rv = nghttp2_session_terminate_session_with_reason(
              session, NGHTTP2_PROTOCOL_ERROR,
              "DATA: insufficient padding space");

          if (nghttp2_is_fatal(rv)) {
            return rv;
          }
          return (ssize_t)inlen;
        }

        if (rv == 1) {
          iframe->state = NGHTTP2_IB_READ_PAD_DATA;
          break;
        }

        iframe->state = NGHTTP2_IB_READ_DATA;
        break;
      }
      case NGHTTP2_HEADERS:

        DEBUGF("recv: HEADERS\n");

        iframe->frame.hd.flags &=
            (NGHTTP2_FLAG_END_STREAM | NGHTTP2_FLAG_END_HEADERS |
             NGHTTP2_FLAG_PADDED | NGHTTP2_FLAG_PRIORITY);

        rv = inbound_frame_handle_pad(iframe, &iframe->frame.hd);
        if (rv < 0) {
          rv = nghttp2_session_terminate_session_with_reason(
              session, NGHTTP2_PROTOCOL_ERROR,
              "HEADERS: insufficient padding space");
          if (nghttp2_is_fatal(rv)) {
            return rv;
          }
          return (ssize_t)inlen;
        }

        if (rv == 1) {
          iframe->state = NGHTTP2_IB_READ_NBYTE;
          break;
        }

        pri_fieldlen = nghttp2_frame_priority_len(iframe->frame.hd.flags);

        if (pri_fieldlen > 0) {
          if (iframe->payloadleft < pri_fieldlen) {
            busy = 1;
            iframe->state = NGHTTP2_IB_FRAME_SIZE_ERROR;
            break;
          }

          iframe->state = NGHTTP2_IB_READ_NBYTE;

          inbound_frame_set_mark(iframe, pri_fieldlen);

          break;
        }

        /* Call on_begin_frame_callback here because
           session_process_headers_frame() may call
           on_begin_headers_callback */
        rv = session_call_on_begin_frame(session, &iframe->frame.hd);

        if (nghttp2_is_fatal(rv)) {
          return rv;
        }

        on_begin_frame_called = 1;

        rv = session_process_headers_frame(session);
        if (nghttp2_is_fatal(rv)) {
          return rv;
        }

        busy = 1;

        if (iframe->state == NGHTTP2_IB_IGN_ALL) {
          return (ssize_t)inlen;
        }

        if (rv == NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE) {
          rv = nghttp2_session_add_rst_stream(
              session, iframe->frame.hd.stream_id, NGHTTP2_INTERNAL_ERROR);
          if (nghttp2_is_fatal(rv)) {
            return rv;
          }
          iframe->state = NGHTTP2_IB_IGN_HEADER_BLOCK;
          break;
        }

        if (rv == NGHTTP2_ERR_IGN_HEADER_BLOCK) {
          iframe->state = NGHTTP2_IB_IGN_HEADER_BLOCK;
          break;
        }

        iframe->state = NGHTTP2_IB_READ_HEADER_BLOCK;

        break;
      case NGHTTP2_PRIORITY:
        DEBUGF("recv: PRIORITY\n");

        iframe->frame.hd.flags = NGHTTP2_FLAG_NONE;

        if (iframe->payloadleft != NGHTTP2_PRIORITY_SPECLEN) {
          busy = 1;

          iframe->state = NGHTTP2_IB_FRAME_SIZE_ERROR;

          break;
        }

        iframe->state = NGHTTP2_IB_READ_NBYTE;

        inbound_frame_set_mark(iframe, NGHTTP2_PRIORITY_SPECLEN);

        break;
      case NGHTTP2_RST_STREAM:
      case NGHTTP2_WINDOW_UPDATE:
#ifdef DEBUGBUILD
        switch (iframe->frame.hd.type) {
        case NGHTTP2_RST_STREAM:
          DEBUGF("recv: RST_STREAM\n");
          break;
        case NGHTTP2_WINDOW_UPDATE:
          DEBUGF("recv: WINDOW_UPDATE\n");
          break;
        }
#endif /* DEBUGBUILD */

        iframe->frame.hd.flags = NGHTTP2_FLAG_NONE;

        if (iframe->payloadleft != 4) {
          busy = 1;
          iframe->state = NGHTTP2_IB_FRAME_SIZE_ERROR;
          break;
        }

        iframe->state = NGHTTP2_IB_READ_NBYTE;

        inbound_frame_set_mark(iframe, 4);

        break;
      case NGHTTP2_SETTINGS:
        DEBUGF("recv: SETTINGS\n");

        iframe->frame.hd.flags &= NGHTTP2_FLAG_ACK;

        if ((iframe->frame.hd.length % NGHTTP2_FRAME_SETTINGS_ENTRY_LENGTH) ||
            ((iframe->frame.hd.flags & NGHTTP2_FLAG_ACK) &&
             iframe->payloadleft > 0)) {
          busy = 1;
          iframe->state = NGHTTP2_IB_FRAME_SIZE_ERROR;
          break;
        }

        iframe->state = NGHTTP2_IB_READ_SETTINGS;

        if (iframe->payloadleft) {
          nghttp2_settings_entry *min_header_table_size_entry;

          /* We allocate iv with additional one entry, to store the
             minimum header table size. */
          iframe->max_niv =
              iframe->frame.hd.length / NGHTTP2_FRAME_SETTINGS_ENTRY_LENGTH + 1;

          if (iframe->max_niv - 1 > session->max_settings) {
            rv = nghttp2_session_terminate_session_with_reason(
                session, NGHTTP2_ENHANCE_YOUR_CALM,
                "SETTINGS: too many setting entries");
            if (nghttp2_is_fatal(rv)) {
              return rv;
            }
            return (ssize_t)inlen;
          }

          iframe->iv = nghttp2_mem_malloc(mem, sizeof(nghttp2_settings_entry) *
                                                   iframe->max_niv);

          if (!iframe->iv) {
            return NGHTTP2_ERR_NOMEM;
          }

          min_header_table_size_entry = &iframe->iv[iframe->max_niv - 1];
          min_header_table_size_entry->settings_id =
              NGHTTP2_SETTINGS_HEADER_TABLE_SIZE;
          min_header_table_size_entry->value = UINT32_MAX;

          inbound_frame_set_mark(iframe, NGHTTP2_FRAME_SETTINGS_ENTRY_LENGTH);
          break;
        }

        busy = 1;

        inbound_frame_set_mark(iframe, 0);

        break;
      case NGHTTP2_PUSH_PROMISE:
        DEBUGF("recv: PUSH_PROMISE\n");

        iframe->frame.hd.flags &=
            (NGHTTP2_FLAG_END_HEADERS | NGHTTP2_FLAG_PADDED);

        rv = inbound_frame_handle_pad(iframe, &iframe->frame.hd);
        if (rv < 0) {
          rv = nghttp2_session_terminate_session_with_reason(
              session, NGHTTP2_PROTOCOL_ERROR,
              "PUSH_PROMISE: insufficient padding space");
          if (nghttp2_is_fatal(rv)) {
            return rv;
          }
          return (ssize_t)inlen;
        }

        if (rv == 1) {
          iframe->state = NGHTTP2_IB_READ_NBYTE;
          break;
        }

        if (iframe->payloadleft < 4) {
          busy = 1;
          iframe->state = NGHTTP2_IB_FRAME_SIZE_ERROR;
          break;
        }

        iframe->state = NGHTTP2_IB_READ_NBYTE;

        inbound_frame_set_mark(iframe, 4);

        break;
      case NGHTTP2_PING:
        DEBUGF("recv: PING\n");

        iframe->frame.hd.flags &= NGHTTP2_FLAG_ACK;

        if (iframe->payloadleft != 8) {
          busy = 1;
          iframe->state = NGHTTP2_IB_FRAME_SIZE_ERROR;
          break;
        }

        iframe->state = NGHTTP2_IB_READ_NBYTE;
        inbound_frame_set_mark(iframe, 8);

        break;
      case NGHTTP2_GOAWAY:
        DEBUGF("recv: GOAWAY\n");

        iframe->frame.hd.flags = NGHTTP2_FLAG_NONE;

        if (iframe->payloadleft < 8) {
          busy = 1;
          iframe->state = NGHTTP2_IB_FRAME_SIZE_ERROR;
          break;
        }

        iframe->state = NGHTTP2_IB_READ_NBYTE;
        inbound_frame_set_mark(iframe, 8);

        break;
      case NGHTTP2_CONTINUATION:
        DEBUGF("recv: unexpected CONTINUATION\n");

        /* Receiving CONTINUATION in this state are subject to
           connection error of type PROTOCOL_ERROR */
        rv = nghttp2_session_terminate_session_with_reason(
            session, NGHTTP2_PROTOCOL_ERROR, "CONTINUATION: unexpected");
        if (nghttp2_is_fatal(rv)) {
          return rv;
        }

        return (ssize_t)inlen;
      default:
        DEBUGF("recv: extension frame\n");

        if (check_ext_type_set(session->user_recv_ext_types,
                               iframe->frame.hd.type)) {
          if (!session->callbacks.unpack_extension_callback) {
            /* Silently ignore unknown frame type. */

            busy = 1;

            iframe->state = NGHTTP2_IB_IGN_PAYLOAD;

            break;
          }

          busy = 1;

          iframe->state = NGHTTP2_IB_READ_EXTENSION_PAYLOAD;

          break;
        } else {
          switch (iframe->frame.hd.type) {
          case NGHTTP2_ALTSVC:
            if ((session->builtin_recv_ext_types & NGHTTP2_TYPEMASK_ALTSVC) ==
                0) {
              busy = 1;
              iframe->state = NGHTTP2_IB_IGN_PAYLOAD;
              break;
            }

            DEBUGF("recv: ALTSVC\n");

            iframe->frame.hd.flags = NGHTTP2_FLAG_NONE;
            iframe->frame.ext.payload = &iframe->ext_frame_payload.altsvc;

            if (session->server) {
              busy = 1;
              iframe->state = NGHTTP2_IB_IGN_PAYLOAD;
              break;
            }

            if (iframe->payloadleft < 2) {
              busy = 1;
              iframe->state = NGHTTP2_IB_FRAME_SIZE_ERROR;
              break;
            }

            busy = 1;

            iframe->state = NGHTTP2_IB_READ_NBYTE;
            inbound_frame_set_mark(iframe, 2);

            break;
          case NGHTTP2_ORIGIN:
            if (!(session->builtin_recv_ext_types & NGHTTP2_TYPEMASK_ORIGIN)) {
              busy = 1;
              iframe->state = NGHTTP2_IB_IGN_PAYLOAD;
              break;
            }

            DEBUGF("recv: ORIGIN\n");

            iframe->frame.ext.payload = &iframe->ext_frame_payload.origin;

            if (session->server || iframe->frame.hd.stream_id ||
                (iframe->frame.hd.flags & 0xf0)) {
              busy = 1;
              iframe->state = NGHTTP2_IB_IGN_PAYLOAD;
              break;
            }

            iframe->frame.hd.flags = NGHTTP2_FLAG_NONE;

            if (iframe->payloadleft) {
              iframe->raw_lbuf = nghttp2_mem_malloc(mem, iframe->payloadleft);

              if (iframe->raw_lbuf == NULL) {
                return NGHTTP2_ERR_NOMEM;
              }

              nghttp2_buf_wrap_init(&iframe->lbuf, iframe->raw_lbuf,
                                    iframe->payloadleft);
            } else {
              busy = 1;
            }

            iframe->state = NGHTTP2_IB_READ_ORIGIN_PAYLOAD;

            break;
          default:
            busy = 1;

            iframe->state = NGHTTP2_IB_IGN_PAYLOAD;

            break;
          }
        }
      }

      if (!on_begin_frame_called) {
        switch (iframe->state) {
        case NGHTTP2_IB_IGN_HEADER_BLOCK:
        case NGHTTP2_IB_IGN_PAYLOAD:
        case NGHTTP2_IB_FRAME_SIZE_ERROR:
        case NGHTTP2_IB_IGN_DATA:
        case NGHTTP2_IB_IGN_ALL:
          break;
        default:
          rv = session_call_on_begin_frame(session, &iframe->frame.hd);

          if (nghttp2_is_fatal(rv)) {
            return rv;
          }
        }
      }

      break;
    }
    case NGHTTP2_IB_READ_NBYTE:
      DEBUGF("recv: [IB_READ_NBYTE]\n");

      readlen = inbound_frame_buf_read(iframe, in, last);
      in += readlen;
      iframe->payloadleft -= readlen;

      DEBUGF("recv: readlen=%zu, payloadleft=%zu, left=%zd\n", readlen,
             iframe->payloadleft, nghttp2_buf_mark_avail(&iframe->sbuf));

      if (nghttp2_buf_mark_avail(&iframe->sbuf)) {
        return in - first;
      }

      switch (iframe->frame.hd.type) {
      case NGHTTP2_HEADERS:
        if (iframe->padlen == 0 &&
            (iframe->frame.hd.flags & NGHTTP2_FLAG_PADDED)) {
          pri_fieldlen = nghttp2_frame_priority_len(iframe->frame.hd.flags);
          padlen = inbound_frame_compute_pad(iframe);
          if (padlen < 0 ||
              (size_t)padlen + pri_fieldlen > 1 + iframe->payloadleft) {
            rv = nghttp2_session_terminate_session_with_reason(
                session, NGHTTP2_PROTOCOL_ERROR, "HEADERS: invalid padding");
            if (nghttp2_is_fatal(rv)) {
              return rv;
            }
            return (ssize_t)inlen;
          }
          iframe->frame.headers.padlen = (size_t)padlen;

          if (pri_fieldlen > 0) {
            if (iframe->payloadleft < pri_fieldlen) {
              busy = 1;
              iframe->state = NGHTTP2_IB_FRAME_SIZE_ERROR;
              break;
            }
            iframe->state = NGHTTP2_IB_READ_NBYTE;
            inbound_frame_set_mark(iframe, pri_fieldlen);
            break;
          } else {
            /* Truncate buffers used for padding spec */
            inbound_frame_set_mark(iframe, 0);
          }
        }

        rv = session_process_headers_frame(session);
        if (nghttp2_is_fatal(rv)) {
          return rv;
        }

        busy = 1;

        if (iframe->state == NGHTTP2_IB_IGN_ALL) {
          return (ssize_t)inlen;
        }

        if (rv == NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE) {
          rv = nghttp2_session_add_rst_stream(
              session, iframe->frame.hd.stream_id, NGHTTP2_INTERNAL_ERROR);
          if (nghttp2_is_fatal(rv)) {
            return rv;
          }
          iframe->state = NGHTTP2_IB_IGN_HEADER_BLOCK;
          break;
        }

        if (rv == NGHTTP2_ERR_IGN_HEADER_BLOCK) {
          iframe->state = NGHTTP2_IB_IGN_HEADER_BLOCK;
          break;
        }

        iframe->state = NGHTTP2_IB_READ_HEADER_BLOCK;

        break;
      case NGHTTP2_PRIORITY:
        rv = session_process_priority_frame(session);
        if (nghttp2_is_fatal(rv)) {
          return rv;
        }

        if (iframe->state == NGHTTP2_IB_IGN_ALL) {
          return (ssize_t)inlen;
        }

        session_inbound_frame_reset(session);

        break;
      case NGHTTP2_RST_STREAM:
        rv = session_process_rst_stream_frame(session);
        if (nghttp2_is_fatal(rv)) {
          return rv;
        }

        if (iframe->state == NGHTTP2_IB_IGN_ALL) {
          return (ssize_t)inlen;
        }

        session_inbound_frame_reset(session);

        break;
      case NGHTTP2_PUSH_PROMISE:
        if (iframe->padlen == 0 &&
            (iframe->frame.hd.flags & NGHTTP2_FLAG_PADDED)) {
          padlen = inbound_frame_compute_pad(iframe);
          if (padlen < 0 || (size_t)padlen + 4 /* promised stream id */
                                > 1 + iframe->payloadleft) {
            rv = nghttp2_session_terminate_session_with_reason(
                session, NGHTTP2_PROTOCOL_ERROR,
                "PUSH_PROMISE: invalid padding");
            if (nghttp2_is_fatal(rv)) {
              return rv;
            }
            return (ssize_t)inlen;
          }

          iframe->frame.push_promise.padlen = (size_t)padlen;

          if (iframe->payloadleft < 4) {
            busy = 1;
            iframe->state = NGHTTP2_IB_FRAME_SIZE_ERROR;
            break;
          }

          iframe->state = NGHTTP2_IB_READ_NBYTE;

          inbound_frame_set_mark(iframe, 4);

          break;
        }

        rv = session_process_push_promise_frame(session);
        if (nghttp2_is_fatal(rv)) {
          return rv;
        }

        busy = 1;

        if (iframe->state == NGHTTP2_IB_IGN_ALL) {
          return (ssize_t)inlen;
        }

        if (rv == NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE) {
          rv = nghttp2_session_add_rst_stream(
              session, iframe->frame.push_promise.promised_stream_id,
              NGHTTP2_INTERNAL_ERROR);
          if (nghttp2_is_fatal(rv)) {
            return rv;
          }
          iframe->state = NGHTTP2_IB_IGN_HEADER_BLOCK;
          break;
        }

        if (rv == NGHTTP2_ERR_IGN_HEADER_BLOCK) {
          iframe->state = NGHTTP2_IB_IGN_HEADER_BLOCK;
          break;
        }

        iframe->state = NGHTTP2_IB_READ_HEADER_BLOCK;

        break;
      case NGHTTP2_PING:
        rv = session_process_ping_frame(session);
        if (nghttp2_is_fatal(rv)) {
          return rv;
        }

        if (iframe->state == NGHTTP2_IB_IGN_ALL) {
          return (ssize_t)inlen;
        }

        session_inbound_frame_reset(session);

        break;
      case NGHTTP2_GOAWAY: {
        size_t debuglen;

        /* 8 is Last-stream-ID + Error Code */
        debuglen = iframe->frame.hd.length - 8;

        if (debuglen > 0) {
          iframe->raw_lbuf = nghttp2_mem_malloc(mem, debuglen);

          if (iframe->raw_lbuf == NULL) {
            return NGHTTP2_ERR_NOMEM;
          }

          nghttp2_buf_wrap_init(&iframe->lbuf, iframe->raw_lbuf, debuglen);
        }

        busy = 1;

        iframe->state = NGHTTP2_IB_READ_GOAWAY_DEBUG;

        break;
      }
      case NGHTTP2_WINDOW_UPDATE:
        rv = session_process_window_update_frame(session);
        if (nghttp2_is_fatal(rv)) {
          return rv;
        }

        if (iframe->state == NGHTTP2_IB_IGN_ALL) {
          return (ssize_t)inlen;
        }

        session_inbound_frame_reset(session);

        break;
      case NGHTTP2_ALTSVC: {
        size_t origin_len;

        origin_len = nghttp2_get_uint16(iframe->sbuf.pos);

        DEBUGF("recv: origin_len=%zu\n", origin_len);

        if (origin_len > iframe->payloadleft) {
          busy = 1;
          iframe->state = NGHTTP2_IB_FRAME_SIZE_ERROR;
          break;
        }

        if (iframe->frame.hd.length > 2) {
          iframe->raw_lbuf =
              nghttp2_mem_malloc(mem, iframe->frame.hd.length - 2);

          if (iframe->raw_lbuf == NULL) {
            return NGHTTP2_ERR_NOMEM;
          }

          nghttp2_buf_wrap_init(&iframe->lbuf, iframe->raw_lbuf,
                                iframe->frame.hd.length);
        }

        busy = 1;

        iframe->state = NGHTTP2_IB_READ_ALTSVC_PAYLOAD;

        break;
      }
      default:
        /* This is unknown frame */
        session_inbound_frame_reset(session);

        break;
      }
      break;
    case NGHTTP2_IB_READ_HEADER_BLOCK:
    case NGHTTP2_IB_IGN_HEADER_BLOCK: {
      ssize_t data_readlen;
      size_t trail_padlen;
      int final;
#ifdef DEBUGBUILD
      if (iframe->state == NGHTTP2_IB_READ_HEADER_BLOCK) {
        DEBUGF("recv: [IB_READ_HEADER_BLOCK]\n");
      } else {
        DEBUGF("recv: [IB_IGN_HEADER_BLOCK]\n");
      }
#endif /* DEBUGBUILD */

      readlen = inbound_frame_payload_readlen(iframe, in, last);

      DEBUGF("recv: readlen=%zu, payloadleft=%zu\n", readlen,
             iframe->payloadleft - readlen);

      data_readlen = inbound_frame_effective_readlen(
          iframe, iframe->payloadleft - readlen, readlen);

      if (data_readlen == -1) {
        /* everything is padding */
        data_readlen = 0;
      }

      trail_padlen = nghttp2_frame_trail_padlen(&iframe->frame, iframe->padlen);

      final = (iframe->frame.hd.flags & NGHTTP2_FLAG_END_HEADERS) &&
              iframe->payloadleft - (size_t)data_readlen == trail_padlen;

      if (data_readlen > 0 || (data_readlen == 0 && final)) {
        size_t hd_proclen = 0;

        DEBUGF("recv: block final=%d\n", final);

        rv =
            inflate_header_block(session, &iframe->frame, &hd_proclen,
                                 (uint8_t *)in, (size_t)data_readlen, final,
                                 iframe->state == NGHTTP2_IB_READ_HEADER_BLOCK);

        if (nghttp2_is_fatal(rv)) {
          return rv;
        }

        if (iframe->state == NGHTTP2_IB_IGN_ALL) {
          return (ssize_t)inlen;
        }

        if (rv == NGHTTP2_ERR_PAUSE) {
          in += hd_proclen;
          iframe->payloadleft -= hd_proclen;

          return in - first;
        }

        if (rv == NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE) {
          /* The application says no more headers. We decompress the
             rest of the header block but not invoke on_header_callback
             and on_frame_recv_callback. */
          in += hd_proclen;
          iframe->payloadleft -= hd_proclen;

          /* Use promised stream ID for PUSH_PROMISE */
          rv = nghttp2_session_add_rst_stream(
              session,
              iframe->frame.hd.type == NGHTTP2_PUSH_PROMISE
                  ? iframe->frame.push_promise.promised_stream_id
                  : iframe->frame.hd.stream_id,
              NGHTTP2_INTERNAL_ERROR);
          if (nghttp2_is_fatal(rv)) {
            return rv;
          }
          busy = 1;
          iframe->state = NGHTTP2_IB_IGN_HEADER_BLOCK;
          break;
        }

        in += readlen;
        iframe->payloadleft -= readlen;

        if (rv == NGHTTP2_ERR_HEADER_COMP) {
          /* GOAWAY is already issued */
          if (iframe->payloadleft == 0) {
            session_inbound_frame_reset(session);
          } else {
            busy = 1;
            iframe->state = NGHTTP2_IB_IGN_PAYLOAD;
          }
          break;
        }
      } else {
        in += readlen;
        iframe->payloadleft -= readlen;
      }

      if (iframe->payloadleft) {
        break;
      }

      if ((iframe->frame.hd.flags & NGHTTP2_FLAG_END_HEADERS) == 0) {

        inbound_frame_set_mark(iframe, NGHTTP2_FRAME_HDLEN);

        iframe->padlen = 0;

        if (iframe->state == NGHTTP2_IB_READ_HEADER_BLOCK) {
          iframe->state = NGHTTP2_IB_EXPECT_CONTINUATION;
        } else {
          iframe->state = NGHTTP2_IB_IGN_CONTINUATION;
        }
      } else {
        if (iframe->state == NGHTTP2_IB_READ_HEADER_BLOCK) {
          rv = session_after_header_block_received(session);
          if (nghttp2_is_fatal(rv)) {
            return rv;
          }
        }
        session_inbound_frame_reset(session);
      }
      break;
    }
    case NGHTTP2_IB_IGN_PAYLOAD:
      DEBUGF("recv: [IB_IGN_PAYLOAD]\n");

      readlen = inbound_frame_payload_readlen(iframe, in, last);
      iframe->payloadleft -= readlen;
      in += readlen;

      DEBUGF("recv: readlen=%zu, payloadleft=%zu\n", readlen,
             iframe->payloadleft);

      if (iframe->payloadleft) {
        break;
      }

      switch (iframe->frame.hd.type) {
      case NGHTTP2_HEADERS:
      case NGHTTP2_PUSH_PROMISE:
      case NGHTTP2_CONTINUATION:
        /* Mark inflater bad so that we won't perform further decoding */
        session->hd_inflater.ctx.bad = 1;
        break;
      default:
        break;
      }

      session_inbound_frame_reset(session);

      break;
    case NGHTTP2_IB_FRAME_SIZE_ERROR:
      DEBUGF("recv: [IB_FRAME_SIZE_ERROR]\n");

      rv = session_handle_frame_size_error(session);
      if (nghttp2_is_fatal(rv)) {
        return rv;
      }

      assert(iframe->state == NGHTTP2_IB_IGN_ALL);

      return (ssize_t)inlen;
    case NGHTTP2_IB_READ_SETTINGS:
      DEBUGF("recv: [IB_READ_SETTINGS]\n");

      readlen = inbound_frame_buf_read(iframe, in, last);
      iframe->payloadleft -= readlen;
      in += readlen;

      DEBUGF("recv: readlen=%zu, payloadleft=%zu\n", readlen,
             iframe->payloadleft);

      if (nghttp2_buf_mark_avail(&iframe->sbuf)) {
        break;
      }

      if (readlen > 0) {
        inbound_frame_set_settings_entry(iframe);
      }
      if (iframe->payloadleft) {
        inbound_frame_set_mark(iframe, NGHTTP2_FRAME_SETTINGS_ENTRY_LENGTH);
        break;
      }

      rv = session_process_settings_frame(session);

      if (nghttp2_is_fatal(rv)) {
        return rv;
      }

      if (iframe->state == NGHTTP2_IB_IGN_ALL) {
        return (ssize_t)inlen;
      }

      session_inbound_frame_reset(session);

      break;
    case NGHTTP2_IB_READ_GOAWAY_DEBUG:
      DEBUGF("recv: [IB_READ_GOAWAY_DEBUG]\n");

      readlen = inbound_frame_payload_readlen(iframe, in, last);

      if (readlen > 0) {
        iframe->lbuf.last = nghttp2_cpymem(iframe->lbuf.last, in, readlen);

        iframe->payloadleft -= readlen;
        in += readlen;
      }

      DEBUGF("recv: readlen=%zu, payloadleft=%zu\n", readlen,
             iframe->payloadleft);

      if (iframe->payloadleft) {
        assert(nghttp2_buf_avail(&iframe->lbuf) > 0);

        break;
      }

      rv = session_process_goaway_frame(session);

      if (nghttp2_is_fatal(rv)) {
        return rv;
      }

      if (iframe->state == NGHTTP2_IB_IGN_ALL) {
        return (ssize_t)inlen;
      }

      session_inbound_frame_reset(session);

      break;
    case NGHTTP2_IB_EXPECT_CONTINUATION:
    case NGHTTP2_IB_IGN_CONTINUATION:
#ifdef DEBUGBUILD
      if (iframe->state == NGHTTP2_IB_EXPECT_CONTINUATION) {
        fprintf(stderr, "recv: [IB_EXPECT_CONTINUATION]\n");
      } else {
        fprintf(stderr, "recv: [IB_IGN_CONTINUATION]\n");
      }
#endif /* DEBUGBUILD */

      readlen = inbound_frame_buf_read(iframe, in, last);
      in += readlen;

      if (nghttp2_buf_mark_avail(&iframe->sbuf)) {
        return in - first;
      }

      nghttp2_frame_unpack_frame_hd(&cont_hd, iframe->sbuf.pos);
      iframe->payloadleft = cont_hd.length;

      DEBUGF("recv: payloadlen=%zu, type=%u, flags=0x%02x, stream_id=%d\n",
             cont_hd.length, cont_hd.type, cont_hd.flags, cont_hd.stream_id);

      if (cont_hd.type != NGHTTP2_CONTINUATION ||
          cont_hd.stream_id != iframe->frame.hd.stream_id) {
        DEBUGF("recv: expected stream_id=%d, type=%d, but got stream_id=%d, "
               "type=%u\n",
               iframe->frame.hd.stream_id, NGHTTP2_CONTINUATION,
               cont_hd.stream_id, cont_hd.type);
        rv = nghttp2_session_terminate_session_with_reason(
            session, NGHTTP2_PROTOCOL_ERROR,
            "unexpected non-CONTINUATION frame or stream_id is invalid");
        if (nghttp2_is_fatal(rv)) {
          return rv;
        }

        return (ssize_t)inlen;
      }

      /* CONTINUATION won't bear NGHTTP2_PADDED flag */

      iframe->frame.hd.flags = (uint8_t)(
          iframe->frame.hd.flags | (cont_hd.flags & NGHTTP2_FLAG_END_HEADERS));
      iframe->frame.hd.length += cont_hd.length;

      busy = 1;

      if (iframe->state == NGHTTP2_IB_EXPECT_CONTINUATION) {
        iframe->state = NGHTTP2_IB_READ_HEADER_BLOCK;

        rv = session_call_on_begin_frame(session, &cont_hd);

        if (nghttp2_is_fatal(rv)) {
          return rv;
        }
      } else {
        iframe->state = NGHTTP2_IB_IGN_HEADER_BLOCK;
      }

      break;
    case NGHTTP2_IB_READ_PAD_DATA:
      DEBUGF("recv: [IB_READ_PAD_DATA]\n");

      readlen = inbound_frame_buf_read(iframe, in, last);
      in += readlen;
      iframe->payloadleft -= readlen;

      DEBUGF("recv: readlen=%zu, payloadleft=%zu, left=%zu\n", readlen,
             iframe->payloadleft, nghttp2_buf_mark_avail(&iframe->sbuf));

      if (nghttp2_buf_mark_avail(&iframe->sbuf)) {
        return in - first;
      }

      /* Pad Length field is subject to flow control */
      rv = nghttp2_session_update_recv_connection_window_size(session, readlen);
      if (nghttp2_is_fatal(rv)) {
        return rv;
      }

      if (iframe->state == NGHTTP2_IB_IGN_ALL) {
        return (ssize_t)inlen;
      }

      /* Pad Length field is consumed immediately */
      rv =
          nghttp2_session_consume(session, iframe->frame.hd.stream_id, readlen);

      if (nghttp2_is_fatal(rv)) {
        return rv;
      }

      if (iframe->state == NGHTTP2_IB_IGN_ALL) {
        return (ssize_t)inlen;
      }

      stream = nghttp2_session_get_stream(session, iframe->frame.hd.stream_id);
      if (stream) {
        rv = nghttp2_session_update_recv_stream_window_size(
            session, stream, readlen,
            iframe->payloadleft ||
                (iframe->frame.hd.flags & NGHTTP2_FLAG_END_STREAM) == 0);
        if (nghttp2_is_fatal(rv)) {
          return rv;
        }
      }

      busy = 1;

      padlen = inbound_frame_compute_pad(iframe);
      if (padlen < 0) {
        rv = nghttp2_session_terminate_session_with_reason(
            session, NGHTTP2_PROTOCOL_ERROR, "DATA: invalid padding");
        if (nghttp2_is_fatal(rv)) {
          return rv;
        }
        return (ssize_t)inlen;
      }

      iframe->frame.data.padlen = (size_t)padlen;

      iframe->state = NGHTTP2_IB_READ_DATA;

      break;
    case NGHTTP2_IB_READ_DATA:
      stream = nghttp2_session_get_stream(session, iframe->frame.hd.stream_id);

      if (!stream) {
        busy = 1;
        iframe->state = NGHTTP2_IB_IGN_DATA;
        break;
      }

      DEBUGF("recv: [IB_READ_DATA]\n");

      readlen = inbound_frame_payload_readlen(iframe, in, last);
      iframe->payloadleft -= readlen;
      in += readlen;

      DEBUGF("recv: readlen=%zu, payloadleft=%zu\n", readlen,
             iframe->payloadleft);

      if (readlen > 0) {
        ssize_t data_readlen;

        rv = nghttp2_session_update_recv_connection_window_size(session,
                                                                readlen);
        if (nghttp2_is_fatal(rv)) {
          return rv;
        }

        if (iframe->state == NGHTTP2_IB_IGN_ALL) {
          return (ssize_t)inlen;
        }

        rv = nghttp2_session_update_recv_stream_window_size(
            session, stream, readlen,
            iframe->payloadleft ||
                (iframe->frame.hd.flags & NGHTTP2_FLAG_END_STREAM) == 0);
        if (nghttp2_is_fatal(rv)) {
          return rv;
        }

        data_readlen = inbound_frame_effective_readlen(
            iframe, iframe->payloadleft, readlen);

        if (data_readlen == -1) {
          /* everything is padding */
          data_readlen = 0;
        }

        padlen = (ssize_t)readlen - data_readlen;

        if (padlen > 0) {
          /* Padding is considered as "consumed" immediately */
          rv = nghttp2_session_consume(session, iframe->frame.hd.stream_id,
                                       (size_t)padlen);

          if (nghttp2_is_fatal(rv)) {
            return rv;
          }

          if (iframe->state == NGHTTP2_IB_IGN_ALL) {
            return (ssize_t)inlen;
          }
        }

        DEBUGF("recv: data_readlen=%zd\n", data_readlen);

        if (data_readlen > 0) {
          if (session_enforce_http_messaging(session)) {
            if (nghttp2_http_on_data_chunk(stream, (size_t)data_readlen) != 0) {
              if (session->opt_flags & NGHTTP2_OPTMASK_NO_AUTO_WINDOW_UPDATE) {
                /* Consume all data for connection immediately here */
                rv = session_update_connection_consumed_size(
                    session, (size_t)data_readlen);

                if (nghttp2_is_fatal(rv)) {
                  return rv;
                }

                if (iframe->state == NGHTTP2_IB_IGN_DATA) {
                  return (ssize_t)inlen;
                }
              }

              rv = nghttp2_session_add_rst_stream(
                  session, iframe->frame.hd.stream_id, NGHTTP2_PROTOCOL_ERROR);
              if (nghttp2_is_fatal(rv)) {
                return rv;
              }
              busy = 1;
              iframe->state = NGHTTP2_IB_IGN_DATA;
              break;
            }
          }
          if (session->callbacks.on_data_chunk_recv_callback) {
            rv = session->callbacks.on_data_chunk_recv_callback(
                session, iframe->frame.hd.flags, iframe->frame.hd.stream_id,
                in - readlen, (size_t)data_readlen, session->user_data);
            if (rv == NGHTTP2_ERR_PAUSE) {
              return in - first;
            }

            if (nghttp2_is_fatal(rv)) {
              return NGHTTP2_ERR_CALLBACK_FAILURE;
            }
          }
        }
      }

      if (iframe->payloadleft) {
        break;
      }

      rv = session_process_data_frame(session);
      if (nghttp2_is_fatal(rv)) {
        return rv;
      }

      session_inbound_frame_reset(session);

      break;
    case NGHTTP2_IB_IGN_DATA:
      DEBUGF("recv: [IB_IGN_DATA]\n");

      readlen = inbound_frame_payload_readlen(iframe, in, last);
      iframe->payloadleft -= readlen;
      in += readlen;

      DEBUGF("recv: readlen=%zu, payloadleft=%zu\n", readlen,
             iframe->payloadleft);

      if (readlen > 0) {
        /* Update connection-level flow control window for ignored
           DATA frame too */
        rv = nghttp2_session_update_recv_connection_window_size(session,
                                                                readlen);
        if (nghttp2_is_fatal(rv)) {
          return rv;
        }

        if (iframe->state == NGHTTP2_IB_IGN_ALL) {
          return (ssize_t)inlen;
        }

        if (session->opt_flags & NGHTTP2_OPTMASK_NO_AUTO_WINDOW_UPDATE) {

          /* Ignored DATA is considered as "consumed" immediately. */
          rv = session_update_connection_consumed_size(session, readlen);

          if (nghttp2_is_fatal(rv)) {
            return rv;
          }

          if (iframe->state == NGHTTP2_IB_IGN_ALL) {
            return (ssize_t)inlen;
          }
        }
      }

      if (iframe->payloadleft) {
        break;
      }

      session_inbound_frame_reset(session);

      break;
    case NGHTTP2_IB_IGN_ALL:
      return (ssize_t)inlen;
    case NGHTTP2_IB_READ_EXTENSION_PAYLOAD:
      DEBUGF("recv: [IB_READ_EXTENSION_PAYLOAD]\n");

      readlen = inbound_frame_payload_readlen(iframe, in, last);
      iframe->payloadleft -= readlen;
      in += readlen;

      DEBUGF("recv: readlen=%zu, payloadleft=%zu\n", readlen,
             iframe->payloadleft);

      if (readlen > 0) {
        rv = session_call_on_extension_chunk_recv_callback(
            session, in - readlen, readlen);
        if (nghttp2_is_fatal(rv)) {
          return rv;
        }

        if (rv != 0) {
          busy = 1;

          iframe->state = NGHTTP2_IB_IGN_PAYLOAD;

          break;
        }
      }

      if (iframe->payloadleft > 0) {
        break;
      }

      rv = session_process_extension_frame(session);
      if (nghttp2_is_fatal(rv)) {
        return rv;
      }

      session_inbound_frame_reset(session);

      break;
    case NGHTTP2_IB_READ_ALTSVC_PAYLOAD:
      DEBUGF("recv: [IB_READ_ALTSVC_PAYLOAD]\n");

      readlen = inbound_frame_payload_readlen(iframe, in, last);
      if (readlen > 0) {
        iframe->lbuf.last = nghttp2_cpymem(iframe->lbuf.last, in, readlen);

        iframe->payloadleft -= readlen;
        in += readlen;
      }

      DEBUGF("recv: readlen=%zu, payloadleft=%zu\n", readlen,
             iframe->payloadleft);

      if (iframe->payloadleft) {
        assert(nghttp2_buf_avail(&iframe->lbuf) > 0);

        break;
      }

      rv = session_process_altsvc_frame(session);
      if (nghttp2_is_fatal(rv)) {
        return rv;
      }

      session_inbound_frame_reset(session);

      break;
    case NGHTTP2_IB_READ_ORIGIN_PAYLOAD:
      DEBUGF("recv: [IB_READ_ORIGIN_PAYLOAD]\n");

      readlen = inbound_frame_payload_readlen(iframe, in, last);

      if (readlen > 0) {
        iframe->lbuf.last = nghttp2_cpymem(iframe->lbuf.last, in, readlen);

        iframe->payloadleft -= readlen;
        in += readlen;
      }

      DEBUGF("recv: readlen=%zu, payloadleft=%zu\n", readlen,
             iframe->payloadleft);

      if (iframe->payloadleft) {
        assert(nghttp2_buf_avail(&iframe->lbuf) > 0);

        break;
      }

      rv = session_process_origin_frame(session);

      if (nghttp2_is_fatal(rv)) {
        return rv;
      }

      if (iframe->state == NGHTTP2_IB_IGN_ALL) {
        return (ssize_t)inlen;
      }

      session_inbound_frame_reset(session);

      break;
    }

    if (!busy && in == last) {
      break;
    }

    busy = 0;
  }

  assert(in == last);

  return in - first;
}

In nghttp2 before version 1.41.0, the overly large HTTP/2 SETTINGS frame payload causes denial of service. The proof of concept attack involves a malicious client constructing a SETTINGS frame with a length of 14,400 bytes (2400 individual settings entries) over and over again. The attack causes the CPU to spike at 100%. nghttp2 v1.41.0 fixes this vulnerability. There is a workaround to this vulnerability. Implement nghttp2_on_frame_recv_callback callback, and if received frame is SETTINGS frame and the number of settings entries are large (e.g., > 32), then drop the connection.