From nobody Sun Feb 8 17:04:36 2026 Received: from desiato.infradead.org (desiato.infradead.org [90.155.92.199]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 024FC33A6EC for ; Fri, 19 Dec 2025 18:51:45 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=90.155.92.199 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1766170311; cv=none; b=VHgkm7xp9tU9UvGrGm0xud7YjhrsT7mVaZcfLPJSZwZwy/oS3n0hyzhEi60j5kCo2K2P0A+OyFcVE9HnBDheO4NIjzkwGVPK2tVkJuzBzWWmt5oTjbhxZbxLHbt/rr/LO4OK1jmKmaoVAWIwqZ6UUExoEioJH6SVRP3nzm+QVO8= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1766170311; c=relaxed/simple; bh=Ea8zMn5Z6A44/+zb4XzlTgGeI1n9J1SY9L8kvJzf4uY=; h=Message-ID:Subject:From:To:Cc:Date:Content-Type:MIME-Version; b=rE+TrM09OMw5skRadSWi2JPvLbvYCjrx89DRKT7hOPM7ynb1COjxiuZWVd7f4QKHxFt8e05pRzEz13IUo1jC8jVLKXXR2vrKV5lDn9rzUkK2jAHVqBTXbkBA+qg8BYdYnet1hT883c+CjvbE2rS9sEXueLrwWRDDDuPIAHPC5Y8= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=infradead.org; spf=none smtp.mailfrom=desiato.srs.infradead.org; dkim=pass (2048-bit key) header.d=infradead.org header.i=@infradead.org header.b=JDiJ+U0O; arc=none smtp.client-ip=90.155.92.199 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=infradead.org Authentication-Results: smtp.subspace.kernel.org; spf=none smtp.mailfrom=desiato.srs.infradead.org Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=infradead.org header.i=@infradead.org header.b="JDiJ+U0O" DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=infradead.org; s=desiato.20200630; h=MIME-Version:Content-Type:Date:Cc:To: From:Subject:Message-ID:Sender:Reply-To:Content-Transfer-Encoding:Content-ID: Content-Description:In-Reply-To:References; bh=CKx/qc8yCgZk+nkeQveu52CgyCazG+FmtZ1p3J0pBT4=; b=JDiJ+U0Ow5/rU8Ya587317/omX THNfj2boMhNLmLpcOvqab8M2a8BbbZuG3G3O2iJU+JSPPC93KqURpgBEBHrln8n/JQ7X5nk6cH7oV YOZ4kMQ8Sf9VKgX+Q7jgqsAh43iVYcMbbYcu06OfEHoomQbuibOChBTHvZkSrsHxWuV8cQDnfF4xW 5Qm7iEC30jh4OlKAGMFZRm04qR3K4O7gKJEDlV7QwyCthd6Acde+68o3XLxC8eokyrTaD5/ebTNG1 ZB4QzmBkNHNFZ0OR/sw2mAwi5Dh+mJEtX3DXFyXXtwAnDKtOH5YTa+yIQCrlhKv1wv4UKHwhmWFnH 8g8ria+A==; Received: from [172.31.31.148] (helo=u09cd745991455d.ant.amazon.com) by desiato.infradead.org with esmtpsa (Exim 4.98.2 #2 (Red Hat Linux)) id 1vWeiF-0000000BUBK-3J3O; Fri, 19 Dec 2025 17:56:23 +0000 Message-ID: <87cb97d5a26d0f4909d2ba2545c4b43281109470.camel@infradead.org> Subject: [RFC] ptp: ptp_vmclock: Add simulated 1PPS support From: David Woodhouse To: Rodolfo Giometti , linuxpps@ml.enneenne.com, John Stultz , Thomas Gleixner , Stephen Boyd Cc: "Luu, Ryan" , "Ridoux, Julien" , linux-kernel Date: Fri, 19 Dec 2025 18:51:37 +0000 Content-Type: multipart/signed; micalg="sha-256"; protocol="application/pkcs7-signature"; boundary="=-wUEtl6af5mz+nqtxRPij" User-Agent: Evolution 3.52.3-0ubuntu1.1 Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-SRS-Rewrite: SMTP reverse-path rewritten from by desiato.infradead.org. See http://www.infradead.org/rpr.html --=-wUEtl6af5mz+nqtxRPij Content-Transfer-Encoding: quoted-printable MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" VMClock https://uapi-group.org/specifications/specs/vmclock/ is a live migration safe clock designed for virtual machines. It provides clients with a direct mathematical relationship between their CPU counter (TSC, etc.) and accurate real time. This means that we don't have to have hundreds of guests on the same host all duplicating the work of calibrating the *same* underlying hardware counter against an external source, the the added bonus of steal time in the mix. We'll work on *exporting* the kernel's CLOCK_REALTIME to KVM guests separately; this part is about *consuming* VMClock as a guest. Ideally, we'd be able to consume it as directly as possible into kernel timekeeping, especially for microvms. This is a first attempt that just makes it possible for the kernel to consume it as a 1PPS signal. It uses an hrtimer, set to fire when the *vmclock* time reaches the top of the second. Then it calculates what the counter (TSC) *would* have been at the start of the current second, and provides real/raw timestamps for a PPS event based on that counter value. I can boot a test kernel, enable PPS on the /dev/ptp0 device with the=20 PTP_ENABLE_PPS ioctl (is there a standard tool which does that?), then bind the PPS kernel consumer (ppsctl -a -b /dev/pps0), tell the kernel to sync to it (adjtimex --status 6), and I start getting nice clean PPS signals... [root@localhost ~]# ppstest /dev/pps0 trying PPS source "/dev/pps0" found PPS source "/dev/pps0" ok, found 1 source(s), now start fetching data... source 0 - assert 1766169532.000000000, sequence: 1841 - clear 0.000000000= , sequence: 0 source 0 - assert 1766169533.000000000, sequence: 1842 - clear 0.000000000= , sequence: 0 source 0 - assert 1766169534.000000000, sequence: 1843 - clear 0.000000000= , sequence: 0 source 0 - assert 1766169535.000000000, sequence: 1844 - clear 0.000000000= , sequence: 0 source 0 - assert 1766169536.000000000, sequence: 1845 - clear 0.000000000= , sequence: 0 So... does this make sense? It fixes the phase and frequency of the clock but doesn't consume the actual time. And it doesn't work with a NOHZ kernel... well, actually if I remove that check in KConfig it *does* work with a certain amount of jitter; is that expected? Is this worth pursuing, or should I jump straight to trying to consume the information from VMClock directly into the kernel's timekeeping? From 047cc14ab128f6cda3aa400bad052c48303a8fd0 Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Thu, 18 Dec 2025 19:58:58 +0000 Subject: [PATCH] ptp: ptp_vmclock: Add simulated 1PPS support The cleanest way to synchronise the kernel against vmclock is to simulate a 1PPS signal. Set up an hrtimer to run every second, and tweak the vmclock_get_crosststamp() function to be able to return the cycle counter and corresponding time at the *start* of the current second, because the hardpps handling expects that the { real, raw } timestamps it's given for phase and frequency adjustment are the kernel's clock readings when the *true* time is at the top of a second (i.e. when the pulse arrives). Signed-off-by: David Woodhouse --- drivers/ptp/ptp_vmclock.c | 200 +++++++++++++++++++++++++++++++++++--- 1 file changed, 189 insertions(+), 11 deletions(-) diff --git a/drivers/ptp/ptp_vmclock.c b/drivers/ptp/ptp_vmclock.c index deab3205601b..f5b7f3333076 100644 --- a/drivers/ptp/ptp_vmclock.c +++ b/drivers/ptp/ptp_vmclock.c @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -50,6 +51,10 @@ struct vmclock_state { enum clocksource_ids cs_id, sys_cs_id; int index; char *name; + struct hrtimer pps_timer; + bool pps_enabled; + struct system_time_snapshot history_snap; + bool history_valid; }; =20 #define VMCLOCK_MAX_WAIT ms_to_ktime(100) @@ -97,11 +102,14 @@ static bool tai_adjust(struct vmclock_abi *clk, uint64= _t *sec) static int vmclock_get_crosststamp(struct vmclock_state *st, struct ptp_system_timestamp *sts, struct system_counterval_t *system_counter, - struct timespec64 *tspec) + struct timespec64 *tspec, + bool on_second) { ktime_t deadline =3D ktime_add(ktime_get(), VMCLOCK_MAX_WAIT); struct system_time_snapshot systime_snapshot; uint64_t cycle, delta, seq, frac_sec; + uint64_t period_frac_sec; + uint8_t period_shift; =20 #ifdef CONFIG_X86 /* @@ -124,6 +132,10 @@ static int vmclock_get_crosststamp(struct vmclock_stat= e *st, if (st->clk->clock_status =3D=3D VMCLOCK_STATUS_UNRELIABLE) return -EINVAL; =20 + /* Load reused vmclock parameters */ + period_frac_sec =3D le64_to_cpu(st->clk->counter_period_frac_sec); + period_shift =3D st->clk->counter_period_shift; + /* * When invoked for gettimex64(), fill in the pre/post system * times. The simple case is when system time is based on the @@ -153,10 +165,35 @@ static int vmclock_get_crosststamp(struct vmclock_sta= te *st, delta =3D cycle - le64_to_cpu(st->clk->counter_value); =20 frac_sec =3D mul_u64_u64_shr_add_u64(&tspec->tv_sec, delta, - le64_to_cpu(st->clk->counter_period_frac_sec), - st->clk->counter_period_shift, + period_frac_sec, period_shift, le64_to_cpu(st->clk->time_frac_sec)); - tspec->tv_nsec =3D mul_u64_u64_shr(frac_sec, NSEC_PER_SEC, 64); + + /* For simulated PPS, adjust to the most recent second boundary */ + if (on_second) { + uint64_t delta_cycles; + int frac_shift, shift_remain; + + if (tspec->tv_sec =3D=3D 0) + return -EAGAIN; /* No second boundary crossed yet */ + + /* Shift frac_sec left until top bit is set */ + frac_shift =3D __builtin_clzll(frac_sec); + frac_sec <<=3D frac_shift; + + /* Shift period right by remaining bits from counter_period_shift */ + shift_remain =3D period_shift - frac_shift; + if (shift_remain > 0) + period_frac_sec >>=3D shift_remain; + else + frac_sec >>=3D -shift_remain; + + delta_cycles =3D frac_sec / period_frac_sec; + cycle -=3D delta_cycles; + tspec->tv_nsec =3D 0; + } else { + tspec->tv_nsec =3D mul_u64_u64_shr(frac_sec, NSEC_PER_SEC, 64); + } + tspec->tv_sec +=3D le64_to_cpu(st->clk->time_sec); =20 if (!tai_adjust(st->clk, &tspec->tv_sec)) @@ -197,7 +234,8 @@ static int vmclock_get_crosststamp(struct vmclock_state= *st, static int vmclock_get_crosststamp_kvmclock(struct vmclock_state *st, struct ptp_system_timestamp *sts, struct system_counterval_t *system_counter, - struct timespec64 *tspec) + struct timespec64 *tspec, + bool on_second) { struct pvclock_vcpu_time_info *pvti =3D this_cpu_pvti(); unsigned int pvti_ver; @@ -208,7 +246,7 @@ static int vmclock_get_crosststamp_kvmclock(struct vmcl= ock_state *st, do { pvti_ver =3D pvclock_read_begin(pvti); =20 - ret =3D vmclock_get_crosststamp(st, sts, system_counter, tspec); + ret =3D vmclock_get_crosststamp(st, sts, system_counter, tspec, on_secon= d); if (ret) break; =20 @@ -244,10 +282,10 @@ static int ptp_vmclock_get_time_fn(ktime_t *device_ti= me, #ifdef SUPPORT_KVMCLOCK if (READ_ONCE(st->sys_cs_id) =3D=3D CSID_X86_KVM_CLK) ret =3D vmclock_get_crosststamp_kvmclock(st, NULL, system_counter, - &tspec); + &tspec, false); else #endif - ret =3D vmclock_get_crosststamp(st, NULL, system_counter, &tspec); + ret =3D vmclock_get_crosststamp(st, NULL, system_counter, &tspec, false); =20 if (!ret) *device_time =3D timespec64_to_ktime(tspec); @@ -284,6 +322,109 @@ static int ptp_vmclock_getcrosststamp(struct ptp_cloc= k_info *ptp, return ret; } =20 +static int ptp_vmclock_get_time_fn_pps(ktime_t *device_time, + struct system_counterval_t *system_counter, + void *ctx) +{ + struct vmclock_state *st =3D ctx; + struct timespec64 tspec; + int ret; + +#ifdef SUPPORT_KVMCLOCK + if (st->history_valid && st->history_snap.cs_id =3D=3D CSID_X86_KVM_CLK) + ret =3D vmclock_get_crosststamp_kvmclock(st, NULL, system_counter, + &tspec, true); + else +#endif + ret =3D vmclock_get_crosststamp(st, NULL, system_counter, &tspec, true); + + if (!ret) + *device_time =3D timespec64_to_ktime(tspec); + + return ret; +} + +/* + * Generate simulated PPS events for feeding __hardpps(), which expects to + * be given both CLOCK_REALTIME and CLOCK_MONOTONIC_RAW values when a 1PPS + * signal actually happened (i.e. at the top of a second). + * + * Use vmclock_get_crosststamp() to read the vmclock and both CLOCK_REALTI= ME + * and CLOCK_MONOTONIC_RAW system clocks all from the same TSC value. + * + * Look at the nanoseconds field of the true clock reading from vmclock. If + * it's sufficiently close to a second boundary, subtract that nanosecond + * value from both system clocks and simulate a 1PPS event with those time= s. + * + * Strictly speaking, it would be nicer if we could determine the value of + * the cycle counter which would have resulted in the vmclock reporting ze= ro + * nanoseconds, and then calculate CLOCK_REALTIME and CLOCK_MONOTONIC_RAW + * using that cycle counter value. But this is good enough. + * + * Finally, the timer reschedules itself to occur at the top of the next + * second according to vmclock, *not* necessarily CLOCK_REALTIME. + */ +static enum hrtimer_restart ptp_vmclock_pps_timer(struct hrtimer *timer) +{ + struct vmclock_state *st =3D container_of(timer, struct vmclock_state, pp= s_timer); + struct system_device_crosststamp xtstamp; + struct ptp_clock_event event; + ktime_t next; + s64 delta_ns; + int ret; + + if (!st->pps_enabled) + return HRTIMER_NORESTART; + + /* Only report PPS if we have a valid history */ + ret =3D -EINVAL; + if (st->history_valid) { + /* Use historical interpolation to get exact timestamps at second bounda= ry */ + ret =3D get_device_system_crosststamp(ptp_vmclock_get_time_fn_pps, st, + &st->history_snap, &xtstamp); + if (!ret) { + event.type =3D PTP_CLOCK_PPSUSR; + event.pps_times.ts_real =3D ktime_to_timespec64(xtstamp.sys_realtime); +#ifdef CONFIG_NTP_PPS + event.pps_times.ts_raw =3D ktime_to_timespec64(xtstamp.sys_monoraw); +#endif + ptp_clock_event(st->ptp_clock, &event); + } + } + + /* Capture snapshot for next iteration */ + ktime_get_snapshot(&st->history_snap); + st->history_valid =3D true; + + /* + * Schedule the next timer to occur at the top of the next second + * according to *vmclock*, not necessarily according to the kernel's + * CLOCK_REALTIME. + * + * If we successfully reported a PPS event, xtstamp.sys_realtime is + * already at the second boundary, so just add 1 second. + * + * Otherwise, get the current vmclock time and calculate when it will + * next reach a second boundary. + */ + if (!ret) { + next =3D ktime_add_ns(xtstamp.sys_realtime, NSEC_PER_SEC); + } else { + struct timespec64 ts; + + /* No valid result. Is vmclock even working? */ + if (vmclock_get_crosststamp(st, NULL, NULL, &ts, false)) + return HRTIMER_NORESTART; + + delta_ns =3D NSEC_PER_SEC - ts.tv_nsec; + next =3D ktime_add_ns(st->history_snap.real, delta_ns); + } + + hrtimer_set_expires(timer, next); + + return HRTIMER_RESTART; +} + /* * PTP clock operations */ @@ -310,12 +451,43 @@ static int ptp_vmclock_gettimex(struct ptp_clock_info= *ptp, struct timespec64 *t struct vmclock_state *st =3D container_of(ptp, struct vmclock_state, ptp_clock_info); =20 - return vmclock_get_crosststamp(st, sts, NULL, ts); + return vmclock_get_crosststamp(st, sts, NULL, ts, false); } =20 static int ptp_vmclock_enable(struct ptp_clock_info *ptp, struct ptp_clock_request *rq, int on) { + struct vmclock_state *st =3D container_of(ptp, struct vmclock_state, + ptp_clock_info); + + switch (rq->type) { + case PTP_CLK_REQ_PPS: + st->pps_enabled =3D !!on; + if (on) { + struct timespec64 ts; + s64 delta_ns; + + /* Get snapshot to schedule first timer */ + ktime_get_snapshot(&st->history_snap); + st->history_valid =3D true; + + if (vmclock_get_crosststamp(st, NULL, NULL, &ts, false)) + return -EIO; + + /* Calculate when vmclock will next be at second boundary */ + delta_ns =3D NSEC_PER_SEC - ts.tv_nsec; + + /* Schedule relative to kernel's CLOCK_REALTIME */ + hrtimer_start(&st->pps_timer, ktime_add_ns(st->history_snap.real, delta= _ns), + HRTIMER_MODE_ABS); + } else { + hrtimer_cancel(&st->pps_timer); + } + return 0; + default: + break; + } + return -EOPNOTSUPP; } =20 @@ -324,7 +496,7 @@ static const struct ptp_clock_info ptp_vmclock_info =3D= { .max_adj =3D 0, .n_ext_ts =3D 0, .n_pins =3D 0, - .pps =3D 0, + .pps =3D 1, .adjfine =3D ptp_vmclock_adjfine, .adjtime =3D ptp_vmclock_adjtime, .gettimex64 =3D ptp_vmclock_gettimex, @@ -360,6 +532,9 @@ static struct ptp_clock *vmclock_ptp_register(struct de= vice *dev, st->ptp_clock_info =3D ptp_vmclock_info; strscpy(st->ptp_clock_info.name, st->name); =20 + hrtimer_setup(&st->pps_timer, ptp_vmclock_pps_timer, CLOCK_REALTIME, HRTI= MER_MODE_ABS); + st->pps_enabled =3D false; + return ptp_clock_register(&st->ptp_clock_info, dev); } =20 @@ -494,8 +669,11 @@ static void vmclock_remove(void *data) { struct vmclock_state *st =3D data; =20 - if (st->ptp_clock) + if (st->ptp_clock) { + st->pps_enabled =3D false; + hrtimer_cancel(&st->pps_timer); ptp_clock_unregister(st->ptp_clock); + } =20 if (st->miscdev.minor !=3D MISC_DYNAMIC_MINOR) misc_deregister(&st->miscdev); --=20 2.43.0 --=-wUEtl6af5mz+nqtxRPij Content-Type: application/pkcs7-signature; name="smime.p7s" Content-Disposition: attachment; filename="smime.p7s" Content-Transfer-Encoding: base64 MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwEAAKCCD9Aw ggSOMIIDdqADAgECAhAOmiw0ECVD4cWj5DqVrT9PMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYT AlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAi BgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yNDAxMzAwMDAwMDBaFw0zMTEx MDkyMzU5NTlaMEExCzAJBgNVBAYTAkFVMRAwDgYDVQQKEwdWZXJva2V5MSAwHgYDVQQDExdWZXJv a2V5IFNlY3VyZSBFbWFpbCBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMjvgLKj jfhCFqxYyRiW8g3cNFAvltDbK5AzcOaR7yVzVGadr4YcCVxjKrEJOgi7WEOH8rUgCNB5cTD8N/Et GfZI+LGqSv0YtNa54T9D1AWJy08ZKkWvfGGIXN9UFAPMJ6OLLH/UUEgFa+7KlrEvMUupDFGnnR06 aDJAwtycb8yXtILj+TvfhLFhafxroXrflspavejQkEiHjNjtHnwbZ+o43g0/yxjwnarGI3kgcak7 nnI9/8Lqpq79tLHYwLajotwLiGTB71AGN5xK+tzB+D4eN9lXayrjcszgbOv2ZCgzExQUAIt98mre 8EggKs9mwtEuKAhYBIP/0K6WsoMnQCcCAwEAAaOCAVwwggFYMBIGA1UdEwEB/wQIMAYBAf8CAQAw HQYDVR0OBBYEFIlICOogTndrhuWByNfhjWSEf/xwMB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6en IZ3zbcgPMA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDBAYIKwYBBQUHAwIweQYI KwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYIKwYB BQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RD QS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0 QXNzdXJlZElEUm9vdENBLmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQELBQADggEB ACiagCqvNVxOfSd0uYfJMiZsOEBXAKIR/kpqRp2YCfrP4Tz7fJogYN4fxNAw7iy/bPZcvpVCfe/H /CCcp3alXL0I8M/rnEnRlv8ItY4MEF+2T/MkdXI3u1vHy3ua8SxBM8eT9LBQokHZxGUX51cE0kwa uEOZ+PonVIOnMjuLp29kcNOVnzf8DGKiek+cT51FvGRjV6LbaxXOm2P47/aiaXrDD5O0RF5SiPo6 xD1/ClkCETyyEAE5LRJlXtx288R598koyFcwCSXijeVcRvBB1cNOLEbg7RMSw1AGq14fNe2cH1HG W7xyduY/ydQt6gv5r21mDOQ5SaZSWC/ZRfLDuEYwggWbMIIEg6ADAgECAhAH5JEPagNRXYDiRPdl c1vgMA0GCSqGSIb3DQEBCwUAMEExCzAJBgNVBAYTAkFVMRAwDgYDVQQKEwdWZXJva2V5MSAwHgYD VQQDExdWZXJva2V5IFNlY3VyZSBFbWFpbCBHMjAeFw0yNDEyMzAwMDAwMDBaFw0yODAxMDQyMzU5 NTlaMB4xHDAaBgNVBAMME2R3bXcyQGluZnJhZGVhZC5vcmcwggIiMA0GCSqGSIb3DQEBAQUAA4IC DwAwggIKAoICAQDali7HveR1thexYXx/W7oMk/3Wpyppl62zJ8+RmTQH4yZeYAS/SRV6zmfXlXaZ sNOE6emg8WXLRS6BA70liot+u0O0oPnIvnx+CsMH0PD4tCKSCsdp+XphIJ2zkC9S7/yHDYnqegqt w4smkqUqf0WX/ggH1Dckh0vHlpoS1OoxqUg+ocU6WCsnuz5q5rzFsHxhD1qGpgFdZEk2/c//ZvUN i12vPWipk8TcJwHw9zoZ/ZrVNybpMCC0THsJ/UEVyuyszPtNYeYZAhOJ41vav1RhZJzYan4a1gU0 kKBPQklcpQEhq48woEu15isvwWh9/+5jjh0L+YNaN0I//nHSp6U9COUG9Z0cvnO8FM6PTqsnSbcc 0j+GchwOHRC7aP2t5v2stVx3KbptaYEzi4MQHxm/0+HQpMEVLLUiizJqS4PWPU6zfQTOMZ9uLQRR ci+c5xhtMEBszlQDOvEQcyEG+hc++fH47K+MmZz21bFNfoBxLP6bjR6xtPXtREF5lLXxp+CJ6KKS blPKeVRg/UtyJHeFKAZXO8Zeco7TZUMVHmK0ZZ1EpnZbnAhKE19Z+FJrQPQrlR0gO3lBzuyPPArV hvWxjlO7S4DmaEhLzarWi/ze7EGwWSuI2eEa/8zU0INUsGI4ywe7vepQz7IqaAovAX0d+f1YjbmC VsAwjhLmveFjNwIDAQABo4IBsDCCAawwHwYDVR0jBBgwFoAUiUgI6iBOd2uG5YHI1+GNZIR//HAw HQYDVR0OBBYEFFxiGptwbOfWOtMk5loHw7uqWUOnMDAGA1UdEQQpMCeBE2R3bXcyQGluZnJhZGVh ZC5vcmeBEGRhdmlkQHdvb2Rob3Uuc2UwFAYDVR0gBA0wCzAJBgdngQwBBQEBMA4GA1UdDwEB/wQE AwIF4DAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwQwewYDVR0fBHQwcjA3oDWgM4YxaHR0 cDovL2NybDMuZGlnaWNlcnQuY29tL1Zlcm9rZXlTZWN1cmVFbWFpbEcyLmNybDA3oDWgM4YxaHR0 cDovL2NybDQuZGlnaWNlcnQuY29tL1Zlcm9rZXlTZWN1cmVFbWFpbEcyLmNybDB2BggrBgEFBQcB AQRqMGgwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBABggrBgEFBQcwAoY0 aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL1Zlcm9rZXlTZWN1cmVFbWFpbEcyLmNydDANBgkq hkiG9w0BAQsFAAOCAQEAQXc4FPiPLRnTDvmOABEzkIumojfZAe5SlnuQoeFUfi+LsWCKiB8Uextv iBAvboKhLuN6eG/NC6WOzOCppn4mkQxRkOdLNThwMHW0d19jrZFEKtEG/epZ/hw/DdScTuZ2m7im 8ppItAT6GXD3aPhXkXnJpC/zTs85uNSQR64cEcBFjjoQDuSsTeJ5DAWf8EMyhMuD8pcbqx5kRvyt JPsWBQzv1Dsdv2LDPLNd/JUKhHSgr7nbUr4+aAP2PHTXGcEBh8lTeYea9p4d5k969pe0OHYMV5aL xERqTagmSetuIwolkAuBCzA9vulg8Y49Nz2zrpUGfKGOD0FMqenYxdJHgDCCBZswggSDoAMCAQIC EAfkkQ9qA1FdgOJE92VzW+AwDQYJKoZIhvcNAQELBQAwQTELMAkGA1UEBhMCQVUxEDAOBgNVBAoT B1Zlcm9rZXkxIDAeBgNVBAMTF1Zlcm9rZXkgU2VjdXJlIEVtYWlsIEcyMB4XDTI0MTIzMDAwMDAw MFoXDTI4MDEwNDIzNTk1OVowHjEcMBoGA1UEAwwTZHdtdzJAaW5mcmFkZWFkLm9yZzCCAiIwDQYJ KoZIhvcNAQEBBQADggIPADCCAgoCggIBANqWLse95HW2F7FhfH9bugyT/danKmmXrbMnz5GZNAfj Jl5gBL9JFXrOZ9eVdpmw04Tp6aDxZctFLoEDvSWKi367Q7Sg+ci+fH4KwwfQ8Pi0IpIKx2n5emEg nbOQL1Lv/IcNiep6Cq3DiyaSpSp/RZf+CAfUNySHS8eWmhLU6jGpSD6hxTpYKye7PmrmvMWwfGEP WoamAV1kSTb9z/9m9Q2LXa89aKmTxNwnAfD3Ohn9mtU3JukwILRMewn9QRXK7KzM+01h5hkCE4nj W9q/VGFknNhqfhrWBTSQoE9CSVylASGrjzCgS7XmKy/BaH3/7mOOHQv5g1o3Qj/+cdKnpT0I5Qb1 nRy+c7wUzo9OqydJtxzSP4ZyHA4dELto/a3m/ay1XHcpum1pgTOLgxAfGb/T4dCkwRUstSKLMmpL g9Y9TrN9BM4xn24tBFFyL5znGG0wQGzOVAM68RBzIQb6Fz758fjsr4yZnPbVsU1+gHEs/puNHrG0 9e1EQXmUtfGn4InoopJuU8p5VGD9S3Ikd4UoBlc7xl5yjtNlQxUeYrRlnUSmdlucCEoTX1n4UmtA 9CuVHSA7eUHO7I88CtWG9bGOU7tLgOZoSEvNqtaL/N7sQbBZK4jZ4Rr/zNTQg1SwYjjLB7u96lDP sipoCi8BfR35/ViNuYJWwDCOEua94WM3AgMBAAGjggGwMIIBrDAfBgNVHSMEGDAWgBSJSAjqIE53 a4blgcjX4Y1khH/8cDAdBgNVHQ4EFgQUXGIam3Bs59Y60yTmWgfDu6pZQ6cwMAYDVR0RBCkwJ4ET ZHdtdzJAaW5mcmFkZWFkLm9yZ4EQZGF2aWRAd29vZGhvdS5zZTAUBgNVHSAEDTALMAkGB2eBDAEF AQEwDgYDVR0PAQH/BAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDB7BgNVHR8E dDByMDegNaAzhjFodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vVmVyb2tleVNlY3VyZUVtYWlsRzIu Y3JsMDegNaAzhjFodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vVmVyb2tleVNlY3VyZUVtYWlsRzIu Y3JsMHYGCCsGAQUFBwEBBGowaDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29t MEAGCCsGAQUFBzAChjRodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vVmVyb2tleVNlY3VyZUVt YWlsRzIuY3J0MA0GCSqGSIb3DQEBCwUAA4IBAQBBdzgU+I8tGdMO+Y4AETOQi6aiN9kB7lKWe5Ch 4VR+L4uxYIqIHxR7G2+IEC9ugqEu43p4b80LpY7M4KmmfiaRDFGQ50s1OHAwdbR3X2OtkUQq0Qb9 6ln+HD8N1JxO5nabuKbymki0BPoZcPdo+FeRecmkL/NOzzm41JBHrhwRwEWOOhAO5KxN4nkMBZ/w QzKEy4PylxurHmRG/K0k+xYFDO/UOx2/YsM8s138lQqEdKCvudtSvj5oA/Y8dNcZwQGHyVN5h5r2 nh3mT3r2l7Q4dgxXlovERGpNqCZJ624jCiWQC4ELMD2+6WDxjj03PbOulQZ8oY4PQUyp6djF0keA MYIDuzCCA7cCAQEwVTBBMQswCQYDVQQGEwJBVTEQMA4GA1UEChMHVmVyb2tleTEgMB4GA1UEAxMX VmVyb2tleSBTZWN1cmUgRW1haWwgRzICEAfkkQ9qA1FdgOJE92VzW+AwDQYJYIZIAWUDBAIBBQCg ggE3MBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTI1MTIxOTE4NTEz N1owLwYJKoZIhvcNAQkEMSIEIGTy7cScZAfUtzV2sqOqy7WBsBF/DoCPkyGZ2PWPSQXIMGQGCSsG AQQBgjcQBDFXMFUwQTELMAkGA1UEBhMCQVUxEDAOBgNVBAoTB1Zlcm9rZXkxIDAeBgNVBAMTF1Zl cm9rZXkgU2VjdXJlIEVtYWlsIEcyAhAH5JEPagNRXYDiRPdlc1vgMGYGCyqGSIb3DQEJEAILMVeg VTBBMQswCQYDVQQGEwJBVTEQMA4GA1UEChMHVmVyb2tleTEgMB4GA1UEAxMXVmVyb2tleSBTZWN1 cmUgRW1haWwgRzICEAfkkQ9qA1FdgOJE92VzW+AwDQYJKoZIhvcNAQEBBQAEggIATAjqj4rMiYr5 0KCJ60vzMvP6G+5d9WCRPc7iX8iYLbmYsY0FrRVmdDsNrk1CoSmLPku4KW2xsIyqN6swgaNYYBRl B8djLwmY79IJSTXqU9XW7f3S4GVIoDJ9I6slMTDt67R1+ZuPUkevSLIx00fLOlCB60zXmGKmZsMh wyNmnlTdzSL69zBReNv41sV1MnNMJXSWdRwpbRmZEl3GVkVgKkV9CCwwvGdcxnHXunZ3x0Q+E2QO gsA97p9RKd0Z8yor0tZEkBqZ0IoPh355rBoJvA0cKIscy3K6OIKA4VN41cbhv5pa/1K+xbTY7Rsy DTSvjOEVxo/ANSGtR+ULKGTzJk1eOpzJ5yg3IQITgjz/hKib0G8Urk0CAPLkGfGUp0VBDEyO9kat kfjMBTHoSE4RsLRkswpiETEbNESBWPYOdOPB9bl3ZhaBItIB4ow+e6Zfma61j08ZYt+RTEujwUVN zBC4iTTRuPqhMDmUikalAk6M4Y9+sD2FZAwg1NO400TDlPcsEcBGRW2bZeyhmmEP08s5PimMt860 niLvpQa3UhQqQcO8ouzKhdVLU3kJMukKvfDOtqD3zEK3NODVRzhaj4C/HjN9IaS1RMcIJV2eE6Tu r8qPfLtjdeNNOCV3EbuIZOL89OkE25HgMm8ZKKjMRN1GlLuSW63PeaDFNoyMEmQAAAAAAAA= --=-wUEtl6af5mz+nqtxRPij--