Repository: dotnet/runtime
Pull request #114692: Fix inconsistent newline format when logging exceptions to console.
Author: login: lateralusX
Labels: name: area-VM-coreclr
Created: 2025-04-15T15:13:53Z | Merged: 2025-04-22T21:22:07Z

The introduction of `minipal_log_write` API's have flushed out some inconsistency in newline format used on Windows. Previously the Windows implementation logged exceptions to console using `WriteFile` together with `STD_ERR_HANDLE`. It turns out that the file mode of this handle is binary, while the file mode for regular stdout/stderr is text mode. `minipal_log_write` API uses underlying _write instead of `WriteFile` and the fd associated with stdout/stderr, but since they are in text mode, pre-formatted data intended for binary output will expand `\r\n` to `\r\r\n` that could cause side effects due to additional `\r` character in the stream. The use of `WriteFile` handled this case, but we also mixed regular `\n` using the same API, that won't be expanded to `\r\n` when written to a file in binary mode leaving us with output using different line endings through the same logging.

Normally all logging performed from native code use `\n` and expects it to be replaced to `\r\n` on Windows as part of logging through a file in text mode. Some data formatted in managed code intended for output is already pre-formatted, using `\r\n`, intended for files in binary mode. One area where we mix line ending formats is the logging of unhandled exception call stacks, where we currently mix formatted newlines `\r\n` with `\n`.

This fix implement support in `minipal_log_write` to detect if logging should go through binary file mode on Windows, and since this is the default on none Windows platforms its a noop on all other platforms. Since we already needed to take strlen in minipal_log_write, the fix integrate logic to also check for \r\n pattern while calculating the length, leading to no additional pass over the string to detect if a logging needs to go into binary mode instead of default mode..

Fixes https://github.com/dotnet/runtime/issues/114579


Diff:
diff --git a/src/coreclr/vm/excep.cpp b/src/coreclr/vm/excep.cpp
index 25512830088f88..d3bd131910e86c 100644
--- a/src/coreclr/vm/excep.cpp
+++ b/src/coreclr/vm/excep.cpp
@@ -4793,9 +4793,9 @@ DefaultCatchHandlerExceptionMessageWorker(Thread* pThread,
         {
             message.Append(exceptionMessage);
         }
-        message.Append(W("\n"));
 
         PrintToStdErrW(message.GetUnicode());
+        PrintToStdErrA("\n");
 
 #if defined(FEATURE_EVENT_TRACE) && !defined(TARGET_UNIX)
         // Send the log to Windows Event Log
diff --git a/src/native/minipal/log.c b/src/native/minipal/log.c
index 6bbcfc1b02b1fe..5b62d4803ddb40 100644
--- a/src/native/minipal/log.c
+++ b/src/native/minipal/log.c
@@ -224,33 +224,43 @@ void minipal_log_flush_all(void)
 
 typedef ptrdiff_t ssize_t;
 
-static int sync_file(minipal_log_flags flags)
+static HANDLE get_std_handle(minipal_log_flags flags)
 {
     switch(flags)
     {
     case minipal_log_flags_fatal:
     case minipal_log_flags_error:
-        FlushFileBuffers(GetStdHandle(STD_ERROR_HANDLE));
-        break;
+        return GetStdHandle(STD_ERROR_HANDLE);
     case minipal_log_flags_warning:
     case minipal_log_flags_info:
     case minipal_log_flags_debug:
     case minipal_log_flags_verbose:
-    default:
-        FlushFileBuffers(GetStdHandle(STD_OUTPUT_HANDLE));
-        break;
+        return GetStdHandle(STD_OUTPUT_HANDLE);
     }
 
+    return INVALID_HANDLE_VALUE;
+}
+
+
+static int sync_file(minipal_log_flags flags)
+{
+    FlushFileBuffers(get_std_handle(flags));
     return 0;
 }
 
-static ssize_t write_file(int fd, const char* msg, size_t bytes_to_write)
+static ssize_t write_file_binary(minipal_log_flags flags, const char* msg, size_t bytes_to_write)
 {
     assert(bytes_to_write < INT_MAX);
-    return _write(fd, msg, (unsigned int)bytes_to_write);
+
+    DWORD bytes_written = 0;
+    return WriteFile(get_std_handle(flags), msg, (DWORD)bytes_to_write, &bytes_written, NULL) ? (ssize_t)bytes_written : (ssize_t)-1;
 }
 
-#define fileno _fileno
+static ssize_t write_file(minipal_log_flags flags, const char* msg, size_t bytes_to_write)
+{
+    assert(bytes_to_write < INT_MAX);
+    return _write(_fileno(get_std_file(flags)), msg, (unsigned int)bytes_to_write);
+}
 #else
 #if defined(__APPLE__)
 #include <fcntl.h>
@@ -280,25 +290,47 @@ static int sync_file(minipal_log_flags flags)
 }
 #endif
 
-static ssize_t write_file(int fd, const char* msg, size_t bytes_to_write)
+static ssize_t write_file(minipal_log_flags flags, const char* msg, size_t bytes_to_write)
 {
     ssize_t ret = 0;
-    while ((ret = write(fd, msg, bytes_to_write)) < 0 && errno == EINTR);
+    while ((ret = write(fileno(get_std_file(flags)), msg, bytes_to_write)) < 0 && errno == EINTR);
     return ret;
 }
 #endif
 
+typedef ssize_t (*write_file_fnptr)(minipal_log_flags flags, const char* msg, size_t bytes_to_write);
+
 int minipal_log_write(minipal_log_flags flags, const char* msg)
 {
     assert(msg != NULL && msg[0] != '\0');
 
-    size_t bytes_to_write = strlen(msg);
+    size_t bytes_to_write = 0;
     size_t bytes_written = 0;
 
-    int fd = fileno(get_std_file(flags));
+    write_file_fnptr write_fnptr = write_file;
+
+#ifdef HOST_WINDOWS
+    const char* msg_char = msg;
+    while (*msg_char)
+    {   if (msg_char[0] == '\r' && msg_char[1] == '\n')
+        {
+            write_fnptr = write_file_binary;
+            break;
+        }
+        msg_char++;
+    }
+
+    while (*msg_char)
+        msg_char++;
+
+    bytes_to_write = msg_char - msg;
+#else
+    bytes_to_write = strlen(msg);
+#endif
+
     while (bytes_to_write > 0)
     {
-        ssize_t chunk_written = write_file(fd, msg, bytes_to_write < MINIPAL_LOG_MAX_PAYLOAD ? bytes_to_write : MINIPAL_LOG_MAX_PAYLOAD);
+        ssize_t chunk_written = write_fnptr(flags, msg, bytes_to_write < MINIPAL_LOG_MAX_PAYLOAD ? bytes_to_write : MINIPAL_LOG_MAX_PAYLOAD);
         if (chunk_written <= 0)
             break;
 


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR fixes inconsistent newline formatting when logging exceptions on Windows by introducing a new binary logging mode flag.  
- Adds a new flag (minipal_log_flags_binary) and related macros in log.h  
- Updates logging functions in log.c to choose between binary (WriteFile) and text (_write) logging  
- Modifies PrintToStdErr functions in util.hpp/util.cpp and related calls in excep.cpp and eepolicy.cpp to support binary logging mode

### Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File                                | Description                                                        |
| ----------------------------------- | ------------------------------------------------------------------ |
| src/native/minipal/log.h            | Introduces binary flag and macros for flag manipulation            |
| src/native/minipal/log.c            | Refactors logging functions to use the new binary flag appropriately |
| src/coreclr/vm/util.hpp             | Updates PrintToStdErr function signatures to accept a binary flag    |
| src/coreclr/vm/util.cpp             | Implements changes in PrintToStdErr functions using the new flag      |
| src/coreclr/vm/excep.cpp            | Adjusts exception logging to use binary mode for pre-formatted data    |
| src/coreclr/vm/eepolicy.cpp         | Updates logging calls to pass the binary flag where appropriate        |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**src/coreclr/vm/excep.cpp:4798**
* [nitpick] Consider explicitly specifying the binaryOutputMode parameter in the PrintToStdErrA call (e.g., PrintToStdErrA("\n", TRUE)) for consistency with the preceding PrintToStdErrW call that uses binary output mode, unless the default behavior is intentional.
```
PrintToStdErrA("
");
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T15:14:18Z
user: login: janvorli
body: LGTM, thank you!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-16T19:57:34Z
user: login: lateralusX
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T08:27:45Z
user: login: jkotas
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-21T04:08:40Z
user: login: janvorli
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T15:48:38Z
user: login: janvorli
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-22T18:39:45Z

Review comments:
diff_hunk: @@ -23,6 +24,11 @@ typedef enum
     minipal_log_flags_verbose = 1 << 6
 } minipal_log_flags;
 
+#define minipal_log_flags_append(flags, flag) (flags = (minipal_log_flags)(flags | flag))
+#define minipal_log_flags_remove(flags, flag) (flags = (minipal_log_flags)(flags & ~flag))
+#define minipal_log_flags_get_level(flags) ((minipal_log_flags)(flags & ~(minipal_log_flags_binary)))
+#define minipal_log_flags_is_binary(flags) ((flags) & minipal_log_flags_binary)
path: src/native/minipal/log.h
user: login: Copilot
body: Add a clarifying comment that explains the purpose of minipal_log_flags_binary: it indicates that logging should bypass newline translation on Windows by using binary file mode.
created_at: 2025-04-15T15:14:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -280,25 +293,47 @@ static int sync_file(minipal_log_flags flags)
 }
 #endif
 
-static ssize_t write_file(int fd, const char* msg, size_t bytes_to_write)
+static ssize_t write_file(minipal_log_flags flags, const char* msg, size_t bytes_to_write)
 {
     ssize_t ret = 0;
-    while ((ret = write(fd, msg, bytes_to_write)) < 0 && errno == EINTR);
+    while ((ret = write(fileno(get_std_file(flags)), msg, bytes_to_write)) < 0 && errno == EINTR);
     return ret;
 }
 #endif
 
+typedef ssize_t (*write_file_fnptr)(minipal_log_flags flags, const char* msg, size_t bytes_to_write);
+
 int minipal_log_write(minipal_log_flags flags, const char* msg)
 {
     assert(msg != NULL && msg[0] != '\0');
 
-    size_t bytes_to_write = strlen(msg);
+    size_t bytes_to_write = 0;
     size_t bytes_written = 0;
 
-    int fd = fileno(get_std_file(flags));
+    write_file_fnptr write_fnptr = write_file;
+
+#ifdef HOST_WINDOWS
+    const char* msg_char = msg;
+    while (*msg_char)
path: src/native/minipal/log.c
user: login: janvorli
body: A nit - you can use `msg_char = strstr(msg, "\r\n")` for a shorter code
created_at: 2025-04-16T19:57:23Z
author_association: MEMBER
diff_hunk: @@ -280,25 +293,47 @@ static int sync_file(minipal_log_flags flags)
 }
 #endif
 
-static ssize_t write_file(int fd, const char* msg, size_t bytes_to_write)
+static ssize_t write_file(minipal_log_flags flags, const char* msg, size_t bytes_to_write)
 {
     ssize_t ret = 0;
-    while ((ret = write(fd, msg, bytes_to_write)) < 0 && errno == EINTR);
+    while ((ret = write(fileno(get_std_file(flags)), msg, bytes_to_write)) < 0 && errno == EINTR);
     return ret;
 }
 #endif
 
+typedef ssize_t (*write_file_fnptr)(minipal_log_flags flags, const char* msg, size_t bytes_to_write);
+
 int minipal_log_write(minipal_log_flags flags, const char* msg)
 {
     assert(msg != NULL && msg[0] != '\0');
 
-    size_t bytes_to_write = strlen(msg);
+    size_t bytes_to_write = 0;
     size_t bytes_written = 0;
 
-    int fd = fileno(get_std_file(flags));
+    write_file_fnptr write_fnptr = write_file;
+
+#ifdef HOST_WINDOWS
+    const char* msg_char = msg;
+    while (*msg_char)
path: src/native/minipal/log.c
user: login: lateralusX
body: Its actually intentional since we need string length so this does both in one loop over the string. If we used strstrs we would need to do strlen as well, meaning two passes over the string, so this is more performant way of doing both.
created_at: 2025-04-17T08:27:44Z
author_association: MEMBER
diff_hunk: @@ -224,33 +224,46 @@ void minipal_log_flush_all(void)
 
 typedef ptrdiff_t ssize_t;
 
-static int sync_file(minipal_log_flags flags)
+static HANDLE get_std_handle(minipal_log_flags flags)
 {
     switch(flags)
     {
     case minipal_log_flags_fatal:
     case minipal_log_flags_error:
-        FlushFileBuffers(GetStdHandle(STD_ERROR_HANDLE));
+        return GetStdHandle(STD_ERROR_HANDLE);
         break;
path: src/native/minipal/log.c
user: login: jkotas
body: ```suggestion
```
created_at: 2025-04-21T04:04:42Z
author_association: MEMBER
diff_hunk: @@ -224,33 +224,46 @@ void minipal_log_flush_all(void)
 
 typedef ptrdiff_t ssize_t;
 
-static int sync_file(minipal_log_flags flags)
+static HANDLE get_std_handle(minipal_log_flags flags)
 {
     switch(flags)
     {
     case minipal_log_flags_fatal:
     case minipal_log_flags_error:
-        FlushFileBuffers(GetStdHandle(STD_ERROR_HANDLE));
+        return GetStdHandle(STD_ERROR_HANDLE);
         break;
     case minipal_log_flags_warning:
     case minipal_log_flags_info:
     case minipal_log_flags_debug:
     case minipal_log_flags_verbose:
     default:
-        FlushFileBuffers(GetStdHandle(STD_OUTPUT_HANDLE));
+        return GetStdHandle(STD_OUTPUT_HANDLE);
         break;
path: src/native/minipal/log.c
user: login: jkotas
body: ```suggestion
```
created_at: 2025-04-21T04:04:51Z
author_association: MEMBER
diff_hunk: @@ -224,33 +224,46 @@ void minipal_log_flush_all(void)
 
 typedef ptrdiff_t ssize_t;
 
-static int sync_file(minipal_log_flags flags)
+static HANDLE get_std_handle(minipal_log_flags flags)
 {
     switch(flags)
     {
     case minipal_log_flags_fatal:
     case minipal_log_flags_error:
-        FlushFileBuffers(GetStdHandle(STD_ERROR_HANDLE));
+        return GetStdHandle(STD_ERROR_HANDLE);
         break;
     case minipal_log_flags_warning:
     case minipal_log_flags_info:
     case minipal_log_flags_debug:
     case minipal_log_flags_verbose:
     default:
path: src/native/minipal/log.c
user: login: jkotas
body: ```suggestion
```
created_at: 2025-04-21T04:08:36Z
author_association: MEMBER
diff_hunk: @@ -280,25 +293,47 @@ static int sync_file(minipal_log_flags flags)
 }
 #endif
 
-static ssize_t write_file(int fd, const char* msg, size_t bytes_to_write)
+static ssize_t write_file(minipal_log_flags flags, const char* msg, size_t bytes_to_write)
 {
     ssize_t ret = 0;
-    while ((ret = write(fd, msg, bytes_to_write)) < 0 && errno == EINTR);
+    while ((ret = write(fileno(get_std_file(flags)), msg, bytes_to_write)) < 0 && errno == EINTR);
     return ret;
 }
 #endif
 
+typedef ssize_t (*write_file_fnptr)(minipal_log_flags flags, const char* msg, size_t bytes_to_write);
+
 int minipal_log_write(minipal_log_flags flags, const char* msg)
 {
     assert(msg != NULL && msg[0] != '\0');
 
-    size_t bytes_to_write = strlen(msg);
+    size_t bytes_to_write = 0;
     size_t bytes_written = 0;
 
-    int fd = fileno(get_std_file(flags));
+    write_file_fnptr write_fnptr = write_file;
+
+#ifdef HOST_WINDOWS
+    const char* msg_char = msg;
+    while (*msg_char)
path: src/native/minipal/log.c
user: login: janvorli
body: Ah, right, I have not realized that when the string is not found, strstr would return null while we still want the pointer to the end of the string.
created_at: 2025-04-22T15:48:38Z
author_association: MEMBER

PR comments:
user: login: dotnet-policy-service[bot]
created_at: 2025-04-15T15:14:43Z
body: Tagging subscribers to this area: @mangod9
See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed.
<!-- Policy app identification https://img.shields.io/static/v1?label=PullRequestIssueManagement. -->
author_association: CONTRIBUTOR
user: login: lateralusX
created_at: 2025-04-16T10:57:20Z
body: Looking into taking this fix closer to minipal_log_write internals, stay tuned.
author_association: MEMBER
user: login: lateralusX
created_at: 2025-04-22T17:53:16Z
body: Looks like the failure in NET481 happened on other PR\s as well, https://[helixr1107v0xdcypoyl9e7f.blob.core.windows.net/dotnet-runtime-refs-pull-114783-merge-677c12ce065b4a5b86/System.Net.Http.WinHttpHandler.Functional.Tests/3/console.24750079.log?helixlogtype=result](https://helixr1107v0xdcypoyl9e7f.blob.core.windows.net/dotnet-runtime-refs-pull-114783-merge-677c12ce065b4a5b86/System.Net.Http.WinHttpHandler.Functional.Tests/3/console.24750079.log?helixlogtype=result)

It is fixed here, https://github.com/dotnet/runtime/issues/114790.
author_association: MEMBER
user: login: lateralusX
created_at: 2025-04-22T17:54:08Z
body: So if we end up with that as the only failure blocking build analysis, I think we can override and merge. @jkotas, agree? If so maybe we should try to get this in before preview 4 branch?
author_association: MEMBER
user: login: lateralusX
created_at: 2025-04-22T20:11:47Z
body: /ba-g Failing test has been fixed here, https://github.com/dotnet/runtime/issues/114790.
author_association: MEMBER