File indexing completed on 2024-04-21 03:53:58

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, QStringLiteral("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_ptr(&dd)
0122 {
0123     m_terminal = false;
0124     m_erase = false;
0125 }
0126 
0127 PtyProcess::~PtyProcess() = default;
0128 
0129 int PtyProcess::init()
0130 {
0131     Q_D(PtyProcess);
0132 
0133     delete d->pty;
0134     d->pty = new KPty();
0135     if (!d->pty->open()) {
0136         qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
0137                             << "Failed to open PTY.";
0138         return -1;
0139     }
0140     if (!d->wantLocalEcho) {
0141         enableLocalEcho(false);
0142     }
0143     d->inputBuffer.resize(0);
0144     return 0;
0145 }
0146 
0147 /** Set additional environment variables. */
0148 void PtyProcess::setEnvironment(const QList<QByteArray> &env)
0149 {
0150     Q_D(PtyProcess);
0151 
0152     d->env = env;
0153 }
0154 
0155 int PtyProcess::fd() const
0156 {
0157     Q_D(const PtyProcess);
0158 
0159     return d->pty ? d->pty->masterFd() : -1;
0160 }
0161 
0162 int PtyProcess::pid() const
0163 {
0164     return m_pid;
0165 }
0166 
0167 /** Returns the additional environment variables set by setEnvironment() */
0168 QList<QByteArray> PtyProcess::environment() const
0169 {
0170     Q_D(const PtyProcess);
0171 
0172     return d->env;
0173 }
0174 
0175 QByteArray PtyProcess::readAll(bool block)
0176 {
0177     Q_D(PtyProcess);
0178 
0179     QByteArray ret;
0180     if (!d->inputBuffer.isEmpty()) {
0181         // if there is still something in the buffer, we need not block.
0182         // we should still try to read any further output, from the fd, though.
0183         block = false;
0184         ret = d->inputBuffer;
0185         d->inputBuffer.resize(0);
0186     }
0187 
0188     int flags = fcntl(fd(), F_GETFL);
0189     if (flags < 0) {
0190         qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
0191                             << "fcntl(F_GETFL):" << strerror(errno);
0192         return ret;
0193     }
0194     int oflags = flags;
0195     if (block) {
0196         flags &= ~O_NONBLOCK;
0197     } else {
0198         flags |= O_NONBLOCK;
0199     }
0200 
0201     if ((flags != oflags) && (fcntl(fd(), F_SETFL, flags) < 0)) {
0202         // We get an error here when the child process has closed
0203         // the file descriptor already.
0204         return ret;
0205     }
0206 
0207     while (1) {
0208         ret.reserve(ret.size() + 0x8000);
0209         int nbytes = read(fd(), ret.data() + ret.size(), 0x8000);
0210         if (nbytes == -1) {
0211             if (errno == EINTR) {
0212                 continue;
0213             } else {
0214                 break;
0215             }
0216         }
0217         if (nbytes == 0) {
0218             break; // nothing available / eof
0219         }
0220 
0221         ret.resize(ret.size() + nbytes);
0222         break;
0223     }
0224 
0225     return ret;
0226 }
0227 
0228 QByteArray PtyProcess::readLine(bool block)
0229 {
0230     Q_D(PtyProcess);
0231 
0232     d->inputBuffer = readAll(block);
0233 
0234     int pos;
0235     QByteArray ret;
0236     if (!d->inputBuffer.isEmpty()) {
0237         pos = d->inputBuffer.indexOf('\n');
0238         if (pos == -1) {
0239             // NOTE: this means we return something even if there in no full line!
0240             ret = d->inputBuffer;
0241             d->inputBuffer.resize(0);
0242         } else {
0243             ret = d->inputBuffer.left(pos);
0244             d->inputBuffer.remove(0, pos + 1);
0245         }
0246     }
0247 
0248     return ret;
0249 }
0250 
0251 void PtyProcess::writeLine(const QByteArray &line, bool addnl)
0252 {
0253     if (!line.isEmpty()) {
0254         write(fd(), line.constData(), line.length());
0255     }
0256     if (addnl) {
0257         write(fd(), "\n", 1);
0258     }
0259 }
0260 
0261 void PtyProcess::unreadLine(const QByteArray &line, bool addnl)
0262 {
0263     Q_D(PtyProcess);
0264 
0265     QByteArray tmp = line;
0266     if (addnl) {
0267         tmp += '\n';
0268     }
0269     if (!tmp.isEmpty()) {
0270         d->inputBuffer.prepend(tmp);
0271     }
0272 }
0273 
0274 void PtyProcess::setExitString(const QByteArray &exit)
0275 {
0276     m_exitString = exit;
0277 }
0278 
0279 /*
0280  * Fork and execute the command. This returns in the parent.
0281  */
0282 int PtyProcess::exec(const QByteArray &command, const QList<QByteArray> &args)
0283 {
0284     Q_D(PtyProcess);
0285 
0286     int i;
0287 
0288     if (init() < 0) {
0289         return -1;
0290     }
0291 
0292     if ((m_pid = fork()) == -1) {
0293         qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
0294                             << "fork():" << strerror(errno);
0295         return -1;
0296     }
0297 
0298     // Parent
0299     if (m_pid) {
0300         d->pty->closeSlave();
0301         return 0;
0302     }
0303 
0304     // Child
0305     if (setupTTY() < 0) {
0306         _exit(1);
0307     }
0308 
0309     for (const QByteArray &var : std::as_const(d->env)) {
0310         putenv(const_cast<char *>(var.constData()));
0311     }
0312     unsetenv("KDE_FULL_SESSION");
0313     // for : Qt: Session management error
0314     unsetenv("SESSION_MANAGER");
0315     // QMutex::lock , deadlocks without that.
0316     // <thiago> you cannot connect to the user's session bus from another UID
0317     unsetenv("DBUS_SESSION_BUS_ADDRESS");
0318 
0319     // set temporarily LC_ALL to C, for su (to be able to parse "Password:")
0320     const QByteArray old_lc_all = qgetenv("LC_ALL");
0321     if (!old_lc_all.isEmpty()) {
0322         qputenv("KDESU_LC_ALL", old_lc_all);
0323     } else {
0324         unsetenv("KDESU_LC_ALL");
0325     }
0326     qputenv("LC_ALL", "C");
0327 
0328     // From now on, terminal output goes through the tty.
0329 
0330     QByteArray path;
0331     if (command.contains('/')) {
0332         path = command;
0333     } else {
0334         QString file = QStandardPaths::findExecutable(QFile::decodeName(command));
0335         if (file.isEmpty()) {
0336             qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] " << command << "not found.";
0337             _exit(1);
0338         }
0339         path = QFile::encodeName(file);
0340     }
0341 
0342     const char **argp = (const char **)malloc((args.count() + 2) * sizeof(char *));
0343 
0344     i = 0;
0345     argp[i++] = path.constData();
0346     for (const QByteArray &arg : args) {
0347         argp[i++] = arg.constData();
0348     }
0349 
0350     argp[i] = nullptr;
0351 
0352     execv(path.constData(), const_cast<char **>(argp));
0353     qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
0354                         << "execv(" << path << "):" << strerror(errno);
0355     _exit(1);
0356     return -1; // Shut up compiler. Never reached.
0357 }
0358 
0359 /*
0360  * Wait until the terminal is set into no echo mode. At least one su
0361  * (RH6 w/ Linux-PAM patches) sets noecho mode AFTER writing the Password:
0362  * prompt, using TCSAFLUSH. This flushes the terminal I/O queues, possibly
0363  * taking the password  with it. So we wait until no echo mode is set
0364  * before writing the password.
0365  * Note that this is done on the slave fd. While Linux allows tcgetattr() on
0366  * the master side, Solaris doesn't.
0367  */
0368 int PtyProcess::waitSlave()
0369 {
0370     Q_D(PtyProcess);
0371 
0372     struct termios tio;
0373     while (1) {
0374         if (!checkPid(m_pid)) {
0375             qCCritical(KSU_LOG) << "process has exited while waiting for password.";
0376             return -1;
0377         }
0378         if (!d->pty->tcGetAttr(&tio)) {
0379             qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
0380                                 << "tcgetattr():" << strerror(errno);
0381             return -1;
0382         }
0383         if (tio.c_lflag & ECHO) {
0384             // qDebug() << "[" << __FILE__ << ":" << __LINE__ << "] " << "Echo mode still on.";
0385             usleep(10000);
0386             continue;
0387         }
0388         break;
0389     }
0390     return 0;
0391 }
0392 
0393 int PtyProcess::enableLocalEcho(bool enable)
0394 {
0395     Q_D(PtyProcess);
0396 
0397     d->wantLocalEcho = enable;
0398     if (!d->pty) {
0399         // Apply it on init
0400         return 0;
0401     }
0402 
0403     return d->pty->setEcho(enable) ? 0 : -1;
0404 }
0405 
0406 void PtyProcess::setTerminal(bool terminal)
0407 {
0408     m_terminal = terminal;
0409 }
0410 
0411 void PtyProcess::setErase(bool erase)
0412 {
0413     m_erase = erase;
0414 }
0415 
0416 /*
0417  * Copy output to stdout until the child process exits, or a line of output
0418  * matches `m_exitString'.
0419  * We have to use waitpid() to test for exit. Merely waiting for EOF on the
0420  * pty does not work, because the target process may have children still
0421  * attached to the terminal.
0422  */
0423 int PtyProcess::waitForChild()
0424 {
0425     fd_set fds;
0426     FD_ZERO(&fds);
0427     QByteArray remainder;
0428 
0429     while (1) {
0430         FD_SET(fd(), &fds);
0431 
0432         // specify timeout to make sure select() does not block, even if the
0433         // process is dead / non-responsive. It does not matter if we abort too
0434         // early. In that case 0 is returned, and we'll try again in the next
0435         // iteration. (As long as we don't consistently time out in each iteration)
0436         timeval timeout;
0437         timeout.tv_sec = 0;
0438         timeout.tv_usec = 100000;
0439         int ret = select(fd() + 1, &fds, nullptr, nullptr, &timeout);
0440         if (ret == -1) {
0441             if (errno != EINTR) {
0442                 qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
0443                                     << "select():" << strerror(errno);
0444                 return -1;
0445             }
0446             ret = 0;
0447         }
0448 
0449         if (ret) {
0450             for (;;) {
0451                 QByteArray output = readAll(false);
0452                 if (output.isEmpty()) {
0453                     break;
0454                 }
0455                 if (m_terminal) {
0456                     fwrite(output.constData(), output.size(), 1, stdout);
0457                     fflush(stdout);
0458                 }
0459                 if (!m_exitString.isEmpty()) {
0460                     // match exit string only at line starts
0461                     remainder += output;
0462                     while (remainder.length() >= m_exitString.length()) {
0463                         if (remainder.startsWith(m_exitString)) {
0464                             kill(m_pid, SIGTERM);
0465                             remainder.remove(0, m_exitString.length());
0466                         }
0467                         int off = remainder.indexOf('\n');
0468                         if (off < 0) {
0469                             break;
0470                         }
0471                         remainder.remove(0, off + 1);
0472                     }
0473                 }
0474             }
0475         }
0476 
0477         ret = checkPidExited(m_pid);
0478         if (ret == Error) {
0479             if (errno == ECHILD) {
0480                 return 0;
0481             } else {
0482                 return 1;
0483             }
0484         } else if (ret == Killed) {
0485             return 0;
0486         } else if (ret == NotExited) {
0487             continue; // keep checking
0488         } else {
0489             return ret;
0490         }
0491     }
0492 }
0493 
0494 /*
0495  * SetupTTY: Creates a new session. The filedescriptor "fd" should be
0496  * connected to the tty. It is closed after the tty is reopened to make it
0497  * our controlling terminal. This way the tty is always opened at least once
0498  * so we'll never get EIO when reading from it.
0499  */
0500 int PtyProcess::setupTTY()
0501 {
0502     Q_D(PtyProcess);
0503 
0504     // Reset signal handlers
0505     for (int sig = 1; sig < NSIG; sig++) {
0506         signal(sig, SIG_DFL);
0507     }
0508     signal(SIGHUP, SIG_IGN);
0509 
0510     d->pty->setCTty();
0511 
0512     // Connect stdin, stdout and stderr
0513     int slave = d->pty->slaveFd();
0514     dup2(slave, 0);
0515     dup2(slave, 1);
0516     dup2(slave, 2);
0517 
0518     // Close all file handles
0519     // XXX this caused problems in KProcess - not sure why anymore. -- ???
0520     // Because it will close the start notification pipe. -- ossi
0521     struct rlimit rlp;
0522     getrlimit(RLIMIT_NOFILE, &rlp);
0523     for (int i = 3; i < (int)rlp.rlim_cur; i++) {
0524         close(i);
0525     }
0526 
0527     // Disable OPOST processing. Otherwise, '\n' are (on Linux at least)
0528     // translated to '\r\n'.
0529     struct ::termios tio;
0530     if (tcgetattr(0, &tio) < 0) {
0531         qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
0532                             << "tcgetattr():" << strerror(errno);
0533         return -1;
0534     }
0535     tio.c_oflag &= ~OPOST;
0536     if (tcsetattr(0, TCSANOW, &tio) < 0) {
0537         qCCritical(KSU_LOG) << "[" << __FILE__ << ":" << __LINE__ << "] "
0538                             << "tcsetattr():" << strerror(errno);
0539         return -1;
0540     }
0541 
0542     return 0;
0543 }
0544 
0545 void PtyProcess::virtual_hook(int id, void *data)
0546 {
0547     Q_UNUSED(id);
0548     Q_UNUSED(data);
0549     /*BASE::virtual_hook( id, data );*/
0550 }
0551 
0552 } // namespace KDESu