Top
Best
New

Posted by justinweiss 3 hours ago

Bijou64: A variable-length integer encoding(www.inkandswitch.com)
125 points | 47 commentspage 2
nine_k 2 hours ago|
In short: instead of a truly indefinite-length solution with a signal bit on the current byte saying whether to check the next byte, this uses a counter. Values 0x0 to 0xF7 are one-byte integers, 0xF8 to 0xFF use the upper 5 bits as a counter for the number of subsequent bytes. This limits the maximum magnitude to slightly less than 2 ^ 264 (almost all 33-byte values), which seems to be okay for practical computations. The proposed standard limits the supported size to u64 though.

The upsides: the size of the integer is apparent upon reading the first byte, and every number has exactly one canonical representation. I wish C strings had been standardized around something similar, instead on null termination.

> ...adversarial input, which is rarely in the test suite.

This made my scratch my head. My tests for quite pedestrian APIs often contain adversarial input of obvious shapes. I though that for anything security-related (like the author's project) testing against adversarial input would be be a prominent part.

onlyrealcuzzo 1 hour ago|
> I though that for anything security-related (like the author's project) testing against adversarial input would be be a prominent part.

They might have a different definition of adversarial than you.

> My tests for quite pedestrian APIs often contain adversarial input of obvious shapes.

This doesn't seem like what I would call adversarial.

This seems like standard negative testing or boundary value analysis - which I would be shocked if they didn't do.

willtemperley 2 hours ago||
Maybe someone can explain why an encoder would ever create the padding bytes allowed in LEB128. I contributed the parser for LEB128 in apple/swift-binary-parsing and I’m still none the wiser. I’m genuinely mystified.
scottlamb 1 hour ago||
I can think of two reasons.

The first is what they describe here: as an attack. It's like why would anyone ever overflow a buffer with shellcode.

The second is that they are implementing a spec that requires appending a varint length-prefixed field to a buffer but don't really care about the space optimization, don't know the field's length when they start appending it, and don't want to put the field into a second, temporary buffer or slide it down into place. https://github.com/FFmpeg/FFmpeg/blob/468a743af1653a08f47081... vs say my own code which does the slide: https://github.com/scottlamb/retina/blob/6972ac4261ce7bf5b58...

cornstalks 1 hour ago|||
It allows you to fill in padding in a buffer. For example, all data in a buffer will be interpreted by a downstream system, and someone pre-calculated the size of that buffer. Rather than encode everything twice (once to figure out the exact size needed, and a second time to actually populate the buffer) the buffer size was calculated using foreknowledge of how many values would be written to that buffer and then just pessimistically assuming all of them are max-size so writing will never fail. Another situation is when you're rewriting part of an already-encoded file. If you want to change a bit of payload then using padding bytes gives you more flexibility so you can do that without having to do any memcpy into a new buffer.

It's uncommon but I've definitely seen it done (with media containers like Matroska, not actually LEB128) in extremely high-throughput systems that can't spare any cycles.

esrauch 2 hours ago|||
Let's say you are writing into a byte[] and have a LEB128 length-prefix followed by a payload, but that determining the length actually involves nontrivial encoding work. For example, you have a UTF16 string and want to write out a UTF8 string, you want to go over the characters and write them out, but the UTF8 length is not known without doing all of that work.

If you can choose a fixed number of bytes for the length prefix, you can skip that number, do the encoding and find out the length, and then come back and fill in the length-prefix after.

But you actually don't know how many bytes it will take without doing all of the work to know the payload length (since larger payloads take more bytes to represent the length).

If you allow overlong representation you can reserve a few bytes and sometimes it'll just be the effective no-op bytes. If you don't, you won't be able to.

willtemperley 1 hour ago||
Thank you for solving that mystery!
axod 1 hour ago|||
Maybe you want to byte align some data, or pack to a certain size but keep compat. I think they're going to be rare cases, but I can see it being used.
layer8 2 hours ago|||
The issue is that non-unique encodings are an attack vector, because parsers may in practice behave differently for noncanonical (or nominally invalid) encodings.
kbolino 1 hour ago||
For example, you have an envelope format that goes: length prefix in LEB128, message, signature. One party controls the length prefix and signature, a different party controls the message. The message-writing party carefully crafts the message so that, in isolation, it appears innocuous, but when wrapped in the envelope, the first few bytes of the message look like continuation of the length prefix. Best case is the receiving party safely fails to parse the message, worst case is the receiving party successfully parses the message, verifies its digital signature, and interprets it differently than the signer did.
boricj 2 hours ago|||
Laziness probably. Maybe there's an argument if you want to avoid branches and just blast the integer out in a fixed number of statements/instructions/bytes, but that sounds a bit fringe.

I happen to be guilty of a variant of this, where I don't bother emitting a 16-bit floating point number instead of a 32-bit one in my CBOR encoder even if it can be represented exactly. That one is laziness.

Chaosvex 2 hours ago||
You wouldn't. It's a strange argument that can be countered with, "maybe don't do that?"
willtemperley 2 hours ago||
So why does the spec allow it? Like a good engineer I read the spec and tested against the over-wide example encodings given.
Chaosvex 1 hour ago||
Because it's not a real standard and there is no blessed RFC for it. The DWARF spec is as close as you'll get and it says, "The integer zero is a special case, consisting of a single zero byte." So in a way, it doesn't.

Either way, a properly written decoder (and it's like ten lines) should really not have any problems with it. I was agreeing with you.

Edit: to clarify, I was talking about the author's argument being strange, not yours.

willtemperley 1 hour ago||
The WASM spec is more explicit about over-long LEB128 encoding.

Edit: a properly written decoder is a lot more than 10 lines if you properly deal with integer overflow and both signed and unsigned ints.

HansHamster 2 hours ago||
It feels a bit unfair to say that it is faster by being able to tell the total length from the first byte and capping it at 64 bit, while some of the other formats can store arbitrarily large integers. I guess you could use another variable length encoding for the prefix at the cost of some performance and using even more space...
petermcneeley 1 hour ago|
esp when the number is capped at only 64 bits which is quite small for some bigInt style numbers.
RedShift1 2 hours ago||
This seems quite convoluted just to avoid the "0 can be represented in more than one way" problem.
bjoli 2 hours ago||
Having all numbers be valid in only one way is a great idea. So much that I believe webassembly enforced canonical leb128, at the cost of decoding speed.

And say you have it as part of some other data. If you want to be able to hash it by the raw memory bytes, many different ways to represent a number becomes a problem.

matja 49 minutes ago|||
> canonicality matters — for signatures, content-addressing, or any kind of “two implementations must agree on the bytes” property

If you don't do this properly, you end up with things like: - SAML XSW attack due to XML signature wrapping - ASN.1 BER/DER signature forgery - Bitcoin transaction malleability attacks

nine_k 2 hours ago|||
It allows finding out the length (and allocating memory) after reading the first byte.
ape4 2 hours ago|||
Comparing a number to zero is something that's done a lot
Chaosvex 2 hours ago||
True but also not particularly relevant?
ahoka 2 hours ago||
I think it's neat.
cantalopes 2 hours ago||
I love the random hyperlink underlines on that page
spiralganglion 27 minutes ago|
Credit to Roman Komarov who came up with the approach [1], and Todd Matthews [2] who made the art assets.

1: https://kizu.dev/svg-linked-parameters-workaround/ 2: https://www.seaofclouds.com

alex-reyss 1 hour ago||
[dead]
amluto 1 hour ago|
Just a quick reminder:

> This causes problems for signed data if you ever want to do things like compression since you need to know the exact bytes that were signed.

If you are verifying a signature by taking some logical data structure, turning it into a byte string, and calling the verification primitive on those bytes, you likely have a design error. You should instead collect bytes, verify the signature, and then parse the bytes after verifying the signature. And remember to include enough context in those bytes so a different message signed for a different purpose by the same key doesn’t confuse you.