-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Note where a session was already started #10736
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
Conversation
Duplicated session starts can be annoying to debug. The error that occurs when a session is already active doesn't tell you where it was initialized, so figuring out the callsite involves manual debugging to find it out. This keeps track of the call site of session_start as a request global, and frees at the end of the request. It should make it easier to find these instances for PHP users. The resulting message can look like: Notice: session_start(): Ignoring session_start() because a session is already active (started from /home/calvin/src/php-src/inc.php on line 4) in /home/calvin/src/php-src/index.php on line 9 Fixes phpGH-10721
Major caveat: the Zend allocator thinks this leaks:
...but shouldn't the RSHUTDOWN for the module take care of this if I free there? Oddly, Valgrind has no complaints for the tests that do fail (when I do |
This is because Zend's leak detector frees it for you (check the line "Freeing 0x..."). |
I thought |
Sorry, my mistake. A minimal reproducer for your leak is: <?php
session_start();
session_write_close();
session_start(); // creates leak because it overwrites the old filename, without freeing the old one That's because session_write_close() calls php_session_flush() which will set the session status to none. Then the session initialisation code assumes that no filename has been set yet, but it actually has one still set. |
In fact there are a couple of places where the session status may be set to none. (*) nicer imo, because PS() will have a more consistent state. |
If this was already initialized, we'd forget it. Have shared free between session_start and RSHUTDOWN.
Ah, I forgot that if it was already initialized earlier, it would be overwritten because of that. I don't think putting it flush is appropriate; cleaning it RSHUTDOWN and when initializing seems to be adequate. |
I think this PR is a nice improvement. I remember in the past having issues debugging duplicate session starts. So thanks for working on this. |
"header already sent" is not only a session problem, but also when sending a |
This one is slightly different, this one is about sessions already being active, not about header themself. |
The title of #10721 mention "header already sent" which this PR does not fix (but it claims so in the description). |
The referenced issue is unfortunately a little inconsistent. The title mentions headers but the body mentions sessions. I think another PR to handle headers and the status could could be useful, though more care would have to be taken because I think there’s a lot more places where the HTTP header can be sent AFAIK. |
I quickly checked and
|
I guess further questions for the session stuff specifically:
|
Reading around, I think things core PHP use this properly (i.e. |
https://3v4l.org/LrUEr for |
ext/session/session.c
Outdated
|
||
/* Should these be set here, or in session_initialize? */ | ||
php_session_cleanup_filename(); | ||
PS(session_started_filename) = estrdup(zend_get_executed_filename()); |
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.
Can't we just use zend_get_executed_filename_ex
to avoid the string duplication?
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.
This diff would work:
diff --git a/ext/session/php_session.h b/ext/session/php_session.h
index 2f26cd16b5..341aac5716 100644
--- a/ext/session/php_session.h
+++ b/ext/session/php_session.h
@@ -154,7 +154,7 @@ typedef struct _php_ps_globals {
const ps_module *default_mod;
void *mod_data;
php_session_status session_status;
- char *session_started_filename;
+ zend_string *session_started_filename;
uint32_t session_started_lineno;
zend_long gc_probability;
zend_long gc_divisor;
diff --git a/ext/session/session.c b/ext/session/session.c
index f853da4b65..aaaaf98b38 100644
--- a/ext/session/session.c
+++ b/ext/session/session.c
@@ -126,7 +126,7 @@ static inline void php_rinit_session_globals(void) /* {{{ */
static inline void php_session_cleanup_filename(void) /* {{{ */
{
if (PS(session_started_filename)) {
- efree(PS(session_started_filename));
+ zend_string_release_ex(PS(session_started_filename), 0);
PS(session_started_filename) = NULL;
PS(session_started_lineno) = 0;
}
@@ -1505,7 +1505,7 @@ PHPAPI zend_result php_session_start(void) /* {{{ */
switch (PS(session_status)) {
case php_session_active:
if (PS(session_started_filename)) {
- php_error(E_NOTICE, "Ignoring session_start() because a session has already been started (started from %s on line %u)", PS(session_started_filename), PS(session_started_lineno));
+ php_error(E_NOTICE, "Ignoring session_start() because a session has already been started (started from %s on line %u)", ZSTR_VAL(PS(session_started_filename)), PS(session_started_lineno));
} else {
php_error(E_NOTICE, "Ignoring session_start() because a session has already been started");
}
@@ -1621,8 +1621,11 @@ PHPAPI zend_result php_session_start(void) /* {{{ */
/* Should these be set here, or in session_initialize? */
php_session_cleanup_filename();
- PS(session_started_filename) = estrdup(zend_get_executed_filename());
- PS(session_started_lineno) = zend_get_executed_lineno();
+ zend_string *session_started_filename = zend_get_executed_filename_ex();
+ if (session_started_filename != NULL) {
+ PS(session_started_filename) = zend_string_copy(session_started_filename);
+ PS(session_started_lineno) = zend_get_executed_lineno();
+ }
return SUCCESS;
}
@@ -2538,7 +2541,7 @@ PHP_FUNCTION(session_start)
if (PS(session_status) == php_session_active) {
if (PS(session_started_filename)) {
- php_error_docref(NULL, E_NOTICE, "Ignoring session_start() because a session is already active (started from %s on line %d)", PS(session_started_filename), PS(session_started_lineno));
+ php_error_docref(NULL, E_NOTICE, "Ignoring session_start() because a session is already active (started from %s on line %d)", ZSTR_VAL(PS(session_started_filename)), PS(session_started_lineno));
} else {
php_error_docref(NULL, E_NOTICE, "Ignoring session_start() because a session is already active");
}
...but it breaks ext/session/tests/session_start_variation9.diff
:
*** Testing session_start() : variation ***
string(%d) "%s"
004+ Notice: session_start(): Ignoring session_start() because a session is already active in /home/calvin/src/php-src/ext/session/tests/session_start_variation9.php on line 8
004- Notice: session_start(): Ignoring session_start() because a session is already active (started from %s on line %d) in %s on line %d
bool(true)
string(%d) "%s"
bool(true)
--
I think |
I did realize that and fixed it after I had posted the diff. I'm still curious why the |
@NattyNarwhal I missed that part. I don't know, the two functions |
@NattyNarwhal Oh, are you sure the result isn't actually |
Nope, it's null and thus uses the old message:
|
@NattyNarwhal I was referring to the old variation with |
Ugh, you're right. I guess I should probably revert the change to that test then, or is the current output actually desirable? |
@NattyNarwhal |
Also fix the improper test case.
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.
This looks like a nice improvement!
Mainly nits. :)
WRT this, I do realize the test that caused this was setting |
Would it be possible to record the file/position that caused the auto-start of the session? |
diff --git a/ext/session/session.c b/ext/session/session.c
index 95485cba67..e79656691a 100644
--- a/ext/session/session.c
+++ b/ext/session/session.c
@@ -1513,6 +1513,9 @@ PHPAPI zend_result php_session_start(void) /* {{{ */
case php_session_active:
if (PS(session_started_filename)) {
php_error(E_NOTICE, "Ignoring session_start() because a session has already been started (started from %s on line %"PRIu32")", ZSTR_VAL(PS(session_started_filename)), PS(session_started_lineno));
+ } else if (PS(auto_start)) {
+ /* This option can't be changed at runtime, so we can assume it's because of this */
+ php_error(E_NOTICE, "Ignoring session_start() because a session has already been started automatically");
} else {
php_error(E_NOTICE, "Ignoring session_start() because a session has already been started");
}
@@ -2541,6 +2544,9 @@ PHP_FUNCTION(session_start)
if (PS(session_status) == php_session_active) {
if (PS(session_started_filename)) {
php_error_docref(NULL, E_NOTICE, "Ignoring session_start() because a session is already active (started from %s on line %"PRIu32")", ZSTR_VAL(PS(session_started_filename)), PS(session_started_lineno));
+ } else if (PS(auto_start)) {
+ /* This option can't be changed at runtime, so we can assume it's because of this */
+ php_error_docref(NULL, E_NOTICE, "Ignoring session_start() because a session is already automatically active");
} else {
php_error_docref(NULL, E_NOTICE, "Ignoring session_start() because a session is already active");
}
diff --git a/ext/session/tests/session_start_variation9.phpt b/ext/session/tests/session_start_variation9.phpt
index e4c6e57381..3fdcdcffbb 100644
--- a/ext/session/tests/session_start_variation9.phpt
+++ b/ext/session/tests/session_start_variation9.phpt
@@ -26,7 +26,7 @@
*** Testing session_start() : variation ***
string(%d) "%s"
-Notice: session_start(): Ignoring session_start() because a session is already active in %s on line %d
+Notice: session_start(): Ignoring session_start() because a session is already automatically active in %s on line %d
bool(true)
string(%d) "%s"
bool(true) Seems to pass tests, at least. |
What I meant was, can we record the location of the statement (like |
Based on my reading of the source and trying to experiment with it, I don't think it can get initialized when I set |
@NattyNarwhal Ah right, I've never used this setting and assumed the behavior would be different. |
Easy to forget that you have this set, in which case, session start is done at RINIT outside of user code. Because this config option can't change at runtime, we can check for it and make the error more specific if that's the case.
ASAN Failure is legit |
I can't reproduce it; clang ASan seems to pass just fine, and gcc ASan does give me an error, but it's incredibly useless (no stack trace, wrong thread ID, just sits paused until I ^C it):
|
Well the failure is legit as the Release ZTS build segfaults on that regression bug, I'll have a look during the week to see if I can reproduce it. |
I don't doubt it either; I just never have good luck with ASan. I'll try rebuilding with the right arguments tomorrow; hopefully it reproduces fine under Valgrind instead. |
@NattyNarwhal The failing test seems to call diff --git a/ext/session/session.c b/ext/session/session.c
index 919b16b158..7aa80a1997 100644
--- a/ext/session/session.c
+++ b/ext/session/session.c
@@ -110,8 +110,6 @@ static inline void php_rinit_session_globals(void) /* {{{ */
/* TODO: These could be moved to MINIT and removed. These should be initialized by php_rshutdown_session_globals() always when execution is finished. */
PS(id) = NULL;
PS(session_status) = php_session_none;
- PS(session_started_filename) = NULL;
- PS(session_started_lineno) = 0;
PS(in_save_handler) = 0;
PS(set_handler) = 0;
PS(mod_data) = NULL;
@@ -2880,6 +2878,8 @@ static PHP_MINIT_FUNCTION(session) /* {{{ */
my_module_number = module_number;
PS(module_number) = module_number;
+ PS(session_started_filename) = NULL;
+ PS(session_started_lineno) = 0;
PS(session_status) = php_session_none;
REGISTER_INI_ENTRIES(); |
I did manage to reproduce that failure with release ZTS; that patch fixed it. |
Thank you! |
Duplicated session starts can be annoying to debug. The error that occurs when a session is already active doesn't tell you where it was initialized, so figuring out the callsite involves manual debugging to find it out.
This keeps track of the call site of session_start as a request global, and frees at the end of the request. It should make it easier to find these instances for PHP users.
The resulting message can look like:
Fixes GH-10721