From 641cfc250d0e50ac24dcb404422aba5ac01c988d Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Thu, 16 Jul 2026 17:07:53 -0700 Subject: [PATCH 1/4] Fix Get/SetAPI returning unsupported with newer runtime libseccomp When built against libseccomp headers older than v2.4.0, this package compiles its own always-fails fallback definitions of seccomp_api_get and seccomp_api_set. Because cgo places the whole preamble into the same translation unit as the code that calls these functions, the call is resolved at compile time: even though the libseccomp shared library actually loaded at runtime may be much newer and fully support these calls, the compiled-in stub always runs instead, so GetAPI() incorrectly reports "API level operations are not supported" (API level 0). Fix this by having the fallback look up the real symbol at runtime via dlsym(RTLD_DEFAULT, ...), only returning the "unsupported" result if that lookup fails. This correctly handles the common case of a binary built against one libseccomp version but run against another, newer one. Signed-off-by: Kir Kolyshkin --- seccomp_api_compat.go | 18 ++++++++++++ seccomp_api_compat.h | 64 +++++++++++++++++++++++++++++++++++++++++++ seccomp_internal.go | 20 ++++---------- 3 files changed, 87 insertions(+), 15 deletions(-) create mode 100644 seccomp_api_compat.go create mode 100644 seccomp_api_compat.h diff --git a/seccomp_api_compat.go b/seccomp_api_compat.go new file mode 100644 index 0000000..afd8324 --- /dev/null +++ b/seccomp_api_compat.go @@ -0,0 +1,18 @@ +package seccomp + +// -ldl is needed for the dlsym()-based fallback in seccomp_api_compat.h, +// used when this package is compiled against libseccomp headers older +// than v2.4.0. cgo directives can only be written as Go comments (they +// are parsed by the cgo tool itself, not the C compiler), so this can't +// live inside seccomp_api_compat.h; it's kept in its own file instead +// of seccomp_internal.go to stay next to what it's for. +// +// This flag is always passed, even when building against headers new +// enough that seccomp_api_compat.h isn't included, since cgo directives +// can't be conditioned on the C preprocessor macros that decide that. +// That's harmless: on any modern libc, -ldl either links a compatibility +// stub (the dl* functions have lived in libc itself since glibc 2.34) or +// a small, universally available library. + +// #cgo LDFLAGS: -ldl +import "C" diff --git a/seccomp_api_compat.h b/seccomp_api_compat.h new file mode 100644 index 0000000..ab7ec22 --- /dev/null +++ b/seccomp_api_compat.h @@ -0,0 +1,64 @@ +// This file provides fallback definitions of seccomp_api_get() and +// seccomp_api_set(), which were added to libseccomp in v2.4.0. It is +// only included (from seccomp_internal.go) when this package is being +// compiled against libseccomp headers older than that, which don't +// declare these two functions at all. +// +// The libseccomp shared library actually loaded at runtime may still +// be v2.4.0 or newer (and thus provide real implementations of these +// functions) even though the headers used to compile this package are +// older -- for example, this package built against v2.3.3 headers, but +// run against a system libseccomp v2.5.5 or newer. A naive fallback +// that just always returns "unsupported" would permanently hide the +// real API level support in that case: cgo places the whole preamble +// (which is where this file gets included) into the same translation +// unit as the code that calls these functions, so a statically +// compiled-in fallback would be resolved directly at compile time and +// would always run, regardless of what's available at runtime. +// +// So instead, look the real symbols up in whatever shared library +// provides them at runtime via dlsym(), and only fall back to the +// "unsupported" behavior if that lookup fails. The lookup itself is +// only ever done once, in a constructor that runs at load time (before +// any Go code, so before any goroutines or extra OS threads exist), +// which lets it be a simple unsynchronized global write instead of a +// dlsym() call on every invocation guarded by a lock or an atomic +// check-and-set. + +#include +#include +#include + +typedef unsigned int (*seccomp_api_get_func)(void); +typedef int (*seccomp_api_set_func)(unsigned int); + +static seccomp_api_get_func real_seccomp_api_get; +static seccomp_api_set_func real_seccomp_api_set; + +__attribute__((constructor)) +static void resolve_real_seccomp_api_funcs(void) +{ + real_seccomp_api_get = (seccomp_api_get_func)dlsym(RTLD_DEFAULT, "seccomp_api_get"); + real_seccomp_api_set = (seccomp_api_set_func)dlsym(RTLD_DEFAULT, "seccomp_api_set"); +} + +static unsigned int seccomp_api_get(void) +{ + if (real_seccomp_api_get != NULL) + return real_seccomp_api_get(); + + // libseccomp-golang requires libseccomp v2.2.0, at a minimum, which + // supported API level 2. However, the kernel may not support API level + // 2 constructs which are the seccomp() system call and the TSYNC + // filter flag. Return the "reserved" value of 0 here to indicate that + // proper API level support is not available in libseccomp. + return 0; +} + +static int seccomp_api_set(unsigned int level) +{ + if (real_seccomp_api_set != NULL) + return real_seccomp_api_set(level); + + return -EOPNOTSUPP; +} diff --git a/seccomp_internal.go b/seccomp_internal.go index cc90512..b2914aa 100644 --- a/seccomp_internal.go +++ b/seccomp_internal.go @@ -186,22 +186,12 @@ unsigned int get_micro_version() return seccomp_version()->micro; } -// The libseccomp API level functions were added in v2.4.0 +// The libseccomp API level functions were added in v2.4.0; headers +// older than that don't declare seccomp_api_get()/seccomp_api_set() at +// all. See seccomp_api_compat.h for why the fallback used in that case +// lives in its own file instead of being defined here directly. #if SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR < 4 -const unsigned int seccomp_api_get(void) -{ - // libseccomp-golang requires libseccomp v2.2.0, at a minimum, which - // supported API level 2. However, the kernel may not support API level - // 2 constructs which are the seccomp() system call and the TSYNC - // filter flag. Return the "reserved" value of 0 here to indicate that - // proper API level support is not available in libseccomp. - return 0; -} - -int seccomp_api_set(unsigned int level) -{ - return -EOPNOTSUPP; -} +#include "seccomp_api_compat.h" #endif typedef struct scmp_arg_cmp* scmp_cast_t; From 8f40fd4181585ff64b19ffe83c6aac0426488ade Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Thu, 16 Jul 2026 17:08:41 -0700 Subject: [PATCH 2/4] Fix notify support returning unsupported with newer runtime libseccomp Same issue as the previous commit but for seccomp_notify_* functions: when built against libseccomp headers older than v2.5.0, this package compiles its own always-fails fallback definitions of them, which, due to cgo placing the whole preamble into the same translation unit as the calling code, get resolved at compile time regardless of what the runtime libseccomp shared library actually supports. Unlike seccomp_api_get/set, these fallbacks are only reachable via notifSupported(), which already gates every other notify call. So instead of a dlsym()-based runtime lookup, just track at compile time whether the real functions were available (HAVE_NOTIFY) and have notifSupported() report unsupported directly in that case, without ever calling into the fallback stubs. Signed-off-by: Kir Kolyshkin --- seccomp_internal.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/seccomp_internal.go b/seccomp_internal.go index b2914aa..6b4355b 100644 --- a/seccomp_internal.go +++ b/seccomp_internal.go @@ -261,6 +261,10 @@ int seccomp_notify_respond(int fd, struct seccomp_notif_resp *resp) { return -EOPNOTSUPP; } +#define HAVE_NOTIFY 0 + +#else // seccomp >= 2.5.0 +#define HAVE_NOTIFY 1 #endif */ import "C" @@ -294,12 +298,16 @@ const ( // Comparison boundaries to check for comparison operator validity compareOpStart ScmpCompareOp = CompareNotEqual compareOpEnd ScmpCompareOp = CompareMaskedEqual + // Whether this package was compiled against libseccomp headers + // new enough to provide real seccomp_notify_* functions. + notifyAvailable = C.HAVE_NOTIFY != 0 ) var ( // errBadFilter is thrown on bad filter context. - errBadFilter = errors.New("filter is invalid or uninitialized") - errDefAction = errors.New("requested action matches default action of filter") + errBadFilter = errors.New("filter is invalid or uninitialized") + errDefAction = errors.New("requested action matches default action of filter") + errNotifUnsup = errors.New("seccomp notification requires libseccomp >= 2.5.0 at build time") // Constants representing library major, minor, and micro versions verMajor = uint(C.get_major_version()) verMinor = uint(C.get_minor_version()) @@ -777,6 +785,9 @@ func checkAPI(op string, minLevel uint, major, minor, micro uint) error { // Calls to C.seccomp_notify* hidden from seccomp.go func notifSupported() error { + if !notifyAvailable { + return errNotifUnsup + } return checkAPI("seccomp notification", 6, 2, 5, 0) } From ee0f8b6767cabbe3aa6ede5e4bd41540d2898eba Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Thu, 16 Jul 2026 12:28:54 -0700 Subject: [PATCH 3/4] ci: test against latest libseccomp This adds a CI step to run tests from the test binary which was built using older libseccomp version against the latest libseccomp version. This should reveal a bug fixed by the previous commit. Signed-off-by: Kir Kolyshkin --- .github/actions/build-libseccomp/action.yml | 58 +++++++++++++++++++++ .github/workflows/test.yml | 58 +++++++-------------- Makefile | 11 ++-- 3 files changed, 86 insertions(+), 41 deletions(-) create mode 100644 .github/actions/build-libseccomp/action.yml diff --git a/.github/actions/build-libseccomp/action.yml b/.github/actions/build-libseccomp/action.yml new file mode 100644 index 0000000..d560140 --- /dev/null +++ b/.github/actions/build-libseccomp/action.yml @@ -0,0 +1,58 @@ +name: build libseccomp +description: Build and install a specific version of libseccomp, and set up the environment to build and test against it. +inputs: + version: + description: libseccomp version (git tag/branch) to build. Use vX.X.X to use a release tarball. + required: true + +runs: + using: composite + steps: + - name: build libseccomp ${{ inputs.version }} + shell: bash + run: | + set -x + VER="${{ inputs.version }}" + PREFIX="$(pwd)/seccomp-$VER" + LIBDIR="$PREFIX/lib" + + if [[ "$VER" == v* ]]; then + # Released version: fetch and unpack the release tarball. + TAR_FILE="libseccomp-${VER#v}.tar.gz" + TAR_URL="https://github.com/seccomp/libseccomp/releases/download/$VER/$TAR_FILE" + mkdir libseccomp + curl -sSfL "$TAR_URL" | tar -xzf - -C libseccomp --strip-components=1 + cd libseccomp + else + # Get it from git. + git clone https://github.com/seccomp/libseccomp + cd libseccomp + git checkout "$VER" + # In main branch, configure.ac sets libseccomp version to 0.0.0, which + # results in error when compiling libseccomp-golang. While 0.0.0 is + # there for a reason, here we need to build and test against HEAD, so + # set it to a suitable value. + # + # Version 9.9.9 is used because: + # - version >= current is needed; + # - chances are good such version won't ever exist; + # - it is easy to spot in tests output; + # - the LIBFILE pattern below expects single digits. + if [ "$VER" == "HEAD" ]; then + VER=9.9.9 + sed -i "/^AC_INIT(/s/0\.0\.0/$VER/" configure.ac + fi + ./autogen.sh + fi + ./configure --prefix="$PREFIX" --libdir="$LIBDIR" + make + sudo make install + cd - + rm -rf libseccomp + + # For the next steps to build and execute with the compiled library. + echo "PKG_CONFIG_LIBDIR=$LIBDIR/pkgconfig" >> $GITHUB_ENV + LIBFILE="$(echo $LIBDIR/libseccomp.so.?.?.?)" + echo "LD_PRELOAD=$LIBFILE" >> $GITHUB_ENV + # For TestExpectedSeccompVersion. + echo "_EXPECTED_LIBSECCOMP_VERSION=$VER" >> $GITHUB_ENV diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index efae350..c25ed3c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,9 +14,12 @@ jobs: fail-fast: false matrix: go-version: [1.19.x, 1.25.x, 1.26.x] - libseccomp: ["v2.3.3", "v2.4.4", "v2.5.6", "v2.6.0", "HEAD"] + libseccomp: ["v2.3.3", "v2.4.4", "v2.5.6", "v2.6.1", "HEAD"] os: [ubuntu-24.04, ubuntu-24.04-arm] + env: + LATEST_RELEASED_LIBSECCOMP: v2.6.1 + runs-on: ${{ matrix.os }} steps: @@ -24,46 +27,15 @@ jobs: - name: checkout uses: actions/checkout@v6 - - name: build libseccomp ${{ matrix.libseccomp }} + - name: install libseccomp build deps run: | - set -x sudo apt -qq update sudo apt -qq install gperf - PREFIX="$(pwd)/seccomp" - LIBDIR="$PREFIX/lib" - - git clone https://github.com/seccomp/libseccomp - cd libseccomp - git checkout ${{ matrix.libseccomp }} - # In main branch, configure.ac sets libseccomp version to 0.0.0, which - # results in error when compiling libseccomp-golang. While 0.0.0 is - # there for a reason, here we need to build and test against HEAD, so - # set it to a suitable value. - # - # Version 9.9.9 is used because: - # - version >= current is needed; - # - chances are good such version won't ever exist; - # - it is easy to spot in tests output; - # - the LIBFILE pattern below expects single digits. - VER="${{ matrix.libseccomp }}" - if [ "$VER" == "HEAD" ]; then - VER=9.9.9 - sed -i "/^AC_INIT(/s/0\.0\.0/$VER/" configure.ac - fi - ./autogen.sh - ./configure --prefix="$PREFIX" --libdir="$LIBDIR" - make - sudo make install - cd - - rm -rf libseccomp - - # For the next steps to build and execute with the compiled library. - echo "PKG_CONFIG_LIBDIR=$LIBDIR/pkgconfig" >> $GITHUB_ENV - LIBFILE="$(echo $LIBDIR/libseccomp.so.?.?.?)" - echo "LD_PRELOAD=$LIBFILE" >> $GITHUB_ENV - # For TestExpectedSeccompVersion. - echo "_EXPECTED_LIBSECCOMP_VERSION=$VER" >> $GITHUB_ENV + - name: build libseccomp ${{ matrix.libseccomp }} + uses: ./.github/actions/build-libseccomp + with: + version: ${{ matrix.libseccomp }} - name: install go ${{ matrix.go-version }} uses: actions/setup-go@v6 @@ -75,13 +47,23 @@ jobs: go.sum ${{ env.PKG_CONFIG_LIBDIR }}/libseccomp.pc - - name: build run: make check-build - name: test run: make test + - name: build the latest released libseccomp + if: matrix.libseccomp != env.LATEST_RELEASED_LIBSECCOMP + uses: ./.github/actions/build-libseccomp + with: + version: ${{ env.LATEST_RELEASED_LIBSECCOMP }} + + - name: test with latest released libseccomp + if: matrix.libseccomp != env.LATEST_RELEASED_LIBSECCOMP + # Run the test (pre-built with other seccomp version above). + run: make test-run + all-done: needs: - test diff --git a/Makefile b/Makefile index 530f5b4..9fbbfff 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # libseccomp-golang -.PHONY: all check check-build check-syntax fix-syntax vet test lint +.PHONY: all check check-build check-syntax fix-syntax vet test test-build test-run lint all: check-build @@ -24,8 +24,13 @@ vet: # be noticed earlier in the CI. TEST_TIMEOUT=10s -test: - go test -v -timeout $(TEST_TIMEOUT) +test: test-build test-run + +test-build: + go test -c . + +test-run: + ./libseccomp-golang.test -test.v -test.timeout $(TEST_TIMEOUT) lint: golangci-lint run . From 09c89428b8919be6e3b2e13f240ba890f108f3d7 Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Thu, 16 Jul 2026 13:32:36 -0700 Subject: [PATCH 4/4] [test] test against system libseccomp Signed-off-by: Kir Kolyshkin --- .github/workflows/test.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c25ed3c..e549085 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,10 +27,10 @@ jobs: - name: checkout uses: actions/checkout@v6 - - name: install libseccomp build deps + - name: install libseccomp and its build deps run: | sudo apt -qq update - sudo apt -qq install gperf + sudo apt -qq install libseccomp2 gperf - name: build libseccomp ${{ matrix.libseccomp }} uses: ./.github/actions/build-libseccomp @@ -53,6 +53,12 @@ jobs: - name: test run: make test + - name: test with system libseccomp + run: | + unset LD_PRELOAD PKG_CONFIG_LIBDIR + export _EXPECTED_LIBSECCOMP_VERSION="$(dpkg-query -W -f='${Version}' libseccomp2 | sed -E 's/^[0-9]+://; s/-[^-]*$//')" + make test-run + - name: build the latest released libseccomp if: matrix.libseccomp != env.LATEST_RELEASED_LIBSECCOMP uses: ./.github/actions/build-libseccomp