diff --git a/bin/isolated-functions.sh b/bin/isolated-functions.sh
index cbd93fce97..d4248366d7 100644
--- a/bin/isolated-functions.sh
+++ b/bin/isolated-functions.sh
@@ -8,6 +8,15 @@ if ___eapi_has_version_functions; then
 	source "${PORTAGE_BIN_PATH}/eapi7-ver-funcs.sh" || exit 1
 fi
 
+if [[ -v PORTAGE_EBUILD_EXTRA_SOURCE ]]; then
+	source "${PORTAGE_EBUILD_EXTRA_SOURCE}" || exit 1
+	# We deliberately do not unset PORTABE_EBUILD_EXTRA_SOURCE, so
+	# that it keeps being exported in the environment of this
+	# process and its child processes. There, for example portage
+	# helper like doins, can pick it up and set the PMS variables
+	# (usually by sourcing isolated-functions.sh).
+fi
+
 # We need this next line for "die" and "assert". It expands
 # It _must_ preceed all the calls to die and assert.
 shopt -s expand_aliases
diff --git a/bin/phase-functions.sh b/bin/phase-functions.sh
index d9b524c1a2..9d528bf3bf 100644
--- a/bin/phase-functions.sh
+++ b/bin/phase-functions.sh
@@ -20,6 +20,7 @@ PORTAGE_READONLY_VARS="D EBUILD EBUILD_PHASE EBUILD_PHASE_FUNC \
 	PORTAGE_BUILD_USER PORTAGE_BUNZIP2_COMMAND \
 	PORTAGE_BZIP2_COMMAND PORTAGE_COLORMAP PORTAGE_CONFIGROOT \
 	PORTAGE_DEBUG PORTAGE_DEPCACHEDIR PORTAGE_EBUILD_EXIT_FILE \
+	PORTAGE_EBUILD_EXTRA_SOURCE \
 	PORTAGE_ECLASS_LOCATIONS PORTAGE_EXPLICIT_INHERIT \
 	PORTAGE_GID PORTAGE_GRPNAME PORTAGE_INST_GID PORTAGE_INST_UID \
 	PORTAGE_INTERNAL_CALLER PORTAGE_IPC_DAEMON PORTAGE_IUSE PORTAGE_LOG_FILE \
@@ -962,6 +963,18 @@ __ebuild_main() {
 	export EBUILD_MASTER_PID=${BASHPID:-$(__bashpid)}
 	trap 'exit 1' SIGTERM
 
+	if [[ -v PORTAGE_EBUILD_EXTRA_SOURCE &&
+			  ${PORTAGE_EBUILD_EXTRA_SOURCE} != ${T}/* ]]; then
+		# Cleanup PORTAGE_EBUILD_EXTRA_SOURCE after ebuild.sh
+		# (__ebuild_main()) finishes if PORTAGE_EBUILD_EXTRA_SOURCE is
+		# not under T.
+		__portage_ebuild_exit() {
+			rm "${PORTAGE_EBUILD_EXTRA_SOURCE}" ||
+				die "failed to remove PORTAGE_EBUILD_EXTRA_SOURCE file (${PORTAGE_EBUILD_EXTRA_SOURCE})"
+		}
+		trap __portage_ebuild_exit EXIT
+	fi
+
 	# A reasonable default for ${S}
 	[[ -z ${S} ]] && export S=${WORKDIR}/${P}
 
diff --git a/cnf/make.globals b/cnf/make.globals
index 94eac65684..dba24c73c7 100644
--- a/cnf/make.globals
+++ b/cnf/make.globals
@@ -81,7 +81,7 @@ FEATURES="assume-digests binpkg-docompress binpkg-dostrip binpkg-logs
           network-sandbox news parallel-fetch pkgdir-index-trusted pid-sandbox
           preserve-libs protect-owned qa-unresolved-soname-deps sandbox strict
           unknown-features-warn unmerge-logs unmerge-orphans userfetch
-          userpriv usersandbox usersync"
+          userpriv usersandbox usersync export-pms-vars"
 
 # Ignore file collisions in /lib/modules since files inside this directory
 # are never unmerged, and therefore collisions must be ignored in order for
diff --git a/lib/_emerge/EbuildMetadataPhase.py b/lib/_emerge/EbuildMetadataPhase.py
index 54177840c7..b100c4de6e 100644
--- a/lib/_emerge/EbuildMetadataPhase.py
+++ b/lib/_emerge/EbuildMetadataPhase.py
@@ -36,6 +36,7 @@ class EbuildMetadataPhase(SubProcess):
         "repo_path",
         "settings",
         "deallocate_config",
+        "portage_ebuild_extra_source",
         "write_auxdb",
     ) + (
         "_eapi",
@@ -165,6 +166,10 @@ def _async_start_done(self, future):
             self.cancel()
             self._was_cancelled()
 
+        self.portage_ebuild_extra_source = self.settings.get(
+            "PORTAGE_EBUILD_EXTRA_SOURCE"
+        )
+
         if self.deallocate_config is not None and not self.deallocate_config.done():
             self.deallocate_config.set_result(self.settings)
 
@@ -191,6 +196,8 @@ def _unregister(self):
         if self._files is not None:
             self.scheduler.remove_reader(self._files.ebuild)
         SubProcess._unregister(self)
+        if self.portage_ebuild_extra_source:
+            os.unlink(self.portage_ebuild_extra_source)
 
     def _async_waitpid_cb(self, *args, **kwargs):
         """
diff --git a/lib/portage/const.py b/lib/portage/const.py
index c9a71009a7..2049f51311 100644
--- a/lib/portage/const.py
+++ b/lib/portage/const.py
@@ -183,6 +183,7 @@
         "distlocks",
         "downgrade-backup",
         "ebuild-locks",
+        "export-pms-vars",
         "fail-clean",
         "fakeroot",
         "fixlafiles",
diff --git a/lib/portage/eapi.py b/lib/portage/eapi.py
index bdf60ef101..e0a9fc61a4 100644
--- a/lib/portage/eapi.py
+++ b/lib/portage/eapi.py
@@ -48,6 +48,10 @@ def eapi_supports_prefix(eapi: str) -> bool:
     return _get_eapi_attrs(eapi).prefix
 
 
+def eapi_exports_pms_vars(eapi: str) -> bool:
+    return _get_eapi_attrs(eapi).exports_pms_vars
+
+
 def eapi_exports_AA(eapi: str) -> bool:
     return _get_eapi_attrs(eapi).exports_AA
 
@@ -157,6 +161,7 @@ def eapi_has_sysroot(eapi: str) -> bool:
         "exports_ECLASSDIR",
         "exports_KV",
         "exports_merge_type",
+        "exports_pms_vars",
         "exports_PORTDIR",
         "exports_replace_vars",
         "feature_flag_test",
@@ -197,6 +202,7 @@ class Eapi:
         "6",
         "7",
         "8",
+        "9",
     )
 
     _eapi_val: int = -1
@@ -235,6 +241,7 @@ def _get_eapi_attrs(eapi_str: Optional[str]) -> _eapi_attrs:
             exports_ECLASSDIR=False,
             exports_KV=False,
             exports_merge_type=True,
+            exports_pms_vars=True,
             exports_PORTDIR=True,
             exports_replace_vars=True,
             feature_flag_test=False,
@@ -274,6 +281,7 @@ def _get_eapi_attrs(eapi_str: Optional[str]) -> _eapi_attrs:
             exports_ECLASSDIR=eapi <= Eapi("6"),
             exports_KV=eapi <= Eapi("3"),
             exports_merge_type=eapi >= Eapi("4"),
+            exports_pms_vars=eapi <= Eapi("8"),
             exports_PORTDIR=eapi <= Eapi("6"),
             exports_replace_vars=eapi >= Eapi("4"),
             feature_flag_test=False,
diff --git a/lib/portage/package/ebuild/doebuild.py b/lib/portage/package/ebuild/doebuild.py
index 54831ccdae..6465505d68 100644
--- a/lib/portage/package/ebuild/doebuild.py
+++ b/lib/portage/package/ebuild/doebuild.py
@@ -84,6 +84,7 @@
 from portage.eapi import (
     eapi_exports_KV,
     eapi_exports_merge_type,
+    eapi_exports_pms_vars,
     eapi_exports_replace_vars,
     eapi_has_required_use,
     eapi_has_src_prepare_and_src_configure,
@@ -189,6 +190,57 @@
     "RESTRICT",
 )
 
+# The following is a set of PMS § 11.1 and § 7.4 without
+# - TMPDIR
+# - HOME
+# because these variables are often assumed to be exported and
+# therefore consumed by child processes.
+_unexported_pms_vars = frozenset(
+    # fmt: off
+    [
+        # PMS § 11.1 Defined Variables
+        "P",
+        "PF",
+        "PN",
+        "CATEGORY",
+        "PV",
+        "PR",
+        "PVR",
+        "A",
+        "AA",
+        "FILESDIR",
+        "DISTDIR",
+        "WORKDIR",
+        "S",
+        "PORTDIR",
+        "ECLASSDIR",
+        "ROOT",
+        "EROOT",
+        "SYSROOT",
+        "ESYSROOT",
+        "BROOT",
+        "T",
+#        "TMPDIR",        # EXPORTED: often assumed to be exported and available to child processes
+#        "HOME",          # EXPORTED: often assumed to be exported and available to child processes
+        "EPREFIX",
+        "D",
+        "ED",
+        "DESTTREE",
+        "INSDESTTREE",
+        "EBUILD_PHASE",
+        "EBUILD_PHASE_FUNC",
+        "KV",
+        "MERGE_TYPE",
+        "REPLACING_VERSIONS",
+        "REPLACED_BY_VERSION",
+        # PMS § 7.4 Magic Ebuild-defined Variables
+        "ECLASS",
+        "INHERITED",
+        "DEFINED_PHASES",
+    ]
+    # fmt: on
+)
+
 
 def _doebuild_spawn(phase, settings, actionmap=None, **kwargs):
     """
@@ -1922,6 +1974,8 @@ def __init__(self, mydb):
 # XXX This would be to replace getstatusoutput completely.
 # XXX Issue: cannot block execution. Deadlock condition.
 
+_emerge_tmpdir = None
+
 
 def spawn(
     mystring,
@@ -2133,9 +2187,85 @@ def spawn(
         logname_backup = mysettings.configdict["env"].get("LOGNAME")
         mysettings.configdict["env"]["LOGNAME"] = logname
 
+    eapi = mysettings["EAPI"]
+
+    unexported_env_vars = None
+    if "export-pms-vars" not in mysettings.features or not eapi_exports_pms_vars(eapi):
+        unexported_env_vars = _unexported_pms_vars
+
+    if unexported_env_vars:
+        # Starting with EAPI 9 (or if FEATURES="-export-pms-vars"),
+        # PMS variables should not longer be exported.
+
+        phase = mysettings.get("EBUILD_PHASE")
+        is_pms_ebuild_phase = phase in _phase_func_map.keys()
+        # 'None' phase is MiscFunctionsProcess, e.g., where the qa checks run
+        is_ebuild_phase_with_t = phase in [None, "package", "instprep"]
+        # Copy the environment since we are removing the PMS variables from it.
+        env = mysettings.environ().copy()
+
+        # There are three cases to consider when it comes to managing
+        # the life cycle of the PORTAGE_EBUILD_EXTRA_SOURCE file we
+        # are going to create now.
+        # A) phase function with T available (potentially unprivileged)
+        # B) privileged phase function
+        # C) phase=depend (potentially unprivileged with T unavailable and __ebuild_main not called)
+        #
+        # Case A is easy to solve, since we shove
+        # PORTAGE_EBUILD_EXTRA_SOURCE simply in T which will
+        # eventually get claned any way.
+        # Case B requires that we use an extra temp directory to store
+        # PORTAGE_EBUILD_EXTRA_SOURCE. We install an EXIT trap in
+        # __ebuild_main() that will remove PORTAGE_EBUILD_EXTRA_SOURCE
+        # once ebuild.sh finishes.
+        # Case C requires that delete PORTAGE_EBUILD_EXTRA_SOURCE once
+        # the depend phase for that ebuild finished. This is done in
+        # EbuildMetadataPhase._unregister().
+        if is_pms_ebuild_phase or is_ebuild_phase_with_t:  # case A
+            t = env["T"]
+            ebuild_extra_source_path = os.path.join(
+                t, f".portage-ebuild-extra-source-{phase}"
+            )
+        else:  # case B and C
+            global _emerge_tmpdir
+            if _emerge_tmpdir is None:
+                _emerge_tmpdir = tempfile.mkdtemp(
+                    prefix=f"portage-tmpdir-{portage.getpid()}-"
+                )
+                os.chmod(_emerge_tmpdir, 0o1775)
+                os.chown(_emerge_tmpdir, -1, int(portage_gid))
+                portage.process.atexit_register(shutil.rmtree, _emerge_tmpdir)
+            ebuild_extra_source_fd, ebuild_extra_source_path = tempfile.mkstemp(
+                prefix=f"portage-ebuild-extra-source-{phase}-",
+                dir=_emerge_tmpdir,
+            )
+            try:
+                # Make sure that the file can be writen by us (done below)
+                # and that it is world readable.
+                os.fchmod(ebuild_extra_source_fd, 0o644)
+            finally:
+                os.close(ebuild_extra_source_fd)
+            if phase == "depend":  # case C
+                # The file will be deleted by EbuildMetadataPhase._unregister()
+                mysettings["PORTAGE_EBUILD_EXTRA_SOURCE"] = ebuild_extra_source_path
+
+        with open(ebuild_extra_source_path, mode="w") as f:
+            for var_name in unexported_env_vars:
+                var_value = mysettings.environ().get(var_name)
+                if var_value is None:
+                    continue
+                quoted_var_value = shlex.quote(var_value)
+                f.write(f"{var_name}={quoted_var_value}\n")
+                del env[var_name]
+
+        env["PORTAGE_EBUILD_EXTRA_SOURCE"] = str(ebuild_extra_source_path)
+    else:
+        # Pre EAPI 9 behavior, all PMS variables are simply exported into the ebuild's environment.
+        env = mysettings.environ()
+
     try:
         if keywords.get("returnpid") or keywords.get("returnproc"):
-            return spawn_func(mystring, env=mysettings.environ(), **keywords)
+            return spawn_func(mystring, env=env, **keywords)
 
         proc = EbuildSpawnProcess(
             background=False,