RESOLVED FIXED 88500
[Crash][Editing] VisibleSelection::adjustSelectionToAvoidCrossingEditingBoundaries() crashes in some Shadow DOM case
https://bugs.webkit.org/show_bug.cgi?id=88500
Summary [Crash][Editing] VisibleSelection::adjustSelectionToAvoidCrossingEditingBound...
Shinya Kawanaka
Reported 2012-06-06 22:04:17 PDT
HTML <div contenteditable> <div>BEFORE HOST</div> <div id="host" contenteditable="false"> a </div> </div> Shadow DOM for host <div id="shadow-host">SHADOW (BEFORE)<shadow></shadow>SHADOW (AFTER)</div> Nested Shadow DOM for shadow-host <div contenteditable>NESTED BEFORE<shadow></shadow>NESTED AFTER</div> Select from 'a' to left, then ASSERTION triggered. SHOULD NEVER BE REACHED VisibleSelection.cpp(592) : adjustSelectionToAvoidCrossingEditingBoundaries()
Attachments
Patch (7.83 KB, patch)
2012-06-07 05:42 PDT, Shinya Kawanaka
no flags
Patch (7.81 KB, patch)
2012-06-10 21:20 PDT, Shinya Kawanaka
no flags
Patch (8.25 KB, patch)
2012-06-13 18:57 PDT, Shinya Kawanaka
no flags
Patch (9.20 KB, patch)
2012-06-13 23:28 PDT, Shinya Kawanaka
no flags
Patch (9.41 KB, patch)
2012-06-14 00:15 PDT, Shinya Kawanaka
no flags
Patch (9.59 KB, patch)
2012-06-14 21:59 PDT, Shinya Kawanaka
no flags
Patch (9.56 KB, patch)
2012-06-14 22:00 PDT, Shinya Kawanaka
no flags
Patch for landing (9.54 KB, patch)
2012-06-18 11:46 PDT, Shinya Kawanaka
no flags
Patch for landing (9.54 KB, patch)
2012-06-18 15:12 PDT, Shinya Kawanaka
no flags
Shinya Kawanaka
Comment 1 2012-06-07 05:42:16 PDT
Ryosuke Niwa
Comment 2 2012-06-07 23:39:33 PDT
Comment on attachment 146263 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=146263&action=review > Source/WebCore/editing/VisibleSelection.cpp:496 > + m_extent = adjustPositionForEnd(m_start.anchorNode()->treeScope(), m_end, m_start.containerNode()); Why are we using anchorNode's treeScope and then passing containerNode!? anchorNode != containerNode. Can we just pass m_start.containerNode() here and obtain the tree scope inside the function?
Shinya Kawanaka
Comment 3 2012-06-10 21:20:38 PDT
Shinya Kawanaka
Comment 4 2012-06-10 21:21:46 PDT
> Why are we using anchorNode's treeScope and then passing containerNode!? > anchorNode != containerNode. Can we just pass m_start.containerNode() here and obtain the tree scope inside the function? yeah... it was strange... I've update the patch. anchorNode()->treeScope() and containerNode()->treeScope() should be the same basically.
Ryosuke Niwa
Comment 5 2012-06-11 22:25:36 PDT
Comment on attachment 146775 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=146775&action=review > Source/WebCore/editing/VisibleSelection.cpp:501 > - m_extent = adjustPositionBefore(m_start.anchorNode()->treeScope(), m_end); > + m_extent = adjustPositionForEnd(m_start.containerNode()->treeScope(), m_end, m_start.containerNode()); Again, why do we need to call treeScope here? You can just call it inside the function, right? > Source/WebCore/editing/VisibleSelection.cpp:504 > - m_extent = adjustPositionAfter(m_end.anchorNode()->treeScope(), m_start); > + m_extent = adjustPositionForStart(m_end.containerNode()->treeScope(), m_start, m_end.containerNode()); Ditto. > LayoutTests/editing/shadow/breaking-editing-boundaries.html:10 > +<div contenteditable> > + BEFORE<div id="host" contenteditable="false">DRAG FROM<span id="target"> </span>HERE</div> > +</div> Please add a test description.
Shinya Kawanaka
Comment 6 2012-06-13 18:18:08 PDT
(In reply to comment #5) > (From update of attachment 146775 [details]) > View in context: https://bugs.webkit.org/attachment.cgi?id=146775&action=review > > > Source/WebCore/editing/VisibleSelection.cpp:501 > > - m_extent = adjustPositionBefore(m_start.anchorNode()->treeScope(), m_end); > > + m_extent = adjustPositionForEnd(m_start.containerNode()->treeScope(), m_end, m_start.containerNode()); > > Again, why do we need to call treeScope here? You can just call it inside the function, right? Ah, OK. I didn't understand what you said...
Shinya Kawanaka
Comment 7 2012-06-13 18:57:43 PDT
Ryosuke Niwa
Comment 8 2012-06-13 19:31:15 PDT
Comment on attachment 147466 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=147466&action=review > Source/WebCore/editing/VisibleSelection.cpp:468 > + if (Node* ancestor = treeScope->ancestorInThisScope(currentPosition.anchorNode())) { What if this position was an offset in the anchor node? Why is it okay to move it before or after the node? > Source/WebCore/editing/VisibleSelection.cpp:471 > + if (ancestor->contains(startContainerNode)) > + return positionAfterNode(ancestor); > + return positionBeforeNode(ancestor); Why do we need to modify position when the ancestor is in the same tree scope? That sounds red-herring.
Shinya Kawanaka
Comment 9 2012-06-13 23:20:17 PDT
(In reply to comment #8) > (From update of attachment 147466 [details]) > View in context: https://bugs.webkit.org/attachment.cgi?id=147466&action=review > > > Source/WebCore/editing/VisibleSelection.cpp:468 > > + if (Node* ancestor = treeScope->ancestorInThisScope(currentPosition.anchorNode())) { > > What if this position was an offset in the anchor node? Why is it okay to move it before or after the node? > Actually currentPosition.anchorNode()->treeScope() will not be equal to startContainerNode->treeScope() here. I should have asserted. (I'll update the patch soon) So treeScope->ancestorInThisScope(currentPosition.containerNode()) will never return currentPosition.containerNode() itself here. > > Source/WebCore/editing/VisibleSelection.cpp:471 > > + if (ancestor->contains(startContainerNode)) > > + return positionAfterNode(ancestor); > > + return positionBeforeNode(ancestor); > > Why do we need to modify position when the ancestor is in the same tree scope? That sounds red-herring. Let's think: '<div id="host">A<div>D' and host having a ShadowRoot whose innerHTML is "B<content></content>C". When selecting from A (including) to B (including), comparePosition() will judge as B is always before to A. So m_start will be B, and m_end will be A. A and B have the different tree scope, we have to adjust them. However, the current implementation on of adjustSelectionToAvoidCrossingShadowBoundaries() always move B to just after A (positionAfterNode(A)). This breaks the assumption m_start is before m_end. So in that case, we have to adjust A to positionBeforeNode(A). Node that we cannot always move A to positionBeforeNode(A). When selecting from D (including) to B (including), we don't want to select A. It's like if <div id="host"></div> is <input>, we don't want to select <input>. So in this case we want to adjust B to positionAfterNode(A).
Shinya Kawanaka
Comment 10 2012-06-13 23:28:21 PDT
Ryosuke Niwa
Comment 11 2012-06-13 23:49:37 PDT
(In reply to comment #9) > (In reply to comment #8) > > (From update of attachment 147466 [details] [details]) > > View in context: https://bugs.webkit.org/attachment.cgi?id=147466&action=review > > > > > Source/WebCore/editing/VisibleSelection.cpp:468 > > > + if (Node* ancestor = treeScope->ancestorInThisScope(currentPosition.anchorNode())) { > > > > What if this position was an offset in the anchor node? Why is it okay to move it before or after the node? > > > > Actually currentPosition.anchorNode()->treeScope() will not be equal to startContainerNode->treeScope() here. Could be, but not always. > > > Source/WebCore/editing/VisibleSelection.cpp:471 > > > + if (ancestor->contains(startContainerNode)) > > > + return positionAfterNode(ancestor); > > > + return positionBeforeNode(ancestor); > > > > Why do we need to modify position when the ancestor is in the same tree scope? That sounds red-herring. > > Let's think: '<div id="host">A<div>D' and host having a ShadowRoot whose innerHTML is "B<content></content>C". > > When selecting from A (including) to B (including), comparePosition() will judge as B is always before to A. So m_start will be B, and m_end will be A. You can't say m_start is B. You can't talk about a position being at some node. A position is a point in the DOM tree consisting of anchor type, anchor, and offset. There are 5 anchor types, and anchor and offset mean different things in each type: PositionIsOffsetInAnchor - (anchor, offset) specifies the point in DOM. (div, 2) for example means that the position is before the 3rd children of the div (i.e. there are 2 children before this position). PositionIsBeforeAnchor - The position is before the anchor; offset is ignored. i.e. if we have <div><em>a</em>b</div> and the anchor is at "b", then the position is after the em element and before "b". This is the same position as (PositionIsOffsetInAnchor, div, 1) PositionIsAfterAnchor - The position is after the anchor. With the same example, the position is after "b" and is identical to (PositionIsOffsetInAnchor, div, 2) PositionIsBeforeChildren - The position is the first point in the anchor. If the anchor is at the em element above, then the position is before "a" is identical to (PositionIsOffsetInAnchor, em, 0) PositionIsAfterChildren - The position is the last point in the anchor. If the anchor is at the em element above, then the position is after "a" is identical to (PositionIsOffsetInAnchor, em, 1) > A and B have the different tree scope, we have to adjust them. However, the current implementation on of adjustSelectionToAvoidCrossingShadowBoundaries() always move B to just after A (positionAfterNode(A)). This breaks the assumption m_start is before m_end. So in that case, we have to adjust A to positionBeforeNode(A). > > Node that we cannot always move A to positionBeforeNode(A). When selecting from D (including) to B (including), we don't want to select A. It's like if <div id="host"></div> is <input>, we don't want to select <input>. So in this case we want to adjust B to positionAfterNode(A). Because of this, what you're talking here is ambiguous. For example, the position immediately before the letter "B" can be expressed as (PositionIsOffsetInAnchor, shadow root, 0), (PositionIsBeforeChildren, shadow root), (PositionIsBeforeAnchor, "B", 0), or (PositionIsOffsetInAnchor, "B", 0).
Shinya Kawanaka
Comment 12 2012-06-13 23:50:08 PDT
Comment on attachment 147497 [details] Patch Oops, I've uploaded a wrong patch...
Ryosuke Niwa
Comment 13 2012-06-13 23:55:50 PDT
> the current implementation on of adjustSelectionToAvoidCrossingShadowBoundaries() always move B to just after A (positionAfterNode(A)). Here, for example, we need to first identify which of 6 possible positions we're talking about offset 0, offset 1, before, after positions anchored at the text node "B" and beforechildren, offset 0 positions anchored at the shadow root.
Shinya Kawanaka
Comment 14 2012-06-14 00:02:33 PDT
> > Actually currentPosition.anchorNode()->treeScope() will not be equal to startContainerNode->treeScope() here. > > Could be, but not always. Since adjustPositionForEnd and adjustPositionForStart are used in adjustSelectionToAvoidCrossingShadowBoundaries() and it checks the the equality of treeScopes of m_start->anchorNode() and m_end->anchorNode()... if Position is valid, what kind of pattern makes them different? > Because of this, what you're talking here is ambiguous. For example, the position immediately before the letter "B" can be expressed as (PositionIsOffsetInAnchor, shadow root, 0), (PositionIsBeforeChildren, shadow root), (PositionIsBeforeAnchor, "B", 0), or (PositionIsOffsetInAnchor, "B", 0). OK. Let me try to make this strict... (in another reply)
Build Bot
Comment 15 2012-06-14 00:05:55 PDT
Shinya Kawanaka
Comment 16 2012-06-14 00:08:46 PDT
Let's think: '<div id="host">A<div>D' and host having a ShadowRoot whose innerHTML is "B<content></content>C". When selecting from p1 (PositionIsOffsetAnchor, div, 1) to p2 (PositionIsOffsetInAnchor, shadowRoot, 0), comparePosition() will judge as p2 is always before to p1. So m_start will be p2, and m_end will be p1. The anchor nodes of p1 and p2 have the different tree scope, we have to adjust them. However, the current implementation on of adjustSelectionToAvoidCrossingShadowBoundaries() always move p2 to just after A (positionAfterNode(div), i.e. (PositionIsAfterAnchor, div)). This breaks the assumption m_start is before m_end. So in that case, we have to adjust A to positionBeforeNode(div) i.e. (PositionIsBeforeAnchor, div). Node that we cannot always move p1 to positionBeforeNode(div). When selecting from p3 (PositionIsOffsetAnchor, D's parent, 2), to p2, we don't want to select A. It's like if <div id="host"></div> is <input>, we don't want to select <input>. So in this case we want to adjust p2 to positionAfterNode(div).
Shinya Kawanaka
Comment 17 2012-06-14 00:15:46 PDT
Ryosuke Niwa
Comment 18 2012-06-14 01:46:57 PDT
Comment on attachment 147502 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=147502&action=review > Source/WebCore/ChangeLog:11 > + For example, let's think '<div id="host">A</div>D' and host has a Shadow DOM whose innerHTML is Nit: s/let's think/consider/ > Source/WebCore/ChangeLog:13 > + 'B<content></content>C'. When selecting from just after A (p1 = (PositionIsOffsetInAnchor, div, 1)) to > + B (p2 = (PositionIsOffsetInAnchor, shadowRoot, 0)), since comparePosition always judge as p2 is How does (PositionIsOffsetInAnchor, div, 1) make sense? When we have a shadow DOM on #host, #host should be treated as an atomic node. Selection should never be inside #host. Or is this a legacy editing offset?
Shinya Kawanaka
Comment 19 2012-06-14 01:58:05 PDT
> > Source/WebCore/ChangeLog:13 > > + 'B<content></content>C'. When selecting from just after A (p1 = (PositionIsOffsetInAnchor, div, 1)) to > > + B (p2 = (PositionIsOffsetInAnchor, shadowRoot, 0)), since comparePosition always judge as p2 is > > How does (PositionIsOffsetInAnchor, div, 1) make sense? When we have a shadow DOM on #host, #host should be treated as an atomic node. > Selection should never be inside #host. Or is this a legacy editing offset? Yeah, Selection should never be inside #host. But before adjusting, selection could be inside the host. So I wrote as (PositionIsOffsetInAnchor, div, 1). It's a position before calling adjustSelectionToAvoidCrossingShadowBoundaries().
Ryosuke Niwa
Comment 20 2012-06-14 02:43:15 PDT
(In reply to comment #19) > Yeah, Selection should never be inside #host. But before adjusting, selection could be inside the host. > So I wrote as (PositionIsOffsetInAnchor, div, 1). It's a position before calling adjustSelectionToAvoidCrossingShadowBoundaries(). Even before adjusting it, we should never have that position. Isn't it a legacy editing position? (i.e. m_isLegacyEditingPosition is true). If not, we need to figure out where this position is created and fix there as well.
Shinya Kawanaka
Comment 21 2012-06-14 21:45:32 PDT
(In reply to comment #20) > (In reply to comment #19) > > Yeah, Selection should never be inside #host. But before adjusting, selection could be inside the host. > > So I wrote as (PositionIsOffsetInAnchor, div, 1). It's a position before calling adjustSelectionToAvoidCrossingShadowBoundaries(). > > Even before adjusting it, we should never have that position. Isn't it a legacy editing position? (i.e. m_isLegacyEditingPosition is true). If not, we need to figure out where this position is created and fix there as well. Sorry for late response. Yes. It's a legacy editing position. (m_isLegacyEditingPosition is true)
Ryosuke Niwa
Comment 22 2012-06-14 21:47:52 PDT
(In reply to comment #21) > Sorry for late response. Yes. It's a legacy editing position. (m_isLegacyEditingPosition is true) Okay, that makes sense. In that case, (host, 0) and (host, 1) should be treated like before and after the host.
Shinya Kawanaka
Comment 23 2012-06-14 21:59:25 PDT
(In reply to comment #22) > (In reply to comment #21) > > Sorry for late response. Yes. It's a legacy editing position. (m_isLegacyEditingPosition is true) > > Okay, that makes sense. In that case, (host, 0) and (host, 1) should be treated like before and after the host. Really...? OK. I apologize I used a reduced example. Let me try to explain using the real example. # Before that I upload a new Patch...
Shinya Kawanaka
Comment 24 2012-06-14 21:59:47 PDT
Shinya Kawanaka
Comment 25 2012-06-14 22:00:22 PDT
Ryosuke Niwa
Comment 26 2012-06-14 22:07:34 PDT
(In reply to comment #23) > (In reply to comment #22) > > (In reply to comment #21) > > > Sorry for late response. Yes. It's a legacy editing position. (m_isLegacyEditingPosition is true) > > > > Okay, that makes sense. In that case, (host, 0) and (host, 1) should be treated like before and after the host. > > Really...? Yes. See https://rniwa.com/2011-06-26/position-and-anchor-types/ In legacy editing positions, when the anchor node is "atomic", offset 0 and offset 1 mean before and after the anchor.
Shinya Kawanaka
Comment 27 2012-06-14 22:09:33 PDT
In the testcase, after performing eventSender.mouseMoveTo(nestedShadowHost.offsetLeft + 10, nestedShadowHost.offsetTop + nestedShadowHost.offsetHeight / 2), VisibleSelection::adjustSelectionToAvoidCrossingShadowBoundaries() will be executed a few times. In one of them, "if (m_baseIsFirst) { ... } " is executed. In this moment, m_baseIsFirst = false m_start = { m_anchorNode = ...snip.../SPAN[@id="shadow-host"]/#shadow-root[0]/SPAN/text() m_offset = 1, m_anchorType = 0, m_isLegacyEditingPosition = true } m_end = { m_anchorNode = ...snip.../SPAN[@id="host" and position() = 0]/text() m_offset = 13, m_anchorType = 0, m_isLegacyEditingPosition = true } This does not seem that we should treat such positons as before the host or after the host. I'm sorry for my explanation ambiguity...
Shinya Kawanaka
Comment 28 2012-06-14 22:10:55 PDT
I'm very sorry that my previous explanation was not correct or strict.
Ryosuke Niwa
Comment 29 2012-06-15 01:53:37 PDT
Comment on attachment 147727 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=147727&action=review > LayoutTests/editing/shadow/breaking-editing-boundaries.html:12 > + BEFORE<span id="host" contenteditable="false">DRAG FROM HERE<span id="target"> </span>FOO BAR</span> Nit: ALL CAPS DON'T READ WELL. Please use normal capitalization.
Shinya Kawanaka
Comment 30 2012-06-18 11:46:14 PDT
Created attachment 148141 [details] Patch for landing
WebKit Review Bot
Comment 31 2012-06-18 14:40:35 PDT
Comment on attachment 148141 [details] Patch for landing Rejecting attachment 148141 [details] from commit-queue. Failed to run "['/mnt/git/webkit-commit-queue/Tools/Scripts/webkit-patch', '--status-host=queues.webkit.org', '-..." exit_code: 1 ERROR: /mnt/git/webkit-commit-queue/LayoutTests/ChangeLog neither lists a valid reviewer nor contains the string "Unreviewed" or "Rubber stamp" (case insensitive). Full output: http://queues.webkit.org/results/12970785
Shinya Kawanaka
Comment 32 2012-06-18 15:12:54 PDT
Created attachment 148184 [details] Patch for landing
WebKit Review Bot
Comment 33 2012-06-18 21:03:05 PDT
Comment on attachment 148184 [details] Patch for landing Clearing flags on attachment: 148184 Committed r120666: <http://trac.webkit.org/changeset/120666>
WebKit Review Bot
Comment 34 2012-06-18 21:03:12 PDT
All reviewed patches have been landed. Closing bug.
Note You need to log in before you can comment on or make changes to this bug.