1/* Capture output from a subprocess.
2 Copyright (C) 2017-2024 Free Software Foundation, Inc.
3 This file is part of the GNU C Library.
4
5 The GNU C Library is free software; you can redistribute it and/or
6 modify it under the terms of the GNU Lesser General Public
7 License as published by the Free Software Foundation; either
8 version 2.1 of the License, or (at your option) any later version.
9
10 The GNU C Library is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 Lesser General Public License for more details.
14
15 You should have received a copy of the GNU Lesser General Public
16 License along with the GNU C Library; if not, see
17 <https://www.gnu.org/licenses/>. */
18
19#include <support/subprocess.h>
20#include <support/capture_subprocess.h>
21
22#include <errno.h>
23#include <fcntl.h>
24#include <grp.h>
25#include <scratch_buffer.h>
26#include <stdio_ext.h>
27#include <stdlib.h>
28#include <string.h>
29#include <support/check.h>
30#include <support/xunistd.h>
31#include <support/xsocket.h>
32#include <support/xspawn.h>
33#include <support/support.h>
34#include <support/temp_file.h>
35#include <support/test-driver.h>
36
37static void
38transfer (const char *what, struct pollfd *pfd, struct xmemstream *stream)
39{
40 if (pfd->revents != 0)
41 {
42 char buf[1024];
43 ssize_t ret = TEMP_FAILURE_RETRY (read (pfd->fd, buf, sizeof (buf)));
44 if (ret < 0)
45 {
46 support_record_failure ();
47 printf (format: "error: reading from subprocess %s: %m\n", what);
48 pfd->events = 0;
49 pfd->revents = 0;
50 }
51 else if (ret == 0)
52 {
53 /* EOF reached. Stop listening. */
54 pfd->events = 0;
55 pfd->revents = 0;
56 }
57 else
58 /* Store the data just read. */
59 TEST_VERIFY (fwrite (buf, ret, 1, stream->out) == 1);
60 }
61}
62
63static void
64support_capture_poll (struct support_capture_subprocess *result,
65 struct support_subprocess *proc)
66{
67 struct pollfd fds[2] =
68 {
69 { .fd = proc->stdout_pipe[0], .events = POLLIN },
70 { .fd = proc->stderr_pipe[0], .events = POLLIN },
71 };
72
73 do
74 {
75 xpoll (fds, 2, -1);
76 transfer (what: "stdout", pfd: &fds[0], stream: &result->out);
77 transfer (what: "stderr", pfd: &fds[1], stream: &result->err);
78 }
79 while (fds[0].events != 0 || fds[1].events != 0);
80
81 xfclose_memstream (stream: &result->out);
82 xfclose_memstream (stream: &result->err);
83
84 result->status = support_process_wait (proc);
85}
86
87struct support_capture_subprocess
88support_capture_subprocess (void (*callback) (void *), void *closure)
89{
90 struct support_capture_subprocess result;
91 xopen_memstream (stream: &result.out);
92 xopen_memstream (stream: &result.err);
93
94 struct support_subprocess proc = support_subprocess (callback, closure);
95
96 support_capture_poll (result: &result, proc: &proc);
97 return result;
98}
99
100struct support_capture_subprocess
101support_capture_subprogram (const char *file, char *const argv[])
102{
103 struct support_capture_subprocess result;
104 xopen_memstream (stream: &result.out);
105 xopen_memstream (stream: &result.err);
106
107 struct support_subprocess proc = support_subprogram (file, argv);
108
109 support_capture_poll (result: &result, proc: &proc);
110 return result;
111}
112
113/* Copies the executable into a restricted directory, so that we can
114 safely make it SGID with the TARGET group ID. Then runs the
115 executable. */
116static void
117copy_and_spawn_sgid (const char *child_id, gid_t gid)
118{
119 char *dirname = support_create_temp_directory (base: "tst-glibc-sgid-");
120 char *execname = xasprintf (format: "%s/bin", dirname);
121 add_temp_file (name: execname);
122
123 if (access (name: "/proc/self/exe", R_OK) != 0)
124 FAIL_UNSUPPORTED ("unsupported: Cannot read binary from procfs\n");
125
126 support_copy_file (from: "/proc/self/exe", to: execname);
127
128 if (chown (file: execname, owner: getuid (), group: gid) != 0)
129 FAIL_UNSUPPORTED ("cannot change group of \"%s\" to %jd: %m",
130 execname, (intmax_t) gid);
131
132 if (chmod (file: execname, mode: 02750) != 0)
133 FAIL_UNSUPPORTED ("cannot make \"%s\" SGID: %m ", execname);
134
135 /* We have the binary, now spawn the subprocess. Avoid using
136 support_subprogram because we only want the program exit status, not the
137 contents. */
138
139 char * const args[] = {execname, (char *) child_id, NULL};
140 int status = support_subprogram_wait (file: args[0], argv: args);
141
142 free (ptr: execname);
143 free (ptr: dirname);
144
145 if (WIFEXITED (status))
146 {
147 if (WEXITSTATUS (status) == 0)
148 return;
149 else
150 exit (WEXITSTATUS (status));
151 }
152 else
153 FAIL_EXIT1 ("subprogram failed with status %d", status);
154}
155
156/* Returns true if a group with NAME has been found, and writes its
157 GID to *TARGET. */
158static bool
159find_sgid_group (gid_t *target, const char *name)
160{
161 /* Do not use getgrname_r because it does not work in statically
162 linked binaries if the system libc is different. */
163 FILE *fp = fopen (filename: "/etc/group", modes: "rce");
164 if (fp == NULL)
165 return false;
166 __fsetlocking (fp, FSETLOCKING_BYCALLER);
167
168 bool ok = false;
169 struct scratch_buffer buf;
170 scratch_buffer_init (buffer: &buf);
171 while (true)
172 {
173 struct group grp;
174 struct group *result = NULL;
175 int status = fgetgrent_r (stream: fp, resultbuf: &grp, buffer: buf.data, buflen: buf.length, result: &result);
176 if (status == 0 && result != NULL)
177 {
178 if (strcmp (s1: result->gr_name, s2: name) == 0)
179 {
180 *target = result->gr_gid;
181 ok = true;
182 break;
183 }
184 }
185 else if (errno != ERANGE)
186 break;
187 else if (!scratch_buffer_grow (buffer: &buf))
188 break;
189 }
190 scratch_buffer_free (buffer: &buf);
191 fclose (stream: fp);
192 return ok;
193}
194
195void
196support_capture_subprogram_self_sgid (const char *child_id)
197{
198 const int count = 64;
199 gid_t groups[count];
200
201 /* Get a GID which is not our current GID, but is present in the
202 supplementary group list. */
203 int ret = getgroups (size: count, list: groups);
204 if (ret < 0)
205 FAIL_UNSUPPORTED("Could not get group list for user %jd\n",
206 (intmax_t) getuid ());
207
208 gid_t current = getgid ();
209 gid_t target = current;
210 for (int i = 0; i < ret; ++i)
211 {
212 if (groups[i] != current)
213 {
214 target = groups[i];
215 break;
216 }
217 }
218
219 if (target == current)
220 {
221 /* If running as root, try to find a harmless group for SGID. */
222 if (getuid () != 0
223 || (!find_sgid_group (target: &target, name: "nogroup")
224 && !find_sgid_group (target: &target, name: "bin")
225 && !find_sgid_group (target: &target, name: "daemon")))
226 FAIL_UNSUPPORTED("Could not find a suitable GID for user %jd\n",
227 (intmax_t) getuid ());
228 }
229
230 copy_and_spawn_sgid (child_id, gid: target);
231}
232
233void
234support_capture_subprocess_free (struct support_capture_subprocess *p)
235{
236 free (ptr: p->out.buffer);
237 free (ptr: p->err.buffer);
238}
239

source code of glibc/support/support_capture_subprocess.c