File indexing completed on 2025-02-02 14:11:33
0001 /* 0002 This file is part of the KDE project, module kdesu. 0003 SPDX-FileCopyrightText: 1999, 2000 Geert Jansen <jansen@kde.org> 0004 0005 This file contains code from TEShell.C of the KDE konsole. 0006 SPDX-FileCopyrightText: 1997, 1998 Lars Doelle <lars.doelle@on-line.de> 0007 0008 SPDX-License-Identifier: GPL-2.0-only 0009 0010 process.cpp: Functionality to build a front end to password asking terminal programs. 0011 */ 0012 0013 #include "ptyprocess.h" 0014 #include "kcookie_p.h" 0015 #include "ptyprocess_p.h" 0016 0017 #include <config-kdesu.h> 0018 #include <ksu_debug.h> 0019 0020 #include <cerrno> 0021 #include <fcntl.h> 0022 #include <signal.h> 0023 #include <stdlib.h> 0024 #include <termios.h> 0025 #include <unistd.h> 0026 0027 #include <sys/resource.h> 0028 #include <sys/stat.h> 0029 #include <sys/time.h> 0030 #include <sys/wait.h> 0031 0032 #if HAVE_SYS_SELECT_H 0033 #include <sys/select.h> // Needed on some systems. 0034 #endif 0035 0036 #include <QFile> 0037 #include <QStandardPaths> 0038 0039 #include <KConfigGroup> 0040 #include <KSharedConfig> 0041 0042 extern int kdesuDebugArea(); 0043 0044 namespace KDESu 0045 { 0046 using namespace KDESuPrivate; 0047 0048 /* 0049 ** Wait for @p ms milliseconds 0050 ** @param fd file descriptor 0051 ** @param ms time to wait in milliseconds 0052 ** @return 0053 */ 0054 int PtyProcess::waitMS(int fd, int ms) 0055 { 0056 struct timeval tv; 0057 tv.tv_sec = 0; 0058 tv.tv_usec = 1000 * ms; 0059 0060 fd_set fds; 0061 FD_ZERO(&fds); 0062 FD_SET(fd, &fds); 0063 return select(fd + 1, &fds, nullptr, nullptr, &tv); 0064 } 0065 0066 // XXX this function is nonsense: 0067 // - for our child, we could use waitpid(). 0068 // - the configurability at this place it *complete* braindamage 0069 /* 0070 ** Basic check for the existence of @p pid. 0071 ** Returns true iff @p pid is an extant process. 0072 */ 0073 bool PtyProcess::checkPid(pid_t pid) 0074 { 0075 KSharedConfig::Ptr config = KSharedConfig::openConfig(); 0076 KConfigGroup cg(config, "super-user-command"); 0077 QString superUserCommand = cg.readEntry("super-user-command", "sudo"); 0078 // sudo does not accept signals from user so we except it 0079 if (superUserCommand == QLatin1String("sudo")) { 0080 return true; 0081 } else { 0082 return kill(pid, 0) == 0; 0083 } 0084 } 0085 0086 /* 0087 ** Check process exit status for process @p pid. 0088 ** On error (no child, no exit), return Error (-1). 0089 ** If child @p pid has exited, return its exit status, 0090 ** (which may be zero). 0091 ** If child @p has not exited, return NotExited (-2). 0092 */ 0093 int PtyProcess::checkPidExited(pid_t pid) 0094 { 0095 int state; 0096 int ret; 0097 ret = waitpid(pid, &state, WNOHANG); 0098 0099 if (ret < 0) { 0100 qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " 0101 << "waitpid():" << strerror(errno); 0102 return Error; 0103 } 0104 if (ret == pid) { 0105 if (WIFEXITED(state)) { 0106 return WEXITSTATUS(state); 0107 } 0108 0109 return Killed; 0110 } 0111 0112 return NotExited; 0113 } 0114 0115 PtyProcess::PtyProcess() 0116 : PtyProcess(*new PtyProcessPrivate) 0117 { 0118 } 0119 0120 PtyProcess::PtyProcess(PtyProcessPrivate &dd) 0121 : d(&dd) 0122 { 0123 m_terminal = false; 0124 m_erase = false; 0125 } 0126 0127 PtyProcess::~PtyProcess() = default; 0128 0129 int PtyProcess::init() 0130 { 0131 delete d->pty; 0132 d->pty = new KPty(); 0133 if (!d->pty->open()) { 0134 qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " 0135 << "Failed to open PTY."; 0136 return -1; 0137 } 0138 if (!d->wantLocalEcho) { 0139 enableLocalEcho(false); 0140 } 0141 d->inputBuffer.resize(0); 0142 return 0; 0143 } 0144 0145 /** Set additional environment variables. */ 0146 void PtyProcess::setEnvironment(const QList<QByteArray> &env) 0147 { 0148 d->env = env; 0149 } 0150 0151 int PtyProcess::fd() const 0152 { 0153 return d->pty ? d->pty->masterFd() : -1; 0154 } 0155 0156 int PtyProcess::pid() const 0157 { 0158 return m_pid; 0159 } 0160 0161 /** Returns the additional environment variables set by setEnvironment() */ 0162 QList<QByteArray> PtyProcess::environment() const 0163 { 0164 return d->env; 0165 } 0166 0167 QByteArray PtyProcess::readAll(bool block) 0168 { 0169 QByteArray ret; 0170 if (!d->inputBuffer.isEmpty()) { 0171 // if there is still something in the buffer, we need not block. 0172 // we should still try to read any further output, from the fd, though. 0173 block = false; 0174 ret = d->inputBuffer; 0175 d->inputBuffer.resize(0); 0176 } 0177 0178 int flags = fcntl(fd(), F_GETFL); 0179 if (flags < 0) { 0180 qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " 0181 << "fcntl(F_GETFL):" << strerror(errno); 0182 return ret; 0183 } 0184 int oflags = flags; 0185 if (block) { 0186 flags &= ~O_NONBLOCK; 0187 } else { 0188 flags |= O_NONBLOCK; 0189 } 0190 0191 if ((flags != oflags) && (fcntl(fd(), F_SETFL, flags) < 0)) { 0192 // We get an error here when the child process has closed 0193 // the file descriptor already. 0194 return ret; 0195 } 0196 0197 while (1) { 0198 ret.reserve(ret.size() + 0x8000); 0199 int nbytes = read(fd(), ret.data() + ret.size(), 0x8000); 0200 if (nbytes == -1) { 0201 if (errno == EINTR) { 0202 continue; 0203 } else { 0204 break; 0205 } 0206 } 0207 if (nbytes == 0) { 0208 break; // nothing available / eof 0209 } 0210 0211 ret.resize(ret.size() + nbytes); 0212 break; 0213 } 0214 0215 return ret; 0216 } 0217 0218 QByteArray PtyProcess::readLine(bool block) 0219 { 0220 d->inputBuffer = readAll(block); 0221 0222 int pos; 0223 QByteArray ret; 0224 if (!d->inputBuffer.isEmpty()) { 0225 pos = d->inputBuffer.indexOf('\n'); 0226 if (pos == -1) { 0227 // NOTE: this means we return something even if there in no full line! 0228 ret = d->inputBuffer; 0229 d->inputBuffer.resize(0); 0230 } else { 0231 ret = d->inputBuffer.left(pos); 0232 d->inputBuffer.remove(0, pos + 1); 0233 } 0234 } 0235 0236 return ret; 0237 } 0238 0239 void PtyProcess::writeLine(const QByteArray &line, bool addnl) 0240 { 0241 if (!line.isEmpty()) { 0242 write(fd(), line.constData(), line.length()); 0243 } 0244 if (addnl) { 0245 write(fd(), "\n", 1); 0246 } 0247 } 0248 0249 void PtyProcess::unreadLine(const QByteArray &line, bool addnl) 0250 { 0251 QByteArray tmp = line; 0252 if (addnl) { 0253 tmp += '\n'; 0254 } 0255 if (!tmp.isEmpty()) { 0256 d->inputBuffer.prepend(tmp); 0257 } 0258 } 0259 0260 void PtyProcess::setExitString(const QByteArray &exit) 0261 { 0262 m_exitString = exit; 0263 } 0264 0265 /* 0266 * Fork and execute the command. This returns in the parent. 0267 */ 0268 int PtyProcess::exec(const QByteArray &command, const QList<QByteArray> &args) 0269 { 0270 int i; 0271 0272 if (init() < 0) { 0273 return -1; 0274 } 0275 0276 if ((m_pid = fork()) == -1) { 0277 qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " 0278 << "fork():" << strerror(errno); 0279 return -1; 0280 } 0281 0282 // Parent 0283 if (m_pid) { 0284 d->pty->closeSlave(); 0285 return 0; 0286 } 0287 0288 // Child 0289 if (setupTTY() < 0) { 0290 _exit(1); 0291 } 0292 0293 for (const QByteArray &var : std::as_const(d->env)) { 0294 putenv(const_cast<char *>(var.constData())); 0295 } 0296 unsetenv("KDE_FULL_SESSION"); 0297 // for : Qt: Session management error 0298 unsetenv("SESSION_MANAGER"); 0299 // QMutex::lock , deadlocks without that. 0300 // <thiago> you cannot connect to the user's session bus from another UID 0301 unsetenv("DBUS_SESSION_BUS_ADDRESS"); 0302 0303 // set temporarily LC_ALL to C, for su (to be able to parse "Password:") 0304 const QByteArray old_lc_all = qgetenv("LC_ALL"); 0305 if (!old_lc_all.isEmpty()) { 0306 qputenv("KDESU_LC_ALL", old_lc_all); 0307 } else { 0308 unsetenv("KDESU_LC_ALL"); 0309 } 0310 qputenv("LC_ALL", "C"); 0311 0312 // From now on, terminal output goes through the tty. 0313 0314 QByteArray path; 0315 if (command.contains('/')) { 0316 path = command; 0317 } else { 0318 QString file = QStandardPaths::findExecutable(QFile::decodeName(command)); 0319 if (file.isEmpty()) { 0320 qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " << command << "not found."; 0321 _exit(1); 0322 } 0323 path = QFile::encodeName(file); 0324 } 0325 0326 const char **argp = (const char **)malloc((args.count() + 2) * sizeof(char *)); 0327 0328 i = 0; 0329 argp[i++] = path.constData(); 0330 for (const QByteArray &arg : args) { 0331 argp[i++] = arg.constData(); 0332 } 0333 0334 argp[i] = nullptr; 0335 0336 execv(path.constData(), const_cast<char **>(argp)); 0337 qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " 0338 << "execv(" << path << "):" << strerror(errno); 0339 _exit(1); 0340 return -1; // Shut up compiler. Never reached. 0341 } 0342 0343 /* 0344 * Wait until the terminal is set into no echo mode. At least one su 0345 * (RH6 w/ Linux-PAM patches) sets noecho mode AFTER writing the Password: 0346 * prompt, using TCSAFLUSH. This flushes the terminal I/O queues, possibly 0347 * taking the password with it. So we wait until no echo mode is set 0348 * before writing the password. 0349 * Note that this is done on the slave fd. While Linux allows tcgetattr() on 0350 * the master side, Solaris doesn't. 0351 */ 0352 int PtyProcess::waitSlave() 0353 { 0354 struct termios tio; 0355 while (1) { 0356 if (!checkPid(m_pid)) { 0357 qCCritical(KSU_LOG) << "process has exited while waiting for password."; 0358 return -1; 0359 } 0360 if (!d->pty->tcGetAttr(&tio)) { 0361 qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " 0362 << "tcgetattr():" << strerror(errno); 0363 return -1; 0364 } 0365 if (tio.c_lflag & ECHO) { 0366 // qDebug() << "[" << __FILE__ << ":" << __LINE__ << "] " << "Echo mode still on."; 0367 usleep(10000); 0368 continue; 0369 } 0370 break; 0371 } 0372 return 0; 0373 } 0374 0375 int PtyProcess::enableLocalEcho(bool enable) 0376 { 0377 d->wantLocalEcho = enable; 0378 if (!d->pty) { 0379 // Apply it on init 0380 return 0; 0381 } 0382 0383 return d->pty->setEcho(enable) ? 0 : -1; 0384 } 0385 0386 void PtyProcess::setTerminal(bool terminal) 0387 { 0388 m_terminal = terminal; 0389 } 0390 0391 void PtyProcess::setErase(bool erase) 0392 { 0393 m_erase = erase; 0394 } 0395 0396 /* 0397 * Copy output to stdout until the child process exits, or a line of output 0398 * matches `m_exitString'. 0399 * We have to use waitpid() to test for exit. Merely waiting for EOF on the 0400 * pty does not work, because the target process may have children still 0401 * attached to the terminal. 0402 */ 0403 int PtyProcess::waitForChild() 0404 { 0405 fd_set fds; 0406 FD_ZERO(&fds); 0407 QByteArray remainder; 0408 0409 while (1) { 0410 FD_SET(fd(), &fds); 0411 0412 // specify timeout to make sure select() does not block, even if the 0413 // process is dead / non-responsive. It does not matter if we abort too 0414 // early. In that case 0 is returned, and we'll try again in the next 0415 // iteration. (As long as we don't consistently time out in each iteration) 0416 timeval timeout; 0417 timeout.tv_sec = 0; 0418 timeout.tv_usec = 100000; 0419 int ret = select(fd() + 1, &fds, nullptr, nullptr, &timeout); 0420 if (ret == -1) { 0421 if (errno != EINTR) { 0422 qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " 0423 << "select():" << strerror(errno); 0424 return -1; 0425 } 0426 ret = 0; 0427 } 0428 0429 if (ret) { 0430 for (;;) { 0431 QByteArray output = readAll(false); 0432 if (output.isEmpty()) { 0433 break; 0434 } 0435 if (m_terminal) { 0436 fwrite(output.constData(), output.size(), 1, stdout); 0437 fflush(stdout); 0438 } 0439 if (!m_exitString.isEmpty()) { 0440 // match exit string only at line starts 0441 remainder += output; 0442 while (remainder.length() >= m_exitString.length()) { 0443 if (remainder.startsWith(m_exitString)) { 0444 kill(m_pid, SIGTERM); 0445 remainder.remove(0, m_exitString.length()); 0446 } 0447 int off = remainder.indexOf('\n'); 0448 if (off < 0) { 0449 break; 0450 } 0451 remainder.remove(0, off + 1); 0452 } 0453 } 0454 } 0455 } 0456 0457 ret = checkPidExited(m_pid); 0458 if (ret == Error) { 0459 if (errno == ECHILD) { 0460 return 0; 0461 } else { 0462 return 1; 0463 } 0464 } else if (ret == Killed) { 0465 return 0; 0466 } else if (ret == NotExited) { 0467 continue; // keep checking 0468 } else { 0469 return ret; 0470 } 0471 } 0472 } 0473 0474 /* 0475 * SetupTTY: Creates a new session. The filedescriptor "fd" should be 0476 * connected to the tty. It is closed after the tty is reopened to make it 0477 * our controlling terminal. This way the tty is always opened at least once 0478 * so we'll never get EIO when reading from it. 0479 */ 0480 int PtyProcess::setupTTY() 0481 { 0482 // Reset signal handlers 0483 for (int sig = 1; sig < NSIG; sig++) { 0484 signal(sig, SIG_DFL); 0485 } 0486 signal(SIGHUP, SIG_IGN); 0487 0488 d->pty->setCTty(); 0489 0490 // Connect stdin, stdout and stderr 0491 int slave = d->pty->slaveFd(); 0492 dup2(slave, 0); 0493 dup2(slave, 1); 0494 dup2(slave, 2); 0495 0496 // Close all file handles 0497 // XXX this caused problems in KProcess - not sure why anymore. -- ??? 0498 // Because it will close the start notification pipe. -- ossi 0499 struct rlimit rlp; 0500 getrlimit(RLIMIT_NOFILE, &rlp); 0501 for (int i = 3; i < (int)rlp.rlim_cur; i++) { 0502 close(i); 0503 } 0504 0505 // Disable OPOST processing. Otherwise, '\n' are (on Linux at least) 0506 // translated to '\r\n'. 0507 struct ::termios tio; 0508 if (tcgetattr(0, &tio) < 0) { 0509 qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " 0510 << "tcgetattr():" << strerror(errno); 0511 return -1; 0512 } 0513 tio.c_oflag &= ~OPOST; 0514 if (tcsetattr(0, TCSANOW, &tio) < 0) { 0515 qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " 0516 << "tcsetattr():" << strerror(errno); 0517 return -1; 0518 } 0519 0520 return 0; 0521 } 0522 0523 void PtyProcess::virtual_hook(int id, void *data) 0524 { 0525 Q_UNUSED(id); 0526 Q_UNUSED(data); 0527 /*BASE::virtual_hook( id, data );*/ 0528 } 0529 0530 } // namespace KDESu