/*
 * SPDX-FileCopyrightText: 1991 - 1994, Julianne Frances Haugh
 * SPDX-FileCopyrightText: 1996 - 2000, Marek Michałkiewicz
 * SPDX-FileCopyrightText: 2000 - 2006, Tomasz Kłoczko
 * SPDX-FileCopyrightText: 2007 - 2009, Nicolas François
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

#include "config.h"

#include <stddef.h>
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <fcntl.h>
#include <unistd.h>
#include <utmpx.h>

#include "atoi/getnum.h"
#include "defines.h"
#include "fs/readlink/readlinknul.h"
#include "io/fgets/fgets.h"
#include "prototypes.h"
#ifdef ENABLE_SUBIDS
#include "subordinateio.h"
#endif				/* ENABLE_SUBIDS */
#include "shadowlog.h"
#include "string/sprintf/stprintf.h"
#include "string/strcmp/streq.h"
#include "string/strcmp/strneq.h"
#include "string/strcmp/strprefix.h"

#undef NDEBUG
#include <assert.h>


#ifdef __linux__
static int check_status (const char *name, uid_t uid, pid_t pid, pid_t tid);
static int user_busy_processes (const char *name, uid_t uid);
#else				/* !__linux__ */
static int user_busy_utmp (const char *name);
#endif				/* !__linux__ */

/*
 * user_busy - check if a user is currently running processes
 */
int user_busy (const char *name, uid_t uid)
{
	/* There are no standard ways to get the list of processes.
	 * An option could be to run an external tool (ps).
	 */
#ifdef __linux__
	/* On Linux, directly parse /proc */
	return user_busy_processes (name, uid);
#else				/* !__linux__ */
	/* If we cannot rely on /proc, check if there is a record in utmp
	 * indicating that the user is still logged in */
	return user_busy_utmp (name);
#endif				/* !__linux__ */
}


#ifndef __linux__
static int
user_busy_utmp(const char *name)
{
	struct utmpx  *utent;

	setutxent();
	while (NULL != (utent = getutxent()))
	{
		if (utent->ut_type != USER_PROCESS) {
			continue;
		}
		if (!strneq_a(utent->ut_user, name))
			continue;

		if (kill (utent->ut_pid, 0) != 0) {
			continue;
		}

		fprintf (log_get_logfd(),
		         _("%s: user %s is currently logged in\n"),
		         log_get_progname(), name);
		return 1;
	}

	return 0;
}
#endif				/* !__linux__ */


#ifdef __linux__
#ifdef ENABLE_SUBIDS
#define in_parentuid_range(uid) ((uid) >= parentuid && (uid) < parentuid + range)
static int different_namespace (pid_t pid, pid_t tid)
{
	/* 41: /proc/xxxxxxxxxx/task/xxxxxxxxxx/ns/user + \0 */
	char     path[41];
	char     buf[512], buf2[512];

	stprintf_a(path, "/proc/%d/task/%d/ns/user", pid, tid);

	if (readlinknul_a(path, buf) == -1)
		return 0;

	if (readlinknul_a("/proc/self/ns/user", buf2) == -1)
		return 0;

	if (streq(buf, buf2))
		return 0; /* same namespace */

	return 1;
}
#endif                          /* ENABLE_SUBIDS */


static int check_status (const char *name, uid_t uid, pid_t pid, pid_t tid)
{
	/* 40: /proc/xxxxxxxxxx/task/xxxxxxxxxx/status + \0 */
	char  status[40];
	char  line[1024];
	FILE  *sfile;

	stprintf_a(status, "/proc/%d/task/%d/status", pid, tid);

	sfile = fopen (status, "r");
	if (NULL == sfile) {
		return 0;
	}
	while (fgets_a(line, sfile) != NULL) {
		if (strprefix(line, "Uid:\t")) {
			unsigned long ruid, euid, suid;

			assert (uid == (unsigned long) uid);
			(void) fclose (sfile);
			if (sscanf (line,
			            "Uid:\t%lu\t%lu\t%lu\n",
			            &ruid, &euid, &suid) == 3) {
				if (   (ruid == (unsigned long) uid)
				    || (euid == (unsigned long) uid)
				    || (suid == (unsigned long) uid) ) {
					return 1;
				}
#ifdef ENABLE_SUBIDS
				if (    different_namespace (pid, tid)
				     && (   have_sub_uids(name, ruid, 1)
				         || have_sub_uids(name, euid, 1)
				         || have_sub_uids(name, suid, 1))
				   ) {
					return 1;
				}
#endif				/* ENABLE_SUBIDS */
			} else {
				/* Ignore errors. This is just a best effort. */
			}
			return 0;
		}
	}
	(void) fclose (sfile);
	return 0;
}

static int user_busy_processes (const char *name, uid_t uid)
{
	DIR            *proc;
	DIR            *task_dir;
	char           *tmp_d_name;
	/* 22: /proc/xxxxxxxxxx/task + \0 */
	char           task_path[22];
	char           root_path[22];
	pid_t          pid;
	struct stat    sbroot;
	struct stat    sbroot_process;
	struct dirent  *ent;

#ifdef ENABLE_SUBIDS
	sub_uid_open (O_RDONLY);
#endif				/* ENABLE_SUBIDS */

	proc = opendir ("/proc");
	if (proc == NULL) {
		perror ("opendir /proc");
#ifdef ENABLE_SUBIDS
		sub_uid_close(true);
#endif
		return 0;
	}
	if (stat ("/", &sbroot) != 0) {
		perror ("stat (\"/\")");
		(void) closedir (proc);
#ifdef ENABLE_SUBIDS
		sub_uid_close(true);
#endif
		return 0;
	}

	while (NULL != (ent = readdir(proc))) {
		tmp_d_name = ent->d_name;
		/*
		 * Ingo Molnar's patch introducing NPTL for 2.4 hides
		 * threads in the /proc directory by prepending a period.
		 * This patch is applied by default in some RedHat
		 * kernels.
		 */
		if (   streq(tmp_d_name, ".")
		    || streq(tmp_d_name, "..")) {
			continue;
		}
		tmp_d_name = strprefix(tmp_d_name, ".") ?: tmp_d_name;

		/* Check if this is a valid PID */
		if (get_pid(tmp_d_name, &pid) == -1) {
			continue;
		}

		/* Check if the process is in our chroot */
		stprintf_a(root_path, "/proc/%d/root", pid);
		if (stat (root_path, &sbroot_process) != 0) {
			continue;
		}
		if (   (sbroot.st_dev != sbroot_process.st_dev)
		    || (sbroot.st_ino != sbroot_process.st_ino)) {
			continue;
		}

		if (check_status (name, uid, pid, pid) != 0) {
			(void) closedir (proc);
#ifdef ENABLE_SUBIDS
			sub_uid_close(true);
#endif
			fprintf (log_get_logfd(),
			         _("%s: user %s is currently used by process %d\n"),
			         log_get_progname(), name, pid);
			return 1;
		}

		stprintf_a(task_path, "/proc/%d/task", pid);
		task_dir = opendir (task_path);
		if (task_dir != NULL) {
			while (NULL != (ent = readdir(task_dir))) {
				pid_t tid;
				if (get_pid(ent->d_name, &tid) == -1) {
					continue;
				}
				if (tid == pid) {
					continue;
				}
				if (check_status (name, uid, pid, tid) != 0) {
					(void) closedir (proc);
					(void) closedir (task_dir);
#ifdef ENABLE_SUBIDS
					sub_uid_close(true);
#endif
					fprintf (log_get_logfd(),
					         _("%s: user %s is currently used by process %d\n"),
					         log_get_progname(), name, pid);
					return 1;
				}
			}
			(void) closedir (task_dir);
		} else {
			/* Ignore errors. This is just a best effort */
		}
	}

	(void) closedir (proc);
#ifdef ENABLE_SUBIDS
	sub_uid_close(true);
#endif				/* ENABLE_SUBIDS */
	return 0;
}
#endif				/* __linux__ */
