From 96831b5e55fbb5c6f0ae6a8e4788a963d96e0eff Mon Sep 17 00:00:00 2001 From: Nancy <9d.24.nancy.sangani@gmail.com> Date: Fri, 27 Feb 2026 20:51:29 +0530 Subject: [PATCH] fix: avoid CGO getgrgid_r segfault in static Linux binaries Signed-off-by: Nancy <9d.24.nancy.sangani@gmail.com> --- .github/workflows/release.yaml | 4 ++-- pkg/archiver/archiver.go | 42 +++++++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 6333e717..c45aadfc 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -77,7 +77,7 @@ jobs: GOARCH: ${{ matrix.goarch }} run: | go build \ - -tags "static system_libgit2 enable_libgit2" \ + -tags "static system_libgit2 enable_libgit2 osusergo netgo" \ -ldflags "-X github.com/modelpack/modctl/pkg/version.GitVersion=${{ github.ref_name }} \ -X github.com/modelpack/modctl/pkg/version.GitCommit=$(git rev-parse --short HEAD) \ -X github.com/modelpack/modctl/pkg/version.BuildTime=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \ @@ -155,4 +155,4 @@ jobs: checksums.txt generate_release_notes: true env: - GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} \ No newline at end of file diff --git a/pkg/archiver/archiver.go b/pkg/archiver/archiver.go index 9afc8459..eb4fcb34 100644 --- a/pkg/archiver/archiver.go +++ b/pkg/archiver/archiver.go @@ -23,8 +23,39 @@ import ( "os" "path/filepath" "strings" + "syscall" ) +// buildTarHeader creates a tar header from FileInfo without using CGO-based +// os/user.LookupGroupId or os/user.LookupUserId. This avoids a segfault in +// statically linked CGO binaries caused by glibc NSS (getgrgid_r) being +// incompatible with static linking across different glibc versions. +// See: https://github.com/modelpack/modctl/issues/285 +func buildTarHeader(info os.FileInfo) (*tar.Header, error) { + header := &tar.Header{ + Name: info.Name(), + Size: info.Size(), + Mode: int64(info.Mode()), + ModTime: info.ModTime(), + } + + // Set file type flag. + if info.IsDir() { + header.Typeflag = tar.TypeDir + } else { + header.Typeflag = tar.TypeReg + } + + // Safely extract UID/GID from syscall.Stat_t without CGO user/group name lookup. + // We intentionally leave Uname/Gname empty to avoid os/user CGO calls entirely. + if stat, ok := info.Sys().(*syscall.Stat_t); ok { + header.Uid = int(stat.Uid) + header.Gid = int(stat.Gid) + } + + return header, nil +} + // Tar creates a tar archive of the specified path (file or directory) // and returns the content as a stream. For individual files, it preserves // the directory structure relative to the working directory. @@ -56,7 +87,7 @@ func Tar(srcPath string, workDir string) (io.Reader, error) { return fmt.Errorf("failed to get relative path: %w", err) } - header, err := tar.FileInfoHeader(info, "") + header, err := buildTarHeader(info) if err != nil { return fmt.Errorf("failed to create tar header: %w", err) } @@ -95,14 +126,13 @@ func Tar(srcPath string, workDir string) (io.Reader, error) { } defer file.Close() - header, err := tar.FileInfoHeader(info, "") + header, err := buildTarHeader(info) if err != nil { pw.CloseWithError(fmt.Errorf("failed to create tar header: %w", err)) return } - // Use relative path as the header name to preserve directory structure - // This keeps the directory structure as part of the file path in the tar. + // Use relative path as the header name to preserve directory structure. relPath, err := filepath.Rel(workDir, srcPath) if err != nil { pw.CloseWithError(fmt.Errorf("failed to get relative path: %w", err)) @@ -189,9 +219,9 @@ func Untar(reader io.Reader, destPath string) error { } file.Close() - // Set correct permissions for the directory. + // Set correct permissions for the file. if err := os.Chmod(targetPath, os.FileMode(header.Mode)); err != nil { - return fmt.Errorf("failed to set directory permissions %s: %w", targetPath, err) + return fmt.Errorf("failed to set file permissions %s: %w", targetPath, err) } // Set modification time for the file. if err := os.Chtimes(targetPath, header.ModTime, header.ModTime); err != nil {