-
Notifications
You must be signed in to change notification settings - Fork 30k
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
buffer: discuss future direction of Buffer constructor API #9531
Comments
More opinionated stuff from myself: Correspondingly, I would ask of others (although I am aware that I obviously can’t speak for everyone) to try and avoid giving “Subclassability” and “ES6 class” as reasons for the deprecations here; I think it is obvious that our messaging broke here, as reflected by the comments on #7152 and #8169. IMHO, this is primarily about security, and all other benefits of future changes pale in front of that. |
@addaleax Thank you for the detailed write up. Much appreciated. I'll suggest a way forward that have been mentioned by other people already. Instead of deprecating anything start zero-filling buffers returned by the Buffer constructor, similar to Buffer.alloc. Back-port this change to old versions of node (I'd like to see this added all the way back to 0.10 but I know that isn't officially supported anymore). A benefit of this approach is that old code will work just like before - no changes needed. It also introduces an incentive for module authors to upgrade to the new API as the zero filling has a perceived performance penalty. |
@mafintosh ... automatically zero-filling only addresses part of the issue. There are other aspects of the existing |
@mafintosh For more context on what @jasnell said, see #4660 (comment) (from before This might be more doable now that |
Just to clarify: I'm all for the soft deprecating of the constructor. I was referring to doing the zero-fill instead of hard-deprecating it. |
I'm going to disagree with that. The additive approach of "just add a new API" leads to a sprawling and uncohesive design. Newcomers to node already complain there is so much to take in, let's not make it worse.
v6.x and v7.x solve that by throwing an exception when calling I'm in favor, it improves security and I can only see it breaking code that is already prone to getting broken by attackers. Better it's us than some black hat, am I right? |
They don’t when there’s only a single argument, |
I'm on record for supporting deprecation of Node.js APIs that don't make sense (https://www.youtube.com/watch?v=jJaIwea8r2A, https://gist.github.com/sam-github/4c5c019b92cf95fb6571), and I even support the deprecation of these Buffer APIs (slowly, properly communicated), but I think the pain of these kind of changes is not well appreciated. For one, console messages have huge negative impacts downstream, because people who can do nothing about them see them, see #9483 But for another, trivial changes become less and less trivial as they work their way up module dependencies. For example, say a version of glob is deprecated because there is a security problem. A new major comes out. The only change is in an obscure corner case I don't care about, and most don't. But, its a functional change, it must be a major. That's OK. To update to the new major for packages that directly depend on glob is trivial, just bump the major in your package, the API didn't actually chnage, so no code changes. So far, so good. So packages that depend on glob do this, takes a couple minutes, republish, no problem. But authors do this on only the head of their packages. Going back into history and doing a patch release of every major version that has ever used glob? That's a lot of work, so only the latest. And maybe they bump their package (EDIT: major) version because, hey, who knows, maybe some downstream dep depended on that particular glob corner case. And the changes slowly work their way up, 3 levels up through other packages that have had major updates, used by tap.... and now the latest tap depends on the latest glob, which is A-OK. But I have packages with unit tests I haven't touched in years... and now I need to bump to the latest tap to get ride of the messages about glob... and while glob's change was tiny and insignificant, tap has changed a lot, and all my tests are now failing, and I'm spending hours and hours fixing them, even though the original change in glob required no js code changes for the update! This has been happening to me lately, with the graceful-fs and glob updates. I support those updates, and I'm doing the work, and I don't mind. But its worth remembering that both those updates were at their root trivial, graceful-fs and glob had new versions that were drop-in replacements for the previous majors. So, upgrading should have been trivial, and it was for direct dependencies, but as the changes ripple up the dependency trees, the changes become less and less trivial, because they start to get bundled with changes that are not so trivial. This is the kind of thing that will happen with the Buffer changes. Immediate code that uses Buffer will change trivially, but further up the tree, its going to hurt a lot more. |
Yes, and it's regrettable, but that can't be changed anytime soon. Back-porting the changes for the two argument case would at least mitigate @ChALkeR's hoek example. |
I've been thinking a lot about this, and actually written a bit but haven't shared my thoughts yet because they're incomplete. But I wanted to share one of my ideas: perhaps the fundamental problem had nothing to do with Buffers or APIs at all, but how we deprecate things. There are two values that we all share but right now they are in conflict: Security and Stability. Core wants to motivate module authors to fix two highly dangerous flaws that might exist in their code: leaking secrets via uninitialized buffers, and DDOS attacks via buffers constructed with unboundchecked ints. Having created the flawed API, it seems reasonable they assumed responsibility for trying to fix the mistake. But we can't always solve the problems we create on our own, and this is a case where core had dug itself in a hole, and would need module authors to help lift them out. Because the Buffer API was a one-way (lossy) function: you could easily convert Buffer.from, Buffer.allocate, and Buffer.allocUnsafe to the unsafe "Buffer()" form, but given a Buffer() call it is impossible to automatically choose a safe behavior based the arguments, because knowing whether it was being used safely or not requires analyzing the source code in the calling function which the Buffer() function doesn't have access to. When your only tool is a hammer, every problem looks like a nail. So they used the only tools they had: the docs website for node, and a console message in node. That's when we ran into a stability conflict. Because apparently the console output of Node is treated as part of it's API by test runners, which saw the deprecation message and freaked out. Which sort of succeeded at the original aim: get module authors attention so they can fix this flaw. However it flew in the face of another value: Stability. Module author's freaked out, thinking core was randomly changing the Buffer API, and complained loudly. And rightly so, because broken test cases, multiplied by every project that includes that somewhere as a dependency, creates chaos. (See leftpadgate) The literal cause of the problem is not deprecating Buffer - it's how it was communicated, which turned out to break modules, much to core's surprise, I think. I certainly wouldn't have thought one little console log message would break builds. But apparently it does! So what can we do? Core values stability just like module authors, I'm certain. Therefore it seems clear that deprecating things by printing to the console log is the wrong strategy, because that will always interfere with program output. I think that deprecating strategy... has to go. Should be removed from core's toolbox. Now here's my actual novelty/contribution. If the goal of deprecation is to get module authors attention so they fix their code, node core is not the right venue for that. Npm is. I think, core should reach out to npm and see if they could help publicize deprecations. Place a warning banner on every module that uses the old unsafe API. This would have zero effect on how the code runs and therefore would not threaten the stability of the module ecosystem. Yes, developers might be pissed to be publicly called out for their module using insecure APIs. So take advantage of the fact that npm has the email address for all the authors and let them know in advance. Maybe just show a small warning that gets bigger over time. The point is, deprecation is fundamentally a social process done via communication, not a technical one that can be done by modifying the node engine. Sorry if I rambled on a bit. |
So far I've only heard of one module that was actually functionally broken by Buffer-without-new deprecation. All other cases of breakage I've encountered were in tests which look at stderr output.
Static analysis doesn't always work well in JavaScript. |
A broken test is a broken test, is a broken test. It is disruptive, especially if you rely on Continuous Deployment and a breaking test halts deploying to production. But that's a hypothetical on my part. I am a little curious though about the particulars. Can someone (@mafintosh @substack etc) speak to what horrors rained down on them as a consequence of test suites breaking? |
Unexpected console output when using a node CLI program is a functional |
I (along with @mafintosh) were the original reporters of this issue. After reading through this discussion (and dealing with many issues opened by users about this issue), I think the best way forward is to zero-fill buffers created with Proposal: Zero-fill buffers created with This solves the most important issue that is at stake here: the possibility of accidental data leakage via an unintentional Since The risk that module authors will start assuming that In conclusion, zero-filling fixes the most important issue – data leakage – without major breaking changes and without hard-deprecating the Buffer API. This solution lets us support the legacy |
It seems there is consensus that security issues in old modules are a real problem, but some people think it could be better solved by zero-filling Solution A: one-time runtime warning (aka hard deprecation)
Solution B: zero-fill buffers created with
|
There will not be a solution that please everyone. I'm definitely 👎 on zero-filling Just to clarify, the major issue I see with the old API is Emitting the warning just for Can we start adding rules in the linting/security software to catch this? Maybe there is already, but it might be an easy way to get started. |
I also thought about that, but no, that won't work.
|
@feross Re: zero-fill, the only viable time to zero-fill is in the same release that introduces a runtime deprecation warning for the API. E.g. when/if The problem with zero-fill is that if we introduce it in vN.0.0 but don't introduce a runtime-deprecation for Also don't forget that zero-fill doesn't prevent DoS issues. |
@ChALkeR Making things safe and deprecating an API are two separate things. By zero-filling security becomes guaranteed which is a different concern from migrating people onto a new API. I feel while the two might be related, they are by no means tied to each other. @ChALkeR if I understand you correctly you try and make the point that if people don't migrate to a new API the npm ecosystem will be worse off than it currently is. Given that in the current situation nobody is using the new API this is not possible. Instead what we can do is make all buffer code instantly safe by zero-filling, and move the ecosystem forward in a giant leap. Then there's the point of moving people onto a new API. I feel NodeJS has a good outreach through release notes, blog posts talks and other channels. More than enough to make people aware of a changed API. Besides that there's also an incentive for developers to pick up the new API in the form of improved performance. If we do this well all developers that care about performance will have the means and reasons to move to the new API, all without needing to make maintainers sad. I hope this sounds reasonable; I'm in favor of zero-filling by default as it seems like the least problematic way forward. Fwiw I'd also be keen to see the perf implications of zero-filling, as I don't recall seeing any numbers so far and having them would be great to incentivize people to move to a new API. |
console.time('new Buffer(1024)')
for (var i = 0; i < 10000000; i++) {
new Buffer(1024)
}
console.timeEnd('new Buffer(1024)') Here it is, roughly 2x:
(node v6.9.1) IMHO making an old API slower is worse than emitting a warning, because you are "forcing" people to move, with the additional problem that the culprit is not easy to track. On the good side, tests will not cause warnings to be emitted. I am still 👎 on zero-filling, as I am 👎 on the deprecation warnings. However, if we go for a deprecation warning, zero-filling any Maybe we can warn something like: "new Buffer() and Buffer are now zero filled for security reasons, see LINK". In LINK, we show the new APIs, and how to move to them. If we can write something like The above warning is annoying, but it might cause less worry than a full deprecation. It is not a generic "the api might go away", but it's positive: we have deprecated this api to make you safe, read this link to make this warning disappear. |
Keep in mind that zero-filling is currently done in an unsophisticated way. Preallocating memory coupled with offloading zeroing to a separate thread should remove much of the overhead. |
Good to know! If there's only a negligible performance difference to the zero-filling, that seems the best option to me. |
@seishun Most of the cons in your list aren't correct.
If we add zero-filling to supported release lines, then the impact will be minimal. Users on v4, v6, v7 (and 0.12 if this is resolved in time) will be covered. As for users on 0.10 or other unsupported versions -- they're already playing with fire by running an unsupported version in product. Even so, they're unlikely to be affected by new code that uses
This is not an issue. See @bnoordhuis's comment: #9531 (comment)
The point of the new APIs ( |
No, they won't. You are assuming an immediate update, I expect the number of users that will gain problems from this (i.e. that don't update immediately to a latest version in the branch, or don't update fast enough before they install a package that relies on zero-filling) as non-zero and significant. Also, every time someone proposes zero-filling, they are ignoring the DoS issue. |
Why do you think these developers won't make the same mistakes that triggered the creation of the new Buffer API in the first place?
I wouldn't be so hasty with conclusions. Remember that people run Node.js on all kinds of hardware.
No, splitting them apart was not the end goal. The end goal was to fix security and DoS vulnerabilities, and we agreed that it should be solved by introducing new API rather than changing the old one. Zero-filling now means backing down on that decision. |
@jasnell, thinking about it again, #9531 (comment) looks good to me. If we have a concrete short path to deprecation, random-fill is acceptable. I'm still not sure should it be backported or not, though — your plan doesn't include backporting. Perhaps that's the best — that way, the impact of «users start relying on randomfill» concern is reduced to the possible minimum, and 8.0 will be safe from uninitialized memory leaks. It's still not neglectible, though — even now, module authors often just don't see case 3 from #9531 (comment) as an issue in the module. |
Sorry about being absent from this discussion for the last month or so. Can someone clarify if all the "runtime deprecation" options I was shown on the "Node.js Buffer options" spreadsheet mean completely deprecation of the I've voiced this several times in the past, but my opinion is that if any changes are to occur (outside of only zero-filling) then To reiterate,
That's a strong opinion @isaacs, but I don't see any reasoning or support for it. In the past I've extended |
Yeah, it does. |
@trevnorris, note that the table has a «to the technically possible extent» sentence, which means that it shouldn't break code that doesn't use |
Upgrading this one from |
Summing up where things are now, at least as I see them, and reading a lot into the spreadsheet @ChALkeR set up and that all CTC members were invited to fill out:
|
I think this has been resolved, at least for Node.js 8.0.0. We will want to revisit this in 6 months (if not sooner!) before the Node.js 9.0.0 release. For now, the CTC has decided that:
There were no decisions that were going to please everyone. This is where things sit for now. As mentioned above, we'll surely be re-visiting this in the not-too-distant future to assess how things are working or not working. I should also mention that there is an effort to get a rule into ESLint that will flag Buffer constructor usage. I would characterize the state of that proposal as likely to be adopted by ESLint, but not a sure thing at this time. I'm going to close this issue, but feel free to re-open or comment if you think that's not the right thing to do at this time. Thanks! |
FYI, I just released From the 10.0.0 changelog entry:
It's hard to know exactly how many people use I think it would be great if we could lean on community tooling, like |
Also on the tooling front: ESLint will be shipping with a no-buffer-constructor (that may not be the name of the rule, I'm just using that as a shorthand right now) rule in the foreseeable future. See eslint/eslint#5614 (comment) and thank @not-an-aardvark and @jasnell and everyone else who got us to this point. (I don't know if the rule will have enormous impact or modest impact, but we don't know if we don't try!) |
With the Node.js 9.0.0 release looming closer, I think it's time to revisit this. @ChALkeR could you evaluate how much the usage of |
@seishun Thanks for the reminder! I hope to do that in a few days. =) |
@ChALkeR pinging once again... |
In today’s CTC meeting we discussed reverting the
DeprecationWarning
for callingBuffer
withoutnew
that was introduced inv7
(PR up here), and it became clear that we need to come up with a long-term plan on what exactly we want to achieve, how to do that and improve our messaging about it, both before and after our actions. I’ll try to sum up what exactly we are talking about; obviously, I am somewhat biased, having been involved in plenty of the previous discussion here. (This has still gotten pretty long btw, so I hope a lot of people will find the information in here useful enough to warrant a Wall of Text.)The
Buffer
constructor has the usability flaw that it accepts input with different type signatures, sonew Buffer('abcdef')
andnew Buffer(100)
will both return valid buffers, and in the latter case, theBuffer
will contain 100 bytes of unitialized memory. This is a security problem for two reasons:Buffer
constructor where a string is expected but a number is actually passed, uninitialized memory will be returned:Passing the value
100
here will return a slice of memory that may contain garbage, but generally can contain any value previously stored in memory, including credentials, source code, and much more. @ChALkeR has a pretty good write-up of this: https://github.com/ChALkeR/notes/blob/master/Buffer-knows-everything.mdAgain, @ChALkeR has a very-good write-up on these security issues at https://github.com/ChALkeR/notes/blob/master/Lets-fix-Buffer-API.md. It predates the current
Buffer.alloc()
/Buffer.from()
situation, but it contains a helpful FAQ with answers to questions like “Why not just makeBuffer(number)
zero-fill everything by default?”.So far, in Node v6.0.0 the safer
Buffer.alloc()
/Buffer.from()
API was introduced and later backported to the v5.10.0 and v4.5.0 releases. Additionally, v6.0.0 came with a documentation-only deprecation of the oldBuffer()
API.In June, #7152 was opened, which seeks to deprecate the old
Buffer()
API using a runtime deprecation, i.e. printing a single warning per Node process whenBuffer()
ornew Buffer()
is executed for the first time. Currently, that PR is still open. A reduced version of it, #8169, was landed as a semver-major change in v7.0.0, that emits and displaysDeprecationWarning
s for uses ofBuffer()
only, but excludes uses ofnew Buffer()
.I had summarized the goals and possible actions before that decision was made in #7152 (comment) ¹; And @jasnell has written a then-current long-term plan in #7152 (comment) that would include runtime deprecations of
new Buffer()
in v8.0.0 and later actual breaking changes to theBuffer
constructor.The reason for this distinction was trying to keep the possibility of making
Buffer
a proper ES6 class at some point in the far, far future open, which would mean that callingnew Buffer()
may always work. (Effects of turningBuffer
into a class would be proper subclassability and breakingBuffer()
withoutnew
. It is, however, completely possible to add a separate class to the API that would behave like the currentBuffer
implementation does, only with these differences.)As a result of that deprecation for
Buffer()
withoutnew
in v7.0.0, significant pushback from well-known members of the community ensued, both in the threads on #7152 and #8169. On the one hand, it became clear that we failed in our messaging to make clear that the primary motivation for that change was helping our users avoid serious security issues; on the other hand, the added deprecation warning seemed to be incongruent with the expectations of stability and backwards compatibility that module authors and consumers have, as far as Node core is concerned.As a result of this, the CTC decided to consider reverting the deprecation warning, possibly temporarily, and the corresponding PR is in #9529. The decision on that has yet to be made, but the desire has been expressed to reach a decision soon to limit the number of v7.x versions with possibly incongruent behaviour.
From following the discussions, it is obvious that the path forward is a contentious issue; right now, the opinions range from never introducing a runtime deprecation for any version of the
Buffer
constructor, to applying one for all uses of it at the next semver-major release in v8.0.0.The strongest and most frequently expressed argument for fully runtime-deprecating the
Buffer
constructor soon remains that users may not be aware that parts of their application use an unsafe API and should be warned about that.On the other side, the warning itself is perceived as a very disruptive change to the ecosystem, suggesting that it is definitely worth exploring alternative ways to reduce the usage of both
Buffer()
andnew Buffer()
./cc @nodejs/collaborators
¹ It may or may not be obvious from the way I articulate my thoughts here – I try to stick to stating facts – but in hindsight, I regret writing it this way.
The text was updated successfully, but these errors were encountered: