-
Notifications
You must be signed in to change notification settings - Fork 10
perf(swig): release Python GIL, skip JNI detach, use critical array access #82
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -184,11 +184,14 @@ int coraza_set_debug_log_callback(coraza_waf_config_t cfg, PyObject *cb) { | |
| %typemap(jstype) (const unsigned char *data, int length) "byte[]" | ||
| %typemap(javain) (const unsigned char *data, int length) "$javainput" | ||
| %typemap(in) (const unsigned char *data, int length) { | ||
| $1 = (unsigned char *)JCALL2(GetByteArrayElements, jenv, $input, NULL); | ||
| /* GetPrimitiveArrayCritical avoids a copy on most JVMs by pinning the | ||
| * heap array directly. The critical section ends in argout below; no | ||
| * other JNI calls may occur between Get and Release. */ | ||
| $1 = (unsigned char *)JCALL2(GetPrimitiveArrayCritical, jenv, $input, NULL); | ||
| $2 = (int)JCALL1(GetArrayLength, jenv, $input); | ||
| } | ||
| %typemap(argout) (const unsigned char *data, int length) { | ||
| JCALL3(ReleaseByteArrayElements, jenv, $input, (jbyte *)$1, JNI_ABORT); | ||
| JCALL3(ReleasePrimitiveArrayCritical, jenv, $input, (jbyte *)$1, JNI_ABORT); | ||
| } | ||
|
Comment on lines
186
to
195
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# First, locate and examine the coraza.i file around lines 186-195
find . -name "coraza.i" -type fRepository: corazawaf/libcoraza Length of output: 73 🏁 Script executed: #!/bin/bash
# Check the structure to understand the codebase layout
git ls-files | head -30Repository: corazawaf/libcoraza Length of output: 613 🏁 Script executed: #!/bin/bash
# Search for coraza_append_request_body and coraza_append_response_body
rg "coraza_append_(request|response)_body" --type cRepository: corazawaf/libcoraza Length of output: 45 🏁 Script executed: #!/bin/bash
# Search for callback registration patterns in Go files
rg "call_debug_log_cb|call_error_cb" --type go -A 5Repository: corazawaf/libcoraza Length of output: 1064 🏁 Script executed: #!/bin/bash
# Read coraza.i around lines 186-195
sed -n '180,200p' coraza.iRepository: corazawaf/libcoraza Length of output: 1062 🏁 Script executed: #!/bin/bash
# Search for body append functions more broadly
rg "append.*body|body.*append" --type-list | head -5
rg "append.*body|body.*append" -iRepository: corazawaf/libcoraza Length of output: 5219 🏁 Script executed: #!/bin/bash
# Look at the Go file structure to understand callback registration
sed -n '100,180p' libcoraza/coraza.goRepository: corazawaf/libcoraza Length of output: 3034 🏁 Script executed: #!/bin/bash
# Check if there are any references to WAF body processing functions
rg "waf.*body|body.*waf" -iRepository: corazawaf/libcoraza Length of output: 45 🏁 Script executed: #!/bin/bash
# Get the full implementation of coraza_append_request_body
rg -A 20 "//export coraza_append_request_body" libcoraza/coraza.goRepository: corazawaf/libcoraza Length of output: 872 🏁 Script executed: #!/bin/bash
# Get the full implementation of coraza_append_response_body
rg -A 20 "//export coraza_append_response_body" libcoraza/coraza.goRepository: corazawaf/libcoraza Length of output: 753 🏁 Script executed: #!/bin/bash
# Check if AppendRequestBody or AppendResponseBody can trigger callbacks
rg -A 10 "AppendRequestBody|AppendResponseBody" libcoraza/coraza.goRepository: corazawaf/libcoraza Length of output: 45 🏁 Script executed: #!/bin/bash
# Look at lines around where the callbacks are actually used
sed -n '250,350p' libcoraza/coraza.goRepository: corazawaf/libcoraza Length of output: 3093 🏁 Script executed: #!/bin/bash
# Check what the WAF does during body processing - search for callback invocations
rg "WithErrorCallback|WithDebugLogger" libcoraza/coraza.go -B 2 -A 10Repository: corazawaf/libcoraza Length of output: 1525 🏁 Script executed: #!/bin/bash
# Check JNI documentation about GetArrayLength in critical sections
# Also search for what actually happens during WriteRequestBody/WriteResponseBody
# Check if there's Go interface or type definition showing what WriteRequestBody does
rg "WriteRequestBody|WriteResponseBody" libcoraza/coraza.go -B 5 -A 5Repository: corazawaf/libcoraza Length of output: 796 🏁 Script executed: #!/bin/bash
# Check if the WAF processes/matches rules during WriteRequestBody or only during ProcessRequestBody
rg "WriteRequestBody|WriteResponseBody" -i --type go | head -20Repository: corazawaf/libcoraza Length of output: 368 🏁 Script executed: #!/bin/bash
# Look at the coraza.i file more broadly to understand all the typemaps
sed -n '180,250p' coraza.iRepository: corazawaf/libcoraza Length of output: 3522 🏁 Script executed: #!/bin/bash
# Check if there are any other JNI calls or callback invocations that happen during body writing
# Look for what the Java callback examples do
cat examples/java/CorazaDebugLogCallback.javaRepository: corazawaf/libcoraza Length of output: 982 🏁 Script executed: #!/bin/bash
# Check the error callback example too
cat examples/java/CorazaErrorCallback.javaRepository: corazawaf/libcoraza Length of output: 1038
Line 191 calls Minimal safe fallback %typemap(in) (const unsigned char *data, int length) {
- $1 = (unsigned char *)JCALL2(GetPrimitiveArrayCritical, jenv, $input, NULL);
$2 = (int)JCALL1(GetArrayLength, jenv, $input);
+ $1 = (unsigned char *)JCALL2(GetByteArrayElements, jenv, $input, NULL);
}
%typemap(argout) (const unsigned char *data, int length) {
- JCALL3(ReleasePrimitiveArrayCritical, jenv, $input, (jbyte *)$1, JNI_ABORT);
+ JCALL3(ReleaseByteArrayElements, jenv, $input, (jbyte *)$1, JNI_ABORT);
}🤖 Prompt for AI Agents |
||
|
|
||
| /* | ||
|
|
@@ -215,25 +218,21 @@ typedef struct { | |
| static void _swig_java_error_trampoline(void *ctx, coraza_matched_rule_t rule) { | ||
| _swig_java_cb_ctx_t *jctx = (_swig_java_cb_ctx_t *)ctx; | ||
| JNIEnv *env = NULL; | ||
| jboolean attached = JNI_FALSE; | ||
| if ((*jctx->jvm)->GetEnv(jctx->jvm, (void **)&env, JNI_VERSION_1_6) == JNI_EDETACHED) { | ||
| /* Attach as a daemon if needed and leave the thread attached. Daemon | ||
| * threads are cleaned up by the JVM on exit, so DetachCurrentThread is | ||
| * unnecessary and adds ~microseconds of JVM registry overhead per call. */ | ||
| if ((*jctx->jvm)->GetEnv(jctx->jvm, (void **)&env, JNI_VERSION_1_6) == JNI_EDETACHED) | ||
| (*jctx->jvm)->AttachCurrentThreadAsDaemon(jctx->jvm, (void **)&env, NULL); | ||
|
Comment on lines
+221
to
225
|
||
| attached = JNI_TRUE; | ||
| } | ||
| (*env)->CallVoidMethod(env, jctx->obj, jctx->mid, (jlong)(uintptr_t)rule); | ||
| if ((*env)->ExceptionCheck(env)) (*env)->ExceptionDescribe(env); | ||
|
Comment on lines
226
to
227
|
||
| if (attached) (*jctx->jvm)->DetachCurrentThread(jctx->jvm); | ||
| } | ||
|
|
||
| static void _swig_java_debug_trampoline(void *ctx, coraza_debug_log_level_t level, | ||
| const char *msg, const char *fields) { | ||
| _swig_java_cb_ctx_t *jctx = (_swig_java_cb_ctx_t *)ctx; | ||
| JNIEnv *env = NULL; | ||
| jboolean attached = JNI_FALSE; | ||
| if ((*jctx->jvm)->GetEnv(jctx->jvm, (void **)&env, JNI_VERSION_1_6) == JNI_EDETACHED) { | ||
| if ((*jctx->jvm)->GetEnv(jctx->jvm, (void **)&env, JNI_VERSION_1_6) == JNI_EDETACHED) | ||
| (*jctx->jvm)->AttachCurrentThreadAsDaemon(jctx->jvm, (void **)&env, NULL); | ||
| attached = JNI_TRUE; | ||
| } | ||
| jstring jmsg = (*env)->NewStringUTF(env, msg ? msg : ""); | ||
|
Comment on lines
233
to
236
|
||
| jstring jfields = (*env)->NewStringUTF(env, fields ? fields : ""); | ||
| /* NewStringUTF returns NULL on OOM; skip the call rather than crash. */ | ||
|
|
@@ -243,7 +242,6 @@ static void _swig_java_debug_trampoline(void *ctx, coraza_debug_log_level_t leve | |
| if (jmsg) (*env)->DeleteLocalRef(env, jmsg); | ||
| if (jfields) (*env)->DeleteLocalRef(env, jfields); | ||
| if ((*env)->ExceptionCheck(env)) (*env)->ExceptionDescribe(env); | ||
|
||
| if (attached) (*jctx->jvm)->DetachCurrentThread(jctx->jvm); | ||
| } | ||
|
|
||
| JNIEXPORT jint JNICALL Java_coraza_coraza_1set_1error_1callback( | ||
|
|
@@ -353,7 +351,25 @@ typedef enum coraza_severity_t { | |
|
|
||
| /* | ||
| * Function declarations | ||
| * | ||
| * Release the Python GIL for calls that cross into Go and do real work, so | ||
| * other Python threads can run concurrently. Trivial accessors and free | ||
| * functions are excluded because the GIL release/reacquire overhead would | ||
| * exceed any benefit. | ||
| */ | ||
| #ifdef SWIGPYTHON | ||
| %thread coraza_new_waf; | ||
| %thread coraza_rules_merge; | ||
| %thread coraza_process_connection; | ||
| %thread coraza_process_uri; | ||
| %thread coraza_process_request_headers; | ||
| %thread coraza_process_request_body; | ||
| %thread coraza_append_request_body; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -type f -name "coraza.i" | head -5Repository: corazawaf/libcoraza Length of output: 73 🏁 Script executed: find . -type f -path "*libcoraza/coraza.go" | head -5Repository: corazawaf/libcoraza Length of output: 84 🏁 Script executed: git ls-files | grep -E "(coraza\.i|coraza\.go)" | head -10Repository: corazawaf/libcoraza Length of output: 91 🏁 Script executed: wc -l coraza.iRepository: corazawaf/libcoraza Length of output: 75 🏁 Script executed: cat -n coraza.i | sed -n '60,90p'Repository: corazawaf/libcoraza Length of output: 1292 🏁 Script executed: cat -n coraza.i | sed -n '360,380p'Repository: corazawaf/libcoraza Length of output: 1095 🏁 Script executed: rg "coraza_append_request_body|coraza_append_response_body" coraza.i -A 3 -B 1Repository: corazawaf/libcoraza Length of output: 979 🏁 Script executed: rg "coraza_process_request_body|coraza_process_response_body" coraza.i -A 3 -B 1Repository: corazawaf/libcoraza Length of output: 1000 🏁 Script executed: wc -l libcoraza/coraza.goRepository: corazawaf/libcoraza Length of output: 86 🏁 Script executed: rg "func.*append_request_body|func.*append_response_body" libcoraza/coraza.go -A 20Repository: corazawaf/libcoraza Length of output: 1554 🏁 Script executed: cat -n libcoraza/coraza.go | sed -n '1,50p'Repository: corazawaf/libcoraza Length of output: 1570 🏁 Script executed: rg "%thread" coraza.i -B 5 -A 5 | head -40Repository: corazawaf/libcoraza Length of output: 843 🏁 Script executed: cat -n coraza.i | sed -n '67,90p'Repository: corazawaf/libcoraza Length of output: 1028 🏁 Script executed: python3 << 'EOF'
# Check Python's bytearray behavior regarding pointer validity after resize
# This is to understand the hazard
print("C.GoBytes behavior:")
print("- Makes a memcpy of data from C pointer")
print("- Happens AFTER GIL is released (due to %thread)")
print("")
print("PyByteArray_AS_STRING behavior:")
print("- Returns direct pointer to internal buffer")
print("- If bytearray is resized, pointer becomes invalid")
print("- If GIL is released, another thread can resize")
print("")
print("Race condition window:")
print("1. SWIG typemap calls PyByteArray_AS_STRING (GIL held)")
print("2. %thread decorator releases GIL")
print("3. Another Python thread resizes/reallocates bytearray")
print("4. Pointer extracted in step 1 now points to freed memory")
print("5. C.GoBytes attempts memcpy from invalid pointer")
print("6. Result: undefined behavior (crash, data corruption)")
EOFRepository: corazawaf/libcoraza Length of output: 678 Remove These functions use the typemap that extracts a raw pointer from Minimal fix %thread coraza_process_request_body;
-%thread coraza_append_request_body;
%thread coraza_process_response_headers;
%thread coraza_process_response_body;
-%thread coraza_append_response_body;
%thread coraza_process_logging;Also applies to: 370-370 🤖 Prompt for AI Agents |
||
| %thread coraza_process_response_headers; | ||
| %thread coraza_process_response_body; | ||
| %thread coraza_append_response_body; | ||
| %thread coraza_process_logging; | ||
| #endif | ||
|
|
||
| extern coraza_waf_config_t coraza_new_waf_config(); | ||
| extern int coraza_rules_add_file(coraza_waf_config_t c, const char *file); | ||
|
|
||
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.
Using GetPrimitiveArrayCritical here keeps the Java byte[] pinned until the typemap(argout) runs (i.e., after the Go call returns). That can block/stall JVM GC for the full duration of request/response body processing, and the current code also calls GetArrayLength while the array is in a critical section (contradicting the comment that no other JNI calls may occur). Prefer a non-critical access pattern (e.g., GetByteArrayElements / GetByteArrayRegion) and ensure any critical region is entered only after length is known and held for the shortest possible time.