Skip to content

Commit

Permalink
gh-87868: Sort and remove duplicates in getenvironment() (GH-102731)
Browse files Browse the repository at this point in the history
(cherry picked from commit c31be58)

Co-authored-by: AN Long <[email protected]>
Co-authored-by: Alex Waygood <[email protected]>
Co-authored-by: Pieter Eendebak <[email protected]>
Co-authored-by: Erlend E. Aasland <[email protected]>
  • Loading branch information
5 people authored Jan 10, 2024
1 parent 3fc45e6 commit 4050a15
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 4 deletions.
46 changes: 46 additions & 0 deletions Lib/test/test_subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,19 @@ def test_env(self):
stdout, stderr = p.communicate()
self.assertEqual(stdout, b"orange")

@unittest.skipUnless(sys.platform == "win32", "Windows only issue")
def test_win32_duplicate_envs(self):
newenv = os.environ.copy()
newenv["fRUit"] = "cherry"
newenv["fruit"] = "lemon"
newenv["FRUIT"] = "orange"
newenv["frUit"] = "banana"
with subprocess.Popen(["CMD", "/c", "SET", "fruit"],
stdout=subprocess.PIPE,
env=newenv) as p:
stdout, _ = p.communicate()
self.assertEqual(stdout.strip(), b"frUit=banana")

# Windows requires at least the SYSTEMROOT environment variable to start
# Python
@unittest.skipIf(sys.platform == 'win32',
Expand Down Expand Up @@ -822,6 +835,26 @@ def is_env_var_to_ignore(n):
if not is_env_var_to_ignore(k)]
self.assertEqual(child_env_names, [])

@unittest.skipIf(sysconfig.get_config_var('Py_ENABLE_SHARED') == 1,
'The Python shared library cannot be loaded '
'without some system environments.')
@unittest.skipIf(check_sanitizer(address=True),
'AddressSanitizer adds to the environment.')
def test_one_environment_variable(self):
newenv = {'fruit': 'orange'}
cmd = [sys.executable, '-c',
'import sys,os;'
'sys.stdout.write("fruit="+os.getenv("fruit"))']
if sys.platform == "win32":
cmd = ["CMD", "/c", "SET", "fruit"]
with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=newenv) as p:
stdout, stderr = p.communicate()
if p.returncode and support.verbose:
print("STDOUT:", stdout.decode("ascii", "replace"))
print("STDERR:", stderr.decode("ascii", "replace"))
self.assertEqual(p.returncode, 0)
self.assertEqual(stdout.strip(), b"fruit=orange")

def test_invalid_cmd(self):
# null character in the command name
cmd = sys.executable + '\0'
Expand Down Expand Up @@ -862,6 +895,19 @@ def test_invalid_env(self):
stdout, stderr = p.communicate()
self.assertEqual(stdout, b"orange=lemon")

@unittest.skipUnless(sys.platform == "win32", "Windows only issue")
def test_win32_invalid_env(self):
# '=' in the environment variable name
newenv = os.environ.copy()
newenv["FRUIT=VEGETABLE"] = "cabbage"
with self.assertRaises(ValueError):
subprocess.Popen(ZERO_RETURN_CMD, env=newenv)

newenv = os.environ.copy()
newenv["==FRUIT"] = "cabbage"
with self.assertRaises(ValueError):
subprocess.Popen(ZERO_RETURN_CMD, env=newenv)

def test_communicate_stdin(self):
p = subprocess.Popen([sys.executable, "-c",
'import sys;'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Correctly sort and remove duplicate environment variables in
:py:func:`!_winapi.CreateProcess`.
159 changes: 155 additions & 4 deletions Modules/_winapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -772,12 +772,157 @@ gethandle(PyObject* obj, const char* name)
return ret;
}

static PyObject *
sortenvironmentkey(PyObject *module, PyObject *item)
{
return _winapi_LCMapStringEx_impl(NULL, LOCALE_NAME_INVARIANT,
LCMAP_UPPERCASE, item);
}

static PyMethodDef sortenvironmentkey_def = {
"sortenvironmentkey", _PyCFunction_CAST(sortenvironmentkey), METH_O, "",
};

static int
sort_environment_keys(PyObject *keys)
{
PyObject *keyfunc = PyCFunction_New(&sortenvironmentkey_def, NULL);
if (keyfunc == NULL) {
return -1;
}
PyObject *kwnames = Py_BuildValue("(s)", "key");
if (kwnames == NULL) {
Py_DECREF(keyfunc);
return -1;
}
PyObject *args[] = { keys, keyfunc };
PyObject *ret = PyObject_VectorcallMethod(&_Py_ID(sort), args, 1, kwnames);
Py_DECREF(keyfunc);
Py_DECREF(kwnames);
if (ret == NULL) {
return -1;
}
Py_DECREF(ret);

return 0;
}

static int
compare_string_ordinal(PyObject *str1, PyObject *str2, int *result)
{
wchar_t *s1 = PyUnicode_AsWideCharString(str1, NULL);
if (s1 == NULL) {
return -1;
}
wchar_t *s2 = PyUnicode_AsWideCharString(str2, NULL);
if (s2 == NULL) {
PyMem_Free(s1);
return -1;
}
*result = CompareStringOrdinal(s1, -1, s2, -1, TRUE);
PyMem_Free(s1);
PyMem_Free(s2);
return 0;
}

static PyObject *
dedup_environment_keys(PyObject *keys)
{
PyObject *result = PyList_New(0);
if (result == NULL) {
return NULL;
}

// Iterate over the pre-ordered keys, check whether the current key is equal
// to the next key (ignoring case), if different, insert the current value
// into the result list. If they are equal, do nothing because we always
// want to keep the last inserted one.
for (Py_ssize_t i = 0; i < PyList_GET_SIZE(keys); i++) {
PyObject *key = PyList_GET_ITEM(keys, i);

// The last key will always be kept.
if (i + 1 == PyList_GET_SIZE(keys)) {
if (PyList_Append(result, key) < 0) {
Py_DECREF(result);
return NULL;
}
continue;
}

PyObject *next_key = PyList_GET_ITEM(keys, i + 1);
int compare_result;
if (compare_string_ordinal(key, next_key, &compare_result) < 0) {
Py_DECREF(result);
return NULL;
}
if (compare_result == CSTR_EQUAL) {
continue;
}
if (PyList_Append(result, key) < 0) {
Py_DECREF(result);
return NULL;
}
}

return result;
}

static PyObject *
normalize_environment(PyObject *environment)
{
PyObject *keys = PyMapping_Keys(environment);
if (keys == NULL) {
return NULL;
}

if (sort_environment_keys(keys) < 0) {
Py_DECREF(keys);
return NULL;
}

PyObject *normalized_keys = dedup_environment_keys(keys);
Py_DECREF(keys);
if (normalized_keys == NULL) {
return NULL;
}

PyObject *result = PyDict_New();
if (result == NULL) {
Py_DECREF(normalized_keys);
return NULL;
}

for (int i = 0; i < PyList_GET_SIZE(normalized_keys); i++) {
PyObject *key = PyList_GET_ITEM(normalized_keys, i);
PyObject *value = PyObject_GetItem(environment, key);
if (value == NULL) {
Py_DECREF(normalized_keys);
Py_DECREF(result);
return NULL;
}

int ret = PyObject_SetItem(result, key, value);
Py_DECREF(value);
if (ret < 0) {
Py_DECREF(normalized_keys);
Py_DECREF(result);
return NULL;
}
}

Py_DECREF(normalized_keys);

return result;
}

static wchar_t *
getenvironment(PyObject* environment)
{
Py_ssize_t i, envsize, totalsize;
wchar_t *buffer = NULL, *p, *end;
PyObject *keys, *values;
PyObject *normalized_environment = NULL;
PyObject *keys = NULL;
PyObject *values = NULL;

/* convert environment dictionary to windows environment string */
if (! PyMapping_Check(environment)) {
Expand All @@ -786,11 +931,16 @@ getenvironment(PyObject* environment)
return NULL;
}

keys = PyMapping_Keys(environment);
if (!keys) {
normalized_environment = normalize_environment(environment);
if (normalize_environment == NULL) {
return NULL;
}
values = PyMapping_Values(environment);

keys = PyMapping_Keys(normalized_environment);
if (!keys) {
goto error;
}
values = PyMapping_Values(normalized_environment);
if (!values) {
goto error;
}
Expand Down Expand Up @@ -882,6 +1032,7 @@ getenvironment(PyObject* environment)

cleanup:
error:
Py_XDECREF(normalized_environment);
Py_XDECREF(keys);
Py_XDECREF(values);
return buffer;
Expand Down

0 comments on commit 4050a15

Please sign in to comment.