diff --git a/CHANGELOG.md b/CHANGELOG.md index 26ba917..508530d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Rivet 0.2 hardens the native runtime contract and release path while preserving - Native identifier collision detection and language-specific codegen string escaping. - Bounded RVT1 frame/value byte size, value nesting, total value-node count, concurrent backend requests, declared/incoming API-name size, and queued backend output. - Racket List encoding applies the value-node budget while discovering list length instead of fully traversing oversized Lists before rejecting them; improper Lists fail explicitly without formatting the entire value. +- Typed Racket value validation applies the same List nesting and total-node budgets before encoding, avoids `list?`/`andmap` full traversals, and keeps `Any` opaque so validation itself remains bounded. - Racket protocol encoding reports unsupported application values without formatting the object itself, so custom writers cannot amplify or replace the original codec failure. - Synchronized request cancellation/completion ownership and non-zero UInt64 Event ID allocation; Event IDs wrap from `2^64-1` back to `1`, while terminal requests keep their pending slot until their Response/Error is admitted to the bounded output queue. - Duplicate Request IDs and illegal inbound frame types that collide with a pending request use first-request-wins semantics, preventing a second terminal frame from consuming the original native caller's continuation; IDs become reusable after release. diff --git a/rivet/backend.rkt b/rivet/backend.rkt index 0b550af..dca474f 100644 --- a/rivet/backend.rkt +++ b/rivet/backend.rkt @@ -89,30 +89,73 @@ [(list 'Optional inner) (supported-type? inner)] [_ #f]))) -(define (value-matches-type? type value) - (case type - [(String) (string? value)] - [(Int64) - (and (exact-integer? value) - (<= (- (expt 2 63)) value (sub1 (expt 2 63))))] - [(Bool) (boolean? value)] - [(Bytes) (bytes? value)] - [(Void) (void? value)] - [(Any) #t] - [else - (match type - [(list 'List inner) - (and (list? value) - (andmap (lambda (item) (value-matches-type? inner item)) value))] - [(list 'Optional inner) - (or (void? value) (value-matches-type? inner value))] - [_ #f])])) +;; Typed validation must not do more structural work than the protocol encoder +;; it protects. Track the same total value-node and List-nesting budgets while +;; walking only the structure required by the declared type. `Any` is accepted +;; without recursively inspecting its contents; encode-value remains the final +;; authority for arbitrary Any subtrees. +(define (value-validation-result type value) + (define remaining-nodes max-value-nodes) + + (define (consume-node!) + (cond + [(zero? remaining-nodes) #f] + [else + (set! remaining-nodes (sub1 remaining-nodes)) + #t])) + + (define (matches type value depth) + ;; Optional is a schema wrapper, not an extra wire node. A present Optional + ;; delegates node accounting to its inner type; absent Optional uses the one + ;; Null node that is actually encoded. + (match type + [(list 'Optional inner) + (if (void? value) + (if (consume-node!) 'valid 'node-limit) + (matches inner value depth))] + [_ + (cond + [(not (consume-node!)) 'node-limit] + [else + (case type + [(String) (if (string? value) 'valid 'mismatch)] + [(Int64) + (if (and (exact-integer? value) + (<= (- (expt 2 63)) value (sub1 (expt 2 63)))) + 'valid + 'mismatch)] + [(Bool) (if (boolean? value) 'valid 'mismatch)] + [(Bytes) (if (bytes? value) 'valid 'mismatch)] + [(Void) (if (void? value) 'valid 'mismatch)] + [(Any) 'valid] + [else + (match type + [(list 'List inner) + (cond + [(>= depth max-value-depth) 'depth-limit] + [else + ;; Avoid `list?` + `andmap`: both can traverse an arbitrarily + ;; large application List before the RVT1 budget is applied. + ;; Walking cdrs here terminates as soon as an element consumes + ;; the last available protocol node. + (let loop ([rest value]) + (cond + [(null? rest) 'valid] + [(not (pair? rest)) 'mismatch] + [else + (define item-result + (matches inner (car rest) (add1 depth))) + (if (eq? item-result 'valid) + (loop (cdr rest)) + item-result)]))])] + [_ 'mismatch])])])])) + + (matches type value 0)) (define (safe-value-kind value) - ;; Diagnostics must never print an arbitrary application value just to report - ;; a type mismatch. In particular, custom writers can perform unbounded work - ;; or raise while an Error is being constructed. Classify with predicates - ;; only and report this small symbol instead. + ;; Diagnostics must never print or fully traverse an arbitrary application + ;; value. Pair/null classification is O(1); proving proper-List-ness belongs + ;; to bounded typed validation or the protocol encoder, not error reporting. (cond [(void? value) 'Void] [(string? value) 'String] @@ -122,16 +165,28 @@ (<= (- (expt 2 63)) value (sub1 (expt 2 63)))) 'Int64] [(exact-integer? value) 'Integer] - [(list? value) 'List] + [(or (null? value) (pair? value)) 'ListLike] [else 'Unsupported])) (define (validate-value who label type value) - (unless (value-matches-type? type value) - (raise-arguments-error who - "value does not match declared Rivet type" - "position" label - "expected" type - "received" (safe-value-kind value)))) + (case (value-validation-result type value) + [(valid) (void)] + [(node-limit) + (raise-arguments-error who + "value node count exceeds Rivet protocol limit" + "position" label + "maximum nodes" max-value-nodes)] + [(depth-limit) + (raise-arguments-error who + "value nesting exceeds Rivet protocol limit" + "position" label + "maximum depth" max-value-depth)] + [else + (raise-arguments-error who + "value does not match declared Rivet type" + "position" label + "expected" type + "received" (safe-value-kind value))])) (define (register-rpc! name arg-names arg-types result-type proc) (check-api-name-length! 'define-rpc "RPC" (symbol->string name)) diff --git a/tests/backend-type-validation-limits.rkt b/tests/backend-type-validation-limits.rkt new file mode 100644 index 0000000..22a2898 --- /dev/null +++ b/tests/backend-type-validation-limits.rkt @@ -0,0 +1,51 @@ +#lang racket/base + +(require rackunit + racket/list + (for-syntax racket/base + "../rivet/protocol.rkt") + "../rivet/backend.rkt" + "../rivet/protocol.rkt") + +(define-event bounded-int-list-event : (List Int64)) + +;; The outer List itself consumes one RVT1 value node. Exactly +;; max-value-nodes - 1 scalar children therefore fits the protocol budget. With +;; no server active, reaching emit-event! proves type validation accepted it. +(let ([value (make-list (sub1 max-value-nodes) 1)]) + (check-exn #rx"no Rivet server is active" + (lambda () (bounded-int-list-event value)))) + +;; One additional child exceeds the total value-node budget. Typed validation +;; must reject this before it can reach emit-event! or perform an unbounded +;; full-List traversal ahead of the protocol encoder. +(let ([value (make-list max-value-nodes 1)]) + (check-exn #rx"value node count exceeds Rivet protocol limit" + (lambda () (bounded-int-list-event value)))) + +;; Build a declaration with one List layer beyond the RVT1 nesting limit +;; without hand-writing dozens of nested forms. Accept the event identifier from +;; the use site so the generated binding remains visible under macro hygiene. +(define-syntax (define-too-deep-event stx) + (syntax-case stx () + [(_ name) + (let ([type-stx + (for/fold ([type-stx #'Int64]) + ([i (in-range (add1 max-value-depth))]) + #`(List #,type-stx))]) + #`(define-event name : #,type-stx))])) + +(define-too-deep-event too-deep-event) + +(define too-deep-value + (for/fold ([value 1]) + ([i (in-range (add1 max-value-depth))]) + (list value))) + +(check-exn #rx"value nesting exceeds Rivet protocol limit" + (lambda () (too-deep-event too-deep-value))) + +;; Improper pair-shaped values remain ordinary type mismatches; diagnostics do +;; not need to prove proper-List-ness by traversing arbitrary tails. +(check-exn #rx"value does not match declared Rivet type" + (lambda () (bounded-int-list-event (cons 1 2))))