Skip to content

Commit 73ba3f2

Browse files
committed
Re-seed after fork
Add pthread_atfork handler that enables a lazy re-seed to prevent forked processes from generating identical token streams Assisted-by: Claude Fable 5
1 parent 246f8b7 commit 73ba3f2

5 files changed

Lines changed: 227 additions & 29 deletions

File tree

MANIFEST

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ README
1111
standard.h
1212
t/00-system.t
1313
t/alphabets.t
14+
t/fork.t
1415
t/kernel-seeding.t
1516
t/no-mod-bias.t
1617
t/reference.t

Makefile.PL

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ my %args = (
1212
NAME => 'Session::Token',
1313
VERSION_FROM => 'lib/Session/Token.pm',
1414
PREREQ_PM => $deps,
15-
LIBS => [''],
15+
LIBS => ['-lpthread'],
1616
DEFINE => '',
1717
INC => '-I.',
1818
OBJECT => 'Token.o randport.o',

Token.xs

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,20 @@
88
#include <stdlib.h>
99
#include <string.h>
1010
#include <assert.h>
11+
#include <signal.h>
12+
13+
#ifndef _WIN32
14+
#include <pthread.h>
15+
#endif
1116

1217

1318
struct session_token_ctx {
1419
int mask;
1520
int count;
1621
int curr_word;
1722
int bytes_left_in_curr_word;
23+
int reseed_on_fork;
24+
int seed_generation;
1825
struct randctx isaac_ctx;
1926
char *alphabet;
2027
size_t alphabet_length;
@@ -23,6 +30,54 @@ struct session_token_ctx {
2330

2431
typedef struct session_token_ctx * Session_Token;
2532

33+
static volatile sig_atomic_t fork_generation = 0;
34+
35+
#ifndef _WIN32
36+
static void session_token_atfork_child(void) {
37+
fork_generation++;
38+
}
39+
#endif
40+
41+
static void seed_ctx(struct session_token_ctx *ctx, char *seedp) {
42+
memcpy(&ctx->isaac_ctx.randrsl, seedp, 1024);
43+
randinit(&ctx->isaac_ctx, TRUE);
44+
isaac(&ctx->isaac_ctx);
45+
46+
ctx->count = 0;
47+
ctx->curr_word = 0;
48+
ctx->bytes_left_in_curr_word = 0;
49+
ctx->seed_generation = fork_generation;
50+
}
51+
52+
static void reseed_ctx(struct session_token_ctx *ctx) {
53+
dSP;
54+
SV *seed;
55+
char *seedp;
56+
size_t len;
57+
int count;
58+
59+
ENTER;
60+
SAVETMPS;
61+
62+
PUSHMARK(SP);
63+
count = call_pv("Session::Token::_get_seed", G_SCALAR);
64+
65+
SPAGAIN;
66+
67+
if (count != 1) croak("_get_seed didn't return a seed");
68+
69+
seed = POPs;
70+
seedp = SvPV(seed, len);
71+
72+
if (len != 1024) croak("unexpected seed length: %lu", len);
73+
74+
seed_ctx(ctx, seedp);
75+
76+
PUTBACK;
77+
FREETMPS;
78+
LEAVE;
79+
}
80+
2681
static inline int get_new_byte(struct session_token_ctx *ctx) {
2782
int output;
2883

@@ -57,12 +112,19 @@ MODULE = Session::Token PACKAGE = Session::Token
57112

58113
PROTOTYPES: ENABLE
59114

115+
BOOT:
116+
#ifndef _WIN32
117+
if (pthread_atfork(NULL, NULL, session_token_atfork_child) != 0)
118+
croak("unable to register pthread_atfork handler");
119+
#endif
120+
60121

61122
Session_Token
62-
_new_context(seed, alphabet, token_length)
123+
_new_context(seed, alphabet, token_length, reseed_on_fork)
63124
SV *seed
64125
SV *alphabet
65126
size_t token_length
127+
int reseed_on_fork
66128
CODE:
67129
struct session_token_ctx *ctx;
68130
char *seedp;
@@ -80,9 +142,9 @@ _new_context(seed, alphabet, token_length)
80142
ctx = malloc(sizeof(struct session_token_ctx));
81143
memset(ctx, '\0', sizeof(struct session_token_ctx));
82144

83-
memcpy(&ctx->isaac_ctx.randrsl, seedp, 1024);
84-
randinit(&ctx->isaac_ctx, TRUE);
85-
isaac(&ctx->isaac_ctx);
145+
seed_ctx(ctx, seedp);
146+
147+
ctx->reseed_on_fork = reseed_on_fork;
86148

87149
ctx->alphabet_length = SvCUR(alphabet);
88150
ctx->alphabet = malloc(ctx->alphabet_length);
@@ -114,6 +176,9 @@ get(ctx)
114176
char *outputp;
115177
size_t i, curr;
116178

179+
if (ctx->reseed_on_fork && ctx->seed_generation != fork_generation)
180+
reseed_ctx(ctx);
181+
117182
output = newSVpvn("", 0);
118183
SvGROW(output, ctx->token_length + 1);
119184
SvCUR_set(output, ctx->token_length);

lib/Session/Token.pm

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -42,25 +42,8 @@ sub new {
4242
$seed = $args{seed};
4343
}
4444

45-
if (!defined $seed) {
46-
if ($is_windows) {
47-
my $windows_rng_source = Crypt::Random::Source::Strong::Win32->new;
48-
$seed = $windows_rng_source->get(1024);
49-
die "Win32 RNG source didn't provide 1024 bytes" unless length($seed) == 1024;
50-
} else {
51-
my ($fh, $err1, $err2);
52-
53-
open($fh, '<:raw', '/dev/urandom') || ($err1 = $!);
54-
open($fh, '<:raw', '/dev/arandom') || ($err2 = $!)
55-
unless defined $fh;
56-
57-
if (!defined $fh) {
58-
croak "unable to open /dev/urandom ($err1) or /dev/arandom ($err2)";
59-
}
60-
61-
sysread($fh, $seed, 1024) == 1024 || croak "unable to read from random device: $!";
62-
}
63-
}
45+
$seed = _get_seed()
46+
if !defined $seed;
6447

6548

6649
## Init alphabet
@@ -90,7 +73,34 @@ sub new {
9073
}
9174

9275

93-
return _new_context($seed, $alphabet, $token_length);
76+
## Don't re-seed after fork if using a custom seed
77+
78+
return _new_context($seed, $alphabet, $token_length, defined $args{seed} ? 0 : 1);
79+
}
80+
81+
82+
sub _get_seed {
83+
my $seed;
84+
85+
if ($is_windows) {
86+
my $windows_rng_source = Crypt::Random::Source::Strong::Win32->new;
87+
$seed = $windows_rng_source->get(1024);
88+
die "Win32 RNG source didn't provide 1024 bytes" unless length($seed) == 1024;
89+
} else {
90+
my ($fh, $err1, $err2);
91+
92+
open($fh, '<:raw', '/dev/urandom') || ($err1 = $!);
93+
open($fh, '<:raw', '/dev/arandom') || ($err2 = $!)
94+
unless defined $fh;
95+
96+
if (!defined $fh) {
97+
croak "unable to open /dev/urandom ($err1) or /dev/arandom ($err2)";
98+
}
99+
100+
sysread($fh, $seed, 1024) == 1024 || croak "unable to read from random device: $!";
101+
}
102+
103+
return $seed;
94104
}
95105

96106

@@ -144,9 +154,9 @@ When a Session::Token object is created, 1024 bytes are read from C</dev/urandom
144154
145155
Once a generator is created, you can repeatedly call the C<get> method on the generator object and it will return a new token each time.
146156
147-
B<IMPORTANT>: If your application calls C<fork>, make sure that any generators are re-created in one of the processes after the fork since forking will duplicate the generator state and both parent and child processes will go on to produce identical tokens (just like perl's L<rand> after it is seeded).
157+
If your application calls C<fork>, generators will detect this and re-seed themselves from the kernel before producing another token, so parent and child processes will not go on to produce identical tokens (as happens with perl's L<rand> after it is seeded).
148158
149-
After the generator context is created, no system calls are used to generate tokens. This is one way that Session::Token helps with efficiency. However, this is only important for certain use cases (generally not web sessions).
159+
After the generator context is created, no system calls are used to generate tokens (except to re-seed after a fork). This is one way that Session::Token helps with efficiency. However, this is only important for certain use cases (generally not web sessions).
150160
151161
ISAAC is a cryptographically secure PRNG that improves on the well-known RC4 algorithm in some important areas. For instance, it doesn't have short cycles or initial bias like RC4 does. A theoretical shortest possible cycle in ISAAC is C<2**40>, although no cycles this short have ever been found (and probably don't exist at all). On average, ISAAC cycles are C<2**8295>.
152162
@@ -376,6 +386,8 @@ This is done in the test-suite to compare against Jenkins' reference ISAAC outpu
376386
377387
One valid reason for manually seeding is if you have some reason to believe that there isn't enough entropy in your kernel's randomness pool and therefore you don't trust C</dev/urandom>. In this case you should acquire your own seed data from somewhere trustworthy (maybe C</dev/random> or a previously stored trusted seed).
378388
389+
Generators created with a custom seed are not re-seeded after a C<fork>.
390+
379391
380392
381393
@@ -416,8 +428,6 @@ It supports all the options of this module via command line parameters, and mult
416428
417429
Should check for biased alphabets and print warnings.
418430
419-
Would be cool if it could detect forks and warn or re-seed in the child process (without incurring C<getpid> overhead).
420-
421431
There is currently no way to extract the seed from a Session::Token object. Note when implementing this: The saved seed must either store the current state of the ISAAC round as well as the 1024 byte C<randsl> array or else do some kind of minimum fast forwarding in order to protect against a partially duplicated output-stream bug.
422432
423433
Doesn't work on perl 5.6 and below due to the use of C<:raw> (thanks CPAN testers). It could probably use C<binmode> instead, but meh.

t/fork.t

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
use Session::Token;
2+
3+
## The point of this test is to verify that generators detect forks
4+
## and re-seed themselves in the child process so that parent and
5+
## child don't produce identical token streams. Generators with
6+
## custom seeds are deterministic and must not be re-seeded.
7+
8+
use strict;
9+
10+
use Test::More;
11+
12+
plan skip_all => 'fork test requires a real fork()'
13+
if $^O =~ /mswin/i;
14+
15+
plan tests => 6;
16+
17+
18+
sub tokens_from_child {
19+
my ($gen, $n) = @_;
20+
21+
pipe(my $rd, my $wr) || die "unable to pipe: $!";
22+
23+
my $pid = fork;
24+
die "unable to fork: $!" if !defined $pid;
25+
26+
if ($pid == 0) {
27+
close($rd);
28+
print $wr $gen->get, "\n" for 1 .. $n;
29+
close($wr);
30+
exit 0;
31+
}
32+
33+
close($wr);
34+
my @tokens = <$rd>;
35+
close($rd);
36+
waitpid($pid, 0);
37+
38+
chomp @tokens;
39+
die "child produced " . scalar(@tokens) . " tokens instead of $n" if @tokens != $n;
40+
41+
return @tokens;
42+
}
43+
44+
sub all_unique {
45+
my %seen;
46+
$seen{$_} = 1 for @_;
47+
return scalar(keys %seen) == scalar(@_);
48+
}
49+
50+
51+
{
52+
my $gen = Session::Token->new;
53+
$gen->get;
54+
55+
my @child = tokens_from_child($gen, 20);
56+
my @parent = map { $gen->get } 1 .. 20;
57+
58+
ok(all_unique(@child, @parent), "child re-seeds after fork");
59+
}
60+
61+
62+
{
63+
my $gen = Session::Token->new;
64+
65+
my @child = tokens_from_child($gen, 20);
66+
my @parent = map { $gen->get } 1 .. 20;
67+
68+
ok(all_unique(@child, @parent), "child re-seeds when generator was never used before fork");
69+
}
70+
71+
72+
{
73+
my $gen = Session::Token->new;
74+
75+
my @child1 = tokens_from_child($gen, 20);
76+
my @child2 = tokens_from_child($gen, 20);
77+
my @parent = map { $gen->get } 1 .. 20;
78+
79+
ok(all_unique(@child1, @child2, @parent), "sequential children re-seed independently");
80+
}
81+
82+
83+
{
84+
my $gen = Session::Token->new;
85+
86+
pipe(my $rd, my $wr) || die "unable to pipe: $!";
87+
88+
my $pid = fork;
89+
die "unable to fork: $!" if !defined $pid;
90+
91+
if ($pid == 0) {
92+
close($rd);
93+
my @grandchild = tokens_from_child($gen, 20);
94+
print $wr "$_\n" for @grandchild, map { $gen->get } 1 .. 20;
95+
close($wr);
96+
exit 0;
97+
}
98+
99+
close($wr);
100+
chomp(my @lines = <$rd>);
101+
close($rd);
102+
waitpid($pid, 0);
103+
104+
is(scalar(@lines), 40, "received all tokens from forked processes");
105+
106+
my @parent = map { $gen->get } 1 .. 20;
107+
108+
ok(all_unique(@lines, @parent), "grandchild re-seeds too");
109+
}
110+
111+
112+
{
113+
my $gen = Session::Token->new(seed => "\x00" x 1024);
114+
my $mirror = Session::Token->new(seed => "\x00" x 1024);
115+
116+
$gen->get;
117+
$mirror->get;
118+
119+
my ($child_token) = tokens_from_child($gen, 1);
120+
121+
is($child_token, $mirror->get, "custom seeded generator is not re-seeded after fork");
122+
}

0 commit comments

Comments
 (0)