`Bytes.Decode.bytes` reads past the end of the `Bytes` it is decoding
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 48
- Forks
- 14
- Avg merge
- 4h 14m
- Merged PRs (30d)
- 1
Description
Found against: gren 0.6.6, gren-lang/core 7.4.2, node 22
Reproduction: in https://github.com/gilramir/gren-bug-reports, 2026-09-13-bytes-decode-past-slice; ./run.sh prints the
output under "Reproduction".
Summary
The usual way to read a length-prefixed message is to decode the length, take
that many bytes with Decode.bytes, and then decode the body with a decoder of
its own:
firstBody =
Decode.decode (Decode.unsignedInt8 |> Decode.andThen Decode.bytes) buffer
buffer is [3, 10, 20, 30, 2, 40, 50], so firstBody is [10, 20, 30] and
Bytes.length firstBody is 3. A decoder run over firstBody should see those
three bytes and nothing else. Decode.bytes sees the rest of buffer:
| call | result | expected |
|---|---|---|
Decode.decode (Decode.bytes 3) firstBody |
Just [10, 20, 30] |
Just [10, 20, 30] |
Decode.decode (Decode.bytes 4) firstBody |
Just [10, 20, 30, 2] |
Nothing |
Decode.decode (Decode.bytes 5) firstBody |
Just [10, 20, 30, 2, 40] |
Nothing |
Decode.decode (Decode.string 4) firstBody |
Just "\u{A}\u{14}\u{1E}\u{2}" |
Nothing |
four Decode.unsignedInt8 in a row (Decode.map4), over firstBody |
Nothing |
Nothing |
Decode.bytes 4 succeeds on the three-byte body. Its fourth byte is 2, the
length byte of the next message in buffer, and Decode.bytes 5 reaches 40,
that message's first byte. Decode.string 4 is built on Decode.bytes, so it
succeeds too, with the same four bytes read as characters. The last row is the
control. Reading those four bytes one at a time with Decode.unsignedInt8
fails, as it should.
A Bytes is a DataView onto a larger ArrayBuffer, and Decode.bytes returns
a view onto the same buffer rather than a copy. _Bytes_read_bytes makes that
view without checking it against the view it is reading:
var _Bytes_read_bytes = F3(function (len, bytes, offset) {
return {
__$offset: offset + len,
__$value: new DataView(bytes.buffer, bytes.byteOffset + offset, len),
};
});
The DataView constructor throws only when the new view would run past the end
of the whole ArrayBuffer. getUint8 and the other readers check against the
view's own byteLength, which is why unsignedInt8 stops where it should.
Reproduction
./run.sh builds src/Main.gren and runs it:
Each row is Decode.decode <decoder> firstBody, and firstBody is [10, 20, 30].
decoder result expected
Decode.bytes 3 Just [10, 20, 30] Just [10, 20, 30]
Decode.bytes 4 Just [10, 20, 30, 2] Nothing
Decode.bytes 5 Just [10, 20, 30, 2, 40] Nothing
Decode.string 4 Just "\u{A}\u{14}\u{1E}\u{2}" Nothing
four Decode.unsignedInt8 (control) Nothing Nothing
Suggested fix
Check the request against the view being read before making the new view
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Run ./run.sh in the linked reproduction to confirm the failing Decode.bytes cases. Then inspect _Bytes_read_bytes and its callers to understand the view bounds being used. Done means requests within firstBody still decode successfully, while requests beyond its length return Nothing without reading bytes from the following message.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100