Skip to content

[Microsoft.Android.Build.BaseTasks] Guard LogCodedError overloads#12093

Merged
simonrozsival merged 1 commit into
mainfrom
jonathanpeppers-verbose-waddle
Jul 14, 2026
Merged

[Microsoft.Android.Build.BaseTasks] Guard LogCodedError overloads#12093
simonrozsival merged 1 commit into
mainfrom
jonathanpeppers-verbose-waddle

Conversation

@jonathanpeppers

Copy link
Copy Markdown
Member

Why this is necessary

#11994 fixed the XA0145 emulator boot-timeout error rendering literal {0}/{1} placeholders instead of the AVD name and timeout. The underlying cause was overload resolution, and it can bite any coded-error call whose trailing arguments happen to be (string, int):

LogCodedError ("XA0145", Resources.XA0145, Device, BootTimeoutSeconds);

This binds to LogCodedError (code, message, string? file, int lineNumber) instead of the params object[] formatting overload, because a non-params method that matches the (string, int) tail exactly is always preferred. The result: the message is logged verbatim (placeholders never substituted) and Device/BootTimeoutSeconds are silently reinterpreted as a source file and line number. #11994 patched the one call site; this PR turns the whole class of bug into a build break.

Approach

  • Extract private sinks. The real logging body moved into LogCodedErrorCore / LogCodedWarningCore. Every public overload now delegates to them, so no overload routes through the trap internally.
  • Obsolete the trap overloads. LogCodedError (code, message, string? file, int lineNumber) and its LogCodedWarning twin are now [Obsolete (error: true)] with a message steering callers to either the formatting overload (code, message, params object[]) or the file-first overload (code, file, lineNumber, message, ...). Any accidental (string, int) tail now fails to compile with CS0619.
  • Fix a latent FormatException. A new FormatMessage helper returns the message verbatim when there are no args, so pre-formatted messages containing literal {/} (e.g. raw aapt2 output) are never run through string.Format.
  • Migrate the intentional callers. Aapt2's two file+line calls now use the file-first overload.

Notes for reviewers

  • The Log/log extension-method versions in MSBuildExtensions were never affected; that overload set has no ambiguous trailing overload, so nothing there changed.
  • I audited every AsyncTask caller in the repo: only BootAndroidEmulator (already fixed in Fix XA0145 emulator boot-timeout message rendering literal placeholders #11994) and Aapt2 used the trap shape. Everything else passes a trailing string/object or has the int in a non-trap position.
  • Verified by building Microsoft.Android.Build.BaseTasks clean and compiling a probe against the built assembly: the (string, int) tail now errors CS0619, while string.Format, file-first, and literal-brace forms all compile.

Unit tests

No new tests. The behavior is enforced at compile time (CS0619), and the runtime substitution is already covered by the BootAndroidEmulatorTests added in #11994.

PR #11994 fixed the XA0145 emulator boot-timeout message rendering
literal `{0}`/`{1}` placeholders instead of substituting the values.
The root cause was overload resolution: a call like

    LogCodedError ("XA0145", Resources.XA0145, Device, BootTimeoutSeconds);

binds to `LogCodedError (code, message, string? file, int lineNumber)`
rather than the `params object[]` formatting overload, because a
non-`params` method that matches the trailing `(string, int)` exactly is
always preferred. The message was logged verbatim and `Device` /
`BootTimeoutSeconds` were silently reinterpreted as a source file and
line number.

This makes the whole class of bug a compile error instead of a silent
runtime failure:

* Extract the real logging body into private `LogCodedErrorCore` /
  `LogCodedWarningCore` sinks; every public overload now delegates to
  them, so nothing routes through the trap overload internally.
* Mark the `(code, message, string? file, int lineNumber)` overloads on
  both `LogCodedError` and `LogCodedWarning` `[Obsolete (error: true)]`,
  with a message pointing callers at the formatting overload or the
  file-first overload. Any accidental `(string, int)` tail now fails to
  build with CS0619.
* Add a `FormatMessage` helper that returns the message verbatim when
  there are no arguments, so pre-formatted messages containing literal
  `{`/`}` are never passed through `string.Format`.
* Migrate the two intentional `Aapt2` callers to the file-first
  overload `LogCodedError (code, file, lineNumber, message)`.

The `Log`/`log` extension-method versions in `MSBuildExtensions` were
never affected: that overload set has no ambiguous trailing overload.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f03fd34e-2c48-48fe-a2af-b42ed2b4905c
Copilot AI review requested due to automatic review settings July 13, 2026 21:39

Copilot AI 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.

Pull request overview

This PR hardens Microsoft.Android.Build.BaseTasks.AsyncTask logging APIs against accidental overload resolution that can silently treat (string, int) formatting arguments as (file, lineNumber), preventing message placeholders from being substituted and misattributing source locations.

Changes:

  • Refactors AsyncTask to route all LogCodedError/LogCodedWarning overloads through private core sinks and marks the ambiguous (code, message, file, lineNumber) overloads as [Obsolete(error: true)] to force compile-time failures for the trap pattern.
  • Adds a FormatMessage helper to avoid string.Format when no args are provided, preventing FormatException for pre-formatted messages containing literal {/}.
  • Updates Aapt2 to use the file-first overload for the intentional file/line diagnostics.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/Xamarin.Android.Build.Tasks/Tasks/Aapt2.cs Migrates coded-error logging to the file-first overload to avoid the ambiguous (message, file, line) shape.
external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/AsyncTask.cs Introduces core logging sinks, compile-time guards for trap overloads, and safe message formatting behavior.

@jonathanpeppers

Copy link
Copy Markdown
Member Author

What about the TaskLoggingHelper extension methods?

There is an extension-method version of these in MSBuildExtensions.cs (called as Log.LogCodedError(...) / log.LogCodedError(...)), but it is structurally immune to this bug and needs no change:

// A: formatting
public static void LogCodedError (this TaskLoggingHelper log, string code, string message, params object[] messageArgs)
// B: file-first
public static void LogCodedError (this TaskLoggingHelper log, string code, string file, int lineNumber, string message, params object[] messageArgs)

The key difference from the old AsyncTask set: there is no (code, message, string file, int lineNumber) overload, and overload B places int lineNumber in the third position (right after file) rather than trailing. So a call with a stray (string, int) tail -- the exact shape that hijacked the AsyncTask call -- cannot bind to B (B requires the int up front), and it correctly falls through to the params formatting overload A.

Verified with a probe against the built assembly:

// binds to the formatting overload and substitutes correctly -- compiles clean
log.LogCodedError ("XA0145", "boot of {0} timed out after {1}s", "Pixel_6", 300);

So the two APIs were simply inconsistent: the extension set was already designed safely (file-first), while AsyncTask additionally carried the ambiguous trailing overload. This PR brings AsyncTask in line -- it keeps the same safe file-first overload the extensions already have, and obsoletes the extra trap overload the extensions never had. Nothing to change on the extension-method side.

@jonathanpeppers jonathanpeppers added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jul 14, 2026
@simonrozsival simonrozsival merged commit 94bac76 into main Jul 14, 2026
45 checks passed
@simonrozsival simonrozsival deleted the jonathanpeppers-verbose-waddle branch July 14, 2026 09:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants