diff --git a/common/console.c b/common/console.c
index 0724fc9f..b3e89473 100644
--- a/common/console.c
+++ b/common/console.c
@@ -122,7 +122,7 @@ void vprintk(const char *fmt, va_list args)
     unsigned int i;
     int rc;
 
-    rc = vsnprintf(buf, sizeof(buf), fmt, args);
+    rc = vsnprintf_internal(buf, sizeof(buf), fmt, args, LF_TO_CRLF);
 
     if ( rc > (int)sizeof(buf) )
         panic("vprintk() buffer overflow\n");
diff --git a/common/libc/vsnprintf.c b/common/libc/vsnprintf.c
index a49fd308..c907d42b 100644
--- a/common/libc/vsnprintf.c
+++ b/common/libc/vsnprintf.c
@@ -47,6 +47,7 @@ static int isdigit(int c)
 /* Conversions */
 #define UPPER     (1u << 5)
 #define SIGNED    (1u << 6)
+/* Careful not to overlap with vsnprintf_internal() flags. */
 
 /* Shorthand for ensuring str moves forwards, but not overruning the buffer. */
 #define PUT(c)                                  \
@@ -185,7 +186,11 @@ char *fmt_string(char *str, char *end, const char *val,
             PUT(' ');
 
     for ( i = 0; i < len; ++i )
+    {
+        if ( (flags & LF_TO_CRLF) && val[i] == '\n' )
+            PUT('\r');
         PUT(val[i]);
+    }
 
     while ( len < width-- )
         PUT(' ');
@@ -268,7 +273,8 @@ static char *pointer(
                       width, precision, flags);
 }
 
-int vsnprintf(char *buf, size_t size, const char *fmt, va_list args)
+int vsnprintf_internal(char *buf, size_t size, const char *fmt, va_list args,
+                       unsigned int caller_flags)
 {
     char *str = buf, *end = buf + size;
 
@@ -277,15 +283,15 @@ int vsnprintf(char *buf, size_t size, const char *fmt, va_list args)
         const char *spec_start = fmt; /* For rewinding on error. */
 
         unsigned long long num;
-        unsigned int base, flags = 0;
+        unsigned int base, flags = caller_flags;
         int width = -1, precision = -1;
-        char length_mod = 'i';
+        char c, length_mod = 'i';
 
         /* Put regular characters into the destination. */
         if ( *fmt != '%' )
         {
-            PUT(*fmt);
-            continue;
+            c = *fmt;
+            goto put_char;
         }
 
  next_flag: /* Process any flags. */
@@ -359,20 +365,21 @@ int vsnprintf(char *buf, size_t size, const char *fmt, va_list args)
             continue;
 
         case 'c': /* Unsigned char. */
-        {
-            unsigned char c = va_arg(args, int);
+            c = va_arg(args, int);
 
             if ( !(flags & LEFT) )
                 while ( --width > 0 )
                     PUT(' ');
 
+        put_char:
+            if ( (flags & LF_TO_CRLF) && c == '\n' )
+                PUT('\r');
             PUT(c);
 
             while ( --width > 0 )
                 PUT(' ');
 
             continue;
-        }
 
         case 's': /* String. */
             str = fmt_string(str, end, va_arg(args, const char *),
diff --git a/include/xtf/libc.h b/include/xtf/libc.h
index 66f834b4..f24a631f 100644
--- a/include/xtf/libc.h
+++ b/include/xtf/libc.h
@@ -37,8 +37,21 @@ int memcmp(const void *s1, const void *s2, size_t n);
 
 size_t strnlen(const char *str, size_t max);
 
+/*
+ * Internal version of vsnprintf(), taking extra control flags.
+ *
+ * LF_TO_CRLF causes "\n" to be expanded to "\r\n" in the output buffer.
+ */
+#define LF_TO_CRLF (1u << 7)
 int __printf(3, 0)
-    vsnprintf(char *buf, size_t size, const char *fmt, va_list args);
+    vsnprintf_internal(char *buf, size_t size, const char *fmt, va_list args,
+                       unsigned int flags);
+
+static inline int __printf(3, 0)
+    vsnprintf(char *buf, size_t size, const char *fmt, va_list args)
+{
+    return vsnprintf_internal(buf, size, fmt, args, 0);
+}
 
 int __printf(3, 4)
     snprintf(char *buf, size_t size, const char *fmt, ...);
diff --git a/tests/selftest/main.c b/tests/selftest/main.c
index c2f6e727..a5c205ba 100644
--- a/tests/selftest/main.c
+++ b/tests/selftest/main.c
@@ -340,6 +340,43 @@ static void test_driver_init(void)
         xtf_failure("Fail: xtf_init_grant_table(2) returned %d\n", rc);
 }
 
+static void test_vsnprintf_crlf_one(const char *fmt, ...)
+{
+    va_list args;
+
+    char buf[4];
+    int rc;
+
+    va_start(args, fmt);
+    rc = vsnprintf(buf, sizeof(buf), fmt, args);
+    va_end(args);
+
+    if ( rc != 1 )
+        return xtf_failure("Fail: '%s', expected length 1, got %d\n", fmt, rc);
+    if ( strcmp(buf, "\n") )
+        return xtf_failure("Fail: '%s', expected \"\\n\", got %*ph\n",
+                           fmt, (int)sizeof(buf), buf);
+
+    va_start(args, fmt);
+    rc = vsnprintf_internal(buf, sizeof(buf), fmt, args, LF_TO_CRLF);
+    va_end(args);
+
+    if ( rc != 2 )
+        return xtf_failure("Fail: '%s', expected length 2, got %d\n", fmt, rc);
+    if ( strcmp(buf, "\r\n") )
+        return xtf_failure("Fail: '%s', expected \"\\r\\n\", got %*ph\n",
+                           fmt, (int)sizeof(buf), buf);
+}
+
+static void test_vsnprintf_crlf(void)
+{
+    printk("Test: vsnprintf() with CRLF expansion\n");
+
+    test_vsnprintf_crlf_one("\n");
+    test_vsnprintf_crlf_one("%c", '\n');
+    test_vsnprintf_crlf_one("%s", "\n");
+}
+
 void test_main(void)
 {
     /*
@@ -368,6 +405,7 @@ void test_main(void)
     test_extable_handler();
     test_custom_idte();
     test_driver_init();
+    test_vsnprintf_crlf();
 
     if ( has_xenstore )
         test_xenstore();
