Skip to content

Commit de68606

Browse files
committed
regcomp_trie: make octet trie lookups always valid
Fix intermittent segfaults from the trie logic, as reported in #24816, and discussed in #24814. The list compiler previously recorded a slot after a sparse frame in lasttrans without always allocating or clearing it. A lookup could therefore treat uninitialized transition data as belonging to its state and use an arbitrary next state. This issue affected all platforms but was most prominently visible on Windows and Cygwin. (Historically Windows realloc is not as good as *nix realloc.) As part of fixing this, I reworked some of the logic to be more robust and ensure we never see this kind of issue again. We give the root state the complete octet frame at slots 1 through 256. Other states can reuse unoccupied root slots. Each state base maps octet zero to an offset not less than 1, and we ensure there are enough valid null transitions at the end, so we never read a bogus transition and do not need to do bounds checks either. While working on this I noticed we had some logic left over from the old implementation that still used the concept of "charid", which confused matters. This was removed, and we now have simpler runtime logic to do transition lookups. I also added code to validate the state and transition indexes under DEBUG_r. Move that block outside DEBUG_r when validation is wanted in every full DEBUGGING build. Thus, compilation under C<use re 'debug'> also exercises the transition validation, so if you suspect a related bug try the pattern under that pragma. Added support to regexp.t to allow the developer to select an inclusive range of re_tests input lines while retaining its original TAP numbering. This makes the triggering cases practical to stress independently. The re_tests lines 2221 through 2222 also pass with 100,000 iterations, including under ASAN with a refcounted stack (PERL_RC_STACK).
1 parent a49f088 commit de68606

4 files changed

Lines changed: 174 additions & 115 deletions

File tree

regcomp.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1265,7 +1265,7 @@ enum trie_flags {
12651265

12661266
struct reg_trie_data_ {
12671267
U32 refcount; /* number of times this trie is referenced */
1268-
U32 lasttrans; /* last valid transition element */
1268+
U32 lasttrans; /* one past the transition allocation */
12691269
reg_trie_state *states; /* state data */
12701270
reg_trie_trans *trans; /* array of transition elements */
12711271
TRIE_JUMP_TYPE *jump; /* optional 1 indexed array of offsets before tail

regcomp_trie.c

Lines changed: 117 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -538,18 +538,17 @@ S_trie_list_transition(pTHX_ reg_trie_data *trie, U32 *state, const U32 octet,
538538

539539
PERL_STATIC_INLINE U32
540540
S_trie_trans_state(const reg_trie_data *trie, const U32 state,
541-
const U32 base, const U32 ucharcount, const U32 octet,
542-
const U32 special, const U32 ubound)
541+
const U32 base, const U32 octet, const U32 special)
543542
{
544-
const U32 index = base - ucharcount + octet;
545-
546543
/* The packed table stores a transition at base - alphabet_size + octet.
547544
* The check field confirms that the physical slot belongs to this state.
548545
* During Aho-Corasick construction, special is the fallback transition
549546
* used when the lookup is performed from the root state. */
550-
return base + octet >= ucharcount
551-
&& base + octet < ubound
552-
&& state == trie->trans[index].check
547+
if (!base)
548+
return state == 1 ? special : 0;
549+
550+
const U32 index = base - TRIE_ALPHABET_SIZE + octet;
551+
return state == trie->trans[index].check
553552
&& trie->trans[index].next
554553
? trie->trans[index].next
555554
: state == 1 ? special : 0;
@@ -638,17 +637,20 @@ Perl_make_trie(pTHX_ RExC_state_t *pRExC_state, regnode *startbranch,
638637
trie->wordinfo = (reg_trie_wordinfo *) PerlMemShared_calloc(
639638
trie->wordcount+1, sizeof(reg_trie_wordinfo));
640639

640+
#ifdef DEBUGGING
641+
/* Allocate the optional word list only while regex debugging is enabled. */
641642
DEBUG_r({
642643
trie_words = newAV();
643644
});
644645

645646
DEBUG_TRIE_COMPILE_r({
646647
re_indentf(
647648
"make_trie start == %d, first == %d, last == %d, tail == %d depth = %d\n",
648-
depth+1,
649+
depth+1,
649650
REG_NODE_NUM(startbranch), REG_NODE_NUM(first),
650651
REG_NODE_NUM(last), REG_NODE_NUM(tail), (int)depth);
651652
});
653+
#endif
652654

653655
/* Find the node we are going to overwrite */
654656
if ( first == startbranch && OP( last ) != BRANCH ) {
@@ -997,21 +999,43 @@ Perl_make_trie(pTHX_ RExC_state_t *pRExC_state, regnode *startbranch,
997999
#ifdef DEBUGGING
9981000
const STRLEN ideal_transition_count = transition_count - 1;
9991001
#endif
1000-
STRLEN transition_capacity = transition_count;
1002+
/* Allow roughly 20% slack. The root state needs one complete octet
1003+
* frame, and sparse frames can still expand the table. */
1004+
STRLEN transition_capacity = transition_count + transition_count / 5;
1005+
if (transition_capacity < TRIE_ALPHABET_SIZE + 1)
1006+
transition_capacity = TRIE_ALPHABET_SIZE + 1;
10011007
trie->trans = (reg_trie_trans *)
10021008
PerlMemShared_calloc( transition_capacity,
10031009
sizeof(reg_trie_trans) );
10041010
{
10051011
U32 state;
1006-
/* table_highwater is one past the packed portion of trie->trans;
1007-
* next_hole is the first unused slot within that portion. A
1008-
* non-zero next field marks an occupied slot, so holes can be
1009-
* reused without a separate occupancy map. */
1010-
U32 table_highwater = 0;
1011-
U32 next_hole = 0;
1012-
1012+
/* Slot zero is reserved. The root state starts at slot one, so
1013+
* its complete octet frame occupies slots 1 through 256. Other
1014+
* states can reuse its empty slots. */
1015+
U32 table_highwater = TRIE_ALPHABET_SIZE + 1;
1016+
U32 safe_highwater = TRIE_ALPHABET_SIZE + 1;
1017+
U32 next_hole = 1;
1018+
1019+
if (trie->states[1].trans.list) {
1020+
const U32 used = TRIE_LIST_USED(1);
1021+
const U32 base = TRIE_ALPHABET_SIZE + 1;
1022+
U32 transition_index;
1023+
1024+
for (transition_index = 1;
1025+
transition_index <= used;
1026+
transition_index++) {
1027+
const U32 tid = base - TRIE_ALPHABET_SIZE
1028+
+ TRIE_LIST_ITEM(1,
1029+
transition_index).octet;
1030+
trie->trans[tid].next = TRIE_LIST_ITEM(1,
1031+
transition_index).newstate;
1032+
trie->trans[tid].check = 1;
1033+
}
1034+
Safefree(trie->states[1].trans.list);
1035+
trie->states[1].trans.base = base;
1036+
}
10131037

1014-
for( state = 1; state < next_alloc; state ++ ) {
1038+
for( state = 2; state < next_alloc; state ++ ) {
10151039
U32 base = 0;
10161040

10171041
/*
@@ -1030,15 +1054,14 @@ Perl_make_trie(pTHX_ RExC_state_t *pRExC_state, regnode *startbranch,
10301054
bool placed = false;
10311055
U32 transition_index;
10321056

1033-
/* A state's transitions form a frame from min_octet
1034-
* through max_octet. Sparse frames may fit into holes
1035-
* left by earlier frames. Try the first available
1036-
* position. The frame may extend beyond table_highwater,
1037-
* provided it
1038-
* fits in the allocation and its occupied slots do not
1039-
* clash. */
1057+
/* next_hole is the physical slot for min_octet. Its
1058+
* octet-zero slot is next_hole - min_octet, which must
1059+
* not be slot zero. Sparse frames may fit into holes
1060+
* left by earlier frames when their occupied slots do
1061+
* not clash. */
10401062
while (next_hole < table_highwater
1041-
&& trie->trans[next_hole].next)
1063+
&& (next_hole <= min_octet
1064+
|| trie->trans[next_hole].next))
10421065
next_hole++;
10431066
/* Only sparse frames benefit from hole searching. Small
10441067
* frames are cheap to try, while larger frames are tried
@@ -1047,9 +1070,11 @@ Perl_make_trie(pTHX_ RExC_state_t *pRExC_state, regnode *startbranch,
10471070
&& next_hole < table_highwater
10481071
&& next_hole + frame_width >= next_hole) {
10491072
const U32 candidate_end = next_hole + frame_width;
1050-
if (transition_capacity < candidate_end) {
1073+
const U32 candidate_base = TRIE_ALPHABET_SIZE
1074+
+ next_hole - min_octet;
1075+
const U32 needed = MAX(candidate_end, candidate_base);
1076+
if (transition_capacity < needed) {
10511077
const U32 old_capacity = transition_capacity;
1052-
const U32 needed = candidate_end;
10531078

10541079
while (transition_capacity < needed)
10551080
transition_capacity *= 2;
@@ -1073,7 +1098,7 @@ Perl_make_trie(pTHX_ RExC_state_t *pRExC_state, regnode *startbranch,
10731098
break;
10741099
}
10751100
if (transition_index > used) {
1076-
base = TRIE_ALPHABET_SIZE + next_hole - min_octet;
1101+
base = candidate_base;
10771102
for (transition_index = 1;
10781103
transition_index <= used;
10791104
transition_index++) {
@@ -1087,29 +1112,36 @@ Perl_make_trie(pTHX_ RExC_state_t *pRExC_state, regnode *startbranch,
10871112
}
10881113
if (candidate_end > table_highwater)
10891114
table_highwater = candidate_end;
1115+
if (candidate_base > safe_highwater)
1116+
safe_highwater = candidate_base;
10901117
while (next_hole < table_highwater
1091-
&& trie->trans[next_hole].next)
1118+
&& (next_hole <= min_octet
1119+
|| trie->trans[next_hole].next))
10921120
next_hole++;
10931121
placed = true;
10941122
}
10951123
}
10961124

10971125
/* Grow the physical table when the frame cannot fit at
10981126
* the current high-water mark. */
1099-
if (!placed
1100-
&& transition_capacity < table_highwater + frame_width) {
1101-
const U32 old_capacity = transition_capacity;
1102-
const U32 needed = table_highwater + frame_width;
1103-
1104-
while (transition_capacity < needed)
1105-
transition_capacity *= 2;
1106-
trie->trans = (reg_trie_trans *)
1107-
PerlMemShared_realloc( trie->trans,
1127+
if (!placed) {
1128+
const U32 base_at_highwater = TRIE_ALPHABET_SIZE
1129+
+ table_highwater - min_octet;
1130+
const U32 needed = MAX(table_highwater + frame_width,
1131+
base_at_highwater);
1132+
if (transition_capacity < needed) {
1133+
const U32 old_capacity = transition_capacity;
1134+
1135+
while (transition_capacity < needed)
1136+
transition_capacity *= 2;
1137+
trie->trans = (reg_trie_trans *)
1138+
PerlMemShared_realloc( trie->trans,
11081139
transition_capacity
11091140
* sizeof(reg_trie_trans) );
1110-
Zero( trie->trans + old_capacity,
1111-
transition_capacity - old_capacity,
1112-
reg_trie_trans );
1141+
Zero( trie->trans + old_capacity,
1142+
transition_capacity - old_capacity,
1143+
reg_trie_trans );
1144+
}
11131145
}
11141146
if (!placed) {
11151147
base = TRIE_ALPHABET_SIZE + table_highwater - min_octet;
@@ -1119,7 +1151,8 @@ Perl_make_trie(pTHX_ RExC_state_t *pRExC_state, regnode *startbranch,
11191151
if ( !placed && max_octet == min_octet ) {
11201152
U32 set = 0;
11211153
for ( ; next_hole < table_highwater ; next_hole++ ) {
1122-
if ( ! trie->trans[ next_hole ].next ) {
1154+
if (next_hole > min_octet
1155+
&& !trie->trans[next_hole].next) {
11231156
base = TRIE_ALPHABET_SIZE + next_hole - min_octet;
11241157
trie->trans[ next_hole ].next = TRIE_LIST_ITEM( state,
11251158
1).newstate;
@@ -1136,7 +1169,8 @@ Perl_make_trie(pTHX_ RExC_state_t *pRExC_state, regnode *startbranch,
11361169
next_hole = table_highwater;
11371170
} else {
11381171
while (next_hole < table_highwater
1139-
&& trie->trans[next_hole].next)
1172+
&& (next_hole <= min_octet
1173+
|| trie->trans[next_hole].next))
11401174
next_hole++;
11411175
}
11421176
} else if (!placed) {
@@ -1153,9 +1187,12 @@ Perl_make_trie(pTHX_ RExC_state_t *pRExC_state, regnode *startbranch,
11531187
}
11541188
table_highwater += frame_width;
11551189
while (next_hole < table_highwater
1156-
&& trie->trans[next_hole].next)
1190+
&& (next_hole <= min_octet
1191+
|| trie->trans[next_hole].next))
11571192
next_hole++;
11581193
}
1194+
if (base > safe_highwater)
1195+
safe_highwater = base;
11591196
Safefree(trie->states[ state ].trans.list);
11601197
}
11611198
/*
@@ -1165,9 +1202,20 @@ Perl_make_trie(pTHX_ RExC_state_t *pRExC_state, regnode *startbranch,
11651202
*/
11661203
trie->states[ state ].trans.base = base;
11671204
}
1168-
/* Slot zero is reserved by the packed-table representation; keep
1169-
* it in the recorded range even when table_highwater is zero. */
1170-
trie->lasttrans = table_highwater + 1;
1205+
/* Every state's base is one past its highest possible lookup.
1206+
* Keep sufficient zeroed tail space for the complete frame. */
1207+
trie->lasttrans = safe_highwater;
1208+
if (transition_capacity < trie->lasttrans) {
1209+
const U32 old_capacity = transition_capacity;
1210+
1211+
trie->trans = (reg_trie_trans *)
1212+
PerlMemShared_realloc(trie->trans,
1213+
trie->lasttrans
1214+
* sizeof(reg_trie_trans));
1215+
Zero(trie->trans + old_capacity,
1216+
trie->lasttrans - old_capacity, reg_trie_trans);
1217+
transition_capacity = trie->lasttrans;
1218+
}
11711219
assert(next_hole <= table_highwater);
11721220
assert(table_highwater <= transition_capacity);
11731221
DEBUG_TRIE_COMPILE_MORE_r(
@@ -1194,6 +1242,24 @@ Perl_make_trie(pTHX_ RExC_state_t *pRExC_state, regnode *startbranch,
11941242
PerlMemShared_realloc( trie->trans, trie->lasttrans
11951243
* sizeof(reg_trie_trans) );
11961244

1245+
#ifdef DEBUGGING
1246+
/* Validate the packed trie indexes while regex debugging is enabled.
1247+
* Move this outside DEBUG_r() to validate every full DEBUGGING build. */
1248+
DEBUG_r({
1249+
for (U32 state = 1; state < trie->statecount; state++) {
1250+
assert(!trie->states[state].trans.base
1251+
|| (trie->states[state].trans.base > TRIE_ALPHABET_SIZE
1252+
&& trie->states[state].trans.base <= trie->lasttrans));
1253+
}
1254+
for (U32 transition = 1; transition < trie->lasttrans; transition++) {
1255+
assert(!trie->trans[transition].next
1256+
|| (trie->trans[transition].check
1257+
&& trie->trans[transition].check < trie->statecount
1258+
&& trie->trans[transition].next < trie->statecount));
1259+
}
1260+
});
1261+
#endif
1262+
11971263
{ /* Modify the program and insert the new TRIE node */
11981264
U8 nodetype =(U8) flags;
11991265
U8 trie_op = TRIE;
@@ -1661,9 +1727,7 @@ Perl_construct_ahocorasick_from_trie(pTHX_ RExC_state_t *pRExC_state, regnode *s
16611727
const U32 trie_offset = TRIE_DATA_SLOT(source);
16621728
reg_trie_data *trie = (reg_trie_data *)RExC_rxi->data->data[trie_offset];
16631729
U32 *q;
1664-
const U32 ucharcount = TRIE_ALPHABET_SIZE;
16651730
const U32 numstates = trie->statecount;
1666-
const U32 ubound = trie->lasttrans + ucharcount;
16671731
U32 q_read = 0;
16681732
U32 q_write = 0;
16691733
U32 octet;
@@ -1716,8 +1780,7 @@ Perl_construct_ahocorasick_from_trie(pTHX_ RExC_state_t *pRExC_state, regnode *s
17161780
if (base) {
17171781
for ( octet = trie->states[1].min_octet;
17181782
octet <= trie->states[1].max_octet; octet++ ) {
1719-
const U32 newstate = S_trie_trans_state(trie, 1, base, ucharcount,
1720-
octet, 0, ubound);
1783+
const U32 newstate = S_trie_trans_state(trie, 1, base, octet, 0);
17211784
if ( newstate ) {
17221785
q[ q_write ] = newstate;
17231786
/* set to point at the root */
@@ -1733,19 +1796,18 @@ Perl_construct_ahocorasick_from_trie(pTHX_ RExC_state_t *pRExC_state, regnode *s
17331796
for ( octet = aho->states[cur].min_octet;
17341797
octet <= aho->states[cur].max_octet; octet++ ) {
17351798
const U32 ch_state = S_trie_trans_state(trie, cur, base,
1736-
ucharcount, octet, 1,
1737-
ubound);
1799+
octet, 1);
17381800
if (ch_state) {
17391801
U32 fail_state = cur;
17401802
U32 fail_base;
17411803
do {
17421804
fail_state = fail[ fail_state ];
17431805
fail_base = aho->states[ fail_state ].trans.base;
17441806
} while ( !S_trie_trans_state(trie, fail_state, fail_base,
1745-
ucharcount, octet, 1, ubound) );
1807+
octet, 1) );
17461808

17471809
fail_state = S_trie_trans_state(trie, fail_state, fail_base,
1748-
ucharcount, octet, 1, ubound);
1810+
octet, 1);
17491811
fail[ ch_state ] = fail_state;
17501812
if ( !aho->states[ ch_state ].wordnum && aho->states[ fail_state ].wordnum )
17511813
{

0 commit comments

Comments
 (0)