gren-lang / gren-lang/core

`Bytes.Decode.bytes` reads past the end of the `Bytes` it is decoding

Open Beginner friendly
#154 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.