Skip to content

Commit

Permalink
Bug 1873328 - disallow nested groupings of the same type, to prevent …
Browse files Browse the repository at this point in the history
…infinite recursion r=jimb

Differential Revision: https://phabricator.services.mozilla.com/D198852
  • Loading branch information
hotsphink committed Feb 23, 2024
1 parent 114ca3d commit fb30b19
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 23 deletions.
5 changes: 4 additions & 1 deletion devtools/shared/heapsnapshot/HeapSnapshot.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

#include "jsapi.h"
#include "jsfriendapi.h"
#include "js/GCVector.h"
#include "js/MapAndSet.h"
#include "js/Object.h" // JS::GetCompartment
#include "nsComponentManagerUtils.h" // do_CreateInstance
Expand Down Expand Up @@ -482,7 +483,9 @@ void HeapSnapshot::DescribeNode(JSContext* cx, JS::Handle<JSObject*> breakdown,
ErrorResult& rv) {
MOZ_ASSERT(breakdown);
JS::Rooted<JS::Value> breakdownVal(cx, JS::ObjectValue(*breakdown));
JS::ubi::CountTypePtr rootType = JS::ubi::ParseBreakdown(cx, breakdownVal);
JS::Rooted<JS::GCVector<JSLinearString*>> seen(cx, cx);
JS::ubi::CountTypePtr rootType =
JS::ubi::ParseBreakdown(cx, breakdownVal, &seen);
if (NS_WARN_IF(!rootType)) {
rv.Throw(NS_ERROR_UNEXPECTED);
return;
Expand Down
6 changes: 4 additions & 2 deletions js/public/UbiNodeCensus.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#ifndef js_UbiNodeCensus_h
#define js_UbiNodeCensus_h

#include "js/GCVector.h"
#include "js/UbiNode.h"
#include "js/UbiNodeBreadthFirst.h"

Expand Down Expand Up @@ -222,8 +223,9 @@ using CensusTraversal = BreadthFirst<CensusHandler>;
// Parse the breakdown language (as described in
// js/src/doc/Debugger/Debugger.Memory.md) into a CountTypePtr. A null pointer
// is returned on error and is reported to the cx.
JS_PUBLIC_API CountTypePtr ParseBreakdown(JSContext* cx,
HandleValue breakdownValue);
JS_PUBLIC_API CountTypePtr
ParseBreakdown(JSContext* cx, HandleValue breakdownValue,
MutableHandle<JS::GCVector<JSLinearString*>> seen);

} // namespace ubi
} // namespace JS
Expand Down
1 change: 1 addition & 0 deletions js/public/friend/ErrorNumbers.msg
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ MSG_DEF(JSMSG_QUERY_LINE_WITHOUT_URL, 0, JSEXN_TYPEERR, "findScripts query objec
MSG_DEF(JSMSG_DEBUG_CANT_SET_OPT_ENV, 1, JSEXN_REFERENCEERR, "can't set '{0}' in an optimized-out environment")
MSG_DEF(JSMSG_DEBUG_INVISIBLE_COMPARTMENT, 0, JSEXN_TYPEERR, "object in compartment marked as invisible to Debugger")
MSG_DEF(JSMSG_DEBUG_CENSUS_BREAKDOWN, 1, JSEXN_TYPEERR, "unrecognized 'by' value in takeCensus breakdown: {0}")
MSG_DEF(JSMSG_DEBUG_CENSUS_BREAKDOWN_NESTED, 1, JSEXN_TYPEERR, "takeCensus breakdown 'by' value nested within itself: {0}")
MSG_DEF(JSMSG_DEBUG_PROMISE_NOT_RESOLVED, 0, JSEXN_TYPEERR, "Promise hasn't been resolved")
MSG_DEF(JSMSG_DEBUG_PROMISE_NOT_FULFILLED, 0, JSEXN_TYPEERR, "Promise hasn't been fulfilled")
MSG_DEF(JSMSG_DEBUG_PROMISE_NOT_REJECTED, 0, JSEXN_TYPEERR, "Promise hasn't been rejected")
Expand Down
15 changes: 14 additions & 1 deletion js/src/doc/Debugger/Debugger.Memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ which produces a result like this:

In general, a `breakdown` value has one of the following forms:

* <code>{ by: "count", count:<i>count<i>, bytes:<i>bytes</i> }</code>
* <code>{ by: "count", count:<i>count</i>, bytes:<i>bytes</i> }</code>

The trivial categorization: none whatsoever. Simply tally up the items
visited. If <i>count</i> is true, count the number of items visited; if
Expand Down Expand Up @@ -409,6 +409,16 @@ In general, a `breakdown` value has one of the following forms:
breakdown value produces. All breakdown values are optional, and default
to `{ type: "count" }`.

* `{ by: "filename", then:breakdown, noFilename:noFilenameBreakdown }`

For scripts only, group by the filename of the script.

Further categorize all of the scripts from each distinct filename
using breakdown.

Scripts that lack a filename are counted using noFilenameBreakdown.
These appear in the result `Map` under the key string `"noFilename"`.

* `{ by: "internalType", then: breakdown }`

Group items by the names given their types internally by SpiderMonkey.
Expand Down Expand Up @@ -441,6 +451,9 @@ In general, a `breakdown` value has one of the following forms:
To simplify breakdown values, all `then` and `other` properties are optional.
If omitted, they are treated as if they were `{ type: "count" }`.

Breakdown groupings cannot be nested within themselves. This would not be
useful, and forbidding this prevents infinite recursion.

If the `options` argument has no `breakdown` property, `takeCensus` defaults
to the following:

Expand Down
26 changes: 26 additions & 0 deletions js/src/jit-test/tests/debug/Memory-takeCensus-06.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,29 @@ Pattern({
other: { by: 'count', label: 'other' }
}
}));

try {
const breakdown = { by: "objectClass" };
breakdown.then = breakdown;
dbg.memory.takeCensus({ breakdown });
assertEq(true, false, "should not reach here");
} catch (e) {
assertEq(e.message, "takeCensus breakdown 'by' value nested within itself: \"objectClass\"");
}

try {
const breakdown = { by: "objectClass", then: { by: "objectClass" } };
dbg.memory.takeCensus({ breakdown });
assertEq(true, false, "should not reach here");
} catch (e) {
assertEq(e.message, "takeCensus breakdown 'by' value nested within itself: \"objectClass\"");
}

try {
const breakdown = { by: "coarseType", scripts: { by: "filename" } };
breakdown.scripts.noFilename = breakdown;
dbg.memory.takeCensus({ breakdown });
assertEq(true, false, "should not reach here");
} catch (e) {
assertEq(e.message, "takeCensus breakdown 'by' value nested within itself: \"coarseType\"");
}
66 changes: 47 additions & 19 deletions js/src/vm/UbiNodeCensus.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

#include "js/UbiNodeCensus.h"

#include "mozilla/ScopeExit.h"

#include "builtin/MapObject.h"
#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
#include "js/Printer.h"
Expand Down Expand Up @@ -1062,17 +1064,19 @@ JS_PUBLIC_API bool CensusHandler::operator()(

/*** Parsing Breakdowns *****************************************************/

static CountTypePtr ParseChildBreakdown(JSContext* cx, HandleObject breakdown,
PropertyName* prop) {
static CountTypePtr ParseChildBreakdown(
JSContext* cx, HandleObject breakdown, PropertyName* prop,
MutableHandle<GCVector<JSLinearString*>> seen) {
RootedValue v(cx);
if (!GetProperty(cx, breakdown, breakdown, prop, &v)) {
return nullptr;
}
return ParseBreakdown(cx, v);
return ParseBreakdown(cx, v, seen);
}

JS_PUBLIC_API CountTypePtr ParseBreakdown(JSContext* cx,
HandleValue breakdownValue) {
JS_PUBLIC_API CountTypePtr
ParseBreakdown(JSContext* cx, HandleValue breakdownValue,
MutableHandle<GCVector<JSLinearString*>> seen) {
if (breakdownValue.isUndefined()) {
// Construct the default type, { by: 'count' }
CountTypePtr simple(cx->new_<SimpleCount>());
Expand All @@ -1097,6 +1101,24 @@ JS_PUBLIC_API CountTypePtr ParseBreakdown(JSContext* cx,
return nullptr;
}

for (auto candidate : seen.get()) {
if (EqualStrings(by, candidate)) {
UniqueChars byBytes = QuoteString(cx, by, '"');
if (!byBytes) {
return nullptr;
}

JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEBUG_CENSUS_BREAKDOWN_NESTED,
byBytes.get());
return nullptr;
}
}
if (!seen.append(by)) {
return nullptr;
}
auto popper = mozilla::MakeScopeExit([&]() { seen.popBack(); });

if (StringEqualsLiteral(by, "count")) {
RootedValue countValue(cx), bytesValue(cx);
if (!GetProperty(cx, breakdown, breakdown, cx->names().count,
Expand Down Expand Up @@ -1140,13 +1162,14 @@ JS_PUBLIC_API CountTypePtr ParseBreakdown(JSContext* cx,
}

if (StringEqualsLiteral(by, "objectClass")) {
CountTypePtr thenType(ParseChildBreakdown(cx, breakdown, cx->names().then));
CountTypePtr thenType(
ParseChildBreakdown(cx, breakdown, cx->names().then, seen));
if (!thenType) {
return nullptr;
}

CountTypePtr otherType(
ParseChildBreakdown(cx, breakdown, cx->names().other));
ParseChildBreakdown(cx, breakdown, cx->names().other, seen));
if (!otherType) {
return nullptr;
}
Expand All @@ -1156,27 +1179,27 @@ JS_PUBLIC_API CountTypePtr ParseBreakdown(JSContext* cx,

if (StringEqualsLiteral(by, "coarseType")) {
CountTypePtr objectsType(
ParseChildBreakdown(cx, breakdown, cx->names().objects));
ParseChildBreakdown(cx, breakdown, cx->names().objects, seen));
if (!objectsType) {
return nullptr;
}
CountTypePtr scriptsType(
ParseChildBreakdown(cx, breakdown, cx->names().scripts));
ParseChildBreakdown(cx, breakdown, cx->names().scripts, seen));
if (!scriptsType) {
return nullptr;
}
CountTypePtr stringsType(
ParseChildBreakdown(cx, breakdown, cx->names().strings));
ParseChildBreakdown(cx, breakdown, cx->names().strings, seen));
if (!stringsType) {
return nullptr;
}
CountTypePtr otherType(
ParseChildBreakdown(cx, breakdown, cx->names().other));
ParseChildBreakdown(cx, breakdown, cx->names().other, seen));
if (!otherType) {
return nullptr;
}
CountTypePtr domNodeType(
ParseChildBreakdown(cx, breakdown, cx->names().domNode));
ParseChildBreakdown(cx, breakdown, cx->names().domNode, seen));
if (!domNodeType) {
return nullptr;
}
Expand All @@ -1186,7 +1209,8 @@ JS_PUBLIC_API CountTypePtr ParseBreakdown(JSContext* cx,
}

if (StringEqualsLiteral(by, "internalType")) {
CountTypePtr thenType(ParseChildBreakdown(cx, breakdown, cx->names().then));
CountTypePtr thenType(
ParseChildBreakdown(cx, breakdown, cx->names().then, seen));
if (!thenType) {
return nullptr;
}
Expand All @@ -1195,20 +1219,22 @@ JS_PUBLIC_API CountTypePtr ParseBreakdown(JSContext* cx,
}

if (StringEqualsLiteral(by, "descriptiveType")) {
CountTypePtr thenType(ParseChildBreakdown(cx, breakdown, cx->names().then));
CountTypePtr thenType(
ParseChildBreakdown(cx, breakdown, cx->names().then, seen));
if (!thenType) {
return nullptr;
}
return CountTypePtr(cx->new_<ByDomObjectClass>(thenType));
}

if (StringEqualsLiteral(by, "allocationStack")) {
CountTypePtr thenType(ParseChildBreakdown(cx, breakdown, cx->names().then));
CountTypePtr thenType(
ParseChildBreakdown(cx, breakdown, cx->names().then, seen));
if (!thenType) {
return nullptr;
}
CountTypePtr noStackType(
ParseChildBreakdown(cx, breakdown, cx->names().noStack));
ParseChildBreakdown(cx, breakdown, cx->names().noStack, seen));
if (!noStackType) {
return nullptr;
}
Expand All @@ -1217,13 +1243,14 @@ JS_PUBLIC_API CountTypePtr ParseBreakdown(JSContext* cx,
}

if (StringEqualsLiteral(by, "filename")) {
CountTypePtr thenType(ParseChildBreakdown(cx, breakdown, cx->names().then));
CountTypePtr thenType(
ParseChildBreakdown(cx, breakdown, cx->names().then, seen));
if (!thenType) {
return nullptr;
}

CountTypePtr noFilenameType(
ParseChildBreakdown(cx, breakdown, cx->names().noFilename));
ParseChildBreakdown(cx, breakdown, cx->names().noFilename, seen));
if (!noFilenameType) {
return nullptr;
}
Expand Down Expand Up @@ -1307,8 +1334,9 @@ JS_PUBLIC_API bool ParseCensusOptions(JSContext* cx, Census& census,
return false;
}

Rooted<GCVector<JSLinearString*>> seen(cx, cx);
outResult = breakdown.isUndefined() ? GetDefaultBreakdown(cx)
: ParseBreakdown(cx, breakdown);
: ParseBreakdown(cx, breakdown, &seen);
return !!outResult;
}

Expand Down

0 comments on commit fb30b19

Please sign in to comment.