-
Notifications
You must be signed in to change notification settings - Fork 197
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
Fixed race condition that occurs when initializing the executable_allocator_is_working variable in the pcre2_jit_compile function #91
Conversation
src/pcre2_jit_compile.c
Outdated
@@ -14356,23 +14356,25 @@ return PCRE2_ERROR_JIT_BADOPTION; | |||
|
|||
if ((re->flags & PCRE2_NOJIT) != 0) return 0; | |||
|
|||
if (executable_allocator_is_working == 0) | |||
int executable_allocator_is_working_local = executable_allocator_is_working; | |||
if (executable_allocator_is_working_local == 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The point of this check is checking once that the allocator is working. If it does not, it disables jit compiling. It can be run in multiple threads. Of course the allocator may fail to allocate memory later, but at least it is working, so there is a chance it can do it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic you described is absolutely clear to me.
I'll try to explain the error.
Parallel execution of 2 threads can be like this:
- variable
executable_allocator_is_working
==0
- 1st thread executes
if (executable_allocator_is_working == 0)
- 2nd thread executes
if (executable_allocator_is_working == 0)
- 1st thread executes
void *ptr = SLJIT_MALLOC_EXEC(32, NULL);
- 2nd thread starts execute
void *ptr = SLJIT_MALLOC_EXEC(32, NULL);
- 1st thread executes
if (ptr != NULL)
,SLJIT_FREE_EXEC(((sljit_u8*)(ptr)) + SLJIT_EXEC_OFFSET(ptr), NULL);
,executable_allocator_is_working = 1;
and pause a little
variableexecutable_allocator_is_working
==1
- 2nd thread completes the execution of
void *ptr = SLJIT_MALLOC_EXEC(32, NULL);
- 2nd thread executes
executable_allocator_is_working = -1;
variableexecutable_allocator_is_working
==-1
- 1st thread executes
if (executable_allocator_is_working < 0)
But the value ofexecutable_allocator_is_working
is now-1
,
so 1st thread executesreturn PCRE2_ERROR_NOMEMORY;
- ERROR!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wouldn't it then be better to make the assignment of executable_allocator_is_working as part of the else?
if (ptr != NULL)
{
SLJIT_FREE_EXEC(((sljit_u8*)(ptr)) + SLJIT_EXEC_OFFSET(ptr), NULL);
executable_allocator_is_working = 1;
}
else executable_allocator_is_working = -1;
as documented around lines 14362 to 14365, 2 different threads should follow the same branch regardless. maybe changing the wording of the last sentence might be needed to make that more clear.
there is always a possible race condition, but any application that gets PCRE2_ERROR_NOMEMORY from this function (under this check) is expected to disable JIT, since the only way that would be triggered, is because the system is no longer allowing RWX pages (ex: SELinux enabled
) and the alternative is to segfault.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most likely this will also work correctly:
if (executable_allocator_is_working == 0)
{
/* Checks whether the executable allocator is working. This check
might run multiple times in multi-threaded environments, but the
result should not be affected by it. */
void *ptr = SLJIT_MALLOC_EXEC(32, NULL);
if (ptr != NULL)
{
SLJIT_FREE_EXEC(((sljit_u8*)(ptr)) + SLJIT_EXEC_OFFSET(ptr), NULL);
executable_allocator_is_working = 1;
}
else executable_allocator_is_working = -1;
}
if (executable_allocator_is_working < 0)
return PCRE2_ERROR_NOMEMORY;
Which implementation will we use?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first version has the advantage that there is only one place where the value of the executable_allocator_is_working
variable is set.
But it's not very important.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer the later, but since you are already touching it might as well change it to so it is a more common tri state (-1 = unknown, 0 = false, 1 = true), which will also clean the last check so it will be instead the more reasonable :
if (!executable_alllocator_is_working)
return PCRE2_ERROR_NOMEMORY;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do I understand correctly what you are suggesting to write like this?
static int executable_allocator_is_working = -1;
...
if (executable_allocator_is_working == -1)
{
/* Checks whether the executable allocator is working. This check
might run multiple times in multi-threaded environments, but the
result should not be affected by it. */
void *ptr = SLJIT_MALLOC_EXEC(32, NULL);
if (ptr != NULL)
{
SLJIT_FREE_EXEC(((sljit_u8*)(ptr)) + SLJIT_EXEC_OFFSET(ptr), NULL);
executable_allocator_is_working = 1;
}
else executable_allocator_is_working = 0;
}
if (!executable_allocator_is_working)
return PCRE2_ERROR_NOMEMORY;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
7101009
to
f2fdfd5
Compare
Maybe you could use a switch:
|
An alternative (WIP) provided in the external branch, which has the following advantages:
since it also expands the PCRE2 public API, this could be added to the guidelines and suggest doing it single threaded so it will be thread safe later when queried and without incurring in the costs of having to maintain a lock |
…ocator_is_working variable in the pcre2_jit_compile function
f2fdfd5
to
db861f2
Compare
I rebased this PR branch on current master |
@@ -14384,7 +14384,7 @@ pcre2_jit_compile(pcre2_code *code, uint32_t options) | |||
pcre2_real_code *re = (pcre2_real_code *)code; | |||
#ifdef SUPPORT_JIT | |||
executable_functions *functions; | |||
static int executable_allocator_is_working = 0; | |||
static int executable_allocator_is_working = -1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a problem with 0 value? Normally this can to .bss
instead of initialized data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the explicity/implicit initialization is not the problem here, I think this code change cames from the point of view of making the code easier to reason with by instead moving to a tri-state boolean type where the "unknown" state is -1 instead of 0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If clarify is that important for this code, enums should be used. I don't think a -1 is better than 0 to understand this code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree though, using -1 makes it simpler to write a test that says "fail with error if we either don't know or cannot allocate memory" by doing below:
if (!executable_allocator_is_working)
return PCRE2_ERROR_NOMEMORY;
I have no strong opinions on either implementations though, but I think I happen to know the guy that wrote this code originally, and his choice of 0 for "dunno" and -1 for "no" came from being super lazy and trying to reuse the error response from the underlying functions to signal "no", and not having to initialize the static for "dunno"., and since we are touching this code, it might be worth changing it now as it is being inconsistently used that way in the codebase and is a little strange and unexpected when looking elsewhere in other codebases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
The error fixed by this PR
If
pcre2_jit_compiler
is called simultaneously for the first time from 2 (or more) threads, then one of the calls may fail with thePCRE2_ERROR_NOMEMORY
error.