diff --git a/anaconda.py b/anaconda.py index 0ef9112d638f..dd0be21e22e9 100755 --- a/anaconda.py +++ b/anaconda.py @@ -144,6 +144,12 @@ def setup_environment(): if "LD_PRELOAD" in os.environ: del os.environ["LD_PRELOAD"] + # Go ahead and set $WAYLAND_DISPLAY whether we're going to use Wayland or not + if "WAYLAND_DISPLAY" in os.environ: + flags.preexisting_wayland = True + else: + os.environ["WAYLAND_DISPLAY"] = constants.WAYLAND_SOCKET_NAME + # Go ahead and set $DISPLAY whether we're going to use X or not if "DISPLAY" in os.environ: flags.preexisting_x11 = True @@ -307,10 +313,11 @@ def setup_environment(): except pid.PidFileError as e: log.error("Unable to create %s, exiting", pidfile.filename) - # If we had a $DISPLAY at start and zenity is available, we may be - # running in a live environment and we can display an error dialog. + # If we had a Wayland/X11 display at start and zenity is available, we may + # be running in a live environment and we can display an error dialog. # Otherwise just print an error. - if flags.preexisting_x11 and os.access("/usr/bin/zenity", os.X_OK): + preexisting_graphics = flags.preexisting_wayland or flags.preexisting_x11 + if preexisting_graphics and os.access("/usr/bin/zenity", os.X_OK): # The module-level _() calls are ok here because the language may # be set from the live environment in this case, and anaconda's # language setup hasn't happened yet. diff --git a/anaconda.spec.in b/anaconda.spec.in index 52e94f092270..c2054bae39e3 100644 --- a/anaconda.spec.in +++ b/anaconda.spec.in @@ -245,14 +245,13 @@ Requires: zram-generator # needed for proper driver disk support - if RPMs must be installed, a repo is needed Requires: createrepo_c # Display stuff moved from lorax templates -Requires: xorg-x11-drivers -Requires: xorg-x11-server-Xorg Requires: dbus-x11 Requires: gsettings-desktop-schemas Requires: nm-connection-editor Requires: librsvg2 Requires: gnome-kiosk Requires: brltty +Requires: python3-pam # dependencies for rpm-ostree payload module Requires: rpm-ostree >= %{rpmostreever} Requires: ostree @@ -392,6 +391,7 @@ rm -rf \ %{_sbindir}/anaconda %{_sbindir}/handle-sshpw %{_datadir}/anaconda +%{_sysconfdir}/pam.d/anaconda %{_prefix}/libexec/anaconda %exclude %{_datadir}/anaconda/gnome %exclude %{_datadir}/anaconda/pixmaps diff --git a/configure.ac b/configure.ac index 4c9724e8d578..bdbd416e3929 100644 --- a/configure.ac +++ b/configure.ac @@ -112,6 +112,7 @@ AC_CONFIG_FILES([Makefile data/systemd/Makefile data/dbus/Makefile data/gtk-4.0/Makefile + data/pam/Makefile data/window-manager/Makefile data/window-manager/config/Makefile po/Makefile diff --git a/data/Makefile.am b/data/Makefile.am index d08f3005a0ec..9c5cff7766b6 100644 --- a/data/Makefile.am +++ b/data/Makefile.am @@ -15,7 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . -SUBDIRS = command-stubs gtk-4.0 liveinst systemd pixmaps window-manager dbus conf.d profile.d +SUBDIRS = command-stubs gtk-4.0 liveinst systemd pam pixmaps window-manager dbus conf.d profile.d CLEANFILES = *~ diff --git a/data/pam/Makefile.am b/data/pam/Makefile.am new file mode 100644 index 000000000000..97e6657be152 --- /dev/null +++ b/data/pam/Makefile.am @@ -0,0 +1,21 @@ +# Copyright (C) 2024 Neal Gompa. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +CLEANFILES = *~ + +pamdir = $(sysconfdir)/pam.d +dist_pam_DATA = anaconda + +MAINTAINERCLEANFILES = Makefile.in diff --git a/data/pam/anaconda b/data/pam/anaconda new file mode 100644 index 000000000000..af8758d3b792 --- /dev/null +++ b/data/pam/anaconda @@ -0,0 +1,8 @@ +#%PAM-1.0 +auth sufficient pam_permit.so +account sufficient pam_permit.so +password sufficient pam_permit.so +session required pam_loginuid.so +-session optional pam_keyinit.so revoke +-session optional pam_limits.so +session required pam_systemd.so \ No newline at end of file diff --git a/data/systemd/anaconda.service b/data/systemd/anaconda.service index a80c6bb7075c..0a3580b89ada 100644 --- a/data/systemd/anaconda.service +++ b/data/systemd/anaconda.service @@ -5,6 +5,6 @@ Wants=anaconda-noshell.service [Service] Type=forking -Environment=HOME=/root MALLOC_CHECK_=2 MALLOC_PERTURB_=204 PATH=/usr/bin:/bin:/sbin:/usr/sbin:/mnt/sysimage/bin:/mnt/sysimage/usr/bin:/mnt/sysimage/usr/sbin:/mnt/sysimage/sbin LANG=en_US.UTF-8 GDK_BACKEND=x11 XDG_RUNTIME_DIR=/tmp GIO_USE_VFS=local +Environment=HOME=/root MALLOC_CHECK_=2 MALLOC_PERTURB_=204 PATH=/usr/bin:/bin:/sbin:/usr/sbin:/mnt/sysimage/bin:/mnt/sysimage/usr/bin:/mnt/sysimage/usr/sbin:/mnt/sysimage/sbin LANG=en_US.UTF-8 GDK_BACKEND=wayland XDG_RUNTIME_DIR=/run/user/0 GIO_USE_VFS=local WorkingDirectory=/root ExecStart=/usr/bin/tmux -u -f /usr/share/anaconda/tmux.conf start diff --git a/pyanaconda/core/constants.py b/pyanaconda/core/constants.py index bf163eb9f74b..9f2cb95680ab 100644 --- a/pyanaconda/core/constants.py +++ b/pyanaconda/core/constants.py @@ -267,6 +267,9 @@ class SecretStatus(Enum): IPMI_ABORTED = 0x9 # installation finished unsuccessfully, due to some non-exn error IPMI_FAILED = 0xA # installation hit an exception +# Wayland socket name to use +WAYLAND_SOCKET_NAME = "wl-sysinstall-0" + # X display number to use X_DISPLAY_NUMBER = 1 diff --git a/pyanaconda/core/util.py b/pyanaconda/core/util.py index 537201995d0b..c509f751e02c 100644 --- a/pyanaconda/core/util.py +++ b/pyanaconda/core/util.py @@ -255,7 +255,7 @@ def sigusr1_preexec(): log.debug("Exception handler test suspended to prevent accidental activation by " "delayed Xorg start. Next SIGUSR1 will be handled as delayed Xorg start.") # Raise an exception to notify the caller that things went wrong. This affects - # particularly pyanaconda.display.do_startup_x11_actions(), where the window manager + # particularly pyanaconda.display.do_startup_wl_actions(), where the window manager # is started immediately after this. The WM would just wait forever. raise TimeoutError("Timeout trying to start %s" % argv[0]) diff --git a/pyanaconda/display.py b/pyanaconda/display.py index 601558f5b0ff..b7b8a0c591f0 100644 --- a/pyanaconda/display.py +++ b/pyanaconda/display.py @@ -20,7 +20,6 @@ # Author(s): Martin Kolman # import os -import subprocess import time import textwrap import pkgutil @@ -52,9 +51,9 @@ log = get_module_logger(__name__) stdout_log = get_stdout_logger() -X_TIMEOUT_ADVICE = \ +WAYLAND_TIMEOUT_ADVICE = \ "Do not load the stage2 image over a slow network link.\n" \ - "Wait longer for the X server startup with the inst.xtimeout= boot option." \ + "Wait longer for Wayland startup with the inst.xtimeout= boot option." \ "The default is 60 seconds.\n" \ "Load the stage2 image into memory with the rd.live.ram boot option to decrease access " \ "time.\n" \ @@ -172,22 +171,7 @@ def check_vnc_can_be_started(anaconda): return vnc_startup_possible, error_messages -# X11 - -def start_x11(xtimeout): - """Start the X server for the Anaconda GUI.""" - - # Start Xorg and wait for it become ready - util.startX(["Xorg", "-br", "-logfile", "/tmp/X.log", - ":%s" % constants.X_DISPLAY_NUMBER, "vt6", "-s", "1440", "-ac", - "-nolisten", "tcp", "-dpi", "96", - "-noreset"], - output_redirect=subprocess.DEVNULL, timeout=xtimeout) - - -# function to handle X startup special issues for anaconda - -def do_startup_x11_actions(): +def do_startup_wl_actions(timeout): """Start the window manager. When window manager actually connects to the X server is unknowable, but @@ -209,14 +193,35 @@ def do_startup_x11_actions(): xdg_config_dirs = datadir + ':' + os.environ['XDG_CONFIG_DIRS'] os.environ['XDG_CONFIG_DIRS'] = xdg_config_dirs - def x11_preexec(): + os.environ["XDG_SESSION_TYPE"] = "wayland" + + def wl_preexec(): # to set GUI subprocess SIGINT handler signal.signal(signal.SIGINT, signal.SIG_IGN) - childproc = util.startProgram(["gnome-kiosk", "--display", ":1", "--sm-disable", "--x11"], - env_add={'XDG_DATA_DIRS': xdg_data_dirs}, - preexec_fn=x11_preexec) - WatchProcesses.watch_process(childproc, "gnome-kiosk") + argv = ["/usr/libexec/anaconda/run-in-new-session", + "--user", "root", + "--service", "anaconda", + "--vt", "6", + "--session-type", "wayland", + "--session-class", "user", + "gnome-kiosk", "--sm-disable", "--wayland", "--no-x11", + "--wayland-display", constants.WAYLAND_SOCKET_NAME] + + childproc = util.startProgram(argv, env_add={'XDG_DATA_DIRS': xdg_data_dirs}, + preexec_fn=wl_preexec) + WatchProcesses.watch_process(childproc, argv[0]) + + for _i in range(0, int(timeout / 0.1)): + wl_socket_path = os.path.join(os.getenv("XDG_RUNTIME_DIR"), constants.WAYLAND_SOCKET_NAME) + if os.path.exists(wl_socket_path): + return + + time.sleep(0.1) + + WatchProcesses.unwatch_process(childproc) + childproc.terminate() + raise TimeoutError("Timeout trying to start gnome-kiosk") def set_resolution(runres): @@ -324,17 +329,6 @@ def setup_display(anaconda, options): for error_message in vnc_error_messages: stdout_log.warning(error_message) - # Should we try to start Xorg? - want_x = anaconda.gui_mode and not (flags.preexisting_x11 or flags.usevnc) - - # Is Xorg is actually available? - if want_x and not os.access("/usr/bin/Xorg", os.X_OK): - stdout_log.warning(_("Graphical installation is not available. " - "Starting text mode.")) - time.sleep(2) - anaconda.display_mode = constants.DisplayModes.TUI - want_x = False - if anaconda.tui_mode and flags.vncquestion: # we prefer vnc over text mode, so ask about that message = _("Text mode provides a limited set of installation " @@ -350,18 +344,17 @@ def setup_display(anaconda, options): startup_utils.check_memory(anaconda, options) # check_memory may have changed the display mode - want_x = want_x and (anaconda.gui_mode) - if want_x: + want_gui = anaconda.gui_mode and not (flags.preexisting_wayland or flags.usevnc) + if want_gui: try: - start_x11(xtimeout) - do_startup_x11_actions() + do_startup_wl_actions(xtimeout) except TimeoutError as e: - log.warning("X startup failed: %s", e) - print("\nX did not start in the expected time, falling back to text mode. There are " - "multiple ways to avoid this issue:") + log.warning("Wayland startup failed: %s", e) + print("\nWayland did not start in the expected time, falling back to text mode. " + "There are multiple ways to avoid this issue:") wrapper = textwrap.TextWrapper(initial_indent=" * ", subsequent_indent=" ", width=os.get_terminal_size().columns - 3) - for line in X_TIMEOUT_ADVICE.split("\n"): + for line in WAYLAND_TIMEOUT_ADVICE.split("\n"): print(wrapper.fill(line)) util.vtActivate(1) anaconda.display_mode = constants.DisplayModes.TUI @@ -369,8 +362,8 @@ def setup_display(anaconda, options): time.sleep(2) except (OSError, RuntimeError) as e: - log.warning("X or window manager startup failed: %s", e) - print("\nX or window manager startup failed, falling back to text mode.") + log.warning("Wayland startup failed: %s", e) + print("\nWayland startup failed, falling back to text mode.") util.vtActivate(1) anaconda.display_mode = constants.DisplayModes.TUI anaconda.gui_startup_failed = True @@ -394,7 +387,7 @@ def on_mutter_ready(observer): # if they want us to use VNC do that now if anaconda.gui_mode and flags.usevnc: vnc_server.startServer() - do_startup_x11_actions() + do_startup_wl_actions(xtimeout) # with X running we can initialize the UI interface anaconda.initialize_interface() diff --git a/pyanaconda/flags.py b/pyanaconda/flags.py index 000fd5df876a..b70aa7def8e3 100644 --- a/pyanaconda/flags.py +++ b/pyanaconda/flags.py @@ -35,6 +35,7 @@ def __init__(self): self.__dict__['_in_init'] = True self.usevnc = False self.vncquestion = True + self.preexisting_wayland = False self.preexisting_x11 = False self.automatedInstall = False self.eject = True diff --git a/scripts/Makefile.am b/scripts/Makefile.am index 456be0070cc8..275e1dffedb7 100644 --- a/scripts/Makefile.am +++ b/scripts/Makefile.am @@ -16,7 +16,8 @@ # along with this program. If not, see . scriptsdir = $(libexecdir)/$(PACKAGE_NAME) -dist_scripts_SCRIPTS = run-anaconda anaconda-pre-log-gen log-capture start-module apply-updates +dist_scripts_SCRIPTS = run-anaconda anaconda-pre-log-gen log-capture start-module apply-updates \ + run-in-new-session dist_noinst_SCRIPTS = makeupdates makebumpver diff --git a/scripts/makeupdates b/scripts/makeupdates index a6273590cfbc..3b4f34f381c3 100755 --- a/scripts/makeupdates +++ b/scripts/makeupdates @@ -35,7 +35,8 @@ RPM_RELEASE_DIR_TEMPLATE = "for_%s" SITE_PACKAGES_PATH = "./usr/lib64/python3.12/site-packages/" # Anaconda scripts that should be installed into the libexec folder -LIBEXEC_SCRIPTS = ["log-capture", "start-module", "apply-updates", "anaconda-pre-log-gen"] +LIBEXEC_SCRIPTS = ["log-capture", "start-module", "apply-updates", "anaconda-pre-log-gen", + "run-in-new-session"] # Anaconda scripts that should be installed into /usr/bin USR_BIN_SCRIPTS = ["anaconda-disable-nm-ibft-plugin", "anaconda-nm-disable-autocons"] diff --git a/scripts/run-in-new-session b/scripts/run-in-new-session new file mode 100755 index 000000000000..80aa673eb37c --- /dev/null +++ b/scripts/run-in-new-session @@ -0,0 +1,231 @@ +#!/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 , Ray Strode +# + +import argparse +import fcntl +import pam +import pwd +import os +import signal +import struct +import subprocess +import sys + +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: + os.dup2(tty_input.fileno(), 0) + os.dup2(tty_output.fileno(), 1) + os.dup2(tty_output.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()