Skip to content

Commit 5a80810

Browse files
committed
progress: fix gzip -l injection and EINTR wait loop
Avoid popen()/shell when running gzip -l by forking gzip with argv, and handle EINTR correctly in the wait loop. Add an ATF regression test for -z with metacharacters in filenames. AI-Assisted-by: GPT-5.2 (Codex CLI) Signed-off-by: Lucas Holt <luke@foolishgames.com>
1 parent aab55c2 commit 5a80810

4 files changed

Lines changed: 115 additions & 23 deletions

File tree

usr.bin/progress/Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# $MidnightBSD: src/usr.bin/progress/Makefile,v 1.2 2012/12/02 06:26:32 laffer1 Exp $
22
# $NetBSD: Makefile,v 1.2 2003/01/22 02:56:30 lukem Exp $
33

4+
.include <src.opts.mk>
45
.include <bsd.own.mk>
56

67
PROG= progress
@@ -13,4 +14,9 @@ CFLAGS+= -I${.CURDIR}/../ftp
1314

1415
.PATH: ${.CURDIR}/../../contrib/tnftp/src
1516

17+
.if ${MK_TESTS} != "no"
18+
HAS_TESTS=
19+
SUBDIR+= tests
20+
.endif
21+
1622
.include <bsd.prog.mk>

usr.bin/progress/progress.c

Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ __MBSDID("$MidnightBSD: src/usr.bin/progress/progress.c,v 1.4 2012/12/31 17:14:3
6060

6161
static void broken_pipe(int unused);
6262
static void usage(void);
63+
static off_t gzip_uncompressed_size(const char *);
6364

6465
long long
6566
strsuftoll(const char *desc, const char *val,
@@ -157,26 +158,7 @@ main(int argc, char *argv[])
157158

158159
/* gzip -l the file if we have the name and -z is given */
159160
if (zflag && !lflag && infile != NULL) {
160-
FILE *gzipsizepipe;
161-
char buf[256], *cp, *cmd;
162-
163-
/*
164-
* Read second word of last line of gzip -l output. Looks like:
165-
* % gzip -l ../etc.tgz
166-
* compressed uncompressed ratio uncompressed_name
167-
* 119737 696320 82.8% ../etc.tar
168-
*/
169-
170-
asprintf(&cmd, "gzip -l %s", infile);
171-
if ((gzipsizepipe = popen(cmd, "r")) == NULL)
172-
err(1, "reading compressed file length");
173-
for (; fgets(buf, 256, gzipsizepipe) != NULL;)
174-
continue;
175-
strtoimax(buf, &cp, 10);
176-
filesize = strtoimax(cp, NULL, 10);
177-
if (pclose(gzipsizepipe) < 0)
178-
err(1, "closing compressed file length pipe");
179-
free(cmd);
161+
filesize = gzip_uncompressed_size(infile);
180162
}
181163
/* Pipe input through gzip -dc if -z is given */
182164
if (zflag) {
@@ -250,16 +232,18 @@ main(int argc, char *argv[])
250232
cmdstat = 0;
251233
while (pid || gzippid) {
252234
deadpid = wait(&ws);
235+
if (deadpid == -1 && errno == EINTR)
236+
continue;
237+
if (deadpid == -1)
238+
break;
239+
253240
/*
254241
* We need to exit with an error if the command (or gzip)
255242
* exited abnormally.
256243
* Unfortunately we can't generate a true 'exited by signal'
257244
* error without sending the signal to ourselves :-(
258245
*/
259246
ws = WIFSIGNALED(ws) ? WTERMSIG(ws) : WEXITSTATUS(ws);
260-
261-
if (deadpid != -1 && errno == EINTR)
262-
continue;
263247
if (deadpid == pid) {
264248
pid = 0;
265249
cmdstat = ws;
@@ -280,3 +264,74 @@ main(int argc, char *argv[])
280264

281265
exit(cmdstat ? cmdstat : gzipstat);
282266
}
267+
268+
static off_t
269+
gzip_uncompressed_size(const char *path)
270+
{
271+
char buf[256];
272+
char lastline[256];
273+
char *cp, *endp;
274+
ssize_t nr;
275+
pid_t pid;
276+
int status;
277+
int pfd[2];
278+
off_t uncompressed;
279+
280+
if (path == NULL)
281+
errx(1, "gzip length: missing path");
282+
if (pipe(pfd) < 0)
283+
err(1, "pipe");
284+
pid = fork();
285+
if (pid < 0)
286+
err(1, "fork");
287+
if (pid == 0) {
288+
(void)dup2(pfd[1], STDOUT_FILENO);
289+
close(pfd[0]);
290+
close(pfd[1]);
291+
execlp("gzip", "gzip", "-l", "--", path, (char *)NULL);
292+
_exit(127);
293+
}
294+
295+
close(pfd[1]);
296+
lastline[0] = '\0';
297+
/*
298+
* Read all output, keep the last line.
299+
* gzip -l output contains a header; the last line has numbers.
300+
*/
301+
while ((nr = read(pfd[0], buf, sizeof(buf) - 1)) > 0) {
302+
char *line, *nl;
303+
304+
buf[nr] = '\0';
305+
line = buf;
306+
while ((nl = strchr(line, '\n')) != NULL) {
307+
size_t len = (size_t)(nl - line);
308+
if (len >= sizeof(lastline))
309+
len = sizeof(lastline) - 1;
310+
memcpy(lastline, line, len);
311+
lastline[len] = '\0';
312+
line = nl + 1;
313+
}
314+
}
315+
close(pfd[0]);
316+
317+
if (waitpid(pid, &status, 0) < 0)
318+
err(1, "waitpid");
319+
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0)
320+
errx(1, "gzip -l failed for %s", path);
321+
322+
/*
323+
* Parse: <compressed> <uncompressed> ...
324+
*/
325+
errno = 0;
326+
(void)strtoimax(lastline, &cp, 10);
327+
if (errno != 0 || cp == lastline)
328+
errx(1, "unexpected gzip -l output for %s", path);
329+
while (*cp == ' ' || *cp == '\t')
330+
cp++;
331+
errno = 0;
332+
uncompressed = (off_t)strtoimax(cp, &endp, 10);
333+
if (errno != 0 || endp == cp)
334+
errx(1, "unexpected gzip -l output for %s", path);
335+
336+
return uncompressed;
337+
}

usr.bin/progress/tests/Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
ATF_TESTS_SH= progress_test
3+
4+
.include <bsd.test.mk>
5+
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#
2+
# Regression tests for progress(1) security/robustness.
3+
#
4+
5+
atf_test_case zflag_no_shell_injection
6+
zflag_no_shell_injection_body()
7+
{
8+
PROGRESS_BIN="$(ls /usr/obj/usr/src/*/usr.bin/progress/progress 2>/dev/null | head -n 1)"
9+
if [ -z "${PROGRESS_BIN}" ] || [ ! -x "${PROGRESS_BIN}" ]; then
10+
atf_skip "progress binary not found"
11+
fi
12+
atf_require_prog gzip
13+
14+
# Create a gzipped file whose name contains shell metacharacters.
15+
echo "hello" > "in;echoINJECTED"
16+
gzip -c -- "in;echoINJECTED" > "in;echoINJECTED.gz"
17+
18+
# If progress used popen("gzip -l %s"), this would run "echoINJECTED".
19+
atf_check -s exit:0 -o not-empty -e not-match:"INJECTED" \
20+
"${PROGRESS_BIN}" -z -f "in;echoINJECTED.gz" cat >/dev/null
21+
}
22+
23+
atf_init_test_cases()
24+
{
25+
atf_add_test_case zflag_no_shell_injection
26+
}

0 commit comments

Comments
 (0)