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 1/2] 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 { From 7ce7119143354894442311ab0c9ae4aeb9eb8bef Mon Sep 17 00:00:00 2001 From: Nancy <9d.24.nancy.sangani@gmail.com> Date: Tue, 28 Apr 2026 18:43:53 +0530 Subject: [PATCH 2/2] fix(archiver): use tar.FileInfoNames to avoid CGO user/group lookups Signed-off-by: Nancy <9d.24.nancy.sangani@gmail.com> --- pkg/archiver/archiver.go | 76 +++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/pkg/archiver/archiver.go b/pkg/archiver/archiver.go index eb4fcb34..328d0cb8 100644 --- a/pkg/archiver/archiver.go +++ b/pkg/archiver/archiver.go @@ -23,38 +23,17 @@ 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 - } +type noLookupFileInfo struct { + os.FileInfo +} - // 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) - } +// Uname returns an empty user name to skip CGO-based uid→name lookup. +func (n noLookupFileInfo) Uname() (string, error) { return "", nil } - return header, nil -} +// Gname returns an empty group name to skip CGO-based gid→name lookup. +func (n noLookupFileInfo) Gname() (string, error) { return "", 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 @@ -81,24 +60,35 @@ func Tar(srcPath string, workDir string) (io.Reader, error) { return err } - // Create a relative path for the tar file header. - relPath, err := filepath.Rel(workDir, path) - if err != nil { - return fmt.Errorf("failed to get relative path: %w", err) + // Resolve symlink target if needed. + link := "" + if info.Mode()&os.ModeSymlink != 0 { + link, err = os.Readlink(path) + if err != nil { + return fmt.Errorf("failed to read symlink %s: %w", path, err) + } } - header, err := buildTarHeader(info) + header, err := tar.FileInfoHeader(noLookupFileInfo{info}, link) if err != nil { return fmt.Errorf("failed to create tar header: %w", err) } - // Set the header name to preserve directory structure. + // FileInfoHeader only fills the base name; set the full relative path. + relPath, err := filepath.Rel(workDir, path) + if err != nil { + return fmt.Errorf("failed to get relative path: %w", err) + } header.Name = relPath + if info.IsDir() { + header.Name += "/" + } + if err := tw.WriteHeader(header); err != nil { return fmt.Errorf("failed to write header: %w", err) } - if !info.IsDir() { + if !info.IsDir() && info.Mode()&os.ModeSymlink == 0 { file, err := os.Open(path) if err != nil { return fmt.Errorf("failed to open file %s: %w", path, err) @@ -126,21 +116,29 @@ func Tar(srcPath string, workDir string) (io.Reader, error) { } defer file.Close() - header, err := buildTarHeader(info) + // Resolve symlink target if needed. + link := "" + if info.Mode()&os.ModeSymlink != 0 { + link, err = os.Readlink(srcPath) + if err != nil { + pw.CloseWithError(fmt.Errorf("failed to read symlink %s: %w", srcPath, err)) + return + } + } + header, err := tar.FileInfoHeader(noLookupFileInfo{info}, link) 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. + // FileInfoHeader only fills the base name; set the full relative path. relPath, err := filepath.Rel(workDir, srcPath) if err != nil { pw.CloseWithError(fmt.Errorf("failed to get relative path: %w", err)) return } - - // Use the relative path (including directories) as the header name. header.Name = relPath + if err := tw.WriteHeader(header); err != nil { pw.CloseWithError(fmt.Errorf("failed to write header: %w", err)) return