-
-
Notifications
You must be signed in to change notification settings - Fork 6.9k
Race condition in validators #5760
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
Comments
This is a failing test for encode#5760
Nice catch and very nice work. |
Sorry for the noise, but recently I thought that problem of race condition is that validation was working on while it didn't really make a query. So it looks like this, correct me pls if I will wrong. Step 1 - retrieve the value with the unique constraint for a checking. But between step 1 and step 2 another record has been inserted. |
@dennypenta your use case is different from the OP's. |
@dennypenta Here's the issue related to your case: #3876 The issue was closed with this note:
If this happens only for one of your endpoints, you can catch the IntegrityError in the serializer or view and return 4xx instead of 5xx. If you want to solve it in a generic way, you can write a custom exception handler. Here's an example: from django.db.utils import IntegrityError
from rest_framework.status import HTTP_400_BAD_REQUEST
from rest_framework.views import exception_handler
def my_exception_handler(exc, context):
if isinstance(exc, IntegrityError) and \
'duplicate key value violates unique constraint' in str(exc):
return Response(
'Unique constraint violated',
status=HTTP_400_BAD_REQUEST,
)
return exception_handler(exc, context) |
Is there anything I can do to move my PR #5762 forward? |
@michael-k: Not yet. It's milestoned for 3.8, which means we'll resolve it one way or the other before then. Most likely it's fine and we'll take it as-is — it looks great at first glance — but I just need to sit down with it to review it properly. |
Good job catching this issue! I guess we need some more eyes and opinions to look over the changes in PR #5762 ? |
Presumably |
I've tried |
@michael-k You are running DRF in multiple threads in the same process, right? And the bug happens when Python decides to release the GIL just between the This is a really bad bug, potentially causing silent data corruption. @michael-k's PR #6172 fixes it, and makes the code cleaner and safer in the process by removing the unneeded |
Removing from the milestone, as we're tracking through the PR. |
* Do not persist the context in validators Fixes encode#5760 * Drop set_context() in favour of 'requires_context = True'
* Do not persist the context in validators Fixes encode#5760 * Drop set_context() in favour of 'requires_context = True'
Checklist
master
branch of Django REST framework.Steps to reproduce
Preparation
Create these 2 models with the serializer and the view:
Exploit
Create two Documents (can be in the same collection) and send update requests for both documents in parallel:
Expected behavior
The documents are updated without any side effects.
Actual behavior
We run into
500
status codes. (ie. IntegrityError: duplicate key value violates unique constraint "document_document_collection_id_1234_uniq"; DETAIL: Key (collection_id, uid)=(1, 'foo') already exists.)It's even worse, when the documents are in different collections. Then one of them gets the other one's uid assigned.
The problem
Serializers are instances of
rest_framework.fields.Field
and the following code (inside ofrun_validators
) is responsible for running validators:django-rest-framework/rest_framework/fields.py
Lines 533 to 538 in 78367ba
validator.set_context()
setsvalidator.instance
:django-rest-framework/rest_framework/validators.py
Lines 106 to 112 in 78367ba
Then
validator()
uses this instance.validator
is the same instance for every instance of the serializer:DocumentSerializer(instance=d).validators[0] is DocumentSerializer(instance=d2).validators[0]
. If two serializers callvalidator.set_context(self)
before any one of them callsvalidator(value)
, both use the same instance.Changing uid
validator.filter_queryset()
addsuid
(not sure whycollection
did not behave as I expected) toattr
, which reference the serializersvalidated_data
. This wayuid
of the wrong instance ends up in serializer andserializer.save()
uses it.django-rest-framework/rest_framework/validators.py
Lines 130 to 139 in 78367ba
Is this only theoretical?
uid
happened reliably on every execution of the script against our staging plattform.Possible fixes
Pass
serializer
/serializer_field
to__call__
and dropset_context
.Django's validators do not expect a second argument. According to drf's documentation one “can use any of Django's existing validators“.
This can be solved by using inspect (which has differents APIs for Python 2 and 3) or by catching the
TypeError
(which could come from within the call). => Both solutions have their downsides.This would be a breaking change!
Clone the validator before calling
set_context
.The text was updated successfully, but these errors were encountered: