-
Notifications
You must be signed in to change notification settings - Fork 37.7k
Ensure backupwallet fails when attempting to backup to source file #11376
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
I like this. Generalizing a bit, I don't like the presence of 'overwrite_if_exists'. It seems like a footgun opportunity in other cases (e.g. overwrite one wallet with another). My preference would be to either make that impossible or gate it behind an rpc toggle (default being don't overwrite, like when using 'cp/mv --interactive'). |
Tested and working. Additional test performed (try to backup to a file that symlinks to the wallet)
|
Thanks. I think this cannot be guaranteed to work with hard links and whatnot, but it's better than nothing. |
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.
Code looks good, thanks! Just needs to clean up the test changes.
test/functional/walletbackup.py
Outdated
@@ -190,6 +190,22 @@ def run_test(self): | |||
assert_equal(self.nodes[1].getbalance(), balance1) | |||
assert_equal(self.nodes[2].getbalance(), balance2) | |||
|
|||
# backup to file self must fail | |||
try: |
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.
You can replace these entire try:except: blocks with a simple assert_raises_jsonrpc call (which also checks the error number).
I've changed the test cases to use assert_raises_jsonrpc |
Yikes -- thank you for reporting and fixing this bug. I agree with @esotericnonsense and think we should go further; a command called "backupwallet" should never delete/overwrite anything, in my opinion. |
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.
Thanks for this @tomasvdw . This is really awful and dangerous behaviour so thanks for providing a fix so quickly. I've put a few comments inline, but some more general comments:
- as others have already pointed out,
fs::copy_option::overwrite_if_exists
is completely inappropriate for a backup command. To my mind, a backup command should never result in data being deleted or overwritten (unless perhaps called with an explicit by-default-off overwrite option) - If a relative path is passed to backupwallet, then the backup will be to the directory relative to the directory where bitcoind was started. That's confusing (imagine for example a user who has started bitcoind from the datadir, and then much later calls
bitcoin-cli backupwallet ./
). We should probably just reject relative paths entirely. - now that the
backupwallet
RPC can fail in more than one way, it'd be good to propagate the error message back up to the RPC function and return it to the user. That can be done by adding anstd::string& error_string
parameter toCWallet::BackupWallet()
andCWalletDBWrapper::Backup()
. Without this, the user would just see a generic "Error: Wallet backup failed!" message and not know that it was because he'd provided a path that matches the existing wallet.dat file.
Those points aside, this is a useful change as is. If you're interested in implementing something that covers off those points, then I'd be very happy to review. Otherwise this can go in now and we can further improve behaviour in a future PR.
@@ -672,6 +672,12 @@ bool CWalletDBWrapper::Backup(const std::string& strDest) | |||
pathDest /= strFile; | |||
|
|||
try { | |||
if (fs::equivalent(pathSrc, pathDest)) { |
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.
Good that you've included this in the try-except block, since boost::filesystem::equivalent()
can throw, but the generic catch behaviour below would hide the precise error. You could return a more precise error by:
- using the
equivalent(const path& p1, const path& p2, system::error_code& ec)
form (http://www.boost.org/doc/libs/1_46_0/libs/filesystem/v3/doc/reference.html#equivalent) and checking the returnederror_code
- testing the error code in the catch block below
- having two try-catch blocks
Perhaps a little bit overkill since the only way that boost::filesystem::equivalent()
can throw is if the source wallet.dat
file doesn't exist or isn't a regular file/directory/symlink. But something to consider.
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.
but the generic catch behaviour below would hide the precise error.
It does dumps the error message
Perhaps a little bit overkill since the only way that boost::filesystem::equivalent() can throw is if the source wallet.dat file doesn't exist or isn't a regular file/directory/symlink.
This was also my thought. If equivalant() fails we have bigger problems and a generic error is sufficient.
src/wallet/db.cpp
Outdated
@@ -672,6 +672,12 @@ bool CWalletDBWrapper::Backup(const std::string& strDest) | |||
pathDest /= strFile; | |||
|
|||
try { | |||
if (fs::equivalent(pathSrc, pathDest)) { | |||
LogPrintf("cannot backup to wallet source file %s\n", | |||
pathDest.string()); |
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.
nit: the repository doesn't enforce max line length, so you can join this with the line above.
test/functional/walletbackup.py
Outdated
|
||
for sourcePath in sourcePaths: | ||
assert_raises_jsonrpc(-4, "backup failed", | ||
self.nodes[0].backupwallet, sourcePath) |
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.
nit: again, no max line length, so you can join this with the line above.
@jnewbery , @sdaftuar Personally I think that silently overwriting the destination (if it's not the source!) is perfectly reasonable for a backup api command, and expected behavior. You don't want an rpc method to be interactive, and I am not to fond of something like "mode=overwrite". But if others agree with your point, I am happy to change this, in this PR. With regards to the returning message, my reasoning is that although this is indeed quite a major bug it is also extremely rare and unlikely to happen to more than almost nobody. I considered a generic "fail" and a log enough, but if this reasoning is too lazy, I could enum the return and clarify the message. Do we want that? With regards to relative paths; I would think that the RPC interface is primarily a programming interface. A relative path being relative to the working directory of the daemon seems to be common practice among programmable services, and removing it an odd limitation in flexibility that doesn't really help anyone. |
Removed line breaks as per suggestion @jnewbery |
@tomasvdw I really think overwriting something that could be a wallet file a user needs is a fundamentally dangerous behavior; for example even though we're checking that the destination is different from the source, now that we have multiwallet you could be overwriting some other wallet file (which might even be loaded, though that doesn't really matter) -- again resulting in funds loss. I think the default behavior should not be to overwrite anything, and I'd be fine with making it so that backupwallet never overwrote anything (ie don't bother with a option like mode=overwrite to allow a user to toggle it); it doesn't seem too cumbersome to me to require users to move the backup out of the way first before overwriting, and it's clearly safer. Also it looks to me like boost::filesystem::copy_file is not doing an atomic copy/overwrite, so it's possible that if your disk fills up in the middle or if your computer crashes, that the end result will be that you have now have no backup and that your existing backup has been deleted. I don't think this is an outcome we should permit. |
The sort of standard way to use the dump command is to crontab running it to write to some place where your backup process is going to pick it up. If if doesn't delete it after, you're now backing up nothing. :( I think it's not helpful to complain about overwriting without having an answer to preventing this kind of silent backup failure. Obvious alternatives off the top of my head are to halt and catch fire if you ask it to overwrite (so the above problem won't last), or to move the file its overwriting out of the way first e.g. wallet.backup.dat.1 |
Sure, this seems reasonable to me, as does having the rpc command return failure (which I'd expect someone running this out of cron to already need to be catching). |
utACK b688b5242714f64c36e237b2c5c4151164ee6eb3 |
Added "Need Backport" label. Seems critical. |
Concept ACK. I think it should refuse to overwrite any file, as @sdaftuar says. I vaguely remember that we changed backupwallet a while ago to fail if it's used on an existing file? Edit: never mind - that was dumpwallet in #9937, and apparently it was never merged because people had different ideas about it. |
I complained, and we're now having a discussion, which is helpful in itself 🙂 The 'sort of standard way' is not safe at all. Consider a setup where your cronjob is backing up to a filename, with a separate process or remote machine copying that backup file to a secure location. If that backup archiving process dies, then bitcoind will continue merrily overwriting old backups. Or imagine a scenario where you lose power and both your bitcoind process and backup archiving process die. You regain power and restart bitcoind, but perhaps set the wrong -mainnet/-testnet or -wallet parameter. You've now just blatted your most recent backup prior to the power outage. (If I sound paranoid about this - good! I've supported too many customers who have got backups wrong and experienced a lot of pain later). So at the very least I'd change your cronjob from:
to:
or even better, to:
So that's the motivation. As for your potential solutions:
I think a better way would be to try to write the backup to the provided filename. If a file already exists, then try to write the backup to the provided filename with an index or timestamp suffix. |
Why not move the existing file? The concern I have is that if the provided name is what your backup routine is backing up, you'll just keep backing up the old thing over and over again. Hm. I guess the move it out of the way doesn't work so hot if the "thing in the way" is your live wallet.dat. :( can we just change the darn function to not let you set the filename, but only the path, and have it pick a file name, guaranteed to be unused, and return that? |
You could still leave in this fix (source != target). Generally, as there is some discussion on the move-out-of-way semantics, I think it is better to merge this critical fix and create a new PR to propose fixing overwrite. I think these are separate issues only related by touching the same area of code. |
I'd be fine with that. Obviously it's an API break. I think my general approach is that the process that is backing itself up in general shouldn't delete, overwrite or move files when backing up. It should be the backup archival process's job to manage the backup files (polling for new files, copying to remote location, deleting or renaming the source backup file when done).
ACK. I think this is useful discussion, but I'm happy for this minimal fix to be merged and then to figure out a better more general approach in a different PR. |
test/functional/walletbackup.py
Outdated
tmpdir + "/node0/regtest"] | ||
|
||
for sourcePath in sourcePaths: | ||
assert_raises_jsonrpc(-4, "backup failed", self.nodes[0].backupwallet, sourcePath) |
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.
Needs rebase and replace with "assert_raises_rpc_error"
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.
Done; rebased and changed to assert_rpc_error
Previous behaviour was to destroy the wallet (to zero-length)
Agreed, lets merge this and then we can paint the overwrite-parameter shed whatever color we like. utACK |
If at the end the backup can't overwrite any file then there is no need to merge this. Edit: I mean, should be enough to check if the destination doesn't exists to actually backup. Edit 2: I agree with not allowing overwrites, some timestamp suffix/prefix is pretty common. |
Agree with @TheBlueMatt, though I do think we should never overwrite, better is better, let's get this fix in for now. utACK |
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.
utACK 5d465e3. Seems a step in the right direction. No need let this rot again.
@@ -190,6 +190,16 @@ def run_test(self): | |||
assert_equal(self.nodes[1].getbalance(), balance1) | |||
assert_equal(self.nodes[2].getbalance(), balance2) | |||
|
|||
# Backup to source wallet file must fail | |||
sourcePaths = [ |
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.
µnit: snake_case
…source file 5d465e3 Ensure backupwallet fails when attempting to backup to source file (Tomas van der Wansem) Pull request description: Previous behaviour was to destroy the wallet (to zero-length) This fixes #11375 Tree-SHA512: bfd1738659b15e3f23b6bbdf55ec12269c62c820bf701daec19500b52bd5845bb5516733c6f76f36197eb155182a8a35dc239ad4de2ef1e59bbb0f124a455759
Previous behaviour was to destroy the wallet (to zero-length) Github-Pull: bitcoin#11376 Rebased-From: 5d465e3
…kup to source file 5d465e3 Ensure backupwallet fails when attempting to backup to source file (Tomas van der Wansem) Pull request description: Previous behaviour was to destroy the wallet (to zero-length) This fixes bitcoin#11375 Tree-SHA512: bfd1738659b15e3f23b6bbdf55ec12269c62c820bf701daec19500b52bd5845bb5516733c6f76f36197eb155182a8a35dc239ad4de2ef1e59bbb0f124a455759
…kup to source file 5d465e3 Ensure backupwallet fails when attempting to backup to source file (Tomas van der Wansem) Pull request description: Previous behaviour was to destroy the wallet (to zero-length) This fixes bitcoin#11375 Tree-SHA512: bfd1738659b15e3f23b6bbdf55ec12269c62c820bf701daec19500b52bd5845bb5516733c6f76f36197eb155182a8a35dc239ad4de2ef1e59bbb0f124a455759
…kup to source file 5d465e3 Ensure backupwallet fails when attempting to backup to source file (Tomas van der Wansem) Pull request description: Previous behaviour was to destroy the wallet (to zero-length) This fixes bitcoin#11375 Tree-SHA512: bfd1738659b15e3f23b6bbdf55ec12269c62c820bf701daec19500b52bd5845bb5516733c6f76f36197eb155182a8a35dc239ad4de2ef1e59bbb0f124a455759
Previous behaviour was to destroy the wallet (to zero-length)
This fixes #11375