[SECURITY] Format String Vulnerability in GNU nano Buffer Error Message Display

Jonathan Echavarria <[email protected]> Tue, 31 Mar 2026 15:26:44 -0400
Newsgroups gmane.editors.nano.devel
Message-ID <CAFMnhCq2GN1Z1nV6hDJaqws1PFFkxv-Ljue_=1+oe50RdmpzaQ@mail.gmail.com>
*Overview:*
A format string vulnerability exists in GNU nano's multi-buffer error
message handling. When a user opens multiple files at startup and one
triggers an ALERT-level error (such as permission denied), the
pre-formatted error message is stored and later passed directly as the
format string argument to statusline() when the user switches buffers.

If the filename contains printf format specifiers (for example: %s, %p,
%n), they are re interpreted by vsnprintf() on the second call, leading to
stack information disclosure, denial of service (crash), or *potentially*
arbitrary memory writes on platforms where %n is not disabled.

This appears to have been introduced in 5.7 (ede64d7e) and affects all
versions through 8.7, when compiled with ENABLE_MULTIBUFFER and without
NANO_TINY.

*Cause*:
The error message is stored *after* being formatted by `vsnprintf()` in`
statusline()`:

```c
// winio.c:2326: first call (during file open), msg = "Error reading %s: %s"
vsnprintf(compound, MAXCHARLEN * COLS + 1, msg, ap);

// winio.c:2341: store the already-formatted result
openfile->errormessage = copy_of(compound);
```

When the user switches buffers, the stored string is passed as the format
string to statusline():

```c
// files.c:582
statusline(ALERT, openfile->errormessage);
```

This causes `vsnprintf()` to re-parse any `%` specifiers embedded in the
filename portion of the stored message. As no corresponding arguments are
provided, the function reads from (or writes to, in the case of `%n`)
uninitialized stack memory.

`%p` results in a pointer leak
`%s` results in a SIGSEGV crash as it dereferences garbage pointers
`%x` results in disclosing stack data
`%n` results in an arbitrary memory write, but is only present on some
platforms.

On platforms where `%n` is functional (glibc), an attacker who controls a
filename can write controlled byte values to addresses found on the stack.
On 32-bit glibc systems, where the format string resides on the stack, this
enables classic format string write-what-where exploitation. On 64-bit
glibc, exploitation is limited to writing to addresses already present in
registers or on the stack at the time of the call.

*A simple Information Leak POC:*
```sh
# Create a file with format specifiers in its name, make it unreadable
touch '/tmp/evil_%p_%p_%p_%p.txt'
chmod 000 '/tmp/evil_%p_%p_%p_%p.txt'

# Create a normal readable file
echo hello > /tmp/normal.txt
nano /tmp/normal.txt '/tmp/evil_%p_%p_%p_%p.txt'
# Press ALT+. to switch to the next buffer and trigger the pointer leak
```

*Additional POC that results in a crash:*
```sh
touch '/tmp/evil_%s_%s_%s_%s.txt'
chmod 000 '/tmp/evil_%s_%s_%s_%s.txt'
nano /tmp/normal.txt '/tmp/evil_%s_%s_%s_%s.txt'
# Press ALT+. to switch to the next buffer and trigger the crash
```

*Suggested fix:*
```diff
--- a/src/files.c
+++ b/src/files.c
@@ -579,7 +579,7 @@ void redecorate_after_switch(void)
  /* If the switched-to buffer gave an error during opening, show the
message
  * once; otherwise, indicate on the status bar which file we switched to.
*/
  if (openfile->errormessage) {
- statusline(ALERT, openfile->errormessage);
+ statusline(ALERT, "%s", openfile->errormessage);
  free(openfile->errormessage);
  openfile->errormessage = NULL;
  } else
```

Considering hardening these additional call sites:

```diff
--- a/src/files.c
+++ b/src/files.c
@@ -781,7 +781,7 @@ ssize_t read_file(FILE *f, int fd, const char
*filename, bool undoable)

  /* If there was a real error during the reading, let the user know. */
  if (ferror(f) && errornumber != EINTR && errornumber != 0)
- statusline(ALERT, strerror(errornumber));
+ statusline(ALERT, "%s", strerror(errornumber));
```

```diff
--- a/src/winio.c
+++ b/src/winio.c
     void warn_and_briefly_pause(const char *msg)
     {
     blank_bottombars();
-    statusline(ALERT, msg);
+    statusline(ALERT, "%s", msg);
```

*Timeline:*

- 2021-03-13: Vulnerable code introduced (commit `ede64d7e`)
- 2021-04-29: First affected release (v5.7)
- 2023-03-27: Two analogous format string bugs fixed (Savannah #63964), but
this instance missed
- 2026-03-30: This vulnerability identified
- 2026-03-31: This vulnerability disclosed

I have verified this vulnerability on 7.2 and, additionally
compiled v8.7-83-gf16e8d88 with ASAN and confirmed it is still present.

Regards,

Jonathan Echavarria