Skip to content
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

Clean up once code #359

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

Kixunil
Copy link
Collaborator

@Kixunil Kixunil commented Jan 5, 2022

This was originally intended to just change swap to compare_exchange
however other changes were needed too:

  • Load with Release ordering seemed wrong because if it loaded DONE
    before while loop it wouldn't synchronize with the storing thread.
  • There's no point in repeatedly checking if the value is NONE so this
    check was moved about while loop
  • Due to a mistake I wanted to inspect using miri but couldn't because
    of FFI call. Separating the once implementation from consumer allowed
    writing a test in pure Rust and inspect via miri.
  • The initializing code now skips loading the value again because it
    already knows it's initialized.
  • Orderings are now explained in the comments

Closes #351

@@ -673,6 +672,80 @@ mod fuzz_dummy {
fn rustsecp256k1_v0_4_1_context_preallocated_clone(cx: *const Context, prealloc: *mut c_void) -> *mut Context;
}

mod once {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs some kind of descriptive comment describing why its here, that it's only used in fuzzing, and that we don't really care too much...................all that said, maybe we should care a bit more, I realized yesterday that global-context-less-secure relies on std, even though the whole point of it is to avoid relying on std (it predates no-std so that's expected, but still). It only actually relies on std::sync::Once, so maybe we should use this there as well. Not sure how to get it across both crates, though, maybe just make a single file and symlink them across the two crates to avoid making it public?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case I think it should condinionaly use Once from std as suggested in #352

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole point of the global-context-less-secure feature is to work on no-std, and sadly Once is not no-std as it requires thread-parking support.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since cargo features are additive it's still useful to avoid performance issues if two crates turn on these features. Although maybe they should've been --cfg parameters, not features...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, sorry, I misread your comment again lol. Yes, conditionally using Once if std is turned on is fine, but we still need to somehow have this once available in secp256k1 if we want to fix global-context-less-secure not building with no-std.

This was originally intended to just change `swap` to `compare_exchange`
however other changes were needed too:

* Load with `Release` ordering seemed wrong because if it loaded `DONE`
  before while loop it wouldn't synchronize with the storing thread.
* There's no point in repeatedly checking if the value is `NONE` so this
  check was moved about while loop
* Due to a mistake I wanted to inspect using `miri` but couldn't because
  of FFI call. Separating the once implementation from consumer allowed
  writing a test in pure Rust and inspect via `miri`.
* The initializing code now skips loading the value again because it
  already knows it's initialized.
* Orderings are now explained in the comments
This caches the initialization state of preallocated context in a thread
local variable to avoid atomics in subsequent accesses.
Accessing `std::mem` instead of `core::mem` caused fuzzing to break
without `std`.
@Kixunil
Copy link
Collaborator Author

Kixunil commented Jan 5, 2022

I also fixed broken no_std fuzzing. Would've done in a separate PR but not worth conflict fixing I think.

@TheBlueMatt
Copy link
Member

Are you planning on fixing global-context-less-secure + no-std here?

@Kixunil
Copy link
Collaborator Author

Kixunil commented Jan 13, 2022

Oh, didn't notice it's broken. Is there an advantage of doing it here?

@TheBlueMatt
Copy link
Member

TheBlueMatt commented Jan 14, 2022

I mentioned it in one of the discussions further up, but those were a mess, it's issue is use of std Once. I don't know if this PR is worth bothering unless we also bother refactoring it to make it available in both crates (I guess a symlink to a common module?) and fix global contact with it.

@Kixunil
Copy link
Collaborator Author

Kixunil commented Jan 14, 2022

OK, will try to find the time to look into it.

Copy link
Member

@tcharding tcharding left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few minor suggestions.

if have_ctx == NONE {
// Ordering: on success we're only signalling to other thread that work is in progress
// without transferring other data, so it should be Relaxed, on failure the value may be DONE,
// so we want to Acquire to safely proceed in that case.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be 'so we want Acquire to safely proceed ...'?

// without transferring other data, so it should be Relaxed, on failure the value may be DONE,
// so we want to Acquire to safely proceed in that case.
// However compare_exchange doesn't allow failure ordering to be stronger than success
// so both are Acquire.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, this whole comment might look better like this (includes change above):

                    // Ordering: on success we're only signalling to other thread that work is in
                    // progress without transferring other data, so it should be Relaxed, on failure
                    // the value may be DONE, so we want Acquire to safely proceed in that case.
                    // However compare_exchange doesn't allow failure ordering to be stronger than
                    // success so both are Acquire.

}
}
while have_ctx != DONE {
// Another thread is building, just busy-loop until they're done.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This refers to the whole loop, right? Perhaps clearer if its outside the loop (above the while line).

assert_eq!(rustsecp256k1_v0_4_1_context_preallocated_create(
PREALLOCATED_CONTEXT[..].as_ptr() as *mut c_void,
SECP256K1_START_SIGN | SECP256K1_START_VERIFY),
PREALLOCATED_CONTEXT[..].as_ptr() as *mut Context);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we assign this pointer to a local variable?

            let ptr = PREALLOCATED_CONTEXT[..].as_ptr() as *mut c_void;
            assert_eq!(rustsecp256k1_v0_4_1_context_preallocated_create(ptr, SECP256K1_START_SIGN | SECP256K1_START_VERIFY), ptr);

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are different types. Which is strange. It'd still be shorter to cast one of them but would be better to look if we can unify them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh dear, I missed the type difference :(

// This thread will read whatever the other thread wrote so this needs to be Acquire.
have_ctx = self.0.load(Ordering::Acquire);
#[cfg(feature = "std")]
::std::thread::yield_now();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FTR yield_now is not great. Futex would be the best, cond var as a fallback.

assert_eq!(rustsecp256k1_v0_4_1_context_preallocated_create(
PREALLOCATED_CONTEXT[..].as_ptr() as *mut c_void,
SECP256K1_START_SIGN | SECP256K1_START_VERIFY),
PREALLOCATED_CONTEXT[..].as_ptr() as *mut Context);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are different types. Which is strange. It'd still be shorter to cast one of them but would be better to look if we can unify them.

const HAVE_CONTEXT_NONE: usize = 0;
const HAVE_CONTEXT_WORKING: usize = 1;
const HAVE_CONTEXT_DONE: usize = 2;
static HAVE_PREALLOCATED_CONTEXT: once::Once = once::Once::INIT;
static mut PREALLOCATED_CONTEXT: [u8; CTX_SIZE] = [0; CTX_SIZE];
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just realized this variable is very wrong due to alignment

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, so the fix should be [AlignedType; CTX_SIZE / mem::size_of::<AlignedType>()], right? Although maybe we should have a separate type for that.

Now looking at ffi::Context and it's an int. WTF?

Copy link
Member

@elichai elichai Mar 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idk what CTX_SIZE is and how was it calculated.
we can't have a type for that because we don't know the size of the context in compile time.
although everything here won't be needed when we start supporting the static contexts

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't know the size of the context in compile time.

I think we can know the maximum and guard against it changing by calling secp256k1_context_preallocated_size

@Kixunil Kixunil mentioned this pull request Jun 10, 2022
@tcharding
Copy link
Member

Whats the status of this PR @Kixunil, are there more things to be done or would you like it re-reviewed as is?

@Kixunil
Copy link
Collaborator Author

Kixunil commented Nov 21, 2022

Didn't have much time for it but meanwhile I came to conclusion that I really don't like spin locks for reasons that were mentioned elsewhere. I will probably still finish the PR to fix UB but then I want to make another one fixing the spin lock issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Mild race condition in secp256k1_context_preallocated_create
4 participants