From 9f6fa2cf4bf5e70b0b433f42853a7aa3178505c7 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 25 Aug 2026 20:20:38 +0900 Subject: [PATCH] go-pathrs: fix build failure with Clang The PATHRS_PROC_* values are defined in as an enum with values that are too large to fit in an int, and CGo's interpretation of their signedness turns out to be compiler-dependent: GCC hands them to us as implicitly-converted (negative) signed constants, while Clang gives us the actual unsigned values. Our sanity check in init() only handled the GCC behaviour (by assigning them to an int64), so builds using Clang failed with libpathrs_linux.go:256:32: cannot use (_Ciconst_PATHRS_PROC_ROOT) (untyped int constant 18446744067006164835) as int64 value in variable declaration (overflows) Neither int64 nor uint64 can hold the constant with both compilers. However, Go constant expressions are arbitrary-precision and bitwise operations on negative constants operate on their infinite two's complement representation, so masking off the low 64 bits gives us the same uint64 value regardless of which compiler CGo used. Note that this only affected the internal cross-check of the values (the constants themselves are open-coded on the Go side), so no user-visible behaviour changes -- but the package could not be built at all with Clang. Also add a CC={gcc,clang} dimension to the Go smoke-test matrix so this doesn't regress. CC is only set for the smoke-test step so that we don't change how libpathrs itself is built. Fixes #401 Assisted-by: Claude Opus 5 Signed-off-by: Akihiro Suda --- .github/workflows/bindings-go.yml | 9 +++++ CHANGELOG.md | 5 +++ .../internal/libpathrs/libpathrs_linux.go | 37 +++++++++++++------ 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/.github/workflows/bindings-go.yml b/.github/workflows/bindings-go.yml index 18471f0e..92fa84b3 100644 --- a/.github/workflows/bindings-go.yml +++ b/.github/workflows/bindings-go.yml @@ -76,6 +76,11 @@ jobs: - "1.18" - "oldstable" - "stable" + # CGo's handling of C constants differs between compilers, so make sure + # the bindings build with both of the mainstream C compilers. + cc: + - gcc + - clang runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -92,7 +97,11 @@ jobs: go-version: ${{ matrix.go-version }} check-latest: true # Run smoke-tests. + # NOTE: CC is only set here (and not for the whole job) so that we don't + # affect how libpathrs itself is built. - run: make -C examples/go smoke-test + env: + CC: ${{ matrix.cc }} go-complete: needs: diff --git a/CHANGELOG.md b/CHANGELOG.md index 36f7cba9..c44be786 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). library installation were incorrectly called `--disable-dynamic` rather than autoconf's `--disable-shared`. We have added new `--{enable,disable}-shared` flags and the old ones are kept for compatibility. +- go-pathrs: Fix a build failure when building the Go bindings with Clang. + CGo's handling of the signedness of C constants is compiler-dependent, and + the internal sanity check of the `PATHRS_PROC_*` values only handled the way + GCC does it (which made Clang builds fail with "untyped int constant + ... overflows"). (#401) ## [0.2.5] - 2026-06-17 ## diff --git a/go-pathrs/internal/libpathrs/libpathrs_linux.go b/go-pathrs/internal/libpathrs/libpathrs_linux.go index d610a3be..7c907737 100644 --- a/go-pathrs/internal/libpathrs/libpathrs_linux.go +++ b/go-pathrs/internal/libpathrs/libpathrs_linux.go @@ -219,10 +219,12 @@ func InRootHardlink(oldRootFd uintptr, oldPath string, newRootFd uintptr, newPat // ProcBase is pathrs_proc_base_t (uint64_t). type ProcBase C.pathrs_proc_base_t -// FIXME: We need to open-code the constants because CGo unfortunately will -// implicitly convert any non-literal constants (i.e. those resolved using gcc) -// to signed integers. See for some -// more information on the underlying issue (though. +// FIXME: We need to open-code the constants because CGo's handling of +// non-literal constants (i.e. those resolved using the C compiler) that don't +// fit in an int64 is compiler-dependent -- GCC gives us implicitly-converted +// (negative) signed integers while Clang gives us the actual unsigned values. +// See for some more information on +// the underlying issue. const ( // ProcRoot is PATHRS_PROC_ROOT. ProcRoot ProcBase = 0xFFFF_FFFE_7072_6F63 // C.PATHRS_PROC_ROOT @@ -246,16 +248,27 @@ func assertEqual[T comparable](a, b T, msg string) { } } +// u64Mask is used to convert the C constants below into plain uint64 values, +// regardless of how CGo decided to interpret their signedness. +// +// As explained above, CGo gives us the PATHRS_PROC_* values as negative +// (signed) constants with GCC and as (unsigned) constants that overflow int64 +// with Clang, so neither int64 nor uint64 can hold them with both compilers. +// However, Go constant expressions are arbitrary-precision and bitwise +// operations on negative constants operate on their infinite two's complement +// representation, so masking off the lower 64 bits yields the same uint64 +// value no matter which compiler CGo used. +const u64Mask = 0xFFFF_FFFF_FFFF_FFFF + // Verify that the values above match the actual C values. Unfortunately, Go -// only allows us to forcefully cast int64 to uint64 if you use a temporary -// variable, which means we cannot do it in a const context and thus need to do -// it at runtime (even though it is a check that fundamentally could be done at +// has no way of doing compile-time assertions, so we need to do it at runtime +// (even though it is a check that fundamentally could be done at // compile-time)... func init() { var ( - actualProcRoot int64 = C.PATHRS_PROC_ROOT - actualProcSelf int64 = C.PATHRS_PROC_SELF - actualProcThreadSelf int64 = C.PATHRS_PROC_THREAD_SELF + actualProcRoot uint64 = C.PATHRS_PROC_ROOT & u64Mask + actualProcSelf uint64 = C.PATHRS_PROC_SELF & u64Mask + actualProcThreadSelf uint64 = C.PATHRS_PROC_THREAD_SELF & u64Mask ) assertEqual(ProcRoot, ProcBase(actualProcRoot), "PATHRS_PROC_ROOT") @@ -263,8 +276,8 @@ func init() { assertEqual(ProcThreadSelf, ProcBase(actualProcThreadSelf), "PATHRS_PROC_THREAD_SELF") var ( - actualProcBaseTypeMask uint64 = C.__PATHRS_PROC_TYPE_MASK - actualProcBaseTypePid uint64 = C.__PATHRS_PROC_TYPE_PID + actualProcBaseTypeMask uint64 = C.__PATHRS_PROC_TYPE_MASK & u64Mask + actualProcBaseTypePid uint64 = C.__PATHRS_PROC_TYPE_PID & u64Mask ) assertEqual(ProcBaseTypeMask, ProcBase(actualProcBaseTypeMask), "__PATHRS_PROC_TYPE_MASK")