Skip to content
This repository was archived by the owner on May 1, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions libksuid/ksuid.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
#include <libksuid/byteorder.h>
#include <libksuid/rand.h>

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)
Expand Down
36 changes: 36 additions & 0 deletions libksuid/ksuid.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
* -------------------------------------------------------------------------- */
Expand Down
25 changes: 25 additions & 0 deletions tests/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,31 @@ else
message('Skipping test_rand_tls -- <threads.h> 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()
Expand Down
46 changes: 46 additions & 0 deletions tests/test_init_shared.c
Original file line number Diff line number Diff line change
@@ -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 <libksuid/ksuid.h>
#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 ();
}
28 changes: 28 additions & 0 deletions tests/test_smoke.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
#include <libksuid/ksuid.h>
#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)
{
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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 ();
}
Loading