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
Clamp walletpassphrase timeout to 2^30 seconds and check its bounds #12101
Conversation
src/wallet/rpcwallet.cpp
Outdated
@@ -2298,6 +2298,14 @@ UniValue walletpassphrase(const JSONRPCRequest& request) | |||
// Alternately, find a way to make request.params[0] mlock()'d to begin with. | |||
strWalletPass = request.params[0].get_str().c_str(); | |||
|
|||
// Get the timeout | |||
int32_t nSleepTime = request.params[1].get_int(); | |||
pwallet->nRelockTime = GetTime() + nSleepTime; |
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.
Probably shouldn't set this until after the sanity checks...
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.
Oops, fixed.
src/wallet/rpcwallet.cpp
Outdated
@@ -2298,6 +2298,14 @@ UniValue walletpassphrase(const JSONRPCRequest& request) | |||
// Alternately, find a way to make request.params[0] mlock()'d to begin with. | |||
strWalletPass = request.params[0].get_str().c_str(); | |||
|
|||
// Get the timeout | |||
int32_t nSleepTime = request.params[1].get_int(); |
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.
Probably should use int
, not int32_t
(they're not guaranteed to be the same)
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.
8600546
to
370f105
Compare
src/wallet/rpcwallet.cpp
Outdated
if (nSleepTime < 0) { | ||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Timeout cannot be negative."); | ||
} | ||
pwallet->nRelockTime = GetTime() + nSleepTime; |
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.
Still need to check the passphrase etc first... otherwise we've essentially tricked the wallet into lengthening its unlock time.
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.
Fixed.
nRelockTime
isn't actually used for anything except displaying stuff in getwalletinfo
so this wouldn't actually be a problem.
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.
Displaying the wrong time is kind of a problem, although maybe not a security one.
d64e014
to
f94b423
Compare
src/wallet/rpcwallet.cpp
Outdated
// Get the timeout | ||
int nSleepTime = request.params[1].get_int(); | ||
// Timeout cannot be negative, otherwise it will relock immediately | ||
if (nSleepTime < 0) { |
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.
Do we want to allow zero?
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.
What would be the point of allowing 0?
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.
Perhaps to just test if the passphrase is correct. (You're currently allowing zero 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.
(You're currently allowing zero here.)
Right.
It doesn't matter either way IMO.
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.
Accepting 2147483647 (years long timeout) does not make too much sense in the time of Meltdown and Spectre vulnerabilities.
Anyway having timeout on the level of single days can be too long for these type of attacks.
test/functional/wallet-encryption.py
Outdated
|
||
# Test timeout bounds | ||
assert_raises_rpc_error(-8, "Timeout cannot be negative.", self.nodes[0].walletpassphrase, passphrase, -10) | ||
assert_raises_rpc_error(-1, "JSON integer out of range", self.nodes[0].walletpassphrase, passphrase, 2147483648) |
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.
2147483648 is not guaranteed to be out of range.
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.
How can you guarantee that a number will be out of range?
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.
Why don't you add test for 2147483647? It should work correctly for this value but we don't test it today.
On the other hand waiting for timeout could be too long :) It should just accept the value.
I'd feel more comfortable using int64, but add a clamp (if value is over 100 years, set it to 100 years or so). |
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.
From user experience point of view when we have "seconds" defined timeout I would expect times maybe up to 1 day. Giving big number does not make sense.
If it is intended to allow days then the timeout should be specified in "days" and not "seconds".
Look at the usability aspect. Anyway I don't have a clue how it is used today in real life.
test/functional/wallet-encryption.py
Outdated
|
||
# Test timeout bounds | ||
assert_raises_rpc_error(-8, "Timeout cannot be negative.", self.nodes[0].walletpassphrase, passphrase, -10) | ||
assert_raises_rpc_error(-1, "JSON integer out of range", self.nodes[0].walletpassphrase, passphrase, 2147483648) |
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.
Why don't you add test for 2147483647? It should work correctly for this value but we don't test it today.
On the other hand waiting for timeout could be too long :) It should just accept the value.
src/wallet/rpcwallet.cpp
Outdated
// Get the timeout | ||
int nSleepTime = request.params[1].get_int(); | ||
// Timeout cannot be negative, otherwise it will relock immediately | ||
if (nSleepTime < 0) { |
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.
Accepting 2147483647 (years long timeout) does not make too much sense in the time of Meltdown and Spectre vulnerabilities.
Anyway having timeout on the level of single days can be too long for these type of attacks.
Yes it does. A common and recommended configuration on a system with an online wallet which would otherwise not be encrypted is to encrypt and then manually unlock any time the system is restarted. If we did not allow a setting which was effectively 'until restart' these systems would simply not use encryption. This configuration saved localbitcoins loss of almost all their funds after a datacenter operator was social engineered, just as a single example. -- Facilities social engineering has historically been one of the most common compromise vectors, and it almost always results in a system reboot.
Different applications have different scales. More ordinary CLI user usage typical timeouts are just a few seconds or a few minutes.
To trigger a keypool topup. (though I didn't look to see if that would be guaranteed to work, but we should make it do so by performing the topup on unlock while still holding the lock)
That sounds fine, in particular we don't want people who have been happily using 999999999 to find it suddenly returning an error. |
f94b423
to
c8ca057
Compare
I have changed this to clamp at 2^31 seconds so the wallet will lock for up to 2^31 seconds which is approximately 68 years. This number was chosen because the |
77db820
to
09fbbfa
Compare
I don't know why travis is failing. |
Maybe add a limit to "help" printout? |
It looks like Travis test fails on 32bit architecture. There is something wrong as you get a lot of messages like: Also you can see that in the log printout there is a wrong value: |
fed0254
to
71620b3
Compare
@bolekC Added some info about the limit to the help output. I think I fixed the travis error. It has to do with the type of the value being shifted. The log output is correct. |
test/functional/wallet-encryption.py
Outdated
expected_time = int(time.time()) + (1 << 31) - 600 | ||
self.nodes[0].walletpassphrase(passphrase2, (1 << 31) - 600) | ||
actual_time = self.nodes[0].getwalletinfo()['unlocked_until'] | ||
assert actual_time < expected_time + 5 and actual_time >= expected_time # 5 second buffer |
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: Python allows chained comparisons: expected_time <= actual_time < expected_time + 5
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
71620b3
to
a36965c
Compare
test/functional/wallet-encryption.py
Outdated
expected_time = int(time.time()) + (1 << 31) - 600 | ||
self.nodes[0].walletpassphrase(passphrase2, (1 << 31) - 600) | ||
actual_time = self.nodes[0].getwalletinfo()['unlocked_until'] | ||
assert expected_time <= actual_time < expected_time + 5 # 5 second buffer |
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.
Might want to use the provided assert_greater...
helper methods to give a more verbose output in case of failure.
Current error shows only:
File "/home/travis/build/bitcoin/bitcoin/build/bitcoin-i686-pc-linux-gnu/test/functional/wallet-encryption.py", line 68, in run_test
assert expected_time <= actual_time < expected_time + 5 # 5 second buffer
AssertionError
and not by how much it was off.
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.
aa16e2d
to
7945bd0
Compare
I changed the clamp to be at 2^31 - 1 seconds. Hopefully that fixes the travis problem with 32-bit linux. |
src/wallet/rpcwallet.cpp
Outdated
} | ||
// Clamp timeout to 2^31 seconds | ||
if (nSleepTime > ((int64_t)1 << 31) - 1) { | ||
nSleepTime = ((int64_t)1 << 31) - 1; |
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 could use the named constexpr std::numeric_limits<int32_t>::max()
.
Also, how does that take into account your observation that "the time_t type that is used for the timer can be a 32 bit signed integer on some systems". (Considering that nSleepTime is multiplied by 1000 somewhere down in the call stack)
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 could use the named constexpr
std::numeric_limits<int32_t>::max()
.
I'll try that now.
Also, how does that take into account your observation that "the time_t type that is used for the timer can be a 32 bit signed integer on some systems". (Considering that nSleepTime is multiplied by 1000 somewhere down in the call stack)
It's multiplied by 1000 and then divided by 1000 before going into the time_t
.
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.
Ah, ok. But since nSleepTime represents a delta, that is added onto GetTime()
, capping it at the max of int32_t is not enough?
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 timer itself uses the delta. The only thing that's added is wallet->nRelockTime
which is only used for display in getwalletinfo
.
7945bd0
to
6eeeb13
Compare
@MarcoFalke adjusted |
I still think we don't understand the fault's root cause very well. This 32bit failing doesn't give a rest.
I suspect QTimer implementation but I have not succeeded to troubleshoot it yet. Still open questions: Why with high numbers wallet it re-locked immediately? What is causing this behaviour? Which code exactly? Is it QTimer? |
That's correct. I have no clue why it is failing, although I believe it has to do with floating point numbers. I'm pretty sure the issue has to do with the
We don't use QTimers. Rather it uses |
Sure it's |
In the most common case, yes. IIRC it does use QTimers when -server is false in the GUI: in that case libevent isn't initialized. Multiple timer implementations can be registered through |
Not sure why I didn't think of this sooner: but another thing we can do instead of clamping the time when larger than a certain value, is to not use a timer at all in that case - interpret large numbers as 'infinite', unlocking forever (or until manually re-locked). Of course, this needs to be documented. This avoids having to cope with timer edge-case behavior entirely. |
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 134cdc7
throw JSONRPCError(RPC_INVALID_PARAMETER, "Timeout cannot be negative."); | ||
} | ||
// Clamp timeout to 2^30 seconds | ||
if (nSleepTime > (int64_t)1 << 30) { |
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.
Could just write 1L instead of casting 1.
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 was having some problems with using 1L on 32 bit systems so explicitly casting it like this seemed to be better.
Since it seems infeasible to support large timeouts and clamping doesn't work very well on every platform, I'd prefer the approach by @laanwj to unlock and just return and not set any (potentially false) timers |
Where are you seeing this? It seems like current implementation should work on every platform. Agree that laanwj's suggestion would make code a little cleaner, but the behavior for all practical purposes would seem identical to what is implemented here. Also, regardless of anything, current PR seems like an improvement over status the quo. |
The "magic value" was determined by "ok,
Agree. |
utACK 134cdc7 |
We would still need some magic value where all of the values less than it are valid timeouts. Using |
I think the suggestion is to simply not call RPCRunLater if there is an infinite timeout. |
Just to remind that we don't know the root cause of this behaviour. Suspected is Described behaviour is that with big number wallet gets locked immediately so probably timeout is fired right after the command to set timer is given. I have observed also that sometimes this is few seconds delayed if bitcoind is heavily occupied with some tasks. This suggests event handling. Find a root cause to have 100% safe solution. I have not debugged it to this level yet (lack of time) |
RPCTimerInterface::NewTimer can be either QtRPCTimerInterface (using QTimers) or HTTPRPCTimerInterface (using libevent). QtRPCTimerInterface passes the millisecond value to QTimer::start(), which in Qt5 accepts a std::chrono::milliseconds which is specced to be at least 45 bits, so is fine. In Qt4.8 it's specced as an int, and if int maxes out at 2**31-1 will overflow if nSleepTime is 2147484 or above, which is about 24 days. HTTPRPCTimerInterface makes an HTTPRPCTimer, which populates a timeval with Trying on Debian/i386, I can confirm that libevent gets an event with a negative timeout in that scenario, which I would expect to behave badle. I can't actually get the event to trigger, however, so I'm not sure if this can be made to cause the wallet to actually be re-locked early. (Checking it's re-locked at the correct time doesn't seem worthwhile, since time_t will have wrapped by then, and who knows what will have broken...) So if this needs to be improved further, maybe a check along the lines of But as is, I agree this makes things better than the status-quo, so ACK 134cdc7 . |
Okay, retrying with the limit being |
…k its bounds 134cdc7 Test walletpassphrase timeout bounds and clamping (Andrew Chow) 0b63e3c Clamp walletpassphrase timeout to 2^(30) seconds and check its bounds (Andrew Chow) Pull request description: Fixes #12100 Makes the timeout be clamped to 2^30 seconds to avoid the issue with sign flipping with large timeout values and thus relocking the wallet instantly. Unlocking for at most ~34 years should be sufficient. Also checks that the timeout is not negative to avoid instant relocks. Tree-SHA512: 426922f08c54e323d259e25dcdbebc2cd560708a65111ce6051493a7e7c61e79d9da1ea4026cc0d68807d728f5d7c0d7c58168c6ef4167b94cf6c2877af88794
OK, I agree. I was just a bit wary of setting a timer that we assume will never trigger at all, might as well not set it, but indeed this improves on the issue. Can be improved later. |
…nd check its bounds 134cdc7 Test walletpassphrase timeout bounds and clamping (Andrew Chow) 0b63e3c Clamp walletpassphrase timeout to 2^(30) seconds and check its bounds (Andrew Chow) Pull request description: Fixes bitcoin#12100 Makes the timeout be clamped to 2^30 seconds to avoid the issue with sign flipping with large timeout values and thus relocking the wallet instantly. Unlocking for at most ~34 years should be sufficient. Also checks that the timeout is not negative to avoid instant relocks. Tree-SHA512: 426922f08c54e323d259e25dcdbebc2cd560708a65111ce6051493a7e7c61e79d9da1ea4026cc0d68807d728f5d7c0d7c58168c6ef4167b94cf6c2877af88794
…nd check its bounds 134cdc7 Test walletpassphrase timeout bounds and clamping (Andrew Chow) 0b63e3c Clamp walletpassphrase timeout to 2^(30) seconds and check its bounds (Andrew Chow) Pull request description: Fixes bitcoin#12100 Makes the timeout be clamped to 2^30 seconds to avoid the issue with sign flipping with large timeout values and thus relocking the wallet instantly. Unlocking for at most ~34 years should be sufficient. Also checks that the timeout is not negative to avoid instant relocks. Tree-SHA512: 426922f08c54e323d259e25dcdbebc2cd560708a65111ce6051493a7e7c61e79d9da1ea4026cc0d68807d728f5d7c0d7c58168c6ef4167b94cf6c2877af88794
…nd check its bounds 134cdc7 Test walletpassphrase timeout bounds and clamping (Andrew Chow) 0b63e3c Clamp walletpassphrase timeout to 2^(30) seconds and check its bounds (Andrew Chow) Pull request description: Fixes bitcoin#12100 Makes the timeout be clamped to 2^30 seconds to avoid the issue with sign flipping with large timeout values and thus relocking the wallet instantly. Unlocking for at most ~34 years should be sufficient. Also checks that the timeout is not negative to avoid instant relocks. Tree-SHA512: 426922f08c54e323d259e25dcdbebc2cd560708a65111ce6051493a7e7c61e79d9da1ea4026cc0d68807d728f5d7c0d7c58168c6ef4167b94cf6c2877af88794
…nd check its bounds 134cdc7 Test walletpassphrase timeout bounds and clamping (Andrew Chow) 0b63e3c Clamp walletpassphrase timeout to 2^(30) seconds and check its bounds (Andrew Chow) Pull request description: Fixes bitcoin#12100 Makes the timeout be clamped to 2^30 seconds to avoid the issue with sign flipping with large timeout values and thus relocking the wallet instantly. Unlocking for at most ~34 years should be sufficient. Also checks that the timeout is not negative to avoid instant relocks. Tree-SHA512: 426922f08c54e323d259e25dcdbebc2cd560708a65111ce6051493a7e7c61e79d9da1ea4026cc0d68807d728f5d7c0d7c58168c6ef4167b94cf6c2877af88794
Fixes #12100
Makes the timeout be clamped to 2^30 seconds to avoid the issue with sign flipping with large timeout values and thus relocking the wallet instantly. Unlocking for at most ~34 years should be sufficient.
Also checks that the timeout is not negative to avoid instant relocks.