Skip to content

String positions and indices are clamped before they reach a pointer or a count - #28

Merged
ASDAlexander77 merged 1 commit into
mainfrom
string-bounds-checks
Oct 5, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
string-bounds-checks

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Summary

String positions and indices are clamped before they become a pointer into the string or a byte count.

  • slice overflow (String.slice(start, end) with end < start overflows the result buffer #27): an end at or before the start gave a negative count, which resize and memcpy took as a huge size. slice now returns "", as in JavaScript. It does not swap the way substring does.
  • Unsigned parameters: the indexEnd of slice/substring and the position of endsWith/lastIndexOf defaulted to this.length and so took its unsigned type, although lib.d.ts declares int. Every < 0 check was dead, and "abcdef".slice(1, -3) returned "bcdef". They are annotated int now.
  • Clamping: positions are clamped to [0, length]. Before, startsWith, includes and indexOf read before the string for a negative position, and endsWith and lastIndexOf read after the terminator for a position past the end.
  • Behaviour changes:
    • indexOf past the end returned the length for any search string. Now only "" is found there, as in JavaScript.
    • includes(null) returns false. Before, it passed null to strstr.
    • lastIndexOf(null) returns -1. Before, it returned the position.
    • Both now match indexOf.
  • trim / trimEnd: they passed the last kept character as substring's exclusive end. " ab ".trim() was "a", and a string of spaces kept all but one of them.

Fixes #27

The compiler half of this pass (string concatenation without strcpy/strcat) is ASDAlexander77/TypeScriptCompiler#520. The two are independent.

Test plan

  • New tests/string_bounds.ts: each case above, checked against JavaScript's results
  • Release compile, Windows: gc 162/162; rc and none 161/162 (one gc-only skip)
  • JIT mode, debug, Linux: CI. Locally, any JIT run against a freshly built gc DefaultLib hangs, on main too. That is being looked into separately.

🤖 Generated with Claude Code

…or a count

slice with its end at or before its start computed a negative count, which
resize and memcpy took as a huge size and copied past the result (#27). It
now returns "", as in JavaScript; slice does not swap the way substring does.

The end parameter of slice and substring, and the position of endsWith and
lastIndexOf, defaulted to this.length and so took its unsigned type, though
lib.d.ts declares int: a negative argument became huge, every `< 0` check
was dead, and slice(1, -3) returned "bcdef". They are int now.

Positions are clamped to [0, length] before they become a pointer into the
string: startsWith, includes and indexOf read before the string for a
negative position, and endsWith and lastIndexOf read after its terminator
for one past the end. indexOf past the end returned the length for any
search string; now only "" is found there, as in JavaScript. includes and
lastIndexOf return "not found" for a null search string, as indexOf does
(includes passed null to strstr; lastIndexOf returned the position).

trim and trimEnd passed the last kept character as substring's exclusive
end and dropped it ("  ab  ".trim() was "a"), and a string of spaces kept
all but one of them.

Fixes #27

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 0492c1c into main Oct 5, 2026
@ASDAlexander77
ASDAlexander77 deleted the string-bounds-checks branch October 5, 2026 23:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

String.slice(start, end) with end < start overflows the result buffer

1 participant