Skip to content

notify: fix panic when truncating multi-byte messages - #5544

Open
MaxFreedomPollard wants to merge 1 commit into
prometheus:mainfrom
MaxFreedomPollard:notify-truncate-multibyte-panic
Open

notify: fix panic when truncating multi-byte messages#5544
MaxFreedomPollard wants to merge 1 commit into
prometheus:mainfrom
MaxFreedomPollard:notify-truncate-multibyte-panic

Conversation

@MaxFreedomPollard

Copy link
Copy Markdown

TruncateInBytes in notify/util.go indexes the rune slice with a byte count. On main, line 139 builds the starting slice as r[:truncationTarget], where r is []rune(s) and truncationTarget is n-3, a number of bytes. A string holding multi-byte characters has fewer runes than bytes, so whenever the input is longer than n bytes while holding fewer than n-3 runes, the index runs past the end of the rune slice and panics.

The Webex notifier is the only caller in this repository. notify/webex/webex.go:89 truncates the rendered markdown to maxMessageSize, which is 7439 bytes, so a notification whose message reaches 7440 bytes of non-ASCII text crashes Alertmanager. 3720 Cyrillic characters, 2480 CJK characters or 1860 emoji are enough. The notification pipeline runs in a goroutine started by the dispatcher at dispatch/dispatch.go:578, and neither notify nor dispatch recovers, so the process exits.

panic: runtime error: slice bounds out of range [:7436] with capacity 4096

github.com/prometheus/alertmanager/notify.TruncateInBytes(...)
	notify/util.go:139
github.com/prometheus/alertmanager/notify/webex.(*Notifier).Notify(...)
	notify/webex/webex.go:89

The line arrived in f51f51e, "Use the new truncation in bytes functions to ensure strings are not butchered", part of #3132, which replaced the byte slicing that #3135 had introduced a day earlier. TruncateInRunes is not affected: it returns early when len(r) <= n, so its own r[:n-1] is always in range.

What changed

truncatedRunes now starts at r[:min(truncationTarget, len(r))]. The loop that follows already trims the slice down until it fits, and truncationTarget is still the byte budget it trims to, so the returned string is unchanged for every input that did not panic. The existing TestTruncate table passes untouched.

How tested

Two new tests fail on unmodified main and pass with the fix.

TestTruncateInBytesFewerRunesThanBytes in notify/util_test.go covers two-byte, three-byte and four-byte runes at the Webex limit. It asserts the result stays within the limit, ends with the truncation marker, is a prefix of the input, and keeps as many whole runes as the byte budget allows.

TestWebexTruncatesMultiByteMessage in notify/webex/webex_test.go drives Notify end to end with a 3720 character Cyrillic annotation against an httptest server, and asserts the posted markdown fits maxMessageSize.

On unmodified main, with only the two test files applied:

--- FAIL: TestTruncateInBytesFewerRunesThanBytes/two-byte_runes
panic: runtime error: slice bounds out of range [:7436] with capacity 4096
	notify/util.go:139
	notify/util_test.go:165
FAIL	github.com/prometheus/alertmanager/notify	0.415s

--- FAIL: TestWebexTruncatesMultiByteMessage
panic: runtime error: slice bounds out of range [:7436] with capacity 4096
	notify/util.go:139
	notify/webex/webex.go:89
	notify/webex/webex_test.go:205
FAIL	github.com/prometheus/alertmanager/notify/webex	0.365s

The third test change is one more case in the existing TestTruncate table, TruncateInBytes("😀😀", 7). Two runes, eight bytes: the byte budget left for the text is larger than the number of runes to pick it from. That case passes on main as well, because the rune slice happens to have enough spare capacity for the out of length index to be legal, and the loop then trims the zero runes it picked up back off. It is here to pin the small end of the same mistake.

With the fix applied, on Go 1.26.4 and golangci-lint v2.13.1, the version pinned in Makefile.common:

go test ./notify/ ./notify/webex/                  ok
go test -race -count=5 ./notify/ ./notify/webex/   ok
go test ./notify/...                               ok, all 20 packages
go vet ./notify/ ./notify/webex/                   clean
golangci-lint fmt ./notify/...                     no changes
golangci-lint run ./notify/ ./notify/webex/        0 issues
go build ./notify/...                              ok

I also ran go test across the 76 Go packages that build without the generated UI assets. They all pass except TestDefaultConfigFilesOthersWithXDGConfigHome in cli, which fails identically on unmodified main on macOS, because os.UserConfigDir returns ~/Library/Application Support there and ignores XDG_CONFIG_HOME. go build ./... needs ui/app/dist, which a fresh clone does not have, so I built and tested by package list instead.

Pull Request Checklist

Please check all the applicable boxes.

  • Please list all open issue(s) discussed with maintainers related to this change
    • None. I found this by reading the code, so there is no issue to link.
  • Is this a new Receiver integration?
  • Is this a bugfix?
    • I have added tests that can reproduce the bug which pass with this bugfix applied
  • Is this a new feature?
    • I have added tests that test the new feature's functionality
  • Does this change affect performance?
    • I have provided benchmarks comparison that shows performance is improved or is not degraded
      • You can use benchstat to compare benchmarks
    • I have added new benchmarks if required or requested by maintainers
  • Is this a breaking change?
    • My changes do not break the existing cluster messages
    • My changes do not break the existing api
  • I have added/updated the required documentation. The unreleased section of CHANGELOG.md asks for behaviour notes in the pull request description, so the entry is in the release notes block below. No configuration surface changes.
  • I have signed-off my commits
  • I will follow best practices for contributing to this project

Which user-facing changes does this PR introduce?

[BUGFIX] notify: Fix a panic that crashed Alertmanager when a Webex notification was over the 7439 byte message limit while holding fewer runes than that, which is the case for any sufficiently long non-ASCII message.

TruncateInBytes indexed the rune slice with a byte count. In notify/util.go
it built the starting slice as r[:truncationTarget], where r is []rune(s) and
truncationTarget is n-3, a number of bytes. A string holding multi-byte
characters has fewer runes than bytes, so whenever the input was longer than
n bytes but held fewer than n-3 runes the index ran past the end of the rune
slice and panicked with "slice bounds out of range".

The Webex notifier is the only caller. It truncates the rendered message to
maxMessageSize (7439 bytes) in notify/webex/webex.go:89, so a notification
whose markdown was 7440 bytes or more of non-ASCII text crashed Alertmanager:
3720 Cyrillic characters, 2480 CJK characters or 1860 emoji are enough. The
notification pipeline runs in a goroutine started by the dispatcher and
nothing recovers, so the process exits.

Clamp the starting index to the number of runes available. The loop that
follows already trims the slice down until it fits the byte budget, so the
result is unchanged for every input that did not panic.

Add a case to TestTruncate covering a short string whose byte budget exceeds
its rune count, TestTruncateInBytesFewerRunesThanBytes covering two-, three-
and four-byte runes at the Webex limit, and TestWebexTruncatesMultiByteMessage
covering the notifier end to end.

Signed-off-by: Max Freedom Pollard <272618364+MaxFreedomPollard@users.noreply.github.com>
@MaxFreedomPollard
MaxFreedomPollard requested a review from a team as a code owner September 6, 2026 02:53
@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 8993542e-d43a-448e-add5-1f027bdfdd0e

📥 Commits

Reviewing files that changed from the base of the PR and between 1d82938 and 18399d4.

📒 Files selected for processing (3)
  • notify/util.go
  • notify/util_test.go
  • notify/webex/webex_test.go

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

TruncateInBytes now safely handles multibyte strings when the byte limit exceeds the rune count. Utility tests and a Webex integration test verify truncation without panics and within the configured byte limit.

Changes

Multibyte truncation handling

Layer / File(s) Summary
Safe rune-bound truncation
notify/util.go, notify/util_test.go, notify/webex/webex_test.go
TruncateInBytes limits its initial rune slice to the available runes. Tests cover multibyte utility inputs and Webex messages above the byte limit. The Webex test verifies a bounded, prefix-preserving markdown result.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 18399

Multibyte Webex messages exceeding the byte limit now truncate safely instead of panicking, while preserving the configured payload bound. The focused utility and Webex coverage supports merge readiness.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the affected area and the primary bug fix: preventing a panic when truncating multi-byte messages.
Description check ✅ Passed The description is complete and relevant. It explains the root cause, implementation, user impact, tests, validation results, checklist status, and release notes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.

1 participant