** Description changed:

  [Impact]
  
  memcached's LRU crawler dump path (`lru_crawler metadump all` /
  `mgdump`, handled by `lru_crawler_write()` in `crawler.c`) treats a
  `poll()` timeout on the client socket as a successful buffer flush:
  
-   if (ret == 0) return 0;
+   if (ret == 0) return 0;
  
  When a client starts a dump and then stops reading, the kernel socket
  send buffer fills, `poll()` times out, and `lru_crawler_write()` returns
  "success" to the crawler thread without draining the buffer and without
  resetting `bufused`. The crawler thread believes the buffer was flushed
  and keeps appending crawler output at `c->buf + c->bufused`, past the
  end of the fixed 128 KiB heap buffer allocated in
  `lru_crawler_set_client()`. This is an out-of-bounds heap write (memory
  corruption) that crashes the server (denial of service).
  
  A secondary defect increments the unsigned `sent` counter on a mis-
  placed line, so a persistent `EAGAIN` (`write()` returning -1)
  underflows it and makes the next `data_size - sent` huge.
  
  On Ubuntu's default `/etc/memcached.conf` (`-l 127.0.0.1`) the trigger is 
local; instances bound to a routable address (`-l 0.0.0.0`) are exposed to any 
client
  that can issue a crawler dump. No authentication is required to run 
`lru_crawler metadump` on a default (non-SASL) instance.
  
  Fixed upstream in 1.6.44 (release notes call it "another security
  release").
  
  [Test Plan]
  
  Reproduced and fix-verified in a clean noble LXD container with an 
AddressSanitizer build of the exact archive source.
- All steps are plain CLI (awk, nc, bash /dev/tcp). 
-                                                                               
                                              Build:
-   pull-lp-source memcached noble
-   cd memcached-1.6.24
-   autoreconf -fi
-   CFLAGS="-fsanitize=address -g -O1 -fno-omit-frame-pointer" 
LDFLAGS="-fsanitize=address" ./configure
-   make -j$(nproc) memcached
+ All steps are plain CLI (awk, nc, bash /dev/tcp).
+                                                                               
  Build:
+   pull-lp-source memcached noble
+   cd memcached-1.6.24
+   autoreconf -fi
+   CFLAGS="-fsanitize=address -g -O1 -fno-omit-frame-pointer" 
LDFLAGS="-fsanitize=address" ./configure
+   make -j$(nproc) memcached
  
  Reproduce (unpatched):
  
  1. Shrink the socket buffers so a non-reading client stalls promptly:
-   sysctl -w net.ipv4.tcp_wmem="4096 16384 65536"
-   sysctl -w net.core.wmem_max=65536
-   sysctl -w net.ipv4.tcp_rmem="4096 8192 16384"
+   sysctl -w net.ipv4.tcp_wmem="4096 16384 65536"
+   sysctl -w net.core.wmem_max=65536
+   sysctl -w net.ipv4.tcp_rmem="4096 8192 16384"
  
  2. Start the server under AddressSanitizer:
  
-   ASAN_OPTIONS=abort_on_error=1:halt_on_error=1 ./memcached -l 127.0.0.1
+   ASAN_OPTIONS=abort_on_error=1:halt_on_error=1 ./memcached -l 127.0.0.1
  -p 11211 -m 2048 -u nobody -t 2
  
  3. Store ~300000 keys so a full metadump (~22 MB) far exceeds the kernel
  buffers:
  
-    awk 'BEGIN{
-      v=sprintf("%120s",""); gsub(/ /,"x",v);
-      for(i=1;i<=300000;i++) printf "set k%d 0 0 120 noreply\r\n%s\r\n", i, v;
-      printf "get k1\r\n";
-    }' | nc -q2 127.0.0.1 11211
+    awk 'BEGIN{
+      v=sprintf("%120s",""); gsub(/ /,"x",v);
+      for(i=1;i<=300000;i++) printf "set k%d 0 0 120 noreply\r\n%s\r\n", i, v;
+      printf "get k1\r\n";
+    }' | nc -q2 127.0.0.1 11211
  
-    Confirm they landed:
+    Confirm they landed:
  
-    printf 'stats\r\n' | nc -q1 127.0.0.1 11211 | grep curr_items   #
+    printf 'stats\r\n' | nc -q1 127.0.0.1 11211 | grep curr_items   #
  300000
  
  4. Open one connection that requests the dump and never reads it.
-    nc auto-drains the socket, so use bash /dev/tcp and only write to the fd:
+    nc auto-drains the socket, so use bash /dev/tcp and only write to the fd:
  
-    exec 3<>/dev/tcp/127.0.0.1/11211
-    printf 'lru_crawler enable\r\n' >&3
-    sleep 0.3
-    printf 'lru_crawler metadump all\r\n' >&3
-    sleep 600           # never read fd 3
-    exec 3<&- 3>&-
+    exec 3<>/dev/tcp/127.0.0.1/11211
+    printf 'lru_crawler enable\r\n' >&3
+    sleep 0.3
+    printf 'lru_crawler metadump all\r\n' >&3
+    sleep 600           # never read fd 3
+    exec 3<&- 3>&-
  
  5. After the send buffer fills, each 1000 ms poll timeout lets the crawler 
append ~1 item, so the 128 KiB buffer overruns after a minute or two.
-    AddressSanitizer aborts with:
+    AddressSanitizer aborts with:
  
-    ERROR: AddressSanitizer: heap-buffer-overflow ... WRITE of size 73
-      #3 crawler_metadump_eval  crawler.c:275   (snprintf)
-      #4 item_crawler_thread    crawler.c:555
-    allocated by  lru_crawler_set_client  crawler.c:707
+    ERROR: AddressSanitizer: heap-buffer-overflow ... WRITE of size 73
+      #3 crawler_metadump_eval  crawler.c:275   (snprintf)
+      #4 item_crawler_thread    crawler.c:555
+    allocated by  lru_crawler_set_client  crawler.c:707
  
-    The full ASAN report from the reference run is saved as 
asan-unpatched-overflow.log.
-    A stock (non-ASAN) build instead gives SIGSEGV / malloc(): corrupted.
+    The full ASAN report from the reference run is saved as 
asan-unpatched-overflow.log.
+    A stock (non-ASAN) build instead gives SIGSEGV / malloc(): corrupted.
  
  6. Apply the two patches, rebuild the same way, and repeat steps 2-4.
-    The server stays up for the full run with no overflow and keeps serving 
other clients:
+    The server stays up for the full run with no overflow and keeps serving 
other clients:
  
-    printf 'version\r\n' | nc -q1 127.0.0.1 11211   # VERSION 1.6.24
+    printf 'version\r\n' | nc -q1 127.0.0.1 11211   # VERSION 1.6.24
  
-    Upstream now retries the timed-out client and disconnects it after
+    Upstream now retries the timed-out client and disconnects it after
  ~30 minutes instead of overrunning the buffer.
  
  7. Run the upstream crawler test (t/lru-crawler.t) and confirm it still
  passes.
- 
  
  [Where problems could occur]
  
  The change is limited to lru_crawler_write() and two error handling paths in 
item_crawler_thread() in crawler.c.
  It is a direct upstream backport with no protocol, storage, or wire format 
changes.
  Regression risk is limited to LRU crawler dumps.
  A slow or stalled dump client may now be disconnected after about 30 minutes 
of socket write stalls.
  An incorrect backport of the added lru_locks[i] locking could deadlock the 
crawler thread.
  
  [Other Info]
  
  Upstream fix
  
  "crawler: fix buffer overflow on idle client"
  
https://github.com/memcached/memcached/commit/7eeac6e9a9945aa68298aff379cea5ab3817af7c
  
  "crawler: fix potential unlock of unlocked mutex"
  
https://github.com/memcached/memcached/commit/ea35f441f265b4aef3f7753aa5cbb87d99859123

** Description changed:

  [Impact]
  
  memcached's LRU crawler dump path (`lru_crawler metadump all` /
  `mgdump`, handled by `lru_crawler_write()` in `crawler.c`) treats a
  `poll()` timeout on the client socket as a successful buffer flush:
  
    if (ret == 0) return 0;
  
  When a client starts a dump and then stops reading, the kernel socket
  send buffer fills, `poll()` times out, and `lru_crawler_write()` returns
  "success" to the crawler thread without draining the buffer and without
  resetting `bufused`. The crawler thread believes the buffer was flushed
  and keeps appending crawler output at `c->buf + c->bufused`, past the
  end of the fixed 128 KiB heap buffer allocated in
  `lru_crawler_set_client()`. This is an out-of-bounds heap write (memory
  corruption) that crashes the server (denial of service).
  
  A secondary defect increments the unsigned `sent` counter on a mis-
  placed line, so a persistent `EAGAIN` (`write()` returning -1)
  underflows it and makes the next `data_size - sent` huge.
  
  On Ubuntu's default `/etc/memcached.conf` (`-l 127.0.0.1`) the trigger is 
local; instances bound to a routable address (`-l 0.0.0.0`) are exposed to any 
client
  that can issue a crawler dump. No authentication is required to run 
`lru_crawler metadump` on a default (non-SASL) instance.
  
  Fixed upstream in 1.6.44 (release notes call it "another security
  release").
  
  [Test Plan]
  
  Reproduced and fix-verified in a clean noble LXD container with an 
AddressSanitizer build of the exact archive source.
  All steps are plain CLI (awk, nc, bash /dev/tcp).
-                                                                               
  Build:
+ 
+ Build:
+ 
    pull-lp-source memcached noble
    cd memcached-1.6.24
    autoreconf -fi
    CFLAGS="-fsanitize=address -g -O1 -fno-omit-frame-pointer" 
LDFLAGS="-fsanitize=address" ./configure
    make -j$(nproc) memcached
  
  Reproduce (unpatched):
  
  1. Shrink the socket buffers so a non-reading client stalls promptly:
    sysctl -w net.ipv4.tcp_wmem="4096 16384 65536"
    sysctl -w net.core.wmem_max=65536
    sysctl -w net.ipv4.tcp_rmem="4096 8192 16384"
  
  2. Start the server under AddressSanitizer:
  
    ASAN_OPTIONS=abort_on_error=1:halt_on_error=1 ./memcached -l 127.0.0.1
  -p 11211 -m 2048 -u nobody -t 2
  
  3. Store ~300000 keys so a full metadump (~22 MB) far exceeds the kernel
  buffers:
  
     awk 'BEGIN{
       v=sprintf("%120s",""); gsub(/ /,"x",v);
       for(i=1;i<=300000;i++) printf "set k%d 0 0 120 noreply\r\n%s\r\n", i, v;
       printf "get k1\r\n";
     }' | nc -q2 127.0.0.1 11211
  
     Confirm they landed:
  
     printf 'stats\r\n' | nc -q1 127.0.0.1 11211 | grep curr_items   #
  300000
  
  4. Open one connection that requests the dump and never reads it.
     nc auto-drains the socket, so use bash /dev/tcp and only write to the fd:
  
     exec 3<>/dev/tcp/127.0.0.1/11211
     printf 'lru_crawler enable\r\n' >&3
     sleep 0.3
     printf 'lru_crawler metadump all\r\n' >&3
     sleep 600           # never read fd 3
     exec 3<&- 3>&-
  
  5. After the send buffer fills, each 1000 ms poll timeout lets the crawler 
append ~1 item, so the 128 KiB buffer overruns after a minute or two.
     AddressSanitizer aborts with:
  
     ERROR: AddressSanitizer: heap-buffer-overflow ... WRITE of size 73
       #3 crawler_metadump_eval  crawler.c:275   (snprintf)
       #4 item_crawler_thread    crawler.c:555
     allocated by  lru_crawler_set_client  crawler.c:707
  
     The full ASAN report from the reference run is saved as 
asan-unpatched-overflow.log.
     A stock (non-ASAN) build instead gives SIGSEGV / malloc(): corrupted.
  
  6. Apply the two patches, rebuild the same way, and repeat steps 2-4.
     The server stays up for the full run with no overflow and keeps serving 
other clients:
  
     printf 'version\r\n' | nc -q1 127.0.0.1 11211   # VERSION 1.6.24
  
     Upstream now retries the timed-out client and disconnects it after
  ~30 minutes instead of overrunning the buffer.
  
  7. Run the upstream crawler test (t/lru-crawler.t) and confirm it still
  passes.
  
  [Where problems could occur]
  
  The change is limited to lru_crawler_write() and two error handling paths in 
item_crawler_thread() in crawler.c.
  It is a direct upstream backport with no protocol, storage, or wire format 
changes.
  Regression risk is limited to LRU crawler dumps.
  A slow or stalled dump client may now be disconnected after about 30 minutes 
of socket write stalls.
  An incorrect backport of the added lru_locks[i] locking could deadlock the 
crawler thread.
  
  [Other Info]
  
  Upstream fix
  
  "crawler: fix buffer overflow on idle client"
  
https://github.com/memcached/memcached/commit/7eeac6e9a9945aa68298aff379cea5ab3817af7c
  
  "crawler: fix potential unlock of unlocked mutex"
  
https://github.com/memcached/memcached/commit/ea35f441f265b4aef3f7753aa5cbb87d99859123

-- 
You received this bug notification because you are a member of Ubuntu
Bugs, which is subscribed to Ubuntu.
https://bugs.launchpad.net/bugs/2161693

Title:
  memcached: heap buffer overflow in LRU crawler dump (lru_crawler
  metadump) on an idle client

To manage notifications about this bug go to:
https://bugs.launchpad.net/ubuntu/+source/memcached/+bug/2161693/+subscriptions


-- 
ubuntu-bugs mailing list
[email protected]
https://lists.ubuntu.com/mailman/listinfo/ubuntu-bugs

Reply via email to