Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3.12] gh-87868: Sort and remove duplicates in getenvironment() (GH-102731) #113865

Merged
merged 2 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading