Make all inputs constant#152
Conversation
|
The tokenise_name one could probably be amended. It already passes in the length to The rans decode function should be easily fixable. We uncompress the data and store the new buffer into the So |
|
This looks to fix the name tokeniser, at a cost of a tiny amount of memory to store the read name length. (It could in theory be less due to a 255 char limit in the specification, but I doubt it'd make any difference with structure padding taking up the other bytes) |
|
Thanks! I will implement that. I was just in the process of doing so. |
|
Right, this might look great and all, but I get errors when I run Compiling is fine but the name tokenizer does not finish. That needs to be fixed before this can be merged. |
|
Everything fixed and ready for review! |
| @@ -1045,7 +1045,7 @@ static int decode_name(name_context *ctx, char *name, int name_len) { | |||
| return -1; | |||
|
|
|||
| if (strlen(ctx->lc[pnum].last_name) +1 >= name_len) return -1; | |||
There was a problem hiding this comment.
It looks like I missed a last_name fix in my change!
I was wondering how memcpy to memmove fixes anything, as the name and last_name should never overlap. How has changing from nul-terminated strings to str+length made them overlap? The answer is when I forget to check the length and still rely on strlen! Oops.
Ah wait... this is the decoder, which I don't think we changed for the blk[i] = '\0' bit.
I need to ponder it more I think.
There was a problem hiding this comment.
Ok I see the cause of this now. The original code in the decoder was this:
if (t0 == N_DUP) {
if (pnum == cnum)
return -1;
if (strlen(ctx->lc[pnum].last_name) +1 >= name_len) return -1;
strcpy(name, ctx->lc[pnum].last_name);
// FIXME: optimise this
ctx->lc[cnum].last_name = name;
ctx->lc[cnum].last_ntok = ctx->lc[pnum].last_ntok;
It may look funky and like it'll have a problem with removing the blk[i] = '\0' line, but that was in the encoder. Hence I didn't change it and didn't need to either. The decoder is already producing a nul-terminated output array. This isn't ideal, but this is a specialised codec that is returning an array of read names, and the terminating symbol (nul or newline) isn't part of the data stream. (Hence we have a horrible tr in the comparison code to check it round trips OK.)
So, the names (including ctx->lc[pnum].last_name which is just a pointer into the output buffer decoded so far if I recall correctly) are indeed still nul-terminated here, and strcpy works fine. That said, memcpy would be faster if we knew the length, and thanks to expanding the context struct we do now have a slot for storing the length. So it is worth improving this and my initial proposal wasn't optimal I think.
Your initial change was to this:
if (t0 == N_DUP) {
if (pnum == cnum)
return -1;
if (strlen(ctx->lc[pnum].last_name) +1 >= name_len) return -1;
memcpy(name, ctx->lc[pnum].last_name, name_len);
// FIXME: optimise this
ctx->lc[cnum].last_name = name;
ctx->lc[cnum].last_name_len = name_len;
ctx->lc[cnum].last_ntok = ctx->lc[pnum].last_ntok;
Thhis didn't work as you're copying name_len and not strlen(ctx->lc[pnum].last_name) +1 bytes worth out of last_name.
Here I do have to apologise though! name_len in this function is the size of the name array:
static int decode_name(name_context *ctx, char *name, int name_len) {
It's not the length of an individual name. I should probably have called it something different. Sorry. You can see this in the calling code:
size_t out_sz = 0;
while ((ret = decode_name(ctx, (char *)out+out_sz, ulen)) > 0) {
out_sz += ret;
ulen -= ret;
}
Hence we're copying vast swathes of data with that memcpy. Some 40kb worth on my test. Memmove solves it, but it's the wrong fix and hides a new O(N^2) complexity loop.
We could cache the strlen return value and use that, but we can already do that in last_name_len now anyway. Ie:
diff --git a/htscodecs/tokenise_name3.c b/htscodecs/tokenise_name3.c
index a6fd9e7..5ccd035 100644
--- a/htscodecs/tokenise_name3.c
+++ b/htscodecs/tokenise_name3.c
@@ -1044,11 +1044,15 @@ static int decode_name(name_context *ctx, char *name, int name_len) {
if (pnum == cnum)
return -1;
- if (strlen(ctx->lc[pnum].last_name) +1 >= name_len) return -1;
- memmove(name, ctx->lc[pnum].last_name, name_len);
+ int last_name_len = ctx->lc[pnum].last_name_len;
+ if (last_name_len >= name_len) return -1;
+ memcpy(name, ctx->lc[pnum].last_name, last_name_len);
// FIXME: optimise this
ctx->lc[cnum].last_name = name;
- ctx->lc[cnum].last_name_len = name_len;
+ ctx->lc[cnum].last_name_len = last_name_len;
ctx->lc[cnum].last_ntok = ctx->lc[pnum].last_ntok;
int nc = ctx->lc[cnum].last_ntok ? ctx->lc[cnum].last_ntok : MAX_TOKENS;
@@ -1194,7 +1198,7 @@ static int decode_name(name_context *ctx, char *name, int name_len) {
name[len++] = 0;
ctx->lc[cnum].last[ntok].token_type = N_END;
ctx->lc[cnum].last_name = name;
- ctx->lc[cnum].last_name_len = name_len;
+ ctx->lc[cnum].last_name_len = len;
ctx->lc[cnum].last_ntok = ntok;
last_context_tok *shrunk
This caches the actual length of the single name returned.
There was a problem hiding this comment.
Again, sorry about the awful reuse of name_len for two totally different use cases in the encode function vs the decode function! We should probably call it something else like capacity.
There was a problem hiding this comment.
number_of_names comes to mind.
There was a problem hiding this comment.
I can work on this later. I believe I turned on changes by owners in case you just want to get it over with.
I was indeed surprised by the adress sanitizers warning of overlapping lengths. But given your explanation that makes sense now. What is more surprising is the fact that there is a bug, and still all the tests run fine. With C I always get a little paranoid about what kind of memory I did change in that instance. This could cause very interesting intermittent bugs down the road. I think the memmove should be a memcpy again in any case so address sanitizer will properly catch that it is fixed.
There was a problem hiding this comment.
number_of_names comes to mind.
It's not the number of names, but number of bytes used to store the names. name_capacity perhaps? It's basically there to avoid buffer overruns incase the encoded data stream is lying about the unpacked size.
There was a problem hiding this comment.
name_buffer_size?
There was a problem hiding this comment.
Done. Switched up to memcpy again. This time no errors from address sanitizer. And I trust address sanitizer. Such a great tool to work with.
I think I put all the right lens in the right places.
Also I have to apologize for the amount of time-investment required for you. I still feel guilty about the last time where my idea turned out to be quite a waste of time.
There was a problem hiding this comment.
Don't beat yourself up unnecessarily. You're good to work with and even your previous idea that you referred to did highlight flaws in the original code which wouldn't have been fixed had you not created that PR. Maybe the fix itself wasn't correct, but the code still improved due to your time and effort!
Software engineering is rarely a smooth path in the direction you assume it's going. :-)
Thanks for this. It may be another week or two before I get back to checking it though (depending on my time tomorrow). It's looking promising though.
There was a problem hiding this comment.
Thanks for the kind words. That means a lot.
Please take your time. I can simply build the python bindings on this commit for the time being. I made the python bindings just because I want to mess around with these very good codecs. I want to package them in a way that is more "mess-a-roundable" than a C library. In other words, pip install htscodecs and then import htscodecs, done instead of configuring compile dependencies etc. Python also provides a nice REPL so you can interactively mess around, maybe use iPython notebooks etc.
I am going to abuse the name tokeniser to reproducibly tackle float representations. Converting back to binary and then back again leads to a loss of the original representation. I was thinking about ways to tackle that and realised the name tokeniser actually tackles that particular problem. So I wonder how it performs against generic compression algorithms.
This makes all inputs constant, because input data should not be changed by encoding/decoding functions.
However I found two things in the code that make this not yet possible:
Line 1500 in tokenise_name3.c
blk[i] = '\0';This overwrites the input with zeroes.
Line 1807 in rANS_static4x16pr.c
meta_free = meta = rans_dec_func(do_simd, 0)(in+sz, in_size-sz, NULL, u_meta_size);Meta is equal to the input pointer + something. Meta_free is a mutable buffer. Here both get equated for some reason? I don't fully understand what is happening here.