Autodep
Autodep is a mechanism through which jobs are spied to automatically detect disk accesses. From this information open-lmake can determine if accesses were within the constraints provided by the rule and can list all deps.
Spying methods
There exists two classes of spying methods. Not all methods are supported on all systems, though.
Spying methods based on libc
calls
This consists in spying all calls the the libc
.
Several mechanisms can be used to do so.
All of them consist in diverting the calls to the libc
that access files (typically open
, but there are about a hundred of them) to a piggy-back code
that records the access before handling over to the real libc
.
They differ in the methods used to divert these calls to the autodep code.
This class of methods is fast as there is no need to switch context at each access.
Moreover, the accessed file is first scanned to see if it is a system file (such as in /usr
), in which case we can escape very early from the recording mechanism.
And in practice, such accesses to system files are by far the most common case.
One of the major drawbacks is that it requires the libc
to be dynamically linked. While libc
static linking is very uncommon, it does happen.
$LD_AUDIT
Modern Linux dynamic linkers implement an auditing mechanism.
This works by providing hooks at dynamic link edition time (by setting the environment variable $LD_AUDIT
) when a file is loaded, when a symbol is searched, when a reference is bound, etc.
In our case, we trap all symbol look up into the libc
and interesting calls (i.e. those that access files) are diverted at that time.
However, some linkers do not seem to honor this auditing API.
For example, programs compiled by the rust compiler (including rustc
itself) could not be made working.
Such auditing code is marginally intrusive in the user code as, while lying in the same address space, it is in a different segment.
For example it has its own errno
global variable.
If available, this is the default method.
$LD_PRELOAD
This method consists in pre-loading our spying library before the libc
.
Because it is loaded before and contains the same symbols as the libc
, these calls from the user application are diverted to our code.
this is a little bit more intrusive (e.g. the errno
variable is shared) and this is the default method if $LD_AUDIT
is not available.
$LD_PRELOAD
with jemalloc
The use of jemalloc
creates a chicken and egg problem at start up.
The reason is that the spying code requires the use of malloc
at start up, and jemalloc
(which is called in lieu of malloc
) accesses a configuration file at start up.
A special implementation has been devised to handle this case, but is too fragile and complex to make it the default $LD_PRELOAD
method.
Spying methods based on system calls
The principle is to use ptrace
(the system call used by the strace
utility) to spy user code activity.
This is almost non-intrusive.
In one case, we have seen a commercial tool reading /proc/self/status
to detect such a ptrace
ing process, and it stopped, thinking it was being reverse engineered.
Curiously, it did not detect $LD_PRELOAD
...
The major drawback is performance wise: the impact is more significant as there is a context switch at each system call.
BPF
is used, if available, to decrease the number of useless context switches, but it does not allow to filter out on file name, so it is impossible to have an early ignore of system files.
What to do with accesses
There are 2 questions to solve :
- Determine the
cwd
. Because accesses may be relative to it (and usually are), the spying code must have a precise view of thecwd
. This requires to interceptchdir
although no access is to be reported. - Symbolic link processing.
Open-lmake lies in the physical world (and there is no way it can do anything else) and must be aware of any symbolic link traversal.
This includes the ones on the dir path.
So the spying code includes a functionality that resembles to
realpath
, listing all traversed links.
Lying in the physical world means that symbolic links are handled like plain data files, except that there is a special bit that says it is a symbolic link. Its content is its target. For example, after the code sequence:
cd a
cat b
where b
is symbolic link to c
, 2 deps are recorded:
a/b
(a symbolic link), as if it is modified, job must be rerun.a/c
(a plain data file), same reason.
Generally speaking, read a file makes it a dep, writing to it makes it a target. Of course, reading a file that has been written doe not make it a dep any more.
How to report accesses
When a job is run, a wrapper (called job_exec
) is launched that launches the user process.
job_exec
has several responsibilities, among which :
- Prepare the user environment for the user code (environment variables, cwd, namespace if necessary, etc.).
- Receive the accesses made by the user code (through a socket) and record them.
- Determine what is a dep, what is a target etc.
- Report a job digest to the server (the central process managing the dep DAG).
The major idea is that when an access is reported by the user code (in the case of libc
call spying), there is no reply from job_exec
back to the user code, so no round trip delay is incurred.
Deps order is kept as well
Remember that when a job is run, its deps list is approximative.
It is the one of the previous run, which had different file contents.
For example, a .c
may have changed, including a #include
directive.
In case there are 2 deps d1
and d2
, and d1
was just discovered, it may be out-of date and the job ran with a bad content for d1
.
Most of the time, this is harmless, but sometimes, it may happen that d2
is not necessary any more (because old d1
content had #include "d2"
and new one does not).
In that case, this job must be rerun with the new content od d1
, even if d2
is in error, as d2
might disappaer as de dep.
This may only occurs if d2
was accessed after d1
was accessed. If d2
was accessed before d1
, it is safe to say the job cannot run because d2
is in error: it will never disappear.