Use a critical section to protect getlist() - #9853
Conversation
|
While you're in
n = PySequence_Size(arg); /* -> __len__ */
...
seq = PySequence_Fast(arg, must_be_sequence); /* iterates; may be shorter */
for (i = 0; i < n; i++) {
op = PySequence_Fast_GET_ITEM(seq, i); /* reads past the end when n > len(seq) */Deterministic, single-threaded, GIL on: class Lying:
def __len__(self): return 256 # claims 256
def __getitem__(self, i): # yields 8, then stops
if i >= 8: raise IndexError
return float(i)
Image.new("L", (4, 4)).point(Lying(), "F")
Clamping the loop to the sequence actually returned fixes it: Py_ssize_t seq_n = PySequence_Fast_GET_SIZE(seq);
if (n > seq_n) { n = seq_n; }Not raising this as a security issue — the trigger is an object the caller supplies, so anything that can reach it is already running Python in the process. Flagging it because a critical section around the existing loop would leave it in place, and it seemed worth having in front of you while the function is open. |
|
Thanks, you're right about Two corrections on the other paths. On the fix, I'd rather not clamp. In seq = PySequence_Fast(arg, must_be_sequence);
if (!seq) {
return NULL;
}
n = PySequence_Fast_GET_SIZE(seq);
if (length && wrong_length && n != *length) {
PyErr_SetString(PyExc_ValueError, wrong_length);
Py_DECREF(seq);
return NULL;
}The I built that locally: your example now raises "wrong number of lut entries", the transform one raises "wrong number of matrix entries", It is independent of the race, though. The critical section doesn't help here, and this doesn't close the race either, since a real list can still shrink between the size read and the loop. So I'd keep this PR on #9852 and raise the other one separately, unless a maintainer would rather have both here. |
|
Thanks for checking it properly — and you're right on both corrections. The qtables path was my error. Your fix is better than the clamp and I'd drop mine. The clamp silently hands back a zero-filled LUT and writes the clamped count through One thing that may sharpen the "independent of the race" point in your favour: the loop calls Agreed on keeping this PR scoped to the race and #9852. Happy for you to raise the size one — you have the tested branch and the better patch, so it should be yours; I'll bring the reproducers over and drop the clamp from #9854 so there's nothing overlapping to review. Say the word if you'd rather I opened it instead. |
|
Thanks, and yes, please leave the size one to me. I'll raise it separately with the reproducers I already have, so nothing overlaps with #9854. Your read of the docs is right, a critical section is suspended and its locks released if the thread blocks. In |
Fixes #9852. Alternative to #9854
getlist()reads the sequence length once, then walks the list withPySequence_Fast_GET_ITEM, which does no bounds checking and no locking. On a free-threaded build, another thread shrinking the list during the walk makes the remaining reads go past the end of the storage, so Image.point() segfaults. Wrapping the walk in a critical section on the argument keeps the length valid for the duration, the same approach used for FontObject in #9498.