pecl tokyo tyrant を session.save_handler で使って、 session_regenerate_id を行うと apache がクラッシュする問題について。


セッションハイジャックに対する対策として、 php 5.1 からは、以下のようにセッションを破棄して作り直すことがあります。

session_start();
session_regenerate_id(TRUE);

ふつーに利用している分には何の問題もなくセッションIDが変更されるだけなのですが、
pecl tokyo tyrant を session_handler で使っていると、 session_regenerate_id で apache がクラッシュします。

ま た お 前 (pecl php-tokyo_tyrant) かっ!!!

前回もクラッシュする問題があったけど回避可能だった、しかし、今回は回避不能だったので、根本的な対策を行うべくパッチリリースすることにした。

パッチ

pecl tokyo_tyrant-0.4.0 に対するパッチです。

ついでに、 tch で tokyo tyrant を動作させているとクラッシュするバグも同じ関数にあったので直しておきました。
一粒で二度おいしい。

パッチの当て方

#ダウンロード
pecl download tokyo_tyrant-0.4.0

#解凍
tar zxvf tokyo_tyrant-0.4.0.tgz

#パッチをダウンロード
#
#余談 パッチはこう作った
#patch tokyo_tyrant-0.4.0/session.c  < tokyo_tyrant-0.4.0_fix1.patch
#
wget http://rtilabs.net/files/2010_06_06/tokyo_tyrant-0.4.0_fix1.patch

#パッチあって
patch tokyo_tyrant-0.4.0/session.c  < tokyo_tyrant-0.4.0_fix1.patch

#うまくいかない人はパッチを当てたバージョンもあるよ!
#wget http://rtilabs.net/files/2010_06_06/tokyo_tyrant-0.4.0_fix1.zip

#お約束
phpize
./configure
make
make install

#apache 再起動
/etc/init.d/httpd restart
  or
/etc/init.d/apache2 restart

クラッシュ再現手順

<?
//tokyo tyrant を利用する
ini_set('session.save_handler', 'tokyo_tyrant');
ini_set('session.save_path', 'tcp://127.0.0.1:1978');

session_start();

if ( isset ( $_GET['renew'] ) )
{
	$old_session_id = session_id();
	$result_session_regenerate_id = session_regenerate_id(TRUE);
	$new_session_id = session_id();

	echo "REGENERATE!!<br>\r\n";
	echo "old_session_id:{$old_session_id}<br>\r\n";
	echo "new_session_id:{$new_session_id}<br>\r\n";
	echo "session_regenerate_id return :{$result_session_regenerate_id}<br>\r\n";
	echo "<br>\r\n";
}
else
{
	$new_session_id = session_id();
}

$a = (int)@$_SESSION['a'];
@$_SESSION['a'] = $a + 1;

echo "Count : {$a}<br>\r\n";
echo "Session : {$new_session_id}<br>\r\n";
?>
<br>
<br>
<br>
<a href="?">countup : reload or click</a><br>
<a href="?renew">session_regenerate_id</a><br>
<br>

↑のphpをリロードすると数字がどんどんカウントアップしていきます。
そんで、 session_regenerate_id をクリックすると、、、、

画面が真っ白になった人は apache がクラッシュしています。

tail /var/log/apache2/error.log を見れば、child pid 22351 exit signal Segmentation fault (11) とか表示されているのでよく分かります。

ちなみにこのとき tokyo tyrantのハッシュキー一覧を表示させてみると、該当するキーが両方とも吹っ飛んでいることが分かります。

tcrmgr list localhost

ちなみに、 pecl php-tokyo_tyrant は、セッションの一番最後の数字が tokyo tyrant のキーにマッチしています。

現在のセッションが↓だったら、最後の 345 の部分が tokyo tyrantのキーです。後はチェックサムとか他のものに使われています。

Session :3b6074a7309131ad0df977046da11d03-8fe866d04c41da2dbe9ed7a2013d8b152f9618c6-0-345

私のパッチを当ててからもう一度テストページを開き、 session_regenerate_id をクリックしても何の問題なくカウントupが行われることがわかります。

もう一度、 tokyo tyrant のキーを表示させてみると、 きちんと現在のセッションがあることが確認できます。

パッチと説明

で、修正したのが、 PS_CREATE_SID_FUNC(tokyo_tyrant) 関数です。

PS_CREATE_SID_FUNC(tokyo_tyrant)
{
	php_tt_conn *conn;
	php_tt_server *server;
	php_tt_server_pool *pool;
	bool is_session_regenerate = false;
	
	char *current_rand = NULL;
	char *sess_rand, *sid, *pk = NULL;
	int idx = -1, pk_len;

	if (!TOKYO_G(salt)) {
		php_error_docref(NULL TSRMLS_CC, E_ERROR, "tokyo_tyrant.session_salt needs to be set in order to use the session handler");
	}

	/* Session id is being regenerated. Need to copy some data */
	if (PS(session_status) == php_session_active) {
		TT_SESS_DATA;

		/* check session parameter */
		if (!session) {
			/* call session_regenerate_id. The session will be set again later.  */
			is_session_regenerate = true;
		}
		else {
			/* Use old values unless regeneration is forced */
			if (session->remap == 0) {
				idx          = session->idx;
				pk           = estrdup(session->pk);
				current_rand = estrdup(session->sess_rand);
			} else {
				session->remap = 0;
			}
		}
	}

	/* Create the random part of the session id */
	sess_rand = php_session_create_id(PS(mod_data), NULL TSRMLS_CC);

	if (!sess_rand) {
		php_error_docref(NULL TSRMLS_CC, E_ERROR, "Unable to generate session id");
	}

	/* Init the server pool */
	pool = php_tt_pool_init2(PS(save_path) TSRMLS_CC);
	
	if (!pool) {
		php_error_docref(NULL TSRMLS_CC, E_ERROR, "Unable to parse session.save_path");
	}

	/* Create idx if there isn't one already */
	if (idx == -1) {	
		idx = php_tt_pool_map(pool, sess_rand TSRMLS_CC);

		if (idx < 0) {
			php_error_docref(NULL TSRMLS_CC, E_ERROR, "Unable to map the session to a server");
		}
	}
	
	/* Get the server for the node */
	server = php_tt_pool_get_server(pool, idx TSRMLS_CC);
	
	if (!server) {
		php_error_docref(NULL TSRMLS_CC, E_ERROR, "Internal error: idx does not map to a server (should not happen)");
	}

	/* Create connection to the server */
	conn = php_tt_conn_init(TSRMLS_C);
	if (!php_tt_connect_ex(conn, server->host, server->port, TOKYO_G(default_timeout), 1 TSRMLS_CC)) {
		php_tt_server_fail_incr(server->host, server->port TSRMLS_CC);
		php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to connect to the session server");
	}
	
	if (!pk) {
		pk = php_tt_create_pk(conn, &pk_len TSRMLS_CC);
		/* check pk param */
		if (!pk) {
			php_error_docref(NULL TSRMLS_CC, E_ERROR, "Can not Create PK. Please confirm your tokyo tyrant run mode is tct mode. (defualt tch mode)");
		}
	} else {
		if (!php_tt_sess_touch(conn, current_rand, sess_rand, pk, strlen(pk) TSRMLS_CC)) {
			php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to update the session");
		}
		efree(current_rand);
	}

	sid = php_tt_create_sid(sess_rand, idx, pk, TOKYO_G(salt) TSRMLS_CC); 

	efree(sess_rand);
	efree(pk);

	/* session regenerate!!! */
	if (is_session_regenerate)
	{
		php_tt_session *session = php_tt_session_init(TSRMLS_C);
		if (!session) {
			php_error_docref(NULL TSRMLS_CC, E_ERROR, "Can not init session (for session_regenerate).");
		}

		/* Try to tokenize session id */
		if (!php_tt_tokenize((char *)sid, &(session->sess_rand), &(session->checksum), &(session->idx), &(session->pk) TSRMLS_CC)) {
			php_error_docref(NULL TSRMLS_CC, E_ERROR, "Can not tt_tokenize (for session_regenerate).");
		}
		
		/* Set additional data */
		session->sess_rand_len = strlen(session->sess_rand);
		session->checksum_len  = strlen(session->checksum);
		session->pk_len        = strlen(session->pk);
		session->remap = 0;

		/* keep , pool and conn */
		session->conn = conn;
		session->pool = pool;
		
		PS_SET_MOD_DATA(session);
	}
	else
	{
		php_tt_conn_deinit(conn TSRMLS_CC);
		php_tt_pool_deinit(pool TSRMLS_CC);
	}

	return sid;
}

オリジナルの関数はこーなっていて、 session が NULL なのに -> で内部にアクセスしようとしてチュドーンしてます。

	/* Session id is being regenerated. Need to copy some data */
	if (PS(session_status) == php_session_active) {
		TT_SESS_DATA;

                //ここでクラッシュ CARSH!!!!!!!!!!!!!!!!!!
                //session is NULL!!!!!!

		/* Use old values unless regeneration is forced */
		if (session->remap == 0) {
			idx          = session->idx;
			pk           = estrdup(session->pk);
			current_rand = estrdup(session->sess_rand);
		} else {
			session->remap = 0;
		}
	}

どーも、これは php 本体が session_regenerate_id を del して add するという強引な実装にしているのが問題なようです。
php本体のソースを見てみます。

php 5.3.2 session.c

static PHP_FUNCTION(session_regenerate_id)
{
	zend_bool del_ses = 0;

	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "|b", &del_ses) == FAILURE) {
		return;
	}

	if (SG(headers_sent)) {
		php_error_docref(NULL TSRMLS_CC, E_WARNING, "Cannot regenerate session id - headers already sent");
		RETURN_FALSE;
	}

	if (PS(session_status) == php_session_active) {
	    ///消して、
		if (PS(id)) {
			if (del_ses && PS(mod)->s_destroy(&PS(mod_data), PS(id) TSRMLS_CC) == FAILURE) {
				php_error_docref(NULL TSRMLS_CC, E_WARNING, "Session object destruction failed");
				RETURN_FALSE;
			}
			efree(PS(id));
			PS(id) = NULL;
		}

        //作り直す
		PS(id) = PS(mod)->s_create_sid(&PS(mod_data), NULL TSRMLS_CC);

		PS(send_cookie) = 1;
		php_session_reset_id(TSRMLS_C);

		RETURN_TRUE;
	}
	RETURN_FALSE;
}

なんで、 move や rename を作らなかったし!!!!
これはひどい。(互換性の問題を気にしたんだろうけど、、、)


んなわけで、とりあえず落ちなくしました。
ただ、それだけど、セッションが保存されないというバグを引いてしまうので、そこら辺をうまく調整しました。

それが今回修正したPS_CREATE_SID_FUNC(tokyo_tyrant)関数の一番のsession regenerate!!! ってところ。

	/* session regenerate!!! */
	if (is_session_regenerate)
	{
		php_tt_session *session = php_tt_session_init(TSRMLS_C);
		if (!session) {
			php_error_docref(NULL TSRMLS_CC, E_ERROR, "Can not init session (for session_regenerate).");
		}

		/* Try to tokenize session id */
		if (!php_tt_tokenize((char *)sid, &(session->sess_rand), &(session->checksum), &(session->idx), &(session->pk) TSRMLS_CC)) {
			php_error_docref(NULL TSRMLS_CC, E_ERROR, "Can not tt_tokenize (for session_regenerate).");
		}
		
		/* Set additional data */
		session->sess_rand_len = strlen(session->sess_rand);
		session->checksum_len  = strlen(session->checksum);
		session->pk_len        = strlen(session->pk);
		session->remap = 0;

		/* keep , pool and conn */
		session->conn = conn;
		session->pool = pool;
		
		PS_SET_MOD_DATA(session);
	}
	else
	{
		php_tt_conn_deinit(conn TSRMLS_CC);
		php_tt_pool_deinit(pool TSRMLS_CC);
	}

ここに来る前に php本体から呼び出される PS_DESTROY_FUNC(tokyo_tyrant) によってセッションが破棄されてしまっているので、ここでもう一度セッションを作成します。
こうしないと、存在しないセッションに書き込もうとして、エラーになってしまい、セッションが保存されません。

このとき、 keep , pool and conn で tokyo tyrant への接続を sessionに引き渡して、 sessionの管理下においています。
なんつーか、こーゆー書き方は無駄なリークを呼び寄せる用で好きではありませんね。。。

もし、速度を気にしないのであれば、 PS_OPEN_FUNC(tokyo_tyrant) して、 PS_READ_FUNC(tokyo_tyrant) する方法も有用です。


ちなみに、それを実装するとこうなります。

PS_CREATE_SID_FUNC(tokyo_tyrant)
{
	php_tt_conn *conn;
	php_tt_server *server;
	php_tt_server_pool *pool;
	bool is_session_regenerate = false;
	
	char *current_rand = NULL;
	char *sess_rand, *sid, *pk = NULL;
	int idx = -1, pk_len;

	if (!TOKYO_G(salt)) {
		php_error_docref(NULL TSRMLS_CC, E_ERROR, "tokyo_tyrant.session_salt needs to be set in order to use the session handler");
	}

	/* Session id is being regenerated. Need to copy some data */
	if (PS(session_status) == php_session_active) {
		TT_SESS_DATA;

		/* check session parameter */
		if (!session) {
			/* call session_regenerate_id. The session will be set again later.  */
			is_session_regenerate = true;
		}
		else {
			/* Use old values unless regeneration is forced */
			if (session->remap == 0) {
				idx          = session->idx;
				pk           = estrdup(session->pk);
				current_rand = estrdup(session->sess_rand);
			} else {
				session->remap = 0;
			}
		}
	}

	/* Create the random part of the session id */
	sess_rand = php_session_create_id(PS(mod_data), NULL TSRMLS_CC);

	if (!sess_rand) {
		php_error_docref(NULL TSRMLS_CC, E_ERROR, "Unable to generate session id");
	}

	/* Init the server pool */
	pool = php_tt_pool_init2(PS(save_path) TSRMLS_CC);
	
	if (!pool) {
		php_error_docref(NULL TSRMLS_CC, E_ERROR, "Unable to parse session.save_path");
	}

	/* Create idx if there isn't one already */
	if (idx == -1) {	
		idx = php_tt_pool_map(pool, sess_rand TSRMLS_CC);

		if (idx < 0) {
			php_error_docref(NULL TSRMLS_CC, E_ERROR, "Unable to map the session to a server");
		}
	}
	
	/* Get the server for the node */
	server = php_tt_pool_get_server(pool, idx TSRMLS_CC);
	
	if (!server) {
		php_error_docref(NULL TSRMLS_CC, E_ERROR, "Internal error: idx does not map to a server (should not happen)");
	}

	/* Create connection to the server */
	conn = php_tt_conn_init(TSRMLS_C);
	if (!php_tt_connect_ex(conn, server->host, server->port, TOKYO_G(default_timeout), 1 TSRMLS_CC)) {
		php_tt_server_fail_incr(server->host, server->port TSRMLS_CC);
		php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to connect to the session server");
	}
	
	if (!pk) {
		pk = php_tt_create_pk(conn, &pk_len TSRMLS_CC);
		/* check pk param */
		if (!pk) {
			php_error_docref(NULL TSRMLS_CC, E_ERROR, "Can not Create PK. Please confirm your tokyo tyrant run mode is tct mode. (defualt tch mode)");
		}
	} else {
		if (!php_tt_sess_touch(conn, current_rand, sess_rand, pk, strlen(pk) TSRMLS_CC)) {
			php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to update the session");
		}
		efree(current_rand);
	}

	sid = php_tt_create_sid(sess_rand, idx, pk, TOKYO_G(salt) TSRMLS_CC); 

	efree(sess_rand);
	efree(pk);

	
	php_tt_conn_deinit(conn TSRMLS_CC);
	php_tt_pool_deinit(pool TSRMLS_CC);

	/* session regenerate!!! */
	if (is_session_regenerate)
	{
		int ret = ps_open_tokyo_tyrant(mod_data,PS(save_path),PS(session_name) TSRMLS_CC);
		if ( ret != SUCCESS )
		{
			php_error_docref(NULL TSRMLS_CC, E_ERROR, "Can not PS_OPEN_FUNC(tokyo_tyrant) (for session_regenerate).");
		}

		TT_SESS_DATA;
		char *val_dummy = NULL;		int val_len = 0;
		ret = ps_read_tokyo_tyrant(mod_data, sid, &val_dummy, &val_len TSRMLS_CC);
		if (ret != SUCCESS)
		{
			php_error_docref(NULL TSRMLS_CC, E_ERROR, "Can not PS_READ_FUNC(tokyo_tyrant) (for session_regenerate).");
		}
		if (*val_dummy != NULL){
			efree(*val_dummy);	/* no use... */
		}
	}

	return sid;
}

session regenerate!!!している箇所が php_tt_conn_deinit よりも下に移動しました。
ソースとしてはこっちのほうが読みやすいですね。
リークするようにも見えないし、、、
だけど、速度はこっちのほうがもう一方つなぎなおすので不利になります。

ちなみにこっちのパッチも作りました。
こんな感じでパッチを当てることができます。

#ダウンロード
pecl download tokyo_tyrant-0.4.0

#解凍
tar zxvf tokyo_tyrant-0.4.0.tgz

#パッチをダウンロード
wget http://rtilabs.net/files/2010_06_06/tokyo_tyrant-0.4.0_fix2.patch

#パッチあって
patch tokyo_tyrant-0.4.0/session.c  < tokyo_tyrant-0.4.0_fix1.patch

#うまくいかない人はパッチを当てたバージョンもあるよ!
#wget http://rtilabs.net/files/2010_06_06/tokyo_tyrant-0.4.0_fix2.zip

happy hacking.