forked from quickshell/quickshell
		
	service/pam: move pam execution to subprocess to allow killing it
Many pam modules can't be aborted well without this.
This commit is contained in:
		
							parent
							
								
									b5c8774a79
								
							
						
					
					
						commit
						e89035b18c
					
				
					 10 changed files with 480 additions and 173 deletions
				
			
		| 
						 | 
				
			
			@ -28,6 +28,7 @@ Checks: >
 | 
			
		|||
  -modernize-return-braced-init-list,
 | 
			
		||||
  -modernize-use-trailing-return-type,
 | 
			
		||||
  performance-*,
 | 
			
		||||
  -performance-avoid-endl,
 | 
			
		||||
  portability-std-allocator-const,
 | 
			
		||||
  readability-*,
 | 
			
		||||
  -readability-function-cognitive-complexity,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,8 @@
 | 
			
		|||
qt_add_library(quickshell-service-pam STATIC
 | 
			
		||||
	qml.cpp
 | 
			
		||||
	conversation.cpp
 | 
			
		||||
	ipc.cpp
 | 
			
		||||
	subprocess.cpp
 | 
			
		||||
)
 | 
			
		||||
qt_add_qml_module(quickshell-service-pam
 | 
			
		||||
	URI Quickshell.Services.Pam
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,21 +1,22 @@
 | 
			
		|||
#include "conversation.hpp"
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
#include <qlogging.h>
 | 
			
		||||
#include <qloggingcategory.h>
 | 
			
		||||
#include <qmutex.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qsocketnotifier.h>
 | 
			
		||||
#include <qstring.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <security/_pam_types.h>
 | 
			
		||||
#include <security/pam_appl.h>
 | 
			
		||||
#include <sys/wait.h>
 | 
			
		||||
 | 
			
		||||
#include "ipc.hpp"
 | 
			
		||||
 | 
			
		||||
Q_LOGGING_CATEGORY(logPam, "quickshell.service.pam", QtWarningMsg);
 | 
			
		||||
 | 
			
		||||
QString PamError::toString(PamError::Enum value) {
 | 
			
		||||
	switch (value) {
 | 
			
		||||
	case ConnectionFailed: return "Failed to connect to pam";
 | 
			
		||||
	case StartFailed: return "Failed to start the PAM session";
 | 
			
		||||
	case TryAuthFailed: return "Failed to try authenticating";
 | 
			
		||||
	case InternalError: return "Internal error occurred";
 | 
			
		||||
	default: return "Invalid error";
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -26,160 +27,116 @@ QString PamResult::toString(PamResult::Enum value) {
 | 
			
		|||
	case Failed: return "Failed";
 | 
			
		||||
	case Error: return "Error occurred while authenticating";
 | 
			
		||||
	case MaxTries: return "The authentication method has no more attempts available";
 | 
			
		||||
	// case Expired: return "The account has expired";
 | 
			
		||||
	// case PermissionDenied: return "Permission denied";
 | 
			
		||||
	default: return "Invalid result";
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PamConversation::run() {
 | 
			
		||||
	auto conv = pam_conv {
 | 
			
		||||
	    .conv = &PamConversation::conversation,
 | 
			
		||||
	    .appdata_ptr = this,
 | 
			
		||||
	};
 | 
			
		||||
PamConversation::~PamConversation() { this->abort(); }
 | 
			
		||||
 | 
			
		||||
	pam_handle_t* handle = nullptr;
 | 
			
		||||
 | 
			
		||||
	qCInfo(logPam) << this << "Starting pam session for user" << this->user << "with config"
 | 
			
		||||
	               << this->config << "in configdir" << this->configDir;
 | 
			
		||||
 | 
			
		||||
	auto result = pam_start_confdir(
 | 
			
		||||
	    this->config.toStdString().c_str(),
 | 
			
		||||
	    this->user.toStdString().c_str(),
 | 
			
		||||
	    &conv,
 | 
			
		||||
	    this->configDir.toStdString().c_str(),
 | 
			
		||||
	    &handle
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	if (result != PAM_SUCCESS) {
 | 
			
		||||
		qCCritical(logPam) << this << "Unable to start pam conversation with error"
 | 
			
		||||
		                   << QString(pam_strerror(handle, result));
 | 
			
		||||
		emit this->error(PamError::ConnectionFailed);
 | 
			
		||||
		this->deleteLater();
 | 
			
		||||
void PamConversation::start(const QString& configDir, const QString& config, const QString& user) {
 | 
			
		||||
	this->childPid = PamConversation::createSubprocess(&this->pipes, configDir, config, user);
 | 
			
		||||
	if (this->childPid == 0) {
 | 
			
		||||
		qCCritical(logPam) << "Failed to create pam subprocess.";
 | 
			
		||||
		emit this->error(PamError::InternalError);
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	result = pam_authenticate(handle, 0);
 | 
			
		||||
 | 
			
		||||
	// Seems to require root and quickshell should not run as root.
 | 
			
		||||
	// if (result == PAM_SUCCESS) {
 | 
			
		||||
	//   result = pam_acct_mgmt(handle, 0);
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	switch (result) {
 | 
			
		||||
	case PAM_SUCCESS:
 | 
			
		||||
		qCInfo(logPam) << this << "ended with successful authentication.";
 | 
			
		||||
		emit this->completed(PamResult::Success);
 | 
			
		||||
		break;
 | 
			
		||||
	case PAM_AUTH_ERR:
 | 
			
		||||
		qCInfo(logPam) << this << "ended with failed authentication.";
 | 
			
		||||
		emit this->completed(PamResult::Failed);
 | 
			
		||||
		break;
 | 
			
		||||
	case PAM_MAXTRIES:
 | 
			
		||||
		qCInfo(logPam) << this << "ended with failure: max tries.";
 | 
			
		||||
		emit this->completed(PamResult::MaxTries);
 | 
			
		||||
		break;
 | 
			
		||||
	/*case PAM_ACCT_EXPIRED:
 | 
			
		||||
		qCInfo(logPam) << this << "ended with failure: account expiration.";
 | 
			
		||||
		emit this->completed(PamResult::Expired);
 | 
			
		||||
		break;
 | 
			
		||||
	case PAM_PERM_DENIED:
 | 
			
		||||
		qCInfo(logPam) << this << "ended with failure: permission denied.";
 | 
			
		||||
		emit this->completed(PamResult::PermissionDenied);
 | 
			
		||||
		break;*/
 | 
			
		||||
	default:
 | 
			
		||||
		qCCritical(logPam) << this << "ended with error:" << QString(pam_strerror(handle, result));
 | 
			
		||||
		emit this->error(PamError::TryAuthFailed);
 | 
			
		||||
		break;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	result = pam_end(handle, result);
 | 
			
		||||
	if (result != PAM_SUCCESS) {
 | 
			
		||||
		qCCritical(logPam) << this << "Failed to end pam conversation with error code"
 | 
			
		||||
		                   << QString(pam_strerror(handle, result));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->deleteLater();
 | 
			
		||||
	QObject::connect(&this->notifier, &QSocketNotifier::activated, this, &PamConversation::onMessage);
 | 
			
		||||
	this->notifier.setSocket(this->pipes.fdIn);
 | 
			
		||||
	this->notifier.setEnabled(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PamConversation::abort() {
 | 
			
		||||
	qCDebug(logPam) << "Abort requested for" << this;
 | 
			
		||||
	auto locker = QMutexLocker(&this->wakeMutex);
 | 
			
		||||
	this->mAbort = true;
 | 
			
		||||
	this->waker.wakeOne();
 | 
			
		||||
	if (this->childPid != 0) {
 | 
			
		||||
		qCDebug(logPam) << "Killing subprocess for" << this;
 | 
			
		||||
		kill(this->childPid, SIGKILL); // NOLINT (include)
 | 
			
		||||
		waitpid(this->childPid, nullptr, 0);
 | 
			
		||||
		this->childPid = 0;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PamConversation::respond(QString response) {
 | 
			
		||||
	qCDebug(logPam) << "Set response for" << this;
 | 
			
		||||
	auto locker = QMutexLocker(&this->wakeMutex);
 | 
			
		||||
	this->response = std::move(response);
 | 
			
		||||
	this->hasResponse = true;
 | 
			
		||||
	this->waker.wakeOne();
 | 
			
		||||
void PamConversation::internalError() {
 | 
			
		||||
	if (this->childPid != 0) {
 | 
			
		||||
		qCDebug(logPam) << "Killing subprocess for" << this;
 | 
			
		||||
		kill(this->childPid, SIGKILL); // NOLINT (include)
 | 
			
		||||
		waitpid(this->childPid, nullptr, 0);
 | 
			
		||||
		this->childPid = 0;
 | 
			
		||||
		emit this->error(PamError::InternalError);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int PamConversation::conversation(
 | 
			
		||||
    int msgCount,
 | 
			
		||||
    const pam_message** msgArray,
 | 
			
		||||
    pam_response** responseArray,
 | 
			
		||||
    void* appdata
 | 
			
		||||
) {
 | 
			
		||||
	auto* delegate = static_cast<PamConversation*>(appdata);
 | 
			
		||||
void PamConversation::respond(const QString& response) {
 | 
			
		||||
	qCDebug(logPam) << "Sending response for" << this;
 | 
			
		||||
	if (!this->pipes.writeString(response.toStdString())) {
 | 
			
		||||
		qCCritical(logPam) << "Failed to write response to subprocess.";
 | 
			
		||||
		this->internalError();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PamConversation::onMessage() {
 | 
			
		||||
	{
 | 
			
		||||
		auto locker = QMutexLocker(&delegate->wakeMutex);
 | 
			
		||||
		if (delegate->mAbort) {
 | 
			
		||||
			return PAM_ERROR_MSG;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
		qCDebug(logPam) << "Got message from subprocess.";
 | 
			
		||||
 | 
			
		||||
	// freed by libc so must be alloc'd by it.
 | 
			
		||||
	auto* responses = static_cast<pam_response*>(calloc(msgCount, sizeof(pam_response))); // NOLINT
 | 
			
		||||
		auto type = PamIpcEvent::Exit;
 | 
			
		||||
 | 
			
		||||
	for (auto i = 0; i < msgCount; i++) {
 | 
			
		||||
		const auto* message = msgArray[i]; // NOLINT
 | 
			
		||||
		auto& response = responses[i];     // NOLINT
 | 
			
		||||
		auto ok = this->pipes.readBytes(
 | 
			
		||||
		    reinterpret_cast<char*>(&type), // NOLINT
 | 
			
		||||
		    sizeof(PamIpcEvent)
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		auto msgString = QString(message->msg);
 | 
			
		||||
		auto messageChanged = true; // message->msg_style != PAM_PROMPT_ECHO_OFF;
 | 
			
		||||
		auto isError = message->msg_style == PAM_ERROR_MSG;
 | 
			
		||||
		auto responseRequired =
 | 
			
		||||
		    message->msg_style == PAM_PROMPT_ECHO_OFF || message->msg_style == PAM_PROMPT_ECHO_ON;
 | 
			
		||||
		if (!ok) goto fail;
 | 
			
		||||
 | 
			
		||||
		qCDebug(logPam) << delegate << "got new message message:" << msgString
 | 
			
		||||
		                << "messageChanged:" << messageChanged << "isError:" << isError
 | 
			
		||||
		                << "responseRequired" << responseRequired;
 | 
			
		||||
		if (type == PamIpcEvent::Exit) {
 | 
			
		||||
			auto code = PamIpcExitCode::OtherError;
 | 
			
		||||
 | 
			
		||||
		delegate->hasResponse = false;
 | 
			
		||||
		emit delegate->message(msgString, messageChanged, isError, responseRequired);
 | 
			
		||||
			ok = this->pipes.readBytes(
 | 
			
		||||
			    reinterpret_cast<char*>(&code), // NOLINT
 | 
			
		||||
			    sizeof(PamIpcExitCode)
 | 
			
		||||
			);
 | 
			
		||||
 | 
			
		||||
		{
 | 
			
		||||
			auto locker = QMutexLocker(&delegate->wakeMutex);
 | 
			
		||||
			if (!ok) goto fail;
 | 
			
		||||
 | 
			
		||||
			if (delegate->mAbort) {
 | 
			
		||||
				free(responses); // NOLINT
 | 
			
		||||
				return PAM_ERROR_MSG;
 | 
			
		||||
			qCDebug(logPam) << "Subprocess exited with code" << static_cast<int>(code);
 | 
			
		||||
 | 
			
		||||
			switch (code) {
 | 
			
		||||
			case PamIpcExitCode::Success: emit this->completed(PamResult::Success); break;
 | 
			
		||||
			case PamIpcExitCode::AuthFailed: emit this->completed(PamResult::Failed); break;
 | 
			
		||||
			case PamIpcExitCode::StartFailed: emit this->error(PamError::StartFailed); break;
 | 
			
		||||
			case PamIpcExitCode::MaxTries: emit this->completed(PamResult::MaxTries); break;
 | 
			
		||||
			case PamIpcExitCode::PamError: emit this->error(PamError::TryAuthFailed); break;
 | 
			
		||||
			case PamIpcExitCode::OtherError: emit this->error(PamError::InternalError); break;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (responseRequired) {
 | 
			
		||||
				if (!delegate->hasResponse) {
 | 
			
		||||
					delegate->waker.wait(locker.mutex());
 | 
			
		||||
			waitpid(this->childPid, nullptr, 0);
 | 
			
		||||
			this->childPid = 0;
 | 
			
		||||
		} else if (type == PamIpcEvent::Request) {
 | 
			
		||||
			PamIpcRequestFlags flags {};
 | 
			
		||||
 | 
			
		||||
					if (delegate->mAbort) {
 | 
			
		||||
						free(responses); // NOLINT
 | 
			
		||||
						return PAM_ERROR_MSG;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			ok = this->pipes.readBytes(
 | 
			
		||||
			    reinterpret_cast<char*>(&flags), // NOLINT
 | 
			
		||||
			    sizeof(PamIpcRequestFlags)
 | 
			
		||||
			);
 | 
			
		||||
 | 
			
		||||
				if (!delegate->hasResponse) {
 | 
			
		||||
					qCCritical(logPam
 | 
			
		||||
					) << "Pam conversation requires response and does not have one. This should not happen.";
 | 
			
		||||
				}
 | 
			
		||||
			if (!ok) goto fail;
 | 
			
		||||
 | 
			
		||||
				response.resp = strdup(delegate->response.toStdString().c_str()); // NOLINT (include error)
 | 
			
		||||
			}
 | 
			
		||||
			auto message = this->pipes.readString(&ok);
 | 
			
		||||
 | 
			
		||||
			if (!ok) goto fail;
 | 
			
		||||
 | 
			
		||||
			this->message(
 | 
			
		||||
			    QString::fromUtf8(message),
 | 
			
		||||
			    /*flags.echo*/ true,
 | 
			
		||||
			    flags.error,
 | 
			
		||||
			    flags.responseRequired
 | 
			
		||||
			);
 | 
			
		||||
		} else {
 | 
			
		||||
			qCCritical(logPam) << "Unexpected message from subprocess.";
 | 
			
		||||
			goto fail;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return;
 | 
			
		||||
 | 
			
		||||
	*responseArray = responses;
 | 
			
		||||
	return PAM_SUCCESS;
 | 
			
		||||
fail:
 | 
			
		||||
	qCCritical(logPam) << "Failed to read subprocess request.";
 | 
			
		||||
	this->internalError();
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,13 +2,16 @@
 | 
			
		|||
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
#include <qmutex.h>
 | 
			
		||||
#include <qloggingcategory.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qqmlintegration.h>
 | 
			
		||||
#include <qthread.h>
 | 
			
		||||
#include <qsocketnotifier.h>
 | 
			
		||||
#include <qtclasshelpermacros.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qwaitcondition.h>
 | 
			
		||||
#include <security/pam_appl.h>
 | 
			
		||||
 | 
			
		||||
#include "ipc.hpp"
 | 
			
		||||
 | 
			
		||||
Q_DECLARE_LOGGING_CATEGORY(logPam);
 | 
			
		||||
 | 
			
		||||
/// The result of an authentication.
 | 
			
		||||
class PamResult: public QObject {
 | 
			
		||||
| 
						 | 
				
			
			@ -26,10 +29,6 @@ public:
 | 
			
		|||
		Error = 2,
 | 
			
		||||
		/// The authentication method ran out of tries and should not be used again.
 | 
			
		||||
		MaxTries = 3,
 | 
			
		||||
		// The account has expired.
 | 
			
		||||
		// Expired  4,
 | 
			
		||||
		// Permission denied.
 | 
			
		||||
		// PermissionDenied  5,
 | 
			
		||||
	};
 | 
			
		||||
	Q_ENUM(Enum);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -44,52 +43,56 @@ class PamError: public QObject {
 | 
			
		|||
 | 
			
		||||
public:
 | 
			
		||||
	enum Enum {
 | 
			
		||||
		/// Failed to initiate the pam connection.
 | 
			
		||||
		ConnectionFailed = 1,
 | 
			
		||||
		/// Failed to start the pam session.
 | 
			
		||||
		StartFailed = 1,
 | 
			
		||||
		/// Failed to try to authenticate the user.
 | 
			
		||||
		/// This is not the same as the user failing to authenticate.
 | 
			
		||||
		TryAuthFailed = 2,
 | 
			
		||||
		/// An error occurred inside quickshell's pam interface.
 | 
			
		||||
		InternalError = 3,
 | 
			
		||||
	};
 | 
			
		||||
	Q_ENUM(Enum);
 | 
			
		||||
 | 
			
		||||
	Q_INVOKABLE static QString toString(PamError::Enum value);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class PamConversation: public QThread {
 | 
			
		||||
// PAM has no way to abort a running module except when it sends a message,
 | 
			
		||||
// meaning aborts for things like fingerprint scanners
 | 
			
		||||
// and hardware keys don't actually work without aborting the process...
 | 
			
		||||
// so we have a subprocess.
 | 
			
		||||
class PamConversation: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit PamConversation(QString config, QString configDir, QString user)
 | 
			
		||||
	    : config(std::move(config))
 | 
			
		||||
	    , configDir(std::move(configDir))
 | 
			
		||||
	    , user(std::move(user)) {}
 | 
			
		||||
	explicit PamConversation(QObject* parent): QObject(parent) {}
 | 
			
		||||
	~PamConversation() override;
 | 
			
		||||
	Q_DISABLE_COPY_MOVE(PamConversation);
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	void run() override;
 | 
			
		||||
	void start(const QString& configDir, const QString& config, const QString& user);
 | 
			
		||||
 | 
			
		||||
	void abort();
 | 
			
		||||
	void respond(QString response);
 | 
			
		||||
	void respond(const QString& response);
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void completed(PamResult::Enum result);
 | 
			
		||||
	void error(PamError::Enum error);
 | 
			
		||||
	void message(QString message, bool messageChanged, bool isError, bool responseRequired);
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	void onMessage();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	static int conversation(
 | 
			
		||||
	    int msgCount,
 | 
			
		||||
	    const pam_message** msgArray,
 | 
			
		||||
	    pam_response** responseArray,
 | 
			
		||||
	    void* appdata
 | 
			
		||||
	static pid_t createSubprocess(
 | 
			
		||||
	    PamIpcPipes* pipes,
 | 
			
		||||
	    const QString& configDir,
 | 
			
		||||
	    const QString& config,
 | 
			
		||||
	    const QString& user
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	QString config;
 | 
			
		||||
	QString configDir;
 | 
			
		||||
	QString user;
 | 
			
		||||
	void internalError();
 | 
			
		||||
 | 
			
		||||
	QMutex wakeMutex;
 | 
			
		||||
	QWaitCondition waker;
 | 
			
		||||
	bool mAbort = false;
 | 
			
		||||
	bool hasResponse = false;
 | 
			
		||||
	QString response;
 | 
			
		||||
	pid_t childPid = 0;
 | 
			
		||||
	PamIpcPipes pipes;
 | 
			
		||||
	QSocketNotifier notifier {QSocketNotifier::Read};
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										69
									
								
								src/services/pam/ipc.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/services/pam/ipc.cpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,69 @@
 | 
			
		|||
#include "ipc.hpp"
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
#include <unistd.h>
 | 
			
		||||
 | 
			
		||||
PamIpcPipes::~PamIpcPipes() {
 | 
			
		||||
	if (this->fdIn != 0) close(this->fdIn);
 | 
			
		||||
	if (this->fdOut != 0) close(this->fdOut);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool PamIpcPipes::readBytes(char* buffer, size_t length) const {
 | 
			
		||||
	size_t i = 0;
 | 
			
		||||
 | 
			
		||||
	while (i < length) {
 | 
			
		||||
		auto count = read(this->fdIn, buffer + i, length - i); // NOLINT
 | 
			
		||||
 | 
			
		||||
		if (count == -1 || count == 0) {
 | 
			
		||||
			return false;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		i += count;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool PamIpcPipes::writeBytes(const char* buffer, size_t length) const {
 | 
			
		||||
	size_t i = 0;
 | 
			
		||||
	while (i < length) {
 | 
			
		||||
		auto count = write(this->fdOut, buffer + i, length - i); // NOLINT
 | 
			
		||||
 | 
			
		||||
		if (count == -1 || count == 0) {
 | 
			
		||||
			return false;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		i += count;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::string PamIpcPipes::readString(bool* ok) const {
 | 
			
		||||
	if (ok != nullptr) *ok = false;
 | 
			
		||||
 | 
			
		||||
	uint32_t length = 0;
 | 
			
		||||
	if (!this->readBytes(reinterpret_cast<char*>(&length), sizeof(length))) { // NOLINT
 | 
			
		||||
		return "";
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	char data[length]; // NOLINT
 | 
			
		||||
	if (!this->readBytes(data, length)) {
 | 
			
		||||
		return "";
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (ok != nullptr) *ok = true;
 | 
			
		||||
 | 
			
		||||
	return std::string(data, length);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool PamIpcPipes::writeString(const std::string& string) const {
 | 
			
		||||
	uint32_t length = string.length();
 | 
			
		||||
	if (!this->writeBytes(reinterpret_cast<char*>(&length), sizeof(length))) { // NOLINT
 | 
			
		||||
		return false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return this->writeBytes(string.data(), string.length());
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										43
									
								
								src/services/pam/ipc.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/services/pam/ipc.hpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,43 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
#include <qtclasshelpermacros.h>
 | 
			
		||||
 | 
			
		||||
enum class PamIpcEvent : uint8_t {
 | 
			
		||||
	Request,
 | 
			
		||||
	Exit,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum class PamIpcExitCode : uint8_t {
 | 
			
		||||
	Success,
 | 
			
		||||
	StartFailed,
 | 
			
		||||
	AuthFailed,
 | 
			
		||||
	MaxTries,
 | 
			
		||||
	PamError,
 | 
			
		||||
	OtherError,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct PamIpcRequestFlags {
 | 
			
		||||
	bool echo;
 | 
			
		||||
	bool error;
 | 
			
		||||
	bool responseRequired;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class PamIpcPipes {
 | 
			
		||||
public:
 | 
			
		||||
	explicit PamIpcPipes() = default;
 | 
			
		||||
	explicit PamIpcPipes(int fdIn, int fdOut): fdIn(fdIn), fdOut(fdOut) {}
 | 
			
		||||
	~PamIpcPipes();
 | 
			
		||||
	Q_DISABLE_COPY_MOVE(PamIpcPipes);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] bool readBytes(char* buffer, size_t length) const;
 | 
			
		||||
	[[nodiscard]] bool writeBytes(const char* buffer, size_t length) const;
 | 
			
		||||
	[[nodiscard]] std::string readString(bool* ok) const;
 | 
			
		||||
	[[nodiscard]] bool writeString(const std::string& string) const;
 | 
			
		||||
 | 
			
		||||
	int fdIn = 0;
 | 
			
		||||
	int fdOut = 0;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -13,12 +13,6 @@
 | 
			
		|||
 | 
			
		||||
#include "conversation.hpp"
 | 
			
		||||
 | 
			
		||||
PamContext::~PamContext() {
 | 
			
		||||
	if (this->conversation != nullptr && this->conversation->isRunning()) {
 | 
			
		||||
		this->conversation->abort();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PamContext::componentComplete() {
 | 
			
		||||
	this->postInit = true;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -91,12 +85,12 @@ void PamContext::startConversation() {
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->conversation = new PamConversation(this->mConfig, this->mConfigDirectory, user);
 | 
			
		||||
	this->conversation = new PamConversation(this);
 | 
			
		||||
	QObject::connect(this->conversation, &PamConversation::completed, this, &PamContext::onCompleted);
 | 
			
		||||
	QObject::connect(this->conversation, &PamConversation::error, this, &PamContext::onError);
 | 
			
		||||
	QObject::connect(this->conversation, &PamConversation::message, this, &PamContext::onMessage);
 | 
			
		||||
	emit this->activeChanged();
 | 
			
		||||
	this->conversation->start();
 | 
			
		||||
	this->conversation->start(this->mConfigDirectory, this->mConfig, user);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PamContext::abortConversation() {
 | 
			
		||||
| 
						 | 
				
			
			@ -104,7 +98,7 @@ void PamContext::abortConversation() {
 | 
			
		|||
	this->mTargetActive = false;
 | 
			
		||||
 | 
			
		||||
	QObject::disconnect(this->conversation, nullptr, this, nullptr);
 | 
			
		||||
	if (this->conversation->isRunning()) this->conversation->abort();
 | 
			
		||||
	this->conversation->deleteLater();
 | 
			
		||||
	this->conversation = nullptr;
 | 
			
		||||
	emit this->activeChanged();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -124,9 +118,9 @@ void PamContext::abortConversation() {
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PamContext::respond(QString response) {
 | 
			
		||||
void PamContext::respond(const QString& response) {
 | 
			
		||||
	if (this->isActive() && this->mIsResponseRequired) {
 | 
			
		||||
		this->conversation->respond(std::move(response));
 | 
			
		||||
		this->conversation->respond(response);
 | 
			
		||||
	} else {
 | 
			
		||||
		qWarning() << "PamContext response was ignored as this context does not require one.";
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -51,8 +51,6 @@ class PamContext
 | 
			
		|||
 | 
			
		||||
public:
 | 
			
		||||
	explicit PamContext(QObject* parent = nullptr): QObject(parent) {}
 | 
			
		||||
	~PamContext() override;
 | 
			
		||||
	Q_DISABLE_COPY_MOVE(PamContext);
 | 
			
		||||
 | 
			
		||||
	void classBegin() override {}
 | 
			
		||||
	void componentComplete() override;
 | 
			
		||||
| 
						 | 
				
			
			@ -69,7 +67,7 @@ public:
 | 
			
		|||
	/// Respond to pam.
 | 
			
		||||
	///
 | 
			
		||||
	/// May not be called unless `responseRequired` is true.
 | 
			
		||||
	Q_INVOKABLE void respond(QString response);
 | 
			
		||||
	Q_INVOKABLE void respond(const QString& response);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] bool isActive() const;
 | 
			
		||||
	void setActive(bool active);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										209
									
								
								src/services/pam/subprocess.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								src/services/pam/subprocess.cpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,209 @@
 | 
			
		|||
#include "subprocess.hpp"
 | 
			
		||||
#include <array>
 | 
			
		||||
#include <ostream>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
#include <qlogging.h>
 | 
			
		||||
#include <qloggingcategory.h>
 | 
			
		||||
#include <qstring.h>
 | 
			
		||||
#include <sched.h>
 | 
			
		||||
#include <security/_pam_types.h>
 | 
			
		||||
#include <security/pam_appl.h>
 | 
			
		||||
#include <unistd.h>
 | 
			
		||||
 | 
			
		||||
#include "conversation.hpp"
 | 
			
		||||
#include "ipc.hpp"
 | 
			
		||||
 | 
			
		||||
pid_t PamConversation::createSubprocess(
 | 
			
		||||
    PamIpcPipes* pipes,
 | 
			
		||||
    const QString& configDir,
 | 
			
		||||
    const QString& config,
 | 
			
		||||
    const QString& user
 | 
			
		||||
) {
 | 
			
		||||
	auto toSubprocess = std::array<int, 2>();
 | 
			
		||||
	auto fromSubprocess = std::array<int, 2>();
 | 
			
		||||
 | 
			
		||||
	if (pipe(toSubprocess.data()) == -1 || pipe(fromSubprocess.data()) == -1) {
 | 
			
		||||
		qCDebug(logPam) << "Failed to create pipes for subprocess.";
 | 
			
		||||
		return 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	auto* configDirF = strdup(configDir.toStdString().c_str()); // NOLINT (include)
 | 
			
		||||
	auto* configF = strdup(config.toStdString().c_str());       // NOLINT (include)
 | 
			
		||||
	auto* userF = strdup(user.toStdString().c_str());           // NOLINT (include)
 | 
			
		||||
	auto log = logPam().isDebugEnabled();
 | 
			
		||||
 | 
			
		||||
	auto pid = fork();
 | 
			
		||||
 | 
			
		||||
	if (pid < 0) {
 | 
			
		||||
		qCDebug(logPam) << "Failed to fork for subprocess.";
 | 
			
		||||
	} else if (pid == 0) {
 | 
			
		||||
		close(toSubprocess[1]);   // close w
 | 
			
		||||
		close(fromSubprocess[0]); // close r
 | 
			
		||||
 | 
			
		||||
		{
 | 
			
		||||
			auto subprocess = PamSubprocess(log, toSubprocess[0], fromSubprocess[1]);
 | 
			
		||||
			auto code = subprocess.exec(configDirF, configF, userF);
 | 
			
		||||
			subprocess.sendCode(code);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		free(configDirF); // NOLINT
 | 
			
		||||
		free(configF);    // NOLINT
 | 
			
		||||
		free(userF);      // NOLINT
 | 
			
		||||
 | 
			
		||||
		// do not do cleanup that may affect the parent
 | 
			
		||||
		_exit(0);
 | 
			
		||||
	} else {
 | 
			
		||||
		close(toSubprocess[0]);   // close r
 | 
			
		||||
		close(fromSubprocess[1]); // close w
 | 
			
		||||
 | 
			
		||||
		pipes->fdIn = fromSubprocess[0];
 | 
			
		||||
		pipes->fdOut = toSubprocess[1];
 | 
			
		||||
 | 
			
		||||
		free(configDirF); // NOLINT
 | 
			
		||||
		free(configF);    // NOLINT
 | 
			
		||||
		free(userF);      // NOLINT
 | 
			
		||||
 | 
			
		||||
		return pid;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return -1; // should never happen but lint
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PamIpcExitCode PamSubprocess::exec(const char* configDir, const char* config, const char* user) {
 | 
			
		||||
	logIf(this->log) << "Waiting for parent confirmation..." << std::endl;
 | 
			
		||||
 | 
			
		||||
	auto conv = pam_conv {
 | 
			
		||||
	    .conv = &PamSubprocess::conversation,
 | 
			
		||||
	    .appdata_ptr = this,
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	pam_handle_t* handle = nullptr;
 | 
			
		||||
 | 
			
		||||
	logIf(this->log) << "Starting pam session for user \"" << user << "\" with config \"" << config
 | 
			
		||||
	                 << "\" in dir \"" << configDir << "\"" << std::endl;
 | 
			
		||||
 | 
			
		||||
	auto result = pam_start_confdir(config, user, &conv, configDir, &handle);
 | 
			
		||||
 | 
			
		||||
	if (result != PAM_SUCCESS) {
 | 
			
		||||
		logIf(true) << "Unable to start pam conversation with error \"" << pam_strerror(handle, result)
 | 
			
		||||
		            << "\" (code " << result << ")" << std::endl;
 | 
			
		||||
		return PamIpcExitCode::StartFailed;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	result = pam_authenticate(handle, 0);
 | 
			
		||||
	PamIpcExitCode code = PamIpcExitCode::OtherError;
 | 
			
		||||
 | 
			
		||||
	switch (result) {
 | 
			
		||||
	case PAM_SUCCESS:
 | 
			
		||||
		logIf(this->log) << "Authenticated successfully." << std::endl;
 | 
			
		||||
		code = PamIpcExitCode::Success;
 | 
			
		||||
		break;
 | 
			
		||||
	case PAM_AUTH_ERR:
 | 
			
		||||
		logIf(this->log) << "Failed to authenticate." << std::endl;
 | 
			
		||||
		code = PamIpcExitCode::AuthFailed;
 | 
			
		||||
		break;
 | 
			
		||||
	case PAM_MAXTRIES:
 | 
			
		||||
		logIf(this->log) << "Failed to authenticate due to hitting max tries." << std::endl;
 | 
			
		||||
		code = PamIpcExitCode::MaxTries;
 | 
			
		||||
		break;
 | 
			
		||||
	default:
 | 
			
		||||
		logIf(true) << "Error while authenticating: \"" << pam_strerror(handle, result) << "\" (code "
 | 
			
		||||
		            << result << ")" << std::endl;
 | 
			
		||||
		code = PamIpcExitCode::PamError;
 | 
			
		||||
		break;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	result = pam_end(handle, result);
 | 
			
		||||
	if (result != PAM_SUCCESS) {
 | 
			
		||||
		logIf(true) << "Error in pam_end: \"" << pam_strerror(handle, result) << "\" (code " << result
 | 
			
		||||
		            << ")" << std::endl;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return code;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PamSubprocess::sendCode(PamIpcExitCode code) {
 | 
			
		||||
	{
 | 
			
		||||
		auto eventType = PamIpcEvent::Exit;
 | 
			
		||||
		auto ok = this->pipes.writeBytes(
 | 
			
		||||
		    reinterpret_cast<char*>(&eventType), // NOLINT
 | 
			
		||||
		    sizeof(PamIpcEvent)
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		if (!ok) goto fail;
 | 
			
		||||
 | 
			
		||||
		ok = this->pipes.writeBytes(
 | 
			
		||||
		    reinterpret_cast<char*>(&code), // NOLINT
 | 
			
		||||
		    sizeof(PamIpcExitCode)
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		if (!ok) goto fail;
 | 
			
		||||
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
fail:
 | 
			
		||||
	_exit(1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int PamSubprocess::conversation(
 | 
			
		||||
    int msgCount,
 | 
			
		||||
    const pam_message** msgArray,
 | 
			
		||||
    pam_response** responseArray,
 | 
			
		||||
    void* appdata
 | 
			
		||||
) {
 | 
			
		||||
	auto* delegate = static_cast<PamSubprocess*>(appdata);
 | 
			
		||||
 | 
			
		||||
	// freed by libc so must be alloc'd by it.
 | 
			
		||||
	auto* responses = static_cast<pam_response*>(calloc(msgCount, sizeof(pam_response))); // NOLINT
 | 
			
		||||
 | 
			
		||||
	for (auto i = 0; i < msgCount; i++) {
 | 
			
		||||
		const auto* message = msgArray[i]; // NOLINT
 | 
			
		||||
		auto& response = responses[i];     // NOLINT
 | 
			
		||||
 | 
			
		||||
		auto msgString = std::string(message->msg);
 | 
			
		||||
		auto req = PamIpcRequestFlags {
 | 
			
		||||
		    .echo = message->msg_style != PAM_PROMPT_ECHO_OFF,
 | 
			
		||||
		    .error = message->msg_style == PAM_ERROR_MSG,
 | 
			
		||||
		    .responseRequired =
 | 
			
		||||
		        message->msg_style == PAM_PROMPT_ECHO_OFF || message->msg_style == PAM_PROMPT_ECHO_ON,
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		logIf(delegate->log) << "Relaying pam message: \"" << msgString << "\" echo: " << req.echo
 | 
			
		||||
		                     << " error: " << req.error << " responseRequired: " << req.responseRequired
 | 
			
		||||
		                     << std::endl;
 | 
			
		||||
 | 
			
		||||
		auto eventType = PamIpcEvent::Request;
 | 
			
		||||
		auto ok = delegate->pipes.writeBytes(
 | 
			
		||||
		    reinterpret_cast<char*>(&eventType), // NOLINT
 | 
			
		||||
		    sizeof(PamIpcEvent)
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		if (!ok) goto fail;
 | 
			
		||||
 | 
			
		||||
		ok = delegate->pipes.writeBytes(
 | 
			
		||||
		    reinterpret_cast<const char*>(&req), // NOLINT
 | 
			
		||||
		    sizeof(PamIpcRequestFlags)
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		if (!ok) goto fail;
 | 
			
		||||
		if (!delegate->pipes.writeString(msgString)) goto fail;
 | 
			
		||||
 | 
			
		||||
		if (req.responseRequired) {
 | 
			
		||||
			auto ok = false;
 | 
			
		||||
			auto resp = delegate->pipes.readString(&ok);
 | 
			
		||||
			if (!ok) _exit(static_cast<int>(PamIpcExitCode::OtherError));
 | 
			
		||||
			logIf(delegate->log) << "Got response for request.";
 | 
			
		||||
 | 
			
		||||
			response.resp = strdup(resp.c_str()); // NOLINT (include)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	*responseArray = responses;
 | 
			
		||||
	return PAM_SUCCESS;
 | 
			
		||||
 | 
			
		||||
fail:
 | 
			
		||||
	free(responseArray); // NOLINT
 | 
			
		||||
	_exit(1);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										31
									
								
								src/services/pam/subprocess.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/services/pam/subprocess.hpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <iostream>
 | 
			
		||||
 | 
			
		||||
#include <security/pam_appl.h>
 | 
			
		||||
 | 
			
		||||
#include "ipc.hpp"
 | 
			
		||||
 | 
			
		||||
// endls are intentional as it makes debugging much easier when the buffer actually flushes.
 | 
			
		||||
// NOLINTBEGIN
 | 
			
		||||
#define logIf(log)                                                                                 \
 | 
			
		||||
	if (log) std::cout << "quickshell.service.pam.subprocess: "
 | 
			
		||||
// NOLINTEND
 | 
			
		||||
 | 
			
		||||
class PamSubprocess {
 | 
			
		||||
public:
 | 
			
		||||
	explicit PamSubprocess(bool log, int fdIn, int fdOut): log(log), pipes(fdIn, fdOut) {}
 | 
			
		||||
	PamIpcExitCode exec(const char* configDir, const char* config, const char* user);
 | 
			
		||||
	void sendCode(PamIpcExitCode code);
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	static int conversation(
 | 
			
		||||
	    int msgCount,
 | 
			
		||||
	    const pam_message** msgArray,
 | 
			
		||||
	    pam_response** responseArray,
 | 
			
		||||
	    void* appdata
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	bool log;
 | 
			
		||||
	PamIpcPipes pipes;
 | 
			
		||||
};
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue