Skip to content

Commit ec19a26

Browse files
authored
fix: add iteration limits to prevent parser hangs (fixes #2451) (#2483)
Root cause: Three unbounded loops in expression parsing could hang indefinitely on malformed input or pathological code structures: 1. parse_expression_with_precedence main loop 2. collect_index_arguments argument parsing loop 3. parse_simple_array_elements element parsing loop 4. parse_modern_array_literal element parsing loop 5. Type specifier parenthesis depth matching loop Changes: - Added MAX_ITERATIONS limit to main expression parser (100000) - Added MAX_ARGUMENTS limit to argument collector (10000) - Added element count limits to array literal parsers (100000) - Added MAX_PAREN_DEPTH limit to type specifier parsing (1000) These limits prevent infinite loops while supporting real-world code that uses complex expressions with many nested calls or large array literals. Testing: - Added test_parser_iteration_limits.f90 regression test - Verified all existing tests pass (505/505 non-xfail) - Tested with maxloc/minloc intrinsics with dim and mask - Tested with functions having 20+ arguments - Tested with 50-element array literals ISO compliance: Fortran standard places no hard limits on nesting depth or argument counts. These limits are implementation-defined (ISO/IEC 1539-1:2018 Section 2.3.4) and exceed practical usage.
1 parent cee30d5 commit ec19a26

File tree

3 files changed

+134
-14
lines changed

3 files changed

+134
-14
lines changed

src/parser/expressions/parser_expression_arrays.f90

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ function parse_simple_array_elements(parser, arena, terminator, style, &
5858
element_count = 0
5959
allocate (temp_indices(20))
6060

61-
do
61+
do while (element_count < 100000)
6262
element_count = element_count + 1
6363
if (element_count > size(temp_indices)) then
6464
block
@@ -271,16 +271,22 @@ recursive function parse_modern_array_literal(parser, arena, start_token, &
271271
spec_token = parser%consume()
272272
type_spec_text = trim(type_spec_text) // spec_token%text
273273
paren_depth = 1
274-
do while (paren_depth > 0)
275-
peek_token = parser%peek()
276-
if (peek_token%text == "(") then
277-
paren_depth = paren_depth + 1
278-
else if (peek_token%text == ")") then
279-
paren_depth = paren_depth - 1
280-
end if
281-
spec_token = parser%consume()
282-
type_spec_text = type_spec_text // spec_token%text
283-
end do
274+
block
275+
integer :: paren_count
276+
integer, parameter :: MAX_PAREN_DEPTH = 1000
277+
paren_count = 0
278+
do while (paren_depth > 0 .and. paren_count < MAX_PAREN_DEPTH)
279+
paren_count = paren_count + 1
280+
peek_token = parser%peek()
281+
if (peek_token%text == "(") then
282+
paren_depth = paren_depth + 1
283+
else if (peek_token%text == ")") then
284+
paren_depth = paren_depth - 1
285+
end if
286+
spec_token = parser%consume()
287+
type_spec_text = type_spec_text // spec_token%text
288+
end do
289+
end block
284290
peek_token = parser%peek()
285291
end if
286292

@@ -301,7 +307,7 @@ recursive function parse_modern_array_literal(parser, arena, start_token, &
301307
end if
302308
end if
303309

304-
do
310+
do while (element_count < 100000)
305311
! Skip newlines and comments inside array literals
306312
do
307313
peek_token = parser%peek()
@@ -787,6 +793,8 @@ subroutine collect_index_arguments(parser, arena, helpers, closing_char, &
787793
type(token_t), intent(out) :: close_token
788794
type(token_t) :: token
789795
integer :: arg_index
796+
integer :: arg_count
797+
integer, parameter :: MAX_ARGUMENTS = 10000
790798

791799
if (parser%is_at_end()) then
792800
close_token = parser%peek()
@@ -799,8 +807,10 @@ subroutine collect_index_arguments(parser, arena, helpers, closing_char, &
799807
if (arg_index > 0) then
800808
allocate (arg_indices(1))
801809
arg_indices(1) = arg_index
810+
arg_count = 1
802811

803-
do
812+
do while (arg_count < MAX_ARGUMENTS)
813+
arg_count = arg_count + 1
804814
token = parser%peek()
805815
if (token%kind /= TK_OPERATOR .or. token%text /= ",") exit
806816
token = parser%consume()

src/parser/expressions/parser_expressions.f90

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,15 +443,19 @@ recursive function parse_expression_with_precedence(parser, arena, &
443443
type(token_view_t) :: view
444444
logical :: expect_operand, should_exit
445445
type(token_t) :: token
446+
integer :: iteration_count
447+
integer, parameter :: MAX_ITERATIONS = 100000
446448

447449
call operator_stack_clear(operators)
448450
call operand_stack_clear(operands)
449451
call token_stack_clear(prefix_stack)
450452
call build_token_view(view, parser)
451453
expr_index = 0
452454
expect_operand = .true.
455+
iteration_count = 0
453456

454-
main_loop: do while (.true.)
457+
main_loop: do while (iteration_count < MAX_ITERATIONS)
458+
iteration_count = iteration_count + 1
455459
token = view_peek_token(view, parser)
456460
if (token%kind == TK_EOF) exit main_loop
457461

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
program test_parser_iteration_limits
2+
use transformation_api, only: transform_lazy_fortran_string
3+
implicit none
4+
5+
integer :: test_count, pass_count
6+
7+
test_count = 0
8+
pass_count = 0
9+
10+
print *, "=== Parser Iteration Limit Tests (Issue #2451) ==="
11+
print *
12+
13+
call test_complex_nested_calls()
14+
call test_many_function_arguments()
15+
call test_large_array_literal()
16+
17+
print *
18+
print *, "=== Summary ==="
19+
print *, "Tests run: ", test_count
20+
print *, "Tests passed:", pass_count
21+
22+
if (pass_count == test_count) then
23+
print *, "All parser iteration limit tests passed!"
24+
else
25+
print *, "FAILURE: Some tests failed"
26+
stop 1
27+
end if
28+
29+
contains
30+
31+
subroutine test_complex_nested_calls()
32+
character(len=:), allocatable :: source, result, error_msg
33+
34+
test_count = test_count + 1
35+
print *, "Testing complex nested function calls..."
36+
37+
source = "program test" // new_line('a') // &
38+
" implicit none" // new_line('a') // &
39+
" integer :: arr(3,3), res(2)" // new_line('a') // &
40+
" arr = reshape([1,2,3,4,5,6,7,8,9], [3,3])" // new_line('a') // &
41+
" res = maxloc(arr, dim=1, mask=arr > 5)" // new_line('a') // &
42+
" print *, res" // new_line('a') // &
43+
"end program test"
44+
45+
call transform_lazy_fortran_string(source, result, error_msg)
46+
47+
if (len_trim(result) > 0) then
48+
print *, " PASS: Complex nested calls parsed without hang"
49+
pass_count = pass_count + 1
50+
else
51+
print *, " FAIL: Parser failed on complex nested calls"
52+
if (allocated(error_msg)) print *, " Error: ", trim(error_msg)
53+
end if
54+
end subroutine test_complex_nested_calls
55+
56+
subroutine test_many_function_arguments()
57+
character(len=:), allocatable :: source, result, error_msg
58+
59+
test_count = test_count + 1
60+
print *, "Testing function with many arguments..."
61+
62+
source = "program test" // new_line('a') // &
63+
" implicit none" // new_line('a') // &
64+
" call sub(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20)" // &
65+
new_line('a') // &
66+
"end program test"
67+
68+
call transform_lazy_fortran_string(source, result, error_msg)
69+
70+
if (len_trim(result) > 0) then
71+
print *, " PASS: Many arguments parsed without hang"
72+
pass_count = pass_count + 1
73+
else
74+
print *, " FAIL: Parser failed on many arguments"
75+
if (allocated(error_msg)) print *, " Error: ", trim(error_msg)
76+
end if
77+
end subroutine test_many_function_arguments
78+
79+
subroutine test_large_array_literal()
80+
character(len=:), allocatable :: source, result, error_msg
81+
82+
test_count = test_count + 1
83+
print *, "Testing large array literal..."
84+
85+
source = "program test" // new_line('a') // &
86+
" implicit none" // new_line('a') // &
87+
" integer :: x(50)" // new_line('a') // &
88+
" x = [1,2,3,4,5,6,7,8,9,10," // &
89+
"11,12,13,14,15,16,17,18,19,20," // &
90+
"21,22,23,24,25,26,27,28,29,30," // &
91+
"31,32,33,34,35,36,37,38,39,40," // &
92+
"41,42,43,44,45,46,47,48,49,50]" // new_line('a') // &
93+
"end program test"
94+
95+
call transform_lazy_fortran_string(source, result, error_msg)
96+
97+
if (len_trim(result) > 0) then
98+
print *, " PASS: Large array literal parsed without hang"
99+
pass_count = pass_count + 1
100+
else
101+
print *, " FAIL: Parser failed on large array literal"
102+
if (allocated(error_msg)) print *, " Error: ", trim(error_msg)
103+
end if
104+
end subroutine test_large_array_literal
105+
106+
end program test_parser_iteration_limits

0 commit comments

Comments
 (0)