test_pages_in_a_zone() does not account for the possibility of missing sections in the given pfn range. pfn_valid_within always returns 1 when CONFIG_HOLES_IN_ZONE is not set, allowing invalid pfns from missing sections to pass the test, leading to a kernel oops. Wrap an additional pfn loop with PAGES_PER_SECTION granularity to check for missing sections before proceeding into the zone-check code. This also prevents a crash from offlining memory devices with missing sections. Despite this, it may be a good idea to keep the related patch '[PATCH 3/3] drivers: memory: prohibit offlining of memory blocks with missing sections' because missing sections in a memory block may lead to other problems not covered by the scope of this fix.
Unified · Split
| @@ -1375,23 +1375,30 @@ int is_mem_section_removable(unsigned long start_pfn, unsigned long nr_pages) · 1374 unmodified lines | ||
| 1375 | 1375 | */ |
| 1376 | 1376 | int test_pages_in_a_zone(unsigned long start_pfn, unsigned long end_pfn) |
| 1377 | 1377 | { |
| 1378 | − unsigned long pfn; | |
| 1378 | + unsigned long pfn, sec_end_pfn; | |
| 1379 | 1379 | struct zone *zone = NULL; |
| 1380 | 1380 | struct page *page; |
| 1381 | 1381 | int i; |
| 1382 | − for (pfn = start_pfn; | |
| 1382 | + for (pfn = start_pfn, sec_end_pfn = SECTION_ALIGN_UP(start_pfn); | |
| 1383 | 1383 | pfn < end_pfn; |
| 1384 | − pfn += MAX_ORDER_NR_PAGES) { | |
| 1385 | − i = 0; | |
| 1386 | − /* This is just a CONFIG_HOLES_IN_ZONE check.*/ | |
| 1387 | − while ((i < MAX_ORDER_NR_PAGES) && !pfn_valid_within(pfn + i)) | |
| 1388 | − i++; | |
| 1389 | − if (i == MAX_ORDER_NR_PAGES) | |
| 1384 | + pfn = sec_end_pfn + 1, sec_end_pfn += PAGES_PER_SECTION) { | |
| 1385 | + /* Make sure the memory section is present first */ | |
| 1386 | + if (!present_section_nr(pfn_to_section_nr(pfn))) | |
| 1390 | 1387 | continue; |
| 1391 | − page = pfn_to_page(pfn + i); | |
| 1392 | − if (zone && page_zone(page) != zone) | |
| 1393 | − return 0; | |
| 1394 | − zone = page_zone(page); | |
| 1388 | + for (; pfn < sec_end_pfn && pfn < end_pfn; | |
| 1389 | + pfn += MAX_ORDER_NR_PAGES) { | |
| 1390 | + i = 0; | |
| 1391 | + /* This is just a CONFIG_HOLES_IN_ZONE check.*/ | |
| 1392 | + while ((i < MAX_ORDER_NR_PAGES) && | |
| 1393 | + !pfn_valid_within(pfn + i)) | |
| 1394 | + i++; | |
| 1395 | + if (i == MAX_ORDER_NR_PAGES) | |
| 1396 | + continue; | |
| 1397 | + page = pfn_to_page(pfn + i); | |
| 1398 | + if (zone && page_zone(page) != zone) | |
| 1399 | + return 0; | |
| 1400 | + zone = page_zone(page); | |
| 1401 | + } | |
| 1395 | 1402 | } |
| 1396 | 1403 | return 1; |
| 1397 | 1404 | } |