Skip to content

fix(umsg): reject too few arguments instead of reading past the varargs - #408

Merged
filmil merged 1 commit into
mainfrom
fix-371-umsg-arg-count
Aug 16, 2026
Merged

filmil merged 1 commit into
mainfrom
fix-371-umsg-arg-count

Conversation

@filmil

@filmil filmil commented Aug 14, 2026

Copy link
Copy Markdown
Member

Fixes #371

Root cause

umsg_format is a C variadic function. It decides how many arguments to read from the pattern, not from what the caller passed. The number it reads is the highest argument index in the pattern, plus one.

The pattern in the bug report, "String : {1}", refers to index 1, so ICU reads two arguments. The report supplies one. ICU reads the second from past the end of the argument list and, because a bare {1} is a string argument, dereferences that indeterminate value as a UChar*.

Whether that crashes depends on what happens to be in the register or stack slot ICU reads. The reported program is on the lucky side on Linux with ICU 74.2 — the slot reads as NULL and ICU returns U_ILLEGAL_ARGUMENT_ERROR. Push the missing arguments onto the stack instead and it is not lucky:

// pattern "{0}{1}{2}{3}{4}{5}{6}{7}", three arguments supplied
let result = umsg::message_format!(
    fmt,
    { s.clone() => String },
    { s.clone() => String },
    { s.clone() => String },
)?;
$ cargo run -p rust_icu_umsg --example repro
pattern needs 8 args, passing 3
EXIT=139        # SIGSEGV

That is the same defect as the one reported, just with the indeterminate value reliably non-NULL.

The change

Scan the pattern for its highest argument index when the formatter is built, and refuse a format call that supplies fewer arguments than that. The check runs before the variadic call, so the out-of-bounds read never happens.

The scanner in the new pattern.rs mirrors ICU's MessagePattern grammar:

  • apostrophe quoting, including the context-sensitive cases — '# only quotes inside plural/selectordinal, '| only inside choice, and a doubled apostrophe inside quoted text keeps the text quoted;
  • nested submessages in plural, select, selectordinal and choice, whose arguments count too, while the braces that open a submessage after a selector do not;
  • simple argument styles such as {0,number,##.#}, whose braces and quotes hold no arguments;
  • offset: in a plural style.

It is conservative by construction. Anything it does not model exactly makes it decline to report a count, which leaves the previous behavior in place for that pattern. A count that is too large would reject calls ICU handles fine, so declining is always the safer answer. Named arguments ({name}) also return no count; umsg_format rejects those patterns itself, before it reads anything.

The argument type hazard is unaffected. A pattern cannot tell you what Rust type the caller wrote, so that one stays the caller's obligation and its #[ignore]d test stays as it is. The docs on message_format! and try_format now say which of the two obligations is checked and which is not.

Verification

Built against ICU 74.2 on Linux.

The scanner was checked against what ICU actually does, not just against its own expectations. For 47 patterns, a throwaway harness measured how many arguments umsg_format really reads and compared that with the scanner's answer. The measurement passes k valid UChar* arguments followed by NULLs: ICU raises U_ILLEGAL_ARGUMENT_ERROR as soon as it reads a NULL for a string argument, so the smallest k that succeeds is the number of arguments ICU reads. A second variant passes a leading f64 for the patterns whose argument 0 is a double (plural, choice, number, date), since a double travels in a different register class.

All 47 agree exactly. A sample:

String : {1}                                 icu=Some(2)  scanner=Some(2)  ok
{10}                                         icu=Some(11) scanner=Some(11) ok
'{9}' {0}                                    icu=Some(1)  scanner=Some(1)  ok
it's {9}                                     icu=Some(10) scanner=Some(10) ok
'{9} it'' still quoted {8}'                  icu=Some(0)  scanner=Some(0)  ok
{0,select,other{9}}                          icu=Some(1)  scanner=Some(1)  ok
{0,select,other{{1,select,other{{2}}}}}      icu=Some(3)  scanner=Some(3)  ok
{0,plural,one{one}other{# of {9}}}           icu=Some(10) scanner=Some(10) ok
{0,plural,other{'# {9}'}}                    icu=Some(1)  scanner=Some(1)  ok
{0,plural,offset:1 one{{1}}other{{2}}}       icu=Some(3)  scanner=Some(3)  ok
{0,choice,0#no files|1#one file|1<{1} files} icu=Some(2)  scanner=Some(2)  ok
{0,date,'{'yyyy'}'}                          icu=Some(1)  scanner=Some(1)  ok

The harness is not part of the change — it calls umsg_format with deliberately wrong arguments, which does not belong in the test suite.

The program from the bug report, unchanged, now prints an error rather than crashing:

$ cargo run -p rust_icu_umsg --example issue371
Error: Wrapper(message pattern needs 2 argument(s) because it refers to argument index 1, but 1 argument(s) were supplied)

arg_count_mismatch_is_undefined_behavior documented this crash and was #[ignore]d because it aborted the test binary. It now runs in the normal suite as too_few_arguments_is_an_error, alongside tests for contiguous indices, extra arguments still being harmless, nested arguments counting, and quoted arguments not counting.

$ cargo test -p rust_icu_umsg
test result: ok. 23 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out

$ cargo check --workspace
Finished `dev` profile [unoptimized + debuginfo] target(s) in 7.18s

$ cargo test --workspace
test result: ok. (all suites, 0 failed)

rust_icu_intl is the one in-tree caller of message_format!; its PluralRules pattern needs one argument and supplies one, and its tests pass. cargo clippy -p rust_icu_umsg --all-targets reports the same single pre-existing warning as main.

Nothing under .github/workflows/ is touched. rust_icu_umsg/BUILD.bazel globs src/**/*.rs, so the new module needs no Bazel change.


Generated by Claude Code

@google-cla

google-cla Bot commented Aug 14, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

`umsg_format` is a C variadic function. It decides how many arguments to
read from the *pattern*, not from what the caller passed. The number it
reads is the highest argument index in the pattern, plus one.

So `message_format!(fmt, {s => String})` on the pattern `"String : {1}"`
makes ICU read two arguments when only one was passed. It reads the
second from past the end of the argument list and, because a bare `{1}`
is a string argument, dereferences that indeterminate value as a
`UChar*`. That segfaults.

Scan the pattern for its highest argument index when the formatter is
built, and refuse a format call that supplies fewer arguments than that.
The check happens before the variadic call, so the out-of-bounds read
never occurs.

The scanner mirrors ICU's `MessagePattern` grammar: apostrophe quoting
(including the `#`-in-plural and `|`-in-choice cases), nested
submessages in `plural`, `select`, `selectordinal` and `choice`, and
simple argument styles, whose braces and quotes hold no arguments. It is
conservative by construction: anything it does not model exactly makes
it decline to report a count, which leaves the previous behavior in
place. A count that is too large would reject calls ICU handles fine, so
declining is always the safer answer.

Verified against ICU 74.2 by measuring, for 47 patterns, how many
arguments `umsg_format` really reads, and comparing that with the
scanner. All 47 agree. The measurement passes k valid `UChar*` arguments
followed by NULLs: ICU raises U_ILLEGAL_ARGUMENT_ERROR as soon as it
reads a NULL for a string argument, so the smallest k that succeeds is
the number of arguments ICU reads.

The program from the bug report changes from a segfault to:

    Error: Wrapper(message pattern needs 2 argument(s) because it
    refers to argument index 1, but 1 argument(s) were supplied)

`arg_count_mismatch_is_undefined_behavior` documented the crash and was
`#[ignore]`d because it aborted the test binary. It now runs in the
normal suite as `too_few_arguments_is_an_error`. The type-mismatch
hazard is unaffected and stays `#[ignore]`d: a pattern cannot tell you
what Rust type the caller wrote.

    $ cargo test -p rust_icu_umsg
    test result: ok. 23 passed; 0 failed; 1 ignored; 0 measured

    $ cargo check --workspace
    Finished `dev` profile [unoptimized + debuginfo] target(s)

    $ cargo test --workspace
    test result: ok. (all suites, 0 failed)
@filmil
filmil force-pushed the fix-371-umsg-arg-count branch from aba612d to f7097b9 Compare August 14, 2026 09:32

@clydegerber clydegerber left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be a better idea to fail formatting when the argument count cannot be scanned rather than continuing and passing it along to ICU? If a consumer fat fingers a message format such that it fails the scan, how easily will they be able to pinpoint the cause of the failure?

Comment thread rust_icu_umsg/src/lib.rs
Comment thread rust_icu_umsg/src/lib.rs
@filmil

filmil commented Aug 16, 2026

Copy link
Copy Markdown
Member Author

Would it be a better idea to fail formatting when the argument count cannot be scanned rather than continuing and passing it along to ICU? If a consumer fat fingers a message format such that it fails the scan, how easily will they be able to pinpoint the cause of the failure?

We might want to not restrict things that icu4c already permits, even if confusing.

@filmil
filmil merged commit 3c1d5d1 into main Aug 16, 2026
31 of 34 checks passed
@filmil
filmil deleted the fix-371-umsg-arg-count branch August 16, 2026 04:17
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.

Segfault when using message_format! on an invalid message format

2 participants