This is an automated email from the ASF dual-hosted git repository.
vatsrahul1001 pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-4-test by this push:
new e25535ebfdb [v3-4-test] Fix Go SDK build on 32-bit targets (#75551)
(#75742)
e25535ebfdb is described below
commit e25535ebfdba32bbfd66be5757c4c442f0839516
Author: Jason(Zhe-You) Liu <[email protected]>
AuthorDate: Tue Oct 27 18:45:10 2026 +0800
[v3-4-test] Fix Go SDK build on 32-bit targets (#75551) (#75742)
The Go SDK did not compile where `int` is 32 bits (`GOARCH=386`, `arm`,
`mipsle`), so `airflow-go-pack --goarch=arm` could not build a bundle for a
32-bit worker. `MaxFrameSize` and `MaxRegionSize` are `2^32 - 1`, which does
not fit in a 32-bit `int` when compared with `len()` or passed to
`fmt.Errorf`.
Once it compiled, `parseRetentionDays` also misbehaved on 32-bit: it checked
the value against the int64 range, so `default_retention_days = 3000000000`
wrapped and failed with `must be >= 0, got -2147483648`. It is now bounded
by
the platform's `int`.
CI now vets the Go SDK for a 32-bit target and, on x64 runners, runs its
tests under `GOARCH=386`, so this cannot break again unnoticed.
(cherry picked from commit 0d527b7226584bedd6ccb6785d9511ef366e4af5)
Co-authored-by: ChickenBenny <[email protected]>
Co-authored-by: Claude <[email protected]>
---
.github/workflows/ci-amd.yml | 13 +++++++++++++
.github/workflows/ci-arm.yml | 13 +++++++++++++
go-sdk/cmd/airflow-go-pack/sources.go | 2 +-
go-sdk/internal/bundlefooter/footer.go | 5 +++--
go-sdk/pkg/execution/client.go | 7 ++++---
go-sdk/pkg/execution/client_test.go | 17 ++++++++++++++---
go-sdk/pkg/execution/frames.go | 16 +++++++++++++---
go-sdk/pkg/execution/frames_oversized_test.go | 4 +++-
go-sdk/pkg/execution/frames_test.go | 14 ++++++++++++++
9 files changed, 78 insertions(+), 13 deletions(-)
diff --git a/.github/workflows/ci-amd.yml b/.github/workflows/ci-amd.yml
index b79663bbb9a..e56377e053b 100644
--- a/.github/workflows/ci-amd.yml
+++ b/.github/workflows/ci-amd.yml
@@ -1263,6 +1263,19 @@ jobs:
- name: Run Go tests with the race detector
working-directory: ./go-sdk
run: gotestsum --format github-actions -- -race ./...
+ # Nothing else builds for a 32-bit `int` (386, arm, mipsle), where
2^32-1 constants overflow
+ # and int64 values wrap. Vet type-checks on any host; the tests need one
that runs 386.
+ - name: Vet Go SDK for a 32-bit target
+ working-directory: ./go-sdk
+ env:
+ GOARCH: "386"
+ run: go vet ./...
+ - name: Run Go tests on a 32-bit target
+ if: runner.arch == 'X64'
+ working-directory: ./go-sdk
+ env:
+ GOARCH: "386"
+ run: gotestsum --format github-actions ./...
# Go modules have no central registry that pre-scans releases, so we
scan our own
# dependency graph against the Go vulnerability database. govulncheck is
reachability
# based, so it only fails on advisories that actually affect code we
call. The version
diff --git a/.github/workflows/ci-arm.yml b/.github/workflows/ci-arm.yml
index 9ce7669b669..ba3a3733c0c 100644
--- a/.github/workflows/ci-arm.yml
+++ b/.github/workflows/ci-arm.yml
@@ -1146,6 +1146,19 @@ jobs:
- name: Run Go tests with the race detector
working-directory: ./go-sdk
run: gotestsum --format github-actions -- -race ./...
+ # Nothing else builds for a 32-bit `int` (386, arm, mipsle), where
2^32-1 constants overflow
+ # and int64 values wrap. Vet type-checks on any host; the tests need one
that runs 386.
+ - name: Vet Go SDK for a 32-bit target
+ working-directory: ./go-sdk
+ env:
+ GOARCH: "386"
+ run: go vet ./...
+ - name: Run Go tests on a 32-bit target
+ if: runner.arch == 'X64'
+ working-directory: ./go-sdk
+ env:
+ GOARCH: "386"
+ run: gotestsum --format github-actions ./...
# Go modules have no central registry that pre-scans releases, so we
scan our own
# dependency graph against the Go vulnerability database. govulncheck is
reachability
# based, so it only fails on advisories that actually affect code we
call. The version
diff --git a/go-sdk/cmd/airflow-go-pack/sources.go
b/go-sdk/cmd/airflow-go-pack/sources.go
index 3455b73cb17..0faed5f10b2 100644
--- a/go-sdk/cmd/airflow-go-pack/sources.go
+++ b/go-sdk/cmd/airflow-go-pack/sources.go
@@ -248,7 +248,7 @@ func layoutSources(
if int64(region.Len()) > bundlefooter.MaxRegionSize {
return sourceLayout{}, nil, fmt.Errorf(
"source region too large after embedding %s
(max %d bytes)",
- p, int64(bundlefooter.MaxRegionSize),
+ p, bundlefooter.MaxRegionSize,
)
}
}
diff --git a/go-sdk/internal/bundlefooter/footer.go
b/go-sdk/internal/bundlefooter/footer.go
index 6641ab3af08..1ef4c1e7d36 100644
--- a/go-sdk/internal/bundlefooter/footer.go
+++ b/go-sdk/internal/bundlefooter/footer.go
@@ -49,8 +49,9 @@ const (
FooterVersion = 1
// MaxRegionSize is the largest source or metadata region this footer
- // format can address (uint32 length field).
- MaxRegionSize = math.MaxUint32
+ // format can address (uint32 length field). Typed so that a use site
+ // never turns it into an int, which overflows where int is 32 bits.
+ MaxRegionSize int64 = math.MaxUint32
)
// Magic is the 8-byte ASCII tag that identifies a file as a bundle.
diff --git a/go-sdk/pkg/execution/client.go b/go-sdk/pkg/execution/client.go
index f8263a8236d..69a63b0eee3 100644
--- a/go-sdk/pkg/execution/client.go
+++ b/go-sdk/pkg/execution/client.go
@@ -125,10 +125,11 @@ func parseRetentionDays(raw string) (int, error) {
days, err := strconv.Atoi(raw)
if err != nil {
f, floatErr := strconv.ParseFloat(raw, 64)
- // A float outside int64 range (an infinity included) converts
with an
+ // A float outside int range (an infinity included) converts
with an
// implementation-defined result, so it is rejected rather than
turned
- // into whatever day count this platform happens to produce.
- if floatErr != nil || f != math.Trunc(f) || f >= math.MaxInt64
|| f <= math.MinInt64 {
+ // into whatever day count this platform happens to produce.
int is
+ // 32 bits on 386 and arm, so the bound is not int64's.
+ if floatErr != nil || f != math.Trunc(f) || f >= math.MaxInt ||
f <= math.MinInt {
return 0, fmt.Errorf(
"failed to convert value to int. Please check
%q key in %q section. Current value: %q",
"default_retention_days",
diff --git a/go-sdk/pkg/execution/client_test.go
b/go-sdk/pkg/execution/client_test.go
index ec61b474ec8..2db1a53f769 100644
--- a/go-sdk/pkg/execution/client_test.go
+++ b/go-sdk/pkg/execution/client_test.go
@@ -470,6 +470,8 @@ func TestResolveDefaultExpiry(t *testing.T) {
unset bool
want any
wantErr string
+ // The timestamp only wraps for day counts that need a 64-bit
int.
+ needs64BitInt bool
}{
{name: "unset is an error", unset: true, wantErr: "is not set"},
{name: "honours supervisor value", env: "7", want:
now.UTC().AddDate(0, 0, 7)},
@@ -487,14 +489,23 @@ func TestResolveDefaultExpiry(t *testing.T) {
wantErr: "failed to convert value to int",
},
{
- name: "day count that wraps the timestamp is an
error",
- env: "9223372036854775807",
- wantErr: "overflows the expiry timestamp",
+ name: "out of int range is an error",
+ env: strconv.FormatUint(math.MaxInt+1, 10),
+ wantErr: "failed to convert value to int",
+ },
+ {
+ name: "day count that wraps the timestamp is
an error",
+ env: "9223372036854775807",
+ wantErr: "overflows the expiry timestamp",
+ needs64BitInt: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
+ if tc.needs64BitInt && strconv.IntSize < 64 {
+ t.Skip("requires 64-bit int")
+ }
// Setenv registers the restore, so unsetting it here
is undone after.
t.Setenv(defaultRetentionDaysEnv, tc.env)
if tc.unset {
diff --git a/go-sdk/pkg/execution/frames.go b/go-sdk/pkg/execution/frames.go
index c1f5e73052c..cf6efb2aaf6 100644
--- a/go-sdk/pkg/execution/frames.go
+++ b/go-sdk/pkg/execution/frames.go
@@ -22,6 +22,7 @@ import (
"encoding/binary"
"fmt"
"io"
+ "math"
"github.com/vmihailenco/msgpack/v5"
@@ -95,11 +96,11 @@ func writeFrame(w io.Writer, payload []byte) error {
// below and put a corrupt length prefix on the wire, desynchronising
// the peer instead of failing loudly here. Mirrors the OverflowError
// raised by task-sdk comms.py:_FrameMixin.as_bytes.
- if len(payload) > MaxFrameSize {
+ if uint64(len(payload)) > MaxFrameSize {
return fmt.Errorf(
"frame payload length %d exceeds max %d",
len(payload),
- MaxFrameSize,
+ uint64(MaxFrameSize),
)
}
buf := make([]byte, 4+len(payload))
@@ -131,7 +132,16 @@ func readFrame(r io.Reader) (IncomingFrame, error) {
return IncomingFrame{}, fmt.Errorf(
"frame payload length %d exceeds max %d",
payloadLen,
- MaxFrameSize,
+ uint64(MaxFrameSize),
+ )
+ }
+ // Where int is 32 bits, a length past math.MaxInt wraps negative in
int()
+ // and make panics instead of the read failing.
+ if uint64(payloadLen) > math.MaxInt {
+ return IncomingFrame{}, fmt.Errorf(
+ "frame payload length %d exceeds this platform's max
%d",
+ payloadLen,
+ math.MaxInt,
)
}
payload := make([]byte, int(payloadLen))
diff --git a/go-sdk/pkg/execution/frames_oversized_test.go
b/go-sdk/pkg/execution/frames_oversized_test.go
index 5b862167ef0..789b9d753fd 100644
--- a/go-sdk/pkg/execution/frames_oversized_test.go
+++ b/go-sdk/pkg/execution/frames_oversized_test.go
@@ -55,7 +55,9 @@ func TestWriteFrameRejectsOversizedPayload(t *testing.T) {
t.Skip("requires 64-bit int to construct a slice longer than
MaxFrameSize")
}
var backing byte
- payload := unsafe.Slice(&backing, uint64(MaxFrameSize)+1)
+ // A variable, not a constant expression, so the file still compiles
where int is 32 bits.
+ length := uint64(MaxFrameSize) + 1
+ payload := unsafe.Slice(&backing, length)
err := writeFrame(&bytes.Buffer{}, payload)
require.Error(t, err)
diff --git a/go-sdk/pkg/execution/frames_test.go
b/go-sdk/pkg/execution/frames_test.go
index 6dd8d2a9a9a..c71505a5b60 100644
--- a/go-sdk/pkg/execution/frames_test.go
+++ b/go-sdk/pkg/execution/frames_test.go
@@ -20,6 +20,8 @@ package execution
import (
"bytes"
"encoding/binary"
+ "math"
+ "strconv"
"testing"
"github.com/stretchr/testify/assert"
@@ -110,6 +112,18 @@ func TestWriteAndReadFrame(t *testing.T) {
assert.True(t, isNilRaw(frame.Err))
}
+func TestReadFrameRejectsLengthPastPlatformInt(t *testing.T) {
+ if strconv.IntSize == 64 {
+ t.Skip("a length past a 64-bit int does not fit the 4-byte
prefix")
+ }
+ var buf bytes.Buffer
+ require.NoError(t, binary.Write(&buf, binary.BigEndian,
uint32(math.MaxInt32)+1))
+
+ _, err := readFrame(&buf)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "exceeds this platform's max")
+}
+
func TestDecodeResponseFrame(t *testing.T) {
// Encode a 3-element response frame: [id, body, error]
var buf bytes.Buffer