Skip to content

Commit 508764b

Browse files
committed
proc_config: decompress without a shell, and without naming the file twice
The zcat fallback built a command string and ran it through popen, which runs /bin/sh. A shell expands $( ) inside double quotes, so a resolved path containing one executed: a directory named r$(touch FILE) holding proc/config.gz, passed as KASLD_SYSROOT, created FILE. The comment above it argued the double-quoting was sufficient, which is not a property shells have. It was not a rare path either. No musl toolchain ships zlib, so HAVE_ZLIB is empty on all seventeen cross builds and the shell form was the only decompressor there. Open the file once and identify it by descriptor thereafter. zcat reads standard input when given no file argument, so the child gets the open fd and never the name — no command string, no argument vector, nothing left to quote. Where zlib is linked, gzdopen adopts the same descriptor, so both paths differ only in what they hand it to. That also closes the gap between the check and the use: access() tested one name and the decompressor opened it again, and open() reports the same EACCES/EPERM the exit classifier already keys on, so kasld_access, kasld_resolve and the path buffer are all unnecessary now. execvp rather than execv, since zcat is /bin/zcat on some systems and /usr/bin/zcat on others; PATH belongs to the user running kasld, the same trust the shell form already assumed. The child is reaped so an absent zcat is distinguishable from a file that was not gzip, with the empty-output test still deciding.
1 parent 17d5b9d commit 508764b

1 file changed

Lines changed: 87 additions & 24 deletions

File tree

src/components/proc_config.c

Lines changed: 87 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
// and CONFIG_PAGE_OFFSET (32-bit vmsplit).
77
//
88
// Uses zlib for native gzip decompression when available (HAVE_ZLIB),
9-
// otherwise falls back to popen("zcat").
9+
// otherwise spawns zcat with the open descriptor on its standard input.
1010
//
1111
// Detection component — leaks no randomized (KASLR) address.
1212
// Purpose: reads /proc/config.gz to determine whether
@@ -33,9 +33,11 @@
3333
#include "include/kconfig.h"
3434
#include "include/text_order.h"
3535
#include <errno.h>
36+
#include <fcntl.h>
3637
#include <stdio.h>
3738
#include <stdlib.h>
3839
#include <string.h>
40+
#include <sys/wait.h>
3941
#include <unistd.h>
4042

4143
#ifdef HAVE_ZLIB
@@ -61,27 +63,35 @@ KASLD_META("method:detection\n"
6163
static int proc_config_exit = KASLD_EXIT_UNAVAILABLE;
6264

6365
/* Decompress /proc/config.gz into a seekable FILE*.
64-
* Uses zlib if available, otherwise falls back to popen("zcat"). */
66+
* Uses zlib where it is linked, otherwise spawns zcat.
67+
*
68+
* The file is opened ONCE and thereafter identified only by its descriptor,
69+
* never by name. That is what keeps the name out of the decompressor: zcat
70+
* reads standard input when given no file argument, so the child is handed the
71+
* descriptor and no path at all -- there is no command string, no argument
72+
* vector, and so nothing to quote. It also collapses the check and the use into
73+
* one syscall, where a separate access() would leave a window in which the name
74+
* could come to mean a different file. */
6575
static FILE *open_proc_config(void) {
6676
FILE *fp;
6777
char buf[4096];
68-
/* gzopen()/popen() don't go through the kasld_* wrappers, so resolve the
69-
* KASLD_SYSROOT path explicitly and use it for both decompression paths. */
70-
char pathbuf[KASLD_PATH_MAX];
71-
const char *cfg = kasld_resolve(PROC_CONFIG_GZ, pathbuf, sizeof(pathbuf));
7278

7379
kasld_info("checking %s ...", PROC_CONFIG_GZ);
7480

75-
if (kasld_access(PROC_CONFIG_GZ, R_OK) != 0) {
81+
int fd = kasld_open(PROC_CONFIG_GZ, O_RDONLY);
82+
if (fd < 0) {
7683
/* Preserve WHY across the NULL return: a denied config is the target's
77-
* hardening, an absent one is how it was built. */
84+
* hardening, an absent one is how it was built. open() reports the same
85+
* EACCES/EPERM the exit classifier keys on. */
7886
proc_config_exit = kasld_exit_for_errno();
7987
kasld_err("Could not read %s", PROC_CONFIG_GZ);
8088
return NULL;
8189
}
8290

8391
#ifdef HAVE_ZLIB
84-
gzFile gz = gzopen(cfg, "rb");
92+
/* gzdopen takes ownership of fd: gzclose closes it, and on failure the
93+
* descriptor is closed below before the spawn path would have used it. */
94+
gzFile gz = gzdopen(fd, "rb");
8595
if (gz) {
8696
fp = tmpfile();
8797
if (fp) {
@@ -93,45 +103,98 @@ static FILE *open_proc_config(void) {
93103
return fp;
94104
}
95105
gzclose(gz);
106+
return NULL; /* tmpfile() failed; the descriptor went with gzclose */
107+
}
108+
close(fd);
109+
return NULL; /* zlib is linked, so there is no second decompressor to try */
110+
#else
111+
112+
/* No zlib (the static cross builds have none: no musl toolchain ships it, so
113+
* this is the only decompressor there). Spawn zcat directly rather than
114+
* through a shell -- popen would run /bin/sh, and a shell expands $( ) even
115+
* inside double quotes, so a resolved path containing one would execute.
116+
* zcat reads standard input with no file argument, so the child receives the
117+
* open descriptor and never the name.
118+
*
119+
* execvp, not execv: zcat is /bin/zcat on some systems and /usr/bin/zcat on
120+
* others, and busybox installs it wherever its links live. PATH belongs to
121+
* the user running kasld, which is the same trust the shell form already
122+
* assumed. */
123+
int pipefd[2];
124+
if (pipe(pipefd) != 0) {
125+
perror("[-] pipe");
126+
close(fd);
127+
return NULL;
128+
}
129+
130+
pid_t pid = fork();
131+
if (pid < 0) {
132+
perror("[-] fork");
133+
close(pipefd[0]);
134+
close(pipefd[1]);
135+
close(fd);
136+
return NULL;
137+
}
138+
if (pid == 0) {
139+
if (dup2(fd, STDIN_FILENO) < 0 || dup2(pipefd[1], STDOUT_FILENO) < 0)
140+
_exit(127);
141+
close(pipefd[0]);
142+
close(pipefd[1]);
143+
close(fd);
144+
/* A modifiable array rather than a cast of the literal: execvp's argv is
145+
* char *const[], and casting away const on a string literal is the one
146+
* thing -Wcast-qual is looking for. */
147+
char zcat[] = "zcat";
148+
char *const argv[] = {zcat, NULL};
149+
execvp(zcat, argv);
150+
_exit(127); /* zcat absent; the empty output below reports it */
96151
}
97-
#endif
98152

99-
/* Fallback when zlib is not linked (e.g. the static cross builds): decompress
100-
* via zcat and buffer into a seekable tmpfile. Interpolating `cfg` into the
101-
* shell command is safe: it is the fixed literal "/proc/config.gz", or that
102-
* literal under the KASLD_SYSROOT prefix — an environment variable set by the
103-
* same user who runs kasld. kasld is never setuid, so no privilege boundary
104-
* is crossed and the double-quoting is sufficient (no untrusted input reaches
105-
* the shell). */
106-
char cmd[KASLD_PATH_MAX + 16];
107-
snprintf(cmd, sizeof(cmd), "zcat \"%s\"", cfg);
108-
FILE *proc = popen(cmd, "r");
153+
close(pipefd[1]);
154+
close(fd);
155+
FILE *proc = fdopen(pipefd[0], "r");
109156
if (!proc) {
110-
perror("[-] popen");
157+
perror("[-] fdopen");
158+
close(pipefd[0]);
159+
waitpid(pid, NULL, 0);
111160
return NULL;
112161
}
113162

114163
fp = tmpfile();
115164
if (!fp) {
116165
perror("[-] tmpfile");
117-
pclose(proc);
166+
fclose(proc);
167+
waitpid(pid, NULL, 0);
118168
return NULL;
119169
}
120170

121171
size_t n;
122172
while ((n = fread(buf, 1, sizeof(buf), proc)) > 0)
123173
fwrite(buf, 1, n, fp);
124-
pclose(proc);
174+
fclose(proc);
175+
176+
/* Reap before judging the output. The empty-output test below stays the
177+
* arbiter -- a child that emitted a whole config and then exited non-zero
178+
* has still done the job -- but where there is nothing to show, the status
179+
* separates "zcat is not installed" (127 from the failed exec) from "the
180+
* file was not gzip". */
181+
int status = 0;
182+
while (waitpid(pid, &status, 0) < 0 && errno == EINTR)
183+
;
125184

126185
fseek(fp, 0, SEEK_END);
127186
if (ftell(fp) <= 0) {
128-
kasld_err("Failed to decompress %s", PROC_CONFIG_GZ);
187+
if (WIFEXITED(status) && WEXITSTATUS(status) == 127)
188+
kasld_err("zcat not found; cannot decompress %s", PROC_CONFIG_GZ);
189+
else
190+
kasld_err("Failed to decompress %s", PROC_CONFIG_GZ);
129191
fclose(fp);
130192
return NULL;
131193
}
132194
rewind(fp);
133195

134196
return fp;
197+
#endif
135198
}
136199

137200
static int kaslr_disabled_from_config(FILE *fp) {

0 commit comments

Comments
 (0)