Skip to content

Commit 30ca3f9

Browse files
committed
Fix memory errors in read_property.
Use the __get magic method, instead of attempting to implement the read_property handler, since the expected refcount properties of the value returned from the read_property handler seem to be exceedingly baroque.
1 parent 4f22d4e commit 30ca3f9

File tree

7 files changed

+208
-27
lines changed

7 files changed

+208
-27
lines changed

src/asyncmessageworker.h

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,19 @@ namespace node_php_embed {
6363
}
6464
// Map PHP object to an index (PHP thread only)
6565
objid_t IdForPhpObj(zval *z) {
66-
if (phpObjToId_.count(z)) {
67-
return phpObjToId_.at(z);
66+
assert(Z_TYPE_P(z) == IS_OBJECT);
67+
zend_object_handle handle = Z_OBJ_HANDLE_P(z);
68+
if (phpObjToId_.count(handle)) {
69+
return phpObjToId_.at(handle);
6870
}
6971
uv_mutex_lock(&id_lock_);
7072
objid_t id = (nextId_++);
7173
uv_mutex_unlock(&id_lock_);
72-
Z_ADDREF_P(z);
7374
if (id >= phpObjList_.size()) { phpObjList_.resize(id+1); }
75+
// xxx clone/separate z?
76+
Z_ADDREF_P(z);
7477
phpObjList_[id] = z;
75-
phpObjToId_[z] = id;
78+
phpObjToId_[handle] = id;
7679
return id;
7780
}
7881
// returned value is owned by objectmapper, caller should not release it.
@@ -82,7 +85,7 @@ namespace node_php_embed {
8285
if (z.IsNull()) {
8386
node_php_jsobject_create(z.Ptr(), channel, id TSRMLS_CC);
8487
phpObjList_[id] = z.Ptr();
85-
phpObjToId_[z.Ptr()] = id;
88+
phpObjToId_[Z_OBJ_HANDLE_P(z.Ptr())] = id;
8689
// one excess reference: owned by objectmapper
8790
return z.Escape();
8891
}
@@ -94,7 +97,7 @@ namespace node_php_embed {
9497
zval *z = (id < phpObjList_.size()) ? phpObjList_[id] : NULL;
9598
if (z) {
9699
phpObjList_[id] = NULL;
97-
phpObjToId_.erase(z);
100+
phpObjToId_.erase(Z_OBJ_HANDLE_P(z));
98101
zval_ptr_dtor(&z);
99102
}
100103
}
@@ -255,7 +258,7 @@ namespace node_php_embed {
255258
Nan::Persistent<v8::NativeWeakMap> jsObjToId_;
256259
// PHP Object mapping
257260
// Read/writable only from PHP thread
258-
std::unordered_map<zval*,objid_t> phpObjToId_;
261+
std::unordered_map<zend_object_handle,objid_t> phpObjToId_;
259262
std::vector<zval*> phpObjList_;
260263
// Ids are allocated from both threads, so mutex is required
261264
uv_mutex_t id_lock_;

src/messages.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ class PhpGetPropertyMsg : public MessageToPhp {
151151
: MessageToPhp(m), obj_(m, obj), name_(m, name) { }
152152
protected:
153153
virtual void InPhp(ObjectMapper *m TSRMLS_DC) {
154-
ZVal obj(ZEND_FILE_LINE_C), name(ZEND_FILE_LINE_C);
154+
ZVal obj{ZEND_FILE_LINE_C}, name{ZEND_FILE_LINE_C};
155155
zval *r;
156156
zend_class_entry *ce;
157157
zend_property_info *property_info;

src/node_php_embed.cc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class node_php_embed::PhpRequestWorker : public AsyncMessageWorker {
6161
}
6262
NODE_PHP_EMBED_G(messageChannel) = &messageChannel;
6363
{
64-
ZVal retval(ZEND_FILE_LINE_C);
64+
ZVal retval{ZEND_FILE_LINE_C};
6565
zend_first_try {
6666
char eval_msg[] = { "request" }; // shows up in error messages
6767
if (FAILURE == zend_eval_string_ex(source_, *retval, eval_msg, true TSRMLS_CC)) {
@@ -113,7 +113,7 @@ static int node_php_embed_ub_write(const char *str, unsigned int str_length TSRM
113113
AsyncMessageWorker::MessageChannel *messageChannel = NODE_PHP_EMBED_G(messageChannel);
114114
PhpRequestWorker *worker = (PhpRequestWorker *)
115115
(messageChannel->GetWorker());
116-
ZVal stream(ZEND_FILE_LINE_C);
116+
ZVal stream{ZEND_FILE_LINE_C};
117117
worker->GetStream().ToPhp(messageChannel, stream TSRMLS_CC);
118118
zval buf; INIT_ZVAL(buf); // stack allocate a null zval as a placeholder
119119
zval *args[] = { &buf };
@@ -146,10 +146,10 @@ static void node_php_embed_register_server_variables(zval *track_vars_array TSRM
146146
// relative to the document root."
147147
// XXX
148148
// Put PHP-wrapped version of node context object in $_SERVER['CONTEXT']
149-
ZVal context(ZEND_FILE_LINE_C);
149+
ZVal context{ZEND_FILE_LINE_C};
150150
worker->GetContext().ToPhp(messageChannel, context TSRMLS_CC);
151151
char contextName[] = { "CONTEXT" };
152-
php_register_variable_ex(contextName, context.Transfer(), track_vars_array TSRMLS_CC);
152+
php_register_variable_ex(contextName, context.Transfer(TSRMLS_C), track_vars_array TSRMLS_CC);
153153
// XXX call a JS function passing in $_SERVER to allow init?
154154
}
155155

src/node_php_jsobject_class.cc

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ extern "C" {
1313
#include "values.h"
1414
#include "macros.h"
1515

16+
#define USE_MAGIC_ISSET 0
17+
1618
using namespace node_php_embed;
1719

1820
/* Class Entries */
@@ -81,6 +83,36 @@ class JsHasPropertyMsg : public MessageToJs {
8183
}
8284
};
8385

86+
#if USE_MAGIC_ISSET
87+
88+
PHP_METHOD(JsObject, __isset) {
89+
zval *member;
90+
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z/", &member) == FAILURE) {
91+
zend_throw_exception(
92+
zend_exception_get_default(TSRMLS_C),
93+
"bad property name for __isset", 0 TSRMLS_CC);
94+
return;
95+
}
96+
convert_to_string(member);
97+
node_php_jsobject *obj = (node_php_jsobject *)
98+
zend_object_store_get_object(this_ptr TSRMLS_CC);
99+
JsHasPropertyMsg msg(obj->channel, obj->id, member, 0);
100+
obj->channel->Send(&msg);
101+
msg.WaitForResponse();
102+
// ok, result is in msg.retval_ or msg.exception_
103+
if (msg.HasException()) {
104+
zend_throw_exception_ex(
105+
zend_exception_get_default(TSRMLS_C), 0 TSRMLS_CC,
106+
"JS exception thrown during __isset of \"%*s\"",
107+
Z_STRLEN_P(member), Z_STRVAL_P(member));
108+
return;
109+
}
110+
RETURN_BOOL(msg.retval_.AsBool());
111+
}
112+
113+
#else
114+
// By overriding has_property we can implement property_exists correctly,
115+
// and also handle empty arrays.
84116
static int node_php_jsobject_has_property(zval *object, zval *member, int has_set_exists ZEND_HASH_KEY_DC TSRMLS_DC) {
85117
/* param has_set_exists:
86118
* 0 (has) whether property exists and is not NULL - isset()
@@ -99,6 +131,7 @@ static int node_php_jsobject_has_property(zval *object, zval *member, int has_se
99131
if (msg.HasException()) { return false; /* sigh */ }
100132
return msg.retval_.AsBool();
101133
}
134+
#endif /* USE_MAGIC_ISSET */
102135

103136
class JsReadPropertyMsg : public MessageToJs {
104137
Value object_;
@@ -129,23 +162,30 @@ class JsReadPropertyMsg : public MessageToJs {
129162
}
130163
};
131164

132-
static zval *node_php_jsobject_read_property(zval *object, zval *member, int type ZEND_HASH_KEY_DC TSRMLS_DC) {
133-
ZVal retval(ZEND_FILE_LINE_C);
134-
if (Z_TYPE_P(member) != IS_STRING) {
135-
return retval.Escape();
165+
166+
PHP_METHOD(JsObject, __get) {
167+
zval *member;
168+
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z/", &member) == FAILURE) {
169+
zend_throw_exception(zend_exception_get_default(TSRMLS_C), "bad property name", 0 TSRMLS_CC);
170+
return;
136171
}
172+
convert_to_string(member);
137173
node_php_jsobject *obj = (node_php_jsobject *)
138-
zend_object_store_get_object(object TSRMLS_CC);
139-
JsReadPropertyMsg msg(obj->channel, obj->id, member, type);
174+
zend_object_store_get_object(this_ptr TSRMLS_CC);
175+
JsReadPropertyMsg msg(obj->channel, obj->id, member, 0);
140176
obj->channel->Send(&msg);
141177
msg.WaitForResponse();
142178
// ok, result is in msg.retval_ or msg.exception_
143-
if (msg.HasException()) { msg.retval_.SetNull(); /* sigh */ }
144-
msg.retval_.ToPhp(obj->channel, retval TSRMLS_CC);
145-
return retval.Escape();
179+
if (msg.HasException()) {
180+
zend_throw_exception_ex(
181+
zend_exception_get_default(TSRMLS_C), 0 TSRMLS_CC,
182+
"JS exception thrown during __get of \"%*s\"",
183+
Z_STRLEN_P(member), Z_STRVAL_P(member));
184+
return;
185+
}
186+
msg.retval_.ToPhp(obj->channel, return_value, return_value_ptr TSRMLS_CC);
146187
}
147188

148-
149189
static void node_php_jsobject_free_storage(void *object, zend_object_handle handle TSRMLS_DC) {
150190
node_php_jsobject *c = (node_php_jsobject *) object;
151191

@@ -224,11 +264,22 @@ STUB_METHOD(__construct)
224264
STUB_METHOD(__sleep)
225265
STUB_METHOD(__wakeup)
226266

267+
ZEND_BEGIN_ARG_INFO_EX(node_php_jsobject_one_arg, 0, 0, 1)
268+
ZEND_ARG_INFO(0, member)
269+
ZEND_END_ARG_INFO()
270+
ZEND_BEGIN_ARG_INFO_EX(node_php_jsobject_one_arg_retref, 0, 1, 1)
271+
ZEND_ARG_INFO(0, member)
272+
ZEND_END_ARG_INFO()
273+
227274
static const zend_function_entry node_php_jsobject_methods[] = {
228275
PHP_ME(JsObject, __construct, NULL, ZEND_ACC_PUBLIC|ZEND_ACC_CTOR)
229276
PHP_ME(JsObject, __sleep, NULL, ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)
230277
PHP_ME(JsObject, __wakeup, NULL, ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)
231-
{NULL, NULL, NULL}
278+
#if USE_MAGIC_ISSET
279+
PHP_ME(JsObject, __isset, node_php_jsobject_one_arg, ZEND_ACC_PUBLIC)
280+
#endif
281+
PHP_ME(JsObject, __get, node_php_jsobject_one_arg_retref, ZEND_ACC_PUBLIC)
282+
ZEND_FE_END
232283
};
233284

234285

@@ -245,8 +296,10 @@ PHP_MINIT_FUNCTION(node_php_jsobject_class) {
245296
node_php_jsobject_handlers.clone_obj = NULL;
246297
node_php_jsobject_handlers.cast_object = NULL;
247298
node_php_jsobject_handlers.get_property_ptr_ptr = NULL;
299+
#if !USE_MAGIC_ISSET
248300
node_php_jsobject_handlers.has_property = node_php_jsobject_has_property;
249-
node_php_jsobject_handlers.read_property = node_php_jsobject_read_property;
301+
#endif
302+
//node_php_jsobject_handlers.read_property = node_php_jsobject_read_property;
250303
/*
251304
node_php_jsobject_handlers.write_property = node_php_jsobject_write_property;
252305
node_php_jsobject_handlers.unset_property = node_php_jsobject_unset_property;

src/values.h

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,14 @@ class ZVal : public NonAssignable {
6060
// Support a PHP calling convention where the actual zval object
6161
// is owned by the caller, but the contents are transferred to the
6262
// callee.
63-
inline zval * Transfer() { transferred=true; return zvalp; }
63+
inline zval * Transfer(TSRMLS_D) {
64+
if (IsObject()) {
65+
zend_objects_store_add_ref(zvalp TSRMLS_CC);
66+
} else {
67+
transferred=true;
68+
}
69+
return zvalp;
70+
}
6471
inline zval * operator*() const { return Ptr(); } // shortcut
6572
inline int Type() { return Z_TYPE_P(zvalp); }
6673
inline bool IsNull() { return Type() == IS_NULL; }
@@ -180,10 +187,15 @@ class ZVal : public NonAssignable {
180187
// an "owned string", will copy data on creation and free it on delete.
181188
public:
182189
explicit OStr(const char *data, std::size_t length)
183-
: Str(estrndup(data, length+1), length) { }
190+
: Str(NULL, length) {
191+
char *ndata = new char[length+1];
192+
memcpy(ndata, data, length);
193+
ndata[length] = 0;
194+
data_ = ndata;
195+
}
184196
virtual ~OStr() {
185197
if (data_) {
186-
efree(const_cast<char*>(data_));
198+
delete[] data_;
187199
}
188200
}
189201
};

test/context.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,71 @@ describe('Passing context object from JS to PHP', function() {
5252
);
5353
});
5454
});
55+
it('should implement isset(), empty(), and property_exists', function() {
56+
var out = new StringStream();
57+
return php.request({
58+
file: path.join(__dirname, 'context2.php'),
59+
stream: out,
60+
context: {
61+
a: 0,
62+
b: 42,
63+
c: null,
64+
d: undefined,
65+
e: '0',
66+
f: '1'
67+
}
68+
}).then(function(v) {
69+
out.toString().should.equal(
70+
'a: int(0)\n' +
71+
'isset: bool(true)\n' +
72+
'empty: bool(true)\n' +
73+
'exists: bool(true)\n' +
74+
'\n' +
75+
'b: int(42)\n' +
76+
'isset: bool(true)\n' +
77+
'empty: bool(false)\n' +
78+
'exists: bool(true)\n' +
79+
'\n' +
80+
'c: NULL\n' +
81+
'isset: bool(false)\n' +
82+
'empty: bool(true)\n' +
83+
'exists: bool(true)\n' +
84+
'\n' +
85+
'd: NULL\n' +
86+
'isset: bool(false)\n' +
87+
'empty: bool(true)\n' +
88+
'exists: bool(true)\n' +
89+
'\n' +
90+
'e: string(1) "0"\n' +
91+
'isset: bool(true)\n' +
92+
'empty: bool(true)\n' +
93+
'exists: bool(true)\n' +
94+
'\n' +
95+
'f: string(1) "1"\n' +
96+
'isset: bool(true)\n' +
97+
'empty: bool(false)\n' +
98+
'exists: bool(true)\n' +
99+
'\n' +
100+
'g: NULL\n' +
101+
'isset: bool(false)\n' +
102+
'empty: bool(true)\n' +
103+
'exists: bool(false)\n' +
104+
'\n'
105+
);
106+
});
107+
});
108+
it('should handle exceptions in getters', function() {
109+
var out = new StringStream();
110+
var context = {};
111+
Object.defineProperty(context, 'a', { get: function() {
112+
throw new Error('boo');
113+
} });
114+
return php.request({
115+
source: "call_user_func(function () { try { var_dump($_SERVER['CONTEXT']->a); } catch (Exception $e) { echo 'exception caught'; } })",
116+
stream: out,
117+
context: context
118+
}).then(function() {
119+
out.toString().should.equal('exception caught');
120+
});
121+
});
55122
});

test/context2.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
$c = $_SERVER['CONTEXT'];
3+
4+
echo "a: "; var_dump($c->a);
5+
echo "isset: "; var_dump(isset($c->a));
6+
echo "empty: "; var_dump(empty($c->a));
7+
echo "exists: "; var_dump(property_exists($c, "a"));
8+
echo "\n";
9+
10+
echo "b: "; var_dump($c->b);
11+
echo "isset: "; var_dump(isset($c->b));
12+
echo "empty: "; var_dump(empty($c->b));
13+
echo "exists: "; var_dump(property_exists($c, "b"));
14+
echo "\n";
15+
16+
echo "c: "; var_dump($c->c);
17+
echo "isset: "; var_dump(isset($c->c));
18+
echo "empty: "; var_dump(empty($c->c));
19+
echo "exists: "; var_dump(property_exists($c, "c"));
20+
echo "\n";
21+
22+
echo "d: "; var_dump($c->d);
23+
echo "isset: "; var_dump(isset($c->d));
24+
echo "empty: "; var_dump(empty($c->d));
25+
echo "exists: "; var_dump(property_exists($c, "d"));
26+
echo "\n";
27+
28+
echo "e: "; var_dump($c->e);
29+
echo "isset: "; var_dump(isset($c->e));
30+
echo "empty: "; var_dump(empty($c->e));
31+
echo "exists: "; var_dump(property_exists($c, "e"));
32+
echo "\n";
33+
34+
echo "f: "; var_dump($c->f);
35+
echo "isset: "; var_dump(isset($c->f));
36+
echo "empty: "; var_dump(empty($c->f));
37+
echo "exists: "; var_dump(property_exists($c, "f"));
38+
echo "\n";
39+
40+
echo "g: "; var_dump($c->g);
41+
echo "isset: "; var_dump(isset($c->g));
42+
echo "empty: "; var_dump(empty($c->g));
43+
echo "exists: "; var_dump(property_exists($c, "g"));
44+
echo "\n";
45+
46+
?>

0 commit comments

Comments
 (0)