1 | /* |
2 | This file is part of the KDE project, module kdesu. |
3 | SPDX-FileCopyrightText: 1999, 2000 Geert Jansen <jansen@kde.org> |
4 | |
5 | This file contains code from TEShell.C of the KDE konsole. |
6 | SPDX-FileCopyrightText: 1997, 1998 Lars Doelle <lars.doelle@on-line.de> |
7 | |
8 | SPDX-License-Identifier: GPL-2.0-only |
9 | |
10 | process.cpp: Functionality to build a front end to password asking terminal programs. |
11 | */ |
12 | |
13 | #include "ptyprocess.h" |
14 | #include "kcookie_p.h" |
15 | #include "ptyprocess_p.h" |
16 | |
17 | #include <config-kdesu.h> |
18 | #include <ksu_debug.h> |
19 | |
20 | #include <cerrno> |
21 | #include <fcntl.h> |
22 | #include <signal.h> |
23 | #include <stdlib.h> |
24 | #include <termios.h> |
25 | #include <unistd.h> |
26 | |
27 | #include <sys/resource.h> |
28 | #include <sys/stat.h> |
29 | #include <sys/time.h> |
30 | #include <sys/wait.h> |
31 | |
32 | #if HAVE_SYS_SELECT_H |
33 | #include <sys/select.h> // Needed on some systems. |
34 | #endif |
35 | |
36 | #include <QFile> |
37 | #include <QStandardPaths> |
38 | |
39 | #include <KConfigGroup> |
40 | #include <KSharedConfig> |
41 | |
42 | extern int kdesuDebugArea(); |
43 | |
44 | namespace KDESu |
45 | { |
46 | using namespace KDESuPrivate; |
47 | |
48 | /* |
49 | ** Wait for @p ms milliseconds |
50 | ** @param fd file descriptor |
51 | ** @param ms time to wait in milliseconds |
52 | ** @return |
53 | */ |
54 | int PtyProcess::waitMS(int fd, int ms) |
55 | { |
56 | struct timeval tv; |
57 | tv.tv_sec = 0; |
58 | tv.tv_usec = 1000 * ms; |
59 | |
60 | fd_set fds; |
61 | FD_ZERO(&fds); |
62 | FD_SET(fd, &fds); |
63 | return select(nfds: fd + 1, readfds: &fds, writefds: nullptr, exceptfds: nullptr, timeout: &tv); |
64 | } |
65 | |
66 | // XXX this function is nonsense: |
67 | // - for our child, we could use waitpid(). |
68 | // - the configurability at this place it *complete* braindamage |
69 | /* |
70 | ** Basic check for the existence of @p pid. |
71 | ** Returns true iff @p pid is an extant process. |
72 | */ |
73 | bool PtyProcess::checkPid(pid_t pid) |
74 | { |
75 | KSharedConfig::Ptr config = KSharedConfig::openConfig(); |
76 | KConfigGroup cg(config, QStringLiteral("super-user-command" )); |
77 | QString superUserCommand = cg.readEntry(key: "super-user-command" , aDefault: "sudo" ); |
78 | // sudo does not accept signals from user so we except it |
79 | if (superUserCommand == QLatin1String("sudo" )) { |
80 | return true; |
81 | } else { |
82 | return kill(pid: pid, sig: 0) == 0; |
83 | } |
84 | } |
85 | |
86 | /* |
87 | ** Check process exit status for process @p pid. |
88 | ** On error (no child, no exit), return Error (-1). |
89 | ** If child @p pid has exited, return its exit status, |
90 | ** (which may be zero). |
91 | ** If child @p has not exited, return NotExited (-2). |
92 | */ |
93 | int PtyProcess::checkPidExited(pid_t pid) |
94 | { |
95 | int state; |
96 | int ret; |
97 | ret = waitpid(pid: pid, stat_loc: &state, WNOHANG); |
98 | |
99 | if (ret < 0) { |
100 | qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " |
101 | << "waitpid():" << strerror(errno); |
102 | return Error; |
103 | } |
104 | if (ret == pid) { |
105 | if (WIFEXITED(state)) { |
106 | return WEXITSTATUS(state); |
107 | } |
108 | |
109 | return Killed; |
110 | } |
111 | |
112 | return NotExited; |
113 | } |
114 | |
115 | PtyProcess::PtyProcess() |
116 | : PtyProcess(*new PtyProcessPrivate) |
117 | { |
118 | } |
119 | |
120 | PtyProcess::PtyProcess(PtyProcessPrivate &dd) |
121 | : d_ptr(&dd) |
122 | { |
123 | m_terminal = false; |
124 | m_erase = false; |
125 | } |
126 | |
127 | PtyProcess::~PtyProcess() = default; |
128 | |
129 | int PtyProcess::init() |
130 | { |
131 | Q_D(PtyProcess); |
132 | |
133 | delete d->pty; |
134 | d->pty = new KPty(); |
135 | if (!d->pty->open()) { |
136 | qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " |
137 | << "Failed to open PTY." ; |
138 | return -1; |
139 | } |
140 | if (!d->wantLocalEcho) { |
141 | enableLocalEcho(enable: false); |
142 | } |
143 | d->inputBuffer.resize(size: 0); |
144 | return 0; |
145 | } |
146 | |
147 | /** Set additional environment variables. */ |
148 | void PtyProcess::setEnvironment(const QList<QByteArray> &env) |
149 | { |
150 | Q_D(PtyProcess); |
151 | |
152 | d->env = env; |
153 | } |
154 | |
155 | int PtyProcess::fd() const |
156 | { |
157 | Q_D(const PtyProcess); |
158 | |
159 | return d->pty ? d->pty->masterFd() : -1; |
160 | } |
161 | |
162 | int PtyProcess::pid() const |
163 | { |
164 | return m_pid; |
165 | } |
166 | |
167 | /** Returns the additional environment variables set by setEnvironment() */ |
168 | QList<QByteArray> PtyProcess::environment() const |
169 | { |
170 | Q_D(const PtyProcess); |
171 | |
172 | return d->env; |
173 | } |
174 | |
175 | QByteArray PtyProcess::readAll(bool block) |
176 | { |
177 | Q_D(PtyProcess); |
178 | |
179 | QByteArray ret; |
180 | if (!d->inputBuffer.isEmpty()) { |
181 | // if there is still something in the buffer, we need not block. |
182 | // we should still try to read any further output, from the fd, though. |
183 | block = false; |
184 | ret = d->inputBuffer; |
185 | d->inputBuffer.resize(size: 0); |
186 | } |
187 | |
188 | int flags = fcntl(fd: fd(), F_GETFL); |
189 | if (flags < 0) { |
190 | qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " |
191 | << "fcntl(F_GETFL):" << strerror(errno); |
192 | return ret; |
193 | } |
194 | int oflags = flags; |
195 | if (block) { |
196 | flags &= ~O_NONBLOCK; |
197 | } else { |
198 | flags |= O_NONBLOCK; |
199 | } |
200 | |
201 | if ((flags != oflags) && (fcntl(fd: fd(), F_SETFL, flags) < 0)) { |
202 | // We get an error here when the child process has closed |
203 | // the file descriptor already. |
204 | return ret; |
205 | } |
206 | |
207 | while (1) { |
208 | ret.reserve(asize: ret.size() + 0x8000); |
209 | int nbytes = read(fd: fd(), buf: ret.data() + ret.size(), nbytes: 0x8000); |
210 | if (nbytes == -1) { |
211 | if (errno == EINTR) { |
212 | continue; |
213 | } else { |
214 | break; |
215 | } |
216 | } |
217 | if (nbytes == 0) { |
218 | break; // nothing available / eof |
219 | } |
220 | |
221 | ret.resize(size: ret.size() + nbytes); |
222 | break; |
223 | } |
224 | |
225 | return ret; |
226 | } |
227 | |
228 | QByteArray PtyProcess::readLine(bool block) |
229 | { |
230 | Q_D(PtyProcess); |
231 | |
232 | d->inputBuffer = readAll(block); |
233 | |
234 | int pos; |
235 | QByteArray ret; |
236 | if (!d->inputBuffer.isEmpty()) { |
237 | pos = d->inputBuffer.indexOf(c: '\n'); |
238 | if (pos == -1) { |
239 | // NOTE: this means we return something even if there in no full line! |
240 | ret = d->inputBuffer; |
241 | d->inputBuffer.resize(size: 0); |
242 | } else { |
243 | ret = d->inputBuffer.left(len: pos); |
244 | d->inputBuffer.remove(index: 0, len: pos + 1); |
245 | } |
246 | } |
247 | |
248 | return ret; |
249 | } |
250 | |
251 | void PtyProcess::writeLine(const QByteArray &line, bool addnl) |
252 | { |
253 | if (!line.isEmpty()) { |
254 | write(fd: fd(), buf: line.constData(), n: line.length()); |
255 | } |
256 | if (addnl) { |
257 | write(fd: fd(), buf: "\n" , n: 1); |
258 | } |
259 | } |
260 | |
261 | void PtyProcess::unreadLine(const QByteArray &line, bool addnl) |
262 | { |
263 | Q_D(PtyProcess); |
264 | |
265 | QByteArray tmp = line; |
266 | if (addnl) { |
267 | tmp += '\n'; |
268 | } |
269 | if (!tmp.isEmpty()) { |
270 | d->inputBuffer.prepend(a: tmp); |
271 | } |
272 | } |
273 | |
274 | void PtyProcess::setExitString(const QByteArray &exit) |
275 | { |
276 | m_exitString = exit; |
277 | } |
278 | |
279 | /* |
280 | * Fork and execute the command. This returns in the parent. |
281 | */ |
282 | int PtyProcess::exec(const QByteArray &command, const QList<QByteArray> &args) |
283 | { |
284 | Q_D(PtyProcess); |
285 | |
286 | int i; |
287 | |
288 | if (init() < 0) { |
289 | return -1; |
290 | } |
291 | |
292 | if ((m_pid = fork()) == -1) { |
293 | qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " |
294 | << "fork():" << strerror(errno); |
295 | return -1; |
296 | } |
297 | |
298 | // Parent |
299 | if (m_pid) { |
300 | d->pty->closeSlave(); |
301 | return 0; |
302 | } |
303 | |
304 | // Child |
305 | if (setupTTY() < 0) { |
306 | _exit(status: 1); |
307 | } |
308 | |
309 | for (const QByteArray &var : std::as_const(t&: d->env)) { |
310 | putenv(string: const_cast<char *>(var.constData())); |
311 | } |
312 | unsetenv(name: "KDE_FULL_SESSION" ); |
313 | // for : Qt: Session management error |
314 | unsetenv(name: "SESSION_MANAGER" ); |
315 | // QMutex::lock , deadlocks without that. |
316 | // <thiago> you cannot connect to the user's session bus from another UID |
317 | unsetenv(name: "DBUS_SESSION_BUS_ADDRESS" ); |
318 | |
319 | // set temporarily LC_ALL to C, for su (to be able to parse "Password:") |
320 | const QByteArray old_lc_all = qgetenv(varName: "LC_ALL" ); |
321 | if (!old_lc_all.isEmpty()) { |
322 | qputenv(varName: "KDESU_LC_ALL" , value: old_lc_all); |
323 | } else { |
324 | unsetenv(name: "KDESU_LC_ALL" ); |
325 | } |
326 | qputenv(varName: "LC_ALL" , value: "C" ); |
327 | |
328 | // From now on, terminal output goes through the tty. |
329 | |
330 | QByteArray path; |
331 | if (command.contains(c: '/')) { |
332 | path = command; |
333 | } else { |
334 | QString file = QStandardPaths::findExecutable(executableName: QFile::decodeName(localFileName: command)); |
335 | if (file.isEmpty()) { |
336 | qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " << command << "not found." ; |
337 | _exit(status: 1); |
338 | } |
339 | path = QFile::encodeName(fileName: file); |
340 | } |
341 | |
342 | const char **argp = (const char **)malloc(size: (args.count() + 2) * sizeof(char *)); |
343 | |
344 | i = 0; |
345 | argp[i++] = path.constData(); |
346 | for (const QByteArray &arg : args) { |
347 | argp[i++] = arg.constData(); |
348 | } |
349 | |
350 | argp[i] = nullptr; |
351 | |
352 | execv(path: path.constData(), argv: const_cast<char **>(argp)); |
353 | qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " |
354 | << "execv(" << path << "):" << strerror(errno); |
355 | _exit(status: 1); |
356 | return -1; // Shut up compiler. Never reached. |
357 | } |
358 | |
359 | /* |
360 | * Wait until the terminal is set into no echo mode. At least one su |
361 | * (RH6 w/ Linux-PAM patches) sets noecho mode AFTER writing the Password: |
362 | * prompt, using TCSAFLUSH. This flushes the terminal I/O queues, possibly |
363 | * taking the password with it. So we wait until no echo mode is set |
364 | * before writing the password. |
365 | * Note that this is done on the slave fd. While Linux allows tcgetattr() on |
366 | * the master side, Solaris doesn't. |
367 | */ |
368 | int PtyProcess::waitSlave() |
369 | { |
370 | Q_D(PtyProcess); |
371 | |
372 | struct termios tio; |
373 | while (1) { |
374 | if (!checkPid(pid: m_pid)) { |
375 | qCCritical(KSU_LOG) << "process has exited while waiting for password." ; |
376 | return -1; |
377 | } |
378 | if (!d->pty->tcGetAttr(ttmode: &tio)) { |
379 | qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " |
380 | << "tcgetattr():" << strerror(errno); |
381 | return -1; |
382 | } |
383 | if (tio.c_lflag & ECHO) { |
384 | // qDebug() << "[" << __FILE__ << ":" << __LINE__ << "] " << "Echo mode still on."; |
385 | usleep(useconds: 10000); |
386 | continue; |
387 | } |
388 | break; |
389 | } |
390 | return 0; |
391 | } |
392 | |
393 | int PtyProcess::enableLocalEcho(bool enable) |
394 | { |
395 | Q_D(PtyProcess); |
396 | |
397 | d->wantLocalEcho = enable; |
398 | if (!d->pty) { |
399 | // Apply it on init |
400 | return 0; |
401 | } |
402 | |
403 | return d->pty->setEcho(enable) ? 0 : -1; |
404 | } |
405 | |
406 | void PtyProcess::setTerminal(bool terminal) |
407 | { |
408 | m_terminal = terminal; |
409 | } |
410 | |
411 | void PtyProcess::setErase(bool erase) |
412 | { |
413 | m_erase = erase; |
414 | } |
415 | |
416 | /* |
417 | * Copy output to stdout until the child process exits, or a line of output |
418 | * matches `m_exitString'. |
419 | * We have to use waitpid() to test for exit. Merely waiting for EOF on the |
420 | * pty does not work, because the target process may have children still |
421 | * attached to the terminal. |
422 | */ |
423 | int PtyProcess::waitForChild() |
424 | { |
425 | fd_set fds; |
426 | FD_ZERO(&fds); |
427 | QByteArray remainder; |
428 | |
429 | while (1) { |
430 | FD_SET(fd(), &fds); |
431 | |
432 | // specify timeout to make sure select() does not block, even if the |
433 | // process is dead / non-responsive. It does not matter if we abort too |
434 | // early. In that case 0 is returned, and we'll try again in the next |
435 | // iteration. (As long as we don't consistently time out in each iteration) |
436 | timeval timeout; |
437 | timeout.tv_sec = 0; |
438 | timeout.tv_usec = 100000; |
439 | int ret = select(nfds: fd() + 1, readfds: &fds, writefds: nullptr, exceptfds: nullptr, timeout: &timeout); |
440 | if (ret == -1) { |
441 | if (errno != EINTR) { |
442 | qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " |
443 | << "select():" << strerror(errno); |
444 | return -1; |
445 | } |
446 | ret = 0; |
447 | } |
448 | |
449 | if (ret) { |
450 | for (;;) { |
451 | QByteArray output = readAll(block: false); |
452 | if (output.isEmpty()) { |
453 | break; |
454 | } |
455 | if (m_terminal) { |
456 | fwrite(ptr: output.constData(), size: output.size(), n: 1, stdout); |
457 | fflush(stdout); |
458 | } |
459 | if (!m_exitString.isEmpty()) { |
460 | // match exit string only at line starts |
461 | remainder += output; |
462 | while (remainder.length() >= m_exitString.length()) { |
463 | if (remainder.startsWith(bv: m_exitString)) { |
464 | kill(pid: m_pid, SIGTERM); |
465 | remainder.remove(index: 0, len: m_exitString.length()); |
466 | } |
467 | int off = remainder.indexOf(c: '\n'); |
468 | if (off < 0) { |
469 | break; |
470 | } |
471 | remainder.remove(index: 0, len: off + 1); |
472 | } |
473 | } |
474 | } |
475 | } |
476 | |
477 | ret = checkPidExited(pid: m_pid); |
478 | if (ret == Error) { |
479 | if (errno == ECHILD) { |
480 | return 0; |
481 | } else { |
482 | return 1; |
483 | } |
484 | } else if (ret == Killed) { |
485 | return 0; |
486 | } else if (ret == NotExited) { |
487 | continue; // keep checking |
488 | } else { |
489 | return ret; |
490 | } |
491 | } |
492 | } |
493 | |
494 | /* |
495 | * SetupTTY: Creates a new session. The filedescriptor "fd" should be |
496 | * connected to the tty. It is closed after the tty is reopened to make it |
497 | * our controlling terminal. This way the tty is always opened at least once |
498 | * so we'll never get EIO when reading from it. |
499 | */ |
500 | int PtyProcess::setupTTY() |
501 | { |
502 | Q_D(PtyProcess); |
503 | |
504 | // Reset signal handlers |
505 | for (int sig = 1; sig < NSIG; sig++) { |
506 | signal(sig: sig, SIG_DFL); |
507 | } |
508 | signal(SIGHUP, SIG_IGN); |
509 | |
510 | d->pty->setCTty(); |
511 | |
512 | // Connect stdin, stdout and stderr |
513 | int slave = d->pty->slaveFd(); |
514 | dup2(fd: slave, fd2: 0); |
515 | dup2(fd: slave, fd2: 1); |
516 | dup2(fd: slave, fd2: 2); |
517 | |
518 | // Close all file handles |
519 | // XXX this caused problems in KProcess - not sure why anymore. -- ??? |
520 | // Because it will close the start notification pipe. -- ossi |
521 | struct rlimit rlp; |
522 | getrlimit(RLIMIT_NOFILE, rlimits: &rlp); |
523 | for (int i = 3; i < (int)rlp.rlim_cur; i++) { |
524 | close(fd: i); |
525 | } |
526 | |
527 | // Disable OPOST processing. Otherwise, '\n' are (on Linux at least) |
528 | // translated to '\r\n'. |
529 | struct ::termios tio; |
530 | if (tcgetattr(fd: 0, termios_p: &tio) < 0) { |
531 | qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " |
532 | << "tcgetattr():" << strerror(errno); |
533 | return -1; |
534 | } |
535 | tio.c_oflag &= ~OPOST; |
536 | if (tcsetattr(fd: 0, TCSANOW, termios_p: &tio) < 0) { |
537 | qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " |
538 | << "tcsetattr():" << strerror(errno); |
539 | return -1; |
540 | } |
541 | |
542 | return 0; |
543 | } |
544 | |
545 | void PtyProcess::virtual_hook(int id, void *data) |
546 | { |
547 | Q_UNUSED(id); |
548 | Q_UNUSED(data); |
549 | /*BASE::virtual_hook( id, data );*/ |
550 | } |
551 | |
552 | } // namespace KDESu |
553 | |