Skip to content

Commit 8ee1d35

Browse files
authored
fix(stdlib): CONCAT/INSERT/REPLACE no longer overflow the result buffer (#1791)
Refs: PRG-4398
1 parent 01f6c3b commit 8ee1d35

4 files changed

Lines changed: 194 additions & 7 deletions

File tree

libs/stdlib/src/string_functions.rs

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ use std::{
55

66
use num::PrimInt;
77

8+
/// Capacity (in code units, excluding the null terminator) of the result buffer that the
9+
/// user-facing string builtins write into.
10+
/// Excess input is truncated on a character boundary instead.
11+
/// Keep this in sync with the `.st` declarations.
12+
const STRING_RESULT_LEN: usize = 2048;
13+
814
/// # Helper function
915
///
1016
/// Gets the amount of continuous characters before
@@ -61,6 +67,18 @@ pub trait CharsEncoder<T: PrimInt>: Iterator {
6167
/// given address large enough. It will continue to write unchecked until all characters
6268
/// have been processed and can therefore result in UB.
6369
unsafe fn encode(self, dest: &mut *mut T);
70+
71+
/// Like [`CharsEncoder::encode`], but writes at most `max_elements` code units of
72+
/// content (bytes for UTF-8, `u16` code units for UTF-16), always followed by a
73+
/// null terminator. Truncation happens on a character boundary, so the written
74+
/// string stays valid. Returns the number of content code units written (excluding
75+
/// the terminator), allowing callers to keep a running budget across several inputs.
76+
///
77+
/// # Safety
78+
///
79+
/// Works on raw pointers, inherently unsafe. The destination buffer must have room
80+
/// for `max_elements + 1` code units.
81+
unsafe fn encode_bounded(self, dest: &mut *mut T, max_elements: usize) -> usize;
6482
}
6583

6684
#[derive(Debug)]
@@ -86,31 +104,60 @@ impl<'a> CharsDecoder<u8> for EncodedCharsIter<Utf8Iterator<'a>> {
86104

87105
impl<I: Iterator<Item = char>> CharsEncoder<u8> for I {
88106
unsafe fn encode(self, dest: &mut *mut u8) {
107+
self.encode_bounded(dest, usize::MAX);
108+
}
109+
110+
unsafe fn encode_bounded(self, dest: &mut *mut u8, max_elements: usize) -> usize {
111+
let mut remaining = max_elements;
112+
let mut written = 0;
89113
for char in self {
114+
let len = char.len_utf8();
115+
// Stop before we would exceed the budget; never truncate mid-character.
116+
if len > remaining {
117+
break;
118+
}
90119
let mut temp = [0; 4];
91120
let slice = char.encode_utf8(&mut temp);
92121
for byte in slice.as_bytes() {
93122
**dest = *byte;
94123
*dest = dest.add(1);
95124
}
125+
remaining -= len;
126+
written += len;
96127
}
97128

98129
**dest = 0;
130+
written
99131
}
100132
}
101133

102134
impl<I: Iterator<Item = Result<char, DecodeUtf16Error>>> CharsEncoder<u16> for I {
103135
unsafe fn encode(self, dest: &mut *mut u16) {
136+
self.encode_bounded(dest, usize::MAX);
137+
}
138+
139+
unsafe fn encode_bounded(self, dest: &mut *mut u16, max_elements: usize) -> usize {
140+
let mut remaining = max_elements;
141+
let mut written = 0;
104142
for c in self {
143+
let c = c.unwrap();
144+
let len = c.len_utf16();
145+
// Stop before we would exceed the budget; never truncate mid-character.
146+
if len > remaining {
147+
break;
148+
}
105149
let mut temp = [0_u16; 2];
106-
let slice = c.unwrap().encode_utf16(&mut temp);
150+
let slice = c.encode_utf16(&mut temp);
107151
for word in slice {
108152
**dest = *word;
109153
*dest = dest.add(1);
110154
}
155+
remaining -= len;
156+
written += len;
111157
}
112158

113159
**dest = 0;
160+
written
114161
}
115162
}
116163

@@ -417,11 +464,13 @@ pub unsafe extern "C-unwind" fn INSERT_EXT__STRING(
417464
panic! {"Positional parameter is out of bounds."}
418465
}
419466
let pos = pos as usize;
467+
// Truncate at the result-buffer capacity so we never overflow the caller's
468+
// `STRING[2048]` / `WSTRING[2048]` destination (see `STRING_RESULT_LEN`).
420469
EncodedCharsIter::decode(src_base)
421470
.take(pos)
422471
.chain(EncodedCharsIter::decode(src_to_insert))
423472
.chain(EncodedCharsIter::decode(src_base).skip(pos))
424-
.encode(&mut dest);
473+
.encode_bounded(&mut dest, STRING_RESULT_LEN);
425474

426475
0
427476
}
@@ -450,11 +499,13 @@ pub unsafe extern "C-unwind" fn INSERT_EXT__WSTRING(
450499
panic! {"Positional parameter is out of bounds."}
451500
}
452501
let pos = pos as usize;
502+
// Truncate at the result-buffer capacity so we never overflow the caller's
503+
// `STRING[2048]` / `WSTRING[2048]` destination (see `STRING_RESULT_LEN`).
453504
EncodedCharsIter::decode(src_base)
454505
.take(pos)
455506
.chain(EncodedCharsIter::decode(src_to_insert))
456507
.chain(EncodedCharsIter::decode(src_base).skip(pos))
457-
.encode(&mut dest);
508+
.encode_bounded(&mut dest, STRING_RESULT_LEN);
458509

459510
0
460511
}
@@ -583,13 +634,15 @@ pub unsafe extern "C-unwind" fn REPLACE_EXT__STRING(
583634
pos + 1
584635
)
585636
}
637+
// Truncate at the result-buffer capacity so we never overflow the caller's
638+
// `STRING[2048]` / `WSTRING[2048]` destination (see `STRING_RESULT_LEN`).
586639
EncodedCharsIter::decode(src_base)
587640
.take(pos)
588641
.chain(
589642
EncodedCharsIter::decode(src_replacement)
590643
.chain(EncodedCharsIter::decode(src_base).skip(pos + nreplace)),
591644
)
592-
.encode(&mut dest);
645+
.encode_bounded(&mut dest, STRING_RESULT_LEN);
593646

594647
0
595648
}
@@ -630,13 +683,15 @@ pub unsafe extern "C-unwind" fn REPLACE_EXT__WSTRING(
630683
pos + 1
631684
)
632685
}
686+
// Truncate at the result-buffer capacity so we never overflow the caller's
687+
// `STRING[2048]` / `WSTRING[2048]` destination (see `STRING_RESULT_LEN`).
633688
EncodedCharsIter::decode(src_base)
634689
.take(pos)
635690
.chain(
636691
EncodedCharsIter::decode(src_replacement)
637692
.chain(EncodedCharsIter::decode(src_base).skip(pos + nreplace)),
638693
)
639-
.encode(&mut dest);
694+
.encode_bounded(&mut dest, STRING_RESULT_LEN);
640695

641696
0
642697
}
@@ -677,8 +732,12 @@ pub unsafe extern "C-unwind" fn CONCAT_EXT__STRING(dest: *mut u8, argc: i32, arg
677732
}
678733
let mut dest = dest;
679734
let mut argv = argv;
735+
// Truncate at the result-buffer capacity so we never overflow the caller's
736+
// `STRING[2048]` destination (see `STRING_RESULT_LEN`).
737+
let mut remaining = STRING_RESULT_LEN;
680738
for _ in 0..argc {
681-
EncodedCharsIter::decode(*argv).encode(&mut dest);
739+
let written = EncodedCharsIter::decode(*argv).encode_bounded(&mut dest, remaining);
740+
remaining -= written;
682741
argv = argv.add(1);
683742
}
684743

@@ -725,8 +784,12 @@ pub unsafe extern "C-unwind" fn CONCAT_EXT__WSTRING(
725784
}
726785
let mut dest = dest;
727786
let mut argv = argv;
787+
// Truncate at the result-buffer capacity so we never overflow the caller's
788+
// `WSTRING[2048]` destination (see `STRING_RESULT_LEN`).
789+
let mut remaining = STRING_RESULT_LEN;
728790
for _ in 0..argc {
729-
EncodedCharsIter::decode(*argv).encode(&mut dest);
791+
let written = EncodedCharsIter::decode(*argv).encode_bounded(&mut dest, remaining);
792+
remaining -= written;
730793
argv = argv.add(1);
731794
}
732795

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// RUN: (%COMPILE %s && %RUN) | %CHECK %s
2+
// End-to-end tests for the CONCAT stdlib function (STRING and WSTRING).
3+
// Also covers truncation to the STRING[2048] / WSTRING[2048] result buffer:
4+
// concatenating past 2048 code units used to overflow the result buffer and
5+
// crash; it must now truncate to 2048 instead. See libs/stdlib/src/string_functions.rs.
6+
7+
FUNCTION main : DINT
8+
VAR
9+
s : STRING[2048];
10+
ws : WSTRING[2048];
11+
chunk : STRING[2048] := 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
12+
wchunk : WSTRING[2048] := "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
13+
big : STRING[2048];
14+
wbig : WSTRING[2048];
15+
END_VAR
16+
// --- functional: STRING ---
17+
s := CONCAT('Hello', ', ', 'World', '!');
18+
// CHECK: Hello, World!
19+
printf('%s$N', REF(s));
20+
21+
// --- functional: WSTRING ---
22+
ws := CONCAT("Hello", ", ", "World", "!");
23+
// CHECK: Hello, World!
24+
printf('%s$N', REF(TO_STRING(ws)));
25+
26+
// --- truncation: STRING ---
27+
// 16 * 128 == 2048 fills the buffer exactly (no truncation yet).
28+
big := CONCAT(chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk);
29+
// CHECK: 2048
30+
printf('%d$N', LEN(big));
31+
// CONCAT(big, big) would be 4096; must truncate to 2048 instead of crashing.
32+
s := CONCAT(big, big);
33+
// CHECK: 2048
34+
printf('%d$N', LEN(s));
35+
36+
// --- truncation: WSTRING ---
37+
wbig := CONCAT(wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk);
38+
ws := CONCAT(wbig, wbig);
39+
// CHECK: 2048
40+
printf('%d$N', LEN(ws));
41+
42+
main := 0;
43+
END_FUNCTION
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// RUN: (%COMPILE %s && %RUN) | %CHECK %s
2+
// End-to-end tests for the INSERT stdlib function (STRING and WSTRING).
3+
// INSERT combines two strings, so its result can exceed the STRING[2048] /
4+
// WSTRING[2048] result buffer. That used to overflow the buffer and crash;
5+
// it must now truncate to 2048 instead. See libs/stdlib/src/string_functions.rs.
6+
7+
FUNCTION main : DINT
8+
VAR
9+
s : STRING[2048];
10+
ws : WSTRING[2048];
11+
chunk : STRING[2048] := 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
12+
wchunk : WSTRING[2048] := "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
13+
big : STRING[2048];
14+
wbig : WSTRING[2048];
15+
END_VAR
16+
// --- functional: STRING --- (keep the first 2 characters, then insert)
17+
s := INSERT('Hello', 'XYZ', 2);
18+
// CHECK: HeXYZllo
19+
printf('%s$N', REF(s));
20+
21+
// --- functional: WSTRING ---
22+
ws := INSERT("Hello", "XYZ", 2);
23+
// CHECK: HeXYZllo
24+
printf('%s$N', REF(TO_STRING(ws)));
25+
26+
// --- truncation: STRING ---
27+
big := CONCAT(chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk);
28+
// INSERT(big, big, 10) would be ~4096; must truncate to 2048.
29+
s := INSERT(big, big, 10);
30+
// CHECK: 2048
31+
printf('%d$N', LEN(s));
32+
33+
// --- truncation: WSTRING ---
34+
wbig := CONCAT(wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk);
35+
ws := INSERT(wbig, wbig, 10);
36+
// CHECK: 2048
37+
printf('%d$N', LEN(ws));
38+
39+
main := 0;
40+
END_FUNCTION
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// RUN: (%COMPILE %s && %RUN) | %CHECK %s
2+
// End-to-end tests for the REPLACE stdlib function (STRING and WSTRING).
3+
// REPLACE can grow a string when the replacement is longer than the replaced
4+
// region, so its result can exceed the STRING[2048] / WSTRING[2048] result
5+
// buffer. That used to overflow the buffer and crash; it must now truncate to
6+
// 2048 instead. See libs/stdlib/src/string_functions.rs.
7+
8+
FUNCTION main : DINT
9+
VAR
10+
s : STRING[2048];
11+
ws : WSTRING[2048];
12+
chunk : STRING[2048] := 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
13+
wchunk : WSTRING[2048] := "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
14+
big : STRING[2048];
15+
wbig : WSTRING[2048];
16+
END_VAR
17+
// --- functional: STRING --- (replace 2 characters starting at position 2)
18+
s := REPLACE('Hello', 'XX', 2, 2);
19+
// CHECK: HXXlo
20+
printf('%s$N', REF(s));
21+
22+
// --- functional: WSTRING ---
23+
ws := REPLACE("Hello", "XX", 2, 2);
24+
// CHECK: HXXlo
25+
printf('%s$N', REF(TO_STRING(ws)));
26+
27+
// --- truncation: STRING ---
28+
big := CONCAT(chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk, chunk);
29+
// REPLACE(big, big, 1, 1) would be ~4095; must truncate to 2048.
30+
s := REPLACE(big, big, 1, 1);
31+
// CHECK: 2048
32+
printf('%d$N', LEN(s));
33+
34+
// --- truncation: WSTRING ---
35+
wbig := CONCAT(wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk, wchunk);
36+
ws := REPLACE(wbig, wbig, 1, 1);
37+
// CHECK: 2048
38+
printf('%d$N', LEN(ws));
39+
40+
main := 0;
41+
END_FUNCTION

0 commit comments

Comments
 (0)