xref: /openbmc/bmcweb/COMMON_ERRORS.md (revision 6c58a03e)
1# Commonly recurring errors in bmcweb
2
3What follows is a list of common errors that new users to bmcweb tend to make
4when operating within its bounds for the first time. If this is your first time
5developing in bmcweb, the maintainers highly recommend reading and understanding
6_all_ of common traps before continuing with any development. Every single one
7of the examples below compile without warnings, but are incorrect in
8not-always-obvious ways, or impose a pattern that tends to cause hard to find
9bugs, or bugs that appear later. Every one has been submitted to code review
10multiple times.
11
12## 1. Directly dereferencing a pointer without checking for validity first
13
14```cpp
15int myBadMethod(const nlohmann::json& j){
16    const int* myPtr = j.get_if<int>();
17    return *myPtr;
18}
19```
20
21This pointer is not guaranteed to be filled, and could be a null dereference.
22
23## 2. String views aren't null terminated
24
25```cpp
26int getIntFromString(std::string_view s){
27    return std::atoi(s.data());
28}
29```
30
31This will give the right answer much of the time, but has the possibility to
32fail when `string_view` is not null terminated. Use `from_chars` instead, which
33takes both a pointer and a length
34
35## 3. Not handling input errors
36
37```cpp
38int getIntFromString(const std::string& s){
39    return std::atoi(s.c_str());
40}
41```
42
43In the case where the string is not representable as an int, this will trigger
44undefined behavior at system level. Code needs to check for validity of the
45string, ideally with something like `from_chars`, and return the appropriate
46error code.
47
48## 4. Walking off the end of a string
49
50```cpp
51std::string getFilenameFromPath(const std::string& path){
52    size_t index = path.find("/");
53    if (index != std::string::npos){
54        // If the string ends with "/", this will walk off the end of the string.
55        return path.substr(pos + 1);
56    }
57    return "";
58}
59```
60
61## 5. Using methods that throw (or not handling bad inputs)
62
63```cpp
64int myBadMethod(nlohmann::json& j){
65    return j.get<int>();
66}
67```
68
69This method throws, and bad inputs will not be handled
70
71Commonly used methods that fall into this pattern:
72
73- std::variant::get
74- std::vector::at
75- std::map::at
76- std::set::at
77- std::\<generic container type\>::at
78- nlohmann::json::operator!=
79- nlohmann::json::operator+=
80- nlohmann::json::at
81- nlohmann::json::get
82- nlohmann::json::get_ref
83- nlohmann::json::get_to
84- nlohmann::json::items
85- nlohmann::json::operator\<\<
86- nlohmann::json::operator\>\>
87- std::filesystem::create_directory
88- std::filesystem::rename
89- std::filesystem::file_size
90- std::stoi
91- std::stol
92- std::stoll
93
94### Special note: JSON
95
96`nlohmann::json::parse` by default
97[throws](https://json.nlohmann.me/api/basic_json/parse/) on failure, but also
98accepts an optional argument that causes it to not throw: set the 3rd argument
99to `false`.
100
101`nlohmann::json::dump` by default
102[throws](https://json.nlohmann.me/api/basic_json/dump/) on failure, but also
103accepts an optional argument that causes it to not throw: set the 4th argument
104to `replace`. Although `ignore` preserves content 1:1, `replace` is preferred
105from a security point of view.
106
107### Special note: Boost
108
109there is a whole class of boost asio functions that provide both a method that
110throws on failure, and a method that accepts and returns an error code. This is
111not a complete list, but users should verify in the boost docs when calling into
112asio methods, and prefer the one that returns an error code instead of throwing.
113
114- boost::asio::ip::tcp::acceptor::bind();
115- boost::asio::ip::tcp::acceptor::cancel();
116- boost::asio::ip::tcp::acceptor::close();
117- boost::asio::ip::tcp::acceptor::listen();
118- boost::asio::ip::address::make_address();
119
120## 6. Blocking functions
121
122bmcweb uses a single reactor for all operations. Blocking that reactor for any
123amount of time causes all other operations to stop. The common blocking
124functions that tend to be called incorrectly are:
125
126- sleep()
127- boost::asio::ip::tcp::socket::read()
128- boost::asio::ip::tcp::socket::read_some()
129- boost::asio::ip::tcp::socket::write()
130- boost::asio::ip::tcp::socket::write_some()
131- boost::asio::ip::tcp::socket::connect()
132- boost::asio::ip::tcp::socket::send()
133- boost::asio::ip::tcp::socket::wait()
134- boost::asio::steady_timer::wait()
135
136Note: an exception is made for filesystem/disk IO read and write. This is mostly
137due to not having great abstractions for it that mate well with the async
138system, the fact that most filesystem accesses are into tmpfs (and therefore
139should be "fast" most of the time) and in general how little the filesystem is
140used in practice.
141
142## 7. Lack of locking between subsequent calls
143
144While global data structures are discouraged, they are sometimes required to
145store temporary state for operations that require it. Given the single threaded
146nature of bmcweb, they are not required to be explicitly threadsafe, but they
147must be always left in a valid state, and checked for other uses before
148occupying.
149
150```cpp
151std::optional<std::string> currentOperation;
152void firstCallbackInFlow(){
153    currentOperation = "Foo";
154}
155void secondCallbackInFlow(){
156    currentOperation.reset();
157}
158```
159
160In the above case, the first callback needs a check to ensure that
161currentOperation is not already being used.
162
163## 8. Wildcard reference captures in lambdas
164
165```cpp
166std::string x; auto mylambda = [&](){
167    x = "foo";
168}
169do_async_read(mylambda)
170```
171
172Numerous times, lifetime issues of const references have been injected into
173async bmcweb code. While capturing by reference can be useful, given how
174difficult these types of bugs are to triage, bmcweb explicitly requires that all
175code captures variables by name explicitly, and calls out each variable being
176captured by value or by reference. The above prototypes would change to
177`[&x]()...` Which makes clear that x is captured, and its lifetime needs
178tracked.
179
180## 9. URLs should end in "/"
181
182```cpp
183BMCWEB("/foo/bar");
184```
185
186Unless you explicitly have a reason not to (as there is one known exception
187where the behavior must differ) all URL handlers should end in "/". The bmcweb
188route handler will detect routes ending in slash and generate routes for both
189the route ending in slash and the one without. This allows both URLs to be used
190by users. While many specifications do not require this, it resolves a whole
191class of bug that we've seen in the past.
192
193Note, unit tests can now find this for redfish routes.
194
195## 10. URLs constructed in aggregate
196
197```cpp
198std::string routeStart = "/redfish/v1";
199
200BMCWEB_ROUTE(routestart + "/SessionService/")
201```
202
203Very commonly, bmcweb maintainers and contributors alike have to do audits of
204all routes that are available, to verify things like security and documentation
205accuracy. While these processes are largely manual, they can mostly be conducted
206by a simple grep statement to search for urls in question. Doing the above makes
207the route handlers no longer greppable, and complicates bmcweb patchsets as a
208whole.
209
210## 11. Not responding to 404
211
212```cpp
213BMCWEB_ROUTE("/myendpoint/<str>",
214    [](Request& req, Response& res, const std::string& id){
215     crow::connections::systemBus->async_method_call(
216          [asyncResp](const boost::system::error_code& ec,
217                      const std::string& myProperty) {
218              if (ec)
219              {
220                  messages::internalError(asyncResp->res);
221                  return;
222              }
223              ... handle code
224          },
225          "xyz.openbmc_project.Logging",
226          "/xyz/openbmc_project/mypath/" + id,
227          "xyz.MyInterface", "GetAll", "");
228});
229```
230
231All bmcweb routes should handle 404 (not found) properly, and return it where
232appropriate. 500 internal error is not a substitute for this, and should be only
233used if there isn't a more appropriate error code that can be returned. This is
234important, because a number of vulnerability scanners attempt injection attacks
235in the form of `/myendpoint/foobar`, or `/myendpoint/#$*(%)&#%$)(\*&` in an
236attempt to circumvent security. If the server returns 500 to any of these
237requests, the security scanner logs it as an error for followup. While in
238general these errors are benign, and not actually a real security threat, having
239a clean security run allows maintainers to minimize the amount of time spent
240triaging issues reported from these scanning tools.
241
242An implementation of the above that handles 404 would look like:
243
244```cpp
245BMCWEB_ROUTE("/myendpoint/<str>",
246    [](Request& req, Response& res, const std::string& id){
247     crow::connections::systemBus->async_method_call(
248          [asyncResp](const boost::system::error_code& ec,
249                      const std::string& myProperty) {
250              if (ec == <error code that gets returned by not found>){
251                  messages::resourceNotFound(res);
252                  return;
253              }
254              if (ec)
255              {
256                  messages::internalError(asyncResp->res);
257                  return;
258              }
259              ... handle code
260          },
261          "xyz.openbmc_project.Logging",
262          "/xyz/openbmc_project/mypath/" + id,
263          "xyz.MyInterface", "GetAll", "");
264});
265```
266
267Note: A more general form of this rule is that no handler should ever return 500
268on a working system, and any cases where 500 is found, can immediately be
269assumed to be
270[a bug in either the system, or bmcweb.](https://github.com/openbmc/bmcweb/blob/master/DEVELOPING.md#error-handling)
271
272## 12. Imprecise matching
273
274```cpp
275void isInventoryPath(const std::string& path){
276    if (path.find("inventory")){
277        return true;
278    }
279    return false;
280}
281```
282
283When matching dbus paths, HTTP fields, interface names, care should be taken to
284avoid doing direct string containment matching. Doing so can lead to errors
285where fan1 and fan11 both report to the same object, and cause behavior breaks
286in subtle ways.
287
288When using dbus paths, rely on the methods on `sdbusplus::message::object_path`.
289When parsing HTTP field and lists, use the RFC7230 implementations from
290boost::beast.
291
292Other commonly misused methods are: boost::iequals. Unless the standard you're
293implementing (as is the case in some HTTP fields) requires case insensitive
294comparisons, casing should be obeyed, especially when relying on user-driven
295data.
296
297- boost::starts_with
298- boost::ends_with
299- std::string::starts_with
300- std::string::ends_with
301- std::string::rfind
302
303The above methods tend to be misused to accept user data and parse various
304fields from it. In practice, there tends to be better, purpose built methods for
305removing just the field you need.
306
307## 13. Complete replacement of the response object
308
309```cpp
310void getMembers(crow::Response& res){
311  res.jsonValue = {{"Value", 2}};
312}
313```
314
315In many cases, bmcweb is doing multiple async actions in parallel, and
316orthogonal keys within the Response object might be filled in from another task.
317Completely replacing the json object can lead to convoluted situations where the
318output of the response is dependent on the _order_ of the asynchronous actions
319completing, which cannot be guaranteed, and has many time caused bugs.
320
321```cpp
322void getMembers(crow::Response& res){
323  res.jsonValue["Value"] = 2;
324}
325```
326
327As an added benefit, this code is also more efficient, as it avoids the
328intermediate object construction and the move, and as a result, produces smaller
329binaries.
330
331Note, another form of this error involves calling nlohmann::json::reset(), to
332clear an object that's already been filled in. This has the potential to clear
333correct data that was already filled in from other sources.
334
335## 14. Very long lambda callbacks
336
337```cpp
338dbus::utility::getSubTree("/", interfaces,
339                         [asyncResp](boost::system::error_code& ec,
340                                     MapperGetSubTreeResult& res){
341                            <many lines of code>
342                         })
343```
344
345Inline lambdas, while useful in some contexts, are difficult to read, and have
346inconsistent formatting with tools like clang-format, which causes significant
347problems in review, and in applying patchsets that might have minor conflicts.
348In addition, because they are declared in a function scope, they are difficult
349to unit test, and produce log messages that are difficult to read given their
350unnamed nature.
351
352Prefer to either use std::bind_front, and a normal method to handle the return,
353or a lambda that is less than 10 lines of code to handle an error inline. In
354cases where std::bind_front cannot be used, such as in
355sdbusplus::asio::connection::async_method_call, keep the lambda length less than
35610 lines, and call the appropriate function for handling non-trivial transforms.
357
358```cpp
359void afterGetSubTree(std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
360                     boost::system::error_code& ec,
361                     MapperGetSubTreeResult& res){
362   <many lines of code>
363}
364
365dbus::utility::getSubTree("/xyz/openbmc_project/inventory", interfaces,
366                         std::bind_front(afterGetSubTree, asyncResp));
367```
368
369See also the
370[Cpp Core Guidelines](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#f11-use-an-unnamed-lambda-if-you-need-a-simple-function-object-in-one-place-only)
371for generalized guidelines on when lambdas are appropriate. The above
372recommendation is aligned with the C++ Core Guidelines.
373