-
Notifications
You must be signed in to change notification settings - Fork 407
fix(debugger): limit and safely stringify template log message values #9766
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| 'use strict' | ||
|
|
||
| const { inspect, types } = require('node:util') | ||
|
|
||
| /** @typedef {NonNullable<ReturnType<typeof globalThis.Object.getOwnPropertyDescriptor>>} PropertyDescriptor */ | ||
|
|
||
| const maxProperties = 5 | ||
| const segmentInspectOptions = { | ||
| depth: 0, | ||
| customInspect: false, | ||
| maxArrayLength: 3, | ||
| maxStringLength: 8 * 1024, | ||
| breakLength: Infinity, | ||
| } | ||
|
|
||
| module.exports = inspectSegment | ||
|
|
||
| /** | ||
| * Inspect a dynamic-instrumentation template value without invoking user code. | ||
| * Unlike collections, `util.inspect` has no option for limiting the number of object properties, so this function | ||
| * truncates objects before inspecting them. | ||
| * | ||
| * @param {unknown} value | ||
| * @returns {string} | ||
| */ | ||
| function inspectSegment (value) { | ||
| if (value === null || (typeof value !== 'object' && typeof value !== 'function')) { | ||
| return inspect(value, segmentInspectOptions) | ||
| } | ||
| if (types.isProxy(value)) return '[Proxy]' | ||
| if ( | ||
| Array.isArray(value) || | ||
| types.isTypedArray(value) || | ||
| types.isAnyArrayBuffer(value) || | ||
| types.isDataView(value) || | ||
| types.isMap(value) || | ||
| types.isSet(value) || | ||
| types.isWeakMap(value) || | ||
| types.isWeakSet(value) || | ||
| types.isMapIterator(value) || | ||
| types.isSetIterator(value) | ||
| ) { | ||
| return inspect(value, segmentInspectOptions) | ||
| } | ||
|
|
||
| /** @type {(string | symbol)[]} */ | ||
| const keys = Object.keys(value) | ||
| let propertyCount = keys.length | ||
| const symbols = Object.getOwnPropertySymbols(value) | ||
| for (let i = 0; i < symbols.length; i++) { | ||
| if (Object.getOwnPropertyDescriptor(value, symbols[i])?.enumerable === true) { | ||
| propertyCount++ | ||
| if (keys.length < maxProperties) keys.push(symbols[i]) | ||
| } | ||
| } | ||
|
Comment on lines
+49
to
+55
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was originally thinking the same, but ended up with this implementation to ensure the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, in new Node.js versions having objects with zero symbols is faster again. So it might not hurt that badly anymore. |
||
|
|
||
| if (propertyCount <= maxProperties) { | ||
| // TODO: Decide whether allowing util.inspect to invoke Symbol.toStringTag getters is acceptable. If it is, | ||
| // remove inspectionCanRunUserCode and the related omission paths. | ||
| if (inspectionCanRunUserCode(value)) { | ||
| return '[Value omitted: inspection may execute user code]' | ||
| } | ||
| return inspect(value, segmentInspectOptions) | ||
| } | ||
|
|
||
| const truncated = {} | ||
| for (let i = 0; i < maxProperties; i++) { | ||
| const descriptor = /** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(value, keys[i])) | ||
| if ( | ||
| (keys[i] === Symbol.toStringTag && descriptor.get !== undefined) || | ||
| (descriptor.value !== value && inspectionCanRunUserCode(descriptor.value)) | ||
| ) { | ||
| return '[Value omitted: inspection may execute user code]' | ||
| } | ||
| if (descriptor.value === value) descriptor.value = truncated | ||
| Object.defineProperty(truncated, keys[i], descriptor) | ||
|
Comment on lines
+68
to
+76
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since the object will likely be truncated and it is a different object anyway, is copying the descriptor crucial? If not, we could just check for the presence of the toStringTag with e.g.,
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need all the descriptor flags, but I don't think we can get around having to use descriptor in some capacity. It all depends how many side effects you're willing to potentially trigger. Or maybe I'm misunderstanding your proposal? |
||
| } | ||
|
|
||
| const omitted = propertyCount - maxProperties | ||
| const inspected = inspect(truncated, segmentInspectOptions) | ||
| return `${inspected.slice(0, -2)}, ... ${omitted} more ${omitted === 1 ? 'property' : 'properties'} }` | ||
| } | ||
|
|
||
| /** | ||
| * Determine whether inspecting a value could invoke a proxy trap or toStringTag getter. | ||
| * | ||
| * @param {unknown} value | ||
| * @returns {boolean} | ||
| */ | ||
| function inspectionCanRunUserCode (value) { | ||
| const type = typeof value | ||
| if (value === null || (type !== 'object' && type !== 'function')) return false | ||
| if (types.isProxy(value)) return true | ||
|
|
||
| let current = value | ||
| while (current !== null) { | ||
| if (Object.getOwnPropertyDescriptor(current, Symbol.toStringTag)?.get !== undefined) return true | ||
| current = Object.getPrototypeOf(current) | ||
| if (types.isProxy(current)) return true | ||
| } | ||
| return false | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could check for an iterable symbol. That is not perfect, since it could still be a regular object, while unlikely and it would allow to skip multiple checks.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I don't like this wall of checks either... but was hoping the V8 optimizer would find a way to make it performant.
I am working on a PR to support this directly in Node core though. If that lands, we can live with a slower polyfill.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are implemented in Node.js in C++ as fast calls. So they are inlined, while we still need to go through each.