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

laskoviymishka pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git


The following commit(s) were added to refs/heads/main by this push:
     new 7760e064b fix(puffin): reject trailing data in footer payloads (#1580)
7760e064b is described below

commit 7760e064b52a3e4eeab62612ca1db301eba7047e
Author: Minh Vu <[email protected]>
AuthorDate: Wed Aug 5 18:56:53 2026 +0200

    fix(puffin): reject trailing data in footer payloads (#1580)
    
    * fix(puffin): reject trailing data in footer payloads
    
    Signed-off-by: Minh Vu <[email protected]>
    
    * refactor(puffin): detect trailing values without copying
    
    Signed-off-by: Minh Vu <[email protected]>
    
    * fix(puffin): report trailing footer content consistently
    
    * docs(puffin): explain strict footer validation
    
    ---------
    
    Signed-off-by: Minh Vu <[email protected]>
---
 puffin/puffin_reader.go | 13 ++++++++++++-
 puffin/puffin_test.go   | 39 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 51 insertions(+), 1 deletion(-)

diff --git a/puffin/puffin_reader.go b/puffin/puffin_reader.go
index 9276747b5..75304d541 100644
--- a/puffin/puffin_reader.go
+++ b/puffin/puffin_reader.go
@@ -224,11 +224,22 @@ func (r *Reader) readFooter() error {
        }
 
        payloadReader := io.NewSectionReader(r.r, footerStart+MagicSize, 
payloadSize)
+       decoder := json.NewDecoder(payloadReader)
        var footer Footer
-       if err := json.NewDecoder(payloadReader).Decode(&footer); err != nil {
+       if err := decoder.Decode(&footer); err != nil {
                return fmt.Errorf("puffin: decode footer JSON: %w", err)
        }
 
+       // FooterPayloadSize defines a single JSON footer object. Reject 
trailing
+       // content deliberately, even though some other Iceberg implementations
+       // accept padding or additional values inside the footer payload.
+       if decoder.More() {
+               return errors.New("puffin: unexpected content after footer 
JSON")
+       }
+       if _, err := decoder.Token(); !errors.Is(err, io.EOF) {
+               return errors.New("puffin: unexpected content after footer 
JSON")
+       }
+
        // Validate blob metadata
        if err := r.validateBlobs(footer.Blobs, footerStart); err != nil {
                return err
diff --git a/puffin/puffin_test.go b/puffin/puffin_test.go
index 4c186ebb4..8d1631970 100644
--- a/puffin/puffin_test.go
+++ b/puffin/puffin_test.go
@@ -19,6 +19,7 @@ package puffin_test
 
 import (
        "bytes"
+       "encoding/binary"
        "errors"
        "math"
        "os"
@@ -149,6 +150,15 @@ func validFileWithBlob() []byte {
        return buf.Bytes()
 }
 
+func fileWithFooterPayload(payload []byte) []byte {
+       data := append([]byte("PFA1PFA1"), payload...)
+       trailer := make([]byte, 12)
+       binary.LittleEndian.PutUint32(trailer[:4], uint32(len(payload)))
+       copy(trailer[8:], "PFA1")
+
+       return append(data, trailer...)
+}
+
 // --- Tests ---
 
 // TestRoundTrip verifies that data written by Writer can be read back by 
Reader.
@@ -580,6 +590,35 @@ func TestReaderInvalidFile(t *testing.T) {
        })
 }
 
+func TestReaderRejectsTrailingFooterData(t *testing.T) {
+       t.Parallel()
+
+       tests := []struct {
+               name    string
+               payload string
+               wantErr string
+       }{
+               {name: "single object", payload: `{"blobs":[]}`},
+               {name: "trailing whitespace", payload: "{\"blobs\":[]} \n\t"},
+               {name: "second JSON value", payload: 
`{"blobs":[]}{"blobs":[]}`, wantErr: "unexpected content after footer JSON"},
+               {name: "second scalar value", payload: `{"blobs":[]}42`, 
wantErr: "unexpected content after footer JSON"},
+               {name: "trailing garbage", payload: `{"blobs":[]}garbage`, 
wantErr: "unexpected content after footer JSON"},
+               {name: "trailing closing delimiter", payload: `{"blobs":[]} ]`, 
wantErr: "unexpected content after footer JSON"},
+       }
+
+       for _, test := range tests {
+               t.Run(test.name, func(t *testing.T) {
+                       t.Parallel()
+                       _, err := 
puffin.NewReader(bytes.NewReader(fileWithFooterPayload([]byte(test.payload))))
+                       if test.wantErr == "" {
+                               require.NoError(t, err)
+                       } else {
+                               require.ErrorContains(t, err, test.wantErr)
+                       }
+               })
+       }
+}
+
 // TestReaderBlobAccess verifies blob access methods work correctly.
 // Tests the primary API for retrieving blob data from puffin files.
 func TestReaderBlobAccess(t *testing.T) {

Reply via email to