Skip to content

Small fixes: subtle, two test races, SAFETY notes, json offset, comment cleanup - #143

Merged
rendro merged 7 commits into
w1/build-releasefrom
w1/small-fixes
Oct 23, 2026
Merged

rendro merged 7 commits into
w1/build-releasefrom
w1/small-fixes

Conversation

@rendro

@rendro rendro commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Wave 1, lane S, step 3 (stacked on #140): small fixes from the 0.21 and 0.22 reviews, approved by the owner on 2026-10-22. No language change except a corrected number.

  • The constant-time comparison in the crypto module uses the subtle crate (already in the dependency graph) instead of a hand-written loop.
  • Two runtime-panic tests no longer share a temp directory; the 0.0.0.0 listener test skips with a message when the machine's default route does not reach it (for example behind a VPN).
  • SAFETY comments on two unsafe blocks; a stale test citation in main.rs fixed; the help-text test reads silt --help instead of a comment.
  • json.Syntax reports a real 0-based byte offset into the whole text (it reported serde's column, wrong for any input with more than one line). No existing golden moves; a new one fails on the base.
  • "round NN" comments from the old audit loop reworded or dropped (outside the type checker, which lane T is rewriting).

Verified by an independent verifier; its findings are fixed. The owner approved every new message text on 2026-10-23.

The hand-written OR-accumulating loop gave no guarantee that the
optimizer kept it free of an early exit. `subtle::ConstantTimeEq`
(already in the dependency graph through sha2/hmac, now a direct
dependency) is written for exactly that. Nothing a program sees
changes: equal buffers are equal, buffers of different lengths are
unequal at once (the lengths may leak, the contents do not).
- tests/cli/runtime_panic_tests.rs: two tests named their run "worker",
  so when the harness runs them as threads of one process they shared
  one temp directory and could remove each other's files. The
  builtin-of-a-task test's run is now "builtin_of_a_task".
- tests/lang/http_bind_address_tests.rs: the test of a listener on
  0.0.0.0 failed wherever the discovered LAN address is not reachable
  at all (a firewall, a sandbox). It now first checks that a plain std
  listener on 0.0.0.0 is reached there, and skips the LAN probe with a
  line on stderr when it is not.
…hat exists

- src/main.rs: the SIGPIPE block at the top of `main` and
  src/scheduler.rs `no_death_by_a_closed_stdout` now say why they are
  sound. Every `unsafe` in src/ has a SAFETY comment.
- src/main.rs cited tests/cli/main_stack_size_lock_tests.rs, a source
  grep that no longer exists; it now names the golden case that needs
  the reserve.
- src/main.rs kept a list of the help descriptions in a comment, and
  tests/lang/examples_check.rs parsed that comment. The test now reads
  the descriptions of the eight subcommands from `silt --help` itself
  and checks README.md and docs/getting-started.md against them; the
  comment is gone.
The offset of `json.Syntax(message, offset)` was serde's column: the
1-based count of bytes in the error's line, wrong for every error past
the first line. It is now the 0-based byte offset of the text, as
`toml.Syntax` has it: where the parser stopped, or the text's length
for a text that ends too soon. The message is serde's, unchanged.

  input                                    before   after
  [1,}                                     4        3
  {"a":1                (ends too soon)    6        6
  {\n  "name": "x",\n  "n": 1,\n  "tags": [1 2]\n}
                        (line 4 column 14) 14       40
  [1,\n\n2,\n x]        (line 4 column 2)  2        9

No existing expected output changes: the three goldens that show an
offset hold a text that ends too soon (`{"x": 1` and the toml case's
json text, whose offset was serde's column and equals the length) or an
offset the program wrote itself (json.Syntax("bad digits", 3)).
New golden lang/stdlib/json_syntax__the_offset_counts_bytes_from_the_
start_of_the_text has the four inputs above and fails on the base.
docs/stdlib/json.md says what the offset is.

Also: tests/lang/examples_check.rs drops a `trim_start` before
`split_whitespace` that clippy refuses.
About 125 comment lines outside src/typechecker named an audit round
("Round-101:", "(round-75 DX-4)", "Audit round 87 LATENT", ...). The
labels are dropped where the comment reads on without them, and the
sentence is reworded where it leaned on the round ("prior to this
round", "pre-round-87", "round 93 made it a parse error"). No code
changes; one test-hook panic text loses its round number with the test
that matches it.

Left for later, by ruling: src/typechecker (79 lines, lane T rewrites
those files) and the one line in src/vm/dispatch.rs about the
`.display()` gate, which lane T removes. Test files and test names are
not touched.
rendro added a commit that referenced this pull request Oct 23, 2026
Fix round 1, B4. serde reports a raw newline inside a string at
column 0 of the next line; the offset was the first byte of that
line, one past the newline. Column 0 now means the newline that ends
the line before: `{"a": "<newline>x"}` gives 7. All 19 inputs of the
verifier's probe (lsv/p/js) give the offset their name says, but one:
04_unicode_x_at_17, whose `x` is at byte 18 (the name counts é and €
as one byte too few; the output 18 is right).
The golden json_syntax__the_offset_counts_bytes_from_the_start_of_the_
text gets the case.
@rendro
rendro marked this pull request as ready for review October 23, 2026 14:56
One conflict, src/main.rs: lane L extended the help-phrase comment
(init --lib, remove), which this branch removes because the test reads
the descriptions from `silt --help` itself. The comment stays removed;
the test's subcommands gain `remove`, so its description is checked
against README.md and docs/getting-started.md as lane L's list did.
@rendro
rendro merged commit 316529f into main Oct 23, 2026
23 of 46 checks passed
rendro added a commit that referenced this pull request Oct 23, 2026
…ence warning (#144)

* Docs: the faults the 0.21 review found

- loops-and-pipes.md: "loop() works inside closures" said more than is
  true. A closure can hold a loop of its own, but a `loop(...)` inside
  a closure cannot go on with a loop outside it (that is an error);
  the paragraph now says both.
- bindings-and-functions.md: `return` in a closure ends the closure's
  call, not the function around it, so a callback cannot end a fold
  early; the page says so and shows list.fold_until doing it.
- toml.md: the toml.pretty section showed the error text of
  toml.stringify ("toml.pretty: unsupported value kind Fn" is what it
  prints).
- io-fs.md: fs.copy and fs.remove said `Err(message)`; they give
  `Err(IoError)`.
- getting-started.md: the read_file block used `io` and `string`
  without importing them and called a character count "bytes"; it is
  now a whole program that runs. Section 5 says what a library package
  is (its src/lib.silt is what `import <name>` gives a dependent).
- modules.md: `task` was labelled "Structured concurrency", which silt
  does not have (a cancel does not reach a task's children): now
  "Tasks".
- New page docs/language/error-codes.md: every diagnostic code with
  what it means, by phase, linked from the language guide. A meta test
  checks that it lists each code of `Code::ALL` once and no other.

* Concurrency docs: a channel whose buffer holds the channel stays until the end

A value in a channel's buffer that holds that same channel (in a record
field, or captured by a closure) keeps the channel alive: when the
program drops the channel while the value is still buffered, the
channel and its buffer stay in memory until the program ends. The
limitations section of docs/concurrency.md now says so, and what to do
(receive what is buffered first, or send a value that does not hold
the channel).

* (#140) PEM errors read as they did with rustls-pemfile

Fix round 1, B3. rustls-pki-types prints its PEM errors in its own
words: "missing section end marker: [80, 82, ...]" for a section that
does not end, and "base64 decode error: InvalidCharacter(33)". The
texts are mapped back to the ones 0.23 printed:

  parse private key: section end "PRIVATE KEY" missing
  parse cert chain: InvalidCharacter(33)
  (and `illegal section start: "..."`)

Every case of the verifier's probe (lsv/p/tls_probe.silt, 18 cert/key
pairs) now prints what the 0.23 binary prints. New golden
lang/tcp_tls/tls_server__a_pem_that_does_not_parse_says_why (a cert
whose section does not end, one with a non-base64 character): it holds
on the base and fails on eff35f1.

* (#143) json.Syntax at a raw newline in a string is the newline's offset

Fix round 1, B4. serde reports a raw newline inside a string at
column 0 of the next line; the offset was the first byte of that
line, one past the newline. Column 0 now means the newline that ends
the line before: `{"a": "<newline>x"}` gives 7. All 19 inputs of the
verifier's probe (lsv/p/js) give the offset their name says, but one:
04_unicode_x_at_17, whose `x` is at byte 18 (the name counts é and €
as one byte too few; the output 18 is right).
The golden json_syntax__the_offset_counts_bytes_from_the_start_of_the_
text gets the case.

* (#144) Docs: a git dependency names its ref; error-code rows say what they are for

Fix round 1, B5 and G2.
- getting-started.md and modules.md wrote `silt add <name> --git <url>`,
  which `silt add` refuses ("--git requires exactly one of --rev,
  --branch, or --tag"). Both now write `--tag <tag>` and name the other
  two. The command as written gets past argument checking and fails
  only for the network (probed with an unreachable URL).
- error-codes.md, checked against `silt check --format json` on small
  programs for 37 of the codes:
  E0305 is a field or method the type at hand lacks (`p.b` on a record
  without `b`, a literal or update field its type does not declare),
  not "a field no record in scope declares" (the enum's comment in
  src/diagnostic.rs says the same now);
  E0302 is a name in a pin pattern that nothing binds (a bare unknown
  name is E0201);
  E0303 and E0304 say where they are reported (an anonymous record's
  alias as a literal; a record literal or pattern whose type name names
  no record);
  E0321 is a `_` arm that is not last in a `match` without a scrutinee;
  E0315 is reported by nothing today, and its row says so.

* (#135) io_op test doc: env.set is refused while an operation runs

Fix round 1, N1: the doc of an_operation_is_running_until_it_returns_whoever_waits said env.set waits for running operations; it refuses.

* (#140) The release job asks for no actions: read

Fix round 1, N3. The job checks out the tag, downloads this run's own artifacts, writes SHA256SUMS, attests and makes the release: none of that reads another run's artifacts, which is what actions: read is for. It keeps contents: write, id-token: write and attestations: write. (The top-level permissions, which the other jobs use, are unchanged.)

* (#135) env.set and env.remove work until the first task or I/O, never after

Owner decision V1 (2026-10-23), replacing the rule of 94bfe93. That
rule refused a change while another task was live or an I/O operation
ran, so whether a program could change its environment depended on
when its tasks ended (the verifier's B1/B2/G3). Now it does not depend
on timing: the environment can be changed until the program starts its
first task or I/O operation, and is refused from then on, also once
they have ended:

  env.set cannot change the environment once tasks or I/O have
  started: set it at the start of main

(env.remove likewise). A task is anything the scheduler runs
(task.spawn, task.deadline, a stream's stages, a server's handlers); an
I/O operation is one handed to the I/O pool (files, stdin, connections,
requests). Printing, env.get, fs.exists and tcp.listen start nothing.
The REPL keeps the rule per session; `silt test` per run.

The scheduler keeps one flag, set by its first task and by the pool's
first operation. The counters 94bfe93 added for the old rule
(Scheduler::alone, the pool's `running` count and IoPool::quiet, with
their unit test) are gone: nothing else used them. The SAFETY comments,
docs/stdlib/io-fs.md and docs/ffi.md give the new reason: before the
first task or I/O no thread of the runtime that may read the
environment exists.

Goldens lang/stdlib/env_change__*: works at the start of main; refused
after a joined task, a finished stream (env.remove), a file read, a
task.deadline body, a cancelled server, from a spawned task, and in a
REPL session after its first task. They replace the goldens of the old
rule.

* (#135) Strings, Bytes and channel buffers stop at 10,000,000 too

Owner decision V2 (2026-10-23), the verifier's G1. Three growers had no
bound; a loop that doubles one took the machine's memory (the 0.23
binary is killed at a 2 GB cap by each of the doubling goldens):

  string interpolation: result would exceed maximum string size
    (N bytes > 10000000 limit)
  bytes.concat: result would exceed maximum size (N bytes > 10000000 limit)
  bytes.concat_all: result would exceed maximum size (N bytes > 10000000 limit)
  channel.new: capacity exceeds maximum size of 10000000 elements

Found with the same eye and capped in the form string.repeat and
string.replace already have (a new text for the owner's list):

  string.join: result would exceed maximum string size
    (N bytes > 10000000 limit)

bytes.concat_all and string.join count the size before they copy a
byte: their items can be one value many times, so their result was not
bounded by the memory the program already held. An interpolation is
checked after it is written, as its parts are in memory already.

Goldens lang/limits/collection_cap__{string_interpolation,
bytes_concat, bytes_concat_all, string_join, channel_capacity}_past_the_cap
fail on the base; the concat_all, join and channel ones also show that
exactly ten million is allowed. docs/language/collections.md names them.

* (#144) The usage of silt add shows the git ref as required

Ruling on fix-round finding (1). `silt --help`, README.md,
docs/getting-started.md and the "missing source flag" error showed
`--git <url> [--rev|--branch|--tag <ref>]`, brackets that say the ref
may be left out; `silt add` refuses a git dependency without one. They
now read `--git <url> (--rev|--branch|--tag) <ref>` (the same width, so
the help's columns stay where they were). The comments in
src/cli/add.rs, src/cli/help.rs and src/main.rs that quote it follow.

* (#135) An HTTP body is at most 10,000,000 bytes, the most a Bytes holds

Ruling on the V2 collision (provisional, for the owner's page). The
body limit of the HTTP server (requests it reads) and client
(responses it reads) was 10 MiB, 10,485,760 bytes: more than the
10,000,000 a Bytes may now hold, so a program could be given a body it
could not make. BODY_MAX is now MAX_COLLECTION_LEN, one bound:

  the body is larger than 10000000 bytes   (was: ... than 10 MiB)

docs/stdlib/http.md and errors.md say 10,000,000 bytes (the LSP
hover golden that shows errors.md follows). The golden
http_bodies__a_body_over_the_limit_is_an_error_not_a_cut_body tests
10,000,000 and 10,000,001: a handler sends the body at the limit, and a
server written by hand sends the larger ones in parts, since no program
can make them.

Two server tests answered with a 64 MB body built by bytes.concat_all
(more than the system takes in for a client that reads nothing); they
now read it from a file the test writes (Server::big_answer), as a
program still can: a string read from a file is not capped.

* (#135) The text a builtin writes is no longer than a string may be

Ruling (3) on the growers found with V2 (provisional, for the owner's
page). These could make far more than the program holds, from one long
string used many times; each now stops at the 10,000,000-byte limit,
in the form string.repeat and string.replace have:

  <fn>: result would exceed maximum string size (N bytes > 10000000 limit)

for regex.replace, regex.replace_all, regex.replace_all_with,
encoding.form_encode, json.stringify, json.pretty, toml.stringify,
toml.pretty, string.from, and the text of a value shown in a string
interpolation (which already capped what it joins).

How: regex.replace(_all) build their text match by match (as
Regex::replacen does) and stop at the limit; replace_all_with and
form_encode check as they append; JSON is written into a Bounded writer
(builtins::common) that refuses the write that would pass the limit;
string.from and interpolation show the value into one
(Vm::shown_within); TOML, whose writer cannot be stopped, is refused on
a count of the fewest bytes its data takes, then on its length. Where a
text is stopped as it is written, N is as far as it had come (it would
have been longer); println, panic and `.display()` are not bounded.

Goldens lang/limits/collection_cap__{regex_replace_all,
regex_replace_all_with, form_encode, json_stringify, toml_pretty,
string_from}_past_the_cap. docs/language/collections.md lists them.
builtins::common is pub(crate) now, for the VM's use of Bounded.

* (#135) The deep-value test shows what a string can hold, and the rest is refused

CI's release-only job: deep_values_are_shown_inspected_and_dropped
showed a value 1,500,000 levels deep in a string interpolation, a text
of 12,000,004 bytes, which the string cap of ad43ff2 refuses. The test
guards that showing, inspecting and dropping a deep value goes down no
native stack. It now:
- shows `a` at the deepest level whose text a string holds (1,249,999
  levels in a release build, 9,999,996 bytes; the debug depth of
  150,000 as before), still far beyond any native stack;
- inspects `b`, and a tuple holding both, at the full depth (io.inspect
  is not capped);
- shows `b` at the full depth and asserts the cap's error, exit 1: the
  text is stopped as it is written, not by an overflow (in a debug
  build, whose depth fits, it asserts the length).
Passes in both builds (cargo nextest --release / debug, -E
'test(deep_value)').
@rendro
rendro deleted the w1/small-fixes branch October 23, 2026 17:37
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