File indexing completed on 2024-04-28 05:27:02
0001 /* vi: ts=8 sts=4 sw=4 0002 * 0003 * This file is part of the KDE project, module kdesu. 0004 * SPDX-FileCopyrightText: 1998 Pietro Iglio <iglio@fub.it> 0005 * SPDX-FileCopyrightText: 1999, 2000 Geert Jansen <jansen@kde.org> 0006 * SPDX-License-Identifier: Artistic-2.0 0007 */ 0008 0009 #include <config-kde-cli-tools.h> 0010 0011 #include <errno.h> 0012 #include <pwd.h> 0013 #include <stdlib.h> 0014 #include <string.h> 0015 #include <unistd.h> 0016 0017 #include <sys/resource.h> 0018 #include <sys/time.h> 0019 #if HAVE_SYS_WAIT_H 0020 #include <sys/wait.h> 0021 #endif 0022 #if HAVE_SYS_PRCTL_H 0023 #include <sys/prctl.h> 0024 #endif 0025 #if HAVE_SYS_PROCCTL_H 0026 #include <sys/procctl.h> 0027 #include <unistd.h> 0028 #endif 0029 0030 #include <QApplication> 0031 #include <QCommandLineParser> 0032 #include <QFile> 0033 #include <QFileInfo> 0034 #include <QLoggingCategory> 0035 #include <private/qtx11extras_p.h> 0036 0037 #include <KSharedConfig> 0038 #include <kaboutdata.h> 0039 #include <kconfiggroup.h> 0040 #include <klocalizedstring.h> 0041 #include <kmessagebox.h> 0042 #include <kshell.h> 0043 #include <kstartupinfo.h> 0044 #include <kuser.h> 0045 #include <kwindowsystem.h> 0046 0047 #include <kdesu/client.h> 0048 #include <kdesu/defaults.h> 0049 #include <kdesu/suprocess.h> 0050 0051 #include "sudlg.h" 0052 0053 #define ERR strerror(errno) 0054 0055 static QLoggingCategory category("org.kde.kdesu"); 0056 0057 QByteArray command; 0058 const char *Version = PROJECT_VERSION; 0059 0060 // NOTE: if you change the position of the -u switch, be sure to adjust it 0061 // at the beginning of main() 0062 0063 static int startApp(QCommandLineParser &p); 0064 0065 int main(int argc, char *argv[]) 0066 { 0067 // disable ptrace 0068 #if HAVE_PR_SET_DUMPABLE 0069 prctl(PR_SET_DUMPABLE, 0); 0070 #endif 0071 #if HAVE_PROC_TRACE_CTL 0072 int mode = PROC_TRACE_CTL_DISABLE; 0073 procctl(P_PID, getpid(), PROC_TRACE_CTL, &mode); 0074 #endif 0075 0076 QApplication app(argc, argv); 0077 0078 // FIXME: this can be considered a poor man's solution, as it's not 0079 // directly obvious to a gui user. :) 0080 // anyway, i vote against removing it even when we have a proper gui 0081 // implementation. -- ossi 0082 QByteArray duser = qgetenv("ADMIN_ACCOUNT"); 0083 if (duser.isEmpty()) { 0084 duser = "root"; 0085 } 0086 0087 KLocalizedString::setApplicationDomain(QByteArrayLiteral("kdesu")); 0088 0089 KAboutData aboutData(QStringLiteral("kdesu"), 0090 i18n("KDE su"), 0091 QLatin1String(Version), 0092 i18n("Runs a program with elevated privileges."), 0093 KAboutLicense::Artistic, 0094 i18n("Copyright (c) 1998-2000 Geert Jansen, Pietro Iglio")); 0095 aboutData.addAuthor(i18n("Geert Jansen"), i18n("Maintainer"), QStringLiteral("jansen@kde.org"), QStringLiteral("http://www.stack.nl/~geertj/")); 0096 aboutData.addAuthor(i18n("Pietro Iglio"), i18n("Original author"), QStringLiteral("iglio@fub.it")); 0097 app.setWindowIcon(QIcon::fromTheme(QStringLiteral("dialog-password"))); 0098 0099 KAboutData::setApplicationData(aboutData); 0100 0101 // NOTE: if you change the position of the -u switch, be sure to adjust it 0102 // at the beginning of main() 0103 QCommandLineParser parser; 0104 aboutData.setupCommandLine(&parser); 0105 parser.addPositionalArgument(QStringLiteral("command"), i18n("Specifies the command to run as separate arguments")); 0106 parser.addOption(QCommandLineOption(QStringLiteral("c"), i18n("Specifies the command to run as one string"), QStringLiteral("command"))); 0107 parser.addOption(QCommandLineOption(QStringLiteral("f"), i18n("Run command under target uid if <file> is not writable"), QStringLiteral("file"))); 0108 parser.addOption(QCommandLineOption(QStringLiteral("u"), i18n("Specifies the target uid"), QStringLiteral("user"), QString::fromLatin1(duser))); 0109 parser.addOption(QCommandLineOption(QStringLiteral("n"), i18n("Do not keep password"))); 0110 parser.addOption(QCommandLineOption(QStringLiteral("s"), i18n("Stop the daemon (forgets all passwords)"))); 0111 parser.addOption(QCommandLineOption(QStringLiteral("t"), i18n("Enable terminal output (no password keeping)"))); 0112 parser.addOption( 0113 QCommandLineOption(QStringLiteral("p"), i18n("Set priority value: 0 <= prio <= 100, 0 is lowest"), QStringLiteral("prio"), QStringLiteral("50"))); 0114 parser.addOption(QCommandLineOption(QStringLiteral("r"), i18n("Use realtime scheduling"))); 0115 parser.addOption(QCommandLineOption(QStringLiteral("noignorebutton"), i18n("Do not display ignore button"))); 0116 parser.addOption(QCommandLineOption(QStringLiteral("i"), i18n("Specify icon to use in the password dialog"), QStringLiteral("icon name"))); 0117 parser.addOption(QCommandLineOption(QStringLiteral("d"), i18n("Do not show the command to be run in the dialog"))); 0118 #if WITH_X11 0119 /* KDialog originally used --embed for attaching the dialog box. However this is misleading and so we changed to --attach. 0120 * For consistancy, we silently map --embed to --attach */ 0121 parser.addOption(QCommandLineOption(QStringLiteral("attach"), 0122 i18nc("Transient means that the kdesu app will be attached to the app specified by the winid so that it is like a " 0123 "dialog box rather than some separate program", 0124 "Makes the dialog transient for an X app specified by winid"), 0125 QStringLiteral("winid"))); 0126 parser.addOption(QCommandLineOption(QStringLiteral("embed"), i18n("Embed into a window"), QStringLiteral("winid"))); 0127 #endif 0128 0129 // KApplication::disableAutoDcopRegistration(); 0130 // kdesu doesn't process SM events, so don't even connect to ksmserver 0131 QByteArray session_manager = qgetenv("SESSION_MANAGER"); 0132 if (!session_manager.isEmpty()) { 0133 unsetenv("SESSION_MANAGER"); 0134 } 0135 // but propagate it to the started app 0136 if (!session_manager.isEmpty()) { 0137 setenv("SESSION_MANAGER", session_manager.data(), 1); 0138 } 0139 0140 { 0141 #if WITH_X11 0142 KStartupInfoId id; 0143 id.initId(); 0144 id.setupStartupEnv(); // make DESKTOP_STARTUP_ID env. var. available again 0145 #endif 0146 } 0147 0148 parser.process(app); 0149 aboutData.processCommandLine(&parser); 0150 int result = startApp(parser); 0151 0152 if (result == 127) { 0153 KMessageBox::error(nullptr, i18n("Cannot execute command '%1'.", QString::fromLocal8Bit(command))); 0154 } 0155 if (result == -2) { 0156 KMessageBox::error(nullptr, i18n("Cannot execute command '%1'. It contains invalid characters.", QString::fromLocal8Bit(command))); 0157 } 0158 0159 return result; 0160 } 0161 0162 static int startApp(QCommandLineParser &p) 0163 { 0164 // Stop daemon and exit? 0165 if (p.isSet(QStringLiteral("s"))) { 0166 KDESu::Client client; 0167 if (client.ping() == -1) { 0168 qCCritical(category) << "Daemon not running -- nothing to stop\n"; 0169 p.showHelp(1); 0170 } 0171 if (client.stopServer() != -1) { 0172 qCDebug(category) << "Daemon stopped\n"; 0173 exit(0); 0174 } 0175 qCCritical(category) << "Could not stop daemon\n"; 0176 p.showHelp(1); 0177 } 0178 0179 QString icon; 0180 if (p.isSet(QStringLiteral("i"))) { 0181 icon = p.value(QStringLiteral("i")); 0182 } 0183 0184 bool prompt = true; 0185 if (p.isSet(QStringLiteral("d"))) { 0186 prompt = false; 0187 } 0188 0189 // Get target uid 0190 const QByteArray user = p.value(QStringLiteral("u")).toLocal8Bit(); 0191 QByteArray auth_user = user; 0192 struct passwd *pw = getpwnam(user.constData()); 0193 if (pw == nullptr) { 0194 qCCritical(category) << "User " << user << " does not exist\n"; 0195 p.showHelp(1); 0196 } 0197 bool other_uid = (getuid() != pw->pw_uid); 0198 bool change_uid = other_uid; 0199 if (!change_uid) { 0200 char *cur_user = getenv("USER"); 0201 if (!cur_user) { 0202 cur_user = getenv("LOGNAME"); 0203 } 0204 change_uid = (!cur_user || user != cur_user); 0205 } 0206 0207 // If file is writeable, do not change uid 0208 QString file = p.value(QStringLiteral("f")); 0209 if (other_uid && !file.isEmpty()) { 0210 if (file.startsWith(QLatin1Char('/'))) { 0211 file = QStandardPaths::locate(QStandardPaths::GenericConfigLocation, file); 0212 if (file.isEmpty()) { 0213 qCCritical(category) << "Config file not found: " << file; 0214 p.showHelp(1); 0215 } 0216 } 0217 QFileInfo fi(file); 0218 if (!fi.exists()) { 0219 qCCritical(category) << "File does not exist: " << file; 0220 p.showHelp(1); 0221 } 0222 change_uid = !fi.isWritable(); 0223 } 0224 0225 // Get priority/scheduler 0226 QString tmp = p.value(QStringLiteral("p")); 0227 bool ok; 0228 int priority = tmp.toInt(&ok); 0229 if (!ok || (priority < 0) || (priority > 100)) { 0230 qCCritical(category) << i18n("Illegal priority: %1", tmp); 0231 p.showHelp(1); 0232 } 0233 int scheduler = SuProcess::SchedNormal; 0234 if (p.isSet(QStringLiteral("r"))) { 0235 scheduler = SuProcess::SchedRealtime; 0236 } 0237 if ((priority > 50) || (scheduler != SuProcess::SchedNormal)) { 0238 change_uid = true; 0239 auth_user = "root"; 0240 } 0241 0242 // Get command 0243 if (p.isSet(QStringLiteral("c"))) { 0244 command = p.value(QStringLiteral("c")).toLocal8Bit(); 0245 // Accepting additional arguments here is somewhat weird, 0246 // but one can conceive use cases: have a complex command with 0247 // redirections and additional file names which need to be quoted 0248 // safely. 0249 } else { 0250 if (p.positionalArguments().count() == 0) { 0251 qCCritical(category) << i18n("No command specified."); 0252 p.showHelp(1); 0253 } 0254 } 0255 const auto positionalArguments = p.positionalArguments(); 0256 for (const QString &arg : positionalArguments) { 0257 command += ' '; 0258 command += QFile::encodeName(KShell::quoteArg(arg)); 0259 } 0260 0261 // Don't change uid if we're don't need to. 0262 if (!change_uid) { 0263 int result = system(command.constData()); 0264 result = WEXITSTATUS(result); 0265 return result; 0266 } 0267 0268 // Check for daemon and start if necessary 0269 bool just_started = false; 0270 bool have_daemon = true; 0271 KDESu::Client client; 0272 if (client.ping() == -1) { 0273 if (client.startServer() == -1) { 0274 qCWarning(category) << "Could not start daemon, reduced functionality.\n"; 0275 have_daemon = false; 0276 } 0277 just_started = true; 0278 } 0279 0280 // Try to exec the command with kdesud. 0281 bool keep = !p.isSet(QStringLiteral("n")) && have_daemon; 0282 bool terminal = p.isSet(QStringLiteral("t")); 0283 bool withIgnoreButton = !p.isSet(QStringLiteral("noignorebutton")); 0284 int winid = -1; 0285 bool attach = p.isSet(QStringLiteral("attach")); 0286 if (attach) { 0287 winid = p.value(QStringLiteral("attach")).toInt(&attach, 0); // C style parsing. If the string begins with "0x", base 16 is used; if the string begins 0288 // with "0", base 8 is used; otherwise, base 10 is used. 0289 if (!attach) { 0290 qCWarning(category) << "Specified winid to attach to is not a valid number"; 0291 } 0292 } else if (p.isSet(QStringLiteral("embed"))) { 0293 /* KDialog originally used --embed for attaching the dialog box. However this is misleading and so we changed to --attach. 0294 * For consistancy, we silently map --embed to --attach */ 0295 attach = true; 0296 winid = p.value(QStringLiteral("embed")).toInt(&attach, 0); // C style parsing. If the string begins with "0x", base 16 is used; if the string begins 0297 // with "0", base 8 is used; otherwise, base 10 is used. 0298 if (!attach) { 0299 qCWarning(category) << "Specified winid to attach to is not a valid number"; 0300 } 0301 } 0302 0303 QList<QByteArray> env; 0304 QByteArray options; 0305 env << ("DESKTOP_STARTUP_ID=" + QX11Info::nextStartupId()); 0306 0307 // TODO: Maybe should port this to XDG_*, somehow? 0308 // if (pw->pw_uid) 0309 // { 0310 // // Only propagate KDEHOME for non-root users, 0311 // // root uses KDEROOTHOME 0312 // 0313 // // Translate the KDEHOME of this user to the new user. 0314 // QString kdeHome = KGlobal::dirs()->relativeLocation("home", KGlobal::dirs()->localkdedir()); 0315 // if (kdeHome[0] != '/') 0316 // kdeHome.prepend("~/"); 0317 // else 0318 // kdeHome.clear(); // Use default 0319 // 0320 // env << ("KDEHOME="+ QFile::encodeName(kdeHome)); 0321 // } 0322 0323 KUser u; 0324 env << (QByteArray)("KDESU_USER=" + u.loginName().toLocal8Bit()); 0325 0326 if (keep && !terminal && !just_started) { 0327 client.setPriority(priority); 0328 client.setScheduler(scheduler); 0329 int result = client.exec(command, user, options, env); 0330 if (result == 0) { 0331 result = client.exitCode(); 0332 return result; 0333 } 0334 } 0335 0336 // Set core dump size to 0 because we will have 0337 // root's password in memory. 0338 struct rlimit rlim; 0339 rlim.rlim_cur = rlim.rlim_max = 0; 0340 if (setrlimit(RLIMIT_CORE, &rlim)) { 0341 qCCritical(category) << "rlimit(): " << ERR; 0342 p.showHelp(1); 0343 } 0344 0345 // Read configuration 0346 KConfigGroup config(KSharedConfig::openConfig(), "Passwords"); 0347 int timeout = config.readEntry("Timeout", defTimeout); 0348 0349 // Check if we need a password 0350 SuProcess proc; 0351 proc.setUser(auth_user); 0352 int needpw = proc.checkNeedPassword(); 0353 if (needpw < 0) { 0354 QString err = i18n("Su returned with an error.\n"); 0355 KMessageBox::error(nullptr, err); 0356 p.showHelp(1); 0357 } 0358 if (needpw == 0) { 0359 keep = 0; 0360 qDebug() << "Don't need password!!\n"; 0361 } 0362 0363 const QString cmd = QString::fromLocal8Bit(command); 0364 for (const QChar character : cmd) { 0365 if (!character.isPrint() && character.category() != QChar::Other_Surrogate) { 0366 return -2; 0367 } 0368 } 0369 0370 // Start the dialog 0371 QString password; 0372 if (needpw) { 0373 #if WITH_X11 0374 KStartupInfoId id; 0375 id.initId(); 0376 KStartupInfoData data; 0377 data.setSilent(KStartupInfoData::Yes); 0378 KStartupInfo::sendChange(id, data); 0379 #endif 0380 KDEsuDialog dlg(user, auth_user, keep && !terminal, icon, withIgnoreButton); 0381 if (prompt) { 0382 dlg.addCommentLine(i18n("Command:"), QFile::decodeName(command)); 0383 } 0384 if (defKeep) { 0385 dlg.setKeepPassword(true); 0386 } 0387 0388 if ((priority != 50) || (scheduler != SuProcess::SchedNormal)) { 0389 QString prio; 0390 if (scheduler == SuProcess::SchedRealtime) { 0391 prio += i18n("realtime: "); 0392 } 0393 prio += QStringLiteral("%1/100").arg(priority); 0394 if (prompt) { 0395 dlg.addCommentLine(i18n("Priority:"), prio); 0396 } 0397 } 0398 0399 // Attach dialog 0400 #if WITH_X11 0401 if (attach) { 0402 dlg.setAttribute(Qt::WA_NativeWindow, true); 0403 KWindowSystem::setMainWindow(dlg.windowHandle(), WId(winid)); 0404 } 0405 #endif 0406 int ret = dlg.exec(); 0407 if (ret == KDEsuDialog::Rejected) { 0408 #if WITH_X11 0409 KStartupInfo::sendFinish(id); 0410 #endif 0411 p.showHelp(1); 0412 } 0413 if (ret == KDEsuDialog::AsUser) { 0414 change_uid = false; 0415 } 0416 password = dlg.password(); 0417 keep = dlg.keepPassword(); 0418 #if WITH_X11 0419 data.setSilent(KStartupInfoData::No); 0420 KStartupInfo::sendChange(id, data); 0421 #endif 0422 } 0423 0424 // Some events may need to be handled (like a button animation) 0425 qApp->processEvents(); 0426 0427 // Run command 0428 if (!change_uid) { 0429 int result = system(command.constData()); 0430 result = WEXITSTATUS(result); 0431 return result; 0432 } else if (keep && have_daemon) { 0433 client.setPass(password.toLocal8Bit().constData(), timeout); 0434 client.setPriority(priority); 0435 client.setScheduler(scheduler); 0436 int result = client.exec(command, user, options, env); 0437 if (result == 0) { 0438 result = client.exitCode(); 0439 return result; 0440 } 0441 } else { 0442 SuProcess proc; 0443 proc.setTerminal(terminal); 0444 proc.setErase(true); 0445 proc.setUser(user); 0446 proc.setEnvironment(env); 0447 proc.setPriority(priority); 0448 proc.setScheduler(scheduler); 0449 proc.setCommand(command); 0450 int result = proc.exec(password.toLocal8Bit().constData()); 0451 return result; 0452 } 0453 return -1; 0454 }