{"record":{"id":"673f1be4f0c2eccb","repo":"JuliusBrussee/caveman","slug":"ccr-typed-object-byte-lengths-cannot-be-negative","errorCode":null,"errorMessage":"ccr: typed object byte lengths cannot be negative","messagePattern":"ccr: typed object byte lengths cannot be negative","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"engine/ccr/store.go","lineNumber":143,"sourceCode":"\t\tobj.CreatedAt = time.Now().UTC()\n\t} else {\n\t\tobj.CreatedAt = obj.CreatedAt.UTC()\n\t}\n\tdataSum := sha256.Sum256(obj.Data)\n\tcomputedHash := \"sha256:\" + hex.EncodeToString(dataSum[:])\n\tif obj.ContentHash == \"\" {\n\t\tobj.ContentHash = computedHash\n\t} else if obj.ContentHash != computedHash {\n\t\treturn Object{}, errors.New(\"ccr: typed object content_hash does not match data\")\n\t}\n\tif obj.OriginalByteLength == 0 {\n\t\tobj.OriginalByteLength = len(obj.Data)\n\t}\n\tif obj.StoredByteLength == 0 {\n\t\tobj.StoredByteLength = len(obj.Data)\n\t}\n\tif obj.OriginalByteLength < 0 || obj.StoredByteLength < 0 {\n\t\treturn Object{}, errors.New(\"ccr: typed object byte lengths cannot be negative\")\n\t}\n\tif obj.ID == \"\" {\n\t\tidentity := strings.Join([]string{string(obj.Type), obj.SessionID, obj.Source, obj.RepositoryState, obj.ContentHash}, \"\\x00\")\n\t\tsum := sha256.Sum256([]byte(identity))\n\t\tobj.ID = \"ccr_obj_\" + hex.EncodeToString(sum[:16])\n\t}\n\tobj.Dependencies = append([]string(nil), obj.Dependencies...)\n\tobj.Data = bytes.Clone(obj.Data)\n\treturn obj, nil\n}\n\n// sameImmutableObject compares fields PutObject promises never to change.\n// CreatedAt is assigned at first storage, while currentness and lifecycle have\n// explicit mutation methods, so repeat puts intentionally do not compare them.\nfunc sameImmutableObject(left, right Object) bool {\n\treturn left.ID == right.ID &&\n\t\tleft.Type == right.Type &&\n\t\tleft.ContentHash == right.ContentHash &&","sourceCodeStart":125,"sourceCodeEnd":161,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/engine/ccr/store.go#L125-L161","documentation":"prepareObject rejects typed Objects whose OriginalByteLength or StoredByteLength are negative. These are only defaulted (to len(Data)) when zero; negative values are treated as invalid metadata and the put fails closed rather than storing inconsistent size accounting.","triggerScenarios":"PutObject with an Object built from int arithmetic that can go negative (e.g. length computed from a subtraction, or a negative value unmarshaled from JSON), while len(Data) > 0 so the zero-defaulting never applies.","commonSituations":"Size fields populated by buggy calculations (stored = original - compressed); JSON payloads with negative byteLength accepted upstream; integer underflow when computing compressed savings.","solutions":["Leave both length fields zero so prepareObject defaults them to len(obj.Data).","Clamp computed lengths to >= 0 before calling PutObject.","Validate any unmarshaled byteLength values are non-negative at deserialization time.","Fix the size computation that produced the negative value (usually a subtraction underflow)."],"exampleFix":"// before\nobj.StoredByteLength = len(raw) - len(comp) // may go negative\n\n// after\nif n := len(raw) - len(comp); n >= 0 {\n    obj.StoredByteLength = n\n} // else leave 0 to let prepareObject default it","handlingStrategy":"validation","validationCode":"if obj.OriginalByteLength < 0 || obj.StoredByteLength < 0 {\n    return errors.New(\"negative byte lengths\")\n}","typeGuard":"func saneLengths(o ccr.Object) bool {\n    return o.OriginalByteLength >= 0 && o.StoredByteLength >= 0\n}","tryCatchPattern":"if err := clampLengths(&obj); err != nil { return err }\nif _, err := store.PutObject(obj); err != nil {\n    if strings.Contains(err.Error(), \"cannot be negative\") {\n        return fmt.Errorf(\"size accounting bug: %w\", err)\n    }\n    return err\n}","preventionTips":["Leave length fields zero to use the len(Data) default.","Guard any subtraction-based size math against underflow.","Validate unmarshaled byteLength values are non-negative.","Add a fuzz/property test over size computations."],"tags":["ccr","validation","negative-value","typed-object"],"backgroundTag":"invalid-argument-value","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}