Repository navigation
buffer: discuss future direction of Buffer constructor API #9531
Description
Activity
- addedbufferIssues and PRs related to the buffer subsystem.Issues and PRs related to the buffer subsystem.discussIssues opened for discussion and feedback.Issues opened for discussion and feedback.
on Nov 9, 2016 More opinionated stuff from myself:
I do not think we should ever turnBufferinto an ES6 class; we can create a separate API for that and makeBuffera wrapper around that, as it has been discussed before, and nothing aboutBuffer()ever needs to be truly broken. I know my comment linked above mentions a possible breakage as a motivation for deprecatingBuffer()earlier thannew Buffer(), but I definitely don’t agree with my past self on this anymore. I do however see that there is significant security risk involved with their usage, and that that may warrant a full runtime deprecation at some point in the future.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.
Reacted by Rich Trott@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.
Reacted by Thomas Watson, Feross Aboukhadijeh, toddself and ralphtheninjaReacted by Nikolai Vavilov, Dannii Willis and Søren Nguyen Boisen@mafintosh ... automatically zero-filling only addresses part of the issue. There are other aspects of the existing
Buffer()use that are problematic -- but that can be patched over in much the same way. Unfortunately, doing so makes it less obvious that users on older versions of Node.js are doing the wrong thing -- and transparently fixing the issue in new versions of Node.js could lead to developers being completely unaware that their older version of Node.js may have an issue. That's not to say that zero-filling by default and limiting the maximize size to avoid DOS is not a good approach, it's just that there are still ecosystem and usability issues that go along with it.@mafintosh For more context on what @jasnell said, see #4660 (comment) (from before
Buffer.allocexisted). The concern is that this would actually create security issues, because people would stop usingnew Buffer(num).fill(0)in modules, and then anyone using an older version of Node without the zerofill behavior would be vulnerable.This might be more doable now that
Buffer.allocexists. If we decide to go that route, I think we should at least keepBuffer()andnew Buffer()soft-deprecated to avoid this issue and to encourage everyone to useBuffer.allocinstead.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 do not think we should ever turn Buffer into an ES6 class; we can create a separate API for that and make Buffer a wrapper around that
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.
Accidentally accepting large numeric values can very quickly increase resource usage
v6.x and v7.x solve that by throwing an exception when calling
Buffer(1234, 'encoding')(but not forBuffer('1234', 'encoding')), we should discuss back-porting that to v4.x.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?
Reacted by Nikolai Vavilov, Nikita Skovoroda, Ingvar Stepanyan and Dannii Willisv6.x and v7.x solve that by throwing an exception when calling
Buffer(1234, 'encoding')(but not forBuffer('1234', 'encoding'))They don’t when there’s only a single argument,
Buffer(1234)vsBuffer('1234').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.
Reacted by Anna Henningsen, Rich Trott, Dannii Willis and Feross AboukhadijehThey don’t when there’s only a single argument, Buffer(1234) vs Buffer('1234').
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.
I certainly wouldn't have thought one little console log message would break builds. But apparently it does!
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.
Place a warning banner on every module that uses the old unsafe API.
Static analysis doesn't always work well in JavaScript.
Reacted by Feross AboukhadijehAll other cases of breakage I've encountered were in tests which look at stderr output.
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
breakage.126 remaining items
Can someone clarify if all the "runtime deprecation" options I was shown on the "Node.js Buffer options" spreadsheet mean completely deprecation of the
Bufferconstructor?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
Buffer(arg)explicitly. #7152 and #11808 have a work-around for that, and everything works as far as I am informed. That was done by @seishun, I believe. Could you provide a testcase that would be broken by any of those PRs? It's better to move the dicussion to the PRs, though.Upgrading this one from
ctc-reviewtoctc-agenda. We need to make a decision about what is and isn't going to happen in version 8.0.0.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:
-
While not unanimous, there seems to be a consensus that we should do something--that ignoring the issue is not a wise option.
-
Deprecating in version 8.0.0 has considerable opposition and scant support.
-
Zero-filling has more support than opposition at this time. That said, there are almost as many people who are neutral about it as there are people that are for it. So a lot will depend on how those folks end up voting.
-
Scheduling a deprecation has significant support and slightly less opposition. Again, a lot of neutral folks on that one, so how that goes will depend how the undecideds end up voting.
-
Opt-in deprecate is expected to easily land in version 8.0.0. It has two people on the record as being opposed. One of them is opposed to anything other than updating the docs. If I recall correctly, the other had trouble remembering why they indicated opposition and it may have been an error.
-
For some folks, support or opposition to zero-fill depends on whether there is a commitment to run-time deprecating (for example, announcing that run-time deprecation will happen in version N.0.0) and whether it will be backported to LTS lines. Therefore, it may make sense to try to come to a decision on whether or not to schedule a deprecation before trying to decide whether or not to zero-fill.
-
Random-fill seems to have less support than zero-fill but is still a viable contender. Most of the things said about zero-fill
-
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:
- Node.js 8.0.0 will contain a flag allowing people to opt-in for a runtime deprecation message for Buffer constructor usage. (Will people actually use it? We'll find out.)
- Node.js 8.0.0 will zero-fill buffers created with the Buffer constructor. This behavior, at least for the time being, will not be backported to earlier versions.
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
standard10.0.0 which treats usage of deprecated Node.js APIs as a lint error. So we now have thousands of users (once they update) who will see warnings aboutBuffer()being deprecated in their tests and in their CI pipelines.From the 10.0.0 changelog entry:
- Disallow using deprecated Node.js APIs
- Ensures that code always runs without warnings on the latest versions of Node.js
- Ensures that safe Buffer methods (
Buffer.from(),Buffer.alloc()) are used instead ofBuffer()
It's hard to know exactly how many people use
standard, but our shareable eslint config is downloaded 670K times per month, so I hope this change will have some noticeable effect in the usage numbers. We'll see.I think it would be great if we could lean on community tooling, like
standardand others, to help make these kinds of deprecations less painful in the future. @ChALkeR, it would be great if you could keep an eye on usage ofBuffer()to see how much it changes over the next 1-3 months.Reacted by Linus Unnebäck- Disallow using deprecated Node.js APIs
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
Buffer()has changed in the last 3 months?@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
DeprecationWarningfor callingBufferwithoutnewthat 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
Bufferconstructor 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, theBufferwill contain 100 bytes of unitialized memory. This is a security problem for two reasons:Bufferconstructor where a string is expected but a number is actually passed, uninitialized memory will be returned:Passing the value
100here 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 displaysDeprecationWarnings 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 theBufferconstructor.The reason for this distinction was trying to keep the possibility of making
Buffera proper ES6 class at some point in the far, far future open, which would mean that callingnew Buffer()may always work. (Effects of turningBufferinto 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 currentBufferimplementation does, only with these differences.)As a result of that deprecation for
Buffer()withoutnewin 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
Bufferconstructor, 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
Bufferconstructor 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.