Running Xvfb on a RHEL Shared Host (without X)

This is how I compiled the Xorg Server for RHEL on a CentOS machine with modifications to create a portable Xvfb binary.

Xvfb (X virtual framebuffer) is an in-memory display server for Linux and Unix-like OSes. It enables running graphical applications without a display such as running a headless browser (e.g. A full-blown Firefox instance without a display nor input devices). Out of the box it needs elevated access, or rather, it needs access to certain paths and auxiliary binaries that only an elevated user can control (i.e. /usr/bin/xkbcomp or /usr/share/X11/xkb). On a shared host X is most likely not installed, so that compounds the problem. Here is how I got it working on a shared host running 64-bit RHEL (CentOS).

Copying over Xvfb doesn’t work because some paths are hard-coded.

Initially I copied over the Xvfb binary and shared libraries like this to the shared host. This was sufficient to run ./Xvfb itself, except Xvfb wanted to compile a keymap file to /tmp/server-99.xkm using binary /usr/bin/xkbcomp. Suppose you were to blissfully get your hosting provider to upload xkbcomp and its shared libraries to that path, the next problem is that the needed keymap files are in the non-existent [system path]/X11/xkb folder (e.g. X11/xkb/rules/evdev), but X isn’t installed. Rats.

Hacking the Xvfb binary to bypass xkbcomp isn’t reliable

A clever person on StackOverflow suggested hacking the Xvfb binary with string manipulation in order to trick/bypass the xkbcomp (keymap compiling) section. Using strings on Xvfb he tracked down this bit of code in xkb/ddxLoad.c:

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

if(asprintf(&buf,

"\"%s%sxkbcomp\" -w %d %s -xkm \"%s\" "

"-em1 %s -emp %s -eml %s \"%s%s.xkm\"",

xkbbindir,xkbbindirsep,

((xkbDebugFlags<2)?1:

((xkbDebugFlags>10)?10:(int)xkbDebugFlags)),

xkbbasedirflag?xkbbasedirflag:"",xkmfile,

PRE_ERROR_MSG,ERROR_PREFIX,POST_ERROR_MSG1,

xkm_output_dir,keymap)==-1)

buf=NULL;

free(xkbbasedirflag);

if(!buf){

LogMessage(X_ERROR,

"XKB: Could not invoke xkbcomp: not enough memory\n");

returnNULL;

}

#ifndef WIN32

out=Popen(buf,"w");

#else

out=fopen(tmpname,"w");

#endif

The idea is to patch the binary with another command which merely copies a pre-compiled keymap file to the correct destination and returns successfully. The full procedure is here. This almost works, except we don’t know 100% of the time what the destination compiled keymap file is supposed to be called. It takes the form /tmp/server-[1..99].xkm plus we only have limited real estate in replacing the string above. I tried to patch the string with a shell NOP command instead (:) and manually copied the default.xkm file, but other problems happened later. Good try though.

Near the bottom of xkbInit.c is the function XkbProcessArguments(int argc, char *argv[], int i). At the bottom I added this code to allow environment variables to change the hard-coded locations used in keymap compiling.

812

813

814

815

816

817

818

819

820

821

822

823

824

825

826

827

828

return2;

}

// ADDED - Change xkbcomp bin directory with an environment variable

char*xkbBinDir=getenv("XKB_BINDIR");

if(xkbBinDir){

XkbBinDirectory=Xstrdup(xkbBinDir);

}

// ADDED - Change base xkb directory with an environment variable

char*xkbBaseDir=getenv("XKBDIR");

if(xkbBaseDir){

XkbBaseDirectory=Xstrdup(xkbBaseDir);

}

return0;

}

This got me to the point where xkbcomp is looking in the right folders, but wouldn’t it be nice if I could omit all the extra folders and files needed to compile a default keymap?

To this end I manually compiled a default keymap default.xkm from default.xkb using the following description1:

1

2

3

4

5

6

7

xkb_keymap"default"{

xkb_keycodes{include"evdev+aliases(qwerty)"};

xkb_types{include"complete"};

xkb_compatibility{include"complete"};

xkb_symbols{include"pc+us+inet(evdev)"};

xkb_geometry{include"pc(pc105)"};

};

and running this command to compile it:

1

xkbcomp-xkm default.xkb

This results in a default.xkm which can be copied into the same folder supplied to XKBDIR. This next modification will use the above-created keymap.

Near the top of ddxLoad.c is the function RunXkbComp(xkbcomp_buffer_callback callback, void *userdata). I made the following changes to use the pre-made XKM file:

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

RunXkbComp(xkbcomp_buffer_callback callback,void*userdata)

{

FILE*out;

char*buf=NULL,keymap[PATH_MAX],xkm_output_dir[PATH_MAX];

constchar*emptystring="";

char*xkbbasedirflag=NULL;

constchar*xkbbindir=emptystring;

constchar*xkbbindirsep=emptystring;

#ifdef WIN32

chartmpname[PATH_MAX];

constchar*xkmfile=tmpname;

#else

// MODIFICATION - Now using 'default.xkm' file to satisfy xkbcomp

// const char *xkmfile = "-";

constchar*xkmfile="default.xkm";

#endif

With the above two source file modifications, the xkbcomp command will be constructed like this:

153

154

155

156

157

158

159

160

161

asprintf(&buf,

"\"%s%sxkbcomp\" -w %d %s -xkm \"%s\" "

"-em1 %s -emp %s -eml %s \"%s%s.xkm\"",

xkbbindir,xkbbindirsep,

((xkbDebugFlags<2)?1:

((xkbDebugFlags>10)?10:(int)xkbDebugFlags)),

xkbbasedirflag?xkbbasedirflag:"",xkmfile,

PRE_ERROR_MSG,ERROR_PREFIX,POST_ERROR_MSG1,

xkm_output_dir,keymap)

which effectively becomes a command similar to the following which performs a check on the default.xkm keymap file and copies it to the /tmp folder

1

$XKB_BINDIR/xkbcomp-w1-R$XKBDIR-xkm"default.xkm""/tmp/server-99.xkm"

Next, several dependencies need to be installed to compile Xvfb. To find out which, I cd‘d to the X11 source files and ran ./configure as an unprivileged user (I used the default osboxes account in the VMWare image). ./configure will helpfully die at dependency failures. For example:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

[osboxes@localhost xorg-server-1.17.4]$./configure

checking foraBSD-compatible install.../usr/bin/install-c

checking whether build environment issane...yes

checking forathread-safe mkdir-p.../bin/mkdir-p

checking forgawk...gawk

checking whether makesets$(MAKE)...yes

checking whether makesupports nested variables...yes

checking forstyle of include used by make...GNU

checking forgcc...no

checking forcc...no

checking forcl.exe...no

configure:error:in`/home/osboxes/xorg-server-1.17.4':

configure:error:no acceptableCcompiler found in$PATH

See`config.log'formoredetails

This means a compiler is not present. I use GCC – $ yum search gcc.

To make this task of dependency hunting easier it helped to have two terminals open – one for root and the other for an unprivileged user.

Linux root and normal user side by side

The next dependency missing was pixman.

1

2

3

4

checking forPIXMAN...no

configure:error:Packagerequirements(pixman-1>=0.27.2)were notmet:

No package'pixman-1'found

When messages like this arose, I needed to find the package which supplies the missing dependency using yum search.

Finally I was able to run ./configure and successfully finish the script.

Next, I ran make in the same sources directory to build the Xvfb binary. It is found in [sources]/hw/vfb.

Now this binary, and this binary alone, I uploaded to my shared host and checked the linked dependencies:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

$ldd./Xvfb

linux-vdso.so.1=>(0x00007ffe7d18f000)

libcrypto.so.10=>/usr/lib64/libcrypto.so.10(0x00000037c4000000)

libdl.so.2=>/lib64/libdl.so.2(0x00000037c2800000)

libGL.so.1=>notfound

libpthread.so.0=>/lib64/libpthread.so.0(0x00000037c2400000)

libpixman-1.so.0=>/usr/lib64/libpixman-1.so.0(0x00000037ca400000)

libXfont.so.1=>/usr/lib64/libXfont.so.1(0x00007f70b987d000)

libXau.so.6=>/usr/lib64/libXau.so.6(0x00000037c7800000)

libm.so.6=>/lib64/libm.so.6(0x00000037c3000000)

librt.so.1=>/lib64/librt.so.1(0x00000037c2c00000)

libc.so.6=>/lib64/libc.so.6(0x00000037c2000000)

libz.so.1=>/lib64/libz.so.1(0x00000037c3400000)

/lib64/ld-linux-x86-64.so.2(0x00000037c1c00000)

libfreetype.so.6=>/usr/lib64/libfreetype.so.6(0x00000037c7000000)

libfontenc.so.1=>/usr/lib64/libfontenc.so.1(0x00007f70b9675000)

The good news is it only depends on one shared library – libGL.so.1 (548 Kb) – so I can copy that from my VMWare CentOS to my shared host. I set LD_LIBRARY_PATH to a common path for my lib64 libraries. Here is how I found and copied the library.

On the CentOS VMWare

1

2

3

4

5

# find / -name libGL.so.1

/usr/lib64/libGL.so.1

# readlink /usr/lib64/libGL.so.1

libGL.so.1.2.0

# cp /usr/lib64/libGL.so.1.2.0 /tmp/libGL.so.1

Then I copied the /tmp/libGL.so.1 to my shared host in the same directory as I set LD_LIBRARY_PATH.

ldd libGL.so.1 shows that it depends on other libraries as well. libglapi.so.0 and libXxf86vm.so.1 need to be copied to the shared host too in a similar manner as libGL.so.1.

Once these three libraries are uploaded to the shared host, I could test Xvfb.

1

2

3

4

5

6

7

8

drakes@a2plcpnl1390[~/experiments/xorg/xvfb]$./Xvfb:99

^Z

[1]+Stopped./Xvfb:99

drakes@a2plcpnl1390[~/experiments/xorg/xvfb]$ps

PID TTYTIMECMD

384377pts/000:00:00bash

505717pts/000:00:00Xvfb

506301pts/000:00:00ps

And that is how I got Xvfb to run on a shared host without X and without root access.