8
33
Fork
You've already forked blackmagic
23

Fix: avoid a NULL ptr dereference in HL-remote from !GV# #1866

Merged
ALTracer merged 8 commits from fix/remote-nullptr into main 2024年07月18日 01:08:49 +02:00
ALTracer commented 2024年07月13日 13:30:37 +02:00 (Migrated from github.com)
Copy link

Detailed description

  • No new features.
  • The existing problem is Read of NULL in remote.c on platforms which implement a stub for platform_target_voltage() as returning a NULL char*.
  • This PR offers three possible solutions to this problem, two of them behind #if 0 because of bigger flash impact.

Before deploying a development build of BMF to stlinkv3e in RDP2 (so no inception debug) I tested my MPU setup idea on my trusty blackpill-f411ce. As I was tapping into the newfound power that is a working MPU on Cortex-M, I also felt like adding (to region 6 "uncaching" top 16 KiB out of 128 KiB of F411CE SRAM) a region 7 to catch any accesses to address 0, that is, NULL dereferences as a known UB in C. Even though libopencm3 doesn't help much, I have already had written the helper functions and tested them elsewhere, so appending two lines of register pokes was trivial.

Imagine my surprise when my mem_manage_handler triggered a reboot (as coded) not during GDB operation, but on attempting a BMDA scan/read. Adding a swlink-bluepillplus (with a cheesy 4-pin cable) revealed this (now that the debugger is present, my handler executes a coded breakpoint instead) beautiful stack backtrace:

(gdb) bt
#0 mem_manage_handler () at ../src/platforms/common/blackpill-f4/blackpill-f4.c:334
#1 <signal handler called>
#2 0x080209c6 in strlen ()
#3 0x08006128 in remote_respond_string (str=0x0, response_code=75 'K') at ../src/remote.c:97
#4 0x0800660a in remote_packet_process_general (packet_len=<optimized out>, packet=0x20001198 <pbuf> "GV") at ../src/remote.c:306
#5 remote_packet_process (packet=packet@entry=0x20001198 <pbuf> "GV", packet_length=packet_length@entry=2) at ../src/remote.c:604
#6 0x08005a64 in consume_remote_packet (packet=packet@entry=0x20001198 <pbuf> "GV", size=size@entry=1024) at ../src/gdb_packet.c:91
#7 0x08005ae4 in gdb_getpacket (packet=packet@entry=0x20001198 <pbuf> "GV", size=size@entry=1024) at ../src/gdb_packet.c:147
#8 0x08005f7c in bmp_poll_loop () at ../src/main.c:65
#9 main () at ../src/main.c:84

This was working fine with MPU disabled -- because at 0x0 on STM32 we have the 0x08000000 Internal Flash mapped, and it starts with 0x20001000 for Initial_MSP value of +4KiB, that is bytes 0x00, 0x10, 0x00, 0x20. Or, earlier, some bigger address (actual end of SRAM) divisible by 256, so that it starts with 0x00. strlen(NULL) encounters a null-terminator and returns a 0 length.

  1. Robust solution: in remote_packet_process_general(), catch the NULL returned by platform and turn it into a fixed string, kinda like feeding it to %s on some libc's, e.g. "(null)"
  2. Thin solution: in remote_respond_string(), catch the NULL passed from anywhere in HL-remote stack, skip the strlen & loop and just respond with EOM. A branch-if-zero should be really cheap.
  3. Proper solution: fix all the platforms to return something else. Some platforms return "ABSENT!". However, this needs cooperation with command.c

There are multiple calls of this function in command.c which print nothing or print said fixed string verbatim, or print a decimal-formatted platform ADC voltage.

	if (platform_target_voltage())
		gdb_outf("Target voltage: %s\n", platform_target_voltage());

I tried to return an empty string consisting of a null-terminator, and this no longer passes a NULL pointer around, but also prints this:
Target voltage: Volt
So whatever fixed string response we choose to use, both callsites need a way to detect it (strcmp?) and suppress the print.

Your checklist for this pull request

Closing issues

## Detailed description * No new features. * The existing problem is Read of NULL in remote.c on platforms which implement a stub for `platform_target_voltage()` as returning a NULL char*. * This PR offers three possible solutions to this problem, two of them behind `#if 0` because of bigger flash impact. Before deploying a development build of BMF to stlinkv3e in RDP2 (so no inception debug) I tested my MPU setup idea on my trusty blackpill-f411ce. As I was tapping into the newfound power that is a working MPU on Cortex-M, I also felt like adding (to region 6 "uncaching" top 16 KiB out of 128 KiB of F411CE SRAM) a region 7 to catch any accesses to address 0, that is, NULL dereferences as a known UB in C. Even though libopencm3 doesn't help much, I have already had written the helper functions and tested them elsewhere, so appending two lines of register pokes was trivial. Imagine my surprise when my mem_manage_handler triggered a reboot (as coded) not during GDB operation, but on attempting a BMDA scan/read. Adding a swlink-bluepillplus (with a cheesy 4-pin cable) revealed this (now that the debugger is present, my handler executes a coded breakpoint instead) beautiful stack backtrace: ```c (gdb) bt #0 mem_manage_handler () at ../src/platforms/common/blackpill-f4/blackpill-f4.c:334 #1 <signal handler called> #2 0x080209c6 in strlen () #3 0x08006128 in remote_respond_string (str=0x0, response_code=75 'K') at ../src/remote.c:97 #4 0x0800660a in remote_packet_process_general (packet_len=<optimized out>, packet=0x20001198 <pbuf> "GV") at ../src/remote.c:306 #5 remote_packet_process (packet=packet@entry=0x20001198 <pbuf> "GV", packet_length=packet_length@entry=2) at ../src/remote.c:604 #6 0x08005a64 in consume_remote_packet (packet=packet@entry=0x20001198 <pbuf> "GV", size=size@entry=1024) at ../src/gdb_packet.c:91 #7 0x08005ae4 in gdb_getpacket (packet=packet@entry=0x20001198 <pbuf> "GV", size=size@entry=1024) at ../src/gdb_packet.c:147 #8 0x08005f7c in bmp_poll_loop () at ../src/main.c:65 #9 main () at ../src/main.c:84 ``` This was working fine with MPU disabled -- because at 0x0 on STM32 we have the 0x08000000 Internal Flash mapped, and it starts with 0x20001000 for Initial_MSP value of +4KiB, that is bytes 0x00, 0x10, 0x00, 0x20. Or, earlier, some bigger address (actual end of SRAM) divisible by 256, so that it starts with 0x00. `strlen(NULL)` encounters a null-terminator and returns a 0 length. 1. Robust solution: in remote_packet_process_general(), catch the NULL returned by platform and turn it into a fixed string, kinda like feeding it to %s on some libc's, e.g. "(null)" 2. Thin solution: in remote_respond_string(), catch the NULL passed from anywhere in HL-remote stack, skip the strlen & loop and just respond with EOM. A branch-if-zero should be really cheap. 3. Proper solution: fix all the platforms to return something else. Some platforms return "ABSENT!". However, this needs cooperation with command.c There are multiple calls of this function in command.c which print nothing or print said fixed string verbatim, or print a decimal-formatted platform ADC voltage. ```c if (platform_target_voltage()) gdb_outf("Target voltage: %s\n", platform_target_voltage()); ``` I tried to return an empty string consisting of a null-terminator, and this no longer passes a NULL pointer around, but also prints this: `Target voltage: Volt` So whatever fixed string response we choose to use, both callsites need a way to detect it (strcmp?) and suppress the print. ## Your checklist for this pull request * [x] I've read the [Code of Conduct](https://github.com/blackmagic-debug/blackmagic/blob/main/CODE_OF_CONDUCT.md) * [x] I've read the [guidelines for contributing](https://github.com/blackmagic-debug/blackmagic/blob/main/CONTRIBUTING.md) to this repository * [x] It builds for hardware native (see [Building the firmware](https://github.com/blackmagic-debug/blackmagic?tab=readme-ov-file#building-black-magic-debug-firmware)) * [x] It builds as BMDA (see [Building the BMDA](https://github.com/blackmagic-debug/blackmagic?tab=readme-ov-file#building-black-magic-debug-app)) *and should not affect it* * [x] I've tested it to the best of my ability * [x] My commit messages provide a useful short description of what the commits do ## Closing issues
ALTracer commented 2024年07月15日 20:24:36 +02:00 (Migrated from github.com)
Copy link

I'm confused by this implementation for native. Does it implement a binary logic measurement instead of a full 12-bit analog sample?
github.com/blackmagic-debug/blackmagic@0c4f3040da/src/platforms/native/platform.c (L406-L409)
If so, then I should re-update it to say not OK/ABSENT! but Present/Absent, which is what ftdi_target_voltage() returns (there's no ADC on FTDI MPSSE)... or leave it alone.
github.com/blackmagic-debug/blackmagic@0c4f3040da/src/platforms/hosted/ftdi_bmp.c (L822-L827)
Basically f072, f3, 96b_carbon used "ABSENT!" for unimplemented, but now we're moving to "Unknown".

I'm confused by this implementation for native. Does it implement a binary logic measurement instead of a full 12-bit analog sample? https://github.com/blackmagic-debug/blackmagic/blob/0c4f3040dab8c4ec86a3a447f5369c1cdcc108f6/src/platforms/native/platform.c#L406-L409 If so, then I should re-update it to say not OK/ABSENT! but Present/Absent, which is what `ftdi_target_voltage()` returns (there's no ADC on FTDI MPSSE)... or leave it alone. https://github.com/blackmagic-debug/blackmagic/blob/0c4f3040dab8c4ec86a3a447f5369c1cdcc108f6/src/platforms/hosted/ftdi_bmp.c#L822-L827 Basically f072, f3, 96b_carbon used "ABSENT!" for unimplemented, but now we're moving to "Unknown".

Prior to HW1, BMP had only voltage sense, not ADC-based voltage measurement (as you put it "binary logic measurement"). That code is determining if there is a voltage present or not on its sense pin, which is a separate thing to detection not being implemented.

As much as it does increase the footprint in Flash for this code, "Absent"/"Present" is a much better pair of terms than "OK" and "ABSENT!" are. To this end, we're happy for you to make it match the MPSSE implementation as that's the most accurate handling for HW0 native hardware. Makes everything nice and consistent.

Prior to HW1, BMP had only voltage sense, not ADC-based voltage measurement (as you put it "binary logic measurement"). That code is determining if there is a voltage present or not on its sense pin, which is a separate thing to detection not being implemented. As much as it does increase the footprint in Flash for this code, "Absent"/"Present" is a much better pair of terms than "OK" and "ABSENT!" are. To this end, we're happy for you to make it match the MPSSE implementation as that's the most accurate handling for HW0 native hardware. Makes everything nice and consistent.
ALTracer commented 2024年07月15日 22:20:22 +02:00 (Migrated from github.com)
Copy link

Rebased away the wrong mass-rename and placed a separate commit for native.
Noticed one more return NULL in FTDI impl and added commit for return "Unknown" there (when no dedicated pin registered in cable structure)
Rebased to main.

Rebased away the wrong mass-rename and placed a separate commit for `native`. Noticed one more `return NULL` in FTDI impl and added commit for `return "Unknown"` there (when no dedicated pin registered in cable structure) Rebased to main.
ALTracer commented 2024年07月15日 22:25:29 +02:00 (Migrated from github.com)
Copy link

Original small-scope rejected patches for archival purposes:

  1. Robust solution
diff --git a/src/remote.c b/src/remote.c
index d4909917..ae9559a8 100644
--- a/src/remote.c
+++ b/src/remote.c
@@ -258,7 +258,11 @@ static void remote_packet_process_general(char *packet, const size_t packet_len)
 	(void)packet_len;
 	switch (packet[1]) {
 	case REMOTE_VOLTAGE:
-		remote_respond_string(REMOTE_RESP_OK, platform_target_voltage());
+		// Some platforms return a NULL meaning "not implemented"
+		const char *voltage = platform_target_voltage();
+		if (!voltage)
+			voltage = "(null)";
+		remote_respond_string(REMOTE_RESP_OK, voltage);
 		break;
 	case REMOTE_NRST_SET:
 		platform_nrst_set_val(packet[2] == '1');
  1. Thin solution
diff --git a/src/remote.c b/src/remote.c
index d4909917..a87cc05b 100644
--- a/src/remote.c
+++ b/src/remote.c
@@ -94,6 +94,10 @@ static void remote_respond_string(const char response_code, const char *const st
 {
 	gdb_if_putchar(REMOTE_RESP, 0);
 	gdb_if_putchar(response_code, 0);
+	if (!str) { // Avoid nullptr responses
+		gdb_if_putchar(REMOTE_EOM, 1);
+		return;
+	}
 	const size_t str_length = strlen(str);
 	for (size_t idx = 0; idx < str_length; ++idx) {
 		const char chr = str[idx];
Original small-scope rejected patches for archival purposes: 1. Robust solution ```diff diff --git a/src/remote.c b/src/remote.c index d4909917..ae9559a8 100644 --- a/src/remote.c +++ b/src/remote.c @@ -258,7 +258,11 @@ static void remote_packet_process_general(char *packet, const size_t packet_len) (void)packet_len; switch (packet[1]) { case REMOTE_VOLTAGE: - remote_respond_string(REMOTE_RESP_OK, platform_target_voltage()); + // Some platforms return a NULL meaning "not implemented" + const char *voltage = platform_target_voltage(); + if (!voltage) + voltage = "(null)"; + remote_respond_string(REMOTE_RESP_OK, voltage); break; case REMOTE_NRST_SET: platform_nrst_set_val(packet[2] == '1'); ``` 2. Thin solution ```diff diff --git a/src/remote.c b/src/remote.c index d4909917..a87cc05b 100644 --- a/src/remote.c +++ b/src/remote.c @@ -94,6 +94,10 @@ static void remote_respond_string(const char response_code, const char *const st { gdb_if_putchar(REMOTE_RESP, 0); gdb_if_putchar(response_code, 0); + if (!str) { // Avoid nullptr responses + gdb_if_putchar(REMOTE_EOM, 1); + return; + } const size_t str_length = strlen(str); for (size_t idx = 0; idx < str_length; ++idx) { const char chr = str[idx]; ```
dragonmux left a comment
Copy link

This looks great! There are a couple of stand-outs in the usage side of things as we hadn't appreciated that the firmware already didn't do the antithetical thing of adding " Volt" to the end of target voltage display, allowing further simplification, but with those taken care of, we're happy for this to get merged.

This looks great! There are a couple of stand-outs in the usage side of things as we hadn't appreciated that the firmware already didn't do the antithetical thing of adding " Volt" to the end of target voltage display, allowing further simplification, but with those taken care of, we're happy for this to get merged.
@ -212,8 +212,7 @@ static bool cmd_jtag_scan(target_s *target, int argc, const char **argv)
(void)argc;

Given we're defining platform_target_voltage() to be non-null, think we can get away with simplifying this to just:

	gdb_outf("Target voltage: %s\n", platform_target_voltage());

And likewise for the other command.c invocations - we think displaying "Target voltage: Unknown" on platforms that don't implement this is very acceptable.

Given we're defining `platform_target_voltage()` to be non-null, think we can get away with simplifying this to just: ```suggestion gdb_outf("Target voltage: %s\n", platform_target_voltage()); ``` And likewise for the other command.c invocations - we think displaying "Target voltage: Unknown" on platforms that don't implement this is very acceptable.
@ -480,7 +480,7 @@ int cl_execute(bmda_cli_options_s *opt)
platform_nrst_set_val(opt->opt_connect_under_reset);

This gets a similar request as the command.c version, but including the suggestion to let's drop the world "Volt" from here. This has been the source of all of the funky in BMDA that the firmware doesn't have.

It should be up to the platform_target_voltage() implementation to include the unit on the end of the formatted string, as in "3.3V" or "0.0V" or w/e. That allows the display of "Target voltage: Unknown" or "Target voltage: Present" etc to be nice and consistent across BMDA and the firmware.

This gets a similar request as the command.c version, but including the suggestion to let's drop the world "Volt" from here. This has been the source of all of the funky in BMDA that the firmware doesn't have. It should be up to the `platform_target_voltage()` implementation to include the unit on the end of the formatted string, as in "3.3V" or "0.0V" or w/e. That allows the display of "Target voltage: Unknown" or "Target voltage: Present" etc to be nice and consistent across BMDA and the firmware.
ALTracer (Migrated from github.com) reviewed 2024年07月16日 21:20:02 +02:00
@ -212,8 +212,7 @@ static bool cmd_jtag_scan(target_s *target, int argc, const char **argv)
(void)argc;
ALTracer (Migrated from github.com) commented 2024年07月16日 21:20:02 +02:00
Copy link

I can't drop the runtime NULL check in good faith without a strict compile-time check against that. Neither GCC attribute nonnull nor null_terminated_string_arg trigger a warning, or I'm holding it wrong. The strcmp may be more expensive than rodata pointer equivalence, but I'm not sure whether compiler optimizes former to latter. -fwhole-program? -flto?
Displaying an extra string changes UI for users of GDB CLI, this is outside of my memory UB bugfix scope PR (I'd like a second opinion about that).

I can't drop the runtime NULL check in good faith without a strict compile-time check against that. Neither GCC attribute `nonnull` nor `null_terminated_string_arg` trigger a warning, or I'm holding it wrong. The strcmp may be more expensive than rodata pointer equivalence, but I'm not sure whether compiler optimizes former to latter. -fwhole-program? -flto? Displaying an extra string changes UI for users of GDB CLI, this is outside of my memory UB bugfix scope PR (I'd like a second opinion about that).
ALTracer (Migrated from github.com) reviewed 2024年07月16日 21:23:23 +02:00
@ -480,7 +480,7 @@ int cl_execute(bmda_cli_options_s *opt)
platform_nrst_set_val(opt->opt_connect_under_reset);
ALTracer (Migrated from github.com) commented 2024年07月16日 21:23:23 +02:00
Copy link

It's fair that "Present Volt" is a funny statement. Luckily, some of the implementations are touched by this PR and are kinda loaded in recent memory of contributors/reviewers, so how about we revisit also the hosted backends? JLink has its own command for Vtarget, STLink mostly reports Vref of cable/buffers or Vdd of onboard target, FTDI has no such capability, and I don't know about CMSIS-DAP (Vendor-specific commands?). BMPremote will relay whatever string as a HL message.

It's fair that "Present Volt" is a funny statement. Luckily, some of the implementations are touched by this PR and are kinda loaded in recent memory of contributors/reviewers, so how about we revisit also the hosted backends? JLink has its own command for Vtarget, STLink mostly reports Vref of cable/buffers or Vdd of onboard target, FTDI has no such capability, and I don't know about CMSIS-DAP (Vendor-specific commands?). BMPremote will relay whatever string as a HL message.
@ -212,8 +212,7 @@ static bool cmd_jtag_scan(target_s *target, int argc, const char **argv)
(void)argc;

I can't drop the runtime NULL check in good faith

None of the in-tree platforms are returning NULL now, and this adds a comment to the header explaining valid values. It's not on you at that point, dropping the NULL check is completely safe. It is on out-of-tree platforms to pay attention when they update and to use release notes. If this wasn't true, we'd be permanently shouldered with all the bad decisions of out-of-tree platforms forever and would never be able to change and evolve the project.

Displaying an extra string changes UI for users of GDB CLI, this is outside of my memory UB bugfix scope PR (I'd like a second opinion about that).

It is in-scope because it makes everything much more consistent across the platforms and cleans up after the NULL pointer fix, thus is in scope. From the project's perspective based on the original intention of this from the native platform, it's actually an error that the target voltage display is not on all platforms, making it a bugfix based on the intention that this string should be displayed.

> I can't drop the runtime NULL check in good faith None of the in-tree platforms are returning NULL now, and this adds a comment to the header explaining valid values. It's not on you at that point, dropping the NULL check is completely safe. It is on out-of-tree platforms to pay attention when they update and to use release notes. If this wasn't true, we'd be permanently shouldered with all the bad decisions of out-of-tree platforms forever and would never be able to change and evolve the project. > Displaying an extra string changes UI for users of GDB CLI, this is outside of my memory UB bugfix scope PR (I'd like a second opinion about that). It is in-scope because it makes everything much more consistent across the platforms and cleans up after the NULL pointer fix, thus is in scope. From the project's perspective based on the original intention of this from the native platform, it's actually an error that the target voltage display is not on all platforms, making it a bugfix based on the intention that this string should be displayed.
@ -480,7 +480,7 @@ int cl_execute(bmda_cli_options_s *opt)
platform_nrst_set_val(opt->opt_connect_under_reset);

so how about we revisit also the hosted backends?.

Probably a very good idea to check that they all suffix valid voltages with V and have sensible invalid voltage displays as that's really part and parcel of fixing this 👍🏼

> so how about we revisit also the hosted backends?. Probably a very good idea to check that they all suffix valid voltages with `V` and have sensible invalid voltage displays as that's really part and parcel of fixing this 👍🏼
dragonmux left a comment
Copy link

Only got the one review item on re-reviewing this. With it taken care of, we're happy to merge this.

Only got the one review item on re-reviewing this. With it taken care of, we're happy to merge this.

Not sure it's useful to list these out explicitly as that's what default: means here after all - noting that this is the handling for not-BMP when in a HOSTED_BMP_ONLY=1 build.

Not sure it's useful to list these out explicitly as that's what `default:` means here after all - noting that this is the handling for not-BMP when in a `HOSTED_BMP_ONLY=1` build.
dragonmux left a comment
Copy link

All LGTM. We're happy this patch set and how it fixes the UB that was in play and brings some much needed platform consistency. Merging. Thank you for the contribution!

All LGTM. We're happy this patch set and how it fixes the UB that was in play and brings some much needed platform consistency. Merging. Thank you for the contribution!
Sign in to join this conversation.
No reviewers
Labels
Clear labels
BMD App
Black Magic Debug App (aka. PC hosted) (not firmware)
BMP Firmware
Black Magic Probe Firmware (not PC hosted software)
Bug
Confirmed bug
Build system
Build system
Can't reproduce
Maintainers can't reproduce this problem
CI
Continuous Integration System
Contribution wanted
User contributions welcome
Documentation
Project documentation
Draft
Work in progress draft
Duplicate
This issue or pull request already exists
Enhancement
General project improvement
Feedback wanted
Requires additional submitter feedback
Foreign Host Board
Non Native hardware to runing Black Magic firmware on
GDB
Issue/PR related to GDB
Good first issue
Good for newcommers
HwIssue Mitigation
Solving or mitigating a Hardware issue in Software
Information Needed
Maintainers need more information
NativeHardware
Official Black Magic Debug Hardware
New Host Board
New hardware to run Black Magic firmware on
New Target
New debug target
Off Topic
Something that does not involve the project in any way
Potential Bug
A potential, unconfirmed or very special circumstance bug
Regression
Bug caused by a regression
User Interest Needed
More user interest required before consideration
User Testing Needed
Looking for user testing reports
Won't fix
Outside of the project scope or works as intended
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
blackmagic-debug/blackmagic!1866
Reference in a new issue
blackmagic-debug/blackmagic
No description provided.
Delete branch "fix/remote-nullptr"

Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?