Repository: JonathonReinhart/staticx (language: python)

Issue:
libnssfix fails to build under Ubuntu 22.04
Noted here:
https://github.com/JonathonReinhart/staticx/pull/194#issuecomment-1624289769

```
gcc -o scons_build/debug/libnssfix/libnssfix.so -Wl,--fatal-warnings -Wl,--no-as-needed -nostdlib -Wl,--no-undefined -shared scons_build/debug/libnssfix/nssfix.os -Lscons_build/debug/lib scons_build/debug/libnssfix/libc.so.6 -lnss_dns -lnss_files
/bin/ld: cannot find -lnss_dns: No such file or directory
/bin/ld: cannot find -lnss_files: No such file or directory
```


Discussion / hints:
The problem is missing symlinks:

In ubuntu 22:04, there is no `libnss_files.so` symlink 😦 
```
jreinhart@cbc4e6bb2b96:~/gitrepos/staticx$ ls -l /usr/lib/x86_64-linux-gnu/libnss*
lrwxrwxrwx 1 root root    40 Jul  6  2022 /usr/lib/x86_64-linux-gnu/libnss_compat.so -> /lib/x86_64-linux-gnu/libnss_compat.so.2
-rw-r--r-- 1 root root 44024 Jul  6  2022 /usr/lib/x86_64-linux-gnu/libnss_compat.so.2
-rw-r--r-- 1 root root 14352 Jul  6  2022 /usr/lib/x86_64-linux-gnu/libnss_dns.so.2
-rw-r--r-- 1 root root 14352 Jul  6  2022 /usr/lib/x86_64-linux-gnu/libnss_files.so.2
lrwxrwxrwx 1 root root    40 Jul  6  2022 /usr/lib/x86_64-linux-gnu/libnss_hesiod.so -> /lib/x86_64-linux-gnu/libnss_hesiod.so.2
-rw-r--r-- 1 root root 27160 Jul  6  2022 /usr/lib/x86_64-linux-gnu/libnss_hesiod.so.2
```

Note that there is no `libnss_dns.so` or `libnss_files.so` symlink. Hmm.

It's also a little suspicious that both `libnss_dns.so.2` and `libnss_files.so.2` are exactly the same size:
```
-rw-r--r-- 1 root root 14352 Jul  6  2022 /usr/lib/x86_64-linux-gnu/libnss_dns.so.2
-rw-r--r-- 1 root root 14352 Jul  6  2022 /usr/lib/x86_64-linux-gnu/libnss_files.so.2
```
As I suspected, it appears that `libnss_dns.so.2` and `libnss_files.so.2` are stubs (export no symbols). The real implementation is moved into libc as of GLIBC 2.34.

Relevant changes:
- `libnss_files`
  - bminor/glibc@9ed48feed8 (`glibc-2.33.9000-881-g9ed48feed8`): `nss: Do not install static linker input files for libnss_files`
  - bminor/glibc@f9c8b11ed7726b858cd7b7cea0d3d7c5233d78cf: `nss: Access nss_files through direct references`
  - bminor/glibc@6212bb67f4695962748a5981e1b9fea105af74f6: `nss_files: Move into libc`

- `libnss_dns`
  - bminor/glibc@ee5ed99922ca90bcea4a2f9a48a0c9ae4b534ece: `nss: Directly load nss_dns, without going through dlsym/dlopen`
  - bminor/glibc@e1fcf21474c5b522fdad4ac0191d5dcc3271dba6: `resolv: Move nss_dns into libc`
    - bminor/glibc@ea9878ec271c791880fcbbe519d70c42f8113750
    - bminor/glibc@21a497cc58df2b9b02dc687b97f105335e7a1c50
    - bminor/glibc@b165c65c35d0fc4d60d63ae101f4edfa21c0d30b
    - bminor/glibc@66ac4268f48e4dbcb09b2b6128efa84a6564c1e6
    - bminor/glibc@be5773e1668ab62e980b58c68c3ffd0fd65e31af
    - bminor/glibc@7131727c6ba451e1c5bf075194c7adc9292906c4
    - bminor/glibc@72a51ac647b2fc33a44434d3d125a844801609ae
    - bminor/glibc@762a2b2d341a9d6a4ea088479616907c5b4e9a7b
    - bminor/glibc@08d4a98070c4c4f69c6d04f483d105121effba08
    - bminor/glibc@17d0407a5cac70652f3544e59505c1712b36fd1a
    - bminor/glibc@9515126f905d9322fc6d2b1a3d95539a0a499f48
    - bminor/glibc@2fbe5860d33ca2318b35ea6d31beefa381b4ac8a
    - bminor/glibc@391e02236b931132c0e8b5ba4c3b087c2aaa1044
    - bminor/glibc@fd8a87c0c1932de591e7ad108ff6288a4b6b18c9
    - bminor/glibc@7ec366a08a7cec7bd23d6b2a4f7954860e3a16fd
    - bminor/glibc@640bbdf71c6f10ac26252ac67a22902e26657bd8
    - bminor/glibc@13e1f86706e463de4429f7e88f47c6ff65cd845e
    - bminor/glibc@7ed1ac6da3d6ae5bfbbf4c4bdce07a40e8113df8
    - bminor/glibc@276e9822b3402d49a3c9bea713f89dc855812152
    - bminor/glibc@4e1d3db1e86804283cd21f3186e06d397284ac70
    - bminor/glibc@cff2c78c513ef8d51e69a6933f1c6aef8a24a6d6
    - bminor/glibc@248dbed1187038918d79f62cd9cf631f4150c2a0

## Initial thoughts
If `libnss_files`/`libnss_dns` are built into libc, we can just leave these out of the link.

How do we detect this?
- GLIBC version?
- Detect this at config time by trying to link against `libnss_files` (`_dns`)
  - What if something is broken? How does this interact with the existing NSS config check?

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 919d548..c8c1dd7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,9 @@ All notable changes to this project will be documented in this file.
 This project adheres to [Semantic Versioning](https://semver.org/).
 
 ## [Unreleased]
+### Fixed
+- Fixed issue causing libnssfix link failure when building on GLIBC 2.34 ([#255])
+
 ### Removed
 - Dropped support for Python 3.5 and 3.6 ([#247])
 
@@ -334,3 +337,4 @@ Initial release
 [#237]: https://github.com/JonathonReinhart/staticx/pull/237
 [#238]: https://github.com/JonathonReinhart/staticx/pull/238
 [#247]: https://github.com/JonathonReinhart/staticx/pull/247
+[#255]: https://github.com/JonathonReinhart/staticx/pull/255
diff --git a/SConstruct b/SConstruct
index c7469f0..f3d4325 100644
--- a/SConstruct
+++ b/SConstruct
@@ -59,10 +59,26 @@ def BuildSubdir(env, dirname):
 base_env.AddMethod(BuildSubdir)
 
 
+###########
+# Configure
 conf = base_env.Configure(custom_tests=custom_tests)
-has_nss = conf.CheckNSS()
-conf.Finish()
 
+# Does our default libc have NSS?
+conf.env["HAS_NSS"] = conf.CheckNSS()
+
+# Starting in GLIBC 2.34, some NSS modules (files, dns) can be built-in to
+# libc.so. Determine which ones are builtin. See #245.
+conf.env["NSS_MODS_BUILTIN"] = []
+if conf.env["HAS_NSS"]:
+    maybe_builtin_nss_modules = ["files", "dns"]
+    for mod in maybe_builtin_nss_modules:
+        libname = "libnss_" + mod
+        # It the library doesn't exist, assume it is built-in.
+        if not conf.BasicCheckLib(libname):
+            conf.env.Append(NSS_MODS_BUILTIN=libname)
+    print("Builtin NSS modules:", conf.env["NSS_MODS_BUILTIN"])
+
+base_env = conf.Finish()
 
 ################################################################################
 # Bootloader
@@ -79,7 +95,7 @@ for env in bootloader_env.ModeEnvs():
 
 ################################################################################
 # nssfix
-if has_nss:
+if env['HAS_NSS']:
     for env in base_env.ModeEnvs():
         env.InstallAs('#staticx/assets/$MODE/libnssfix.so', env.BuildSubdir('libnssfix'))
 else:
diff --git a/libnssfix/SConscript b/libnssfix/SConscript
index 08f2fa6..7d4912f 100644
--- a/libnssfix/SConscript
+++ b/libnssfix/SConscript
@@ -22,6 +22,16 @@ env.NsswitchConfH(
     source = nsswitch_conf,
 )
 
+
+# Link against the configured NSS module libraries.
+# These aren't needed directly for the library itself, but this ensures
+# that they will be available for nss at runtime (via dlopen), and
+# allows their paths to be easily discovered via ldd(1).
+nss_libs = set(env.GetNsswitchLibs(nsswitch_conf))
+
+# Exclude those already built-in to glibc.
+nss_libs -= set(env["NSS_MODS_BUILTIN"])
+
 # Build libnssfix.so
 libnssfix = env.SharedLibrary(
     target = 'nssfix',
@@ -50,12 +60,7 @@ libnssfix = env.SharedLibrary(
     ],
     LIBS = [
         libc_stub,
-
-        # These aren't needed directly for the library itself, but this ensures
-        # that they will be available for nss at runtime (via dlopen), and
-        # allows their paths to be easily discovered via ldd(1).
-        env.GetNsswitchLibs(nsswitch_conf),
-    ],
+    ] + list(nss_libs),
 )
 
 Return('libnssfix')


Test patch:
diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml
index 760fdc0..58fa030 100644
--- a/.github/workflows/build-test.yml
+++ b/.github/workflows/build-test.yml
@@ -7,13 +7,27 @@ on:
 
 jobs:
   build:
-
-    runs-on: ubuntu-20.04  # See #245
     strategy:
       matrix:
-        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+        # It's undocumented, but matrix variable arrays can be maps, too!
+        # https://stackoverflow.com/a/68940067/119527
+        os:
+          - version: "ubuntu-20.04"
+            native-python: "3.8"
+          - version: "ubuntu-22.04"
+            native-python: "3.10"
+        python-version:
+          - "3.7"
+          - "3.8"
+          - "3.9"
+          - "3.10"
+          - "3.11"
       fail-fast: False
 
+    name: "${{matrix.os.version}}: Python ${{matrix.python-version}}"
+
+    runs-on: ${{ matrix.os.version }}
+
     steps:
     - uses: actions/checkout@v3
 
@@ -25,7 +39,7 @@ jobs:
     # with StaticX. See #224.
     - name: Set up Python ${{ matrix.python-version }}
       uses: deadsnakes/action@v3.0.1
-      if: "(matrix.python-version != '3.8')"  # Already installed in ubuntu-20.04
+      if: "(matrix.python-version != matrix.os.native-python)"  # Already installed
       with:
         python-version: ${{ matrix.python-version }}
 
diff --git a/site_scons/conftest.py b/site_scons/conftest.py
index dc09809..fb139dd 100644
--- a/site_scons/conftest.py
+++ b/site_scons/conftest.py
@@ -14,6 +14,26 @@ def CheckNSS(context):
     return ok
 
 
+def BasicCheckLib(context, libname):
+    """Check for a C library.
+
+    Similar to built-in CheckLib but works around
+    https://github.com/SCons/scons/issues/4373.
+    """
+    context.Message("Checking for library %s... " % libname)
+
+    oldLIBS = context.AppendLIBS([libname])
+    ok = context.TryLink(
+        "int main(void) { return 0; }",
+        ".c"
+    )
+    context.SetLIBS(oldLIBS)
+
+    context.Result(ok)
+    return ok
+
+
 custom_tests = dict(
     CheckNSS = CheckNSS,
+    BasicCheckLib = BasicCheckLib,
 )