-
Notifications
You must be signed in to change notification settings - Fork 29.7k
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
Discussion: private fields and util.inspect #27404
Comments
@jasnell I definitely think we should add support for these behind the But I guess your question was also (or mainly) in the direction of how to retrieve the information. I personally think the work by @joyeecheung looks very promising (while I agree that we could just extend the existing API as discussed in the comments of the reverenced PR). |
Yep, thank you for linking the V8 issue @devsnek .. I knew that conversation was happening but I had lost track of where it was. I opened this issue largely just so we could track the discussion and see if there were any actual objections to including those fields. |
FWIW, I'm in favor of including private fields in |
From the feedback we've got from https://chromium-review.googlesource.com/c/v8/v8/+/1546559 I think we should try using the inspector to implement the inspection of private fields for a start. It could be built on top of #27110 for more readable interactions with the inspector. |
As a library author I would prefer if these were not exposed through But with this in place, people could read my private data easily at runtime: class X {
#secret = generateSecretNumber();
}
const x = new X();
const secret = /#secret: '([^']+)'/.exec(util.inspect(x))[1];
console.log("no longer a secret: ", secret); Introducing this into Node.js would force me to avoid using private fields, and use WeakMaps instead, since those cannot be discovered by util.inspect() in this way. In other words, this feature effectively makes private fields no longer useful, as they're just symbols that require different awkward contortions to access (grepping through util.inspect output, versus using indices into getOwnPropertySymbols). I would prefer that private fields only be visible to users of the inspector, not to unprivileged Node.js code. |
the inspector itself is also available to unprivileged node code, and returns the private fields as a nice object you don't need to use regex on. |
My understanding is that you need to be started with a flag to include the inspector, per https://nodejs.org/en/docs/guides/debugging-getting-started/. That would be "privileged" code then, in my (admittedly imprecise) terminology. |
You can start the inspector at runtime via |
Oh :(. Then I guess Node.js is just not a feasible runtime for writing encapsulated code. |
@domenic I don't think we are going to implement what the snippet shows exactly - this is supposed to be only available when FWIW you can break encapsulation of any runtime that embeds v8 with post-mortem support by using the JS API of llnode - take a core dump via gcore (or something else that capture a core dump) and then iterate over the heap to find your object, then you can even break closure encapsulations by inspecting context that references your objects. It's just too slow to do this hack though. |
I am just used to the browser where we have actual encapsulation from other code. It is sad and disappointing to find out that Node does not have the same level of encapsulation. But given that it doesn't, I guess I don't have much more to add to this conversation. |
@domenic Node.js has a history of treating all user code (third party or not) as trusted and equal, I think that's what makes its privacy model different from the browser. Would it help if we make inspector part of the experimental policy? Although whether it can be made opt-in or opt-out depends on how much breakage the ecosystem can take since It would be quite hacky to use inspector to implement this (either in core or in the user land) though, as you need to inspect your own scope chain which means you need to tell the debugger to pause at your code and do a lot of stuff that depends on the exact shape of the functions in the call stack. What's your primary concern about thing encapsulation being broken? That the users get to see the fields or that they get to use them programmatically? It would be quite difficult to use these programmatically if the private fields are not primitives, as the inspector would only give you a preview of them, and doing all these hacky things would bring a lot of overhead as well. |
Please do not show private fields anywhere, just like you wouldn't show closed-over variables when you log a function. This is the domain of |
@domenic and @ljharb ... this is precisely the kind of feedback I was hoping to solicit when opening the issue so thank you for stepping in. Within core, we're always considered Perhaps a compromise here would be to show the private field names but not the actual values, specifically to make it clear that, yes, there is private state at play but you'll have to grab the inspector to get to the values. |
BTW, I just noticed that
But I guess without |
@jasnell i think it shouldn’t even show that. Private class fields are conceptually nothing more than closed-over variables for instances - if you wouldn’t show the names of closed-over variables used in a function when inspecting a function, you shouldn’t show the names of private fields when inspecting an instance. |
Put another way, it was a very explicit and critical design goal that it be impossible, modulo function toString, to expose even the existence of private fields - if node exposes it, it will be destroying a vital part of the design of the feature. |
Can definitely respect that and see the connection to closed over variables. Is there an approach here that Would make sense outside of stepping through with a debugger? |
No, that’s the only method i think should be possible - just like closed-over variables. |
i also don’t think it should do that in chrome, and I’ll file a bug about it when i get the chance. |
I believe on the Chrome's side it is because the private field support in the DevTools are added through Note that |
One problem with deciding whether or not to show private fields is that they can kinda serve two purposes. One is as a privacy mechanism, but the other is as a brand mechanism similar to internal slots. When being used as a brand mechanism they are arguably part of your API and would be worth showing to consumers of your object. I'm not sure how would be the best way to surface these but I think opt-in would probably make sense. Maybe something like this with decorators: class Stream {
// Is provided by the user and probably needs to be inspected
// by the user
@util.expose('value')
#queue;
constructor(queue) {
this.#queue = queue
}
} |
The way that brand is exposed on builtins is with hardcoded type-specific checks; I think that if a type wants to expose that in a generic way, it should use the If it doesn't want to expose it, it should be impossible to do so. |
As a note, since there is a const inspector = require('inspector');
const session = new inspector.Session();
session.connect();
session.post('Runtime.evaluate', { expression: `class Foo { #bar = 'bar' }; new Foo` },
(error, { result }) => {
session.post('Runtime.getProperties', { objectId: result.objectId },
(error, { privateProperties }) => {
console.log('private properties', privateProperties);
});
}); |
Can that also expose function variables? If not, then it shouldn’t be able to expose private fields. |
I think it's possible since we could inspect scoped variables while paused on breakpoints on chrome devtools, but I haven't dig into it. |
@ljharb The variables can be inspected by looking up the scope chain (unless they are not referenced in the code and therefore not allocated - in that case they can be considered "optimized out"), and the scope chain of the call site can be inspected by setting |
Actually, we can't just remove the inspector module. It's a supported feature that would need to go through a full semver-major deprecation cycle which would take quite some time. There are systems actively using it in production for other purposes. |
I definitely don't think the inspector module should be removed. I think that modifying/wrapping |
Since this terrible thing is possible, I went ahead and made a terrible thing: https://www.npmjs.com/package/private-fields I would gladly invite node to break it by removing |
Is there any willingness to accept such a PR for node 18? |
@legendecas that work seems like the opposite of my intent; private fields shouldn’t be observable whatsoever by the runtime. |
@ljharb sorry, what do you mean by "observable"? I was thinking that PR is similar to your work https://www.npmjs.com/package/private-fields. It doesn't get hands-on the private fields directly, only get a cloned shape of the values. Please correct me if I misunderstood. |
I want to make a PR that breaks that package :-) the entire point of the private fields feature is that it is impossible for the js runtime to observe them. It shouldn’t be possible to know that something has a private field or not. |
I understand the point, but in Node.js, the most common way to debug values is to inspect them. If it becomes impossible, developers could not just |
They shouldn’t be able to do that from outside the class, is the point. It’s not required for debugging. |
To be clear, i don’t mind console.log doing it; the facility i want to remove is getting structured data about it. |
I think it's impossible to have both. If the Chrome developer tools are able to use a websocket connection to retrieve the structured data, so is a Node.js program. |
@targos it seems entirely possible to extract the ability from the inspector module, save it in an internal module that only node core can access, and then delete it so that userland code can't get access to it. |
I think what is meant is that as long as Node exposes an inspector port whatsoever then one can just connect to it with a generic websocket API e.g.: const { w3cwebsocket: WebSocket } = require("websocket");
inspector.open();
const ws = new WebSocket(inspector.url());
ws.send(JSON.stringify({
// Do whatever devtools protocol stuff to get private fields here
});
ws.addEventListener("message", () => {
// Use the private fields here somehow
}); There's really nothing Node can do, like sure it could remove Fundamentally denying access to debugger to the Node program is impossible, as long as Node is able to run arbitrary code on the machine it can always find some way of capturing the socket between Node and devtools somewhere. If you really want to deny such power you can use something such as policy to deny access to the |
@Jamesernator i don’t understand. I’m saying nothing would have that access except the internal console.log, and maybe util.inspect - so there’d be no inspector object that provided private fields info, no matter how you connected to it. |
Or are you saying that node has no ability to alter the inspector object served by the protocol? |
If console.log can display the fields, then anything with access to stdout can read them... This doesn't make a lot of sense to me. |
Even if it does, I strongly don't think it should, it is a debugging protocol, people use Chrome Devtools and VSCode devtools with it already. If people are using it for things other than debugging that is their problem. And again, you cannot deny access to private fields entirely, regardless of what you try, because as long as Node has general access to child processes, you cannot realistically prevent it from capturing its own memory (both internally with |
@domenic that’s not structured data, though. I think there’s a distinct difference between directly providing the inspector API, and forcing someone to parse console output. |
Feels like some seriously shifting goalposts to me... From "must not be exposed" to "only console.log can expose them" to "can be exposed as long as doing so requires parsing some strings". |
Being pragmatic isn't shifting goalposts. They must not be exposed at all, but there's still multiple "tiers" of exposing them, and the less they are exposed, the better. |
I wonder if it would make sense to make the private fields visible in console.log() behind a flag ( EDIT: thinking about the idea in the parentheses again, it probably would be somewhat complicated if the private fields need to remain debuggable through DevTools by default at the same time... |
Figured out how to instead implement the NBTData class's read-only nature. This still allows for custom object changes if you know what you are trying to do, unlike when the read-only properties were established using `Object.freeze()`. Discovered a lot about how getters, setters, readability, and things like that work with the prototype chain. There was a lot to learn, and I got a lot of helpful links to try figuring this out a bit more. Essentially, I want to make `NBTData` look and feel just like the properties on classes like `File`, `Blob`, and `ImageData` do. It's not quite built in the same way yet I think, but this new implementation's behavior is the most similar to it so far. I think the way to make it work is to define enumerable getters for the properties on the `NBTData` prototype, which can be visible as properties from the regular classes themselves, which inherit the prototype. The closest I got code-wise to making this work, unfortunately isn't represented the same in the browser DevTools, because it shows the getter `(...)` instead of the currently calculated value. I just found one more SO post that seems to be covering this issue; I didn't find any solod solutions there as to what can make a getter not 'lazy'. It sonded like enabling Strict Mode for it might help, but I couldn't find any other information about that anywhere else. https://stackoverflow.com/questions/29772403/cant-enumerate-getters-setters-properties https://stackoverflow.com/questions/34517538/setting-an-es6-class-getter-to-enumerable https://github.com/nodejs/node/blob/7bc0e6a4e79684a98cb64c61452af00e20f590e7/lib/internal/file.js https://github.com/nodejs/node/blob/7bc0e6a4e79684a98cb64c61452af00e20f590e7/lib/internal/blob.js https://stackoverflow.com/questions/54783604/what-is-happening-in-the-console-when-i-look-at-getter-properties-on-dom-objects https://dev.to/mustapha/a-deep-dive-into-javascript-object-properties-598h https://developer.chrome.com/blog/new-in-devtools-101/#private-props nodejs/node#27404 https://stackoverflow.com/questions/49710825/implement-smart-self-overwriting-lazy-getters-in-strict-mode
Figured out how to instead implement the NBTData class's read-only nature. This still allows for custom object changes if you know what you are trying to do, unlike when the read-only properties were established using `Object.freeze()`. Discovered a lot about how getters, setters, readability, and things like that work with the prototype chain. There was a lot to learn, and I got a lot of helpful links to try figuring this out a bit more. Essentially, I want to make `NBTData` look and feel just like the properties on classes like `File`, `Blob`, and `ImageData` do. It's not quite built in the same way yet I think, but this new implementation's behavior is the most similar to it so far. I think the way to make it work is to define enumerable getters for the properties on the `NBTData` prototype, which can be visible as properties from the regular classes themselves, which inherit the prototype. The closest I got code-wise to making this work, unfortunately isn't represented the same in the browser DevTools, because it shows the getter `(...)` instead of the currently calculated value. I just found one more SO post that seems to be covering this issue; I didn't find any solod solutions there as to what can make a getter not 'lazy'. It sonded like enabling Strict Mode for it might help, but I couldn't find any other information about that anywhere else. https://stackoverflow.com/questions/29772403/cant-enumerate-getters-setters-properties https://stackoverflow.com/questions/34517538/setting-an-es6-class-getter-to-enumerable https://github.com/nodejs/node/blob/7bc0e6a4e79684a98cb64c61452af00e20f590e7/lib/internal/file.js https://github.com/nodejs/node/blob/7bc0e6a4e79684a98cb64c61452af00e20f590e7/lib/internal/blob.js https://stackoverflow.com/questions/54783604/what-is-happening-in-the-console-when-i-look-at-getter-properties-on-dom-objects https://dev.to/mustapha/a-deep-dive-into-javascript-object-properties-598h https://developer.chrome.com/blog/new-in-devtools-101/#private-props nodejs/node#27404 https://stackoverflow.com/questions/49710825/implement-smart-self-overwriting-lazy-getters-in-strict-mode
If they are exposed in any way, I'd prefer they're behind a distinct option. It would be a pain if I want a full dump of just the public surface of an object but can't get it, because all my private fields are included. util.inspect(instance, { showHidden: true })
util.inspect(instance, { showHidden: true, showPrivate: true }) |
Now that we have support for private class fields... we need to figure out how (and if) we want to support them with regards to
util.inspect()
...Right now, those properties are hidden from the output even when
showHidden
istrue
...How should we handle these with inspect?
The text was updated successfully, but these errors were encountered: