Closed Bug 354145 Opened 18 years ago Closed 18 years ago

Wrong assumptions about immutable XML

Categories

(Core :: JavaScript Engine, defect)

defect
Not set
critical

Tracking

()

VERIFIED FIXED

People

(Reporter: igor, Assigned: igor)

References

Details

(Keywords: verified1.8.0.8, verified1.8.1, Whiteboard: [sg:critical?])

Attachments

(9 files, 24 obsolete files)

78.71 KB, patch
brendan
: review+
beltzner
: approval1.8.1+
Details | Diff | Splinter Review
2.15 KB, text/plain
Details
2.15 KB, text/plain
Details
2.15 KB, text/plain
Details
2.16 KB, text/plain
Details
2.16 KB, text/plain
Details
2.14 KB, text/plain
Details
2.06 KB, text/plain
Details
78.83 KB, patch
brendan
: review+
Details | Diff | Splinter Review
Consider the following example:

var xml = <></>
var N = 10;
for (var i = 0; i != N; ++i) {
  xml[i] = <{"a"+i}/>;
}

function prepare()
{
  for (var i = N - 1; i >= 0; --i)
    delete xml[i];
  gc();
  return "test";
}

xml[N - 1] = { toString: prepare };

Currently it crashes the shell since PutProperty in jsxml.c assumes that after ensuring that the insert index is less then the list length, the list length would not change. This is wrong since the code later  convert the passed value to string which can execute arbitrary scrip code that modifies the list.

In the above example the crash happens when accessing NULL pointer, but its modification can be used to reinterpret as XML object arbitrary script-controlled memory.
Here is a similar problem with xml_child code:

~/m/trunk/mozilla/js/src> cat ~/m/files/y.js
var list = <></>
var N = 10;
for (var i = 0; i != N; ++i)
  list[i] = <{"a"+i}/>;

function prepare()
{
  for (var i = N - 1; i >= 0; --i)
    delete list[i];
  gc();
  return "test";
}

list.child({ toString: prepare });
~/m/trunk/mozilla/js/src> ./Linux_All_DBG.OBJ/js ~/m/files/y.js
before 9232, after 9232, break 08181000
Segmentation fault
xml_contains has the bug:

~/m/trunk/mozilla/js/src> cat ~/m/files/y.js
var list = <></>
var N = 10;
for (var i = 0; i != N; ++i)
  list[i] = <{"a"+i}/>;

function prepare()
{
  for (var i = N - 1; i >= 0; --i)
    delete list[i];
  gc();
  return "test";
}

list.contains({ toString: prepare });
~/m/trunk/mozilla/js/src> ./Linux_All_DBG.OBJ/js ~/m/files/y.js
before 9232, after 9232, break 08141000
Segmentation fault
Is this a bug in the spec?  I implemented the spec in as brain-off a way as I could stand.  I should have seen this.

Immutability is lovely, even if used only under the hood.  Is there a simple fix, or is the assumption about immutability deeply enough wired that we should try to satisfy it, instead of work around the lack of it?

/be
(In reply to comment #3)
> Is this a bug in the spec?  I implemented the spec in as brain-off a way as I
> could stand.  I should have seen this.
> 
> Immutability is lovely, even if used only under the hood.  Is there a simple
> fix, or is the assumption about immutability deeply enough wired that we should
> try to satisfy it, instead of work around the lack of it?
 
A simple fix would be to always throw an exception when converting non-xml-object into string unless it is an instance of String. AFAICS this deviates from E4X but I am in favour of it, as otherwise the amount of code to fix is huge.  
Summary: Wrong assumtions about immutable XML → Wrong assumptions about immutable XML
The same story with insertChildAfter/Before:

var xml = <tag/>;
var N = 10;
for (var i = 0; i != N; ++i)
  xml.appendChild(<{'a'+i}/>);

function prepare()
{
  delete xml.*;
  gc();
  return "test";
}

var last = xml.*[N - 1];
xml.insertChildAfter(last, { toString: prepare });
~/m/trunk/mozilla/js/src> ./Linux_All_DBG.OBJ/js ~/m/files/y.js
Segmentation fault
var xml = <tag/>;
var N = 10;
for (var i = 0; i != N; ++i)
  xml.appendChild(<{'a'+i}/>);

function prepare()
{
  delete xml.*;
  gc();
  return "test";
}

var last = xml.*[N - 1];
xml.insertChildBefore(last, { toString: prepare });
~/m/trunk/mozilla/js/src> ./Linux_All_DBG.OBJ/js ~/m/files/y.js
Segmentation fault

(In reply to comment #4)
> (In reply to comment #3)
> > Is this a bug in the spec?  I implemented the spec in as brain-off a way as I
> > could stand.  I should have seen this.

It is a spec bug too: the spec seems to assume that ToString cannot mutate the XML list or object being operated upon.

> > Immutability is lovely, even if used only under the hood.  Is there a simple
> > fix, or is the assumption about immutability deeply enough wired that we should
> > try to satisfy it, instead of work around the lack of it?
> 
> A simple fix would be to always throw an exception when converting
> non-xml-object into string unless it is an instance of String. AFAICS this
> deviates from E4X but I am in favour of it, as otherwise the amount of code to
> fix is huge.  

Another idea is to detect mutation cheaply and restart the operation with the converted parameter or right-hand side.

/be
Attached patch Food for thoughts (obsolete) — Splinter Review
All but PutProperty bugs happens because the code caches the length value to speedup the loop. So if this optimization is removed, the bug would be mostly fixed. The question is how useful the optimization? 

In most cases where the length caching occurs is used it to optimized loops like in:

        for (i = 0, n = xml->xml_kids.length; i < n; i++) {

Since the loop body is complex code in such cases, the optimization can not speedup things in any visible matter. So what about code size? To check this, I created this patch which replaces in all the places accessing cached value by direct member queries. The result was rather interesting. 

An optimized build of the shell with the default -O flag did not change the size of jsxml.o compiled with GCC 4.1 for i386 Linux from its 56604 bytes. When I compiled with -Os with the patch applied jsxml.o dropped from 49180 to 48796 bytes, a saving of 0.8%. When I compiled with -O2 size also dropped from 59964 to 59900, a saving about 0.1%.

Thus AFAICS the optimization in fact harmful. What happens is that the compiler  , besides generating the code to access length, also has to generate an extra code to save/restore the cached value in the register across function calls.  

So in fact this patch can be commited under a separated bug like shrinking jsxml.o size and nobody would notice that it also fixes xml_child problem.
Attachment #241326 - Flags: review?
Attachment #241326 - Attachment description: Food for thought → Food for thoughts
Comment on attachment 241326 [details] [diff] [review]
Food for thoughts

>@@ -3253,25 +3252,24 @@ DeepCopy(JSContext *cx, JSXML *xml, JSOb
>  * (ii) parent must have a rooted object.
>  * (iii) from's owning object must be locked if not thread-local.
>  */
> static JSBool
> DeepCopySetInLRS(JSContext *cx, JSXMLArray *from, JSXMLArray *to, JSXML *parent,
>                  uintN flags)
> {
>-    uint32 i, j, n;
>+    uint32 i, j;
>     JSXML *kid, *kid2;
>     JSString *str;
> 
>     JS_ASSERT(cx->localRootStack);
> 
>-    n = from->length;
>-    if (!XMLArraySetCapacity(cx, to, n))
>+    if (!XMLArraySetCapacity(cx, to, from->length))
>         return JS_FALSE;
> 
>-    for (i = j = 0; i < n; i++) {
>+    for (i = j = 0; i < from->length; i++) {

This is ok if it saves code size on x86, but just for this particular case there should never be a mutation to the from array when copying -- right?

>@@ -4301,15 +4297,15 @@ PutProperty(JSContext *cx, JSObject *obj
>                      * y.[[Parent]] is here called kid->parent, which we know
>                      * from 2(c)(ii) is _r_, here called rxml.  So let's just
>                      * test that!  Erratum, the spec should be simpler here.
>                      */
>                     if (rxml) {
>                         JS_ASSERT(JSXML_HAS_KIDS(rxml));
>                         n = rxml->xml_kids.length;
>-                        j = n - 1;
>+                        j = rxml->xml_kids.length - 1;

This seems like a gratuitous change -- there's no mutation hazard, and n is a common subexpression above and just below:

>                         if (n != 0 && i != 0) {
>                             for (n = j, j = 0; j < n; j++) {
>                                 if (rxml->xml_kids.vector[j] ==
>                                     xml->xml_kids.vector[i-1]) {
>                                     break;
>                                 }
>                             }

>@@ -5233,15 +5228,15 @@ again:
>             kid = XMLARRAY_MEMBER(&xml->xml_kids, 0, JSXML);
>             xml = kid;
>             goto again;
>         }
>         /* FALL THROUGH */
>       default:
>         simple = JS_TRUE;
>-        for (i = 0, n = JSXML_LENGTH(xml); i < n; i++) {
>+        for (i = 0; i < JSXML_LENGTH(xml); i++) {
>             kid = XMLARRAY_MEMBER(&xml->xml_kids, i, JSXML);
>             if (kid->xml_class == JSXML_CLASS_ELEMENT) {
>                 simple = JS_FALSE;
>                 break;
>             }
>         }
>         return simple;

This is also a safe commoning; not sure it hurts code size but there's no mutation hazard.

> static JSBool
> xml_childIndex(JSContext *cx, JSObject *obj, uintN argc, jsval *argv,
>                jsval *rval)
> {
>     JSXML *xml, *parent;
>-    uint32 i, n;
>+    uint32 i;
> 
>     XML_METHOD_PROLOG;
>     parent = xml->parent;
>     if (!parent || xml->xml_class == JSXML_CLASS_ATTRIBUTE) {
>         *rval = DOUBLE_TO_JSVAL(cx->runtime->jsNaN);
>         return JS_TRUE;
>     }
>-    for (i = 0, n = JSXML_LENGTH(parent); i < n; i++) {
>+    for (i = 0; i < JSXML_LENGTH(parent); i++) {
>         if (XMLARRAY_MEMBER(&parent->xml_kids, i, JSXML) == xml)
>             break;
>     }
>-    JS_ASSERT(i < n);
>+    JS_ASSERT(i < JSXML_LENGTH(parent));

Again the existing code is safe and perhaps even yields better code.  It's not even clearly a source code with to undo the commoning to n, given the brevity of n vs. JSXML_LENGTH(parent).

I'm obviously a RISC fan who expects lots of registers, and I common too much with explicit locals, but the old code seemed better here.

>@@ -6010,15 +6005,15 @@ again:
>             obj = kidobj;
>             xml = (JSXML *) JS_GetPrivate(cx, obj);
>             goto again;
>         }
>         /* FALL THROUGH */
>       default:
>         *rval = JSVAL_FALSE;
>-        for (i = 0, n = xml->xml_kids.length; i < n; i++) {
>+        for (i = 0; i < xml->xml_kids.length; i++) {
>             kid = XMLARRAY_MEMBER(&xml->xml_kids, i, JSXML);
>             if (kid->xml_class == JSXML_CLASS_ELEMENT) {
>                 *rval = JSVAL_TRUE;
>                 break;
>             }
>         }
>         break;

Another simple case with no calls out of the loop body and no direct mutation.

> /* XML and XMLList */
> static JSBool
> xml_parent(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
> {
>     JSXML *xml, *parent, *kid;
>-    uint32 i, n;
>+    uint32 i;
>     JSObject *parentobj;
> 
>     XML_METHOD_PROLOG;
>     parent = xml->parent;
>     if (xml->xml_class == JSXML_CLASS_LIST) {
>         *rval = JSVAL_VOID;
>-        n = xml->xml_kids.length;
>-        if (n == 0)
>+        if (xml->xml_kids.length == 0)
>             return JS_TRUE;
> 
>         kid = XMLARRAY_MEMBER(&xml->xml_kids, 0, JSXML);
>         parent = kid->parent;
>-        for (i = 1; i < n; i++) {
>+        for (i = 1; i < xml->xml_kids.length; i++) {
>             kid = XMLARRAY_MEMBER(&xml->xml_kids, i, JSXML);
>             if (kid->parent != parent)
>                 return JS_TRUE;
>         }
>     }
> 
>     if (!parent) {

Another simple case.

I'm curious if reverting the changes for simple cases worsens code size.  Also curious about what MSVC++ does with and without the patch.

Filing a separate "cover" cleanup bug sounds like a good idea.

/be
Attached patch Fix via explicit abort (obsolete) — Splinter Review
The patch just calls abort for out-of-bounds access. Yes, this would crash a browser, but it would be a controllable crash until a better remedy would be found. 

An alternative is to do nothing in XMLArraySetMember and return other array element in XMLArrayMember, but then to return something for the empty array cx has to be passed to the macro.
Attachment #241326 - Attachment is obsolete: true
Attachment #241374 - Flags: review?(brendan)
Attachment #241326 - Flags: review?
(In reply to comment #9)
> An alternative is to do nothing in XMLArraySetMember and return other array
> element in XMLArrayMember, but then to return something for the empty array cx
> has to be passed to the macro.

I'm totally ok with using cx from the macro call's lexical environment.  That seems better than naked abort calls, and easy enough.

/be
Attached patch Fix v2 (obsolete) — Splinter Review
This a version of the patch that does nothing on set and return null on get. The good side of this patch is that after having it in CVS it would be safe to explicit null checks in broken places, exposing them would do no harm with the explicit protection.
Assignee: general → igor.bukanov
Attachment #241374 - Attachment is obsolete: true
Status: NEW → ASSIGNED
Attachment #241378 - Flags: review?(brendan)
Attachment #241374 - Flags: review?(brendan)
Another observation. If the simple loops with cached length would be turned into non-cached version a good compiler should be able to figure out that the bounds checks are not reachable.
I filed a cover bug 355611 for optimizing jsxml.c for size.
Comment on attachment 241378 [details] [diff] [review]
Fix v2


>+static void
>+XMLArraySetMember(JSXMLArray *array, uint32 index, void *elt)
>+{
>+    if (index >= array->length) {
>+        if (JS_UNLIKELY(index >= JSXML_CAPACITY(array)))
>+            return;
>+        array->length = index + 1;
>+    }
>+    array->vector = elt;

Typo for sure: array->vector[index] = elt;

r+ with that fixed.

/be
Blocks: js1.7src
Flags: blocking1.8.1?
Flags: blocking1.8.1.1?
Attached patch Fix v3 (obsolete) — Splinter Review
Patch to commit with stupid typo fixed
Attachment #241378 - Attachment is obsolete: true
Attachment #241386 - Flags: review+
Attachment #241378 - Flags: review?(brendan)
Comment on attachment 241386 [details] [diff] [review]
Fix v3

I am stupid! I should not touch orphaned child!
Attachment #241386 - Attachment is obsolete: true
Attachment #241386 - Flags: review+
Attached patch Fix v4 (obsolete) — Splinter Review
The proper patch. Asking for r one more time just to be sure.
Attachment #241387 - Flags: review?(brendan)
I think I spend too much time on this. The patch only forces the cases from comment 5 and 2, the cases from comment 0 and comment 1 are still exploitable through GC. There the problem is that temporary XML is stored on the stack and is killed from the script. 

At this point I really suggest to define something like SafeToString that would throw an exception when the target is an object and not String/XML. 


Attached patch Fix v5 via SafeToString (obsolete) — Splinter Review
[This is untested but sufficient to address all the examples]

The patch adds SafeToString which never execute any scripted code and should work for most useful cases with throwing exception when necessary.

The patch also uses explicit local roots in many places where they are not strictly necessary.

Now after playing for sometime with this I think there is a better solution based on the following observation. XML code eventually converts the majority of passed jsval to string if they are not primitive values or XML, QName, Namespace  objects. Thus the idea to convert such jsval ASAP and then replace various js_valueToString by a helper method that does string conversation assuming that the value is sanitized.

I will try to see how much work that would require.
Attachment #241387 - Attachment is obsolete: true
Attachment #241457 - Flags: review?(brendan)
Attachment #241387 - Flags: review?(brendan)
Attached patch Fix v6 via value sanitation (obsolete) — Splinter Review
Work in progress that allows to resolve all the reported problems.

This adds SanitizeValue/ConvertToString pair that allows to convert objects to string earlier. The critical part is to call ConvertToString in places that navigates XML structures. The function asserts in the debug builds if the value a generic object but in optimized build it returns empty string. 

Then to support a generic value SanitizeValue should be called ASAP in the places that receives an arbitrary jsval. I will test/review the patch few more times.
Attachment #241457 - Attachment is obsolete: true
Attachment #241457 - Flags: review?(brendan)
Comment on attachment 241514 [details] [diff] [review]
Fix v6 via value sanitation

>Index: jsxml.c
>===================================================================
>RCS file: /cvsroot/mozilla/js/src/jsxml.c,v
>retrieving revision 3.130
>diff -U7 -p -r3.130 jsxml.c
>--- jsxml.c	5 Oct 2006 20:35:29 -0000	3.130
>+++ jsxml.c	7 Oct 2006 01:26:24 -0000
>@@ -129,17 +129,128 @@ const char js_gt_entity_str[]     = "&gt
> const char js_lt_entity_str[]     = "&lt;";
> const char js_quot_entity_str[]   = "&quot;";
> 
> #define IS_EMPTY(str) (JSSTRING_LENGTH(str) == 0)
> #define IS_STAR(str)  (JSSTRING_LENGTH(str) == 1 && *JSSTRING_CHARS(str) == '*')
> 
> static JSBool
>+js_IsXMLName(JSContext *cx, jsval v);

Any reason to make this static in this patch?  Minimize, also leave it in case the DOM<=>E4X hookup needs it (why I exposed it).

>+    if (!JSVAL_IS_PRIMITIVE(*vp)) {
>+        obj = JSVAL_TO_OBJECT(*vp);
>+        if (OBJECT_IS_XML(cx, obj)) {
>+            JSXML *xml;
>+
>+            xml = (JSXML *) JS_GetPrivate(cx, obj);
>+            str = xml_toString_helper(cx, xml);
>+            goto string_result;
>+        }

else {...} here to avoid goto string_result?

>+
>+        clasp = OBJ_GET_CLASS(cx, obj);
>+        if (clasp == &js_NamespaceClass.base) {
>+            JSXMLNamespace *ns;
>+
>+            ns = (JSXMLNamespace *) JS_GetPrivate(cx, obj);
>+            *vp = STRING_TO_JSVAL(ns->uri);
>+            return JS_TRUE;
>+        } else

else after return non-sequitur.

>+        if (clasp == &js_QNameClass.base ||
>+                   clasp == &js_AttributeNameClass ||
>+                   clasp == &js_AnyNameClass) {
>+            JSXMLQName *qn;
>+
>+            qn = (JSXMLQName *) JS_GetPrivate(cx, obj);
>+            JS_PUSH_TEMP_ROOT_OBJECT(cx, obj, &tvr);
>+            ok = QNameToString(cx, clasp, qn, vp);
>+            JS_POP_TEMP_ROOT(cx, &tvr);
>+            return ok;
>+        } else

Ditto.

>+            JS_NOT_REACHED("Not a result of SanitizeValue");
>+            str = cx->runtime->emptyString;
>+            goto string_result;
>+        }

This end brace goes away, leaving the goto string_result easily replaced by else around this:

>+    }
>+    JS_ASSERT(JSVAL_IS_PRIMITIVE(*vp));
>+    str = js_ValueToString(cx, *vp);
>+
>+  string_result:
>+    if (!str)
>+        return JS_FALSE;
>+    *vp = STRING_TO_JSVAL(str);
>+    return JS_TRUE;
>+}

[. . .]

>+  eq_list:

Use a tail call to a helper to avoid this goto?  Wonder if that would even save code size.

/be
Attached patch Fix v7 (obsolete) — Splinter Review
I think I covered all suspicious paths and added over-paranoid JSTempValueRoot litter in the places where apparently reliance on newborn is enough. But I want to play it save especially after ToXML that does not wrap ParseXMLSource into Enter/LeaveLocalRootScope while ToXMLList does that.
Attachment #241514 - Attachment is obsolete: true
Note that v7 does not address the comment 21.
(In reply to comment #21)
> > static JSBool
> >+js_IsXMLName(JSContext *cx, jsval v);
> 
> Any reason to make this static in this patch?  Minimize, also leave it in case
> the DOM<=>E4X hookup needs it (why I exposed it).

It uses ConvertToString which requires the caller to call SanitizeValue. If exposure is important, then js_IsXMLName(JSContext *cx, jsval v) can be split in 2 functions with the nested one calling SanitizeValue. 
One more thing about the previous attachment. It changes ToXMLName to use
ConvertToString to ensure that String instances with overwritten
String.prototype.toString still uses the default ToString conversion. But that
required that SanitizeValue skips string converssion for Number, Boolean and
String object instances.

Here is another source of worries. If some object creation hook can trigger a full GC, then a close hook from a generator can do exactly the damage that the examples demonstrates. I really thinking of delegating running close hooks to the embedding. 
Attached patch Fix v8 (obsolete) — Splinter Review
This version mostly simplifies Equals implementation and changes xml_toString_helper to use passed root instead of EnterLocalRoots.

I still do not ask for a review as I have to do it myself first.
Attachment #241520 - Attachment is obsolete: true
Attached patch Fix v8 for real (obsolete) — Splinter Review
I attached the older file
Attachment #241555 - Attachment is obsolete: true
Attached patch Fix v9 (obsolete) — Splinter Review
Fixing bug in ToXMLList and using ok in ToXML and ToXMLList for consistency.
Attachment #241556 - Attachment is obsolete: true
Attached patch Fix v10 (obsolete) — Splinter Review
Here xml_replace calls SanitizeValue for both arguments ASAP not to worry about effects of executing arbitrary script on CHECK_COPY_ON_WRITE. This is AFAICS groundless, but I want to be 100% sure.
Attachment #241557 - Attachment is obsolete: true
The patch causes a single regression among e4x tests in e4x/GC/regress-313952-02.js for bug 313952 which is in fact expected since the test contains:

var str = String(1);
var expected = String(1);

var x = new XML("text");
x.function::toString = function() {
        var tmp = str; str = null; return tmp;
}

...
var likeString = { toString: function() { bug-related-stuff; return expected }};

var test = (x == likeString);

After the patch x.function::toString is no longer called for XML objects so the test fails.

I think this is an acceptable price to pay for GC-safty.
Attachment #241559 - Flags: review?(brendan)
Attachment #241559 - Flags: review?(mrbkap)
Here is another problem not covered by the patch in js_FilterXMLList. It contains:

    for (i = 0, n = list->xml_kids.length; i < n; i++) {
        kid = XMLARRAY_MEMBER(&list->xml_kids, i, JSXML);
        kidobj = js_GetXMLObject(cx, kid);
        if (!kidobj)
            goto bad;
        OBJ_SET_PROTO(cx, withobj, kidobj);
        ok = js_Interpret(cx, pc, vp) && js_ValueToBoolean(cx, *vp, &match);

Since here list is the original passed object when it is itself a list, so this just asks for troubles.
I'm wondering whether we can't take a different approach based on cursors, either snapshotting the array in a copy-on-write fashion, or locking it against mutation. If we make the XMLArray layer handle this problem, the higher layers may not need so much changing -- just the loops.

/be
Here is a test case that exposes the hazard with the filter via segfault:

var list = <><a/><b/></>

function a() {
   delete list[1];
   delete list[0];
   gc();
   for (var i = 0; i != 50000; ++i) 
     var tmp = ""+i;     
  return true;
}

var list2 = list.(a());

print(uneval(list2))

Note that gc() here is used to shrink spare list capacity that becomes available after the removal of elements. It is also interesting to note that since js_ValueToBoolean does not do any valueOf calls, the bug can not be exposed using a specially crafted like_boolean object. 

But on 1.8.0 branch the situation id different AFAICS. There if a script can set a version to 1.2, then js_ValueToBoolean calls valueOf on the result. Now that can not only change the list, but also unroot kidobj via changing the prototype of with. So to make this as safe as possible I will explicitly root kidobj in the patch.  
(In reply to comment #33)
> I'm wondering whether we can't take a different approach based on cursors,
> either snapshotting the array in a copy-on-write fashion, or locking it against
> mutation. If we make the XMLArray layer handle this problem, the higher layers
> may not need so much changing -- just the loops.

But how would you deal with Insert/PutProperty? It requires changing the arrays! 
Attached patch Fix v11 (obsolete) — Splinter Review
This is just an extension of v9 to cover XML filtering.
Attachment #241559 - Attachment is obsolete: true
Attachment #241587 - Flags: review?(brendan)
Attachment #241559 - Flags: review?(mrbkap)
Attachment #241559 - Flags: review?(brendan)
Attachment #241587 - Flags: review?(mrbkap)
Note about GC hazard with js_FilterXMLList. It does not require any specially crafted like_boolean objects and JS 1.2 as long as a script can reach via __parent__ property the scope containing kidobj. If this is possible, then the script can replace the value of __proto__ and unroot kidobj.

On the other hand  I was not able to get hold on that with using __parent__, it seems with(xml) behaves differently then with({}).

In case, the patch v11 does contain explicit GC rooting for kidobj. 
This does not include any of the specific GC safety fixes you've made in your good work on the patches before it, but it does mark cursor's current items, so e.g. it protects js_FilterXMLList without extra code beyond the switch from for (i = 0...) to a cursor while loop.

XMLARRAY_MEMBER is nullable and so all callers must check and do something sane to avoid a null dereference crash.  I hope I changed all the callers correctly.

/be
Attachment #241600 - Flags: review?(igor.bukanov)
(In reply to comment #35)
> (In reply to comment #33)
> > I'm wondering whether we can't take a different approach based on cursors,
> > either snapshotting the array in a copy-on-write fashion, or locking it against
> > mutation. If we make the XMLArray layer handle this problem, the higher layers
> > may not need so much changing -- just the loops.
> 
> But how would you deal with Insert/PutProperty? It requires changing the
> arrays! 

Yes, that's the problem with immutability in an implementation of a spec with mutation.  Instead of trying to solve that conundrum, the patch I just attached uses the existing cursor mechanism to avoid indexing deleted items in arrays, and it also roots the last cursor'ed item.  Sorry I didn't suggest this sooner.  Is it enough for memory safety in the face of mutation?

/be
Attached patch alternative approach, minimized (obsolete) — Splinter Review
ParseNodeTo* functions do not need to worry about null from XMLARRAY_MEMBER, and xml_enumerateValues should skip any null kids, not terminate the enumeration on the first null kid.

/be
Attachment #241600 - Attachment is obsolete: true
Attachment #241601 - Flags: review?(igor.bukanov)
Attachment #241600 - Flags: review?(igor.bukanov)
Argh, --n in XMLARRAY_MEMBER actual parameter came back from the dead via patch -R blind usage.  Interdiff shows all.

/be
Attachment #241601 - Attachment is obsolete: true
Attachment #241603 - Flags: review?(igor.bukanov)
Attachment #241601 - Flags: review?(igor.bukanov)
Thanks to Igor for pointing out a few problems that this patch fixes.  What is left to fix here?  I'll have to take a fresh look on Monday.  Comments welcome.

/be
Attachment #241603 - Attachment is obsolete: true
Attachment #241604 - Flags: review?(igor.bukanov)
Attachment #241603 - Flags: review?(igor.bukanov)
Attachment #241604 - Attachment is obsolete: true
Attachment #241605 - Flags: review?(igor.bukanov)
Attachment #241604 - Flags: review?(igor.bukanov)
With the patch I was not able to find any more GC bugs. In principle I would prefer a patch that uses a version of cursor that roots the current element via tvr rooter without the assumption that the parent is rooted , as it decreses the number of of cases to consider, but even in the current form its reflect the spirit of E4X through cursor loops, not explicit element loops.  

AFAICS the only problematic behavior left is that Insert sets the element to NULL before inserting it. Thus if replace fails (perhaps due to exception throws during ValueToString ;) , null kid would remain in the array leading to null-pointer derefs later or infinite for-each loop in enumerate as the following example demonstrates:

var a = <a><b/></a>
try {
  a.insertChildAfter(a.b[0], {toString: function() { throw 1; }});
} catch (e) { }

for each (var i in a) { }

But my patch has not fixed it either and it can go to a separated bug.

Still before giving r=+ I check few more times.
That xml_enumerateValues iloop was just a late-night glitch -- easy to fix.

There should not be null dereferences now that all the XMLARRAY_MEMBER uses except those under ParseNodeTo{QName,XML} do null checks.

/be
Attachment #241605 - Attachment is obsolete: true
Attachment #241620 - Flags: review?(igor.bukanov)
Attachment #241605 - Flags: review?(igor.bukanov)
Attachment #241620 - Attachment is obsolete: true
Attachment #241626 - Flags: review?(igor.bukanov)
Attachment #241620 - Flags: review?(igor.bukanov)
The null defense needed in most calls to XMLARRAY_MEMBER to cope with both failed Insert (due to exception under toString) and any mutations that leave the length shorter than the index is usually written so as to continue for (i=0...) loops -- but XMLArrayCursorNext loops will break on the first null embedded in an array due to throw from toString.

If the nulls can only be embedded at the end, this is ok.  But it's inconsistent and perhaps incorrect.  Igor, what do you think?  XMLArrayCursorNext could skip null items (and then xml_enumerate* should use XMLArrayCursorNext instead of the pre-existing inline equivalents).

/be
(In reply to comment #47)
> If the nulls can only be embedded at the end, this is ok.

But if so, then for(i=0...) loops should break on the first null item.

/be
The solution for Insert problem is simple: just inline Replace code and move value-to-string-to-text conversion before the insertion begins which allows not to worry about insert injecting nulls at all.

The patch also changes Insert to accept insert index as uint32 directly as it is always used with integer ids. 

I hope the interdiff would tell the story in details.
Attachment #241587 - Attachment is obsolete: true
Attachment #241646 - Flags: review?(brendan)
Attachment #241587 - Flags: review?(mrbkap)
Attachment #241587 - Flags: review?(brendan)
The previous patch ignores insert if value-to-string-to-xml removes elements so the index is out of bounds. I think it is better to always insert so the new version resets i when necessary.
Attachment #241646 - Attachment is obsolete: true
Attachment #241647 - Flags: review?(brendan)
Attachment #241646 - Flags: review?(brendan)
Attachment #241626 - Flags: review?(igor.bukanov)
Comment on attachment 241647 [details] [diff] [review]
alternative approach + always insert fix

Great, land it on the trunk and we can get it into the 1.8 branch for rc3 tomorrow (Bob, Jeff, anyone else who can, please pound on E4X).

/be
Attachment #241647 - Flags: review?(brendan) → review+
Comment on attachment 241626 [details] [diff] [review]
alternative approach, minimal iloop fix

I give r=+ based on the observation that the patch solves all the discovered problems with jsxml.c and looking through the changes I do not see anything suspicious. On the other hand after starring at jsxml.c for few days chances that I would notes the problem with the way patch is coded is 0. So it would be nice if somebody fresh just look at it.   





After starring into jsxml.c  for few days I ca
Attachment #241626 - Flags: review+
Flags: blocking1.8.1.1? → blocking1.8.0.8?
(In reply to comment #52)
> (From update of attachment 241626 [details] [diff] [review] [edit])
> On the other hand after starring at jsxml.c for few days chances
> that I would notes the problem with the way patch is coded is 0. So it would
> be nice if somebody fresh just look at it.

Jeff Walden, you are that man! :-P

I'm happier with the code now, finally.  Onward to thread safety!

/be
Comment on attachment 241626 [details] [diff] [review]
alternative approach, minimal iloop fix

Leaving r+ for posterity but marking obsolete by Igor's patch based on this one, with Insert fixed not to leave null item on exception from ToString.

/be
Attachment #241626 - Attachment is obsolete: true
I committed the patch from comment 50 (: nice number :) to the trunk:

Checking in jsxml.c;
/cvsroot/mozilla/js/src/jsxml.c,v  <--  jsxml.c
new revision: 3.131; previous revision: 3.130
done
Checking in jsxml.h;
/cvsroot/mozilla/js/src/jsxml.h,v  <--  jsxml.h
new revision: 3.15; previous revision: 3.14
done
Status: ASSIGNED → RESOLVED
Closed: 18 years ago
Resolution: --- → FIXED
The patch does not applies to 1.8.1 branch as the branch lacks patches from bug  354151 and bug 355474.
Blocking for Fx2 RC3
Flags: blocking1.8.1? → blocking1.8.1+
I record strong dependency of the committed patch to the trunk on the patch from bug 354151 that show the same scale of threat as this bug but the patch there is not yet approved for 1.8.1.
Depends on: 354151
Depends on: 353165
If I apply the patches from bug 353165 and bug 354151 and remove junk trailing whitespace in xml_namespace(), then the patch applies cleanly to 1.8.1 branch.
Attachment #241647 - Flags: approval1.8.1?
Flags: in-testsuite+
Flags: blocking1.8.0.8? → blocking1.8.0.8+
verified fixed 1.9 20061009 windows/linux
Status: RESOLVED → VERIFIED
Comment on attachment 241647 [details] [diff] [review]
alternative approach + always insert fix

a=beltzner on behalf of drivers for 1.8.1 branch RC 3
Attachment #241647 - Flags: approval1.8.1? → approval1.8.1+
I committed the patch from comment 50 to MOZILLA_1_8_BRANCH:

Checking in jsxml.c;
/cvsroot/mozilla/js/src/jsxml.c,v  <--  jsxml.c
new revision: 3.50.2.52; previous revision: 3.50.2.51
done
Checking in jsxml.h;
/cvsroot/mozilla/js/src/jsxml.h,v  <--  jsxml.h
new revision: 3.10.4.4; previous revision: 3.10.4.3
done

Note that before applying the patch I have to remove junk whitespace at line 6323. After that the patch applied as is and jsxml.c became the same as it is on the trunk, which is expected.
Keywords: fixed1.8.1
The branch 1.8.0 requires own patch as does not have js_EqualStrings.
For the record: after the patch xml_child_helper may set rval to JSVAL_NULL, but xml_child does not check for this. This could be non-bug since the current code may never set a member to null AFAICS. Still for the future this has to be dealt somehow. 
This is a backport to 1.8.0 branch. I will do some quick checks before asking for approval etc.
The prev patch missed POP_TEMP_ROOT in PutProperty.
Attachment #241897 - Attachment is obsolete: true
Attachment #241900 - Flags: review?(brendan)
Attachment #241900 - Flags: approval1.8.0.8?
Attached file diff between trunk and 1.8.0 patches (obsolete) —
Few comments: the biggest deviations between trunk and 1.8.0 comes from XMLToXMLString and js_MarkXML. In the former case the oder of applying steps 17(c) and 17(b) are different and in the latter case xml_mark_tail is inlined on the trunk. So this has to be checked in the patch itself.

In the rest of cases this diff is pretty effective for patch checking.
Comment on attachment 241900 [details] [diff] [review]
alternative approach + always insert fix for 1.8.0 branch

Looks correct -- thanks.

/be
Attachment #241900 - Flags: review?(brendan) → review+
(In reply to comment #71)
> For the record: after the patch xml_child_helper may set rval to JSVAL_NULL,
> but xml_child does not check for this. This could be non-bug since the current
> code may never set a member to null AFAICS. Still for the future this has to be
> dealt somehow. 

File a new bug?

/be
Blocks: 356238
verified fixed 1.8 20061010 windows/mac*/linux
In the previous patch I forgot to remove in XMLToXMLString the assignment to kid at line 2683. With this patch e4x tests show the same list of regressions with the same output as was before the patch.
Attachment #241900 - Attachment is obsolete: true
Attachment #241901 - Attachment is obsolete: true
Attachment #241934 - Flags: review?(brendan)
Attachment #241934 - Flags: approval1.8.0.8?
Attachment #241900 - Flags: approval1.8.0.8?
Whiteboard: [sg:critical?]
Comment on attachment 241934 [details] [diff] [review]
alternative approach + always insert fix for 1.8.0 branch v3

I should have seen that in the plain diff of patches:

433c433
< -            kid = XMLARRAY_MEMBER(&xml->xml_kids, i, JSXML);
---
>              kid = XMLARRAY_MEMBER(&xml->xml_kids, i, JSXML);

/be
Attachment #241934 - Flags: review?(brendan) → review+
Comment on attachment 241934 [details] [diff] [review]
alternative approach + always insert fix for 1.8.0 branch v3

approved for 1.8.0 branch, a=dveditz for drivers
Attachment #241934 - Flags: approval1.8.0.8? → approval1.8.0.8+
I committed the patch from comment 78 to MOZILLA_1_8_0_BRANCH:

Checking in jsxml.c;
/cvsroot/mozilla/js/src/jsxml.c,v  <--  jsxml.c
new revision: 3.50.2.15.2.22; previous revision: 3.50.2.15.2.21
done
Checking in jsxml.h;
/cvsroot/mozilla/js/src/jsxml.h,v  <--  jsxml.h
new revision: 3.10.4.1.2.1; previous revision: 3.10.4.1
done
Keywords: fixed1.8.0.8
Comment on attachment 241647 [details] [diff] [review]
alternative approach + always insert fix

Sorry for the delay, but I've been pretty busy lately, and looking through this took a while.

As a general note, I'm slightly confused about the null-checking strategy here.  Most places null-check, a very few don't, and it's not really obvious to me how the distinction was made.  In some cases proving that the null-check is unnecessary requires reading up a few screens and thus is reasonable from a maintenance standpoint, but in other cases it seems more obvious but is still done.


>@@ -1384,15 +1417,16 @@ ParseNodeToQName(JSContext *cx, JSParseN
>             while (n != 0) {
>-                ns = XMLARRAY_MEMBER(inScopeNSes, --n, JSXMLNamespace);
>+                --n;
>+                ns = XMLARRAY_MEMBER(inScopeNSes, n, JSXMLNamespace);
>                 if (ns->prefix && js_EqualStrings(ns->prefix, prefix)) {

Irrelevant aside: this is because macro arguments can be evaluated multiple times, correct?


>@@ -1418,15 +1452,16 @@ ParseNodeToQName(JSContext *cx, JSParseN
>             n = inScopeNSes->length;
>             while (n != 0) {
>-                ns = XMLARRAY_MEMBER(inScopeNSes, --n, JSXMLNamespace);
>+                --n;
>+                ns = XMLARRAY_MEMBER(inScopeNSes, n, JSXMLNamespace);
>                 if (!ns->prefix || IS_EMPTY(ns->prefix)) {
>                     uri = ns->uri;
>                     break;
>                 }

No null-check of ns here, and at least from my readthrough it doesn't look easy (if it's even possible) to prove that inScopeNSes won't contain a null.


>     for (i = 0, n = decls.length; i < n; i++) {
>         ns2 = XMLARRAY_MEMBER(&decls, i, JSXMLNamespace);
>+        if (!ns2)
>+            continue;
>         JS_ASSERT(!XMLARRAY_HAS_MEMBER(&ancdecls, ns2, namespace_identity));
>         if (!XMLARRAY_APPEND(cx, &ancdecls, ns2))
>             goto out;
>     }

This null-check isn't necessary, as can easily be proven from the preceding code, although it requires a few screens of reading to verify and thus is probably reasonable.


>@@ -3227,26 +3290,30 @@ DeepCopy(JSContext *cx, JSXML *xml, JSOb

>-    for (i = j = 0; i < n; i++) {
>-        kid = XMLARRAY_MEMBER(from, i, JSXML);
>+    XMLArrayCursorInit(&cursor, from);
>+    j = 0;
>+    ok = JS_TRUE;
>+    while ((kid = (JSXML *) XMLArrayCursorNext(&cursor)) != NULL) {
>         if ((flags & XSF_IGNORE_COMMENTS) &&
>             kid->xml_class == JSXML_CLASS_COMMENT) {
>             continue;
>         }
...

What's the reason for the |ok| temp variable here?  Why not use break/if (kid) since that's what's used everywhere else?


>    } else {
>        xvec = (JSXML **) xml->xml_kids.vector;
>        vvec = (JSXML **) vxml->xml_kids.vector;
>        for (i = 0; i < n; i++) {
>            xobj = js_GetXMLObject(cx, xvec[i]);
>            vobj = js_GetXMLObject(cx, vvec[i]);
>            if (!xobj || !vobj)
>                return JS_FALSE;
>            if (!js_XMLObjectOps.equality(cx, xobj, OBJECT_TO_JSVAL(vobj), bp))
>                return JS_FALSE;
>            if (!*bp)
>                break;
>        }

This clearly isn't going to handle holes in xml->xml_kids very well -- looks like js_GetXMLObject would crash trying to dereference (x|v)vec[i].  (Note: this was inside XMLEquals but not in the patch's context.)


>     * [[Replace]] handles _i >= x.[[Length]]_ by incrementing _x.[[Length]_.

This isn't part of patch, but this line's missing a "]" at the end.  Could someone with the time to do so fix this?


The namespace_identity function doesn't null-check either of its arguments, but it's used as a comparator with XMLARRAY_HAS_MEMBER multiple times (and in places where I'm not sure it's completely safe to do so -- SyncInScopeNamespaces in particular looks suspicious).  The same holds for the attr_identity and namespace_full_match functions.


Anyway, that's all I saw in the patch that looks possibly-relevant.  Hope that helps...
Blocks: 357063
(In reply to comment #82)
> 
> >    } else {
> >        xvec = (JSXML **) xml->xml_kids.vector;
> >        vvec = (JSXML **) vxml->xml_kids.vector;
> >        for (i = 0; i < n; i++) {
> >            xobj = js_GetXMLObject(cx, xvec[i]);
> >            vobj = js_GetXMLObject(cx, vvec[i]);
> >            if (!xobj || !vobj)
> >                return JS_FALSE;
> >            if (!js_XMLObjectOps.equality(cx, xobj, OBJECT_TO_JSVAL(vobj), bp))
> >                return JS_FALSE;
> >            if (!*bp)
> >                break;
> >        }
> 
> This clearly isn't going to handle holes in xml->xml_kids very well -- looks
> like js_GetXMLObject would crash trying to dereference (x|v)vec[i].  (Note:
> this was inside XMLEquals but not in the patch's context.)

It is worse than that since in fact this is a GC hazard, see bug 354145.
No longer blocks: 357063
(In reply to comment #83)
> It is worse than that since in fact this is a GC hazard, see bug 354145.
                                                           ^^^^^^^^^^^^^^

That should be bug 357063.

Blocks: 357063
Comment on attachment 241647 [details] [diff] [review]
alternative approach + always insert fix

>@@ -6464,22 +6619,24 @@ xml_normalize(JSContext *cx, JSObject *o

>         } else if (kid->xml_class == JSXML_CLASS_TEXT) {
>             while (i + 1 < n &&
>-                   (kid2 = XMLARRAY_MEMBER(&xml->xml_kids, i + 1, JSXML))
>-                   ->xml_class == JSXML_CLASS_TEXT) {
>+                   (kid2 = XMLARRAY_MEMBER(&xml->xml_kids, i + 1, JSXML)) &&
>+                   kid2->xml_class == JSXML_CLASS_TEXT) {
>                 str = js_ConcatStrings(cx, kid->xml_value, kid2->xml_value);
>                 if (!str)
>                     return JS_FALSE;

Another missing null-check here.
Igor: e4x/Regress/regress-354145-03.js fails to execute the TEST function call after the list.contains() call. Is this a bug?
(In reply to comment #86)
> Igor: e4x/Regress/regress-354145-03.js fails to execute the TEST function call
> after the list.contains() call. Is this a bug?

The file attached to the bug contains the TEST call. Do you mean some file?
(In reply to comment #87)

add some print statements around the list.contains(...) like:

print("Before list.contains");
list.contains({ toString: prepare });
print("After list.contains");

After list.contains is not printed.
(In reply to comment #88)
> (In reply to comment #87)
> 
> add some print statements around the list.contains(...) like:
> 
> print("Before list.contains");
> list.contains({ toString: prepare });
> print("After list.contains");
> 
> After list.contains is not printed.

That is a regression. I will file a bug for it.
Blocks: 358965
Bug not relevant to aviary/moz1.7 branches
Flags: blocking1.7.14-
Flags: blocking-aviary1.0.9-
Group: security
verified fixed 1.8.0.8 20061015. Note 'FAILED HARNESS' on -03 was fixed in bug 358965.
RCS file: /cvsroot/mozilla/js/tests/e4x/Regress/regress-354145-01.js,v
done
Checking in regress-354145-01.js;
/cvsroot/mozilla/js/tests/e4x/Regress/regress-354145-01.js,v  <--  regress-354145-01.js
initial revision: 1.1
done
RCS file: /cvsroot/mozilla/js/tests/e4x/Regress/regress-354145-02.js,v
done
Checking in regress-354145-02.js;
/cvsroot/mozilla/js/tests/e4x/Regress/regress-354145-02.js,v  <--  regress-354145-02.js
initial revision: 1.1
done
RCS file: /cvsroot/mozilla/js/tests/e4x/Regress/regress-354145-03.js,v
done
Checking in regress-354145-03.js;
/cvsroot/mozilla/js/tests/e4x/Regress/regress-354145-03.js,v  <--  regress-354145-03.js
initial revision: 1.1
done
RCS file: /cvsroot/mozilla/js/tests/e4x/Regress/regress-354145-04.js,v
done
Checking in regress-354145-04.js;
/cvsroot/mozilla/js/tests/e4x/Regress/regress-354145-04.js,v  <--  regress-354145-04.js
initial revision: 1.1
done
RCS file: /cvsroot/mozilla/js/tests/e4x/Regress/regress-354145-05.js,v
done
Checking in regress-354145-05.js;
/cvsroot/mozilla/js/tests/e4x/Regress/regress-354145-05.js,v  <--  regress-354145-05.js
initial revision: 1.1
done
RCS file: /cvsroot/mozilla/js/tests/e4x/Regress/regress-354145-06.js,v
done
Checking in regress-354145-06.js;
/cvsroot/mozilla/js/tests/e4x/Regress/regress-354145-06.js,v  <--  regress-354145-06.js
initial revision: 1.1
done
RCS file: /cvsroot/mozilla/js/tests/e4x/Regress/regress-354145-07.js,v
done
Checking in regress-354145-07.js;
/cvsroot/mozilla/js/tests/e4x/Regress/regress-354145-07.js,v  <--  regress-354145-07.js
initial revision: 1.1
done
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: