Skip to content

Commit eb0c940

Browse files
committed
api: drop KASLD_* on a privilege-gaining exec
KASLD_COMPONENT_DIR and KASLD_EXEC_WRAPPER name programs kasld executes, and KASLD_SYSROOT names the files it reads. Nothing stripped them, so a set-uid or set-gid install, or one carrying file capabilities, handed whoever set them code execution at that privilege. The dynamic loader sanitises only the variables it owns and these are ordinary ones. No install here sets those bits — the install target uses mode 755 throughout — but nothing would notice if that changed, and setcap on a tool that reads /proc/kallsyms and the kernel log is a plausible step for someone to take. A constructor in api.h, which the orchestrator and every component include, unlinks each KASLD_ entry from environ when getauxval(AT_SECURE) reports the exec gained privilege, falling back to a uid/gid comparison where the auxiliary vector carries no such entry. AT_SECURE is the only one of the two that accounts for file capabilities. The whole prefix goes rather than a list of names, so a variable added later needs no change here. Removing the entries rather than ignoring them at each read also cleans what the component children inherit, which matters because a child gains no privilege at its own exec and cannot detect the case itself. environ is edited directly because unsetenv is not declared under the strict -std=c99 compile that check-headers builds api.h as. Nothing aborts: a privileged install still runs, on its own configuration rather than a caller's. The removal is covered over a synthetic prefix, since dropping the real one in-process would take the staged sysroot with it and send every later test to the live host. kasld(1) records the behaviour beside the env_reset note it already carried.
1 parent a734207 commit eb0c940

3 files changed

Lines changed: 125 additions & 0 deletions

File tree

man/kasld.1

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,13 @@ rule with
187187
or
188188
.BR SETENV ,
189189
or any wrapper that elevates while keeping the environment, does not.
190+
.B kasld
191+
covers the remaining case itself: where the kernel marks the exec as
192+
privilege-gaining \(em a set-uid or set-gid binary, or one carrying file
193+
capabilities \(em every
194+
.B KASLD_*
195+
variable is dropped before it is read, and the run proceeds on its own
196+
configuration.
190197
.TP
191198
.B KASLD_COMPONENT_DIR
192199
Directory to load component binaries from, overriding the default search

src/include/kasld/api.h

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,6 +1228,8 @@ static inline unsigned long arch_default_phys_text_base(void) { return 0; }
12281228
#include <stdio.h>
12291229
#include <stdlib.h>
12301230
#include <string.h>
1231+
#include <sys/auxv.h>
1232+
#include <unistd.h>
12311233

12321234
/* Filesystem-fact reads route through the KASLD_SYSROOT redirection layer so a
12331235
* copied filesystem tree can be analyzed offline. api.h is kasld's universal
@@ -2427,6 +2429,72 @@ __attribute__((constructor)) static void kasld_init_buffering(void) {
24272429
setvbuf(stdout, NULL, _IOLBF, 0);
24282430
}
24292431

2432+
/* environ is undeclared under a strict -std=c99 compile and declared by
2433+
* unistd.h under a feature-test macro, so this covers the first case and the
2434+
* pragma the second, whichever order a translation unit reaches them in. */
2435+
#pragma GCC diagnostic push
2436+
#pragma GCC diagnostic ignored "-Wredundant-decls"
2437+
extern char **environ;
2438+
#pragma GCC diagnostic pop
2439+
2440+
/* Unlink every environment entry whose name starts with `prefix`.
2441+
*
2442+
* The entries are removed from environ directly. unsetenv() is the obvious
2443+
* call and is not declared under a strict -std=c99 compile, whereas environ
2444+
* needs no library function at all; getenv() reads the same array, so a name
2445+
* removed here is a name no later read can find. The strings themselves are
2446+
* left alone — they may not be the allocator's to release. */
2447+
__attribute__((unused)) static void kasld_env_drop_prefix(const char *prefix) {
2448+
size_t n = strlen(prefix);
2449+
char **src, **dst;
2450+
2451+
if (environ == NULL)
2452+
return;
2453+
for (src = dst = environ; *src != NULL; src++) {
2454+
if (strncmp(*src, prefix, n) == 0)
2455+
continue;
2456+
*dst++ = *src;
2457+
}
2458+
*dst = NULL;
2459+
}
2460+
2461+
/* Non-zero when this exec gained privilege: a set-uid or set-gid binary, or
2462+
* one carrying file capabilities.
2463+
*
2464+
* AT_SECURE is the kernel's own verdict and the only one of the two tests that
2465+
* accounts for file capabilities; the uid/gid comparison stands in where the
2466+
* auxiliary vector carries no such entry. */
2467+
__attribute__((unused)) static int kasld_exec_gained_privilege(void) {
2468+
unsigned long secure;
2469+
int gained;
2470+
2471+
errno = 0;
2472+
secure = getauxval(AT_SECURE);
2473+
gained = (secure == 0 && errno != 0)
2474+
? (getuid() != geteuid()) || (getgid() != getegid())
2475+
: (secure != 0);
2476+
errno = 0;
2477+
return gained;
2478+
}
2479+
2480+
/* An exec that gained privilege holds rights its caller does not, while the
2481+
* caller still chose the environment it started in. KASLD_COMPONENT_DIR and
2482+
* KASLD_EXEC_WRAPPER name programs this process executes and KASLD_SYSROOT
2483+
* names the files it reads, so honouring them there lends those rights to
2484+
* whoever set them. The dynamic loader strips only the variables it owns;
2485+
* these are not among them.
2486+
*
2487+
* The whole KASLD_ prefix goes, so a variable added later needs no change
2488+
* here. Dropping the entries rather than ignoring them at each read also
2489+
* cleans the environment the orchestrator's component children inherit, which
2490+
* matters because those children gain no privilege of their own at exec and so
2491+
* cannot detect the case themselves. Nothing aborts: a privileged install
2492+
* still runs, on its own configuration rather than a caller's. */
2493+
__attribute__((constructor)) static void kasld_drop_inherited_env(void) {
2494+
if (kasld_exec_gained_privilege())
2495+
kasld_env_drop_prefix("KASLD_");
2496+
}
2497+
24302498
/* Suppress -Wpedantic "ISO C forbids an empty translation unit". */
24312499
typedef int make_iso_compilers_happy;
24322500

tests/test_kasld.c

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3127,6 +3127,54 @@ static void test_vantage_oracle_readable_each_path(void) {
31273127
th_sysroot_clear();
31283128
}
31293129

3130+
/* The removal half of the privilege-gaining environment guard. The detection
3131+
* half is not exercisable here: this process gained no privilege at exec, and
3132+
* making one that did needs a set-uid install.
3133+
*
3134+
* A synthetic prefix is used rather than the real KASLD_ one. Dropping that
3135+
* prefix in-process would take the staged KASLD_SYSROOT with it and send every
3136+
* later test to the live host — which is the hazard, not an inconvenience. */
3137+
static void test_env_drop_prefix_removes_only_the_prefix(void) {
3138+
setenv("KASLDTEST_ALPHA", "1", 1);
3139+
setenv("KASLDTEST_BETA", "2", 1);
3140+
setenv("KASLDTESTNOUNDERSCORE", "keep", 1);
3141+
setenv("PATH_KASLDTEST_MIDDLE", "keep", 1);
3142+
3143+
assert(getenv("KASLDTEST_ALPHA") != NULL);
3144+
assert(getenv("PATH") != NULL);
3145+
3146+
kasld_env_drop_prefix("KASLDTEST_");
3147+
3148+
/* Every name carrying the prefix goes, including one no read site knows
3149+
* about — the property that keeps a variable added later covered. */
3150+
assert(getenv("KASLDTEST_ALPHA") == NULL);
3151+
assert(getenv("KASLDTEST_BETA") == NULL);
3152+
3153+
/* A name merely beginning with the same letters, or carrying them in the
3154+
* middle, is not the prefix and stays. */
3155+
assert(getenv("KASLDTESTNOUNDERSCORE") != NULL);
3156+
assert(getenv("PATH_KASLDTEST_MIDDLE") != NULL);
3157+
3158+
/* Unrelated entries survive, the staged sysroot among them, and the block is
3159+
* still a well-formed environment that setenv and getenv work over. */
3160+
assert(getenv("PATH") != NULL);
3161+
assert(getenv("KASLD_SYSROOT") != NULL);
3162+
setenv("KASLDTEST_GAMMA", "3", 1);
3163+
assert(getenv("KASLDTEST_GAMMA") != NULL);
3164+
3165+
kasld_env_drop_prefix("KASLDTEST_");
3166+
assert(getenv("KASLDTEST_GAMMA") == NULL);
3167+
assert(getenv("KASLDTESTNOUNDERSCORE") != NULL);
3168+
}
3169+
3170+
/* An ordinary run must not lose its environment: the constructor has already
3171+
* run by the time any test does, and it must have changed nothing. */
3172+
static void test_unprivileged_exec_keeps_its_environment(void) {
3173+
assert(kasld_exec_gained_privilege() == 0);
3174+
assert(getenv("PATH") != NULL);
3175+
assert(getenv("KASLD_SYSROOT") != NULL);
3176+
}
3177+
31303178
int main(void) {
31313179
th_sysroot_init("kasld");
31323180
TEST_SUITE("test_kasld");
@@ -3246,6 +3294,8 @@ int main(void) {
32463294
RUN(test_vantage_group_names);
32473295
RUN(test_unread_marker_separates_denial_from_absence);
32483296
RUN(test_environment_defaults_to_unknown);
3297+
RUN(test_env_drop_prefix_removes_only_the_prefix);
3298+
RUN(test_unprivileged_exec_keeps_its_environment);
32493299
RUN(test_vantage_mac_absent_then_present);
32503300
RUN(test_vantage_oracle_readable_each_path);
32513301
RUN(test_discard_ledger_aggregates_and_reports_truncation);

0 commit comments

Comments
 (0)