diff --git a/libksuid/ksuid.c b/libksuid/ksuid.c index ebf4ed0..44be238 100644 --- a/libksuid/ksuid.c +++ b/libksuid/ksuid.c @@ -17,14 +17,14 @@ #include #include -KSUID_PUBLIC const ksuid_t KSUID_NIL = {.b = {0} }; - -KSUID_PUBLIC const ksuid_t KSUID_MAX = { - .b = { - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - }, -}; +/* Drive both definitions from the public KSUID_*_INIT macros so the + * runtime symbols and the static-storage initializer form can never + * drift out of byte-for-byte agreement -- the regression + * test_init_macros_match_symbols pins the same equivalence at runtime + * but a single source of truth at the definition site removes the + * possibility entirely. */ +KSUID_PUBLIC const ksuid_t KSUID_NIL = KSUID_NIL_INIT; +KSUID_PUBLIC const ksuid_t KSUID_MAX = KSUID_MAX_INIT; bool ksuid_is_nil (const ksuid_t *id) diff --git a/libksuid/ksuid.h b/libksuid/ksuid.h index 6e1d8d4..3dc400b 100644 --- a/libksuid/ksuid.h +++ b/libksuid/ksuid.h @@ -47,6 +47,22 @@ extern "C" uint8_t b[KSUID_BYTES]; } ksuid_t; +/* The static-storage initializer macros below assume ksuid_t is + * exactly its byte array -- no padding, no extra fields. If a future + * change adds a field, the assertion fails at compile time and forces + * the macro author to update KSUID_NIL_INIT / KSUID_MAX_INIT in + * lockstep. C11 spells the assertion `_Static_assert`; C++ since + * C++11 spells it `static_assert`. Gate so the public header + * compiles for both, since this file lives inside extern "C" for + * C++ consumers. */ +#ifdef __cplusplus + static_assert (sizeof (ksuid_t) == KSUID_BYTES, + "ksuid_t must be exactly KSUID_BYTES; KSUID_*_INIT macros depend on it"); +#else + _Static_assert (sizeof (ksuid_t) == KSUID_BYTES, + "ksuid_t must be exactly KSUID_BYTES; KSUID_*_INIT macros depend on it"); +#endif + typedef enum ksuid_err { KSUID_OK = 0, @@ -59,9 +75,29 @@ extern "C" KSUID_ERR_TIME_RANGE = -7 /* unix_seconds outside KSUID epoch range */ } ksuid_err_t; +/* Two forms of the same sentinel values: + * + * - KSUID_NIL / KSUID_MAX (extern const ksuid_t) + * Use these for runtime comparison and parameter passing: + * if (ksuid_compare (&id, &KSUID_NIL) == 0) ... + * + * - KSUID_NIL_INIT / KSUID_MAX_INIT (aggregate-initializer macros) + * Use these as constant expressions in a declaration: + * static const ksuid_t g_zero = KSUID_NIL_INIT; + * The macro form is REQUIRED on Windows DLL builds, where + * KSUID_PUBLIC expands to __declspec(dllimport) and the + * symbol is therefore not a constant expression in user TUs. + * + * The two forms are guaranteed byte-for-byte equal; tests/test_smoke.c + * pins the equivalence with ASSERT_EQ_BYTES. */ KSUID_PUBLIC extern const ksuid_t KSUID_NIL; KSUID_PUBLIC extern const ksuid_t KSUID_MAX; +#define KSUID_NIL_INIT { { 0 } } +#define KSUID_MAX_INIT \ + { { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, \ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff } } + /* -------------------------------------------------------------------------- * Predicates and ordering. * -------------------------------------------------------------------------- */ diff --git a/tests/meson.build b/tests/meson.build index 5eeccdb..b294d70 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -32,6 +32,31 @@ else message('Skipping test_rand_tls -- not available on this host') endif +# Issue #1 (KSUID_NIL_INIT / KSUID_MAX_INIT) is specifically about +# Windows DLL consumers, where KSUID_PUBLIC expands to +# __declspec(dllimport) and the public sentinel symbols stop being +# constant expressions in user TUs. Every other test in this +# directory links the static archive (where KSUID_BUILDING stays +# defined and dllimport never triggers), so they DO NOT exercise +# the regression issue #1 was filed against. +# +# This dedicated TU links the SHARED library and explicitly undefines +# KSUID_BUILDING so the public header sees the dllimport / hidden- +# visibility consumer code path. On Windows this catches the original +# bug; on Linux/macOS it exercises shared-library export semantics +# uniformly. +if cc.get_argument_syntax() == 'msvc' + test_init_shared_args = ['/UKSUID_BUILDING'] +else + test_init_shared_args = ['-UKSUID_BUILDING'] +endif +exe = executable('test_init_shared', 'test_init_shared.c', + include_directories : test_inc, + link_with : ksuid_lib.get_shared_lib(), + c_args : test_init_shared_args, +) +test('test_init_shared', exe, timeout : 30) + # Integration test for the ksuid-gen CLI; only registered when the # CLI is built (default on). meson resolves File objects and build # targets to absolute paths automatically when they appear in test() diff --git a/tests/test_init_shared.c b/tests/test_init_shared.c new file mode 100644 index 0000000..64c5dfb --- /dev/null +++ b/tests/test_init_shared.c @@ -0,0 +1,46 @@ +/* SPDX-License-Identifier: LGPL-3.0-or-later + * + * Companion to test_smoke.c that links against the libksuid SHARED + * library rather than the static archive. This is the Windows DLL + * consumer scenario in miniature: KSUID_PUBLIC expands to + * __declspec(dllimport) here (because KSUID_BUILDING is undefined for + * this TU), and KSUID_NIL / KSUID_MAX are therefore *not* constant + * expressions in this translation unit. If the build of this file + * succeeds, the public KSUID_*_INIT macros really do work as static- + * storage initialisers under the same constraints downstream + * consumers will face. The test then memcmp's against the symbols + * to prove the bytes match. + * + * This file exists because tests/test_smoke.c links the static + * archive (KSUID_BUILDING is in scope, no dllimport, the symbol is a + * normal const) and so does NOT exercise the very codepath issue #1 + * was filed about. */ + +#include +#include "test_util.h" + +/* File-scope static storage from the macro form. On Windows DLL + * consumers the equivalent `static const ksuid_t g = KSUID_NIL;` + * fails with C2099 "initializer is not a constant" -- that is the + * regression we are guarding against. */ +static const ksuid_t kSharedNilInit = KSUID_NIL_INIT; +static const ksuid_t kSharedMaxInit = KSUID_MAX_INIT; + +static void +test_macros_at_static_storage_match_shared_symbols (void) +{ + /* If KSUID_NIL / KSUID_MAX really are dllimport on this build (the + * Windows lane) the runtime symbol resolution lands here at + * comparison time, after the file-scope statics above were already + * frozen at load time from the macro values. */ + ASSERT_EQ_BYTES (kSharedNilInit.b, KSUID_NIL.b, KSUID_BYTES); + ASSERT_EQ_BYTES (kSharedMaxInit.b, KSUID_MAX.b, KSUID_BYTES); + ASSERT_TRUE (ksuid_is_nil (&kSharedNilInit)); +} + +int +main (void) +{ + RUN_TEST (test_macros_at_static_storage_match_shared_symbols); + TEST_MAIN_END (); +} diff --git a/tests/test_smoke.c b/tests/test_smoke.c index 23ebe02..a1259d0 100644 --- a/tests/test_smoke.c +++ b/tests/test_smoke.c @@ -2,6 +2,15 @@ #include #include "test_util.h" +/* File-scope statics initialised from the public macros. The point of + * this test pattern is that exactly this codepath -- aggregate + * initialisation at static storage from a public sentinel -- is what + * fails to compile on Windows DLL with the symbol form (issue #1). If + * this TU compiles, KSUID_*_INIT works as a constant expression on + * the target. The runtime memcmp below proves the bytes are right. */ +static const ksuid_t kStaticNilInit = KSUID_NIL_INIT; +static const ksuid_t kStaticMaxInit = KSUID_MAX_INIT; + static void test_constants_have_expected_layout (void) { @@ -48,6 +57,24 @@ test_compare_first_byte_dominates (void) ASSERT_TRUE (ksuid_compare (&b, &a) < 0); } +static void +test_init_macros_match_symbols (void) +{ + /* File-scope statics: the codepath that fails on Windows DLL today. + * Byte-for-byte parity with the runtime symbols proves the macros + * encode the right constants. */ + ASSERT_EQ_BYTES (kStaticNilInit.b, KSUID_NIL.b, KSUID_BYTES); + ASSERT_EQ_BYTES (kStaticMaxInit.b, KSUID_MAX.b, KSUID_BYTES); + ASSERT_TRUE (ksuid_is_nil (&kStaticNilInit)); + + /* Block-scope static storage: distinct codepath from file scope on + * some compilers, worth pinning separately. */ + static const ksuid_t local_nil = KSUID_NIL_INIT; + static const ksuid_t local_max = KSUID_MAX_INIT; + ASSERT_TRUE (ksuid_is_nil (&local_nil)); + ASSERT_EQ_BYTES (local_max.b, KSUID_MAX.b, KSUID_BYTES); +} + static void test_version_macros_are_consistent (void) { @@ -84,6 +111,7 @@ main (void) RUN_TEST (test_max_is_all_ff); RUN_TEST (test_compare_orders_lex); RUN_TEST (test_compare_first_byte_dominates); + RUN_TEST (test_init_macros_match_symbols); RUN_TEST (test_version_macros_are_consistent); TEST_MAIN_END (); }