Skip to content

ESP32S3 and ESP32S2 (and potentially others) ROM bug in Cache_Flash_To_SPIRAM_Copy (IDFGH-14494) #15263

Open
@EliteTK

Description

@EliteTK

Answers checklist.

  • I have read the documentation ESP-IDF Programming Guide and the issue is not addressed there.
  • I have updated my IDF branch (master or release) to the latest version and checked that the issue is present there.
  • I have searched the issue tracker for a similar issue and not found a similar issue.

General issue report

While working on implementing XiP from PSRAM for the rust esp-hal project I reverse engineered the Cache_Flash_To_SPIRAM_Copy from the ESP32S3 ROM (revision 0). I have reproduced my cleaned up version of the function (reverse engineered with the help of Ghidra) below:

#define SPARE_PAGE 0x1ff
#define SPARE_PAGE_ADDR (volatile void *)(0x3dff0000)
uint32_t Cache_Flash_To_SPIRAM_Copy(
    uint32_t bus, uint32_t bus_addr, uint32_t start_page, uint32_t *page0_page
)
{
    volatile uint32_t *mmu_table = (volatile uint32_t *)DR_REG_MMU_TABLE;
    uint32_t saved_mapping = mmu_table[SPARE_PAGE];
    uint32_t start, end;
    if (bus == CACHE_IBUS) {
        start = CACHE_IROM_MMU_START;
        end = CACHE_IROM_MMU_END;
    } else {
        start = CACHE_DROM_MMU_START;
        end = CACHE_DROM_MMU_END;
    }
    Cache_WriteBack_All();
    for (uint32_t i = start >> 2; i < end >> 2; i++) {
        uint32_t mapping = mmu_table[i];
        if ((mapping & (SOC_MMU_INVALID | SOC_MMU_TYPE)) == (SOC_MMU_VALID | SOC_MMU_ACCESS_FLASH)) {
            bool is_page0 = false;
            if ((mapping & SOC_MMU_VALID_VAL_MASK) == 0) {
                if (*page0_page != INVALID_PHY_PAGE) {
                    mmu_table[i] = *page0_page |
                               SOC_MMU_ACCESS_SPIRAM;
                    continue;
                }
                is_page0 = true;
            }
            mmu_table[SPARE_PAGE] = start_page | SOC_MMU_ACCESS_SPIRAM;
            Cache_Invalidate_Addr(SPARE_PAGE_ADDR, MMU_PAGE_SIZE);
            memcpy(SPARE_PAGE_ADDR,
                   (void *)(i * MMU_PAGE_SIZE + bus_addr),
                   MMU_PAGE_SIZE);
            Cache_WriteBack_Addr(SPARE_PAGE_ADDR, MMU_PAGE_SIZE);
            mmu_table[i] = start_page | SOC_MMU_ACCESS_SPIRAM;
            // BUG: The ordering here is wrong, the ROM increments start_page
            // before assigning it to *page0_page, so every page0 mapping after
            // the first actually ends up pointing at the wrong place which will
            // end up containing non-page0 content (or, if all the page0
            // mappings happen to be the last ones in the MMU table, it will
            // point into an uninitialised page)
            start_page += 1;
            if (is_page0) *page0_page = start_page;
        }
    };
    mmu_table[SPARE_PAGE] = saved_mapping;
    Cache_Invalidate_Addr(SPARE_PAGE_ADDR, MMU_PAGE_SIZE);
    return start_page;
}

As can be seen from the comment and code above, the start_page variable is incremented before the page0_page in-out parameter is updated. This means that the start_page variable will be referring to the PSRAM page immediately after the location of the page which contains flash page 0 data.

This means that only the first page which is mapped to page 0 in flash will be handled correctly and all subsequent pages will end up pointing at a copy of a differnet flash page or at an uninitialised PSRAM page.

In cases where the page 0 mapping points at an uninitialised PSRAM page, the unused page may even be allocated for other purposes.

I believe a fix for this may be to re-implement the functionality of this function entirely in the IDF or to ensure that this function is never called if there is more than 1 flash page 0 mapping.

I confirmed this bug in a bare-metal environment by mapping flash page 0 multiple times and calling this function.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions