Executable incorrectly checks for the FILE "/bin/bash"

I’m trying to get the Unity Licensing Server running on nixos. (I know).

Unfortunately it has a dumb filesystem check within it where it first checks to make sure the file “/bin/bash” exists in the filesystem before trying to execute bash. You can tell from the stack trace that they manually added the check with a custom error message.

Unhandled exception. System.IO.FileNotFoundException: Bash cannot be found
File name: '/bin/bash'
   at Unity.Licensing.Platform.CommonUtils.RunInBash(String cmd, String& output)
   at Unity.Licensing.Server.Services.ServerPermissionService.ApplyCommand(String command)
   at Unity.Licensing.Server.Services.ServerPermissionService.IsUserInGroup()
   at Unity.Licensing.Server.Services.ServerPermissionService.IsUserInGroupInCurrentSession()
   at Unity.Licensing.Server.ServerImportExecutor.ValidateUserPermission()
   at Unity.Licensing.Server.ServerImportExecutor.OnExecute(CommandLineApplication app, IConsole console, String archivePath)
   at Unity.Licensing.Server.ServerImport.Execute(ServerImportExecutor commandService, CommandLineApplication app, IConsole console)
   at Unity.Licensing.Server.InjectedCommand`1.OnExecute(CommandLineApplication app, IConsole console)
   at Unity.Licensing.Server.InjectedCommand`1.OnExecute(CommandLineApplication app, IConsole console)
   at McMaster.Extensions.CommandLineUtils.Conventions.ExecuteMethodConvention.InvokeAsync(MethodInfo method, Object instance, Object[] arguments)
   at McMaster.Extensions.CommandLineUtils.Conventions.ExecuteMethodConvention.OnExecute(ConventionContext context, CancellationToken cancellationToken)
   at McMaster.Extensions.CommandLineUtils.Conventions.ExecuteMethodConvention.<>c__DisplayClass0_0.<<Apply>b__0>d.MoveNext()
--- End of stack trace from previous location ---
   at McMaster.Extensions.CommandLineUtils.CommandLineApplication.ExecuteAsync(String[] args, CancellationToken cancellationToken)
   at Program.<Main>$(String[] args)
   at Program.<Main>(String[] args)
fish: Job 1, 'sudo ./Unity.Licensing.Server i…' terminated by signal SIGABRT (Abort)

Simultaneously, it requires write access to the directory /usr/share/unity3d.

I first made /usr/share/unity3d available via tmpfiles.d in my config (with only lax permissions because I’m trying to get it to work).

# configuration.nix
systemd.tmpfiles.rules = [
  "d  /usr/share/unity3d/ 7777 root root - -"
];

Then I use a shell.nix to set up a sandbox to run the server.

# shell.nix
{ pkgs ? import <nixpkgs> {} }:
(pkgs.buildFHSEnv {
  targetPkgs = pkgs: (with pkgs; [
    bash
  ];
  runScript = "./Unity.Licensing.Server"
}).env

Well great, now I’m in a catch-22:

  • If I use buildFHSEnv, this doesn’t mount the /usr/share/unity3d/ to the sandbox and it’s seemingly not possible to. (/usr is read-only)
  • If I use mkShell to run it, it fails to find /bin/bash and I cannot create it. (/bin is read-only)

So my question is, is there a way to make /bin/bash available in mkShell? OR
Is there a way to make the host’s /usr/share/unity3d available to buildFHSEnv?

1 Like

Sounds like a job for bubblewrap?

1 Like

SNIP
NEVERMIND it didn’t work.

I’d like bubblewrap, though I couldn’t find much reading material about the subject. I’ll keep looking.

Bubblewrap is only present in buildFHSDev, but since /usr is mounted read only, you can’t add to it with extra bubble wrap mounts. Bubblewrap doesn’t appear to be available with mkShell, and even if it were, /bin is also read-only.

I tried the following:

  extraBwrapArgs = [
    "--bind ./unity3d /usr/share/unity3d"
  ];

bwrap: Can’t mkdir /usr/share/unity3d: Read-only file system

Looking at the other bubble wrap arguments, I’m not finding any other tools at my disposal.

What do you mean? Just add pkgs.bubblewrap to your shell, or run nix-shell -p bubblewrap, or use any other technique for getting an application on Nix.

If you don’t bind /, you don’t have to worry about /bin or /usr being read-only. Just bind in the bits you need—/nix and /dev, any user directories you want access to, and populate /bin and /usr with whatever mix of binds, writable directories, and overlays you want.

1 Like

Oh okay!

Manually binding everything I need sucks, the darn app requires a dozen packages. I’ll do my best though.

An solution which works for me is using:

    services.envfs.enable = true;

See: GitHub - Mic92/envfs: Fuse filesystem that returns symlinks to executables based on the PATH of the requesting process.

1 Like

Create a package that adds a symlink from /usr/share/unity3d to something that gets bind-mounted? Or even create a package with this directory and do extraBwrapArgs with --bind?

You can buildEnv all the stuff you need, then bind-mount /nix and use all-the-stuff-env for /bin and /usr/bin

1 Like

Unfortunately envfs didn’t work, it appears that Unity’s file-exists check bypasses envfs’s “magic” mount. (I did run sudo envfs /bin before trying)

bwrap seems really promising, struggling to get it to run properly in a shell.nix though. I’ll keep hacking at it.

I’ll try making a package that symlinks if I can’t find a way to easily bwrap.

buildFHSEnv is already Bubblewrap so you can add the options there

1 Like

Tried bwrap. Ran into various nix-ld panics, and extremely undescriptive errors. I kept mounting more stuff and just kept getting new unhelpful errors. I don’t have the prerequisite knowledge to reconstruct the filesystem state from scratch without /bin or /usr. I was reading how buildFHSEnv does it and think it might be easier for me to try 7c6f434c’s suggestion.

Here’s a few errors I got. started with mounting proc, and dev.

bwrap: execvp bash: No such file or directory

realized I forgot /nix

bash-5.2# ./Unity.Licensing.Server import /home/naelstrof/Downloads/kobold.zip
bash: ./Unity.Licensing.Server: cannot execute: required file not found

tried adding /lib and /lib64

bash-5.2# ./Unity.Licensing.Server import /home/naelstrof/Downloads/kobold.zip
[nix-ld] FATAL: panicked at src/main.rs:185:55:
called `Result::unwrap()` on an `Err` value: Posix(2)
Aborted (core dumped)

Moving on to trying to construct a package that contains a symlink to use in buildFHSEnv.

Unfortunately, both /bin and /usr are already read-only by the time my arguments get added. Unless you can overlay on top of a read-only directory-- I didn’t try that. I’m not exactly sure how it works. I’ll look into it.

You can overlay on top of a read-only directory, but it needs to exist. However, I believe that share gets aggregated into /usr/share/ so you can just add a quasi-package that creates an empty directory or a symlink to an RW-mounted location at /usr/share/unity3d as a dependency of your FHS env.

1 Like

Nice, yeah this is very close. I created a quasi-package that looks like this:

#./unity-folder/default.nix
{writeTextFile}:
  writeTextFile {
  name = "testing";
  text = "";
  destination = "/share/unity3d/testing";
}

And I included it at the top of my shell.nix with:

{ pkgs ? import <nixpkgs> {}, unity-folder ? pkgs.callPackage ./unity-folder { } }:

including unity-folder as a package ensures that /usr/share/unity3d exists by the time my bwrapArgs are evaluated.

There’s no documentation for --overlays though, all I’ve got is the help. Not even present within the manpages.

extraBwrapArgs = [
  "--overlay-src /usr/share/unity3d"
  "--overlay /usr/share/unity3d /usr/share/unity3d /usr/share/unity3d"
];

bwrap: Can’t make overlay mount on /newroot/usr/share/unity3d with options upperdir=/oldroot/usr/share/unity3d,workdir=/oldroot/usr/share/unity3d,lowerdir=/oldroot/usr/share/unity3d,userxattr: Invalid argument

Guess I was naive thinking I could get get away with that. Going to try to finding reading material. This feature seems very new.

I’ll try some more after I get some sleep. I realize I can try making a package that tries to symlink to the working directory or similar instead of trying to use bwrap’s overlay like you suggested earlier.

Thank you all for the super helpful input and patience!

Why not just --bind or --ro-bind ?

1 Like

Oh I assumed that wouldn’t work due to it being read-only. I’ll try when I wake up in the morning.

This worked! Nice!
Now it has the proper file structure, and it can also be ran as a normal user. Sweet! I’ll share my script as soon as I get it working.

However it’s now trying to run “groupadd” within the sandbox, which isn’t allowed.

INFO  - [Unity.Licensing.Server.Services.ServerPermissionService] Will run: groupadd unity-licensing-server
INFO  - [Unity.Licensing.Server.Services.ServerPermissionService] cmd result: 10: groupadd: cannot open /etc/group: Too many levels of symbolic links

It also seems to check for the group with a id -nG root | grep unity-licensing-server which also fails, but it only does this when I try to run it with fakeroot.

Faking groups and sharing namespaces is not very well documented for bwrap, I’ll keep looking.

I think you can just hand-craft and /etc/groups and bind-mount it over, but not sure.

1 Like

I had the same thought, and I gave it a shot, but /etc/ needs to exist, so I bind mount /etc also and then we’re back to the really unhelpful panic messages. I can see that buildFHSEnv handles /etc/ extremely carefully. This has become a 3 day project and I realize I’m silly for even trying to run such scuffed software from NixOS haha. Not to mention every time I fail the verification I have to manually reach out to support to clear my “unhealthy license” status lmao.

I’ve submitted a bugreport to unity for trying to load /bin/bash as a file because if that is fixed then I’d be able to trivially load it with mkShell (at the cost of it being unsandboxed and running as root).

My solution for now is to spin up an Arch VM who’s job is to just run the licensing server. Where it can have as jank of a setup as it needs to run.

The package almost did work however, just failed to get permissions. I’ll post my script for posterity.

# shell.nix
{ pkgs ? import <nixpkgs> {}, unity-folder ? pkgs.callPackage ./unity-folder { } }:

(pkgs.buildFHSEnv {
  name = "simple-x11-env";
  targetPkgs = pkgs: (with pkgs; [
    udev
    alsa-lib
    gtk3
    udev
    xorg.libXrandr
    stdenv.cc.cc.lib
    libglvnd
    xorg.libX11
    xorg.libXcursor
    glib
    gdk-pixbuf
    libxml2
    zlib
    icu
    git
    ripgrep
    openssl
    bash
    fakeroot
    shadow

    xorg.libXi
    xorg.libXrender
    gnome2.GConf
    libcap
    unity-folder
  ]);
  multiPkgs = pkgs: (with pkgs; [
    udev
    alsa-lib
  ]);

  extraInstallCommands = ''
    mkdir -p ./unity3d
    mkdir -p ./root-home
    mkdir -p ./fakehome/licensing-server
  '';

  extraBwrapArgs = [
    "--bind ./unity3d /usr/share/unity3d"
    "--bind ./root-home /root"
    "--bind ./fakehome /home/naelstrof"
    "--bind ./ ./home/naelstrof/licensing-server"

    #"--setenv FAKEROOTDONTTRYCHOWN 1"
    #"--unshare-user"
    #"--uid 0"
    #"--gid 0"
  ];

  runScript = pkgs.writeScript "run.sh" ''
    ./Unity.Licensing.Server setup /home/naelstrof/Downloads/kobold.zip
  '';

}).env

And the package…

# ./unity-folder/default.nix
{writeTextFile}:

writeTextFile {
  name = "testing";
  text = "";
  destination = "/share/unity3d/testing";
}

Most of the packages in there may or may not be required as I was mid-tinkering.
Running shell-nix in the folder properly creates a /usr/share/unity3d, ensures /bin/bash is on the path, and actually allows Unity.Licensing.Server to run setup. Even keeps files owned by a unprivileged user.
Just fails at creating a group on run, and switching to that group due to the sandbox.

tl;dr: I’m gonna fix it with a VM running a different OS and asking Unity to fix it. Thanks for all the help!

Just in case, you might want to know that you can bind-mount a file over a file.