Repository: benoitc/gunicorn

Issue:
Gunicorn 20.0.0: find_library('c') doesn't work in Alpine Linux
I was trying to run gunicorn 20.0.0 inside an Alpine Linux docker container (python:3.8-alpine).
Gunicorn 19.9.0 works just fine on the same docker image.

After installing `musl`, the error `libc not found` is still present.

Here's a block of code extracted from `gunicorn/socketfromfd.py` that doesn't work in Alpine:

test.py
```python
import ctypes
from ctypes.util import find_library

_libc_name = find_library('c')
if _libc_name is not None:
    libc = ctypes.CDLL(_libc_name, use_errno=True)
else:
    raise OSError('libc not found')
```

Dockerfile
```dockerfile
FROM python:3.8-alpine

WORKDIR /app

RUN apk add musl

COPY . /app

CMD ["python", "test.py"]
```


Discussion:
Right. We should probably test **if python=< 3.7** to use `socket.from_fd` in https://github.com/benoitc/gunicorn/blob/438371ee90b9676336a44c7abaeb30ee7fc57a5c/gunicorn/socketfromfd.py#L65
First, thanks for a great piece of software!
Second, I can confirm this issue; it has occurred in every installation of 20.0.0 on Alpine.
> Right. We should probably test **if python=< 3.7** to use `socket.from_fd` in
> 
> https://github.com/benoitc/gunicorn/blob/438371ee90b9676336a44c7abaeb30ee7fc57a5c/gunicorn/socketfromfd.py#L65

Can confirm that the issue isn't related to a specific Python version, have it happening in 3.6,3.7,3.8 so long as it's in Alpine.
 It's a side effect of using an operating system without all the basic libs in a container unfortunately. Maybe you could use an image using a debian? or an alpine image coming with a libc?

This api is there is pre-3.7 to replace a lack of API for it in Python. Detecting if the api is available (for python >= 3.7) would at least solve the issue for these versions. Before that we need to investigate what could be done.
> It's a side effect of using an operating system without all the basic libs in a container unfortunately. Maybe you could use an image using a debian? or an alpine image coming with a libc?
> 
> This api is there is pre-3.7 to replace a lack of API for it in Python. Detecting if the api is available (for python >= 3.7) would at least solve the issue for these versions. Before that we need to investigate what could be done.

Alpine uses musl libc in place of glibc as part of it being based on busybox, of course using other base images would be a workaround but it will limit gunicorn uses as both alpine & other minimal OS that are growing in popularity (which are often based on it\busybox\etc) all have musl instead of glibc so the affect is likely not limited to alpine (just the most popular one affected).

Sounds to me that something like detecting if musl is installed instead of glibc & if so use whatever solution was in place in 19.9.x is a potential fix that isn't python version specific as we know 19.9.x works well.
found a related issue in docker-library/python#111

maybe this code can be reused in a way: https://github.com/python/cpython/pull/10453/commits/e3f67780aab24401a50af64e688d38c24ee41ad0

There is one possible workaround for that, as twisted users encountered the same:
https://stackoverflow.com/questions/48234723/twisted-server-monkey-patch-file
That piece of code from `gunicorn/socketfromfd.py` could be rewritten as following:
```
_libc_name = find_library('c')
if _libc_name is None:
    _libc_name = 'libc.so.6'
libc = ctypes.CDLL(_libc_name, use_errno=True)
```
Just one of the possible solutions,but probably not the best one.
> maybe this code can be reused in a way: python/cpython@e3f6778

I think that's this issue: https://bugs.python.org/issue21622

Let's find a workaround and make a patch release.
@tilgovi the patch I linked above does the trick (and is linked in the bug ticket you added). I will work on a PR that use it.

Resolving patch:
diff --git a/gunicorn/socketfromfd.py b/gunicorn/socketfromfd.py
--- a/gunicorn/socketfromfd.py
+++ b/gunicorn/socketfromfd.py
@@ -12,7 +12,7 @@
 import sys
 import platform
 
-from ctypes.util import find_library
+from .util import find_library
 
 __all__ = ('fromfd',)
 
diff --git a/gunicorn/util.py b/gunicorn/util.py
--- a/gunicorn/util.py
+++ b/gunicorn/util.py
@@ -3,6 +3,7 @@
 # This file is part of gunicorn released under the MIT license.
 # See the NOTICE for more information.
 import ast
+import ctypes.util
 import email.utils
 import errno
 import fcntl
@@ -635,3 +636,34 @@ def bytes_to_str(b):
 
 def unquote_to_wsgi_str(string):
     return urllib.parse.unquote_to_bytes(string).decode('latin-1')
+
+
+def _findWalk_ldpath(name):
+    def _is_elf(filepath):
+        try:
+            with open(filepath, 'rb') as fh:
+                return fh.read(4) == b'\x7fELF'
+        except:
+            return False
+    from glob import glob
+    if os.path.isabs(name):
+        return name
+
+    # search LD_LIBRARY_PATH list
+    paths = ['/lib', '/usr/local/lib', '/usr/lib']
+    if 'LD_LIBRARY_PATH' in os.environ:
+        paths = os.environ['LD_LIBRARY_PATH'].split(':') + paths
+
+    for d in paths:
+        f = os.path.join(d, name)
+        if _is_elf(f):
+            return os.path.basename(f)
+        prefix = os.path.join(d, 'lib'+name)
+        for suffix in ['so', 'so.*']:
+            for f in glob('{0}.{1}'.format(prefix, suffix)):
+                if _is_elf(f):
+                    return os.path.basename(f)
+
+
+def find_library(name):
+    return ctypes.util.find_library(name) or _findWalk_ldpath(name)


Test patch: