Repository: WasmEdge/WasmEdge (2026-04-13T19:42:11Z)

Issue:
bug: CLI accepts numeric options with trailing garbage and silently drops parse errors
### Summary

WasmEdge's Program Options (PO) numeric parser silently accepts values with trailing non-numeric characters. Flags such as `--time-limit=100abc` or `--gas-limit 42xyz` are quietly accepted as `100` and `42` instead of being rejected as invalid input. In addition, when a value fails to parse, the error path from `consume_argument` is discarded in two branches of the `ArgumentParser` state machine, so the user never sees any error at all.

### Current State

The helpers `stringToInteger` and `stringToFloating` in `include/po/parser.h` only reject input when no characters at all could be converted (`EndPtr == CStr`). They do not check that the entire string was consumed, so `strtol("123abc", ...)` returns `123` with `"abc"` left unparsed and the parser reports success.

In addition, both returned error messages are hard-coded to the empty string, so if an error ever did reach the CLI, the user would only see a blank line.

Finally, `lib/po/argument_parser.cpp` drops the result of `consume_argument` in two places:

1. In `SubCommandDescriptor::parse`, when consuming a non-escaped value that follows a short/long option (e.g. `--time-limit 100abc`).
2. In `consume_long_option_with_argument`, when consuming the right-hand side of `--option=value` (e.g. `--time-limit=100abc`).

This means that even if the parser did produce a useful error, the top-level parser would still return success and the invalid value would silently reach the runtime. There is also a minor cosmetic bug in the "doesn't need arguments" message where a space is missing between the option name and the rest of the sentence, producing output like `option foodoesn't need arguments.`.

### Expected State

For every registered numeric CLI option (e.g. `--time-limit`, `--gas-limit`, `--memory-page-limit`, and any numeric plugin option), WasmEdge should reject inputs that contain non-numeric trailing content and surface a clear, actionable error message:

```
$ wasmedge --time-limit=100abc module.wasm
invalid integer value: 100abc
```

Parser errors for option values must be propagated to the caller, both for `--option value` and `--option=value` forms, so that the CLI exits with a non-zero status and the user can see the problem.

### Reproduction steps

1. Build WasmEdge from source on the current `master` branch.
2. Run `wasmedge --time-limit=100abc module.wasm`.
3. Observe that the CLI silently accepts `100` as the time limit and runs the module.
4. Run `wasmedge --time-limit 100abc module.wasm` (space-separated form).
5. Observe the same silent acceptance, because the error is dropped by the argument parser.

### Components

CLI

### WasmEdge Version or Commit you used

master @ f3ee7865 (0.17.0-alpha.2-1-gf3ee7865)

### Operating system information

macOS 15.5 (Darwin 24.6.0), also reproducible on Linux.

### Hardware Architecture

arm64 (reproduces on any architecture, numeric parsing is not platform-specific)



Discussion:

Patch:
diff --git a/include/po/parser.h b/include/po/parser.h
index b143510e184a..8320cbf569bd 100644
--- a/include/po/parser.h
+++ b/include/po/parser.h
@@ -11,6 +11,7 @@
 #include "po/error.h"
 #include <algorithm>
 #include <cstdint>
+#include <fmt/format.h>
 #include <string>
 #include <utility>
 
@@ -33,9 +34,10 @@ stringToInteger(ConvResultT (&Conv)(const char *, char **, int),
   auto SavedErrNo = std::exchange(errno, 0);
   const auto Result = Conv(CStr, &EndPtr, 10);
   std::swap(SavedErrNo, errno);
-  if (EndPtr == CStr) {
-    return cxx20::unexpected<Error>(std::in_place, ErrCode::InvalidArgument,
-                                    ""s);
+  if (Value.empty() || *EndPtr != '\0') {
+    return cxx20::unexpected<Error>(
+        std::in_place, ErrCode::InvalidArgument,
+        fmt::format("invalid integer value: {}", Value));
   }
   auto InsideRange = [](auto WiderResult) constexpr noexcept {
     using WiderResultT = decltype(WiderResult);
@@ -49,7 +51,9 @@ stringToInteger(ConvResultT (&Conv)(const char *, char **, int),
     }
   };
   if (SavedErrNo == ERANGE || !InsideRange(Result)) {
-    return cxx20::unexpected<Error>(std::in_place, ErrCode::OutOfRange, ""s);
+    return cxx20::unexpected<Error>(
+        std::in_place, ErrCode::OutOfRange,
+        fmt::format("integer value out of range: {}", Value));
   }
   return static_cast<ResultT>(Result);
 }
@@ -64,12 +68,15 @@ stringToFloating(ConvResultT (&Conv)(const char *, char **),
   auto SavedErrNo = std::exchange(errno, 0);
   const auto Result = Conv(CStr, &EndPtr);
   std::swap(SavedErrNo, errno);
-  if (EndPtr == CStr) {
-    return cxx20::unexpected<Error>(std::in_place, ErrCode::InvalidArgument,
-                                    ""s);
+  if (Value.empty() || *EndPtr != '\0') {
+    return cxx20::unexpected<Error>(
+        std::in_place, ErrCode::InvalidArgument,
+        fmt::format("invalid floating-point value: {}", Value));
   }
   if (SavedErrNo == ERANGE) {
-    return cxx20::unexpected<Error>(std::in_place, ErrCode::OutOfRange, ""s);
+    return cxx20::unexpected<Error>(
+        std::in_place, ErrCode::OutOfRange,
+        fmt::format("floating-point value out of range: {}", Value));
   }
   return Result;
 }
diff --git a/lib/po/argument_parser.cpp b/lib/po/argument_parser.cpp
index d54a5a82f04c..e8752d1c3b11 100644
--- a/lib/po/argument_parser.cpp
+++ b/lib/po/argument_parser.cpp
@@ -51,7 +51,9 @@ cxx20::expected<bool, Error> ArgumentParser::SubCommandDescriptor::parse(
         }
       }
     } else if (!Escaped && CurrentDesc) {
-      consume_argument(*CurrentDesc, Arg);
+      if (auto Res = consume_argument(*CurrentDesc, Arg); !Res) {
+        return cxx20::unexpected(Res.error());
+      }
       CurrentDesc = nullptr;
     } else {
       // no more options
@@ -275,11 +277,14 @@ ArgumentParser::SubCommandDescriptor::consume_long_option_with_argument(
     if (auto Res = consume_long_option(Option); !Res) {
       return cxx20::unexpected<Error>(Res.error());
     } else if (ArgumentDescriptor *CurrentDesc = *Res; !CurrentDesc) {
-      return cxx20::unexpected<Error>(std::in_place, ErrCode::InvalidArgument,
-                                      "option "s + std::string(Option) +
-                                          "doesn't need arguments."s);
+      return cxx20::unexpected<Error>(
+          std::in_place, ErrCode::InvalidArgument,
+          fmt::format("option {} doesn't need arguments.", Option));
     } else {
-      consume_argument(*CurrentDesc, Argument);
+      if (auto ConsumeRes = consume_argument(*CurrentDesc, Argument);
+          !ConsumeRes) {
+        return cxx20::unexpected<Error>(ConsumeRes.error());
+      }
       return nullptr;
     }
   } else {


Test patch:
diff --git a/test/po/po.cpp b/test/po/po.cpp
index 3cdb60efc9af..4b44767fe182 100644
--- a/test/po/po.cpp
+++ b/test/po/po.cpp
@@ -93,7 +93,13 @@ INSTANTIATE_TEST_SUITE_P(
         Param(true, true, 0, {0}, {}, {"test", "-c", "-a"}),
         Param(true, true, 0, {1}, {}, {"test", "-c", "1", "-a"}),
         Param(true, false, 0, {2}, {"1", "-a"}, {"test", "-c", "2", "1", "-a"}),
-        Param(true, true, 0, {2, 1}, {}, {"test", "-c", "2", "-c", "1", "-a"})),
+        Param(true, true, 0, {2, 1}, {}, {"test", "-c", "2", "-c", "1", "-a"}),
+        Param(false, {"test", "--b=1abc"}),
+        Param(false, {"test", "--b_option=1abc"}),
+        Param(false, {"test", "-b", "1abc"}), Param(false, {"test", "--b=1.5"}),
+        Param(false, {"test", "-c", "42xyz"}),
+        Param(false, {"test", "--b=99999999999999999999"}),
+        Param(false, {"test", "--b="})),
     [](const testing::TestParamInfo<GeneralOptions::ParamType> &Info) {
       std::string Name;
       for (const auto &Arg : Info.param.Args) {