anaconda/anaconda-40.22.3.13/scripts/run-in-new-session
2024-11-14 21:39:56 -08:00

235 lines
8.1 KiB
Python
Executable file

#!/usr/bin/python3
#
# Copyright (C) 2024 Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details. You should have received a copy of the
# GNU General Public License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
# Author(s): Martin Kolman <mkolman@redhat.com>, Ray Strode <rstrode@redhat.com>
#
import argparse
import fcntl
import pam
import pwd
import os
import signal
import struct
import subprocess
import sys
from systemd import journal
VT_GETSTATE = 0x5603
VT_ACTIVATE = 0x5606
VT_OPENQRY = 0x5600
VT_WAITACTIVE = 0x5607
TIOCSCTTY = 0x540E
def is_running_in_logind_session():
try:
with open('/proc/self/loginuid', 'r') as f:
loginuid = int(f.read().strip())
return loginuid != 0xFFFFFFFF
except Exception as e:
# pylint: disable-next=broad-exception-raised
raise Exception(f"Error reading /proc/self/loginuid: {e}") from e
def find_free_vt():
with open('/dev/tty0', 'w') as console:
result = fcntl.ioctl(console, VT_OPENQRY, struct.pack('i', 0))
vt = struct.unpack('i', result)[0]
return vt
def run_program_in_new_session(arguments, pam_environment, user, service,
tty_input, tty_output, vt):
pam_handle = pam.pam()
for key, value in pam_environment.items():
pam_handle.putenv(f'{key}={value}')
old_tty_input = os.fdopen(os.dup(0), 'r')
os.dup2(os.dup(tty_input.fileno()), 0)
if not pam_handle.authenticate(user, '', service=service, call_end=False):
# pylint: disable-next=broad-exception-raised
raise Exception("Authentication failed")
for key, value in pam_environment.items():
pam_handle.putenv(f'{key}={value}')
if pam_handle.open_session() != pam.PAM_SUCCESS:
# pylint: disable-next=broad-exception-raised
raise Exception("Failed to open PAM session")
session_environment = os.environ.copy()
session_environment.update(pam_handle.getenvlist())
os.dup2(old_tty_input.fileno(), 0)
user_info = pwd.getpwnam(user)
uid = user_info.pw_uid
gid = user_info.pw_gid
old_tty_output = os.fdopen(os.dup(2), 'w')
console = open("/dev/tty0", 'w')
try:
old_vt = 0
if vt:
vt_state = fcntl.ioctl(console, VT_GETSTATE, struct.pack('HHH', 0, 0, 0))
old_vt, _, _ = struct.unpack('HHH', vt_state)
except OSError as e:
print(f"Could not read current VT: {e}", file=old_tty_output)
pid = os.fork()
if pid == 0:
try:
os.setsid()
except OSError as e:
print(f"Could not create new pid session: {e}", file=old_tty_output)
try:
fcntl.ioctl(tty_output, TIOCSCTTY, 1)
except OSError as e:
print(f"Could not take control of tty: {e}", file=old_tty_output)
try:
fcntl.ioctl(console, VT_ACTIVATE, vt)
except OSError as e:
print(f"Could not change to VT {vt}: {e}", file=old_tty_output)
try:
fcntl.ioctl(console, VT_WAITACTIVE, vt)
except OSError as e:
print(f"Could not wait for VT {vt} to change: {e}", file=old_tty_output)
try:
# redirect output (both stodout & stderr) from the command to Journal
new_session_stdout_stream = journal.stream("run-in-new-session", priority=journal.LOG_INFO)
new_session_stderr_stream = journal.stream("run-in-new-session", priority=journal.LOG_ERR)
os.dup2(tty_input.fileno(), 0)
os.dup2(new_session_stdout_stream.fileno(), 1)
os.dup2(new_session_stderr_stream.fileno(), 2)
except OSError as e:
print(f"Could not set up standard i/o: {e}", file=old_tty_output)
try:
os.initgroups(user, gid)
os.setgid(gid)
os.setuid(uid)
except OSError as e:
print(f"Could not become user {user} (uid={uid}): {e}", file=old_tty_output)
try:
os.execvpe(arguments[0], arguments, session_environment)
except OSError as e:
print(f"Could not run program \"{' '.join(arguments)}\": {e}", file=old_tty_output)
os._exit(1)
try:
(_, exit_code) = os.waitpid(pid, 0)
except KeyboardInterrupt:
os.kill(pid, signal.SIGINT)
except OSError as e:
print(f"Could not wait for program to finish: {e}", file=old_tty_output)
try:
if old_vt:
fcntl.ioctl(console, VT_ACTIVATE, old_vt)
fcntl.ioctl(console, VT_WAITACTIVE, old_vt)
except OSError as e:
print(f"Could not change VTs back: {e}", file=old_tty_output)
if os.WIFEXITED(exit_code):
exit_code = os.WEXITSTATUS(exit_code)
else:
os.kill(os.getpid(), os.WTERMSIG(exit_code))
old_tty_output.close()
console.close()
if pam_handle.close_session() != pam.PAM_SUCCESS:
# pylint: disable-next=broad-exception-raised
raise Exception("Failed to close PAM session")
pam_handle.end()
return exit_code
def main():
parser = argparse.ArgumentParser(description='Run a program in a PAM session with specific'
' environment variables as a specified user.')
parser.add_argument('--user', default='root', help='Username for which to run the program')
parser.add_argument('--service', default='su-l', help='PAM service to use')
parser.add_argument('--session-type', default='x11', help='e.g., x11, wayland, or tty')
parser.add_argument('--session-class', default='user', help='e.g., greeter or user')
parser.add_argument('--session-desktop', help='desktop file id associated with session, e.g.'
' gnome, gnome-classic, gnome-wayland')
parser.add_argument('--vt', help='VT to run on')
args, remaining_args = parser.parse_known_args()
if not remaining_args:
remaining_args = ["bash", "-l"]
if not args.vt:
vt = find_free_vt()
print(f'Using VT {vt}')
else:
vt = int(args.vt)
if is_running_in_logind_session():
program = ['systemd-run',
f'--unit=run-in-new-session-{os.getpid()}.service',
'--pipe',
'--wait',
'-d']
program += [sys.executable]
program += sys.argv
subprocess.run(program, check=False)
return
try:
tty_path = f'/dev/tty{vt}'
tty_input = open(tty_path, 'r')
tty_output = open(tty_path, 'w')
pam_environment = {}
pam_environment['XDG_SEAT'] = "seat0"
pam_environment['XDG_SESSION_TYPE'] = args.session_type
pam_environment['XDG_SESSION_CLASS'] = args.session_class
pam_environment['XDG_SESSION_DESKTOP'] = args.session_desktop
pam_environment['XDG_VTNR'] = vt
try:
result = run_program_in_new_session(remaining_args, pam_environment, args.user,
args.service, tty_input, tty_output, vt)
except OSError as e:
# pylint: disable-next=broad-exception-raised
raise Exception(f"Error running program \"{' '.join(remaining_args)}\": {e}") from e
tty_input.close()
tty_output.close()
sys.exit(result)
except OSError as e:
# pylint: disable-next=broad-exception-raised
raise Exception(f"Error opening tty associated with VT {vt}: {e}") from e
if __name__ == '__main__':
main()