Prevent DoS by large int-str conversions
github.com
github.com
All: submitted title was "`int('1' * 4301)` will raise ValueError starting with Python 3.10.7" and comments reference that, so you might want to take a look at both URLs.
A two-year old CVE is picked up by gpshead as suddenly urgent enough to justify a fairly significant breaking change in a patch release, despite another issue (#90716) already tracking better solutions to the same problem.
gpshead submits a PR which doesn't solve the issue (the DoS still happens, and then a ValueError comes afterwards!), so mdickinson submits a PR which works around the problem in the first PR. Nobody else is apparently involved in discussing whether this change should even be made, and whether the fixed fix genuinely eliminates the DoS.
mdickinson then has to fight for several comments to convince gpshead to make an obviously-correct and zero-cost change to the integer type, with gpshead dropping a passive-aggressive "pedantically correct" when they finally fix it.
Then when other people finally notice the issue and start to question the need for the change, especially in the context of another issue discussing a better solution, they are ignored and the discussion is shut down with a reference to the Code of Conduct.
Why would anyone want to participate in a community this toxic?
I'm not sure why people are so excited about this issue. It's not much different than sys.setrecursionlimit(). We know how to implement tail recursion. Python doesn't do it though and so there is a limit, set high enough that most people don't care. It seems a perfectly practical approach to me.
As traditional as it gets really: https://www.cvedetails.com/vulnerability-list/opdos-1/denial...
It also has a financial impact definitely something security people are interested in.
It's similar to what we've done with hash collisions - we can't count on everyone fixing that in every place released so far, so all languages added hash randomisation at startup. That one fortunately had no side effects like this one does.
> They set the limit at (on my computer) 0.1ms.
That seems reasonable to me. I'm not sure if you're saying it's too low or too high?
> the problem is that web-servers aren't validating their inputs.
And they won't. This would be repeated many times in various form affecting Web servers, job queues, parsers, etc. and keep coming back for decades. We already have that with Java XML entities. They weren't fixed at the source and we get a new implementation with that bug ever day.
I sanitize all my inputs while they are still strings, like we have been doing for thirty years in web apps.
Now explain how you can "trivially" DoS my service.
> like we have been doing for thirty years in web apps.
I have bad news for you. If everything you said is true, you've been working on some ideal code in a prefect team with no dependencies... But more likely you're going to have a bad collision with reality one day.
Also, your code, I believe? https://github.com/rec/gitz/blob/0c15c9e3d213c3556f38f6fbf63... (I know that's not a Web service, but couldn't find one. Still untrusted input. If you point me at one, I'll be happy to find another example.)
gpshead is Google:
thats all you need to know. Google makes some great stuff (Go language), but their community has plenty of arrogant members that have no concept or interest in nuanced discussions.
I wait a few months, follow up and nothing. Pretty much dropped it for 2 years and now I see that they finally fixed it and never let me know. I asked a few days ago whether I'd be credited in the CVE, still no reply...
Why do I feel as if there are serious communication issues here?
Digging through our history, a person who reported the same thing earlier than you never got a response at all. Like I said, we've identified organizational issues to be addressed.
(I honestly don't know who should be "credited" on the CVE nor do I have control over that, sorry)
Code of conduct is apart of this, it is designed to prevent strong dissenting opinion by claiming its offensive/improper. You cannot call out the true bad actors, because culturally we view offense as wrong and authority to be correct. COC is basically an HR document, it's to protect the org, not to do the right thing. History repeats itself.
There was no fighting. As soon as Mark piped up I was extremely pleased to see that he had found something that should've been obvious that we'd overlooked in the process of doing everything spread over time. Mark wasn't able to review the PR code before it was made public due to the current processes (lack of...) we're working to improve for the Python security response team.
"pedantically correct" was not intended to be read as passive aggressive. I use that term to mean exact vs almost when it comes to computations. I didn't need convincing. I wanted the reasoning to be made understandable to everyone else in the future (future selves included) who was going to read this code later. I still think there is room for better explanation of the math but that is true for large parts of Objects/longobject.c anyways.
I find your interpretation of events... amusing. :P
I find this a strange modification to the language, though probably not a particularly painful one. Has python saved you from yourself when dealing with non-linear built-in algorithms before? IIRC it is also possible to have the regex engine take an inordinate amount of time for certain matching concepts(I think stackoverflow was affected by this?), but the engine wasn't hobbled to throw in those cases, it is merely up to the user to write efficient regex that aren't subject to those problems.
Converting a string to an integer is somewhat less well known as a DOS vector, more painful to avoid as an application creator, and easier to fix in code.
So there's a cost-benefit argument that you should just do this before you rewrite your regex engine.
On the other hands, lots of buyers are not aware that it's an issue, and more frustratingly there are regex engines which are very resilient to it... but are not widely used.
Python's stdlib will fall over on any exponential backtracking pattern, but last time I tried to make postgres fall over I didn't succeed. Even though it does have lookahead, lookbehind, and backrefs, so should be sensible to the issue (aka it's not a pure DFA).
str.ccClone(4301) # ConCatenate Clones of the source string N times.
Would even an abbreviated, named, function not be more self documenting and better for human and machine reviews?
Isn't all that context there in the stack trace?
When writing the code in the first place, though, it's difficult to see problems like that because it's all hidden behind magic calls to copy constructors, move semantics, and destructor calls. Out of sight, out of mind.
cc prefix for concatenate because that word is very long and it seemed likely that strings may have a large number of different concatenation focused functions that could all share the prefix.
Clone as the type of concatenation operation to perform.
https://play.rust-lang.org/?version=stable&mode=debug&editio...
This example makes a repeat(0) but asks for just the first 12 things in it, they are, of course, all twelve zeroes. Feel free to adjust it to ask for more if you think maybe there aren't any more, or they stop being zero.
https://doc.rust-lang.org/std/primitive.str.html#method.repe...
str::repeat() behaves exactly as you describe. "No".repeat(4) is "NoNoNoNo" and "What".repeat(0) is "" and so on.
Note that str::repeat() returns a String, not a str, because if n > 1 it obviously needs to allocate somewhere to put the String. As a result it is not available in environments which don't have String, like a tiny embedded device - for them str::repeat() does not exist whereas stuff like str::starts_with() and str::split_once() are fine.
How exactly? What would you expect an expression like ('1' * 4301) to give you, and why would you think it would be different from ('caterpillar' * 4301)?
To my taste operator overloading is fine, but concatenation isn't addition, so they shouldn't be overloaded because... [gestures vaguely at a half dozen language]
(By extension, string repeat is exponentiation!)
>>> 'caterpillar' * 2
'caterpillarcaterpillar'
OK, now for something different: >>> [1, 2, 3] * 2
[1, 2, 3, 1, 2, 3]
Marvellous! How about this then: >>> True * 2
2
Wait, what? Hm. >>> False * 2
0
Whoops! Implicit type conversion takes place...
Even worse: >>> 'abc' + 'efg'
'acbefg'
>>> 'efg' + 'abc'
'efgabc'
Now I'm stumped. Isn't addition supposed to be commutative?So yeah, without contracts in place, operator overloading is BAD. You can never know what the operator does, or what its properties are by just looking at how it's used. There's simply no enforced rules and so no-one's stopping you from doing
>>> class Complex:
def __init__(self, real, imag):
self.real = real
self.imag = imag
def __add__(self, other):
return Complex(self.real - other.real, self.imag - other.imag)
def __repr__(self):
return f'Complex({self.real}+{self.imag})'
>>> x = Complex(1, 2)
>>> y = Complex(1, 2)
>>> x + y
Complex(0+0j)
Now this intentionally being malicious of course, but plenty of libraries overload operators in non-intuitive ways so that the operator's properties and behaviour isn't obvious. This is especially true if commutative operators are implemented as being non-commutative (e.g. abusing '+' for concatenation instead of using another symbol like '&' for example) or if the behaviour changes depending on the order of operands.If you didn't read it, you don't know. If you don't know, you have only yourself to blame when demons fly out your nose.
Friends don't let friends spew demons out their nose.
Here's what Rust tells programmers in core::ops (the sub-library whose safe Traits result in operator "overloading" for Rust). I put "overloading" in quotes because these operators just don't exist at all for your type if you don't implement the appropriate Trait.
> Implementations of operator traits should be unsurprising in their respective contexts, keeping in mind their usual meanings and operator precedence. For example, when implementing Mul, the operation should have some resemblance to multiplication (and share expected properties like associativity).
No. Maybe crack open an introductory textbook to abstract algebra.
Sources please! '+' is used as the standard operator for abelian groups.
Here are 4 elementary properties that + satisfies:
• (Associativity): a + (b + c) = (a + b) + c ∀a, b, c ∈ Z
• (Existence of additive identity) a + 0 = 0 + a = a ∀a ∈ Z.
• (Existence of additive inverses) a + (−a) = (−a) + a = 0 ∀a ∈ Z
• (Commutativity) a + b = b + a ∀a, b ∈ Z.
Also: Key Observation: There are naturally occuring sets (other than Z and Q) which
come equipped with a concept of + and ×, whose most basic properties are the
same as those of the usual addition and multiplication on Z or Q.
I could go on with rings, fields, and vector spaces which also rely on the concept of addition as defined in Z, but I'm really curious to learn about addition being commonly used as a non-commutative operation, especially in the presence of an accompanying multiplication operation.edit: I forgot to provide proper references, so here's another example including a reference:
When dealing with Abelian groups, it is customary to use additive notation.
That is, the group operation will be called "addition," and
we will "add" two elements rather than "multiply" them. We write g + h instead of g • h or gh, and ng replaces g^n.
[Walker, Elbert A., "Introduction to Abstract Algebra", 1987; Ch. 2, Pg. 70]String concatenation is not addition. How would you expect commutative string concatenation to work?
I guess, to boil it right down, what I'm asking is "do you understand what the word 'context' means?"
Exactly! So why use the addition operator then? It's not as if alternatives (like the '&' I mentioned) aren't available.
for _ in range(4301):
llama.append(‘1’)
(there’s probably an easier way to do that but you get the point)where python can see both sides of the operation and optimize it on the C side of things.
The issue really has nothing to do with that though, it is converting a string to an int which is the whole point of the security update.
If sizeof(int)=2, the result is undefined.
I don’t believe that. I did a quick search and didn’t find much, but:
Let d_0, d_1, etc be decimal digits, little endian (so the number is d_0 + 10d_1, etc). The goal is to compute that quantity in binary.
The naive algorithm is to convert d_0 to binary. Then compute 10 in binary, multiply by d_1, and add it. Then multiply to compute 10^2 in binary, and accumulate 10^2 · d_2, and repeat. For n digits, there are n steps, and each step involves two multiplications by small factors and an addition. The overall time is O(n^2).
But I would try it differently. To convert 2n-digit number, first convert the first n digits to binary (recursively) and convert the second n digits to binary. Then multiple the second result by 10^n and add to the first result.
Let’s simplify this analysis by making n be a power of 2, so 2n = 2^k. Then the big powers of 10 are all 10 to a power of 2, so 10 needs to be squared k times. Additionally, there is one 2^k-digit multiplication, two 2^(k-1)-digit multiplications, four 2^(k-2)-digit multiplications, etc.
With FFT multiplication, multiplication is O(digits * poly(log(digits)), as is squaring. This will all add up to k levels of recursion, each acting on a total of 2^k or so bits, taking time O(2^k) times some log factors. This comes out to O(n · poly(log(n)).
I have not implemented this or done the math rigorously :)
edit: this is also buried in the issue. There’s also:
https://github.com/python/cpython/issues/90716
I don’t get it.
...and in doing a bit of research on how Python does multiplication, I found some... rather odd opinions, considering it's one of the few languages with arbitrary precision integers: https://discuss.python.org/t/faster-large-integer-multiplica...
As you can see, the Python developers have chosen to leave the original incorrect statement about O(n²) time up in the initial post.
String to integer conversion, however, is a lot harder to do in log n time, but each iteration is usually faster. The same trick doesn't work - you can't efficiently do math on the base 10 string, so the equivalent division by 2^16 is very hard. I think it has to be done in linear time, but this expands to O(n log n) for arbitrary word width due to math ops.
However, a lot of what we do for atoi/itoa assumes you have a finite length. Same with the FFTs: the algorithms rely on finite length. Infinite-length bignum libraries have a huge cost on trivial things like this, and it's part of the cost of doing business in python.
There is a very good chance that the bignum library used here is not optimized for things like atoi and itoa - most bignum libraries are written for cryptography and math where these are not done frequently.
By this logic they should also block me from running benchmarks on too big lists, because I'm dossing myself.
> Everyone auditing all existing code for this, adding length guards, and maintaining that practice everywhere is not feasible nor is it what we deem the vast majority of our users want to do.
It's hard not to read this as "we want to use untrusted input everywhere with no consequences". Seems like we'll be kicking as many issues under the rug as we're fixing with this change, right?
Are you really going to use a JSON or XML stream parser first before feeding it to the stdlib module? And one that does not try to expand the read values to native types? As for logging, that is certainly the place where you are not only expected, but often required to use untrusted input.
The fix feels like a heuristic and a compromise. None of the [easily available] solutions are robust, solid or performant, so someone picked an arbitrary threshold that should never be hit in sane code.
The linked issue mentions that GMP remains fast even in face of absurdly big numbers. No surprise, the library is literally designed for it: MP stands for multi-precision (ie. big int and friends).
One of the nicest AND most frustrating parts of taint-mode, for myself, is that once activated it remains on until the end of execution. It's not scoped, which removes some of the headache of using such a thing. Switch on, and assume always on.
http://example.com/upload.pl?file=../../../../etc/passwd
The main thing you had to remember was the parameters (all tainted of course) had to be filtered through a regular expression before you could use them.
It reminds me of this old "feature" PHP had, no doubt everyone who was on the Internet at the time of its popularity saw its unintended effects:
https://www.php.net/manual/en/info.configuration.php#ini.mag...
However, you shouldn't be passing million-digit numbers around as (decimal) text. Even if you're not at risk of DOS attacks, there's still the issue that it's very, very slow:
$ python3 -m timeit -s "s='1'*1000000" "i=int(s)"
1 loop, best of 5: 5.77 sec per loop
A ValueError alerting you to that fact could be considered a service.Contrast and compare:
$ python3 -m timeit -s "s='1'*1000000" "i=int(s,16)"
200 loops, best of 5: 1.45 msec per loopThis is about numbers that are thousands of digits, not millions. Regardless, why not? What's the alternative that supports easy exchange? If you stick it in some hexified representation, you still have to parse text, and put it into some non-machine-native number container. It's going to be slow no matter what.
Hex number parsing is linear time.
Mostly.
One time I implemented RSA in Python. I needed it to read some legacy data with the wrong padding, something which the libraries couldn't (wouldn't) do. It performed just fine. The ternary pow() that does the heavy lifting is not quite as fast as the dedicated crypto libraries, but it was close enough.
This is the case of most Python code & is why many attempts to compile Python to native code results in slower overall performance.
One of the great things about the Python community is that we don't have these kinds of artificial hangups. You just solve the problem, and if part of the solution involves something that someone wrote in a different programming language, that's fine.
Earlier this year, I implemented ECIES in Python. Not a very complicated algorithm, but it still needed to be done. So there is C involved in the underlying EC and AES implementations, big deal, I don't see how that should disqualify my ECIES work from being "doing cryptography".
-X int_max_str_digits=number
limit the size of int<->str conversions.
This helps avoid denial of service attacks when parsing untrusted data.
The default is sys.int_info.default_max_str_digits. 0 disables.
this should not be a runtime configuration setting, fix the sodding algorithm to not be quadraticwill we be getting PHP style magic quotes soon? that also protects developers against untrusted input (bonus! this could be configured too!)
or an inability to pass strings into the regular expression module? that can also cause DoS
(what happened to Python?)
Update: I may have understood incorrectly, see https://github.com/python/cpython/issues/90716
> If you know of one, the Python core development team would love to hear about it!
it's mentioned on the issue page that makes up the article...
(before they closed it due to the "code of conduct")
> Closing. This is fixed. We'll open new issues for any follow up work necessary.
The issue was marked closed, because the associated work was completed and the PR was merged. The same comment happened to mention the code of conduct, but the code of conduct wasn't why the issue was closed--it was just because the work was done.
I think the comment mentioned the CoC because the previous comment, "This is appalling" was a bit rude.
The previous comment was indeed a bit rude. I personally wouldn't think it was rude enough to invoke a code of conduct.
Even just referring to a code of conduct has, IMO, a rather strong vibe of policing and perhaps even an implication of wrongdoing, more so than merely a suggestion to keep it calm.
I don't know the culture or context of Python development (either the language or CPython), but I'm inclined to agree with gdf that it's a bit weird to start reminding people of a CoC because of a slightly rude sentence or two, especially since the rest of the comment was reasonable technical argumentation even if unapologetic.
Even if closing the issue were entirely because of other reasons and benign (someone did still reference the issue in a commit later, though), it's all too easy to see the issue-closing comment as shutting out dissenting opinions, either because of a somewhat unpleasantly expressed argument or simply because "this is fixed, no further discussion needed".
The "this is appalling" comment may have been a bit rude but the closing one wasn't exactly a triumph in communication either.
I'd say the opposite. A suggestion to "keep it calm" is inappropriate, because it carries the implication that someone is not calm. This is inappropriate because it is a comment on a person's emotional state rather than on what they say or how they say it.
In fact, if someone on my team said to "keep it calm", I'd take that person aside and explain, in private, the reasons why not to say that.
> Even if closing the issue were entirely because of other reasons and benign (someone did still reference the issue in a commit later, though), it's all too easy to see the issue-closing comment as shutting out dissenting opinions, [...]
If somebody thought that closing the issue shut out dissenting opinions, then that person has forgotten how GitHub issues work or how bug trackers work in general. Closing an issue just means that someone thinks that the work on it is done; it does not stop discussion on the issue. I can see why someone might forget and not realize that the issue was closed and not the discussion, but I don't think that it's a problem that someone visiting the bug from HN would forget how GitHub issues work for a minute.
With any online community above a certain size, there's a certain amount of policing not just of what is said, but where people have discussions. Anyone who regularly uses a forum, Subreddit, Discord server, IRC, Slack, etc. will see this pattern of behavior everywhere. For example--the discussion about whether this is the right way to fix a bug is a discussion which should be held elsewhere, where people can see the context and interested parties can respond to it.
Which is why there is a comment at the bottom,
> Please redirect further discussion to discuss.python.org.
It's crystal clear to me that this is not about shutting out dissenting voices, but just saying that this GitHub issue is the wrong place for this discussion.
You can see that there is a related issue which was closed, but there was a lot of discussion afterwards--but because the discussion was on-topic, the issue was not locked.
Perhaps a suggestion to "keep it calm" wouldn't be the best. English isn't my first language and my verbal expression isn't always the greatest. But referring to a code of conduct does also carry the implication that someone isn't minding that code, and I don't see how that would necessarily be better.
In my view, suggesting that someone isn't calm is less of a reprimand than suggesting they might be in breach of a code of conduct which, among other things, includes rules against outright harassment and other clearly reprehensible behaviour. It's normal to not be calm at times; it's another thing if someone needs to be reminded of the rules of a community. Perhaps it's a cultural thing but to me the latter is stronger judgement.
There may well be reasons for not saying to keep it calm (it sometimes simply doesn't work), but I can equally well see how people might see a reference to a CoC as strong-armed.
> If somebody thought that closing the issue shut out dissenting opinions, then that person has forgotten how GitHub issues work or how bug trackers work in general. Closing an issue just means that someone thinks that the work on it is done; it does not stop discussion on the issue.
That's fair enough. Perhaps the intention is clear enough within the community that it would indeed be deemed as simply closing that rather specific GitHub issue without implying that the matter is closed.
Human communication isn't always quite that simple, though. People get impressions from the way things are expressed. "This is fixed." makes it feel that there is nothing to be discussed about that particular change and that it is final.
I don't know the particular community well enough to know how it would be interpreted, though.
> Which is why there is a comment at the bottom,
>> Please redirect further discussion to discuss.python.org.
That's after the comment that closed the issue. Had it been in the issue-closing comment, that would have left a different taste to the closing.
Yes, a suggestion to "keep it calm" is definitely bad.
It would be nice if there were an easy way for people who are not good at English to respond to comments without having to figure out the right way to respond. You could create a "standardized response", and have a committee of people with different backgrounds review the content of that response to ensure that it clear and conveys the right messages.
That "standardized response" is the code of conduct.
A code of conduct is a beautiful thing. You do not need to be a skilled English speaker to send someone a link to the code of conduct. The
> [...] but I can equally well see how people might see a reference to a CoC as strong-armed.
It sounds like these people may be prejudiced against the code of conduct, or prejudiced against codes of conduct in general. I think if you have such people in your community, that the right thing to do is to expose them to the code of conduct so they get used to it and realize that the code of conduct is not such a bad thing or a scary thing.
If you try and shield people in the community from the code of conduct out of fear that they might react poorly to it, then I think you're doing a disservice to the community.
Have you personally read the Python code of conduct? Or are you just imagining what is written in the code of conduct?
> Human communication isn't always quite that simple, though. People get impressions from the way things are expressed.
Yes... which is why for important messages, we have teams of people that review the messages over and over again to make sure that those messages are expressed properly and they are easy to understand. Messages like the code of conduct.
You cannot expect the same level of clarity from a two-line comment in a GitHub issue. This is why referring to the code of conduct is such a good idea--it is much clearer and easier to understand, because it has been reviewed so thoroughly.
this particular wording could be used to ban any criticism of contributions (regardless of the criticism's correctness):
> Being respectful. We're respectful of others, their positions, their skills, their commitments, and their efforts.
in this sort of environment I guess it's far from surprising that the technical decisions are suffering (to put it politely)
Also as noted if your whole process crashes because of errant input to int() you are beyond fucked in other ways.
> Chosen such that this isn't wildly slow on modern hardware and so that everyone's existing deployed numpy test suite passes before https://github.com/numpy/numpy/issues/22098 is widely available.
https://github.com/python/cpython/blob/511ca9452033ef95bc7d7...
Edit: Looks like they added a python argument to increase the limit. So if you really need this, I suppose you can search around until you figure out why it's not working and pass the correct argument to the python bin.
Having been bitten by the Smalltalk behavior, I am skeptical that the Python 2 change was a good idea.
The problem with transparently overflowing to Python `long` is that, most of the time, the overflow is unintended, and the resulting performance collapse is a bug that's harder to track down than a ValueError.
> It takes about 50ms to parse an int string with 100,000 digits and about 5sec for 1,000,000 digits. The float type, decimal type, int.from_bytes(), and int() for binary bases 2, 4, 8, 16, and 32 are not affected.
Sure seems strange to set the limit to 4300. 50ms is not a DoS.
that's only 20req/sec to fill a core of execution
Turning on that behavior for all python code in a patch release with only a global override is sloppy.
It seems short sighted to not provide some function that mimics legacy functionality exactly. Even if it is something like int.parse_string_unlimited(). Especially since a random library can just set the cap to 0 and side-step the problem entirely.
Until another random library sets it to its preferred value (see https://news.ycombinator.com/item?id=32738206 for a similar issue with a CPU flag for supporting IEEE subnormals)
We might end up with libraries that keep setting that global to the value they need on every call into them.
try:
value = int(value_to_parse)
except ValueError:
import sys
__old_int_max_str_digits = sys.get_int_max_str_digits()
sys.set_int_max_str_digits(0)
value = int(value_to_parse)
sys.set_int_max_str_digits(__old_int_max_str_digits)
Or maybe just this: class UnboundedIntParsing:
def __enter__(self):
self.__old_int_max_str_digits = sys.get_int_max_str_digits()
return self
def __exit__(self, *args):
sys.set_int_max_str_digits(self.__old_int_max_str_digits)
with UnboundedIntParsing as uip:
value = int(str_value)This is pretty interestign in itself. Are there other sw compoenents that have flagged & fixed this vulnerability? Seems like there should be many.
The CVE hasn't been published, but for example there's an explanation at
https://github.com/python/cpython/issues/95778
https://github.com/python/cpython/issues/90716
Is there something bad going on with Python's internal representation of big integers, too? I thought I might have understood Tim Peters to be saying that in the latter thread.
It does look like gmpy2.mpz() is like 100 times faster than int() or something. Is this just because it's doing it all in assembly rather than in Python bytecodes, or are the Python data structures here also not so hot?
It's not the data structures. The data structures are really more or less the same: you have some array of words, with a length and a sign. The only real differences are in the particular length of word that you choose, which is not a very interesting difference.
Assembly language optimizations do tend to matter here, because you're working with the carry bit for lots of these operations, and each architecture also has some different way of multiplying numbers. Multiplying numbers is "funny" because it produces two words of output for one word of input.
There are also sometimes some different algorithms in use, and GMP uses some different algorithms depending on the size. Here's a page describing the algorithms used by GMP:
https://gmplib.org/manual/Multiplication-Algorithms
Here's a description of how carries are propagated:
https://gmplib.org/manual/Assembly-Carry-Propagation
IMO, I wouldn't expect my language's built-in bigint type to use the best, most cutting-edge algorithms and lots of hand-tuned assembly. GMP is a specialized library for doing special things.
‘1234’ => 1x1000 + 2x100 + 3x10 + 4x1
Is faster and has room to improve
There are d additions, so the addition is linear time.
Each multiplication is potentially quadratic, but it seems optimizable since it’s never multiplication of two large numbers—always one large and one small number.
In a power-of-2 base, the result of the multiplication is a constant number of digits (because the multiplication is just a shift of a single digit), so the additions could each be constant time in that case.
Another issue you have not accounted for is how you convert to base two. If you want the final product to be in base two, your additions have to be in base two, so the result of your multiplications have to be in base two, which means you will have to convert 1000, 100, 10, 1 into base two as well. Again this takes \Theta(n^2) time if you cache results (1000_2 = 100_2 *_2 10_2): you're doing n operations, and the cost of each operation is 1, 2, 3, 4, ..., n (times some constant. 10^(n-1) * 10 all in base two takes \Theta(n) time).
But it's not actually worse than O(n^2), my mistake.
Technically speaking, yes, there should be some limit if you are accepting an untrusted input. But there is a good argument for making this limit built-in for integers but not lists: integers are expected to be atomic while lists are wildly understood as aggregates, therefore large integers can more easily propagate throughout unsuspecting code base than large lists.
(Or, if you are just saying that once you have sub-quadratic algorithms you don't need language-imposed limits anymore, maybe you are right.)
Even a naive divide and conquer decimal to binary algorithm is only logarithmically slower than multiplication.