powerpc/pseries: Avoid NULL pointer dereference when drmem is unavailable
authorLibor Pechacek <lpechacek@suse.cz>
Fri, 31 Jan 2020 13:28:29 +0000 (14:28 +0100)
committerGreg Kroah-Hartman <gregkh@linuxfoundation.org>
Fri, 17 Apr 2020 08:50:20 +0000 (10:50 +0200)
commit a83836dbc53e96f13fec248ecc201d18e1e3111d upstream.

In guests without hotplugagble memory drmem structure is only zero
initialized. Trying to manipulate DLPAR parameters results in a crash.

  $ echo "memory add count 1" > /sys/kernel/dlpar
  Oops: Kernel access of bad area, sig: 11 [#1]
  LE PAGE_SIZE=64K MMU=Hash SMP NR_CPUS=2048 NUMA pSeries
  ...
  NIP:  c0000000000ff294 LR: c0000000000ff248 CTR: 0000000000000000
  REGS: c0000000fb9d3880 TRAP: 0300   Tainted: G            E      (5.5.0-rc6-2-default)
  MSR:  8000000000009033 <SF,EE,ME,IR,DR,RI,LE>  CR: 28242428  XER: 20000000
  CFAR: c0000000009a6c10 DAR: 0000000000000010 DSISR: 40000000 IRQMASK: 0
  ...
  NIP dlpar_memory+0x6e4/0xd00
  LR  dlpar_memory+0x698/0xd00
  Call Trace:
    dlpar_memory+0x698/0xd00 (unreliable)
    handle_dlpar_errorlog+0xc0/0x190
    dlpar_store+0x198/0x4a0
    kobj_attr_store+0x30/0x50
    sysfs_kf_write+0x64/0x90
    kernfs_fop_write+0x1b0/0x290
    __vfs_write+0x3c/0x70
    vfs_write+0xd0/0x260
    ksys_write+0xdc/0x130
    system_call+0x5c/0x68

Taking closer look at the code, I can see that for_each_drmem_lmb is a
macro expanding into `for (lmb = &drmem_info->lmbs[0]; lmb <=
&drmem_info->lmbs[drmem_info->n_lmbs - 1]; lmb++)`. When drmem_info->lmbs
is NULL, the loop would iterate through the whole address range if it
weren't stopped by the NULL pointer dereference on the next line.

This patch aligns for_each_drmem_lmb and for_each_drmem_lmb_in_range
macro behavior with the common C semantics, where the end marker does
not belong to the scanned range, and alters get_lmb_range() semantics.
As a side effect, the wraparound observed in the crash is prevented.

Fixes: 6c6ea53725b3 ("powerpc/mm: Separate ibm, dynamic-memory data from DT format")
Cc: stable@vger.kernel.org # v4.16+
Signed-off-by: Libor Pechacek <lpechacek@suse.cz>
Signed-off-by: Michal Suchanek <msuchanek@suse.de>
Signed-off-by: Michael Ellerman <mpe@ellerman.id.au>
Link: https://lore.kernel.org/r/20200131132829.10281-1-msuchanek@suse.de
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
arch/powerpc/include/asm/drmem.h
arch/powerpc/platforms/pseries/hotplug-memory.c

index 3d76e1c..28c3d93 100644 (file)
@@ -27,12 +27,12 @@ struct drmem_lmb_info {
 extern struct drmem_lmb_info *drmem_info;
 
 #define for_each_drmem_lmb_in_range(lmb, start, end)           \
-       for ((lmb) = (start); (lmb) <= (end); (lmb)++)
+       for ((lmb) = (start); (lmb) < (end); (lmb)++)
 
 #define for_each_drmem_lmb(lmb)                                        \
        for_each_drmem_lmb_in_range((lmb),                      \
                &drmem_info->lmbs[0],                           \
-               &drmem_info->lmbs[drmem_info->n_lmbs - 1])
+               &drmem_info->lmbs[drmem_info->n_lmbs])
 
 /*
  * The of_drconf_cell_v1 struct defines the layout of the LMB data
index 4c3af2e..f188835 100644 (file)
@@ -223,7 +223,7 @@ static int get_lmb_range(u32 drc_index, int n_lmbs,
                         struct drmem_lmb **end_lmb)
 {
        struct drmem_lmb *lmb, *start, *end;
-       struct drmem_lmb *last_lmb;
+       struct drmem_lmb *limit;
 
        start = NULL;
        for_each_drmem_lmb(lmb) {
@@ -236,10 +236,10 @@ static int get_lmb_range(u32 drc_index, int n_lmbs,
        if (!start)
                return -EINVAL;
 
-       end = &start[n_lmbs - 1];
+       end = &start[n_lmbs];
 
-       last_lmb = &drmem_info->lmbs[drmem_info->n_lmbs - 1];
-       if (end > last_lmb)
+       limit = &drmem_info->lmbs[drmem_info->n_lmbs];
+       if (end > limit)
                return -EINVAL;
 
        *start_lmb = start;