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