Validator does not distinguish bignums (tags 2/3) from plain ints

Ouverte
#207 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

@Soupstraw y travaille déjà.

Depuis le 14/7/2026.

  • #209 par @Soupstraw — ouverte

Évaluation

Difficulté
5/5
Temps estimé
Plus d'une semaine
Accessibilité débutants
45/100
Type d'issue
Bug
Clarté
Plutôt claire
Activité
Calme
Stack technique
haskell
Domaine
compilers, testing

Piste de recherche

Lisez d’abord Gen.hs, validateBigInt de validateInteger et les primitives du décodeur cborg ; suivez ensuite l’API custom-validator et la gestion des littéraux VBignum par le générateur. Vérifiez comment les producteurs et les consommateurs de Term représentent les wire bignums, y compris les payloads indéfinis et les contrôles d’octets. C’est terminé lorsque la validation sensible à la représentation fonctionne sans perdre les informations de signe ou de payload, et que les conventions indiquées du générateur et de l’API sont prises en compte.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

bug

Summary

The validator decodes input with cborg's decodeTerm, which folds tags 2/3 into TInteger at the token level (cborg maps wire headers 0xc2/0xc3 to TypeInteger and decodeInteger consumes the tag + payload). By the time validateTerm runs, the distinction between "major type 0/1" and "tag 2/3 bignum" is erased — but CDDL's matching rules are representation-sensitive: per RFC 8610 (§3.3, Appendix C/D), uint = #0, nint = #1, and numeric literals match only major type 0/1 data items, never tag 2/3 items. That's why the prelude needs integer = int / bigint and unsigned = uint / biguint.

This matters for interop: encoders other than cborg may legally emit valid-but-non-preferred forms (RFC 8949 §3.4.3 — bignums with leading zeroes MUST be accepted; values that fit major type 0/1 wrapped in tag 2/3 are valid; indefinite-length payloads are valid outside deterministic encoding).

Observed behavior

Hard failure on valid CBOR:

  • a = biguint vs C2 5F 41 05 FF (tag 2, indefinite-length payload) → DecodingFailed "expected integer". Valid CBOR per RFC 8949 §3.4.3/§2 (definite vs. indefinite is a serialization variant; only deterministic encoding §4.2.1 forbids it). Known issue — see the TODO at Gen.hs (disableTwiddle for tags 2/3).

Wrong results (sign not checked — validateInteger treats Tag 2 and Tag 3 rules identically):

  • a = biguint vs C3 49 010000000000000000 (negative bignum) → accepted
  • a = bignint vs C2 49 010000000000000000 (positive bignum) → accepted
  • a = biguint vs 20 (plain −1) → accepted

Too strict (payload bytes are gone, so byte controls run against mempty — see TODO in validateInteger's validateBigInt):

  • a = #6.2(bstr .size 9) vs C2 49 010000000000000000 (exactly 9 payload bytes) → rejected

Too lenient (spec deviation, fails open):

  • a = {1: uint} vs map key encoded as C2 41 01 → accepted; Appendix C: a literal "matches only a data item with that specific value", and a tag-2 item is a different data-model value

What already works correctly: bignums ≥ 2^64 accepted for biguint/bigint/integer and rejected for int/uint; leading-zero bignum payloads accepted by value; non-minimal int widths accepted (correct — CDDL matches at the data-model level); generator output is preferred serialization and round-trips.

Root cause and proposed fix

The information loss is unrecoverable at the Term level in general: TInteger i with |i| < 2^63 can only come from a bignum, but values in [2^63, 2^64) (and the negative mirror) decode to TInteger from both major type 0/1 and tags 2/3. So a TInteger-only fix in the validator cannot be complete, and can't fix the decode failure or byte controls at all.

Proposed: a cuddle-local variant of cborg's decodeTerm that keeps wire bignums as TTagged 2/3 (TBytes …) instead of folding them. This is straightforward because in cborg's decoder primitives, peekTokenType returns TypeInteger exclusively for wire headers 0xc2/0xc3 (genuine ints come back as TypeUInt/TypeUInt64/TypeNInt/TypeNInt64), and decodeTag happily consumes 0xc2/0xc3 as ordinary tags. So the TypeInteger branch can do decodeTag + payload decode instead of decodeInteger. Verified this preserves indefinite-length payloads too (C2 5F 41 05 FF → TTagged 2 (TBytesI …)).

This fixes all four gaps: representation-correct matching for uint/int/literals, sign correctness for biguint/bignint, working .size/.cbor controls on #6.2(bstr …) payloads, and no decode failure on indefinite-length payloads.

Note this establishes a convention on Term that differs from cborg's: TInt/TInteger would mean "major type 0/1 on the wire" and TTagged 2/3 (TBytes …) "bignum on the wire" (cborg's encodeTerm already round-trips this faithfully). The convention must then hold for all Term producers, not just the decoder:

  • the generator currently emits TInteger for VBignum literals — it should emit TTagged 2/3 (TBytes minimalBytes);
  • the custom-validator API wraps every integer as SingleTerm (TInteger i) and accepts user-constructed terms via validateFromName/validateAgainst — the convention needs documenting at that API boundary (or, more heavyweight, a provenance newtype / dedicated term type, in the spirit of CanonicalTerm).

Value-level equivalence (prelude integer/unsigned, toCanonical map-key dedup, matching literals > 2^64−1 against bignums by numeric value) then becomes an explicit conversion where intended. On that last point: RFC 8610 never actually defines what a literal ≥ 2^64 matches (the basic data model has no such integer) — matching it against tag 2/3 by value is a pragmatic extension, same as the reference cddl tool, and worth stating in the docs.

Spec references

  • RFC 8949 §3.4.3 (bignums: content must be a byte string; leading zeroes MUST be accepted; preferred serialization), §4.2.1/§4.2.2 (deterministic encoding), §5.3.2 (tag validity)
  • RFC 8610 §2.2.3 (representation types are data-model level), §3.3 (uint/nint), Appendix C (type2 = value: "matches only a data item with that specific value (no conversions defined)"), Appendix D (prelude: uint = #0, biguint = #6.2(bstr), unsigned = uint / biguint, …)
Langage dominant
Haskell
Étoiles
8
Forks
5
Merge moyen
3 j 12 h
PR mergées (30 j)
4

Préparer son environnement

Ce projet ne fournit ni conteneur de développement, ni Dockerfile, ni guide de contribution : l'installation est à votre charge. Commencez par son README, et consultez notre guide de la première contribution pour les étapes générales.

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de IntersectMBO/cuddle

Toutes les issues de IntersectMBO/cuddle

Issues similaires

Plus d'issues Haskell

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.