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
Prevent race condition in guarded_import #124
Conversation
I accidentally included an |
I stopped the tests, pushed the fix and now they are running again. |
Thanks! |
Does your test actually provoke the problem? It does not in my testing. When I remove the fix the test still succeeds. |
Hm it does on my system (ArchLinux, 8 cores, Python 3.10.3)
Not sure how to provoke a race condition consistently. I was actually surprised that it worked on the first try here. |
I tried it a few more times and managed to see the error, seems to be a matter of luck with machine speed etc. |
Searched a bit and found this, which might provide a framework for somewhat more reliably provoking a race condition in a testing framework. But if I understand it correctly, the best I can hope for if I do not want to introduce additional control flow into the target function |
Thanks for the approval, but could you check if the following test more reliably provokes the problem on your machine?
|
The new code fails maybe 1 out of 4 times in the sandbox here (Python 3.7 on macOS 12.3, MacBook Pro Apple M1 Max, 64GB RAM) |
The old test fails more frequently, maybe 1 in 3 times |
@viktordick you're free to merge |
Sorry!
I just recognized that a `try ... except KeyError: pass`
is not sufficient. We must also move it after the
`_appliedModuleSecurity[mname] = modsec`.
Otherwise, a concurrent thread could run the code after
the `del` (and therefore sees `modsec = None`)
but before `_applied...` is updated; `secureModule` will (wrongly)
return `None` (rather than the module) in this case.
Let's get an abstract look at the code
and why the changed code is correct.
We have two mappings "do_do" (module -> sec_info) (named `_moduleSecurity`)
with module security (likely) still to be applied
and "done" (module -> sec_info) (named `_appliedModuleSecurity`)
with module security already applied.
The fixed code (essentially) looks like:
```
modsec = to_do.get("module")
if modsec is None:
# either no security declarations known or already applied
return module if module in done else None
else:
# security declarations known; probably not yet applied
... apply modsec to module ...
done[module] = modsec
# it is now safe to remove from `to_do`
try: del to_do[module]
except KeyError: pass
return module
```
Why is this (almost) safe?
If we do not have security information for *module*,
it will never be in `to_do` and therefore never be in `done`.
The module will get rejected.
If we have security information for *module*,
it will initially be in `to_do`.
If the module is actually used, then it will get added
to `done` and only then removed from `to_do`.
Therefore, it will in this case always be either in `to_do`
or in `done` -- and therefore, it will be allowed.
This is safe, if applying *modsec* to a module is idempotent
(which is very likely true).
Why is it only almost safe?
The reasoning above assumes that `secureModule` is the only
place related to module security prone to potential race
conditions.
Likely, this is not the case:
New modules are added to `to_do` dynamically
(when `allow_module` is executed).
It is possible that those additions depend on the executed requests.
Therefore, it is possible that the allowedness of a module
depends from the request order.
This can be avoided if all `allow_module` are executed during startup.
|
OK, if I understand correctly, the info about a module being allowed to be imported is added to I guess a clean way without race conditions would be to keep the entry in The next best fix would be to change the order - first add to Regarding the "almost safe": |
Somehow I am unable to push another commit to this PR:
Maybe this is because the PR is already approved? I will try to push into a separate branch and maybe start a new PR. |
Viktor Dick wrote at 2022-3-27 04:49 -0700:
OK, if I understand correctly, the info about a module being allowed to be imported is added to `_moduleSecurity` if `allow_module` is executed, and once a module is actually used (`import ...`), this info is removed from `_moduleSecurity` and the actually imported module is added to `_appliedModuleSecurity`, correct?
Yes.
I guess a clean way without race conditions would be to keep the entry in `_moduleSecurity` and simply add an actually imported module to `_appliedModuleSecurity`. But this would require a larger rewrite.
You can safely delete *module* from `_moduleSecurityInfo`
(with `try: del ... except KeyError: pass`) provided
that you do this *after* the addition to `_applied...`.
Remaining race conditions do not come from `secureModule` (alone)
but from concurrent additions to `_moduleSecurityInfo` (by `allow_module`)
and `secureModule` calls. Not changing `_moduleSecurityInfo` in
`secureModule` will not help in this case.
The next best fix would be to change the order - first add to `_appliedModuleSecurity`, then remove from `_moduleSecurity` - which is your suggestion, correct?
Yes.
|
Closing this as the discussed change has been implemented in #125 |
No description provided.