diff --git a/docs/markdown/Installing.md b/docs/markdown/Installing.md index 0bc9a4744..2d18c178f 100644 --- a/docs/markdown/Installing.md +++ b/docs/markdown/Installing.md @@ -110,7 +110,9 @@ targets as root. This results in various bad behaviors due to build outputs and ninja internal files being owned by root. Running `meson install` is preferred for several reasons. It can rebuild out of -date targets and then re-invoke itself as root. +date targets and then re-invoke itself as root. *(since 1.1.0)* Additionally, +running `sudo meson install` will drop permissions and rebuild out of date +targets as the original user, not as root. *(since 1.1.0)* Re-invoking as root will try to guess the user's preferred method for re-running commands as root. The order of precedence is: sudo, doas, pkexec diff --git a/docs/markdown/snippets/meson_install_drop_privs.md b/docs/markdown/snippets/meson_install_drop_privs.md new file mode 100644 index 000000000..e08dfc000 --- /dev/null +++ b/docs/markdown/snippets/meson_install_drop_privs.md @@ -0,0 +1,16 @@ +## `sudo meson install` now drops privileges when rebuilding targets + +It is common to install projects using sudo, which should not affect build +outputs but simply install the results. Unfortunately, since the ninja backend +updates a state file when run, it's not safe to run ninja as root at all. + +It has always been possible to carefully build with: + +``` +ninja && sudo meson install --no-rebuild +``` + +Meson now tries to be extra safe as a general solution. `sudo meson install` +will attempt to rebuild, but has learned to run `ninja` as the original +(pre-sudo or pre-doas) user, ensuring that build outputs are generated/compiled +as non-root. diff --git a/mesonbuild/minstall.py b/mesonbuild/minstall.py index ab797b5ee..a64596087 100644 --- a/mesonbuild/minstall.py +++ b/mesonbuild/minstall.py @@ -42,7 +42,7 @@ if T.TYPE_CHECKING: ExecutableSerialisation, InstallDataBase, InstallEmptyDir, InstallSymlinkData, TargetInstallData ) - from .mesonlib import FileMode + from .mesonlib import FileMode, EnvironOrDict try: from typing import Protocol @@ -753,7 +753,41 @@ def rebuild_all(wd: str) -> bool: print("Can't find ninja, can't rebuild test.") return False - ret = subprocess.run(ninja + ['-C', wd]).returncode + def drop_privileges() -> T.Tuple[T.Optional[EnvironOrDict], T.Optional[T.Callable[[], None]]]: + if not is_windows() and os.geteuid() == 0: + import pwd + env = os.environ.copy() + + if os.environ.get('SUDO_USER') is not None: + orig_user = env.pop('SUDO_USER') + orig_uid = env.pop('SUDO_UID', 0) + orig_gid = env.pop('SUDO_GID', 0) + homedir = pwd.getpwuid(int(orig_uid)).pw_dir + elif os.environ.get('DOAS_USER') is not None: + orig_user = env.pop('DOAS_USER') + pwdata = pwd.getpwnam(orig_user) + orig_uid = pwdata.pw_uid + orig_gid = pwdata.pw_gid + homedir = pwdata.pw_dir + else: + return None, None + + env['USER'] = orig_user + env['HOME'] = homedir + + def wrapped() -> None: + print(f'Dropping privileges to {orig_user!r} before running ninja...') + if orig_gid is not None: + os.setgid(int(orig_gid)) + if orig_uid is not None: + os.setuid(int(orig_uid)) + + return env, wrapped + else: + return None, None + + env, preexec_fn = drop_privileges() + ret = subprocess.run(ninja + ['-C', wd], env=env, preexec_fn=preexec_fn).returncode if ret != 0: print(f'Could not rebuild {wd}') return False