dnkl/foot
41
2.0k
Fork
You've already forked foot
243

uri_parse: out-of-bounds read on malformed text/uri-list #2353

Closed
opened 2026年05月21日 15:01:00 +02:00 by jackoftrade · 0 comments

Foot Version

foot 1.27.0

TERM environment variable

foot

Compositor Name and Version

sway 1.11

Distribution

arch

Terminal multiplexer

No response

Shell, TUI, application

No response

Server/standalone mode

  • Standalone
  • Server

Foot config

default

Description of Bug and Steps to Reproduce

The %xx decode loop in uri_parse() (uri.c) reads next[1] and next[2]
without checking that two bytes remain:

const char *next = memchr(encoded, '%', encoded_len);
...
if (hex2nibble(next[1]) <= 15 && hex2nibble(next[2]) <= 15) {
*p++ = hex2nibble(next[1]) << 4 | hex2nibble(next[2]);
encoded_len -= 3; // underflows if encoded_len < 3
encoded = next + 3;
}

A URI ending in '%' makes next[1]/next[2] read past the end of the
input.

The OSC 7 / OSC 8 / url-mode callers pass strlen-terminated strings, so
a trailing '%' short-circuits safely there. The clipboard /
drag-and-drop text/uri-list path does not: in selection.c,
fdm_receive_decoder_uri() xrealloc's ctx->buf.data to exactly the data
size (no NUL terminator, no slack), and fdm_receive_finish_uri() calls
decode_one_uri(ctx, ctx->buf.data, ctx->buf.idx) -> uri_parse(). For a
file:// URI, query_start/fragment_start are NULL so path_len reaches
the end of the buffer and a trailing '%' is the last byte.

If the two out-of-bounds bytes are valid hex, encoded_len (==1) -= 3
underflows to a huge size_t and the decode loop then memcpy/strncpy
runs out of bounds into 'decoded' (allocated only path_len + 1).

Triggering requires a Wayland client that offers a malformed
text/uri-list (last entry not terminated by CRLF, ending in '%') plus
a user paste/drop. RFC 2483 specifies that every URI is followed by
CRLF, so well-behaved clipboard owners never produce the trigger
input. The bug is still worth fixing as a defensive-coding hardening,
not because there is a credible exploit path in normal use.

Reproduce: drive the clipboard URI pipeline with a 10-byte buffer
sized exactly to the payload (no NUL slack):

payload: "file:///a%" (10 bytes, no trailing CRLF)
buffer: xrealloc'd to exactly 10 bytes by fdm_receive_decoder_uri()
call: fdm_receive_finish_uri() -> decode_one_uri() -> uri_parse()

Fix: require at least 3 bytes remaining before reading next[1]/next[2],
otherwise treat '%' literally; alternatively, NUL-terminate the
clipboard URI buffer before parsing.

Relevant logs, stacktraces, etc.

Debug log (selection.c + uri.c)

[*] driving REAL begin/decoder/finish uri pipeline with 10-byte
non-terminated payload ending in '%'

==ERROR== AddressSanitizer: heap-buffer-overflow
READ of size 1
#0 uri_parse (uri.c:198, the next[1] access)
#1 decode_one_uri (selection.c)
0 bytes after a 10-byte region
allocated by:
#1 xrealloc
#2 fdm_receive_decoder_uri (selection.c)

### Foot Version foot 1.27.0 ### TERM environment variable foot ### Compositor Name and Version sway 1.11 ### Distribution arch ### Terminal multiplexer _No response_ ### Shell, TUI, application _No response_ ### Server/standalone mode - [ ] Standalone - [ ] Server ### Foot config ```ini default ``` ### Description of Bug and Steps to Reproduce The %xx decode loop in uri_parse() (uri.c) reads next[1] and next[2] without checking that two bytes remain: const char *next = memchr(encoded, '%', encoded_len); ... if (hex2nibble(next[1]) <= 15 && hex2nibble(next[2]) <= 15) { *p++ = hex2nibble(next[1]) << 4 | hex2nibble(next[2]); encoded_len -= 3; // underflows if encoded_len < 3 encoded = next + 3; } A URI ending in '%' makes next[1]/next[2] read past the end of the input. The OSC 7 / OSC 8 / url-mode callers pass strlen-terminated strings, so a trailing '%' short-circuits safely there. The clipboard / drag-and-drop text/uri-list path does not: in selection.c, fdm_receive_decoder_uri() xrealloc's ctx->buf.data to exactly the data size (no NUL terminator, no slack), and fdm_receive_finish_uri() calls decode_one_uri(ctx, ctx->buf.data, ctx->buf.idx) -> uri_parse(). For a file:// URI, query_start/fragment_start are NULL so path_len reaches the end of the buffer and a trailing '%' is the last byte. If the two out-of-bounds bytes are valid hex, encoded_len (==1) -= 3 underflows to a huge size_t and the decode loop then memcpy/strncpy runs out of bounds into 'decoded' (allocated only path_len + 1). Triggering requires a Wayland client that offers a malformed text/uri-list (last entry not terminated by CRLF, ending in '%') plus a user paste/drop. RFC 2483 specifies that every URI is followed by CRLF, so well-behaved clipboard owners never produce the trigger input. The bug is still worth fixing as a defensive-coding hardening, not because there is a credible exploit path in normal use. Reproduce: drive the clipboard URI pipeline with a 10-byte buffer sized exactly to the payload (no NUL slack): payload: "file:///a%" (10 bytes, no trailing CRLF) buffer: xrealloc'd to exactly 10 bytes by fdm_receive_decoder_uri() call: fdm_receive_finish_uri() -> decode_one_uri() -> uri_parse() Fix: require at least 3 bytes remaining before reading next[1]/next[2], otherwise treat '%' literally; alternatively, NUL-terminate the clipboard URI buffer before parsing. ### Relevant logs, stacktraces, etc. Debug log (selection.c + uri.c) [*] driving REAL begin/decoder/finish uri pipeline with 10-byte non-terminated payload ending in '%' ==ERROR== AddressSanitizer: heap-buffer-overflow READ of size 1 #0 uri_parse (uri.c:198, the next[1] access) #1 decode_one_uri (selection.c) 0 bytes after a 10-byte region allocated by: #1 xrealloc #2 fdm_receive_decoder_uri (selection.c)
Sign in to join this conversation.
No Branch/Tag specified
master
osc-5522
sixel-heap-buffer-overflow
releases/1.27
releases/1.26
releases/1.25
releases/1.24
multi-cursor
releases/1.23
pixman-16f-2
releases/1.22
releases/1.21
releases/1.20
releases/1.19
releases/1.18
releases/1.17
releases/1.16
releases/1.15
releases/1.14
releases/1.13
releases/1.12
releases/1.11
releases/1.10
releases/1.9
releases/1.8
releases/1.7
releases/1.6
releases/1.5
releases/1.4
releases/1.3
releases/1.2
releases/1.1
releases/1.0
1.27.0
1.26.1
1.26.0
1.25.0
1.24.0
1.23.1
1.23.0
1.22.3
1.22.2
1.22.1
1.22.0
1.21.0
1.20.2
1.20.1
1.20.0
1.19.0
1.18.1
1.18.0
1.17.2
1.17.1
1.17.0
1.16.2
1.16.1
1.16.0
1.15.3
1.15.2
1.15.1
1.15.0
1.14.0
1.13.1
1.13.0
1.12.1
1.12.0
1.11.0
1.10.3
1.10.2
1.10.1
1.10.0
1.9.2
1.9.1
1.9.0
1.8.2
1.8.1
1.8.0
1.7.2
1.7.1
1.7.0
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.0
1.2.3
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
0.9.0
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
dnkl/foot#2353
Reference in a new issue
dnkl/foot
No description provided.
Delete branch "%!s()"

Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?