On 2026-07-27 13:15:14-07:00, Jakub Kicinski wrote:
> This is an AI-generated review of your patch. The human sending this
> email has considered the AI review valid, or at least plausible.
> ---
> ptp: Add ioctls for PHC timestamps with quality attributes
> 
> This introduces PTP_SYS_OFFSET_EXTENDED_ATTRS and
> PTP_SYS_OFFSET_PRECISE_ATTRS, extending the existing extended/precise
> offset ioctls with clock quality attributes (error bound, timescale,
> sync status) and raw system counter fields. New driver callbacks
> gettimexattrs64 and getcrosststampattrs are added to ptp_clock_info.
> 
> Link: https://lore.kernel.org/netdev/[email protected]/
> Link: https://lore.kernel.org/all/87se7ht25o.ffs@tglx/
> 
> A few questions inline below.
> 
> > diff --git a/drivers/ptp/ptp_chardev.c b/drivers/ptp/ptp_chardev.c
> > index dc23cd708cfe..d30ee4f51fbb 100644
> > --- a/drivers/ptp/ptp_chardev.c
> > +++ b/drivers/ptp/ptp_chardev.c
> > @@ -190,6 +190,8 @@ static long ptp_clock_getcaps(struct ptp_clock *ptp, 
> > void __user *arg)
> >             .cross_timestamping     = ptp->info->getcrosststamp != NULL,
> >             .adjust_phase           = ptp->info->adjphase != NULL &&
> >                                       ptp->info->getmaxphase != NULL,
> > +           .extended_attrs         = ptp->info->gettimexattrs64 != NULL,
> > +           .precise_attrs          = ptp->info->getcrosststampattrs != 
> > NULL,
> >     };
> 
> [Medium]
> Are the capability bits consistent with what the ioctls will actually
> accept?
> 
> The caps are set based only on the presence of the new *_attrs callbacks,
> but the ioctl handlers fall back to gettimex64/getcrosststamp when the
> *_attrs callback is absent:
> 
>     if (ptp->info->gettimexattrs64)
>         err = ptp->info->gettimexattrs64(ptp->info, &ts, &sts, &att);
>     else if (ptp->info->gettimex64)
>         err = ptp->info->gettimex64(ptp->info, &ts, &sts);
>     else
>         return -EOPNOTSUPP;
> 
> A userspace that respects the capability bits would refuse to call the
> new ioctl on drivers that would in fact serve it via the fallback. Is
> the fallback intended, and if so, should the caps advertise it? The
> commit message does not describe this behavior.
> 

The fallback is intentional. I'll widen the caps in v6 so they advertise it
(gettimexattrs64 || gettimex64, getcrosststampattrs || getcrosststamp) and
document the behavior in the commit message. Thanks.

> [ ... ]
> 
> > @@ -404,6 +413,130 @@ static long ptp_sys_offset_extended(struct ptp_clock 
> > *ptp, void __user *arg,
> >     return copy_to_user(arg, extoff, sizeof(*extoff)) ? -EFAULT : 0;
> >  }
> >  
> > +static long ptp_sys_offset_extended_attrs(struct ptp_clock *ptp, void 
> > __user *arg)
> > +{
> > +   struct ptp_sys_offset_attrs *data __free(kfree) = NULL;
> > +   struct ptp_attrs_request request;
> > +   struct ptp_system_timestamp sts;
> 
> [Medium]
> Can this leak uninitialized stack contents to userspace?
> 
> Only sts.clockid is initialized below, and sts is reused across loop
> iterations. If any driver's gettimex64/gettimexattrs64 returns success
> without touching pre_sts/post_sts on the sts pointer, sts.pre_sts.valid
> is either uninitialized (first iteration) or stale (later iterations),
> and the "if (!sts.pre_sts.valid || !sts.post_sts.valid)" gate can admit
> a partially-populated snapshot.
> 
> On the success path the code then copies pre_sts.cycles, pre_sts.cs_id,
> pre_sts.monoraw, pre_sts.systime and the post_sts equivalents to
> userspace. The pre-existing ptp_sys_offset_extended() has the same
> shape but only copied systime, so the exposed surface is now wider.
> Would something like:
> 
>     struct ptp_system_timestamp sts = {};
> 
> inside the loop (or at declaration) be safer?
>

Yes — I'll zero-initialize sts per loop iteration in v6, so nothing
uninitialized or stale can reach userspace. Thanks.
 
> > +   unsigned int n_samples;
> > +   int err;
> > +
> > +   if (copy_from_user(&request, arg, sizeof(request)))
> > +           return -EFAULT;
> > +
> > +   if (request.valid ||
> > +       request.num_samples > PTP_MAX_SAMPLES ||
> > +       request.num_samples == 0)
> > +           return -EINVAL;
> 
> [Medium]
> Should this also reject non-zero request.rsv[]?
> 
> The kernel-doc for struct ptp_attrs_request says "Reserved for future
> use, must be zero", but rsv[3] is never validated. The neighboring
> ptp_sys_offset_extended() enforces the same contract on its own
> reserved fields:
> 
>     if (extoff->n_samples > PTP_MAX_SAMPLES || extoff->rsv[0] || 
> extoff->rsv[1])
>         return -EINVAL;
> 
> Without a similar check, existing userspace binaries can start sending
> garbage in rsv[], which then blocks any future repurposing of those
> bytes.
> 

Agreed — v6 will reject a non-zero rsv[], matching
ptp_sys_offset_extended(). Thanks.

> > +
> > +   err = ptp_validate_sys_offset_clockid(request.clock_id);
> > +   if (err)
> > +           return err;
> > +
> > +   n_samples = request.num_samples;
> > +   sts.clockid = request.clock_id;
> > +
> > +   data = kzalloc(struct_size(data, timestamps, n_samples), GFP_KERNEL);
> > +   if (!data)
> > +           return -ENOMEM;
> > +
> > +   data->request.num_samples = n_samples;
> 
> [Low]
> This is a _IOWR ioctl and the whole struct (including data->request) is
> copied back to userspace at the end, but only data->request.num_samples
> is populated. data->request.valid, data->request.clock_id and
> data->request.rsv[] therefore always come back as zero, silently
> overwriting the values userspace put in the request buffer.
> 
> Is that intentional? It is not mentioned in the commit message, and it
> locks in a slightly surprising contract for future extensions.
> 

Not intentional — v6 will echo the full request back so
valid/clock_id/num_samples/rsv round-trip as submitted. Thanks.

> > +
> > +   for (unsigned int i = 0; i < n_samples; i++) {
> > +           struct ptp_clock_attrs att = {};
> > +           struct timespec64 ts;
> > +
> > +           if (ptp->info->gettimexattrs64)
> > +                   err = ptp->info->gettimexattrs64(ptp->info, &ts,
> > +                                                    &sts, &att);
> > +           else if (ptp->info->gettimex64)
> > +                   err = ptp->info->gettimex64(ptp->info, &ts, &sts);
> > +           else
> > +                   return -EOPNOTSUPP;
> > +
> > +           if (err)
> > +                   return err;
> > +
> > +           /* Filter out disabled or unavailable clocks */
> > +           if (!sts.pre_sts.valid || !sts.post_sts.valid)
> > +                   return -EINVAL;
> > +
> > +           data->timestamps[i].pre_systime.sys_time =
> > +                   ktime_to_ns(sts.pre_sts.systime);
> > +           data->timestamps[i].pre_systime.sys_rawtime =
> > +                   ktime_to_ns(sts.pre_sts.monoraw);
> > +           data->timestamps[i].pre_systime.sys_counter =
> > +                   sts.pre_sts.cycles;
> > +           data->timestamps[i].pre_systime.sys_counter_id =
> > +                   sts.pre_sts.cs_id;
> 
> [Critical]
> Does the sys_counter_id assignment produce the correct UAPI value?
> 
> sts.pre_sts.cs_id is enum clocksource_ids from
> include/linux/clocksource_ids.h:
> 
>     CSID_GENERIC          = 0,
>     CSID_ARM_ARCH_COUNTER = 1,
>     CSID_S390_TOD         = 2,
>     CSID_X86_TSC_EARLY    = 3,
>     CSID_X86_TSC          = 4,
>     CSID_X86_KVM_CLK      = 5,
>     CSID_X86_ART          = 6,
> 
> But sys_counter_id is documented as enum ptp_counter_id from the UAPI:
> 
>     PTP_COUNTER_UNKNOWN  = 0,
>     PTP_COUNTER_X86_TSC  = 1,
>     PTP_COUNTER_ARM_ARCH = 2,
> 
> With this direct assignment, on ARM sts.pre_sts.cs_id == 1
> (CSID_ARM_ARCH_COUNTER) is reported to userspace as PTP_COUNTER_X86_TSC.
> On x86, real CSID_X86_TSC (4) has no defined UAPI meaning. On s390,
> CSID_S390_TOD (2) is reported as PTP_COUNTER_ARM_ARCH.
> 
> Should the ioctl translate cs_id into the UAPI enum before copying it
> out? Once released, later adding a translation layer would break any
> userspace that started decoding the raw kernel values.
> 

Good catch — that's a bug. v6 will translate enum clocksource_ids to the
UAPI enum ptp_counter_id before copying out. Thanks.

> [High]
> On clocksources that are derived/virtualised (kvm-clock, scaled arch
> counter, ART, ...), the raw underlying counter identity is exposed by
> system_time_snapshot as hw_cycles/hw_csid rather than cycles/cs_id.
> The new UAPI only propagates cycles/cs_id.
> 
> The commit message says one motivation is:
> 
>     3. VMMs that need raw system counter values paired with PTP
>        timestamps for feed-forward clock calibration ...
> 
> but a guest on a derived clocksource will not see the underlying
> hardware counter identity or value here. Should the UAPI also expose
> hw_cycles/hw_csid, given that the struct is frozen once merged?
> 

This is being handled as a follow-up (David Woodhouse offered on-list to
take the hw_csid side). This series exposes the base cs_id/cycles, and the
UAPI leaves room to add the hw_* values without an ABI change.

> > +           data->timestamps[i].devtime.device_time.sec = ts.tv_sec;
> > +           data->timestamps[i].devtime.device_time.nsec = ts.tv_nsec;
> > +           data->timestamps[i].devtime.attrs = att;
> 
> [ ... ]
> 
> > +}
> > +
> > +static long ptp_sys_offset_precise_attrs(struct ptp_clock *ptp, void 
> > __user *arg)
> > +{
> > +   struct ptp_sys_offset_attrs *data __free(kfree) = NULL;
> > +   struct system_device_crosststamp xtstamp;
> > +   struct ptp_attrs_request request;
> > +   struct ptp_clock_attrs att = {};
> > +   struct timespec64 ts;
> > +   int err;
> > +
> > +   if (copy_from_user(&request, arg, sizeof(request)))
> > +           return -EFAULT;
> > +
> > +   if (request.valid ||
> > +       request.num_samples != 1)
> > +           return -EINVAL;
> > +
> > +   err = ptp_validate_sys_offset_clockid(request.clock_id);
> > +   if (err)
> > +           return err;
> > +
> > +   data = kzalloc(struct_size(data, timestamps, 1), GFP_KERNEL);
> > +   if (!data)
> > +           return -ENOMEM;
> > +
> > +   if (ptp->info->getcrosststampattrs)
> > +           err = ptp->info->getcrosststampattrs(ptp->info, &xtstamp, &att);
> > +   else if (ptp->info->getcrosststamp)
> > +           err = ptp->info->getcrosststamp(ptp->info, &xtstamp);
> > +   else
> > +           return -EOPNOTSUPP;
> 
> [High]
> Can any driver using get_device_system_crosststamp() actually serve
> this ioctl?
> 
> xtstamp is declared without an initializer, so xtstamp.clock_id holds
> whatever was on the stack. The pre-existing ptp_sys_offset_precise()
> explicitly sets it:
> 
>     struct system_device_crosststamp xtstamp = {
>         .clock_id = CLOCK_REALTIME,
>     };
> 
> Drivers commonly forward xtstamp to get_device_system_crosststamp() in
> kernel/time/timekeeping.c, which switches on xtstamp->clock_id and
> falls through to:
> 
>     default:
>         WARN_ON_ONCE(1);
>         return -ENODEV;
> 
> So on most drivers implementing getcrosststamp (kvm, mlx5, ice, igc,
> bnxt, s390, ...), an unprivileged caller of PTP_SYS_OFFSET_PRECISE_ATTRS
> would trigger a first-hit WARN and get -ENODEV.
> 
> In addition, request.clock_id is validated by
> ptp_validate_sys_offset_clockid() but never assigned to
> xtstamp.clock_id, so the userspace-selected clock is silently dropped
> even for the paths documented in the UAPI header (CLOCK_MONOTONIC,
> CLOCK_MONOTONIC_RAW, CLOCK_AUX...).
> 
> Should xtstamp.clock_id be set from request.clock_id (and xtstamp
> zeroed before use)?
> 

Real bug, thanks. v6 zero-initializes xtstamp and sets clock_id from the
validated request. To avoid the get_device_system_crosststamp() default
WARN, precise will validate clock_id against a dedicated allowlist (REALTIME
+ AUX) and return -EINVAL for anything else.

> [ ... ]
> 
> > diff --git a/drivers/ptp/ptp_clock.c b/drivers/ptp/ptp_clock.c
> > index d6f54ccaf93b..849aef8191c5 100644
> > --- a/drivers/ptp/ptp_clock.c
> > +++ b/drivers/ptp/ptp_clock.c
> > @@ -112,7 +112,9 @@ static int ptp_clock_gettime(struct posix_clock *pc, 
> > struct timespec64 *tp)
> >     struct ptp_clock *ptp = container_of(pc, struct ptp_clock, clock);
> >     int err;
> >  
> > -   if (ptp->info->gettimex64)
> > +   if (ptp->info->gettimexattrs64)
> > +           err = ptp->info->gettimexattrs64(ptp->info, tp, NULL, NULL);
> > +   else if (ptp->info->gettimex64)
> >             err = ptp->info->gettimex64(ptp->info, tp, NULL);
> >     else
> >             err = ptp->info->gettime64(ptp->info, tp);
> 
> [Low]
> This call site passes both sts and att as NULL, but the kernel-doc for
> gettimexattrs64 only documents that att may be NULL ("fills @att (if
> not NULL)"). The dual-NULL contract on sts is implicitly inherited
> from gettimex64 but is not stated.
> 
> Would it be worth spelling out in the kernel-doc that both sts and att
> may be NULL, so a new driver author does not skip the NULL check on
> sts?

Yes — v6 will state that both sts and att may be NULL. Thanks.

> 
> [ ... ]



Reply via email to