Skip to content
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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ jobs:
with:
persist-credentials: false
- name: Build and test
run: ./Android/android.py ci --fast-ci ${{ matrix.arch }}-linux-android
run: python3 Platforms/Android ci --fast-ci ${{ matrix.arch }}-linux-android

build-ios:
name: iOS
Expand Down
2 changes: 1 addition & 1 deletion Doc/library/argparse.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1970,7 +1970,7 @@ FileType objects
run and then use the :keyword:`with`-statement to manage the files.

.. versionchanged:: 3.4
Added the *encodings* and *errors* parameters.
Added the *encoding* and *errors* parameters.

.. deprecated:: 3.14

Expand Down
2 changes: 1 addition & 1 deletion Include/internal/pycore_dict.h
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ _PyDict_NotifyEvent(PyDict_WatchEvent event,
PyObject *value)
{
assert(Py_REFCNT((PyObject*)mp) > 0);
int watcher_bits = FT_ATOMIC_LOAD_UINT64_RELAXED(mp->_ma_watcher_tag) & DICT_WATCHER_MASK;
int watcher_bits = FT_ATOMIC_LOAD_UINT64_ACQUIRE(mp->_ma_watcher_tag) & DICT_WATCHER_MASK;
if (watcher_bits) {
RARE_EVENT_STAT_INC(watched_dict_modification);
_PyDict_SendEvent(watcher_bits, event, mp, key, value);
Expand Down
3 changes: 3 additions & 0 deletions Include/internal/pycore_pyatomic_ft_wrappers.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ extern "C" {
_Py_atomic_load_uint16_relaxed(&value)
#define FT_ATOMIC_LOAD_UINT32_RELAXED(value) \
_Py_atomic_load_uint32_relaxed(&value)
#define FT_ATOMIC_LOAD_UINT64_ACQUIRE(value) \
_Py_atomic_load_uint64_acquire(&value)
#define FT_ATOMIC_LOAD_UINT64_RELAXED(value) \
_Py_atomic_load_uint64_relaxed(&value)
#define FT_ATOMIC_LOAD_ULONG_RELAXED(value) \
Expand Down Expand Up @@ -154,6 +156,7 @@ extern "C" {
#define FT_ATOMIC_LOAD_UINT8_RELAXED(value) value
#define FT_ATOMIC_LOAD_UINT16_RELAXED(value) value
#define FT_ATOMIC_LOAD_UINT32_RELAXED(value) value
#define FT_ATOMIC_LOAD_UINT64_ACQUIRE(value) value
#define FT_ATOMIC_LOAD_UINT64_RELAXED(value) value
#define FT_ATOMIC_LOAD_ULONG_RELAXED(value) value
#define FT_ATOMIC_STORE_PTR_RELAXED(value, new_value) value = new_value
Expand Down
16 changes: 14 additions & 2 deletions Lib/asyncio/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,17 +162,29 @@ def interrupt(self) -> None:
"ps", help="Display a table of all pending tasks in a process"
)
ps.add_argument("pid", type=int, help="Process ID to inspect")
ps.add_argument(
"--retries",
type=int,
default=3,
help="Number of retries on transient attach errors",
)
pstree = subparsers.add_parser(
"pstree", help="Display a tree of all pending tasks in a process"
)
pstree.add_argument("pid", type=int, help="Process ID to inspect")
pstree.add_argument(
"--retries",
type=int,
default=3,
help="Number of retries on transient attach errors",
)
args = parser.parse_args()
match args.command:
case "ps":
asyncio.tools.display_awaited_by_tasks_table(args.pid)
asyncio.tools.display_awaited_by_tasks_table(args.pid, retries=args.retries)
sys.exit(0)
case "pstree":
asyncio.tools.display_awaited_by_tasks_tree(args.pid)
asyncio.tools.display_awaited_by_tasks_tree(args.pid, retries=args.retries)
sys.exit(0)
case None:
pass # continue to the interactive shell
Expand Down
41 changes: 26 additions & 15 deletions Lib/asyncio/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,27 +231,38 @@ def exit_with_permission_help_text():
print(
"Error: The specified process cannot be attached to due to insufficient permissions.\n"
"See the Python documentation for details on required privileges and troubleshooting:\n"
"https://docs.python.org/3.14/howto/remote_debugging.html#permission-requirements\n"
"https://docs.python.org/3/howto/remote_debugging.html#permission-requirements\n",
file=sys.stderr,
)
sys.exit(1)


def _get_awaited_by_tasks(pid: int) -> list:
try:
return get_all_awaited_by(pid)
except RuntimeError as e:
while e.__context__ is not None:
e = e.__context__
print(f"Error retrieving tasks: {e}")
sys.exit(1)
except PermissionError:
exit_with_permission_help_text()
_TRANSIENT_ERRORS = (RuntimeError, OSError, UnicodeDecodeError, MemoryError)


def _get_awaited_by_tasks(pid: int, retries: int = 3) -> list:
for attempt in range(retries + 1):
try:
return get_all_awaited_by(pid)
except PermissionError:
exit_with_permission_help_text()
except ProcessLookupError:
print(f"Error: process {pid} not found.", file=sys.stderr)
sys.exit(1)
except _TRANSIENT_ERRORS as e:
if attempt < retries:
continue
if isinstance(e, RuntimeError):
while e.__context__ is not None:
e = e.__context__
print(f"Error retrieving tasks: {e}", file=sys.stderr)
sys.exit(1)


def display_awaited_by_tasks_table(pid: int) -> None:
def display_awaited_by_tasks_table(pid: int, retries: int = 3) -> None:
"""Build and print a table of all pending tasks under `pid`."""

tasks = _get_awaited_by_tasks(pid)
tasks = _get_awaited_by_tasks(pid, retries=retries)
table = build_task_table(tasks)
# Print the table in a simple tabular format
print(
Expand All @@ -262,10 +273,10 @@ def display_awaited_by_tasks_table(pid: int) -> None:
print(f"{row[0]:<10} {row[1]:<20} {row[2]:<20} {row[3]:<50} {row[4]:<50} {row[5]:<15} {row[6]:<15}")


def display_awaited_by_tasks_tree(pid: int) -> None:
def display_awaited_by_tasks_tree(pid: int, retries: int = 3) -> None:
"""Build and print a tree of all pending tasks under `pid`."""

tasks = _get_awaited_by_tasks(pid)
tasks = _get_awaited_by_tasks(pid, retries=retries)
try:
result = build_async_tree(tasks)
except CycleFoundException as e:
Expand Down
9 changes: 6 additions & 3 deletions Lib/configparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,12 +315,15 @@ def __init__(self, source, *args):

def append(self, lineno, line):
self.errors.append((lineno, line))
self.message += '\n\t[line %2d]: %s' % (lineno, repr(line))
self.message += f'\n\t[line {lineno:2d}]: {line!r}'

def combine(self, others):
messages = [self.message]
for other in others:
for error in other.errors:
self.append(*error)
for lineno, line in other.errors:
self.errors.append((lineno, line))
messages.append(f'\n\t[line {lineno:2d}]: {line!r}')
self.message = "".join(messages)
return self

@staticmethod
Expand Down
28 changes: 16 additions & 12 deletions Lib/socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -649,18 +649,22 @@ def _fallback_socketpair(family=AF_INET, type=SOCK_STREAM, proto=0):
# Authenticating avoids using a connection from something else
# able to connect to {host}:{port} instead of us.
# We expect only AF_INET and AF_INET6 families.
try:
if (
ssock.getsockname() != csock.getpeername()
or csock.getsockname() != ssock.getpeername()
):
raise ConnectionError("Unexpected peer connection")
except:
# getsockname() and getpeername() can fail
# if either socket isn't connected.
ssock.close()
csock.close()
raise
#
# Note that we skip this on WASI because on that platorm the client socket
# may not have finished connecting by the time we've reached this point (gh-146139).
if sys.platform != "wasi":
try:
if (
ssock.getsockname() != csock.getpeername()
or csock.getsockname() != ssock.getpeername()
):
raise ConnectionError("Unexpected peer connection")
except:
# getsockname() and getpeername() can fail
# if either socket isn't connected.
ssock.close()
csock.close()
raise

return (ssock, csock)

Expand Down
13 changes: 13 additions & 0 deletions Lib/test/test_configparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1729,6 +1729,19 @@ def test_error(self):
self.assertEqual(e1.message, e2.message)
self.assertEqual(repr(e1), repr(e2))

def test_combine_error_linear_complexity(self):
# Ensure that ParsingError.combine() has linear complexity.
# See https://github.com/python/cpython/issues/148370.
n = 50000
s = '[*]\n' + (err_line := '=\n') * n
p = configparser.ConfigParser(strict=False)
with self.assertRaises(configparser.ParsingError) as cm:
p.read_string(s)
errlines = cm.exception.message.splitlines()
self.assertEqual(len(errlines), n + 1)
self.assertStartsWith(errlines[0], "Source contains parsing errors: ")
self.assertEqual(errlines[42], f"\t[line {43:2d}]: {err_line!r}")

def test_nosectionerror(self):
import pickle
e1 = configparser.NoSectionError('section')
Expand Down
9 changes: 9 additions & 0 deletions Lib/test/test_webbrowser.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ def test_open_bad_new_parameter(self):
arguments=[URL],
kw=dict(new=999))

def test_reject_action_dash_prefixes(self):
browser = self.browser_class(name=CMD_NAME)
with self.assertRaises(ValueError):
browser.open('%action--incognito')
# new=1: action is "--new-window", so "%action" itself expands to
# a dash-prefixed flag even with no dash in the original URL.
with self.assertRaises(ValueError):
browser.open('%action', new=1)


class EdgeCommandTest(CommandTestMixin, unittest.TestCase):

Expand Down
5 changes: 3 additions & 2 deletions Lib/webbrowser.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,6 @@ def _invoke(self, args, remote, autoraise, url=None):

def open(self, url, new=0, autoraise=True):
sys.audit("webbrowser.open", url)
self._check_url(url)
if new == 0:
action = self.remote_action
elif new == 1:
Expand All @@ -288,7 +287,9 @@ def open(self, url, new=0, autoraise=True):
raise Error("Bad 'new' parameter to open(); "
f"expected 0, 1, or 2, got {new}")

args = [arg.replace("%s", url).replace("%action", action)
self._check_url(url.replace("%action", action))

args = [arg.replace("%action", action).replace("%s", url)
for arg in self.remote_args]
args = [arg for arg in args if arg]
success = self._invoke(args, True, autoraise, url)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The Android build tools have been moved to the Platforms folder.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:mod:`configparser`: prevent quadratic behavior when a :exc:`~configparser.ParsingError`
is raised after a parser fails to parse multiple lines. Patch by Bénédikt Tran.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:mod:`asyncio` debugging tools (``python -m asyncio ps`` and ``pstree``)
now retry automatically on transient errors that can occur when attaching
to a process under active thread delegation. The number of retries can be
controlled with the ``--retries`` flag. Patch by Bartosz Sławecki.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
A bypass in :mod:`webbrowser` allowed URLs prefixed with ``%action`` to pass
the dash-prefix safety check.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Hardened :mod:`!_remote_debugging` by validating remote debug offset tables
before using them to size memory reads or interpret remote layouts.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fixed stack depth calculation in :mod:`!_remote_debugging` when decoding
certain ``.pyb`` inputs on 32-bit builds. Issue originally identified and
diagnosed by Tristan Madani (@TristanInSec on GitHub).
6 changes: 4 additions & 2 deletions Modules/_remote_debugging/_remote_debugging.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,10 @@ typedef enum _WIN32_THREADSTATE {
* MACROS AND CONSTANTS
* ============================================================================ */

#define GET_MEMBER(type, obj, offset) (*(type*)((char*)(obj) + (offset)))
#define GET_MEMBER(type, obj, offset) \
(*(const type *)memcpy(&(type){0}, (const char *)(obj) + (offset), sizeof(type)))
#define CLEAR_PTR_TAG(ptr) (((uintptr_t)(ptr) & ~Py_TAG_BITS))
#define GET_MEMBER_NO_TAG(type, obj, offset) (type)(CLEAR_PTR_TAG(*(type*)((char*)(obj) + (offset))))
#define GET_MEMBER_NO_TAG(type, obj, offset) (type)(CLEAR_PTR_TAG(GET_MEMBER(type, obj, offset)))

/* Size macros for opaque buffers */
#define SIZEOF_BYTES_OBJ sizeof(PyBytesObject)
Expand Down Expand Up @@ -417,6 +418,7 @@ extern void cached_code_metadata_destroy(void *ptr);
/* Validation */
extern int is_prerelease_version(uint64_t version);
extern int validate_debug_offsets(struct _Py_DebugOffsets *debug_offsets);
#define PY_REMOTE_DEBUG_INVALID_ASYNC_DEBUG_OFFSETS (-2)

/* ============================================================================
* MEMORY READING FUNCTION DECLARATIONS
Expand Down
14 changes: 12 additions & 2 deletions Modules/_remote_debugging/asyncio.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
******************************************************************************/

#include "_remote_debugging.h"
#include "debug_offsets_validation.h"

/* ============================================================================
* ASYNCIO DEBUG ADDRESS FUNCTIONS
Expand Down Expand Up @@ -71,8 +72,13 @@ read_async_debug(RemoteUnwinderObject *unwinder)
int result = _Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, async_debug_addr, size, &unwinder->async_debug_offsets);
if (result < 0) {
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read AsyncioDebug offsets");
return result;
}
return result;
if (_PyRemoteDebug_ValidateAsyncDebugOffsets(&unwinder->async_debug_offsets) < 0) {
set_exception_cause(unwinder, PyExc_RuntimeError, "Invalid AsyncioDebug offsets");
return PY_REMOTE_DEBUG_INVALID_ASYNC_DEBUG_OFFSETS;
}
return 0;
}

int
Expand All @@ -85,7 +91,11 @@ ensure_async_debug_offsets(RemoteUnwinderObject *unwinder)

// Try to load async debug offsets (the target process may have
// loaded asyncio since we last checked)
if (read_async_debug(unwinder) < 0) {
int result = read_async_debug(unwinder);
if (result == PY_REMOTE_DEBUG_INVALID_ASYNC_DEBUG_OFFSETS) {
return -1;
}
if (result < 0) {
PyErr_Clear();
PyErr_SetString(PyExc_RuntimeError, "AsyncioDebug section not available");
set_exception_cause(unwinder, PyExc_RuntimeError,
Expand Down
32 changes: 20 additions & 12 deletions Modules/_remote_debugging/binary_io_reader.c
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,20 @@ reader_get_or_create_thread_state(BinaryReader *reader, uint64_t thread_id,
* STACK DECODING HELPERS
* ============================================================================ */

/* Validate that final_depth fits in the stack buffer.
* Uses uint64_t to prevent overflow on 32-bit platforms. */
static inline int
validate_stack_depth(ReaderThreadState *ts, uint64_t final_depth)
{
if (final_depth > ts->current_stack_capacity) {
PyErr_Format(PyExc_ValueError,
"Final stack depth %llu exceeds capacity %zu",
(unsigned long long)final_depth, ts->current_stack_capacity);
return -1;
}
return 0;
}

/* Decode a full stack from sample data.
* Updates ts->current_stack and ts->current_stack_depth.
* Returns 0 on success, -1 on error (bounds violation). */
Expand Down Expand Up @@ -658,12 +672,9 @@ decode_stack_suffix(ReaderThreadState *ts, const uint8_t *data,
return -1;
}

/* Validate final depth doesn't exceed capacity */
size_t final_depth = (size_t)shared + new_count;
if (final_depth > ts->current_stack_capacity) {
PyErr_Format(PyExc_ValueError,
"Final stack depth %zu exceeds capacity %zu",
final_depth, ts->current_stack_capacity);
/* Use uint64_t to prevent overflow on 32-bit platforms */
uint64_t final_depth = (uint64_t)shared + new_count;
if (validate_stack_depth(ts, final_depth) < 0) {
return -1;
}

Expand Down Expand Up @@ -713,12 +724,9 @@ decode_stack_pop_push(ReaderThreadState *ts, const uint8_t *data,
}
size_t keep = (ts->current_stack_depth > pop) ? ts->current_stack_depth - pop : 0;

/* Validate final depth doesn't exceed capacity */
size_t final_depth = keep + push;
if (final_depth > ts->current_stack_capacity) {
PyErr_Format(PyExc_ValueError,
"Final stack depth %zu exceeds capacity %zu",
final_depth, ts->current_stack_capacity);
/* Use uint64_t to prevent overflow on 32-bit platforms */
uint64_t final_depth = (uint64_t)keep + push;
if (validate_stack_depth(ts, final_depth) < 0) {
return -1;
}

Expand Down
Loading
Loading