Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Crash when using String::move on empty string #10938

Closed
1 task done
TD-er opened this issue Feb 6, 2025 · 0 comments · Fixed by #10945
Closed
1 task done

Crash when using String::move on empty string #10938

TD-er opened this issue Feb 6, 2025 · 0 comments · Fixed by #10945
Labels
Status: Awaiting triage Issue is waiting for triage

Comments

@TD-er
Copy link
Contributor

TD-er commented Feb 6, 2025

Board

ESP32-classic

Device Description

Not tried on several boards, but got it reproducible on ESP32-classic.

Hardware Configuration

Version

latest master (checkout manually)

IDE Name

PlatformIO

Operating System

Windows 11 / WSL2

Flash frequency

PSRAM enabled

yes

Upload speed

Description

Assigning an empty string using std::move may cause a crash.

So also assigning an empty string as result from a function will also eventually end up in crashes.

The fix is extremely simple; the memmove should only be called when rhs.len() > 0

Maybe the check should even be extended to if (rhs.len() && rhs.buffer())

void String::move(String &rhs) {
  if (buffer()) {
    if (capacity() >= rhs.len()) {
      if (rhs.len()) {  // <<== This check added
        memmove(wbuffer(), rhs.buffer(), rhs.length() + 1);
      }
      setLen(rhs.len());
      rhs.invalidate();
      return;
    } else {
      if (!isSSO()) {
        free(wbuffer());
        setBuffer(nullptr);
      }
    }
  }
  if (rhs.isSSO()) {
    setSSO(true);
    memmove(sso.buff, rhs.sso.buff, sizeof(sso.buff));
  } else {
    setSSO(false);
    setBuffer(rhs.wbuffer());
  }
  setCapacity(rhs.capacity());
  setLen(rhs.len());
  rhs.setSSO(false);
  rhs.setCapacity(0);
  rhs.setBuffer(nullptr);
  rhs.setLen(0);
}
#endif

N.B. it is a bit confusing that buffer() can return both a nullptr as well as a pointer when it is an empty string.
This might cause other issues as well as the rest of the coding-style in the class suggests it is more like checking for a non-empty string.

N.B.2. This bug is likely present for quite a long time, so maybe also needs backporting?

Sketch

N.B. I did not exactly test this sketch below, but it is a very stripped down gist of the code which is causing crashes in my project.

++
String foo() {
  return String();
}

// Important to note that the strings have to be constructed first 
// and then assigned a new value.
String bar, working, crashing, crashing_too;

bar = foo();
working = bar;
crashing = std::move(bar);
crashing_too = foo();

Debug Message

Part of crash log, pointing clearly to `String::move`


Guru Meditation Error: Core  1 panic'ed (LoadProhibited). Exception was unhandled.

Core  1 register dump:
PC      : 0x4008b05e  PS      : 0x00060730  A0      : 0x80160436  A1      : 0x3ffb1760  
A2      : 0x3ffb17d8  A3      : 0x00000000  A4      : 0x00000001  A5      : 0x3ffb1770  
A6      : 0x3ffb1760  A7      : 0x3ffb1770  A8      : 0x00000000  A9      : 0x00000000  
A10     : 0x3ffb1808  A11     : 0x00000001  A12     : 0x00000007  A13     : 0x3ffb1770  
A14     : 0x0000004e  A15     : 0x0000003f  SAR     : 0x00000010  EXCCAUSE: 0x0000001c  
EXCVADDR: 0x00000000  LBEG    : 0x4008af4c  LEND    : 0x4008af62  LCOUNT  : 0xffffffff  


Backtrace: 0x4008b05b:0x3ffb1760 0x40160433:0x3ffb1780 0x40131e06:0x3ffb17a0 0x40101a5f:0x3ffb1890 0x4017312b:0x3ffb1ab0 0x40125bb5:0x3ffb1ad0 0x40157a4f:0x3ffb1c70 0x40152899:0x3ffb1e20 0x400d61fe:0x3ffb1e40 0x400e7d1e:0x3ffb1e60 0x400ecfe2:0x3ffb1e80 0x40137ca5:0x3ffb2000 0x4016a671:0x3ffb2040 0x40124ed5:0x3ffb2110 0x40162daf:0x3ffb2290
  #0  0x4008b05b in memmove at /builds/idf/crosstool-NG/.build/xtensa-esp-elf/src/newlib/newlib/libc/string/memmove.c:66
  #1  0x40160433 in String::move(String&) at ??:?
  #2  0x40131e06 in ESPEasy_TouchHandler::loadTouchObjects(EventStruct*) at ??:?
  #3  0x40101a5f in Plugin_123(unsigned char, EventStruct*, String&) at ??:?
  #4  0x4017312b in _Z21getDeviceIndex_sorted13deviceIndex_t$isra$0 at ??:?
  #5  0x40125bb5 in PluginCall(unsigned char, EventStruct*, String&) at ??:?
  #6  0x40157a4f in handle_devices() at :?
  #7  0x40152899 in UriGlob::canHandle(String const&, std::vector<String, std::allocator<String> >&) at :?
  #8  0x400d61fe in std::function<void ()>::operator()() const at .platformio/packages/toolchain-xtensa-esp-elf/xtensa-esp-elf/include/c++/13.2.0/bits/std_function.h:591
  #9  0x400e7d1e in FunctionRequestHandler::handle(WebServer&, http_method, String const&) at :?
  #10 0x400ecfe2 in WebServer::handleClient() at ??:?
  #11 0x40137ca5 in run10TimesPerSecond() at ??:?
  #12 0x4016a671 in _ZN17ESPEasy_Scheduler15handle_scheduleEv$constprop$0 at ??:?
  #13 0x40124ed5 in ESPEasy_loop() at ??:?
  #14 0x40162daf in app_main at ??:?

Other Steps to Reproduce

No response

I have checked existing issues, online documentation and the Troubleshooting Guide

  • I confirm I have checked existing issues, online documentation and Troubleshooting guide.
@TD-er TD-er added the Status: Awaiting triage Issue is waiting for triage label Feb 6, 2025
TD-er added a commit to TD-er/arduino-esp32 that referenced this issue Feb 6, 2025
TD-er added a commit to TD-er/arduino-esp32 that referenced this issue Feb 6, 2025
Fixes: espressif#10938
Keep allocated memory when rhs fits

Use case: Appending to a String with pre-allocated memory (e.g. from `reserve()`)
No need to move 0-termination char in String::move
Simplify calls to String::copy

A lot of the same checks were done before calling `copy()` which should be done in the `copy()` function itself.
String::copy() Should not copy more than given length
Fix potential out of range in String::concat

There is no prerequisite the given array has to be a 0-terminated char array.
So we should only copy the length that has been given.

The `setLen()` function will make sure the internal string is 0-terminated.
So no need to dangerously assume there will be 1 more byte to copy
Allow String::concat(const String &s) with s.buffer() == nullptr

When constructing a String object, the internal buffer is a nullptr.
However concatenating this to another String would return `false` while this is perfectly fine to do.
me-no-dev pushed a commit that referenced this issue Feb 13, 2025
Fixes: #10938
Keep allocated memory when rhs fits

Use case: Appending to a String with pre-allocated memory (e.g. from `reserve()`)
No need to move 0-termination char in String::move
Simplify calls to String::copy

A lot of the same checks were done before calling `copy()` which should be done in the `copy()` function itself.
String::copy() Should not copy more than given length
Fix potential out of range in String::concat

There is no prerequisite the given array has to be a 0-terminated char array.
So we should only copy the length that has been given.

The `setLen()` function will make sure the internal string is 0-terminated.
So no need to dangerously assume there will be 1 more byte to copy
Allow String::concat(const String &s) with s.buffer() == nullptr

When constructing a String object, the internal buffer is a nullptr.
However concatenating this to another String would return `false` while this is perfectly fine to do.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Status: Awaiting triage Issue is waiting for triage
Projects
None yet
1 participant