From 1beb4555768f063a9027ccf38c72353751447fa4 Mon Sep 17 00:00:00 2001 From: Arseniy Alekseyev Date: Tue, 6 Feb 2024 11:09:06 +0000 Subject: [PATCH 1/2] Add support for precise absolute tapered start time Adding two variables, specifying the beginning and the end of a tapered start interval, measured in nanoseconds since epoch: ``` FAKETIME_TAPER_BEGIN_NSEC_SINCE_EPOCH FAKETIME_TAPER_END_NSEC_SINCE_EPOCH ``` The behavior these implement is similar to what `FAKETIME_START_AFTER_SECONDS` does, with a few key differences: * timestamp is absolute, instead of being relative to process startup * timestamp is specified in nanoseconds * conversion interacts correctly with `utimes` family of functions * conversion is tapered (see below), which makes mapping reversible (up to loss of precision) The reason we want this feature is the following use case. We run a large test suite under faketime. That test suite has access to filesystem artifacts that were created prior to test start up. Among those artifacts are some caches which are considered up to date iff the timestamps of the files match what's recorded in a data structure. This means that for those caches to be considered valid we need their timestamps to not be fake. The reason we can't use `FAKETIME_START_AFTER_SECONDS` directly is that the test suite consists of multiple processes, for those processes to correctly interact with each other they need a consistent timestamp mapping that is shared between them. In fact the simplest bash script already behaves incorrectly because the commands use different process start times. ``` touch old LD_PRELOAD=... FAKETIME=+100d FAKETIME_START_AFTER_SECONDS=0 bash -c 'touch new; stat old new' ``` The expected behavior is that the timestamp of `old` is not rewritten, while the timestamp of `new` is rewritten. That is in fact achievable now: ``` FAKETIME_TAPER_BEGIN_NSEC_SINCE_EPOCH=$(date +%s%N) sleep 0.1 FAKETIME_TAPER_END_NSEC_SINCE_EPOCH=$(date +%s%N) FAKETIME=+100d FAKETIME_KEEP_BEFORE_NSEC_SINCE_EPOCH="$now_ns" bash -c 'touch new; stat old new' ``` What is tapering and why do we need it? The idea is to make the time transition smooth instead of abrupt, gradually increasing the offset amount from the start to the end of the tapering interval. The reason we want this is to make the time mapping reversible (up to some loss of precision). This means some programs that interact with the file system will no longer be confused. For example, if you do the equivalent of `touch -d "3 days ago"` and then read back the timestamp, you'll get approximately the expected timestamp, instead of something completely off. --- src/libfaketime.c | 183 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 181 insertions(+), 2 deletions(-) diff --git a/src/libfaketime.c b/src/libfaketime.c index 3acd571..f7cc438 100644 --- a/src/libfaketime.c +++ b/src/libfaketime.c @@ -340,6 +340,11 @@ static int outfile = -1; static bool limited_faking = false; static long callcounter = 0; + +static bool ft_taper = 0; +static long long ft_taper_begin_nsec_since_epoch = -1; +static long long ft_taper_end_nsec_since_epoch = -1; + static long ft_start_after_secs = -1; static long ft_stop_after_secs = -1; static long ft_start_after_ncalls = -1; @@ -1156,6 +1161,133 @@ int statx(int dirfd, const char *pathname, int flags, unsigned int mask, struct } #endif +static long long faketime_offset_ns() +{ + return ((long long)user_offset.tv_sec) * 1000000000 + (long long)user_offset.tv_nsec; +} + +// (a * b + c) / d, but do something to avoid multiplication overflow +// (only "mostly" prevents overflow, not if the values are already +// over 2^62) +// +// precondition: a < d, r < d +static long long mad_div_avoid_overflow_inner(long long a, long long b, long long c, long long d) +{ + if (a <= 1 || b <= 1) { + return (a * b + c) / d; + } + // how many multiples of [a] we need to make a full [d] + long long k = (d + a - 1) / a; // 2 <= k <= d/2+1 + long long rem_per_f = a * k - d; + // how many occurrences of [a*k] (and therefore [d]) we can + // easily take from [a*b] + long long f = b / k; + // remaining: rem_per_f * f + (b % k) * a + c + long long rem = (b % k) * a + c; + long long res = f + rem / d; + rem = rem % d; + return res + mad_div_avoid_overflow_inner(rem_per_f, f, rem, d); +} + +static long long mult_div_avoid_overflow(long long a, long long b, long long d) +{ + long long res = a / d; + return res + mad_div_avoid_overflow_inner(a%d, b, 0, d); +} + +static long long faketime_do_tapered_transform_gen(long long t, long long taper_begin, long long taper_end, long long offset) +{ + if (t <= taper_begin) + { + return t; + } + if (t >= taper_end) + { + return t + offset; + } + long long w = taper_end - taper_begin; + long long h = w + offset; + // [h] output time passes over [w] input time + long long dt1 = t - taper_begin; + // so we're looking for dt1 * h / w + long long dt2 = mult_div_avoid_overflow(dt1, h, w); + return taper_begin + dt2; +} + +static long long faketime_do_tapered_offset(long long t) +{ + if (!ft_taper) + { + return t + faketime_offset_ns(); + } + return faketime_do_tapered_transform_gen(t, ft_taper_begin_nsec_since_epoch, ft_taper_end_nsec_since_epoch, faketime_offset_ns()); +} + +static long long faketime_undo_tapered_offset(long long t) +{ + if (!ft_taper) + { + return t - faketime_offset_ns(); + } + long long offset = faketime_offset_ns(); + return faketime_do_tapered_transform_gen(t, ft_taper_begin_nsec_since_epoch, ft_taper_end_nsec_since_epoch + offset, -offset); +} + +static void faketime_div_mod(long long ns, long long d, long long *res, long long *rem) +{ + *res = ns / d; + *rem = ns % d; + if(*rem < 0) { + (*rem) += d; + (*res)--; + } +} + +static struct timeval faketime_undo_tapered_offset_timeval(struct timeval t) +{ + long long nsec = ((long long)t.tv_sec) * 1000000000 + ((long long)t.tv_usec) * 1000; + nsec = faketime_undo_tapered_offset(nsec); + long long sec; + long long nsec_rem; + faketime_div_mod(nsec, 1000000000, &sec, &nsec_rem); + t.tv_sec = sec; + t.tv_usec = nsec_rem / 1000; + return t; +} + +static struct timespec faketime_undo_tapered_offset_timespec(struct timespec t) +{ + long long nsec = ((long long)t.tv_sec) * 1000000000 + ((long long)t.tv_nsec); + nsec = faketime_undo_tapered_offset(nsec); + long long sec; + long long nsec_rem; + faketime_div_mod(nsec, 1000000000, &sec, &nsec_rem); + t.tv_sec = sec; + t.tv_nsec = nsec_rem; + return t; +} + +static struct timespec faketime_do_tapered_offset_timespec(struct timespec t) +{ + long long nsec = ((long long)t.tv_sec) * 1000000000 + ((long long)t.tv_nsec); + nsec = faketime_do_tapered_offset(nsec); + long long sec; + long long nsec_rem; + faketime_div_mod(nsec, 1000000000, &sec, &nsec_rem); + t.tv_sec = sec; + t.tv_nsec = nsec_rem; + return t; +} + +// the precision here will be really atrocious, +// if tapering compresses time significantly +static int faketime_undo_tapered_offset_sec(int t) +{ + long long nsec = ((long long)t) * 1000000000; + nsec = faketime_undo_tapered_offset(nsec); + return nsec / 1000000000; +} + #ifdef FAKE_FILE_TIMESTAMPS #ifdef MACOS_DYLD_INTERPOSE int macos_utime(const char *filename, const struct utimbuf *times) @@ -1180,6 +1312,13 @@ int utime(const char *filename, const struct utimbuf *times) { ntbuf.actime = times->actime - user_offset.tv_sec; ntbuf.modtime = times->modtime - user_offset.tv_sec; + + if (ft_taper) + { + ntbuf.actime = faketime_undo_tapered_offset_sec(times->actime); + ntbuf.modtime = faketime_undo_tapered_offset_sec(times->modtime); + } + times = &ntbuf; } #ifdef MACOS_DYLD_INTERPOSE @@ -1217,6 +1356,11 @@ int utimes(const char *filename, const struct timeval times[2]) user_offset2.tv_usec = user_offset.tv_nsec / 1000; timersub(×[0], &user_offset2, &tn[0]); timersub(×[1], &user_offset2, &tn[1]); + if (ft_taper) + { + tn[0] = faketime_undo_tapered_offset_timeval(times[0]); + tn[1] = faketime_undo_tapered_offset_timeval(times[1]); + } times = tn; } #ifdef MACOS_DYLD_INTERPOSE @@ -1261,6 +1405,11 @@ static void fake_two_timespec(const struct timespec in_times[2], struct timespec else { timersub2(&in_times[j], &user_offset, &out_times[j], n); + if (ft_taper) + { + out_times[j] = faketime_undo_tapered_offset_timespec(in_times[j]); + } + } } } @@ -2897,6 +3046,24 @@ static void ftpl_really_init(void) } } + if ((tmp_env = getenv("FAKETIME_TAPER_BEGIN_NSEC_SINCE_EPOCH")) != NULL) + { + ft_taper_begin_nsec_since_epoch = atoll(tmp_env); + if(!ft_taper) + { + ft_taper = true; + ft_taper_end_nsec_since_epoch = ft_taper_begin_nsec_since_epoch; + } + } + if ((tmp_env = getenv("FAKETIME_TAPER_END_NSEC_SINCE_EPOCH")) != NULL) + { + ft_taper_end_nsec_since_epoch = atoll(tmp_env); + if(!ft_taper) + { + ft_taper = true; + ft_taper_begin_nsec_since_epoch = ft_taper_end_nsec_since_epoch; + } + } if ((tmp_env = getenv("FAKETIME_START_AFTER_SECONDS")) != NULL) { ft_start_after_secs = atol(tmp_env); @@ -3220,7 +3387,6 @@ int fake_clock_gettime(clockid_t clk_id, struct timespec *tp) timespecsub(tp, &ftpl_starttime.real, &tmp_ts); break; } - if (limited_faking) { /* Check whether we actually should be faking the returned timestamp. */ @@ -3373,7 +3539,20 @@ int fake_clock_gettime(clockid_t clk_id, struct timespec *tp) { timeadj = tdiff; } - timespecadd(&user_faked_time_timespec, &timeadj, tp); + + if(ft_taper) + { + // Tapered offset time transition ignores the speed up/slow + // down setting. Someone should figure out how to do speed + // up / slow down correctly in that case (ideally, a global + // speed-up, as a part of a global time mapping routine, not + // relying on a per-process state). + *tp = faketime_do_tapered_offset_timespec(*tp); + } + else + { + timespecadd(&user_faked_time_timespec, &timeadj, tp); + } } break; From 3ce11e5071c90d997fedaa1d1b53a0b47be3fb07 Mon Sep 17 00:00:00 2001 From: Arseniy Alekseyev Date: Fri, 9 Feb 2024 18:27:50 +0000 Subject: [PATCH 2/2] add a simple mult_div_avoid_overflow implementation --- src/libfaketime.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/libfaketime.c b/src/libfaketime.c index f7cc438..955b868 100644 --- a/src/libfaketime.c +++ b/src/libfaketime.c @@ -1166,6 +1166,20 @@ static long long faketime_offset_ns() return ((long long)user_offset.tv_sec) * 1000000000 + (long long)user_offset.tv_nsec; } +// figure out what combination of ifdefs is appropriate, or use +// this unconditionally +#if 1 + +static long long mult_div_avoid_overflow(long long a, long long b, long long d) +{ + __int128_t acc = a; + acc *= b; + acc /= d; + return (long long)acc; +} + +#else + // (a * b + c) / d, but do something to avoid multiplication overflow // (only "mostly" prevents overflow, not if the values are already // over 2^62) @@ -1195,6 +1209,8 @@ static long long mult_div_avoid_overflow(long long a, long long b, long long d) return res + mad_div_avoid_overflow_inner(a%d, b, 0, d); } +#endif + static long long faketime_do_tapered_transform_gen(long long t, long long taper_begin, long long taper_end, long long offset) { if (t <= taper_begin)