Replace simple CLR local strings - #12517
Conversation
There was a problem hiding this comment.
Pull request overview
This PR continues the effort to remove straightforward uses of the native local-string hierarchy in the CLR host/runtime by switching to fixed-size buffers (with snprintf) and by using a fixed-buffer system property retrieval API that rejects undersized buffers to avoid truncation.
Changes:
- Added a
monodroid_get_system_property(std::string_view, char*, size_t)overload and updated fixed-array callers to write directly into their buffers. - Reworked primary override directory path formatting to use
snprintfinto fixed buffers. - Replaced debug typemap full-name composition with fixed-buffer
snprintfformatting.
Show a summary per file
| File | Description |
|---|---|
| src/native/clr/runtime-base/android-system-shared.cc | Adds fixed-buffer monodroid_get_system_property overload that rejects undersized buffers and avoids truncation. |
| src/native/clr/include/runtime-base/android-system.hh | Switches property callers to fixed buffers and formats primary override dir using snprintf. |
| src/native/clr/host/typemap.cc | Uses snprintf into a fixed buffer to build managed type debug names for typemap lookup. |
| src/native/clr/host/assembly-store.cc | Uses a fixed property buffer for debug.net.asmcache instead of dynamic_local_property_string. |
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 1
- Review effort level: Lite
c111658 to
3a05311
Compare
2a03078 to
cda8d20
Compare
cda8d20 to
2e9c443
Compare
6d947da to
ddae8f9
Compare
ddae8f9 to
4478d1c
Compare
4478d1c to
ce12492
Compare
ce12492 to
85f0111
Compare
85f0111 to
8c8f213
Compare
4a079b8 to
fb358cf
Compare
fb358cf to
ea5dfcb
Compare
Narrowing the `strings.hh` include in `logger.hh` also removed two symbols that headers were picking up transitively through it: * `strings.hh` included `shared/helpers.hh`, which is where `os-bridge.hh` was getting `abort_unless` from. * `strings.hh` included `<unistd.h>`, which is where `bridge-processing.cc` was getting `gettid()` from. Include both explicitly at their point of use. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
968857f to
a7e70fe
Compare
Narrowing the strings.hh include in logger.hh removed the transitive path that util.cc relied on for dynamic_local_string, breaking the CoreCLR and NativeAOT builds. Include the header where it is used. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
|
/review |
|
✅ Android PR Reviewer completed successfully! Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
To allow these domains, add them to the network:
allowed:
- defaults
- "azcliprod.blob.core.windows.net"See Network Configuration for more information.
|
There was a problem hiding this comment.
⚠️ Needs Changes
Findings: 0 errors, 1 warning, 0 suggestions.
The fixed-buffer refactoring is focused, and the retrying path-format changes preserve dynamic growth where expected. However, bundled system-property values can exceed the Android platform limit, so rejecting them at the new fixed boundary silently changes existing logging, profiling, and timing behavior.
CI is not green: Package Tests macOS > Tests > APKs 1 is failing, and the aggregate dotnet-android check had not completed. Deeper failure details were unavailable because Azure DevOps requires authentication.
Warning
Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
azcliprod.blob.core.windows.net
To allow these domains, add them to the network.allowed list in your workflow frontmatter:
network:
allowed:
- defaults
- "azcliprod.blob.core.windows.net"See Network Configuration for more information.
Generated by Android PR Reviewer for #12517 · gpt56 · 347.6 AIC · ⌖ 8.93 AIC · ⊞ 25.7K
Comment /review to run again
Addresses review feedback: the fixed-buffer overload returned -1 when a bundled (build-time) property value did not fit into the caller's buffer, so long values were treated as if the property were not set at all. The `dynamic_local_string` based overload it replaced grew onto the heap and had no such limit. Bundled properties come from `@(AndroidEnvironment)` files and are stored as NUL-terminated strings in static application data, so they are neither subject to Android's 92 byte property limit nor in need of copying. Return a `std::string_view` instead of an `int`: for Android system properties it views the caller's scratch buffer, for bundled properties it points directly at the application data, which restores the previous behaviour and avoids a copy. `FastTiming::parse_options()` used to tokenize its argument in place, which is not safe for a view over static data, so it now parses without mutating. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
…perty
The previous commit changed `monodroid_get_system_property ()` to return a
`std::string_view` so that bundled properties, whose length is not limited by
`PROPERTY_VALUE_BUFFER_LEN`, could be returned without copying them into the
caller's scratch buffer.
That works, but `std::string_view` deliberately makes no promise about
NUL-termination, while every value this function can return happens to be
NUL-terminated: `__system_property_get ()` terminates what it writes, and
bundled properties are NUL-terminated strings in static application data. The
header had to document that invariant in a comment ("The returned value is
always NUL-terminated") precisely because the type denies it, and callers such
as `get_max_gref_count_from_system ()` silently relied on it by passing
`.data ()` to `strtol ()` and to a `%s` format specifier.
Return `const char*` instead (and `nullptr` when the property is not set). The
lifetime rule is unchanged and still uniform - the result is valid for at least
as long as the caller's buffer - but NUL-termination is now guaranteed by the
type rather than by a comment, so `.data ()` no longer has to be laundered
through a `std::string_view`. Callers that need to tokenize the value construct
a `std::string_view` explicitly, which is honest about what they are doing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
- `format_managed_type_name ()` builds the name with a single `snprintf ()` instead of three `memcpy ()` calls and hand-rolled length arithmetic. The negative-required-capacity retry contract is unchanged, and the helper is now guarded by `#if defined (DEBUG)` like its only caller, which removes the unused-function warning this pull request introduced. - `FastTiming::parse_options ()` takes a `const char*` again and tokenizes with `strchr ()`/`strncmp ()`/`strtoull ()`. It had been rewritten around `std::string_view`, which added a C++ layer to code that was already plain C. The parser still cannot NUL-terminate in place - the value may point at immortal bundled property data - so each parameter is bounded by its length instead. The `duration=` and `filename=` edge cases behave as they did before. - The property lookup chain (`monodroid_get_system_property ()`, `monodroid__system_property_get ()` and `lookup_system_property ()`) takes `const char *name`, matching the other overloads. Previously it took a `std::string_view` and immediately called `.data ()` on it, which is the same NUL-termination laundering that motivated changing the return type. This also lets `HostEnvironment::lookup_system_property ()` use `strcmp ()` directly and drops `<string_view>` from `android-system-shared.cc` entirely. - Shorten the comments added by this pull request, and drop the `strings.hh` include from `logger.cc`, which no longer uses `dynamic_local_string`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
`Logger::init_logging_categories ()` split the property value with `std::string_view`, and `set_category ()`, `set_log_file ()` and `open_file ()` took views as well. Tokenize the value with `strchr ()`/`strncmp ()` instead and pass the parameters around as a pointer and a length. This also removes a subtle NUL-termination assumption: `open_file ()` called `unlink (path.data ())`, which is only correct because every caller happened to pass a view over a NUL-terminated buffer. It now takes a `const char*`. A single `param_matches ()` helper does all of the comparisons, so the parameters no longer have to be NUL-terminated in place - the value may point at immortal bundled property data. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
The function returns either the caller's scratch buffer or a pointer into application data, which is easy to mistake for the "stack buffer or malloc" convention used elsewhere in this header, where the caller has to free the result when it differs from the buffer it passed in. Say so explicitly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
`Util::create_public_directory ()`, `Util::monodroid_fopen ()` and `Util::set_world_accessable ()` each took a `std::string_view` and immediately called `.data ()` on it to hand the path to `mkdir ()`, `fopen ()` or `chmod ()`. That is only correct because every caller happens to pass a view over a NUL-terminated buffer, which nothing enforces. Take a `const char*` instead, which is what these functions actually need. `Logger::open_file ()` and `Logger::init_reference_logging ()` follow, so `logger.cc` no longer refers to `std::string_view` at all and the `"..."sv` literals are gone with it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
Address issues found while reviewing the C string conversion of the system property APIs. `AndroidSystem::lookup_system_property()` returned `prop_iter->first`, which is the map *key* -- the property name -- rather than its value, while reporting `prop_iter->second.length ()` as the length. In a DEBUG build with bundled `@(AndroidEnvironment)` properties, callers such as `Logger::init_logging_categories()` and `get_max_gref_count_from_system()` therefore parsed the property name instead of the value. Return `prop_iter->second` instead. `monodroid__system_property_get()` had a fallback branch that copied through a heap buffer whenever the caller's buffer was smaller than `PROPERTY_VALUE_BUFFER_LEN`. Its only caller now rejects that case before calling, so the branch was dead -- and it wrote a terminating NUL one byte past the end of the caller's buffer. Remove it, along with the now-unused `sp_value_len` parameter. This also drops a `new[]`/`delete[]` pair, removing two more libc++ references from the object file. Passing an undersized buffer to `monodroid_get_system_property()` was reported as `nullptr`, indistinguishable from an unset property. It is a programming error, so `abort_unless()` on it instead. Finally, derive the "gref="/"lref=" prefix length with `sizeof()` rather than hardcoding it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
|
/review |
|
✅ Android PR Reviewer completed successfully!
|
There was a problem hiding this comment.
❌ Reject
Found 1 error and 0 warnings. The Mono DEBUG bundled-property lookup still returns the property name rather than its value through the newly added no-copy getter; this needs to be corrected for parity with the CLR fix in this PR.
The buffer lifetime contracts and exact-size fallback paths are otherwise well documented and consistently cleaned up. CI build 1572360 is still in progress; completed build and tooling lanes are passing with no reported failures so far.
Generated by Android PR Reviewer for #12517 · gpt56 · 331 AIC · ⌖ 9.09 AIC · ⊞ 25.7K
Comment /review to run again
… lookup `AndroidSystem::lookup_system_property (name, value_len)` returned `p->name` while reporting `p->value_len` as its length, so every bundled `debug.*` property in a MonoVM Debug build resolved to its own name instead of its configured value. When the value was longer than the name this also read past the end of the name buffer. The CoreCLR implementation already returns the value; this brings Mono to parity. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
|
/review |
|
✅ Android PR Reviewer completed successfully!
|
There was a problem hiding this comment.
❌ Reject
Found 1 error, 0 warnings, and 0 suggestions. The new pointer-returning property API correctly avoids copying bundled values, but get_max_gref_count_from_system() no longer distinguishes an empty override from a non-empty one; that can set the CoreCLR GREF threshold to zero and force repeated full collections.
CI is still running; the completed checks observed so far passed, with no failure reported at review time.
Generated by Android PR Reviewer for #12517 · gpt56 · 242.5 AIC · ⌖ 8.86 AIC · ⊞ 25.7K
Comment /review to run again
`monodroid_get_system_property()` returned a non-null pointer for a
bundled property that exists with an empty value, where the previous
`dynamic_local_string` overload reported a length of 0 and callers
skipped it.
`get_max_gref_count_from_system()` then ran `strtol ("")` and set the
max JNI global reference count to 0 instead of keeping the 51200
default, and `create_update_dir()` treated an empty `debug.mono.profile`
as a request to create the `.__override__` directory.
The real-property path already behaves this way -- `__system_property_get`
returns 0 for a property that is absent *and* for one set to an empty
string -- so the bundled path now matches it rather than fixing each
call site.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
* `Logger::open_file()` now bails out when `override_dir` is null or empty instead of passing it to `Util::create_public_directory()` and `Util::join_paths()`. The latter takes `std::string_view`, and constructing one from a null `const char*` is undefined behaviour. The old `std::string_view const&` signature made this impossible; `const char*` does not. * MonoVM's `monodroid_get_system_property()` now aborts on a too-small scratch buffer, matching the CoreCLR implementation. Silently returning `nullptr` made a programming error indistinguishable from "property not set". * `format_primary_override_dir()` checked `buffer == nullptr` only after already passing `buffer` to `snprintf()`. Assert up front. * Fix stray indentation on the `Logger::set_category()` declaration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
`monodroid_get_system_property()` claimed that a returned bundled property value "lives as long as the process". That is only true for NativeAOT, where the value points into the immortal app-environment blob. In Debug builds: * CoreCLR stores bundled properties in a `std::unordered_map<std::string, std::string>`, so re-assigning a property replaces the mapped `std::string`. * MonoVM's `add_system_property()` explicitly `free()`s `p->value` before installing the replacement. In both cases a previously returned pointer dangles. No current caller retains the pointer past the call, but the comment invited them to, so document the real guarantee instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
* `format_primary_override_dir()` passed `home.get_cstr ()` straight to `snprintf()`'s `%s`. `jstring_wrapper::get_cstr()` returns `nullptr` for a null `jstring`, and passing `nullptr` to `%s` is undefined behaviour. Abort with a clear message instead: an application without a files directory cannot work regardless. * `HostEnvironment::lookup_system_property()` left the `value_len` out-parameter untouched on both of its `nullptr` return paths, so the NativeAOT implementation of `AndroidSystem::lookup_system_property()` returned an uninitialized length while the CoreCLR one zeroed it first. The only caller happens to be safe because `||` short-circuits on the null pointer, but the two implementations of the same declaration should not disagree. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
`format_joined_path()`, `format_primary_override_dir()` and `format_managed_type_name()` all share one protocol: write into the supplied buffer and return either the formatted length or the negative required capacity. Each had its own open-coded "try the stack buffer, malloc and retry on failure" dance, two of which this branch had just introduced. Hoist that into `Util::format_with_retry()` and use it in all three places. This removes ~25 lines of triplicated code and, more importantly, leaves a single copy of the logic that has to get the retry, the abort-on-OOM and the `!= stack_buffer` ownership check right. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
|
/review |
|
✅ Android PR Reviewer completed successfully!
|
There was a problem hiding this comment.
Reviewed the current 18-file native-runtime diff against the repository’s native C++, JNI/interop, security, and compatibility rules. I found no new actionable defects in this revision; the earlier long bundled-property, Mono property value/name, and empty-value regressions are addressed by the latest commits.
CI is still in progress on Azure DevOps build #1572876: the completed checks are passing, with five checks still running and the aggregate check queued. No failing check is currently reported.
Generated by Android PR Reviewer for #12517 · gpt56 · 222.8 AIC · ⌖ 8.77 AIC · ⊞ 25.7K
Comment /review to run again
Summary
Remove simple CLR local strings while preserving dynamic growth where the old local-string type supported it.
Changes
malloc()storage(buffer, buffer_size)calls using the platform property boundmonodroid_get_system_property(..., dynamic_local_property_string&)overloadValidation
Follow-up: return
const char*rather thanstd::string_viewThe first pass here replaced the local string with a fixed caller-supplied buffer, which regressed bundled properties: those come from
@(AndroidEnvironment)and are not bound by Android'sPROP_VALUE_MAX, so any value longer than the buffer was reported as absent. Returning a view fixed that — Android system properties view the caller's scratch buffer, bundled properties point straight at immortal static application data, with no copy and no length cap.That part stands, but
std::string_viewwas the wrong vehicle for it. Every value the function can return is NUL-terminated (__system_property_get()terminates what it writes, and bundled properties are NUL-terminated static strings), yetstring_viewexplicitly promises the opposite. The invariant had to be asserted in a header comment — "The returned value is always NUL-terminated" — precisely because the type denied it, andget_max_gref_count_from_system()quietly depended on it by handing.data()tostrtol()and to a%sconversion.So the getter now returns
const char*(nullptrwhen the property is unset), in both the CoreCLR and MonoVM implementations:.data()no longer has to be laundered through astring_viewto reach a C API;Logger::init_logging_categories(),FastTiming::parse_options()) construct astd::string_viewexplicitly, which is honest about what they are doing.No change in libc++ references (
string_viewis header-only) — this is a type-safety fix, not a size one. All three runtime lanes build clean.Follow-up: fixes from the offline review
Reviewing the conversion above turned up three problems in the code it touches, all fixed in the last commit.
lookup_system_property()returned the property name, not its value. It returnedprop_iter->first— the map key — while reportingprop_iter->second.length()as the length. In a DEBUG build with bundled@(AndroidEnvironment)properties,Logger::init_logging_categories()andget_max_gref_count_from_system()were parsing the property name. This predates the PR, but the conversion above makes that value the direct return of the public getter, and the no-copy contract documented in the header describes exactly this path, so it belongs here.monodroid__system_property_get()wrote one byte past the end of the caller's buffer. Its small-buffer fallback copied through a heap buffer and then didsp_value[sp_value_len] = '\0'. The branch was already dead — the only caller rejects undersized buffers before calling — so thesp_value_lenparameter is gone along with it. That also drops anew[]/delete[]pair, removing two libc++ references. #12523 previously carried its own version of this removal; it is now fixed here, in the PR that makes the path unreachable.An undersized buffer was reported as an unset property. Returning
nullptrconflated a programming error with a legitimate runtime outcome, so it is anabort_unless()now.Also derives the
"gref="/"lref="prefix length withsizeof()instead of hardcoding5.All three runtime lanes build clean; libc++ references at the stack tip are unchanged at 21.