Message ID | 20190416202013.4034148-3-arnd@arndb.de |
---|---|
State | New |
Headers | show |
Series | compat_ioctl: cleanups | expand |
On Tue, Apr 16, 2019 at 10:19:40PM +0200, Arnd Bergmann wrote: > diff --git a/drivers/net/ppp/ppp_generic.c b/drivers/net/ppp/ppp_generic.c > index c708400fff4a..04252c3492ee 100644 > --- a/drivers/net/ppp/ppp_generic.c > +++ b/drivers/net/ppp/ppp_generic.c > @@ -899,6 +899,7 @@ static const struct file_operations ppp_device_fops = { > .write = ppp_write, > .poll = ppp_poll, > .unlocked_ioctl = ppp_ioctl, > + .compat_ioctl = ppp_ioctl, Oh? What happens on e.g. s390 with something like PPPIOCNEWUNIT? Current kernel: * no ->compat_ioctl() * ->unlock_ioctl() is present * found by compat_ioctl_check_table() * pass (unsigned long)compat_ptr(arg) to do_vfs_ioctl() * pass that to ppp_ioctl() * pass that to ppp_unattached_ioctl() * fetch int from (int __user *)compat_ptr(arg) With your patch: * call ppp_ioctl() * pass arg to ppp_unattached_ioctl() * fetch int from (int __user *)arg AFAICS, that's broken... Looking at that ppp_ioctl(), pointer to arch-independent type or ignored: PPPIOCNEWUNIT PPPIOCATTACH PPPIOCATTCHAN PPPIOCSMRU PPPIOCSFLAGS PPPIOCGFLAGS PPPIOCGUNIT PPPIOCSDEBUG PPPIOCSMAXCID PPPIOCCONNECT PPPIOCGDEBUG PPPIOCSMAXCID PPPIOCSMRRU PPPIOCDETACH PPPIOCDISCONN PPPIOCGASYNCMAP PPPIOCSASYNCMAP PPPIOCGRASYNCMAP PPPIOCSRASYNCMAP PPPIOCGXASYNCMAP PPPIOCSXASYNCMAP PPPIOCGNPMODE PPPIOCSNPMODE pointer to struct ppp_option_data (with further pointer-chasing in it): PPPIOCSCOMPRESS pointer to struct ppp_idle: PPPIOCGIDLE pointer to struct sock_filter (with hidden pointer-chasing, AFAICS): PPPIOCSPASS PPPIOCSACTIVE Pretty much all of them take pointers. What's more, reaction to unknown is -ENOTTY, not -ENOIOCTLCM, so that patch will have prevent the translated ones from reaching do_ioctl_trans() What am I missing here? Why not simply do compat_ppp_ioctl() { PPPIOCSCOMPRESS32 => deal with it PPPIOCGIDLE32 => deal with it PPPIOCSPASS32 / PPPIOCSACTIVE32 => deal with it default: pass compat_ptr(arg) to ppp_ioctl() and be done with that } with BPF-related bits (both compat and native) taken to e.g. net/core/bpf-ppp.c, picked by both generic and isdn? IDGI...
On Wed, Apr 17, 2019 at 11:13 PM Al Viro <viro@zeniv.linux.org.uk> wrote: > > On Tue, Apr 16, 2019 at 10:19:40PM +0200, Arnd Bergmann wrote: > > diff --git a/drivers/net/ppp/ppp_generic.c b/drivers/net/ppp/ppp_generic.c > > index c708400fff4a..04252c3492ee 100644 > > --- a/drivers/net/ppp/ppp_generic.c > > +++ b/drivers/net/ppp/ppp_generic.c > > @@ -899,6 +899,7 @@ static const struct file_operations ppp_device_fops = { > > .write = ppp_write, > > .poll = ppp_poll, > > .unlocked_ioctl = ppp_ioctl, > > + .compat_ioctl = ppp_ioctl, > > Oh? What happens on e.g. s390 with something like PPPIOCNEWUNIT? > Current kernel: > * no ->compat_ioctl() > * ->unlock_ioctl() is present > * found by compat_ioctl_check_table() > * pass (unsigned long)compat_ptr(arg) to do_vfs_ioctl() > * pass that to ppp_ioctl() > * pass that to ppp_unattached_ioctl() > * fetch int from (int __user *)compat_ptr(arg) > > With your patch: > * call ppp_ioctl() > * pass arg to ppp_unattached_ioctl() > * fetch int from (int __user *)arg > > AFAICS, that's broken... Correct. I had added this patch to the series from an older set of patches (predating the compat_ptr_ioctl() series) , and did not check for this issue again. When I originally created the patch, I assumed that even on s390 it would be no problem. > Looking at that ppp_ioctl(), > pointer to arch-independent type or ignored: > PPPIOCNEWUNIT PPPIOCATTACH PPPIOCATTCHAN PPPIOCSMRU PPPIOCSFLAGS > PPPIOCGFLAGS PPPIOCGUNIT PPPIOCSDEBUG PPPIOCSMAXCID PPPIOCCONNECT > PPPIOCGDEBUG PPPIOCSMAXCID PPPIOCSMRRU > PPPIOCDETACH PPPIOCDISCONN > PPPIOCGASYNCMAP PPPIOCSASYNCMAP PPPIOCGRASYNCMAP PPPIOCSRASYNCMAP > PPPIOCGXASYNCMAP PPPIOCSXASYNCMAP > PPPIOCGNPMODE PPPIOCSNPMODE > pointer to struct ppp_option_data (with further pointer-chasing in it): > PPPIOCSCOMPRESS > pointer to struct ppp_idle: > PPPIOCGIDLE > pointer to struct sock_filter (with hidden pointer-chasing, AFAICS): > PPPIOCSPASS PPPIOCSACTIVE > > Pretty much all of them take pointers. What's more, reaction to > unknown is -ENOTTY, not -ENOIOCTLCM, so that patch will have > prevent the translated ones from reaching do_ioctl_trans() Good point, this patch sequence does break bisection. > What am I missing here? Why not simply do > > compat_ppp_ioctl() > { > PPPIOCSCOMPRESS32 => deal with it > PPPIOCGIDLE32 => deal with it > PPPIOCSPASS32 / PPPIOCSACTIVE32 => deal with it > default: pass compat_ptr(arg) to ppp_ioctl() and be done with that > } > > with BPF-related bits (both compat and native) taken to e.g. net/core/bpf-ppp.c, > picked by both generic and isdn? IDGI... I was trying to unify the native and compat code paths as much as possible here. Handling the four PPPIO*32 commands in compat_ppp_ioctl would either require duplicating large chunks of ppp_ioctl, or keeping the extra compat_alloc_user_space() copy from the existing implementation. I'll try to come up with a different way to structure the patches. Arnd
On Thu, Apr 18, 2019 at 12:03:07AM +0200, Arnd Bergmann wrote: > On Wed, Apr 17, 2019 at 11:13 PM Al Viro <viro@zeniv.linux.org.uk> wrote: > > > > On Tue, Apr 16, 2019 at 10:19:40PM +0200, Arnd Bergmann wrote: > > > diff --git a/drivers/net/ppp/ppp_generic.c b/drivers/net/ppp/ppp_generic.c > > > index c708400fff4a..04252c3492ee 100644 > > > --- a/drivers/net/ppp/ppp_generic.c > > > +++ b/drivers/net/ppp/ppp_generic.c > > > @@ -899,6 +899,7 @@ static const struct file_operations ppp_device_fops = { > > > .write = ppp_write, > > > .poll = ppp_poll, > > > .unlocked_ioctl = ppp_ioctl, > > > + .compat_ioctl = ppp_ioctl, > > > > Oh? What happens on e.g. s390 with something like PPPIOCNEWUNIT? > > Current kernel: > > * no ->compat_ioctl() > > * ->unlock_ioctl() is present > > * found by compat_ioctl_check_table() > > * pass (unsigned long)compat_ptr(arg) to do_vfs_ioctl() > > * pass that to ppp_ioctl() > > * pass that to ppp_unattached_ioctl() > > * fetch int from (int __user *)compat_ptr(arg) > > > > With your patch: > > * call ppp_ioctl() > > * pass arg to ppp_unattached_ioctl() > > * fetch int from (int __user *)arg > > > > AFAICS, that's broken... > > Correct. I had added this patch to the series from an older set of > patches (predating > the compat_ptr_ioctl() series) , and did not check for this issue again. When > I originally created the patch, I assumed that even on s390 it would be no > problem. > > > Looking at that ppp_ioctl(), > > pointer to arch-independent type or ignored: > > PPPIOCNEWUNIT PPPIOCATTACH PPPIOCATTCHAN PPPIOCSMRU PPPIOCSFLAGS > > PPPIOCGFLAGS PPPIOCGUNIT PPPIOCSDEBUG PPPIOCSMAXCID PPPIOCCONNECT > > PPPIOCGDEBUG PPPIOCSMAXCID PPPIOCSMRRU > > PPPIOCDETACH PPPIOCDISCONN > > PPPIOCGASYNCMAP PPPIOCSASYNCMAP PPPIOCGRASYNCMAP PPPIOCSRASYNCMAP > > PPPIOCGXASYNCMAP PPPIOCSXASYNCMAP > > PPPIOCGNPMODE PPPIOCSNPMODE > > pointer to struct ppp_option_data (with further pointer-chasing in it): > > PPPIOCSCOMPRESS > > pointer to struct ppp_idle: > > PPPIOCGIDLE > > pointer to struct sock_filter (with hidden pointer-chasing, AFAICS): > > PPPIOCSPASS PPPIOCSACTIVE > > > > Pretty much all of them take pointers. What's more, reaction to > > unknown is -ENOTTY, not -ENOIOCTLCM, so that patch will have > > prevent the translated ones from reaching do_ioctl_trans() > > Good point, this patch sequence does break bisection. > > > What am I missing here? Why not simply do > > > > compat_ppp_ioctl() > > { > > PPPIOCSCOMPRESS32 => deal with it > > PPPIOCGIDLE32 => deal with it > > PPPIOCSPASS32 / PPPIOCSACTIVE32 => deal with it > > default: pass compat_ptr(arg) to ppp_ioctl() and be done with that > > } > > > > with BPF-related bits (both compat and native) taken to e.g. net/core/bpf-ppp.c, > > picked by both generic and isdn? IDGI... > > I was trying to unify the native and compat code paths as much > as possible here. Handling the four PPPIO*32 commands in > compat_ppp_ioctl would either require duplicating large chunks > of ppp_ioctl, or keeping the extra compat_alloc_user_space() > copy from the existing implementation. > > I'll try to come up with a different way to structure the patches. Huh? Instead of case PPPIOCSCOMPRESS: err = ppp_set_compress(ppp, arg); break; in native, have struct ppp_option_data data; ... case PPPIOCSCOMPRESS: if (copy_from_user(&data, argp, sizeof(data))) err = -EFAULT; else err = ppp_set_compress(ppp, &data); break; while in compat do struct ppp_option_data32 data32; case PPPIOCSCOMPRESS32: if (copy_from_user(&data32, argp, sizeof(data32))) err = -EFAULT; else err = ppp_set_compress(ppp, &(struct ppp_option_data){ .ptr = compat_ptr(data32.ptr), .length = data32.length, .transmit = data32.transmit }); break; PPPIOCGIDLE is small to start with - not a lot to copy there. And as for the filters... What you need is something like struct bpf_prog *get_ppp_bpf(struct sock_fprog __user *p) { struct sock_fprog uprog; struct sock_fprog_kern fprog; struct sock_filter *code = NULL; struct bpf_prog *res; int err; if (copy_from_user(&uprog, p, sizeof(uprog))) return ERR_PTR(-EFAULT); if (!uprog.len) return NULL; fprog.len = uprog.len * sizeof(struct sock_filter); code = fprog.filter = memdup_user(uprog.filter, fprog.len); if (IS_ERR(code)) return ERR_CAST(code); err = bpf_prog_create(&res, &fprog); kfree(code); if (err) return ERR_PTR(err); return res; } in net/core/ppp-filter.c (or in net/core/filter.c, for that matter, under appropriate ifdef)) with obvious compat counterpart next to it. Hell, turn the above into static struct bpf_prog *__get_ppp_bpf(struct sock_fprog *kp) { struct sock_fprog_kern fprog; struct sock_filter *code = NULL; struct bpf_prog *res; int err; if (!kp->len) return NULL; fprog.len = kp->len * sizeof(struct sock_filter); code = fprog.filter = memdup_user(kp->filter, fprog.len); if (IS_ERR(code)) return ERR_CAST(code); err = bpf_prog_create(&res, &fprog); kfree(code); if (err) return ERR_PTR(err); return res; } struct bpf_prog *get_ppp_bpf(struct sock_fprog __user *p) { struct sock_fprog uprog; if (copy_from_user(&uprog, p, sizeof(uprog))) return ERR_PTR(-EFAULT); return __get_ppp_bpf(&uprog); } struct bpf_prog *compat_get_ppp_bpf(struct sock_fprog32 __user *p) { struct sock_fprog uprog32; if (copy_from_user(&uprog32, p, sizeof(uprog32))) return ERR_PTR(-EFAULT); return __get_ppp_bpf(&(struct sock_fprog){ .len = uprog32.len, .filter = compat_ptr(uprog32.filter)}); } Then in native ioctl do case PPPIOCSPASS: case PPPIOCSACTIVE: { struct bpf_prog *filter = get_bpf_ppp(argp); if (IS_ERR(filter)) { err = PTR_ERR(filter); } else { struct bpf_prog **which; if (cmd == PPPIOCSPASS) which = &ppp->pass_filter; else which = &ppp->active_filter; ppp_lock(ppp); if (*which) bpf_prog_destroy(*which); *which = filter; ppp_unlock(ppp); err = 0; } break; } in native and similar in compat, with get_bpf_ppp() replaced with call of compat_get_bpf_ppp() and ioctl numbers obviously adjusted. All there is to it... Helpers obviously shared with isdn and yes, all crap gone from fs/compat_ioctl.c... Why would you want to duplicate large chunks of anything? The above is not even compile-tested, but... I can put together a patch if you wish. Or am I missing something here?
On Thu, Apr 18, 2019 at 12:53:00AM +0100, Al Viro wrote: > Why would you want to duplicate large chunks of anything? > The above is not even compile-tested, but... I can put > together a patch if you wish. Or am I missing something > here? Actually, there's another broken part - pppox. And that one is nastier - you've just lost everything outside of protocol family ioctls on those sockets. Try e.g. SIOCGIFMTU in 32bit process on those. We really can't reuse native ->ioctl() there - proto family ->compat_ioctl() *must* return -ENOIOCTLCMD on everything it doesn't handle.
On Thu, Apr 18, 2019 at 1:53 AM Al Viro <viro@zeniv.linux.org.uk> wrote: > On Thu, Apr 18, 2019 at 12:03:07AM +0200, Arnd Bergmann wrote: > > On Wed, Apr 17, 2019 at 11:13 PM Al Viro <viro@zeniv.linux.org.uk> wrote: > > > > > > On Tue, Apr 16, 2019 at 10:19:40PM +0200, Arnd Bergmann wrote: > > > > diff --git a/drivers/net/ppp/ppp_generic.c b/drivers/net/ppp/ppp_generic.c > > > > index c708400fff4a..04252c3492ee 100644 _ptr(arg) to ppp_ioctl() and be done with that > > > } > > > > > > with BPF-related bits (both compat and native) taken to e.g. net/core/bpf-ppp.c, > > > picked by both generic and isdn? IDGI... > > > > I was trying to unify the native and compat code paths as much > > as possible here. Handling the four PPPIO*32 commands in > > compat_ppp_ioctl would either require duplicating large chunks > > of ppp_ioctl, or keeping the extra compat_alloc_user_space() > > copy from the existing implementation. > > > > I'll try to come up with a different way to structure the patches. > > Huh? Instead of > case PPPIOCSCOMPRESS: > err = ppp_set_compress(ppp, arg); > break; > in native, have > struct ppp_option_data data; > ... > case PPPIOCSCOMPRESS: > if (copy_from_user(&data, argp, sizeof(data))) > err = -EFAULT; > else > err = ppp_set_compress(ppp, &data); > break; Right, I ended up with something similar before I saw your message. > in native and similar in compat, with get_bpf_ppp() replaced > with call of compat_get_bpf_ppp() and ioctl numbers obviously > adjusted. All there is to it... Helpers obviously shared > with isdn and yes, all crap gone from fs/compat_ioctl.c... I would still leave the ISDN side alone, aside from adding the 64-bit time_t support. > Why would you want to duplicate large chunks of anything? > The above is not even compile-tested, but... I can put > together a patch if you wish. Or am I missing something > here? I expected that the ppp_compat_ioctl() function would end up fairly complex, to duplicate the logic before the switch()/case. What I have now is static long ppp_compat_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { struct ppp_file *pf; struct ppp *ppp; int err = -ENOIOCTLCMD; struct ppp_option_data32 data32; struct ppp_option_data data; void __user *argp = compat_ptr(arg); mutex_lock(&ppp_mutex); pf = file->private_data; if (!pf || pf->kind != INTERFACE) goto out; ppp = PF_TO_PPP(pf); switch (cmd) { case PPPIOCSCOMPRESS32: if (copy_from_user(&data32, argp, sizeof(data32))) { err = -EFAULT; goto out; } data.ptr = compat_ptr(data32.ptr); data.length = data32.length; data.transmit = data32.transmit; err = ppp_set_compress(ppp, &data); break; #ifdef CONFIG_PPP_FILTER case PPPIOCSPASS32: err = compat_get_sock_fprog(&uprog, argp); if (err) break; err = ppp_set_filter(ppp, &uprog, &ppp->pass_filter); break; case PPPIOCSACTIVE32: err = compat_get_sock_fprog(&uprog, argp); if (err) break; err = ppp_set_filter(ppp, &uprog, &ppp->active_filter); break; #endif /* CONFIG_PPP_FILTER */ default: break; } out: mutex_unlock(&ppp_mutex); if (err == -ENOIOCTLCMD) err = ppp_ioctl(file, cmd, (unsigned long)compat_ptr(arg)); return err; } Which doesn't look nearly as bad as I had feared, but still is a larger change to the existing code than what I had before, so there is a bigger risk that I screwed up somewhere new. Arnd
diff --git a/drivers/net/ppp/ppp_generic.c b/drivers/net/ppp/ppp_generic.c index c708400fff4a..04252c3492ee 100644 --- a/drivers/net/ppp/ppp_generic.c +++ b/drivers/net/ppp/ppp_generic.c @@ -899,6 +899,7 @@ static const struct file_operations ppp_device_fops = { .write = ppp_write, .poll = ppp_poll, .unlocked_ioctl = ppp_ioctl, + .compat_ioctl = ppp_ioctl, .open = ppp_open, .release = ppp_release, .llseek = noop_llseek, diff --git a/drivers/net/ppp/pppoe.c b/drivers/net/ppp/pppoe.c index c5e7435db86c..5eccb49bcd2e 100644 --- a/drivers/net/ppp/pppoe.c +++ b/drivers/net/ppp/pppoe.c @@ -1124,6 +1124,9 @@ static const struct proto_ops pppoe_ops = { .recvmsg = pppoe_recvmsg, .mmap = sock_no_mmap, .ioctl = pppox_ioctl, +#ifdef CONFIG_COMPAT + .compat_ioctl = pppox_ioctl, +#endif }; static const struct pppox_proto pppoe_proto = { diff --git a/drivers/net/ppp/pptp.c b/drivers/net/ppp/pptp.c index 50c60550f295..7cf56ad50d07 100644 --- a/drivers/net/ppp/pptp.c +++ b/drivers/net/ppp/pptp.c @@ -628,6 +628,9 @@ static const struct proto_ops pptp_ops = { .recvmsg = sock_no_recvmsg, .mmap = sock_no_mmap, .ioctl = pppox_ioctl, +#ifdef CONFIG_COMPAT + .compat_ioctl = pppox_ioctl, +#endif }; static const struct pppox_proto pppox_pptp_proto = { diff --git a/fs/compat_ioctl.c b/fs/compat_ioctl.c index f1065d116b55..54f26a9fa9f2 100644 --- a/fs/compat_ioctl.c +++ b/fs/compat_ioctl.c @@ -608,39 +608,8 @@ COMPATIBLE_IOCTL(SG_SET_KEEP_ORPHAN) COMPATIBLE_IOCTL(SG_GET_KEEP_ORPHAN) #endif /* PPP stuff */ -COMPATIBLE_IOCTL(PPPIOCGFLAGS) -COMPATIBLE_IOCTL(PPPIOCSFLAGS) -COMPATIBLE_IOCTL(PPPIOCGASYNCMAP) -COMPATIBLE_IOCTL(PPPIOCSASYNCMAP) COMPATIBLE_IOCTL(PPPIOCGUNIT) -COMPATIBLE_IOCTL(PPPIOCGRASYNCMAP) -COMPATIBLE_IOCTL(PPPIOCSRASYNCMAP) -COMPATIBLE_IOCTL(PPPIOCGMRU) -COMPATIBLE_IOCTL(PPPIOCSMRU) -COMPATIBLE_IOCTL(PPPIOCSMAXCID) -COMPATIBLE_IOCTL(PPPIOCGXASYNCMAP) -COMPATIBLE_IOCTL(PPPIOCSXASYNCMAP) -COMPATIBLE_IOCTL(PPPIOCXFERUNIT) -/* PPPIOCSCOMPRESS is translated */ -COMPATIBLE_IOCTL(PPPIOCGNPMODE) -COMPATIBLE_IOCTL(PPPIOCSNPMODE) -COMPATIBLE_IOCTL(PPPIOCGDEBUG) -COMPATIBLE_IOCTL(PPPIOCSDEBUG) -/* PPPIOCSPASS is translated */ -/* PPPIOCSACTIVE is translated */ -/* PPPIOCGIDLE is translated */ -COMPATIBLE_IOCTL(PPPIOCNEWUNIT) -COMPATIBLE_IOCTL(PPPIOCATTACH) -COMPATIBLE_IOCTL(PPPIOCDETACH) -COMPATIBLE_IOCTL(PPPIOCSMRRU) -COMPATIBLE_IOCTL(PPPIOCCONNECT) -COMPATIBLE_IOCTL(PPPIOCDISCONN) -COMPATIBLE_IOCTL(PPPIOCATTCHAN) COMPATIBLE_IOCTL(PPPIOCGCHAN) -COMPATIBLE_IOCTL(PPPIOCGL2TPSTATS) -/* PPPOX */ -COMPATIBLE_IOCTL(PPPOEIOCSFWD32) -COMPATIBLE_IOCTL(PPPOEIOCDFWD) /* Big A */ /* sparc only */ /* Big Q for sound/OSS */ diff --git a/net/l2tp/l2tp_ppp.c b/net/l2tp/l2tp_ppp.c index 04d9946dcdba..8ef66513fbe0 100644 --- a/net/l2tp/l2tp_ppp.c +++ b/net/l2tp/l2tp_ppp.c @@ -1686,6 +1686,9 @@ static const struct proto_ops pppol2tp_ops = { .recvmsg = pppol2tp_recvmsg, .mmap = sock_no_mmap, .ioctl = pppox_ioctl, +#ifdef CONFIG_COMPAT + .compat_ioctl = pppox_ioctl, +#endif }; static const struct pppox_proto pppol2tp_proto = {
There are multiple implementations of the PPP ioctl interface in the kernel: - drivers/net/ppp/ppp_generic.c implements a generic interface for the /dev/ppp chardev used by some subdrivers. - drivers/net/ppp/pppox.c implements a socket based interface for pppoe, pptp and l2tp. - drivers/isdn/i4l/isdn_ppp.c is for the i4l ISDN stack All ioctl commands in the respective functions are compatible between 32-bit and 64-bit kernels, so we can simply mark the handlers themselves as compatible and stop listing the commands individually. Four commands (PPPIOCSCOMPRESS, PPPIOCSPASS, PPPIOCSACTIVE, and PPPIOCGIDLE) are incompatible on the user level but have a translation handler to make them compatible. I'm simplifying that compat handling in separate patches. The PPPIOCGUNIT and PPPIOCGCHAN ioctl commands are special, they are implemented on various other file descriptors, so we have to keep them listed as COMPATIBLE_IOCTL(). For the isdn_ppp code, additional ioctl commands are needed that have never had working compat handling, so I'm leaving that part out: If they are remaining users of i4l's ippp, they are not using compat mode today, and are highly unlikely in the future before the last ISDN network gets shut down. I4L has been deprecated since 2002. Signed-off-by: Arnd Bergmann <arnd@arndb.de> --- drivers/net/ppp/ppp_generic.c | 1 + drivers/net/ppp/pppoe.c | 3 +++ drivers/net/ppp/pptp.c | 3 +++ fs/compat_ioctl.c | 31 ------------------------------- net/l2tp/l2tp_ppp.c | 3 +++ 5 files changed, 10 insertions(+), 31 deletions(-) -- 2.20.0