Skip to content

Calls made to malloc/free will enable interrupts on return #6246

Closed
@mhightower83

Description

@mhightower83

Basic Infos

  • This issue complies with the issue POLICY doc.
  • I have read the documentation at readthedocs and the issue is not addressed there.
  • I have tested that the issue is present in current master branch (aka latest git).
  • I have searched the issue tracker for a similar issue.
  • If there is a stack dump, I have decoded it.
  • I have filled out all fields below.

Platform

  • Hardware: ESP-12
  • Core Version: 5a47cab
  • Development Env: Arduino IDE
  • Operating System: Ubuntu

Settings in IDE

  • Module: Adafruit Feather HUZZAH ESP8266
  • Flash Mode: qio
  • Flash Size: 4MB
  • lwip Variant: v2 Lower Memory
  • Reset Method: nodemcu
  • Flash Frequency: 40Mhz
  • CPU Frequency: 80Mhz
  • Upload Using: SERIAL
  • Upload Speed: 115200 (serial upload only)

Problem Description

To simulate an IRQ handler calling malloc I used xt_rsil() to set INTLEVEL.
when malloc/free are called with an elevated INTLEVEL, INTLEVEL is set to 0 on return.

I noticed that ets_intr_lock() and ets_intr_unlock() are being used to handle critical section for umm_malloc. ets_intr_unlock() does not restore the previous INTLEVEL; it does an rsil a2, 0.
Ref: #2218 (comment) Thus causing the immediate problem.

MCVE Sketch

#include <ets_sys.h>
#include <interrupts.h>

#ifndef xt_rsr_ps
#define xt_rsr_ps()  (__extension__({uint32_t state2; __asm__ __volatile__("rsr.ps %0" : "=a" (state2)); state2;}))
#endif

/* 
 * A printf routine that can safely print with interrupts disabled
 * and does no "malloc lib" calls. 
 * 
 * The default printf will do a malloc call, spoiling the test.
 */
 
// ROM _putc1, ignores CRs and sends CR/LF for LF, newline.
// Always returns character sent.
extern "C" int constexpr (*_putc1)(int) = (int (*)(int))0x40001dcc;

extern "C" void uart_buff_switch(uint8_t);

int _sz_printf_P(const size_t buf_len, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
int _sz_printf_P(const size_t buf_len, const char *fmt, ...) {
  
#ifdef DEBUG_ESP_PORT
#define VALUE(x) __STRINGIFY(x)
  // Preprocessor and compiler together will optimize away the if.
  if (strcmp("Serial1", VALUE(DEBUG_ESP_PORT)) == 0) {
    uart_buff_switch(1U);
  } else {
    uart_buff_switch(0U);
  }
#else
  uart_buff_switch(0U); // Side effect, clears RX FIFO
#endif

  char __aligned(4) ram_buf[buf_len];
  ets_memcpy(ram_buf, fmt, buf_len);
  va_list argPtr;
  va_start(argPtr, fmt);
  int result = ets_vprintf(_putc1, ram_buf, argPtr);
  va_end(argPtr);
  return result;
}

#define printf(fmt, ...) _sz_printf_P((sizeof(fmt) + 3) & ~0x03U, PSTR(fmt), ##__VA_ARGS__)



void setup() {
  Serial.begin(115200);
  delay(20);
  uint32_t beforePS, afterPS;

  printf("\n\n\nShow what happens to an elevated INTLEVEL after a \"malloc\" and \"free\"?\n");
  
  printf("\n  Preset INTLEVEL with \"xt_rsil(3);\"\n");
  xt_rsil(3);
  // Confirm that printf() is not spoiling our test.
  //   * A pass while the other tests fail is confirming.
  //   * A pass while the other tests pass tells nothing.
  beforePS = xt_rsr_ps();
  printf("  %s\n", "Indirectly confirm that printf() is not calling malloc and spoiling our test?");
  afterPS = xt_rsr_ps();
  printf("  PS register before/after \"printf();\":  0x%04X/0x%04X, %s\n", beforePS, afterPS, (beforePS ==  afterPS) ? "Pass" : "Fail!");

  printf("\n  Preset INTLEVEL with \"xt_rsil(4);\"\n");
  xt_rsil(4);
  beforePS = xt_rsr_ps();
  char *pc = (char *) malloc(3);
  afterPS = xt_rsr_ps();
  printf("  PS register before/after \"malloc(3);\": 0x%04X/0x%04X, %s\n", beforePS, afterPS, (beforePS ==  afterPS) ? "Pass" : "Fail!");

  *pc = '1'; // w/o this, malloc/free appears to be optimized away.
  
  printf("\n  Preset INTLEVEL with \"xt_rsil(5);\"\n");
  xt_rsil(5);
  beforePS = xt_rsr_ps();
  free(pc);
  afterPS = xt_rsr_ps();
  printf("  PS register before/after \"free(pc);\":  0x%04X/0x%04X, %s\n", beforePS, afterPS, (beforePS ==  afterPS) ? "Pass" : "Fail!");
  
  xt_rsil(0);
  printf("\n\n");
}

void loop() {
  // Something to show we did not hang
  static size_t count = 0;
  delay(1000);
  printf(".");
  if (((++count) % 72) == 0)
    printf("\n");
}

Debug Messages

Show what happens to an elevated INTLEVEL after a "malloc" and "free"?

  Preset INTLEVEL with "xt_rsil(3);"
  Indirectly confirm that printf() is not calling malloc and spoiling our test?
  PS register before/after "printf();":  0x0023/0x0023, Pass

  Preset INTLEVEL with "xt_rsil(4);"
  PS register before/after "malloc(3);": 0x0024/0x0020, Fail!

  Preset INTLEVEL with "xt_rsil(5);"
  PS register before/after "free(pc);":  0x0025/0x0020, Fail!

Other Thoughts and Observations

The comments in the code indicate that the locking macros must be allowed to nest. In regards to the comment about _umm_malloc() having a call to _umm_free() I think the code may have been changed after that was written; however, there are calls to _umm_malloc() and _umm_free() from _umm_realloc() that would require nest support.

/*
* A couple of macros to make it easier to protect the memory allocator
* in a multitasking system. You should set these macros up to use whatever
* your system uses for this purpose. You can disable interrupts entirely, or
* just disable task switching - it's up to you
*
* NOTE WELL that these macros MUST be allowed to nest, because umm_free() is
* called from within umm_malloc()
*/
#define UMM_CRITICAL_ENTRY() ets_intr_lock()
#define UMM_CRITICAL_EXIT() ets_intr_unlock()

I have explored using XTOS_SET_MIN_INTLEVEL and XTOS_RESTORE_INTLEVEL from
ESP8266_RTOS_SDK/components/esp8266/include/xtensa/xtruntime.h for the UMM_CRITICAL_ macros. It has been working well for me. And there are comments in xtruntime.h about how it works, something I could never find for ets_intr_lock(). I like that you can specify an INTLEVEL to XTOS_SET_MIN_INTLEVEL and if the current level is higher it will keep it, instead of demoting it, as ets_intr_lock did.

An interesting side benefit from the broken nesting with the ets_intr_lock was that in one section of _umm_realloc() , after a _umm_malloc() call, interrupts were enabled before memcpy() was called. Curiously - this looks like it was safe. All variables referenced at this point were on the stack. I say benefit in the sense that a large block of data could be copied without exceeding the 10us max time for interrupts off.

/*
* Now _umm_malloc() a new/ one, copy the old data to the new block, and
* free up the old block, but only if the malloc was sucessful!
*/
if( (ptr = _umm_malloc( size )) ) {
memcpy( ptr, oldptr, curSize );
_umm_free( oldptr );
}
}
/* Release the critical section... */
UMM_CRITICAL_EXIT();

I have explored the idea of lightly refactoring _umm_realloc() to support locks off during copy/memmove to reduce the over 10us burdon. Of course, when called from an IRQ routine it has no effect, interrupts are off for the whole process. It seems to be working; however, I may not have a sketch that is demanding enough.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions