This is an automated email from the ASF dual-hosted git repository.

chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git


The following commit(s) were added to refs/heads/main by this push:
     new 88a803855 fix(go): reference tracking fails when >127 objects 
serialized (#3086)
88a803855 is described below

commit 88a80385592779fff335495faefe79df86dfef53
Author: Jonathan Yoder <[email protected]>
AuthorDate: Wed Dec 24 12:25:43 2025 -0500

    fix(go): reference tracking fails when >127 objects serialized (#3086)
    
    ## What does this PR do?
    
    Fixes reference tracking bug that causes deserialization to fail with
    hash mismatch errors when more than 127 objects are serialized with
    reference tracking enabled.
    
    ## Root Cause
    
    The check `if int8(refID) < NotNullValueFlag` in multiple files causes
    integer overflow when `refID >= 128`:
    
    ```go
    // BEFORE (buggy):
    if int8(refID) < NotNullValueFlag {
    ```
    
    - `int8(128)` = `-128` (two's complement overflow)
    - `-128 < -1` (NotNullValueFlag) = `TRUE` (incorrectly!)
    
    This causes the code to incorrectly enter the "reference found" branch,
    corrupting buffer positions and causing hash mismatches.
    
    ## Fix
    
    Replace int8 cast with int32 comparison:
    
    ```go
    // AFTER (fixed):
    if refID < int32(NotNullValueFlag) {
    ```
    
    Since `refID` is already `int32` (returned from `TryPreserveRefId`), and
    the flag constants are small negative int8 values that correctly
    sign-extend to int32, this comparison is type-safe and avoids
    truncation.
    
    ## Files Changed
    
    - `array.go` (1 occurrence)
    - `map.go` (3 occurrences)
    - `pointer.go` (2 occurrences)
    - `reader.go` (7 occurrences)
    - `serializer.go` (1 occurrence)
    - `set.go` (4 occurrences)
    - `slice.go` (1 occurrence)
    - `slice_dyn.go` (4 occurrences)
    - `struct.go` (2 occurrences)
    
    ## Testing
    
    Added regression test `TestRefTrackingLargeCount` in
    `ref_resolver_test.go` that verifies serialization/deserialization works
    correctly with 127, 128, and 200 items.
    
    All existing tests pass.
    
    ## Related Issues
    
    Closes #3085
    
    ## Checklist
    
    - [x] I have read the [contributing guidelines](../CONTRIBUTING.md)
    - [x] I have added tests that prove my fix is effective
    - [x] All new and existing tests pass locally with my changes
---
 go/fory/array.go             |  2 +-
 go/fory/map.go               |  6 ++---
 go/fory/pointer.go           |  4 +--
 go/fory/reader.go            | 14 +++++-----
 go/fory/ref_resolver_test.go | 64 ++++++++++++++++++++++++++++++++++++++++++++
 go/fory/serializer.go        |  2 +-
 go/fory/set.go               |  8 +++---
 go/fory/slice.go             |  2 +-
 go/fory/slice_dyn.go         |  8 +++---
 go/fory/struct.go            |  4 +--
 10 files changed, 89 insertions(+), 25 deletions(-)

diff --git a/go/fory/array.go b/go/fory/array.go
index 9845d0e61..ea222cd01 100644
--- a/go/fory/array.go
+++ b/go/fory/array.go
@@ -46,7 +46,7 @@ func readArrayRefAndType(ctx *ReadContext, refMode RefMode, 
readType bool, value
                        ctx.SetError(FromError(refErr))
                        return false
                }
-               if int8(refID) < NotNullValueFlag {
+               if refID < int32(NotNullValueFlag) {
                        obj := ctx.RefResolver().GetReadObject(refID)
                        if obj.IsValid() {
                                value.Set(obj)
diff --git a/go/fory/map.go b/go/fory/map.go
index 450b71d30..f910146e6 100644
--- a/go/fory/map.go
+++ b/go/fory/map.go
@@ -82,7 +82,7 @@ func readMapRefAndType(ctx *ReadContext, refMode RefMode, 
readType bool, value r
                        ctx.SetError(FromError(refErr))
                        return false
                }
-               if int8(refID) < NotNullValueFlag {
+               if refID < int32(NotNullValueFlag) {
                        obj := ctx.RefResolver().GetReadObject(refID)
                        if obj.IsValid() {
                                value.Set(obj)
@@ -375,7 +375,7 @@ func (s mapSerializer) ReadData(ctx *ReadContext, type_ 
reflect.Type, value refl
                                                        
ctx.SetError(FromError(err))
                                                        return
                                                }
-                                               if int8(refID) < 
NotNullValueFlag {
+                                               if refID < 
int32(NotNullValueFlag) {
                                                        // Reference to 
existing object
                                                        obj := 
refResolver.GetReadObject(refID)
                                                        if obj.IsValid() {
@@ -466,7 +466,7 @@ func (s mapSerializer) ReadData(ctx *ReadContext, type_ 
reflect.Type, value refl
                                                        
ctx.SetError(FromError(err))
                                                        return
                                                }
-                                               if int8(refID) < 
NotNullValueFlag {
+                                               if refID < 
int32(NotNullValueFlag) {
                                                        // Reference to 
existing object
                                                        obj := 
refResolver.GetReadObject(refID)
                                                        if obj.IsValid() {
diff --git a/go/fory/pointer.go b/go/fory/pointer.go
index 17c560965..b223c4e68 100644
--- a/go/fory/pointer.go
+++ b/go/fory/pointer.go
@@ -103,7 +103,7 @@ func (s *ptrToValueSerializer) Read(ctx *ReadContext, 
refMode RefMode, readType
                        ctx.SetError(FromError(refErr))
                        return
                }
-               if int8(refID) < NotNullValueFlag {
+               if refID < int32(NotNullValueFlag) {
                        // Reference found
                        obj := ctx.RefResolver().GetReadObject(refID)
                        if obj.IsValid() {
@@ -210,7 +210,7 @@ func (s *ptrToInterfaceSerializer) Read(ctx *ReadContext, 
refMode RefMode, readT
                        ctx.SetError(FromError(refErr))
                        return
                }
-               if int8(refID) < NotNullValueFlag {
+               if refID < int32(NotNullValueFlag) {
                        // Reference found
                        obj := ctx.RefResolver().GetReadObject(refID)
                        if obj.IsValid() {
diff --git a/go/fory/reader.go b/go/fory/reader.go
index 288eed1aa..8bd05a883 100644
--- a/go/fory/reader.go
+++ b/go/fory/reader.go
@@ -569,7 +569,7 @@ func (c *ReadContext) ReadValue(value reflect.Value) {
                        c.SetError(FromError(err))
                        return
                }
-               if int8(refID) < NotNullValueFlag {
+               if refID < int32(NotNullValueFlag) {
                        // Reference found
                        obj := c.RefResolver().GetReadObject(refID)
                        if obj.IsValid() {
@@ -625,7 +625,7 @@ func (c *ReadContext) ReadValue(value reflect.Value) {
 
                // For named structs, register the pointer BEFORE reading data
                // This is critical for circular references to work correctly
-               if isNamedStruct && int8(refID) >= NotNullValueFlag {
+               if isNamedStruct && refID >= int32(NotNullValueFlag) {
                        c.RefResolver().SetReadObject(refID, newValue)
                }
 
@@ -643,7 +643,7 @@ func (c *ReadContext) ReadValue(value reflect.Value) {
                }
 
                // Register reference after reading data for non-struct types
-               if !isNamedStruct && int8(refID) >= NotNullValueFlag {
+               if !isNamedStruct && refID >= int32(NotNullValueFlag) {
                        c.RefResolver().SetReadObject(refID, newValue)
                }
 
@@ -701,7 +701,7 @@ func (c *ReadContext) ReadStruct(value reflect.Value) {
        }
 
        // Handle null
-       if int8(refID) == NullFlag {
+       if refID == int32(NullFlag) {
                if isPtr {
                        value.Set(reflect.Zero(valueType))
                }
@@ -709,7 +709,7 @@ func (c *ReadContext) ReadStruct(value reflect.Value) {
        }
 
        // Handle reference to existing object
-       if int8(refID) < NotNullValueFlag {
+       if refID < int32(NotNullValueFlag) {
                obj := c.RefResolver().GetReadObject(refID)
                if obj.IsValid() {
                        value.Set(obj)
@@ -771,7 +771,7 @@ func (c *ReadContext) readArrayValue(target reflect.Value) {
                c.SetError(FromError(err))
                return
        }
-       if int8(refID) < NotNullValueFlag {
+       if refID < int32(NotNullValueFlag) {
                // Reference to existing object
                obj := c.RefResolver().GetReadObject(refID)
                if obj.IsValid() {
@@ -812,7 +812,7 @@ func (c *ReadContext) readArrayValue(target reflect.Value) {
        reflect.Copy(target, tempSlice)
 
        // Register for circular refs
-       if int8(refID) >= NotNullValueFlag {
+       if refID >= int32(NotNullValueFlag) {
                c.RefResolver().SetReadObject(refID, target)
        }
 }
diff --git a/go/fory/ref_resolver_test.go b/go/fory/ref_resolver_test.go
index 9c2773fe9..cfabb602f 100644
--- a/go/fory/ref_resolver_test.go
+++ b/go/fory/ref_resolver_test.go
@@ -18,6 +18,7 @@
 package fory
 
 import (
+       "fmt"
        "github.com/stretchr/testify/require"
        "reflect"
        "testing"
@@ -119,3 +120,66 @@ func same(x, y interface{}) bool {
        }
        return unsafe.Pointer(reflect.ValueOf(x).Pointer()) == 
unsafe.Pointer(reflect.ValueOf(y).Pointer())
 }
+
+// TestRefTrackingLargeCount tests that reference tracking works correctly
+// when the number of tracked objects exceeds 127 (the int8 overflow boundary).
+// This is a regression test for https://github.com/apache/fory/issues/3085
+func TestRefTrackingLargeCount(t *testing.T) {
+       type Inner struct {
+               Name     string
+               Operator string
+               Version  string
+       }
+
+       type Outer struct {
+               Id    int32
+               Name  string
+               Items []Inner
+       }
+
+       tests := []struct {
+               name  string
+               count int
+       }{
+               {"127 items (boundary)", 127},
+               {"128 items (overflow boundary)", 128},
+               {"200 items (well over boundary)", 200},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       f := New(WithRefTracking(true))
+
+                       err := f.RegisterByName(&Inner{}, 
fmt.Sprintf("RefTest_Inner_%d", tt.count))
+                       require.NoError(t, err)
+                       err = f.RegisterByName(&Outer{}, 
fmt.Sprintf("RefTest_Outer_%d", tt.count))
+                       require.NoError(t, err)
+
+                       original := make([]Outer, tt.count)
+                       for i := 0; i < tt.count; i++ {
+                               original[i] = Outer{
+                                       Id:   int32(i),
+                                       Name: fmt.Sprintf("item%d", i),
+                                       Items: []Inner{
+                                               {Name: "dep1", Operator: ">=", 
Version: "1.0.0"},
+                                       },
+                               }
+                       }
+
+                       data, err := f.Marshal(original)
+                       require.NoError(t, err)
+
+                       var loaded []Outer
+                       err = f.Unmarshal(data, &loaded)
+                       require.NoError(t, err, "Unmarshal should succeed with 
%d items", tt.count)
+                       require.Equal(t, len(original), len(loaded))
+
+                       // Verify data integrity
+                       for i := 0; i < tt.count; i++ {
+                               require.Equal(t, original[i].Id, loaded[i].Id)
+                               require.Equal(t, original[i].Name, 
loaded[i].Name)
+                               require.Equal(t, len(original[i].Items), 
len(loaded[i].Items))
+                       }
+               })
+       }
+}
diff --git a/go/fory/serializer.go b/go/fory/serializer.go
index b9737a542..3d5ed82e9 100644
--- a/go/fory/serializer.go
+++ b/go/fory/serializer.go
@@ -214,7 +214,7 @@ func (s *extensionSerializerAdapter) Read(ctx *ReadContext, 
refMode RefMode, rea
                        ctx.SetError(FromError(refErr))
                        return
                }
-               if int8(refID) < NotNullValueFlag {
+               if refID < int32(NotNullValueFlag) {
                        obj := ctx.RefResolver().GetReadObject(refID)
                        if obj.IsValid() {
                                value.Set(obj)
diff --git a/go/fory/set.go b/go/fory/set.go
index 5538927ad..eed5cd087 100644
--- a/go/fory/set.go
+++ b/go/fory/set.go
@@ -290,7 +290,7 @@ func (s setSerializer) readSameType(ctx *ReadContext, buf 
*ByteBuffer, value ref
                if trackRefs {
                        // Handle reference tracking if enabled
                        refID, _ = ctx.RefResolver().TryPreserveRefId(buf)
-                       if int8(refID) < NotNullValueFlag {
+                       if refID < int32(NotNullValueFlag) {
                                // Use existing reference if available
                                elem := ctx.RefResolver().GetReadObject(refID)
                                value.SetMapIndex(reflect.ValueOf(elem), 
reflect.ValueOf(true))
@@ -328,11 +328,11 @@ func (s setSerializer) readDifferentTypes(ctx 
*ReadContext, buf *ByteBuffer, val
                                ctx.SetError(FromError(refErr))
                                return
                        }
-                       if int8(refID) == NullFlag {
+                       if refID == int32(NullFlag) {
                                // Null element - skip for sets
                                continue
                        }
-                       if int8(refID) < NotNullValueFlag {
+                       if refID < int32(NotNullValueFlag) {
                                // Use existing reference if available
                                elem := ctx.RefResolver().GetReadObject(refID)
                                value.SetMapIndex(elem, reflect.ValueOf(true))
@@ -412,7 +412,7 @@ func (s setSerializer) Read(ctx *ReadContext, refMode 
RefMode, readType bool, ha
                        ctx.SetError(FromError(refErr))
                        return
                }
-               if int8(refID) < NotNullValueFlag {
+               if refID < int32(NotNullValueFlag) {
                        // Reference found
                        obj := ctx.RefResolver().GetReadObject(refID)
                        if obj.IsValid() {
diff --git a/go/fory/slice.go b/go/fory/slice.go
index 73606ad2f..e085c6acb 100644
--- a/go/fory/slice.go
+++ b/go/fory/slice.go
@@ -73,7 +73,7 @@ func readSliceRefAndType(ctx *ReadContext, refMode RefMode, 
readType bool, value
                        ctx.SetError(FromError(refErr))
                        return true
                }
-               if int8(refID) < NotNullValueFlag {
+               if refID < int32(NotNullValueFlag) {
                        obj := ctx.RefResolver().GetReadObject(refID)
                        if obj.IsValid() {
                                value.Set(obj)
diff --git a/go/fory/slice_dyn.go b/go/fory/slice_dyn.go
index 4373b3fd1..64cd22ce3 100644
--- a/go/fory/slice_dyn.go
+++ b/go/fory/slice_dyn.go
@@ -319,11 +319,11 @@ func (s sliceDynSerializer) readSameType(ctx 
*ReadContext, buf *ByteBuffer, valu
                                ctx.SetError(FromError(refErr))
                                return
                        }
-                       if int8(refID) == NullFlag {
+                       if refID == int32(NullFlag) {
                                continue
                        }
                        // Handle RefFlag - element references a previously 
read object
-                       if int8(refID) < NotNullValueFlag {
+                       if refID < int32(NotNullValueFlag) {
                                obj := ctx.RefResolver().GetReadObject(refID)
                                if obj.IsValid() {
                                        value.Index(i).Set(obj)
@@ -379,10 +379,10 @@ func (s sliceDynSerializer) readDifferentTypes(
                                ctx.SetError(FromError(refErr))
                                return
                        }
-                       if int8(refID) == NullFlag {
+                       if refID == int32(NullFlag) {
                                continue
                        }
-                       if int8(refID) < NotNullValueFlag {
+                       if refID < int32(NotNullValueFlag) {
                                // Reference to existing object
                                obj := ctx.RefResolver().GetReadObject(refID)
                                if obj.IsValid() {
diff --git a/go/fory/struct.go b/go/fory/struct.go
index 849a86ca6..eb91e0b17 100644
--- a/go/fory/struct.go
+++ b/go/fory/struct.go
@@ -551,7 +551,7 @@ func (s *structSerializer) Read(ctx *ReadContext, refMode 
RefMode, readType bool
                        ctx.SetError(FromError(refErr))
                        return
                }
-               if int8(refID) < NotNullValueFlag {
+               if refID < int32(NotNullValueFlag) {
                        // Reference found
                        obj := ctx.RefResolver().GetReadObject(refID)
                        if obj.IsValid() {
@@ -2080,7 +2080,7 @@ func (s *skipStructSerializer) Read(ctx *ReadContext, 
refMode RefMode, readType
                        ctx.SetError(FromError(refErr))
                        return
                }
-               if int8(refID) < NotNullValueFlag {
+               if refID < int32(NotNullValueFlag) {
                        // Reference found, nothing to skip
                        return
                }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to