Skip to content

Conversation

@anonrig
Copy link
Member

@anonrig anonrig commented Oct 30, 2025

Another take to #5442 which should be a lot faster since we're basically caching now.

@anonrig anonrig requested a review from kentonv October 30, 2025 15:51
@anonrig anonrig requested review from a team as code owners October 30, 2025 15:51
@anonrig anonrig force-pushed the yagiz/buffered-entropy-source branch 2 times, most recently from 0c342d0 to 8d2b2b6 Compare October 30, 2025 16:14
@jasnell
Copy link
Collaborator

jasnell commented Oct 30, 2025

Needs tests for the new utility

@anonrig anonrig requested a review from jasnell October 30, 2025 16:53
@anonrig anonrig force-pushed the yagiz/buffered-entropy-source branch 2 times, most recently from 0aeedfc to cd9aa70 Compare October 30, 2025 17:03
@codspeed-hq
Copy link

codspeed-hq bot commented Oct 30, 2025

CodSpeed Performance Report

Merging #5444 will not alter performance

Comparing yagiz/buffered-entropy-source (63ec89a) with main (ea2995b)

Summary

✅ 34 untouched
⏩ 9 skipped1

Footnotes

  1. 9 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@anonrig anonrig force-pushed the yagiz/buffered-entropy-source branch from cd9aa70 to d8f1256 Compare October 30, 2025 17:57
@kentonv
Copy link
Member

kentonv commented Oct 30, 2025

This design based on EntropySource isn't what I had in mind. I think we should define a simple global function like:

void getEntropy(kj::ArrayPtr<byte> output);

This should use a thread_local buffer in its implementation.

Then we can replace all the RAND_bytes calls with this.

This makes the change light-touch with no need to change any design around EntropySource.

@anonrig anonrig force-pushed the yagiz/buffered-entropy-source branch from d8f1256 to 4642d32 Compare October 30, 2025 20:59
@anonrig anonrig requested a review from jasnell October 30, 2025 21:00
Copy link
Member

@kentonv kentonv left a comment

Choose a reason for hiding this comment

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

OK, this now looks pretty good to me but I wonder if we should check in with a security person to be extra-sure we aren't doing something bad here -- random number generation problems can be catastrophic for crypto.

@mschwarzl what do you think?

@anonrig anonrig changed the title implement BufferedEntrophySource to reduce syscalls use shared buffer for retrieving random bytes Oct 30, 2025
@anonrig anonrig force-pushed the yagiz/buffered-entropy-source branch from 4642d32 to 63ec89a Compare October 31, 2025 13:57
@anonrig anonrig requested review from jasnell and kentonv October 31, 2025 13:57
size_t toCopy = kj::min(state.data.size(), output.size());
output.first(toCopy).copyFrom(state.data.first(toCopy));
// Zero out the source buffer after copying to prevent sensitive data from remaining in memory
OPENSSL_cleanse(state.data.first(toCopy).begin(), toCopy);
Copy link
Collaborator

@jasnell jasnell Oct 31, 2025

Choose a reason for hiding this comment

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

Using fill(0) here works just as well for this and is simpler.

Suggested change
OPENSSL_cleanse(state.data.first(toCopy).begin(), toCopy);
state.data.first(toCopy).fill(0);

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess the idea behind OPENSSL_cleanse() is to overwrite memory in a way that the compiler won't try to optimize away if it thinks the value will not be read again -- presumably to reduce the likelihood of the value being readable in memory dumps, or via bad pointer accesses? Unclear if fill() would necessarily do the same.

It's interesting that the value is considered sensitive immediately after a copy of it is given out, but may otherwise have been sitting in the buffer an arbitrary amount of time prior to that. Don't know if that might change the security properties of the code.

Copy link
Collaborator

Choose a reason for hiding this comment

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

fill(...) uses a simple for loop rather than using memset. The way it's structured the compiler shouldn't be able to optimize it away the same way it can with memset.

One thing that I think we can establish is that this should not be used at all for sensitive data like secret keys. Randomized ids and random uint64's etc are fine.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, OK... In that case, it might not even be that important to clear the buffer, but I suppose there's little downside to doing so. Probably worth noting in the doc comment (if not the function name) that the function isn't intended for sensitive use cases.

Copy link
Member

Choose a reason for hiding this comment

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

fill(...) uses a simple for loop rather than using memset. The way it's structured the compiler shouldn't be able to optimize it away the same way it can with memset.

What difference does it make to the compiler whether its a for loop or memset? If the compiler can determine the writes will never be observed, it can optimize it out.

That said it would take some extremely complex logical reasoning for the compiler to be able to optimize out this case, so I wouldn't be too worried about it.

One thing that I think we can establish is that this should not be used at all for sensitive data like secret keys. Randomized ids and random uint64's etc are fine.

Why would we want to say that? The output of this function should work just fine for generating cryptographic secrets.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Why would we want to say that? ...

Sure it would technically work. While FIPS 140 does not explicitly state that cached entropy sources are forbidden, the requirements are fairly clear about entropy sources needing to be live and actively reseeded. While our 4096-byte cache would, most likely, be drained quickly, it still counts as cached entropy. We'd likely want to have a security team sign-off to rely on it for cryptographic secrets.

Copy link
Member

Choose a reason for hiding this comment

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

I agree anything FIPS-sensitive likely can't use this.

And I think we should get security team sign-off before landing this at all.

@jasnell jasnell requested a review from mschwarzl October 31, 2025 19:46
Comment on lines -139 to +138
KJ_ASSERT(RAND_bytes(reinterpret_cast<uint8_t*>(&ret), sizeof(ret)) == 1);
getEntropy(kj::asBytes(ret));
Copy link
Contributor

@jclee jclee Nov 1, 2025

Choose a reason for hiding this comment

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

Maybe worth noting that this probably will have no effect. As far as I can tell, all callers of getRandom64Bit() pass in an a non-none entropySource. We should probably just remove the parameter's Maybe-ness.

(or update callers to pass none in the appropriate places)

@jasnell
Copy link
Collaborator

jasnell commented Nov 4, 2025

Point raised from the security team: is forking an issue here? Specifically, if the process is forked after the cache is populated, the cache for the current thread will be duplicated in the fork. We either need to (a) require that the cache is only populated after forking happens or (b) refresh the cache when forking happens.

@jclee
Copy link
Contributor

jclee commented Nov 4, 2025

Point raised from the security team: is forking an issue here? Specifically, if the process is forked after the cache is populated, the cache for the current thread will be duplicated in the fork. We either need to (a) require that the cache is only populated after forking happens or (b) refresh the cache when forking happens.

FWIW, since this code is most likely invoked in the sandbox, it shouldn't be forking. We avoid forking in a process that already has additional threads, since another thread could be holding locks that would never release in the new process. In the past, this has typically manifested as "interrupt" signals in tcmalloc functions.

@kentonv
Copy link
Member

kentonv commented Nov 4, 2025

This code should be called strictly post-fork, but we should nevertheless add some debug checks to verify that the value returned by getpid() hasn't changed -- and just crash if a change is seen. If we see such a crash in tests, it means there's some pre-fork call to getEntropy() that needs to be removed.

We should skip these checks in release builds as getpid() is a syscall which is expensive.

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.

4 participants