Skip to content

fix(security): use the public Bytes API instead of the constructor - #7

Merged
Ch4s3 merged 2 commits into
mainfrom
claude/array-backed-bytes-compat
Aug 11, 2026
Merged

Ch4s3 merged 2 commits into
mainfrom
claude/array-backed-bytes-compat

Conversation

@Ch4s3

@Ch4s3 Ch4s3 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Unblocks march#247 (array-backed Bytes), which is merged on march main.

Why

#247 changes type Bytes = Bytes(List(Int)) to type Bytes = Bytes(String) — a contiguous buffer instead of a cons list. Five sites in lib/security/ reach through the constructor and break against it:

file site
crypto.march bytes_to_hex destructured Bytes(xs)
crypto.march bytes_to_string destructured Bytes(xs)
crypto.march built Bytes(byte_list) directly
hkdf.march built Bytes(Nil) (twice)
hkdf.march built Bytes(Cons(i, Nil))

What changed

All five go through the public API — Bytes.to_list, Bytes.from_list, Bytes.to_string, Bytes.empty — which exists in both representations with identical behaviour.

bytes_to_string collapses to Bytes.to_string, which it was reimplementing by hand. Since #247 that is a single buffer read rather than a per-byte walk.

Verification

Backward compatible: 369 tests, 0 failures on the current toolchain.

Forward compatible: the exact API surface this migrates to produces identical output under both representations —

                          cons-list      array-backed
empty len:                0              0
from_list -> to_string:   hi!            hi!
concat len:               4              4
to_list length:           4              4
get(0):                   65             65

(Run directly rather than through bastion's suite, because of the caveat below.)

Caveat — necessary but not sufficient

This does not make bastion build against march main. bastion still declares the pre-split capability names (needs IO.File, IO.Dir) and hits the capability ceiling on IO.FileRead / IO.FileWrite across Bastion, Static, Forge.Lower, Forge.BuildIslands, and the test module. That migration is a separate change and is not attempted here.

So the ordering for anything downstream is: this PR → bastion's capability migration → a bastion release → consumers bump their toolchain.

march-language/march#247 changes Bytes from a cons list to a contiguous
buffer (`Bytes(String)`). Five sites here reached through the constructor and
break against it:

  crypto.march  bytes_to_hex / bytes_to_string destructured `Bytes(xs)`
  crypto.march  built `Bytes(byte_list)` directly
  hkdf.march    built `Bytes(Nil)` and `Bytes(Cons(i, Nil))`

All five now go through `Bytes.to_list` / `from_list` / `to_string` / `empty`,
which exist in both representations and behave identically — verified by
running the same program under a cons-list toolchain and an array-backed one.

bytes_to_string collapses to `Bytes.to_string`, which it was reimplementing;
since #247 that is a single buffer read rather than a per-byte walk.

Backward compatible: 369 tests pass on the current toolchain.

NOTE: this is necessary but not sufficient for bastion to build against march
main. bastion still declares the pre-split capability names (`needs IO.File,
IO.Dir`) and hits the capability ceiling on IO.FileRead/IO.FileWrite across
Bastion, Static, Forge.Lower and Forge.BuildIslands. That migration is
separate from this change.
bastion did not build against march `main` for reasons independent of the
Bytes change in the previous commit. March enforces capabilities in three
separate places, and bastion tripped all three:

  1. typecheck   "function body calls a builtin that requires Cap(X)"
  2. codegen     "CAPABILITY CEILING: a module's emitted code must stay
                 within its own needs" — 13 violations, mostly file I/O
  3. imports     "module M imports N which requires Cap(X)" — propagation,
                 which is why BastionServer needs IO.Process purely for
                 importing Bastion

Also drops `needs IO.File, IO.Dir` from the test module. Those are not
capabilities in march's lattice — it has IO.FileRead, IO.FileWrite and
IO.FileSystem — so the declaration never granted anything, which is why the
file-I/O ceiling violations appeared despite it looking like they were
covered.

16 lines across 7 files. Every one is exactly what the compiler named; none
is wider. No behaviour change — these declare authority the code already
exercises.

Verified against march main (cbb8346e, the revision CI resolves): 369 tests,
0 failures.
@Ch4s3
Ch4s3 merged commit 8157d0a into main Aug 11, 2026
2 checks passed
Ch4s3 added a commit that referenced this pull request Aug 11, 2026
Ships #7: the public-Bytes-API migration for march#247 (array-backed Bytes),
and the capability declarations march main now enforces at typecheck, codegen
ceiling, and import propagation.

Needed as a RELEASE, not just a merge — same reason as 0.2.4: forgepm resolves
bastion from the registry (`bastion = "0.2.4"`), so its CI still typechecks the
published 0.2.4 sources and fails on exactly the errors #7 fixed. A git dep is
not an alternative here: this repo is private and forgepm's CI clones git deps
anonymously.

Verified against march main (cbb8346e): 369 tests, 0 failures.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant