The Goal

Today I had a simple problem that ended up teaching me a lot: connecting a printer to my computer.

I needed to connect a Nelko PL70E-BT to my computer to print shipping labels. Unfortunately I am using FreeBSD as an operating system and Nelko does not offer a driver for the system, only driver packages for Windows, MacOS, and Linux.

The Attempt

Fortunately, Unix & unix-like systems use PostScript Printer Descriptions (.ppd) files as a common method to configure the printer using CUPS, the Common UNIX Printing System.

Getting the .ppf file from the was not too difficult. Nelko offers a installation package for Linux systems in the form of a .deb file. Deb files are installation files made for Debian based Linux distributions. However, they are actually Unix archive files that contain tar files, one of which contains installation information and another that contains the files to be installed. We can use the ar x [file.deb] command to unpack the .deb file, and tar to extract the files in data.tar.xz. This gives us a directory, usr/, which contains the directory structure and files that are to be installed on a Linux distribution. Inside of here we can find the required .ppd file for the printer, as well as a binary file, rastertolabel.

Copying the .ppd file to the respective directory for CUPS on FreeBSD is easy enough (it can actually be uploaded on the CUPS web application running on the localhost), but the challenge is in the custom binary file required for the printer.

The Problem

The .ppd file tells CUPS to run /usr/local/libexec/cups/filter/Nelko/Filter/rastertolabel. If CUPS tries to print to the PL70E-BT without this file running on the host it will throw an error. We can make the directory and put the binary inside of it. However, rastertolabel was designed for Linux systems, not FreeBSD. This can be confirmed with the file command

$ file /usr/local/libexec/cups/filter/Nelko/Filter/rastertolabel
./rastertolabel: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=21c7b30c06d2a0f8fc2dfe438bfd20ef456579ed, for GNU/Linux 3.2.0, not stripped

We can see how this is different from a file built for a for a FreeBSD system when we compare the above with a binary compiled specifically for it.

$ file /bin/sh
/bin/sh: ELF 64-bit LSB pie executable, x86-64, version 1 (FreeBSD), dynamically linked, interpreter /libexec/ld-elf.so.1, for FreeBSD 14.2, FreeBSD-style, stripped

Fortunately FreeBSD has a Linux binary compatibility layer which we can use to run rastertolabel. After enabling the binary compatibility layer we can try running the file to see what we get.

$ /usr/local/libexec/cups/filter/Nelko/Filter/rastertolabel
./rastertolabel: /lib64/libm.so.6: version GLIBC_2.27' not found (required by ./rastertolabel)
./rastertolabel: /lib64/libm.so.6: version GLIBC_2.29' not found (required by ./rastertolabel)
./rastertolabel: /lib64/libstdc++.so.6: version CXXABI_1.3.8' not found (required by ./rastertolabel)
./rastertolabel: /lib64/libstdc++.so.6: version CXXABI_1.3.9' not found (required by ./rastertolabel)
./rastertolabel: /lib64/libstdc++.so.6: version GLIBCXX_3.4.21' not found (required by ./rastertolabel)

This tells us that the dynamic linker is encountering an error when it tries to execute the binary, it cannot find two the shared library objects libm.so.6 and libstdc++.so.6 in the directory /lib64. These two files are apart of the glibc library.

The missing files can be found by using the program ldd to list the dynamic object dependencies

$ ldd /usr/local/libexec/cups/filter/Nelko/Filter/rastertolabel
rastertolabel:
	libcups.so.2 => /usr/local/lib/libcups.so.2 (0x21b860bdb000)
	libstdc++.so.6 => /usr/local/lib/gcc12/libstdc++.so.6 (0x21b864600000)
	libm.so.6 => not found (0)
	libgcc_s.so.1 => /lib/libgcc_s.so.1 (0x21b86117b000)
	libc.so.6 => not found (0)
	libavahi-common.so.3 => /usr/local/lib/libavahi-common.so.3 (0x21b86295a000)
	libavahi-client.so.3 => /usr/local/lib/libavahi-client.so.3 (0x21b862c10000)
	libgnutls.so.30 => /usr/local/lib/libgnutls.so.30 (0x21b86357e000)
	libz.so.6 => /lib/libz.so.6 (0x21b86612b000)
	libthr.so.3 => /lib/libthr.so.3 (0x21b864c53000)
	libm.so.5 => /lib/libm.so.5 (0x21b864e6e000)
	libcrypt.so.5 => /lib/libcrypt.so.5 (0x21b865dda000)
	libc.so.7 => /lib/libc.so.7 (0x21b86627a000)
	libintl.so.8 => /usr/local/lib/libintl.so.8 (0x21b8683d7000)
	libdbus-1.so.3 => /usr/local/lib/libdbus-1.so.3 (0x21b866b47000)
	libp11-kit.so.0 => /usr/local/lib/libp11-kit.so.0 (0x21b866e12000)
	libidn2.so.0 => /usr/local/lib/libidn2.so.0 (0x21b867330000)
	libunistring.so.5 => /usr/local/lib/libunistring.so.5 (0x21b86885d000)
	libdl.so.1 => /usr/lib/libdl.so.1 (0x21b867f10000)
	libtasn1.so.6 => /usr/local/lib/libtasn1.so.6 (0x21b86a5fe000)
	libhogweed.so.6 => /usr/local/lib/libhogweed.so.6 (0x21b868b09000)
	libnettle.so.8 => /usr/local/lib/libnettle.so.8 (0x21b86938e000)
	libgmp.so.10 => /usr/local/lib/libgmp.so.10 (0x21b869f41000)
	libexecinfo.so.1 => /usr/lib/libexecinfo.so.1 (0x21b86b5db000)
	libffi.so.8 => /usr/local/lib/libffi.so.8 (0x21b86bbe6000)
	libelf.so.2 => /lib/libelf.so.2 (0x21b86c406000)
	[vdso] (0x21b860b40000)

The file libc.so.6 is missing, as indicated by the ldd output. Although the system can locate libstdc++.so.6, the binary specifically requires a version that provides the CXXABI_1.3.8 and CXXABI_1.3.9 application binary interfaces, as well as the GLIBCXX_3.4.21 C++ library features. I tried configure rastertolabel to use the FreeBSD version of the library object (the details on how I’ll skip for now), but when I tried to run the application I received the following error:

$ /usr/local/libexec/cups/filter/Nelko/Filter/rastertolabel
./rastertolabel: symbol lookup error: /lib64/libdl.so.2: undefined symbol: _dl_vsym, version GLIBC_PRIVATE

Although the FreeBSD version of libstdc++.so.6 includes these ABIs and features, they differ from those expected by the Linux ELF file. As a result of this incompatibility, the binary will not run even if linked to the FreeBSD version of libstdc++.so.6. Therefore, we need the Linux version of the standard C library installed on the computer.

While it is possible glibc on FreeBSD by downloading the tar file and extracting onto the root drive, doing so would risk harming the system as we would be introducing a different standard library to the system. Instead it would be better to get the files for glibc, extract them into an isolated directory, and then edit the binary file to look for for the shared objects in the new directory.

Solving The Problem

Obtaining the necessary files for glibc was straightforward. In the past I installed the FreeBSD package debootstrap to download an Ubuntu userland to the directory /compat/ubuntu. The Ubuntu userland contains all the necessary files forglibc When I copied and ran the file rastertolabel inside of Linux chroot I get the following:

// inside an Ubuntu chroot
linuxHolloway@j_holloway:~$ ./rastertolabel
ERROR: rastertolabel job-id user title copies options [file]

The error message it gave was in relation to it expecting data for the printing process, not missing shared objects. This tells us that the rastertolabel will run on FreeBSD using Linux compatibility, if it can find the required shared objects. However, we don’t want to run the binary in a chroot as that complicates running it with CUPS. Instead we want rastertolabel to use the Linux shared object files inside of the /compat/ubunut/lib64 rather than where it is currently trying to find them.

We can use the application PatchELF to modify the runpath of the binary executable. By editing te runpath, the dynamic linker will search for a different location

$ patchelf --set-rpath /compat/ubuntu/lib/x86_64-linux-gnu:/compat/ubuntu/lib64 /usr/local/libexec/cups/filter/Nelko/Filter/rastertolabel

The arguments “–set-rpath /compat/ubuntu/lib/x8664-linux-gnu:/compat/ubuntu/lib64” change the runtime path for the dynamic linker to look for the library fles in /compat/ubuntu/lib/x86_64-linux-gnu and /compat/ubuntu/lib64. This can be confirmed by using _readelf and searching for the runpath

$ readelf -d /usr/local/libexec/cups/filter/Nelko/Filter/rastertolabel | grep PATH
 0x000000000000001d RUNPATH              Library runpath: [/compat/ubuntu/lib/x86_64-linux-gnu:/compat/ubuntu/lib64]

If I didn’t include both directories the error message would not change. After changing the ELF’s information to include both directories running the binary on FreeBSD would give a new error:

$ /usr/local/libexec/cups/filter/Nelko/Filter/rastertolabel
ELF interpreter /compat/ubuntu/lib64/ld-linux-x86-64.so.2 not found, error 2
Abort trap

This error confused me as I knew I had the file /compat/ubuntu/lib64/ld-linux-x86-64.so.2, it was a symlink that points to the file /compact/ubuntu/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2. The existence of both can be confirmed by using the ls program

$ ls -l /compat/ubuntu/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
-rwxr-xr-x  1 root wheel 240936 May  6  2024 /compat/ubuntu/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2

$ ls -l /compat/ubuntu/lib64/ld-linux-x86-64.so.2
lrwxr-xr-x  1 root wheel 42 May  6  2024 /compat/ubuntu/lib64/ld-linux-x86-64.so.2 -> /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2

When looking at the symlink path you can see the issue. It tells the linker to look for the file in /lib/x86_64-linux-gnu/ rather than in /compact/ubuntu/lib/x86_64-linux-gnu/. It is giving a symlink for for the absolute path from inside the chroot, not from the file system root. However, if I were to create a new symlink that redirected to “/compact/ubuntu/lib/x86_64-linux-gnu/” then the Ubuntu chroot would no longer work. The chrooted linker would now have the wrong address for the shared library. It would try to find the file in a directory that doesn’t exist from the chroot’s point of view.

Successful Printing!

To solve the problem so that the /compat/ubuntu/lib64/ld-linux-x86-64.so.2 links to the correct file on both the FreeBSD host as well as inside the Linux chroot the symlink should instead link to the relative path for /compact/ubuntu/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2

$ sudo ln -sf ../lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 /compat/ubuntu/lib64/ld-linux-x86-64.so.2

$ ls -l /compat/ubuntu/lib64/ld-linux-x86-64.so.2
lrwxr-xr-x  1 root wheel 44 Jan  3 17:36 /compat/ubuntu/lib64/ld-linux-x86-64.so.2 -> ../lib/x86_64-linux-gnu/ld-linux-x86-64.so.2

Running rastertolabel from FreeBSD now gives us the same message as it did from inside the Linux chroot

$ /usr/local/libexec/cups/filter/Nelko/Filter/rastertolabel
ERROR: rastertolabel job-id user title copies options [file]

And it worked!! Testing the printer in CUPS I was able to print the shipping labels I needed without any trouble.

All in all a playing with the printer started with what seemed like a small task. But to get it running I had to extract Debian packages, manipulate binaries, review dynamic linker paths and fiddle around with symlinks meant for a chroot environment. All this so that I didn’t have to walk across the room to my colleague’s printer.