xref: /openbmc/bmcweb/COMMON_ERRORS.md (revision 28cfceb2)
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
193## 10. URLs constructed in aggregate
194
195```cpp
196std::string routeStart = "/redfish/v1";
197
198BMCWEB_ROUTE(routestart + "/SessionService/")
199```
200
201Very commonly, bmcweb maintainers and contributors alike have to do audits of
202all routes that are available, to verify things like security and documentation
203accuracy. While these processes are largely manual, they can mostly be conducted
204by a simple grep statement to search for urls in question. Doing the above makes
205the route handlers no longer greppable, and complicates bmcweb patchsets as a
206whole.
207
208## 11. Not responding to 404
209
210```cpp
211BMCWEB_ROUTE("/myendpoint/<str>",
212    [](Request& req, Response& res, const std::string& id){
213     crow::connections::systemBus->async_method_call(
214          [asyncResp](const boost::system::error_code& ec,
215                      const std::string& myProperty) {
216              if (ec)
217              {
218                  messages::internalError(asyncResp->res);
219                  return;
220              }
221              ... handle code
222          },
223          "xyz.openbmc_project.Logging",
224          "/xyz/openbmc_project/mypath/" + id,
225          "xyz.MyInterface", "GetAll", "");
226});
227```
228
229All bmcweb routes should handle 404 (not found) properly, and return it where
230appropriate. 500 internal error is not a substitute for this, and should be only
231used if there isn't a more appropriate error code that can be returned. This is
232important, because a number of vulnerability scanners attempt injection attacks
233in the form of `/myendpoint/foobar`, or `/myendpoint/#$*(%)&#%$)(\*&` in an
234attempt to circumvent security. If the server returns 500 to any of these
235requests, the security scanner logs it as an error for followup. While in
236general these errors are benign, and not actually a real security threat, having
237a clean security run allows maintainers to minimize the amount of time spent
238triaging issues reported from these scanning tools.
239
240An implementation of the above that handles 404 would look like:
241
242```cpp
243BMCWEB_ROUTE("/myendpoint/<str>",
244    [](Request& req, Response& res, const std::string& id){
245     crow::connections::systemBus->async_method_call(
246          [asyncResp](const boost::system::error_code& ec,
247                      const std::string& myProperty) {
248              if (ec == <error code that gets returned by not found>){
249                  messages::resourceNotFound(res);
250                  return;
251              }
252              if (ec)
253              {
254                  messages::internalError(asyncResp->res);
255                  return;
256              }
257              ... handle code
258          },
259          "xyz.openbmc_project.Logging",
260          "/xyz/openbmc_project/mypath/" + id,
261          "xyz.MyInterface", "GetAll", "");
262});
263```
264
265Note: A more general form of this rule is that no handler should ever return 500
266on a working system, and any cases where 500 is found, can immediately be
267assumed to be
268[a bug in either the system, or bmcweb.](https://github.com/openbmc/bmcweb/blob/master/DEVELOPING.md#error-handling)
269
270## 12. Imprecise matching
271
272```cpp
273void isInventoryPath(const std::string& path){
274    if (path.find("inventory")){
275        return true;
276    }
277    return false;
278}
279```
280
281When matching dbus paths, HTTP fields, interface names, care should be taken to
282avoid doing direct string containment matching. Doing so can lead to errors
283where fan1 and fan11 both report to the same object, and cause behavior breaks
284in subtle ways.
285
286When using dbus paths, rely on the methods on `sdbusplus::message::object_path`.
287When parsing HTTP field and lists, use the RFC7230 implementations from
288boost::beast.
289
290Other commonly misused methods are: boost::iequals. Unless the standard you're
291implementing (as is the case in some HTTP fields) requires case insensitive
292comparisons, casing should be obeyed, especially when relying on user-driven
293data.
294
295- boost::starts_with
296- boost::ends_with
297- std::string::starts_with
298- std::string::ends_with
299- std::string::rfind
300
301The above methods tend to be misused to accept user data and parse various
302fields from it. In practice, there tends to be better, purpose built methods for
303removing just the field you need.
304
305## 13. Complete replacement of the response object
306
307```cpp
308void getMembers(crow::Response& res){
309  res.jsonValue = {{"Value", 2}};
310}
311```
312
313In many cases, bmcweb is doing multiple async actions in parallel, and
314orthogonal keys within the Response object might be filled in from another task.
315Completely replacing the json object can lead to convoluted situations where the
316output of the response is dependent on the _order_ of the asynchronous actions
317completing, which cannot be guaranteed, and has many time caused bugs.
318
319```cpp
320void getMembers(crow::Response& res){
321  res.jsonValue["Value"] = 2;
322}
323```
324
325As an added benefit, this code is also more efficient, as it avoids the
326intermediate object construction and the move, and as a result, produces smaller
327binaries.
328
329Note, another form of this error involves calling nlohmann::json::reset(), to
330clear an object that's already been filled in. This has the potential to clear
331correct data that was already filled in from other sources.
332
333## 14. Very long lambda callbacks
334
335```cpp
336dbus::utility::getSubTree("/", interfaces,
337                         [asyncResp](boost::system::error_code& ec,
338                                     MapperGetSubTreeResult& res){
339                            <many lines of code>
340                         })
341```
342
343Inline lambdas, while useful in some contexts, are difficult to read, and have
344inconsistent formatting with tools like clang-format, which causes significant
345problems in review, and in applying patchsets that might have minor conflicts.
346In addition, because they are declared in a function scope, they are difficult
347to unit test, and produce log messages that are difficult to read given their
348unnamed nature.
349
350Prefer to either use std::bind_front, and a normal method to handle the return,
351or a lambda that is less than 10 lines of code to handle an error inline. In
352cases where std::bind_front cannot be used, such as in
353sdbusplus::asio::connection::async_method_call, keep the lambda length less than
35410 lines, and call the appropriate function for handling non-trivial transforms.
355
356```cpp
357void afterGetSubTree(std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
358                     boost::system::error_code& ec,
359                     MapperGetSubTreeResult& res){
360   <many lines of code>
361}
362
363dbus::utility::getSubTree("/xyz/openbmc_project/inventory", interfaces,
364                         std::bind_front(afterGetSubTree, asyncResp));
365```
366
367See also the
368[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)
369for generalized guidelines on when lambdas are appropriate. The above
370recommendation is aligned with the C++ Core Guidelines.
371