Heredoc rewind inside unclosed interpolation causes unsigned location length underflow (~4 GB out-of-bounds read)
Mantenedores costumam responder em até 1 dia
Ninguém assumiu esta issue ainda.
Avaliação
- Dificuldade
- 4/5
- Tempo estimado
- 3-5 dias
- Facilidade para iniciantes
- 45/100
Direção de pesquisa
Start by tracing the EOF handling and heredoc rewind described in the issue, then inspect the parser lexing and statement parsing paths for PM_LEX_EMBEXPR, PM_LEX_HEREDOC, and PM_TOKEN_HEREDOC_END. Reproduce with the supplied Ruby input and verify the resulting AST locations and pm_prettyprint behavior under AddressSanitizer. Done means malformed heredoc interpolation no longer produces underflowed locations or an out-of-bounds read.
Escrita pelo modelo de indexação a partir do texto da issue.
Descrição
Fuzzing find on bdbd482dc.
When an unterminated heredoc contains an unclosed interpolation (#{...) with unclosed inner constructs at EOF (such as <<0\n#{f{ or <<0\n#{{{{s[f{), hitting EOF inside PM_LEX_EMBEXPR pops PM_LEX_EMBEXPR and causes PM_LEX_HEREDOC to emit PM_TOKEN_HEREDOC_END and rewind parser->next_start back to the heredoc opener's line (line 1).
Because PM_TOKEN_HEREDOC_END is not treated as a context terminator for PM_CONTEXT_EMBEXPR (and token_begins_expression_p returns true for PM_TOKEN_HEREDOC_END), inner parser routines on line 2 do not unwind on PM_TOKEN_HEREDOC_END; instead, they skip or consume PM_TOKEN_HEREDOC_END, lex the rewound newline from line 1 (3..4), and leave parser->previous anchored at byte 4 ((2,0)). When those line-2 AST nodes finalize their locations using parser->previous (with end < start), the unsigned 32-bit length calculation U32(end - start) underflows modulo $2^{32}$ to ~4,294,967,295 (0xFFFFFFF...), causing a ~4 GB out-of-bounds heap buffer read in C consumers such as pm_prettyprint.
Reproduction
1. Ruby / ./bin/parse Output on <<0\n#{{{{s[f{ (13 bytes)
Prism.parse("<<0\n\#{{{{s[f{")
Running ./bin/parse on <<0\n#{{{{s[f{ produces the following AST with end column 4294967296 ($2^{32}$) on HashNode, AssocNode, CallNode#message_loc, CallNode, and BlockNode:
@ ProgramNode (location: (1,0)-(1,3))
└── statements:
@ StatementsNode (location: (1,0)-(1,3))
└── body: (length: 1)
└── @ InterpolatedStringNode (location: (1,0)-(1,3))
├── opening_loc: (1,0)-(1,3) = "<<0"
├── parts: (length: 1)
│ └── @ EmbeddedStatementsNode (location: (2,0)-(2,0))
│ ├── opening_loc: (2,0)-(2,2) = "\#{"
│ ├── statements:
│ │ @ StatementsNode (location: (2,2)-(2,2))
│ │ └── body: (length: 1)
│ │ └── @ HashNode (location: (2,2)-(2,4294967296))
│ │ ├── opening_loc: (2,2)-(2,3) = "{"
│ │ ├── elements: (length: 1)
│ │ │ └── @ AssocNode (location: (2,3)-(2,4294967296))
│ │ │ ├── key:
│ │ │ │ @ HashNode (location: (2,3)-(2,4294967296))
│ │ │ │ ├── opening_loc: (2,3)-(2,4) = "{"
│ │ │ │ ├── elements: (length: 1)
│ │ │ │ │ └── @ AssocNode (location: (2,4)-(2,4294967296))
│ │ │ │ │ ├── key:
│ │ │ │ │ │ @ HashNode (location: (2,4)-(2,4294967296))
│ │ │ │ │ │ ├── opening_loc: (2,4)-(2,5) = "{"
│ │ │ │ │ │ ├── elements: (length: 1)
│ │ │ │ │ │ │ └── @ AssocNode (location: (2,5)-(2,4294967296))
│ │ │ │ │ │ │ ├── key:
│ │ │ │ │ │ │ │ @ CallNode (location: (2,5)-(2,7))
│ │ │ │ │ │ │ │ ├── name: :[]
│ │ │ │ │ │ │ │ ├── message_loc: (2,6)-(2,4294967296) = "[f{"
│ │ │ │ │ │ │ │ ├── opening_loc: (2,6)-(2,7) = "["
│ │ │ │ │ │ │ │ ├── arguments:
│ │ │ │ │ │ │ │ │ @ ArgumentsNode (location: (2,7)-(2,7))
│ │ │ │ │ │ │ │ │ └── arguments: (length: 1)
│ │ │ │ │ │ │ │ │ └── @ CallNode (location: (2,7)-(2,4294967296))
│ │ │ │ │ │ │ │ │ ├── name: :f
│ │ │ │ │ │ │ │ │ ├── message_loc: (2,7)-(2,8) = "f"
│ │ │ │ │ │ │ │ │ └── block:
│ │ │ │ │ │ │ │ │ @ BlockNode (location: (2,8)-(2,4294967296))
│ │ │ │ │ │ │ │ │ ├── body:
│ │ │ │ │ │ │ │ │ │ @ StatementsNode (location: (1,3)-(2,0))
│ │ │ │ │ │ │ │ │ │ └── body: (length: 2)
│ │ │ │ │ │ │ │ │ │ ├── @ ErrorRecoveryNode (location: (2,8)-(2,9))
│ │ │ │ │ │ │ │ │ │ └── @ ErrorRecoveryNode (location: (1,3)-(2,0))
│ │ │ │ │ │ │ │ │ ├── opening_loc: (2,8)-(2,9) = "{"
│ │ │ │ │ │ │ │ │ └── closing_loc: (2,0)-(2,0) = ""
2. C pm_prettyprint Reproduction & AddressSanitizer Output
Passing the parsed AST of "<<0\n#{{{{s[f{" (or the 10-byte "<<0\n#{s[f{") to pm_prettyprint causes pm_buffer_append_source to read (size_t) node->message_loc.length (4,294,967,290 bytes, 0xFFFFFFFA) starting at parser->start + 10:
#include "prism.h"
#include <stdlib.h>
#include <string.h>
int main(void) {
char *src = strdup("<<0\n#{{{{s[f{");
pm_arena_t *arena = pm_arena_new();
pm_parser_t *parser = pm_parser_new(arena, (const uint8_t *) src, strlen(src), NULL);
pm_node_t *node = pm_parse(parser);
pm_buffer_t *buf = pm_buffer_new();
pm_prettyprint(buf, parser, node);
pm_buffer_free(buf);
pm_parser_free(parser);
pm_arena_free(arena);
free(src);
return 0;
}
Actual AddressSanitizer Output
=================================================================
==21289==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000095e at pc 0x0001026cbf20 bp 0x00016d6bb860 sp 0x00016d6bb858
READ of size 1 at 0x60200000095e thread T0
#0 0x0001026cbf1c in pm_buffer_append_source buffer.c:260
#1 0x0001026e3e44 in prettyprint_node prettyprint.c:1226
#2 0x0001026dcad0 in prettyprint_node prettyprint.c:491
#3 0x0001026fa478 in prettyprint_node prettyprint.c:3923
#4 0x0001026dcad0 in prettyprint_node prettyprint.c:491
#5 0x0001026fa478 in prettyprint_node prettyprint.c:3923
#6 0x0001026dcad0 in prettyprint_node prettyprint.c:491
#7 0x0001026fa478 in prettyprint_node prettyprint.c:3923
#8 0x000102719c6c in prettyprint_node prettyprint.c:8195
#9 0x0001026f4de8 in prettyprint_node prettyprint.c:3162
#10 0x000102705b9c in prettyprint_node prettyprint.c:5416
#11 0x000102719c6c in prettyprint_node prettyprint.c:8195
#12 0x000102714030 in prettyprint_node prettyprint.c:7338
#13 0x0001026d838c in pm_prettyprint prettyprint.c:9020
#14 0x00010287dba0 in main test.c:11
#15 0x000194e9fe7c in start+0x1a1c (dyld:arm64e+0x31e7c)
0x60200000095e is located 0 bytes after 14-byte region [0x602000000950,0x60200000095e)
allocated by thread T0 here:
#0 0x000102f16c28 in strdup+0x108 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x3ac28)
#1 0x00010287db40 in main test.c:6
#2 0x000194e9fe7c in start+0x1a1c (dyld:arm64e+0x31e7c)
SUMMARY: AddressSanitizer: heap-buffer-overflow buffer.c:260 in pm_buffer_append_source
Shadow bytes around the buggy address:
0x602000000680: fa fa 00 00 fa fa 00 00 fa fa 00 00 fa fa 00 00
0x602000000700: fa fa 00 00 fa fa 00 00 fa fa 00 00 fa fa 00 00
0x602000000780: fa fa 00 00 fa fa 00 00 fa fa 00 00 fa fa 00 00
0x602000000800: fa fa 00 00 fa fa 00 00 fa fa 00 00 fa fa 00 00
0x602000000880: fa fa 00 00 fa fa 00 00 fa fa 00 00 fa fa 00 fa
=>0x602000000900: fa fa 00 04 fa fa 00 00 fa fa 00[06]fa fa 00 00
0x602000000980: fa fa fd fa fa fa fd fa fa fa fd fd fa fa fa fa
0x602000000a00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x602000000a80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x602000000b00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x602000000b80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==21289==ABORTING
Root Cause
Step-by-step trace of "<<0\n#{{{{s[f{" (interactive simulation in Prism Loupe):
-
Line 1 (
<<0\n, bytes0..4): Lexing<<0(PM_TOKEN_HEREDOC_START[0..3]) pushesPM_LEX_HEREDOContolex_modeswithlex_mode.as.heredoc.next_start = 3(saving the position of\non line 1 inside the heredoc frame) while settingparser->next_start = 4(body_start) so the nextparser_lexjumps forward to line 2. -
Line 2 (
#{{{{s[f{, bytes4..13):#{(PM_TOKEN_EMBEXPR_BEGIN[4..6]) resetsparser->next_start = NULL, pushesPM_LEX_EMBEXPR(lex_modes = [EMBEXPR, HEREDOC, DEFAULT]), and entersparse_statements(parser, PM_CONTEXT_EMBEXPR, ...), descending through{(6..7),{(7..8),{(8..9),s[(parse_arefat10..11), andf(11..12) intoparse_arguments_listwith lookaheadparser->current = PM_TOKEN_BRACE_LEFT[12..13]. -
1-token lookahead on
{(12..13) popsPM_LEX_EMBEXPRandPM_LEX_HEREDOCbeforeparse_blockenters: Insideparse_arguments_list(whilecontextsis still[EMBEXPR, MAIN]),accept2(parser, PM_TOKEN_BRACE_LEFT, ...)consumes{(12..13) and immediately callsparser_lexfor lookahead:- At byte
13 >= parser->end(EOF),parser_lexpopsPM_LEX_EMBEXPR(lex_modes = [HEREDOC, DEFAULT]) and jumps toswitch_lex_modes. PM_LEX_HEREDOCruns at EOF, records the missing heredoc terminator diagnostic, setsparser->next_start = 3(fromlex_mode.as.heredoc.next_start), setsparser->heredoc_end = 13, setslex_state = PM_LEX_STATE_END, popsPM_LEX_HEREDOC(lex_modes = [DEFAULT]), and emitsparser->current = PM_TOKEN_HEREDOC_END[13..13].- Only after
lex_modeshas collapsed to[DEFAULT]doesparse_arguments_listcallparse_block->parse_statements(parser, PM_CONTEXT_BLOCK_BRACES, ...), pushingPM_CONTEXT_BLOCK_BRACESontocontexts([BLOCK_BRACES, EMBEXPR, MAIN]).
- At byte
-
Two
parse_statementsiterations and the13 -> 3..4 -> 13HEREDOC_FLUSHbounce:- Iteration 1 (on
PM_TOKEN_HEREDOC_END[13..13]):PM_TOKEN_HEREDOC_ENDis not acontext_terminatorforPM_CONTEXT_BLOCK_BRACESorPM_CONTEXT_EMBEXPR, soparse_expression_prefixemits a 1stErrorRecoveryNode(12..13) without settingparser->recovering. - Rewind to byte
3andparser_flush_heredoc_end:parse_statementsskips the unparseable token viaparser_lex(parser), jumping toparser->next_start = 3(\non line 1). Inside the\nhandler,parser->heredoc_end == 13triggersparser_flush_heredoc_end(parser), which setsparser->next_start = 13andparser->heredoc_end = NULLwhile returningparser->current = PM_TOKEN_NEWLINE[3..4](lex_state = PM_LEX_STATE_BEG). - Bounce back to
PM_TOKEN_EOF[13..13]:while (accept2(parser, PM_TOKEN_NEWLINE, PM_TOKEN_SEMICOLON));acceptsPM_TOKEN_NEWLINE[3..4]—movingNEWLINE[3..4]intoparser->previous—and callsparser_lex, which followsparser->next_start = 13right back toparser->current = PM_TOKEN_EOF[13..13]. - Iteration 2 (on
PM_TOKEN_EOF[13..13]): BecausePM_TOKEN_EOFdoes not directly terminatePM_CONTEXT_BLOCK_BRACES, theparse_statementsloop runs a second iteration.context_recoverableseesPM_TOKEN_EOFterminatesPM_CONTEXT_MAIN, setsparser->recovering = true, emits a 2ndErrorRecoveryNode(3..4) without advancingparser->previous, and breaks out ofparse_statements.
- Iteration 1 (on
-
Synthesizing
PM_TOKEN_NOT_PROVIDED[4..4]anduint32_tunderflow during unwinding:- In
parse_block,expect1_opening(parser, PM_TOKEN_BRACE_RIGHT, ...)fails againstPM_TOKEN_EOF[13..13]and synthesizes a zero-width missing token atparser->previous.end:parser->previous = PM_TOKEN_NOT_PROVIDED[4..4]((2,0), the first byte after\n). - Every enclosing construct on line 2 then unwinds under
parser->recovering == truewithparser->previous = NOT_PROVIDED[4..4](end = 4). ComputingU32(4 - start)underflowsuint32_tacross 8 AST nodebase.locations plusCallNode#message_locons[f{:
AST Node / Location start_offset(Line, Col)Target End Offset Signed Diff ( end - start)uint32_t length(U32(diff))BlockNode({)12((2,8))4((2,0))-84294967288($2^{32} - 8$)CallNode(f{)11((2,7))4((2,0))-74294967289($2^{32} - 7$)CallNode#message_loc([f{)*10((2,6))4((2,0))-64294967290($2^{32} - 6$)Inner AssocNode(s[f{)9((2,5))4((2,0))-54294967291($2^{32} - 5$)Inner HashNode({s[f{) & MiddleAssocNode8((2,4))4((2,0))-44294967292($2^{32} - 4$)Middle HashNode({{s[f{) & OuterAssocNode7((2,3))4((2,0))-34294967293($2^{32} - 3$)Outer HashNode({{{s[f{)6((2,2))4((2,0))-24294967294($2^{32} - 2$)*Note on
s[f{'sCallNode(base.location = (2,5)-(2,7),9..11): Its ownbase.locationdoes not underflow because inpm_arguments_node_arguments_append,PM_NODE_END(argument)wraps modulo $2^{32}$ (11 + 4294967289 == 4), makingPM_NODE_END(node) < PM_NODE_END(argument)(11 < 4) evaluate tofalsesoArgumentsNodestays11..11, whilepm_call_node_aref_createcomputesmessage_loc.length = (4 + 0) - 10 = 4294967290. - In
- Linguagem predominante
- C
- Estrelas
- 1k
- Forks
- 202
- Merge médio
- 5h 55min
- PRs com merge (30d)
- 85
Preparar o ambiente
- Sem Dockerfile nem arquivo Docker Compose
- Sem modelo de pull request
- Ler o guia de contribuição
Primeiros passos
- Leia a issue inteira e depois o guia de contribuição do projeto.
- Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
- Faça um fork do repositório e trabalhe em uma branch.
- Abra um pull request que referencie o número da issue.
Mais de ruby/prism
-
Prism::Translation::Parser raises NoMethodError for a method with a &nil parameter and a bodyTalvez já em andamento @bbatsov assumiu hoje. Aberta
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 82/100
Mantenedores costumam responder em até 1 dia
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 74/100
Mantenedores costumam responder em até 1 dia
-
Heredoc and outer-context recovery at recursion depth / stack budget limit causes location underflowAberta
Dificuldade 4/5 3-5 dias Facilidade para iniciantes 43/100
Mantenedores costumam responder em até 1 dia
-
Dificuldade 4/5 3-5 dias Facilidade para iniciantes 45/100
Mantenedores costumam responder em até 1 dia
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 1/100
Mantenedores costumam responder em até 1 dia
Issues semelhantes
-
runtime: lf_f128_mod is documented as fmodq (truncating) but implements floor-based modulo; unusedAbertainternal correctness low priority rt_lib
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 72/100
Mantenedores costumam responder em até 1 dia
-
Unreachable code warningAbertabug
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 70/100
SanderMertens/flecs#2395 ·
Mantenedores costumam responder em até 1 dia
-
Memory leak: "Add to Playlist" (`CMD_EVENT_ADD_TO_PLAYLIST`) never frees the playlist it loadsAberta
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 76/100
Mantenedores costumam responder em até 1 dia
-
Signed/unsigned mix in the EOCD backward scan rejects valid archives with a 65535-byte commentAberta
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 78/100
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 78/100
OpenXiangShan/NEMU#1287 ·
Mantenedores costumam responder em até 1 dia