-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
lib/model: Verify request content against weak hash #4767
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
Conversation
@calmh comments on the generic idea? |
I'm not sure I like a protocol message for this. As far as I'm concerned this is a bug on the sender side - we're specifically asking for a block with a certain hash (or, I forget, maybe just implicitly because it claimed it had that hash to begin with) and it's giving us something else. It should not be our responsibility, as the receiver, to tell it what to do about that. We could / should instead verify the hash as we're sending and trigger a rescan as appropriate. Of course we don't want to do that all the time as it's expensive. Potential ways to mitigate;
|
Well my argument is that receiver already does that, so why not use that as a mechanism for signaling? |
Because it adds odd cruft to the protocol that doesn’t belong there.
|
Well in that case the last option sounds most reasonable. |
So do we agree with the last one as the measure? |
To be honest, my heart breaks thinking about all the phones and RPIs doing that, versus just one lousy protocol message. |
If you think that's too painful, why not choose the middle one (check the block the second time the same peer asks for it - this should never happen, really). That still keeps the bug workaround where it belongs, on the buggy side. |
I don't think this is always a bug, it's also interactions with other apps. |
Do you agree or disagree that it's not always a bug? |
I’m not sure it 100% matters, but since the protocol exchange today is
A: I have a block with hash 1234
B: Give me the block with hash 1234
A: Here’s a banana
B: ...
I think it’s a bug on A side. I think it’s reasonable to expect A to not send something other than what it said it had, and what we asked for.
That other apps can fool Syncthing by changing the contents and retaining the timestamp is sort of beside the point. It can be resolved by A not being lazy and double checking its data. It can be optimized so this is not hideously expensive.
|
Ok |
So I was thinking about this, but any LRU idea seems it's going to be a hit and miss with many small files situation. Bloom filter would probably work okay'ish but getting the size right is hard, as at some point you'll just start flagging everything. What about this. We make weak hash always be there (in the scans atleast, we can still skip it on rolling local file to reduce transfers). Alternative would be to check if the weak hash sent is non-zero, and only do this checking in that case, but it's somewhat half-assed as then we sort of don't have a problem anymore and have it at the same time, based on the current moon phase. |
Yeah I agree on the weak hash thing. Make it mandatory for scans. Check it on transmission. I don't think we should do anything fancy on failure though - return an error code to the requestor, tag the file as invalid in the database (this will also fire an index update at some point, so we stop getting requests for it), fire a rescan (which should pick up that it's invalid in the db and do a full hash). |
d109460
to
9b449cd
Compare
Have a look at this once you have time. Well tested as usual. |
I have no idea how to fix the serialization test, I probably need to downgrade something but it's not obvious what. |
Or was that fixed by hand? |
@imsodin have a look |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the added benchs and updated vendor should go into a separate PR.
} else { | ||
l.Debugln("Finder failed to verify buffer", err) | ||
} | ||
if err := verifyBuffer(buf, block); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we trigger a rescan of the file here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are mid way through a pull, so if the stuff doesn't match, scanning fill pick it up next I suspect.
This block map fixing stuff was to deal with me fking up the first version and storing half of the hash in the map, so I am just removing the 2-3 year old code that was fixing the damage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah right, I didn't get the context right. And thanks for the map fix info, I was sort of wondering about that but didn't see the point of the removed code, so kept silent :)
lib/model/model.go
Outdated
|
||
// The file probably did not get rescanned, as mtimes haven't changed. | ||
// Invalidate it, and trigger another scan. | ||
cf.Invalidate(m.shortID) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't get where mtime comes into play. Neither do I understand the need to invalidate (that's maybe linked to not getting the mtime bit).
In general wouldn't it be simpler and use less resources to rescan the file directly instead of explicitly hashing after checking the block hash. The current flow hashes a changed file twice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea here is as follows:
- We get what other side expects to get as part of the request.
- We validate that what we've read matches the weak hash, if it does, nothing happens.
- If it doesn't match, we validate using SHA256, if it matches it's all good.
- If it still doesn't match, look up our own file in the database, and compare hashes for that block. If the our hashes don't match request hashes, it means the requestor is asking for something different, so we do nothing.
You are right that we don't need to hash here, as we know that what we've read doesn't match the hash the user has asked us for, and we also know that our hash matches his, which means the file is somehow changed, so we can skip the HashFile and go straight to invalidate and rescan.
The idea behind the mtime is the fact that if something modifies files in place, or artificially adjusts mtimes, we could miss some changes, hence we double check what we're reading.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I get the invalidation now. One little nick there: In almost all cases it will result in an index update sent with the invalid file info directly followed by an update valid file info. Only in the rare case of a file that has unchanged size and mtime, but changed content, will the invalidation have any effect, in all others it is useless overhead. You could pass the file info from Request
and check whether it is equivalent to the current file in the database (as in (f FileInfo) IsEquivalent
) and invalidate if it is and scan otherwise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Request does not have a FileInfo, it just has folder, name, offset, size, hash, weakHash.
Essentially the goal is to force rescan a file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know whether force rescanning is a good idea. The only real case where a file is has changed content without changed size&mtime I can think of is bitrot. In that case we'd rather not scan and send the data.
As to what I meant with checking whether the file is changed: Within Request
the file is stated and within recheckFile
the current file info is obtained from the db. That's all the information needed to check whether the two are "equivalent".
This will have a negative effect that the files sourced from devices that did not weak hash will now be hit with constant SHA256 checks for every request. The alternative would be to:
|
Please not 2. Other options: |
4 is essentially 1. @calmh ? |
I’ll check this out tomorrow. No review time this evening.
|
I'm not sure why we need to pass the weak hash in the request. As far as I understand it, we only need to check that the hash of the data we are going to send matches the hash we have on file locally, and by extension matches the weak hash we have on file locally. Adding it to the protocol requires us to nail down what the weak hash is exactly and require everyone to use it etc. |
Right, it seems build.go updated gogoproto or something, as I can't repro running go generate locally anymore. |
Huh, still fails. |
Yeah I guess you need to update your gogoproto compiler or protoc or something.
|
I pushed a commit to your branch. From the diff it looks like I have newer protoc stuff and I guess that does not come from vendoring |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this makes weak hashes non-optional right? So we should remove the config, auto detection / benchmarking, environment variable, etc?
@@ -106,7 +104,7 @@ func (ph *parallelHasher) hashFiles(ctx context.Context) { | |||
panic("Bug. Asked to hash a directory or a deleted file.") | |||
} | |||
|
|||
blocks, err := HashFile(ctx, ph.fs, f.Name, f.BlockSize(), ph.counter, ph.useWeakHashes) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe remove this parameter altogether
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's still used in some cases I think
The autodetection et all is still useful as it still can be disabled when pulling. |
So the autodetection is about scanning speed, and with this in we should always scan with both the real and weak hash, no? For pulling we have the min-percentage-changed threshold which I guess should remain, and can be set >100 to disable the check for weak hashes. I mean, we could still avoid scanning with the weak hash, but that shifts the burden as the sender then always has to use the strong hash for verification, which does not seem fair? |
Yeah, sure, we can just keep the percentage. |
So the hashFile(useWeakHash) parameter is used in cpuBench which is then reported back to UR. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With verification I believe it is beneficial to always use weak hashing in the long run even for devices that would previously have it disabled, as the verification will be cheaper.
@@ -1382,9 +1382,54 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset | |||
return protocol.ErrGeneric | |||
} | |||
|
|||
if !scanner.Validate(buf, hash, weakHash) { | |||
m.recheckFile(deviceID, folderFs, folder, name, int(offset)/len(buf), hash) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Return with ErrNoSuchFile
here.
// The hashes provided part of the request match what we expect to find according | ||
// to what we have in the database, yet the content we've read off the filesystem doesn't | ||
// Something is fishy, invalidate the file and rescan it. | ||
cf.Invalidate(m.shortID) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My concern from an outdated comment is still applies:
I don't know whether force rescanning is a good idea. The only real case where a file has changed content without changed size&mtime I can think of is bitrot. In that case we'd rather not scan and send the data.
The only benefit of invalidation that I see is that the other device won't try to pull from us again until we update. That might even be bad, if other blocks are still fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't force a rescan if the mtime has not changed for example, so invalidation allows us to force rescan.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am aware it is necessary for force rescanning, my concern is exactly about force rescanning. It feels inconsistent, as such changes will only be detected and scanned if they are requested from another device. And I still don't know about another typical case of modified file with mtime&size unchanged except bitrot.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mmaped files, files in fat32 where the mtime granularity is 2s etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah the common case is the file changed without mtime changing; because mmap, low resolution mtime and unlucky scan time, intentional reset of mtime (id3 taggers), etc.
I’m fine with hash perf meaning strong+weak. It fits with the spirit of the benchmark being “how fast is this machine at hashing a file”.
So I'll leave hash performance as it is, and leave the flag in there, as I don't see much harm in reporting two numbers rather than one, just for the same of having a flag. What is left to do here? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I am convinced.
I still propose to check whether the db file and file info from lstat are "equivalent" (as in scanner won't hash) and only invalidate in that case, but can file a followup PR to discuss that.
A test? I think the reques_tests.go thing has a couple of tests that runs the request path, something in there maybe could be used for inspiration |
Added a crude test |
So, what's up with this. |
Not sure if I am doing the invalidation right, yet you get the idea of what I am trying to do.