Press "Enter" to skip to content

An Error from an Unwarranted Assumption (the Return Value of an Assignment)

An error born of an unwarranted assumption.
The requirement was this: I needed to add a signature string to a binary stream. So, naturally, I first wrote a signing function. Since I wanted to tell whether the signing operation succeeded, I used a reference parameter:

function sign(&$carrier, $fingerprint) {
	if (NULL === $fingerprint) {
		return FALSE;
	}
	//add the signature
	$carrier = signing logic.
	return TRUE;
}

Next, reasoning that if signing fails we should still use the original string as the result, I confidently wrote the following code:

$bin_str = **************;
$success = sign($after_signed = $bin_str, "laruence's fingerprint");
if ($success) {
	//use $after_signed
} else {
	//use $bin_str
}

Given that simple variables are passed by value, I assumed that after $bin_str is assigned to $after_signed, the reference of $after_signed would be passed on to $carrier...
But it turned out that $after_signed did not have the signature string added to it at all...
So why was it wrong?
After analysing the opcodes, it turned out that when PHP performs a binary assignment, the return value is not the left-hand side but a temporary variable. That is, for:

$a = $b;

the return value is not $a but a temporary variable — call it $2.
So what gets passed to the sign function is not a reference to $a, but $2;
The following is source-level analysis; if you only want the conclusion, you can skip it:
Now, combining this with the material from my earlier article Understanding PHP Internals: Variables Separation/Reference, let's do a detailed analysis:

$a = "laruence";
function ch(&$p) {
    debug_zval_dump($p);
    $p = 'eve';
    debug_zval_dump($p);
}
debug_zval_dump($a);
ch($a);
ch($b=$a);

The output is:

//the initial a
string(8) "laruence" refcount(2)
//p during the first call to change
string(8) "laruence" refcount(1)
string(3) "eve" refcount(1)
//a during the second call, with the assignment result
string(3) "eve" refcount(3)
string(3) "eve" refcount(2)

First, at the very start $a's refcount is 2, because simple variables are passed by value: after being handed to debug_zval_dump, $a has the refcount of a copy.
On the first call we pass $a directly. Since the change function declares its parameter by reference, when debug_zval_dump is called inside change, $p is a reference. Passing it by value therefore triggers a separation, giving a count of 1.
The interesting part is the second call, where the refcount at the first debug_zval_dump inside change is 3. How can it be 3?
For it to be 3 here, $p must be a non-reference variable with a refcount of 2.
But didn't we declare that change takes a reference parameter?
No choice but to look at the source. At parse time, the difference becomes visible:

non_empty_function_call_parameter_list:
        expr_without_variable
		   { Z_LVAL($$.u.constant) = 1;
				zend_do_pass_param(&$1, ZEND_SEND_VAL, Z_LVAL($$.u.constant) TSRMLS_CC); }
    |   variable
			{ Z_LVAL($$.u.constant) = 1;
				zend_do_pass_param(&$1, ZEND_SEND_VAR, Z_LVAL($$.u.constant) TSRMLS_CC); }

For the second call, the first reduction rule is the one that matches, so the second argument passed to zend_do_pass_param is ZEND_SEND_VAL...
Following zend_do_pass_param further:

if (op == ZEND_SEND_VAR && zend_is_function_or_method_call(param)) {
	//is it the return value of a function or method
	/* Method call */
	op = ZEND_SEND_VAR_NO_REF;
	send_function = ZEND_ARG_SEND_FUNCTION;
} else if (op == ZEND_SEND_VAL && (param->op_type & (IS_VAR|IS_CV))) {
	op = ZEND_SEND_VAR_NO_REF;//changed to NO_REF here
}

So, because on the second call the variable passed to change is a "literal" of type IS_VAR, PHP changes the passing mode to by-value.
That's why the refcount is already 3 at the first debug_zval_dump inside change.
In fact, at this point we can draw an analogy with the case of a function's return value used as an argument (the condition in the first if). If you imagine that assignment, like a function call, has a return value, it becomes much easier to understand.

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.