Skip to content
zit Artifact [d62d7bd753]
Login

Artifact [d62d7bd753]

Artifact d62d7bd753bf64f4fa8123310c7ac4d5035ae3e5277b96493e05f8c282c30ea0:


# Erratum — draft-fossil-sync-protocol-00 §5.1

| | |
|---|---|
| **Document** | `draft-fossil-sync-protocol-00` |
| **Section** | 5.1 — Message Structure (byte-counted payloads) |
| **Type** | Compatibility note (Fossil accepts a payload cut short by the end of the body; the draft's framing is kept and made explicit) |
| **Status** | Applied in `draft-fossil-sync-protocol-01` (2026-10-12) |
| **Reported** | 2026-10-11 |
| **Submitter** | Dan Mestas |
| **Found via** | go-libfossil's conformance suite against Fossil 2.28 (scope note SP-5.1-1); confirmed by reading Fossil's source and by pushing it a short file card |

## 1. Summary

§5.1 says a byte-counted payload ends "only at the declared count", but says
nothing about a body that ends first. Fossil reads whatever bytes remain and
carries on. For a full-content `file`, those bytes are then hash-checked, so a
card that declares more bytes than the body holds is stored whenever the bytes
that are there hash to HASH.

## 2. Location

§5.1 (~lines 935-940):

> Bodies are sequences of LF-terminated lines.  Byte-counted payloads
> begin immediately after the line LF and may contain LF.  They end
> only at the declared count. …

## 3. The divergence

Source, Fossil 2.28:

- `blob_extract()` (`src/blob.c:675-692`) clamps the requested count to the
  bytes left in the input and returns the shorter blob; it reports the actual
  length but does not fail.
- `xfer_accept_file()` (`src/xfer.c:160`) ignores that return value. It
  hash-checks the extracted bytes (line 211) and stores them (line 214). The
  `cfile` handler (line 280) and `uvfile` handler (line 349) extract the same
  way.

Run: a `fossil new` repository, `nobody` given `gioz`, sent through
`fossil http` as `application/x-fossil-debug`:

```
push 0000 PROJECTCODE
file 13febeeb…5615 1000
truncated payload test\n          (23 bytes, then end of body)
```

The reply is `# timestamp … errors 0`, and `blob` holds `13febeeb…` with size
23. With bytes that do not hash to HASH, the request fails as any wrong-hash
`file` does.

## 4. Impact

- **Integrity.** None for `file`: the hash check still decides what is
  stored, so nothing false is accepted.
- **Framing.** A sender that miscounts can still get its last artifact
  stored by Fossil, and will be refused by a receiver that enforces the count.
  Seen from the sender, the two look inconsistent.
- **Conformance.** A test that a short payload is rejected fails against
  Fossil; go-libfossil leaves it out.
- **Not checked here:** truncated `cfile` and `uvfile` payloads, and
  truncated deltas. They take the same extraction path, but what happens
  next (decompression, delta application, UV hash checks) was not run.

## 5. Correction

State the rule the draft implies and record Fossil's leniency:

```diff
    Bodies are sequences of LF-terminated lines.  Byte-counted payloads
    begin immediately after the line LF and may contain LF.  They end
-   only at the declared count.  A cfile sender appends one further LF
+   only at the declared count.  A body that ends before a payload's
+   declared count is malformed; senders MUST NOT produce one.  (Fossil
+   instead uses the bytes that are present, and stores a full-content
+   file whose shorter payload still hashes to HASH.)  A cfile sender
+   appends one further LF
```

## 6. Rationale

- **The count is the frame.** "End only at the declared count" already makes
  a short payload a framing error; saying so closes the gap the scope note
  found.
- **No receiver requirement Fossil cannot meet.** The sender rule is MUST
  NOT; receivers are told what Fossil does rather than being required to
  reject, so Fossil stays conforming and a strict receiver is not wrong.
- **The hash makes the leniency safe,** which is why a note is enough.

## 7. Verification

- Source: Fossil 2.28 `src/blob.c:675-692` (`blob_extract`); `src/xfer.c:160`,
  `:211-214`, `:280`, `:349`.
- Run: the short `file` card in §3 against a fresh repository through
  `fossil http`; stored with size 23 and `errors 0`.

---

*Prepared against `draft-fossil-sync-protocol-00`. The RFC remains the oracle;
the reference implementation is used only to confirm the observable behavior.*