fnord bugs I found + new httpd (was: A fnord fork (a fnork?))
"Neale Pickett" <[email protected]> Tue, 20 Mar 2012 17:20:52 -0500 (CDT)
| Newsgroups | gmane.comp.web.fnord |
|---|---|
| Message-ID | <[email protected]> |
On 2/16/2012, "Felix von Leitner" <[email protected]> wrote: >Fork away if you want to, but if you actually discovered bugs, then I >would like to hear about them. For the last 5 or so weeks, I have been doing exactly this. Five paragraphs down I will talk about the bugs; my fork has been running my personal server for over a week now, though, and I feel it's time to invite people to help me test it. I wound up rewriting large sections of the server. The resulting code is 1083 lines of C, compared to 1469 in fnord 1.10 (omitting 288 lines from libowfat). It is mostly feature-compatible with fnord, with some documented exceptions. My limited httperf benchmarking shows its performance is exactly the same as fnord's (unsurprising given identical architecture). In keeping with the discordian theme, I named it "Eris HTTPd". http://woozle.org/~neale/src/eris.html Please try it if you're the adventurous sort. Fnord Bugs I Found --------------------------- In doing the rewrite I identified 10 bugs with fnord 1.10. Only one results in a crash, and none are exploitable. A few have major performance or functionality impacts, though; such as the if-modified-since parsing in non-UTC timezones I mentioned in a previous email, or the mime-type handling for unmatched extensions during keepalive. Here is a shell script to demonstrate the bugs. I was not able to trigger the first bug on a 32-bit Intel build of fnord, but a 64-bit Intel build did cause the crash. I'm sure this is due to page alignment differences. #! /bin/sh ## Breaking fnord 1.10 if [ "$1" = "clean" ]; then rm -rf fnord-1.10 fi # Set HTTPD= to test something else case ${HTTPD:=./fnord} in *fnord) : ${HTTPD_IDX:=$HTTPD-idx} : ${HTTPD_CGI:=$HTTPD-cgi} ;; esac title() { printf "%-50s: " "$1" tests=$(expr $tests + 1) } successes=0 pass () { echo 'pass' successes=$(expr $successes + 1) } failures=0 fail () { echo 'fail' failures=$(expr $failures + 1) } d () { tr '\r\n' '#%' } if [ ! -f fnord-1.10.tar.bz2 ]; then wget http://www.fefe.de/fnord/fnord-1.10.tar.bz2 fi if [ ! -f fnord-1.10/httpd.c ]; then rm -rf fnord-1.10 bzcat fnord-1.10.tar.bz2 | tar xf - fi cd fnord-1.10 # Comment this out if you want to build with diet libc make DIET= if [ ! -d default ]; then mkdir default echo james > default/index.html touch default/a cat <<EOD > default/a.cgi #! /bin/sh echo 'Content-type: text/plain' ls / > /dev/null # delay a little set EOD chmod +x default/a.cgi mkdir empty:80 fi cat <<EOD HTTPD: $HTTPD CGI: $HTTPD_CGI IDX: $HTTPD_IDX ----------------------------------------- EOD # 1. Should return directory listing of /; instead segfaults title "Directory indexing of /" printf 'GET / HTTP/1.0\r\nHost: empty\r\n\r\n' | $HTTPD_IDX 2>/dev/null | grep -q 200 && pass || fail # 2. Should output \r\n\r\n; instead outputs \r\n\n title "CGI output bare newlines" printf 'GET /a.cgi HTTP/1.0\r\n\r\n' | $HTTPD_CGI 2>/dev/null | d | grep -q '#%#%' && pass || fail # 3. Should process both requests; instead drops second title "Multiple requests in one packet" printf 'GET / HTTP/1.1\r\nHost: a\r\nConnection: keep-alive\r\n\r\nGET / HTTP/1.1\r\nHost: a\r\nConnection: keep-alive\r\n\r\n' | $HTTPD 2>/dev/null | grep -c '^HTTP/1.' | grep -q 2 && pass || fail # 4. Should return 406 Not Acceptable; instead ignores Accept header title "Accept header" printf 'GET / HTTP/1.0\r\nAccept: nothing\r\n\r\n' | $HTTPD 2>/dev/null | grep 406 && pass || fail # 5. Should serve second request as default MIME-Type (text/plain); instead uses previous mime type title "Second MIME-Type" (printf 'GET / HTTP/1.1\r\nHost: a\r\nConnection: keep-alive\r\n\r\n' ls / > /dev/null # Delay required to work around test #3 printf 'GET /a HTTP/1.1\r\nHost: a\r\nConnection: keep-alive\r\n\r\n') | $HTTPD 2>/dev/null | grep -q 'text/plain\|application/octet-stream' && pass || fail # 6. Should consume POST data; instead tries to read POST data as second request title "POST to static HTML" (printf 'POST / HTTP/1.1\r\nHost: a\r\nConnection: keep-alive\r\nContent-Type: text/plain\r\nContent-Length: 1\r\n\r\n'; ls / > /dev/null printf 'aPOST / HTTP/1.1\r\nHost: a\r\nConnection: keep-alive\r\nContent-Type: text/plain\r\nContent-Length: 1\r\n\r\na') | $HTTPD 2>/dev/null | grep -c '200 OK' | grep -q 2 && pass || fail # 7. HTTP/1.1 should default to keepalive; instead connection is closed title "HTTP/1.1 default keepalive" (printf 'GET / HTTP/1.1\r\nHost: a\r\n\r\n' ls / >/dev/null printf 'GET / HTTP/1.1\r\nHost: a\r\n\r\n') | $HTTPD 2>/dev/null | grep -c '^HTTP/' | grep -q 2 && pass || fail # 8. Should parse "Thursday"; instead assumes all day names are 6 characters long title "RFC 850 Date" printf 'GET / HTTP/1.0\r\nIf-Modified-Since: Thursday, 27-Feb-30 12:12:12 GMT\r\n\r\n' | $HTTPD 2>/dev/null | grep -q '304 Not Changed' && pass || fail # 9. Should set PATH_INFO to /; instead sets it to /index.html title "PATH_INFO=/" printf 'GET /a.cgi/ HTTP/1.0\r\n\r\n' | $HTTPD_CGI 2>/dev/null | grep -Eq 'PATH_INFO=.?/.?$' && pass || fail cat <<EOD ----------------------------------------- $successes of $tests tests passed ($failures failed). EOD exit $failures