Skip to content

Commit 3cea83c

Browse files
authored
Merge branch 'main' into support-IPv6-in-cookiejar
2 parents e7e1d83 + b3ae769 commit 3cea83c

File tree

17 files changed

+263
-84
lines changed

17 files changed

+263
-84
lines changed

Doc/c-api/arg.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,12 @@ Building values
686686
``p`` (:class:`bool`) [int]
687687
Convert a C :c:expr:`int` to a Python :class:`bool` object.
688688
689+
Be aware that this format requires an ``int`` argument.
690+
Unlike most other contexts in C, variadic arguments are not coerced to
691+
a suitable type automatically.
692+
You can convert another type (for example, a pointer or a float) to a
693+
suitable ``int`` value using ``(x) ? 1 : 0`` or ``!!x``.
694+
689695
.. versionadded:: 3.14
690696
691697
``c`` (:class:`bytes` of length 1) [char]

Doc/library/netrc.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ the Unix :program:`ftp` program and other FTP clients.
2424
a :exc:`FileNotFoundError` exception will be raised.
2525
Parse errors will raise :exc:`NetrcParseError` with diagnostic
2626
information including the file name, line number, and terminating token.
27+
2728
If no argument is specified on a POSIX system, the presence of passwords in
2829
the :file:`.netrc` file will raise a :exc:`NetrcParseError` if the file
2930
ownership or permissions are insecure (owned by a user other than the user
3031
running the process, or accessible for read or write by any other user).
3132
This implements security behavior equivalent to that of ftp and other
32-
programs that use :file:`.netrc`.
33+
programs that use :file:`.netrc`. Such security checks are not available
34+
on platforms that do not support :func:`os.getuid`.
3335

3436
.. versionchanged:: 3.4 Added the POSIX permission check.
3537

InternalDocs/asyncio.md

Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ asyncio
22
=======
33

44

5-
This document describes the working and implementation details of C
6-
implementation of the
5+
This document describes the working and implementation details of the
76
[`asyncio`](https://docs.python.org/3/library/asyncio.html) module.
87

8+
**The following section describes the implementation details of the C implementation**.
9+
10+
# Task management
911

1012
## Pre-Python 3.14 implementation
1113

@@ -158,7 +160,8 @@ flowchart TD
158160
subgraph two["Thread deallocating"]
159161
A1{"thread's task list empty? <br> llist_empty(tstate->asyncio_tasks_head)"}
160162
A1 --> |true| B1["deallocate thread<br>free_threadstate(tstate)"]
161-
A1 --> |false| C1["add tasks to interpreter's task list<br> llist_concat(&tstate->interp->asyncio_tasks_head,tstate->asyncio_tasks_head)"]
163+
A1 --> |false| C1["add tasks to interpreter's task list<br> llist_concat(&tstate->interp->asyncio_tasks_head,
164+
&tstate->asyncio_tasks_head)"]
162165
C1 --> B1
163166
end
164167
@@ -205,6 +208,121 @@ In free-threading, it avoids contention on a global dictionary as
205208
threads can access the current task of thier running loop without any
206209
locking.
207210

211+
---
212+
213+
**The following section describes the implementation details of the Python implementation**.
214+
215+
# async generators
216+
217+
This section describes the implementation details of async generators in `asyncio`.
218+
219+
Since async generators are meant to be used from coroutines,
220+
their finalization (execution of finally blocks) needs
221+
to be done while the loop is running.
222+
Most async generators are closed automatically
223+
when they are fully iterated over and exhausted; however,
224+
if the async generator is not fully iterated over,
225+
it may not be closed properly, leading to the `finally` blocks not being executed.
226+
227+
Consider the following code:
228+
```py
229+
import asyncio
230+
231+
async def agen():
232+
try:
233+
yield 1
234+
finally:
235+
await asyncio.sleep(1)
236+
print("finally executed")
237+
238+
239+
async def main():
240+
async for i in agen():
241+
break
242+
243+
loop = asyncio.EventLoop()
244+
loop.run_until_complete(main())
245+
```
246+
247+
The above code will not print "finally executed", because the
248+
async generator `agen` is not fully iterated over
249+
and it is not closed manually by awaiting `agen.aclose()`.
250+
251+
To solve this, `asyncio` uses the `sys.set_asyncgen_hooks` function to
252+
set hooks for finalizing async generators as described in
253+
[PEP 525](https://peps.python.org/pep-0525/).
254+
255+
- **firstiter hook**: When the async generator is iterated over for the first time,
256+
the *firstiter hook* is called. The async generator is added to `loop._asyncgens` WeakSet
257+
and the event loop tracks all active async generators.
258+
259+
- **finalizer hook**: When the async generator is about to be finalized,
260+
the *finalizer hook* is called. The event loop removes the async generator
261+
from `loop._asyncgens` WeakSet, and schedules the finalization of the async
262+
generator by creating a task calling `agen.aclose()`. This ensures that the
263+
finally block is executed while the event loop is running. When the loop is
264+
shutting down, the loop checks if there are active async generators and if so,
265+
it similarly schedules the finalization of all active async generators by calling
266+
`agen.aclose()` on each of them and waits for them to complete before shutting
267+
down the loop.
268+
269+
This ensures that the async generator's `finally` blocks are executed even
270+
if the generator is not explicitly closed.
271+
272+
Consider the following example:
273+
274+
```python
275+
import asyncio
276+
277+
async def agen():
278+
try:
279+
yield 1
280+
yield 2
281+
finally:
282+
print("executing finally block")
283+
284+
async def main():
285+
async for item in agen():
286+
print(item)
287+
break # not fully iterated
288+
289+
asyncio.run(main())
290+
```
291+
292+
```mermaid
293+
flowchart TD
294+
subgraph one["Loop running"]
295+
A["asyncio.run(main())"] --> B
296+
B["set async generator hooks <br> sys.set_asyncgen_hooks()"] --> C
297+
C["async for item in agen"] --> F
298+
F{"first iteration?"} --> |true|D
299+
F{"first iteration?"} --> |false|H
300+
D["calls firstiter hook<br>loop._asyncgen_firstiter_hook(agen)"] --> E
301+
E["add agen to WeakSet<br> loop._asyncgens.add(agen)"] --> H
302+
H["item = await agen.\_\_anext\_\_()"] --> J
303+
J{"StopAsyncIteration?"} --> |true|M
304+
J{"StopAsyncIteration?"} --> |false|I
305+
I["print(item)"] --> S
306+
S{"continue iterating?"} --> |true|C
307+
S{"continue iterating?"} --> |false|M
308+
M{"agen is no longer referenced?"} --> |true|N
309+
M{"agen is no longer referenced?"} --> |false|two
310+
N["finalize agen<br>_PyGen_Finalize(agen)"] --> O
311+
O["calls finalizer hook<br>loop._asyncgen_finalizer_hook(agen)"] --> P
312+
P["remove agen from WeakSet<br>loop._asyncgens.discard(agen)"] --> Q
313+
Q["schedule task to close it<br>self.create_task(agen.aclose())"] --> R
314+
R["print('executing finally block')"] --> E1
315+
316+
end
317+
318+
subgraph two["Loop shutting down"]
319+
A1{"check for alive async generators?"} --> |true|B1
320+
B1["close all async generators <br> await asyncio.gather\(*\[ag.aclose\(\) for ag in loop._asyncgens\]"] --> R
321+
A1{"check for alive async generators?"} --> |false|E1
322+
E1["loop.close()"]
323+
end
324+
325+
```
208326

209327
[^1]: https://github.com/python/cpython/issues/123089
210-
[^2]: https://github.com/python/cpython/issues/80788
328+
[^2]: https://github.com/python/cpython/issues/80788

Lib/_pyrepl/base_eventqueue.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def push(self, char: int | bytes) -> None:
8787
if isinstance(k, dict):
8888
self.keymap = k
8989
else:
90-
self.insert(Event('key', k, self.flush_buf()))
90+
self.insert(Event('key', k, bytes(self.flush_buf())))
9191
self.keymap = self.compiled_keymap
9292

9393
elif self.buf and self.buf[0] == 27: # escape
@@ -96,7 +96,7 @@ def push(self, char: int | bytes) -> None:
9696
# the docstring in keymap.py
9797
trace('unrecognized escape sequence, propagating...')
9898
self.keymap = self.compiled_keymap
99-
self.insert(Event('key', '\033', bytearray(b'\033')))
99+
self.insert(Event('key', '\033', b'\033'))
100100
for _c in self.flush_buf()[1:]:
101101
self.push(_c)
102102

@@ -106,5 +106,5 @@ def push(self, char: int | bytes) -> None:
106106
except UnicodeError:
107107
return
108108
else:
109-
self.insert(Event('key', decoded, self.flush_buf()))
109+
self.insert(Event('key', decoded, bytes(self.flush_buf())))
110110
self.keymap = self.compiled_keymap

Lib/netrc.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@
77
__all__ = ["netrc", "NetrcParseError"]
88

99

10+
def _can_security_check():
11+
# On WASI, getuid() is indicated as a stub but it may also be missing.
12+
return os.name == 'posix' and hasattr(os, 'getuid')
13+
14+
15+
def _getpwuid(uid):
16+
try:
17+
import pwd
18+
return pwd.getpwuid(uid)[0]
19+
except (ImportError, LookupError):
20+
return f'uid {uid}'
21+
22+
1023
class NetrcParseError(Exception):
1124
"""Exception raised on syntax errors in the .netrc file."""
1225
def __init__(self, msg, filename=None, lineno=None):
@@ -142,21 +155,15 @@ def _parse(self, file, fp, default_netrc):
142155
self._security_check(fp, default_netrc, self.hosts[entryname][0])
143156

144157
def _security_check(self, fp, default_netrc, login):
145-
if os.name == 'posix' and default_netrc and login != "anonymous":
158+
if _can_security_check() and default_netrc and login != "anonymous":
146159
prop = os.fstat(fp.fileno())
147-
if prop.st_uid != os.getuid():
148-
import pwd
149-
try:
150-
fowner = pwd.getpwuid(prop.st_uid)[0]
151-
except KeyError:
152-
fowner = 'uid %s' % prop.st_uid
153-
try:
154-
user = pwd.getpwuid(os.getuid())[0]
155-
except KeyError:
156-
user = 'uid %s' % os.getuid()
160+
current_user_id = os.getuid()
161+
if prop.st_uid != current_user_id:
162+
fowner = _getpwuid(prop.st_uid)
163+
user = _getpwuid(current_user_id)
157164
raise NetrcParseError(
158-
(f"~/.netrc file owner ({fowner}, {user}) does not match"
159-
" current user"))
165+
f"~/.netrc file owner ({fowner}) does not match"
166+
f" current user ({user})")
160167
if (prop.st_mode & (stat.S_IRWXG | stat.S_IRWXO)):
161168
raise NetrcParseError(
162169
"~/.netrc access too permissive: access"

Lib/test/test_netrc.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
import netrc, os, unittest, sys, textwrap
2+
from test import support
23
from test.support import os_helper
34

4-
try:
5-
import pwd
6-
except ImportError:
7-
pwd = None
8-
95
temp_filename = os_helper.TESTFN
106

117
class NetrcTestCase(unittest.TestCase):
@@ -269,9 +265,14 @@ def test_comment_at_end_of_machine_line_pass_has_hash(self):
269265
machine bar.domain.com login foo password pass
270266
""", '#pass')
271267

268+
@unittest.skipUnless(support.is_wasi, 'WASI only test')
269+
def test_security_on_WASI(self):
270+
self.assertFalse(netrc._can_security_check())
271+
self.assertEqual(netrc._getpwuid(0), 'uid 0')
272+
self.assertEqual(netrc._getpwuid(123456), 'uid 123456')
272273

273274
@unittest.skipUnless(os.name == 'posix', 'POSIX only test')
274-
@unittest.skipIf(pwd is None, 'security check requires pwd module')
275+
@unittest.skipUnless(hasattr(os, 'getuid'), "os.getuid is required")
275276
@os_helper.skip_unless_working_chmod
276277
def test_security(self):
277278
# This test is incomplete since we are normally not run as root and

Lib/test/test_perf_profiler.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,9 +506,12 @@ def _is_perf_version_at_least(major, minor):
506506
# The output of perf --version looks like "perf version 6.7-3" but
507507
# it can also be perf version "perf version 5.15.143", or even include
508508
# a commit hash in the version string, like "6.12.9.g242e6068fd5c"
509+
#
510+
# PermissionError is raised if perf does not exist on the Windows Subsystem
511+
# for Linux, see #134987
509512
try:
510513
output = subprocess.check_output(["perf", "--version"], text=True)
511-
except (subprocess.CalledProcessError, FileNotFoundError):
514+
except (subprocess.CalledProcessError, FileNotFoundError, PermissionError):
512515
return False
513516
version = output.split()[2]
514517
version = version.split("-")[0]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:mod:`netrc`: skip security checks if :func:`os.getuid` is missing.
2+
Patch by Bénédikt Tran.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:mod:`netrc`: improve the error message when the security check for the
2+
ownership of the default configuration file ``~/.netrc`` fails. Patch by
3+
Bénédikt Tran.

Modules/_hashopenssl.c

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,8 @@ py_hashentry_table_new(void) {
255255
return NULL;
256256
}
257257

258-
/* Module state */
258+
// --- Module state -----------------------------------------------------------
259+
259260
static PyModuleDef _hashlibmodule;
260261

261262
typedef struct {
@@ -277,6 +278,8 @@ get_hashlib_state(PyObject *module)
277278
return (_hashlibstate *)state;
278279
}
279280

281+
// --- Module objects ---------------------------------------------------------
282+
280283
typedef struct {
281284
HASHLIB_OBJECT_HEAD
282285
EVP_MD_CTX *ctx; /* OpenSSL message digest context */
@@ -291,15 +294,17 @@ typedef struct {
291294

292295
#define HMACobject_CAST(op) ((HMACobject *)(op))
293296

294-
#include "clinic/_hashopenssl.c.h"
297+
// --- Module clinic configuration --------------------------------------------
298+
295299
/*[clinic input]
296300
module _hashlib
297-
class _hashlib.HASH "HASHobject *" "((_hashlibstate *)PyModule_GetState(module))->HASH_type"
298-
class _hashlib.HASHXOF "HASHobject *" "((_hashlibstate *)PyModule_GetState(module))->HASHXOF_type"
299-
class _hashlib.HMAC "HMACobject *" "((_hashlibstate *)PyModule_GetState(module))->HMAC_type"
301+
class _hashlib.HASH "HASHobject *" "&PyType_Type"
302+
class _hashlib.HASHXOF "HASHobject *" "&PyType_Type"
303+
class _hashlib.HMAC "HMACobject *" "&PyType_Type"
300304
[clinic start generated code]*/
301-
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=eb805ce4b90b1b31]*/
305+
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=6b5c9ce5c28bdc58]*/
302306

307+
#include "clinic/_hashopenssl.c.h"
303308

304309
/* LCOV_EXCL_START */
305310

0 commit comments

Comments
 (0)