From 8b6308b6c18095f0f48f85ae262719aedb9105da Mon Sep 17 00:00:00 2001
From: Nicholas Yang <nicholas.yang@suse.com>
Date: Wed, 13 Mar 2024 17:35:48 +0800
Subject: [PATCH 1/5] Fix: sh: pass env to child process explicitly
 (bsc#1205925)

A bug in python stardard library, python/cpython#100516, make
environment variables, `COLUMNS` and `ROWS` to be added at fork. These
environment variables may make some of the commands called by crmsh to
truncate its output even if those commands are not writing to the
terminal.

Passing argument `env` explicitly is a workaround for the python bug.
---
 crmsh/report/sh.py    |  1 +
 crmsh/sh.py           | 12 +++++++++---
 crmsh/user_of_host.py |  2 ++
 3 files changed, 12 insertions(+), 3 deletions(-)

diff --git a/crmsh/report/sh.py b/crmsh/report/sh.py
index 46f981a863..c3627fcfe1 100644
--- a/crmsh/report/sh.py
+++ b/crmsh/report/sh.py
@@ -69,6 +69,7 @@ def subprocess_run_without_input(self, cmd: str, **kwargs):
         return subprocess.run(
             args,
             input=cmd.encode('utf-8'),
+            env=os.environ,  # bsc#1205925
             **kwargs,
         )
 
diff --git a/crmsh/sh.py b/crmsh/sh.py
index 5b9c052621..16470e3a5d 100644
--- a/crmsh/sh.py
+++ b/crmsh/sh.py
@@ -150,12 +150,12 @@ def su_subprocess_run(
             )
         if not self.additional_environ:
             logger.debug('su_subprocess_run: %s, %s', args, kwargs)
-            return subprocess.run(args, **kwargs)
+            env = os.environ    # bsc#1205925
         else:
             logger.debug('su_subprocess_run: %s, env=%s, %s', args, self.additional_environ, kwargs)
             env = dict(os.environ)
             env.update(self.additional_environ)
-            return subprocess.run(args, env=env, **kwargs)
+        return subprocess.run(args, env=env, **kwargs)
 
     def get_rc_stdout_stderr_raw(self, user: typing.Optional[str], cmd: str, input: typing.Optional[bytes] = None):
         result = self.su_subprocess_run(
@@ -268,6 +268,7 @@ def subprocess_run_without_input(self, host: typing.Optional[str], user: str, cm
             return subprocess.run(
                 args,
                 input=cmd.encode('utf-8'),
+                env=os.environ,     # bsc#1205925
                 **kwargs,
             )
         else:
@@ -317,6 +318,7 @@ def subprocess_run_without_input(self, host: typing.Optional[str], user: typing.
                 return subprocess.run(
                     ['/bin/sh'],
                     input=cmd.encode('utf-8'),
+                    env=os.environ,  # bsc#1205925
                     **kwargs,
                 )
             else:
@@ -436,6 +438,7 @@ def get_stdout(cls, cmd, input_s=None, stderr_on=True, shell=True, raw=False):
             stdin=subprocess.PIPE,
             stdout=subprocess.PIPE,
             stderr=subprocess.DEVNULL if stderr_on else subprocess.PIPE,
+            env=os.environ,  # bsc#1205925
         )
         stdout_data, _ = proc.communicate(input_s)
         if raw:
@@ -457,7 +460,9 @@ def get_stdout_stderr(cls, cmd, input_s=None, shell=True, raw=False, no_reg=Fals
             shell=shell,
             stdin=input_s and subprocess.PIPE or None,
             stdout=subprocess.PIPE,
-            stderr=subprocess.PIPE)
+            stderr=subprocess.PIPE,
+            env=os.environ,  # bsc#1205925
+        )
         # will raise subprocess.TimeoutExpired if set timeout
         stdout_data, stderr_data = proc.communicate(input_s, timeout=timeout)
         if raw:
@@ -489,3 +494,4 @@ def subprocess_run_without_input(self, host: str, user: typing.Optional[str], cm
 
 def cluster_shell():
     return ClusterShell(LocalShell(), user_of_host.instance(), raise_ssh_error=True)
+
diff --git a/crmsh/user_of_host.py b/crmsh/user_of_host.py
index 4551dce978..9c990daec3 100644
--- a/crmsh/user_of_host.py
+++ b/crmsh/user_of_host.py
@@ -1,4 +1,5 @@
 import logging
+import os
 import socket
 import subprocess
 import typing
@@ -107,6 +108,7 @@ def _guess_user_for_ssh(host: str) -> typing.Tuple[str, str]:
             stdin=subprocess.DEVNULL,
             stdout=subprocess.DEVNULL,
             stderr=subprocess.DEVNULL,
+            env=os.environ,  # bsc#1205925
         )
         if rc == 0:
             user = userdir.getuser()

From b901b4ef236e84f1a56fd6b05fc796935a3dd242 Mon Sep 17 00:00:00 2001
From: Nicholas Yang <nicholas.yang@suse.com>
Date: Wed, 13 Mar 2024 17:55:52 +0800
Subject: [PATCH 2/5] Dev: unittest: update unit tests for previous changes

---
 test/unittests/test_sh.py | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/test/unittests/test_sh.py b/test/unittests/test_sh.py
index b3c0f0bb11..221820afd2 100644
--- a/test/unittests/test_sh.py
+++ b/test/unittests/test_sh.py
@@ -13,8 +13,9 @@ def setUp(self) -> None:
         self.local_shell.geteuid = mock.Mock(self.local_shell.geteuid)
         self.local_shell.hostname = mock.Mock(self.local_shell.hostname)
 
+    @mock.patch('os.environ')
     @mock.patch('subprocess.run')
-    def test_su_subprocess_run(self, mock_run: mock.MagicMock):
+    def test_su_subprocess_run(self, mock_run: mock.MagicMock, mock_environ: mock.MagicMock):
         self.local_shell.get_effective_user_name.return_value = 'root'
         self.local_shell.geteuid.return_value = 0
         self.local_shell.su_subprocess_run(
@@ -24,10 +25,12 @@ def test_su_subprocess_run(self, mock_run: mock.MagicMock):
         mock_run.assert_called_once_with(
             ['su', 'alice', '--login', '-s', '/bin/sh', '-c', 'foo'],
             input=b'bar',
+            env=mock_environ,
         )
 
+    @mock.patch('os.environ')
     @mock.patch('subprocess.run')
-    def test_su_subprocess_run_as_root(self, mock_run: mock.MagicMock):
+    def test_su_subprocess_run_as_root(self, mock_run: mock.MagicMock, mock_environ: mock.MagicMock):
         self.local_shell.get_effective_user_name.return_value = 'root'
         self.local_shell.geteuid.return_value = 0
         self.local_shell.su_subprocess_run(
@@ -37,6 +40,7 @@ def test_su_subprocess_run_as_root(self, mock_run: mock.MagicMock):
         mock_run.assert_called_once_with(
             ['/bin/sh', '-c', 'foo'],
             input=b'bar',
+            env=mock_environ,
         )
 
     @mock.patch('subprocess.run')
@@ -124,8 +128,9 @@ def test_subprocess_run_without_input_with_input_kwargs(self):
             )
         self.ssh_shell.local_shell.su_subprocess_run.assert_not_called()
 
+    @mock.patch('os.environ')
     @mock.patch('subprocess.run')
-    def test_subprocess_run_without_input_local(self, mock_run):
+    def test_subprocess_run_without_input_local(self, mock_run, mock_environ: mock.MagicMock):
         self.ssh_shell.subprocess_run_without_input(
             'node1', 'bob',
             'foo',
@@ -138,6 +143,7 @@ def test_subprocess_run_without_input_local(self, mock_run):
             input=b'foo',
             stdout=subprocess.PIPE,
             stderr=subprocess.PIPE,
+            env=mock_environ,
         )
 
 

From b604b9b9d42b315403117e2d2c6c351312151c25 Mon Sep 17 00:00:00 2001
From: Nicholas Yang <nicholas.yang@suse.com>
Date: Wed, 13 Mar 2024 17:49:05 +0800
Subject: [PATCH 3/5] Fix: term: unset env `COLUMNS` and `ROWS` (bsc#1205925)

A bug in python stardard library, python/cpython#100516, make
environment variables, `COLUMNS` and `ROWS` to be added at fork. These
environment variables may make some of the commands called by crmsh to
truncate its output even if those commands are not writing to the
terminal.

These variables is set when bash enables its `checkwinsize` option. As
crmsh is able to read terminal size with curses, these variables are
unneeded and may be out of sync with actual terminal size.
---
 crmsh/term.py | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/crmsh/term.py b/crmsh/term.py
index 1ac309dd53..675abd3917 100644
--- a/crmsh/term.py
+++ b/crmsh/term.py
@@ -1,6 +1,6 @@
 # Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic@suse.de>
 # See COPYING for license information.
-
+import os
 import sys
 import re
 # from: http://code.activestate.com/recipes/475116/
@@ -151,6 +151,7 @@ def init():
     from . import config
     if not _term_stream.isatty() and 'color-always' not in config.color.style:
         return
+    _ignore_environ()
     # Check the terminal type.  If we fail, then assume that the
     # terminal has no capabilities.
     try:
@@ -177,4 +178,12 @@ def is_color(s):
     return hasattr(colors, s.upper())
 
 
+def _ignore_environ():
+    """Ignore environment variable COLUMNS and ROWS"""
+    # See https://bugzilla.suse.com/show_bug.cgi?id=1205925
+    # and https://gitlab.com/procps-ng/procps/-/blob/c415fc86452c933716053a50ab1777a343190dcc/src/ps/global.c#L279
+    for name in ["COLUMNS", "ROWS"]:
+        if name in os.environ:
+            del os.environ[name]
+
 # vim:ts=4:sw=4:et:

From 8dde6dbb48582ed40e7099356f39a271a4e07cd8 Mon Sep 17 00:00:00 2001
From: Nicholas Yang <nicholas.yang@suse.com>
Date: Wed, 13 Mar 2024 17:55:26 +0800
Subject: [PATCH 4/5] Fix: pass env to child process explicitly (bsc#1205925)

A bug in python stardard library, python/cpython#100516, make
environment variables, `COLUMNS` and `ROWS` to be added at fork. These
environment variables may make some of the commands called by crmsh to
truncate its output even if those commands are not writing to the
terminal.

Passing argument `env` explicitly is a workaround for the python bug.
---
 crmsh/scripts.py | 10 ++++++++--
 crmsh/ui_node.py | 11 +++++++----
 crmsh/utils.py   | 31 +++++++++++++++++++++++++------
 crmsh/xmlutil.py |  6 +++++-
 4 files changed, 45 insertions(+), 13 deletions(-)

diff --git a/crmsh/scripts.py b/crmsh/scripts.py
index 7add6150c6..3b49c90ed4 100644
--- a/crmsh/scripts.py
+++ b/crmsh/scripts.py
@@ -1068,7 +1068,9 @@ def _check_control_persist():
         print((".EXT", cmd))
     cmd = subprocess.Popen(cmd,
                            stdout=subprocess.PIPE,
-                           stderr=subprocess.PIPE)
+                           stderr=subprocess.PIPE,
+                           env=os.environ,  # bsc#1205925
+                           )
     (out, err) = cmd.communicate()
     return "Bad configuration option" not in err
 
@@ -1138,7 +1140,11 @@ def _cleanup_local(workdir):
     if workdir and os.path.isdir(workdir):
         cleanscript = os.path.join(workdir, 'crm_clean.py')
         if os.path.isfile(cleanscript):
-            if subprocess.call([cleanscript, workdir], shell=False) != 0:
+            if subprocess.call(
+                    [cleanscript, workdir],
+                    shell=False,
+                    env=os.environ,  # bsc#1205925
+            ) != 0:
                 shutil.rmtree(workdir)
         else:
             shutil.rmtree(workdir)
diff --git a/crmsh/ui_node.py b/crmsh/ui_node.py
index 91a124fa05..05bd8e192d 100644
--- a/crmsh/ui_node.py
+++ b/crmsh/ui_node.py
@@ -1,7 +1,7 @@
 # Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic@suse.de>
 # Copyright (C) 2013 Kristoffer Gronlund <kgronlund@suse.com>
 # See COPYING for license information.
-
+import os
 import re
 import copy
 import subprocess
@@ -550,10 +550,13 @@ def do_delete(self, context, node):
         'usage: delete <node>'
         logger.warning('`crm node delete` is deprecated and will very likely be dropped in the near future. It is auto-replaced as `crm cluster remove -c {}`.'.format(node))
         if config.core.force:
-            rc = subprocess.call(['crm', 'cluster', 'remove', '-F', '-c', node])
+            args = ['crm', 'cluster', 'remove', '-F', '-c', node]
         else:
-            rc = subprocess.call(['crm', 'cluster', 'remove', '-c', node])
-        return rc == 0
+            args = ['crm', 'cluster', 'remove', '-c', node]
+        return 0 == subprocess.call(
+            args,
+            env=os.environ,  # bsc#1205925
+        )
 
     @command.wait
     @command.completers(compl.nodes, compl.choice(['set', 'delete', 'show']), _find_attr)
diff --git a/crmsh/utils.py b/crmsh/utils.py
index 2e422c009e..8d9adac757 100644
--- a/crmsh/utils.py
+++ b/crmsh/utils.py
@@ -523,7 +523,12 @@ def pipe_string(cmd, s):
     logger.debug("piping string to %s", cmd)
     if options.regression_tests:
         print(".EXT", cmd)
-    p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE)
+    p = subprocess.Popen(
+        cmd,
+        shell=True,
+        stdin=subprocess.PIPE,
+        env=os.environ,  # bsc#1205925
+    )
     try:
         # communicate() expects encoded bytes
         if isinstance(s, str):
@@ -552,7 +557,9 @@ def filter_string(cmd, s, stderr_on=True, shell=True):
                          shell=shell,
                          stdin=subprocess.PIPE,
                          stdout=subprocess.PIPE,
-                         stderr=stderr)
+                         stderr=stderr,
+                         env=os.environ,  # bsc#1205925
+                         )
     try:
         # bytes expected here
         if isinstance(s, str):
@@ -770,7 +777,9 @@ def show_dot_graph(dotfile, keep_file=False, desc="transition graph"):
     if options.regression_tests:
         print(".EXT", cmd)
     subprocess.Popen(cmd, shell=True, bufsize=0,
-                     stdin=None, stdout=None, stderr=None, close_fds=True)
+                     stdin=None, stdout=None, stderr=None, close_fds=True,
+                     env=os.environ,  # bsc#1205925
+                     )
     logger.info("starting %s to show %s", config.core.dotty, desc)
 
 
@@ -779,13 +788,21 @@ def ext_cmd(cmd, shell=True):
     if options.regression_tests:
         print(".EXT", cmd)
     logger.debug("invoke: %s", cmd)
-    return subprocess.call(cmd, shell=shell)
+    return subprocess.call(
+        cmd,
+        shell=shell,
+        env=os.environ,  # bsc#1205925
+    )
 
 
 def ext_cmd_nosudo(cmd, shell=True):
     if options.regression_tests:
         print(".EXT", cmd)
-    return subprocess.call(cmd, shell=shell)
+    return subprocess.call(
+        cmd,
+        shell=shell,
+        env=os.environ,  # bsc#1205925
+    )
 
 
 def rmdir_r(d):
@@ -918,7 +935,9 @@ def pipe_cmd_nosudo(cmd):
     proc = subprocess.Popen(cmd,
                             shell=True,
                             stdout=subprocess.PIPE,
-                            stderr=subprocess.PIPE)
+                            stderr=subprocess.PIPE,
+                            env=os.environ,  # bsc#1205925
+                            )
     (outp, err_outp) = proc.communicate()
     proc.wait()
     rc = proc.returncode
diff --git a/crmsh/xmlutil.py b/crmsh/xmlutil.py
index ee3f96a4e7..64a088d9ca 100644
--- a/crmsh/xmlutil.py
+++ b/crmsh/xmlutil.py
@@ -91,7 +91,11 @@ def sudocall(cmd):
     cmd = add_sudo(cmd)
     if options.regression_tests:
         print(".EXT", cmd)
-    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    p = subprocess.Popen(
+        cmd, shell=True,
+        stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+        env=os.environ,  # bsc#1205925
+    )
     try:
         outp, errp = p.communicate()
         p.wait()

From 1ceec363c4ecbc8da6f3f3e449f2482a4f08869b Mon Sep 17 00:00:00 2001
From: Nicholas Yang <nicholas.yang@suse.com>
Date: Wed, 13 Mar 2024 18:47:46 +0800
Subject: [PATCH 5/5] Fix: utils: set env `CIB_shadow` using `os.environ`
 (bsc#1205925)

`os.environ` is captured once from libc on python process startup (when
module `os` is imported from `site.py`) and is the only one interface
able to enumerate environment variables. Changing environment variables
with other interfaces (including `os.putenv`, `os.unsetenv`, or call
libc interface in some CPython modules) makes `os.environ` out of sync
and should be avoided.
---
 crmsh/utils.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/crmsh/utils.py b/crmsh/utils.py
index 8d9adac757..19fc90cec7 100644
--- a/crmsh/utils.py
+++ b/crmsh/utils.py
@@ -150,13 +150,14 @@ def this_node():
 
 
 def set_cib_in_use(name):
-    os.putenv(_cib_shadow, name)
+    os.environ[_cib_shadow] = name
     global _cib_in_use
     _cib_in_use = name
 
 
 def clear_cib_in_use():
-    os.unsetenv(_cib_shadow)
+    if _cib_shadow in os.environ:
+        del os.environ[_cib_shadow]
     global _cib_in_use
     _cib_in_use = ''