`uuid.uuid4()` can generate duplicates
Les mainteneurs répondent en général sous 1 jour
Personne n'a encore pris cette issue.
Évaluation
- Difficulté
- 4/5
- Temps estimé
- 3-5 jours
- Accessibilité débutants
- 42/100
Piste de recherche
Start by reading the C UUID implementation in Modules/_uuidmodule.c, especially gen_random_lock_held and the random_buf PID check described in the report. Reproduce the duplicate UUID with the provided PID namespace proof of concept on Linux, then investigate how the cache behaves across process forks and PID reuse. Done means uuid.uuid4() no longer produces duplicate values in the reported scenario, with a regression test covering it.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Description
Bug report
https://github.com/python/cpython/pull/139123 rewrites the uuid module to be in C. On its face, that's fine; however, it also adds an entropy cache to the C uuid module. See gen_random_lock_held, with specific attention to this portion.
It contains the following comment:
// Overfetching & caching entropy improves the performance 10x.
// There's a precedent with NodeJS doing exact same thing for
// improving performance of their UUID implementation.
For context, I'm looking because NodeJS today also released CVE-2026-106074, an error in their equivalent cache.
Specifically, the CPython entropy cache (state->random_buf) is in memory; it will regenerate it if:
if (pid != state->random_last_pid) {
Read this as "current pid != the pid that created the buffer".
Linux (but this applies to at least macOS as well) do not guarantee that PIDs are unique across a boot. In Linux, there are two circumstances in which a child might see the same PID as a parent or ancestor:
- The PID has been recycled, and the ancestor has died.
- The process is in a separate PID NS, and is seeing the same PID that an ancestor (including, possibly, the parent) witnessed in a different NS.
Either case, I believe, can be used to generate duplicate UUIDs.
I started with the latter case when I was hacking this up: say I am the first process in a container; in Linux, such a process is "init" for that PID NS, and will always be PID 1. If that process itself creates child inside a new container, that child will also be PID 1. Since the child then sees that the pid is "unchanged" since state->random_buf was generated, it will not re-create the buffer, and we can end up with two processes with the same random_buf buffer.
A proof of concept
import ctypes
import os
import signal
import sys
import time
import uuid
# Define Linux clone flags
CLONE_NEWPID = 0x20000000
CLONE_NEWNS = 0x00020000
CLONE_NEWUSER = 0x10000000
libc = ctypes.CDLL("libc.so.6", use_errno=True)
def unshare(flags):
res = libc.unshare(flags)
if res != 0:
err = ctypes.get_errno()
#print("error :(")
#time.sleep(100)
raise OSError(err, os.strerror(err))
def dream_child():
# Unshare PID namespace (and user/mount namespaces for full isolation)
#unshare(CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWUSER)
#print("unsharing.")
unshare(CLONE_NEWPID)
# Fork is required because the current process stays in the old PID namespace;
# only its future children enter the new PID namespace.
pid = os.fork()
if pid == 0:
# We are inside the new PID namespace (Child process)
print(f"Child process PID in *new* new namespace: {os.getpid()}")
print("In dream level 2.")
print(f"uuid4() in child -> {uuid.uuid4()}")
sys.exit(0)
else:
# Parent process waits for child
_, status = os.waitpid(pid, 0)
def main():
# Unshare PID namespace (and user/mount namespaces for full isolation)
unshare(CLONE_NEWPID)
# Fork is required because the current process stays in the old PID namespace;
# only its future children enter the new PID namespace.
pid = os.fork()
if pid == 0:
# We are inside the new PID namespace (Child process)
print(f"Child process PID in new namespace: {os.getpid()}")
# Optional: Mount a new /proc so tools like 'ps' work properly
# (Requires CLONE_NEWNS)
try:
os.mount("proc", "/proc", "proc", 0, "")
except Exception:
pass
# Burn a UUID to set the cache up.
uuid.uuid4()
print("In dream level 1.")
dream_child()
print(f"uuid4() in parent -> {uuid.uuid4()}")
else:
# Parent process waits for child
_, status = os.waitpid(pid, 0)
sys.exit(os.waitstatus_to_exitcode(status))
if __name__ == "__main__":
main()
On my system, this Python script emits:
» sudo ~/code/third-party/cpython/python unshare_and_uuid.py
Password:
Child process PID in new namespace: 1
In dream level 1.
Child process PID in *new* new namespace: 1
In dream level 2.
uuid4() in child -> 1df59e77-c35a-4921-afa7-98e653300c67
uuid4() in parent -> 1df59e77-c35a-4921-afa7-98e653300c67
Note that you do not need PID NSes for this to work. The first bullet in the list above would function something like,
- In the initial process, call
uuid.uuid4()to primerandom_buf; let's call this pid A, sorandom_last_pidisA. - Fork, let the parent die; PID A is now free.
- In a loop, spawn children until you obtain PID A. In the child, if the child's PID is A, generate a UUID.
- All children where the child PID is A will obtain the same UUID.
(This has the benefit of not needing ctypes or PID NSes to accomplish.)
(Note that the PID NS example requires root, in order to unshare the PID NS. Unsharing the PID NS as a normal user should also be possible, but it requires more setup that I didn't want to hassle with. I.e., it should be possible to write a more complex PoC that doesn't require sudo, so I wouldn't focus on that.)
(Admittedly, the circumstances required for this might be difficult to run into.)
CPython versions tested on:
CPython main branch
(n.b., at the time of writing, this only applies to main. The latest released version, 3.15.1, does not contain the bug discussed here.)
Operating systems tested on:
Linux
- Langage dominant
- Python
- Étoiles
- 77.7k
- Forks
- 38.3k
- Merge moyen
- 14 h 29 min
- PR mergées (30 j)
- 699
Préparer son environnement
Lance le conteneur de développement du projet dans votre navigateur, avec votre propre compte GitHub.
- Aucun Dockerfile ni fichier Docker Compose
- Propose un modèle de pull request
- Lire le guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Autres issues de python/cpython
-
topic-socket type-bug
Difficulté 2/5 1-3 heures Accessibilité débutants 72/100
python/cpython#160561 · 7 commentaires ·
Les mainteneurs répondent en général sous 1 jour
-
tests topic-profiling type-bug
Difficulté 2/5 1-3 heures Accessibilité débutants 78/100
python/cpython#160334 · 1 commentaire ·
Les mainteneurs répondent en général sous 1 jour
-
extension-modules type-bug
Difficulté 2/5 1-3 heures Accessibilité débutants 84/100
Les mainteneurs répondent en général sous 1 jour
-
Remove unused FVS_* macrosOuverteinterpreter-core type-feature
Difficulté 1/5 Moins d'une heure Accessibilité débutants 78/100
python/cpython#160109 · 1 commentaire ·
Les mainteneurs répondent en général sous 1 jour
-
extension-modules type-bug
Difficulté 2/5 1-3 heures Accessibilité débutants 72/100
python/cpython#160001 · 3 commentaires ·
Les mainteneurs répondent en général sous 1 jour
Toutes les issues de python/cpython
Issues similaires
-
request-theme
Difficulté 1/5 Moins d'une heure Accessibilité débutants 66/100
LizardByte/ThemerrDB#9575 ·
Les mainteneurs répondent en général sous 1 jour
-
Author Page: noshin-nawalOuverteauthor-page correction
Difficulté 2/5 1-3 heures Accessibilité débutants 62/100
acl-org/acl-anthology#10599 · 1 commentaire ·
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 2/5 1-3 heures Accessibilité débutants 78/100
zilliztech/memsearch#818 ·
Les mainteneurs répondent en général sous 1 jour
-
[FAQ] Commit ID for ProjectsPeut-être pris @github-actions l’a pris aujourd’hui. Ouvertefaq-proposal
Difficulté 1/5 Moins d'une heure Accessibilité débutants 84/100
DataTalksClub/faq#503 · 1 commentaire ·
Les mainteneurs répondent en général sous 2 jours
-
README.zh-CN/ja/es still hard-code 501 test counts after README.md went count-freePeut-être pris @Aaqibhafeezkhan l’a pris aujourd’hui. Ouvertebeginner documentation good first issue
Difficulté 2/5 1-3 heures Accessibilité débutants 75/100
BistaDinesh03/critique#164 · 1 commentaire ·
Les mainteneurs répondent en général sous 1 jour