#990244 tests/streams: Failing test on pipe stdout file descriptor

Package:
clisp
Source:
clisp
Description:
GNU CLISP, a Common Lisp implementation
Submitter:
Ariel D'Alessandro
Date:
2021-12-10 20:27:04 UTC
Severity:
important
Tags:
#990244#5
Date:
2021-06-23 19:58:02 UTC
From:
To:
Dear Maintainer,

Building clisp package in an OBS instance FTBFS because of the following
test failure:


https://salsa.debian.org/common-lisp-team/clisp/-/blob/0a8137b7caa777e1c08e06fa7161784f947fa5af/tests/streams.tst#L1290

  (let ((*reopen-open-file* nil)) ; stdout can be a file, it will be
detected!
    (with-open-file (copy s :direction :output) (streamp copy))) T

Build error log shows the following output for that test:

  $ cat usr/src/packages/BUILD/debian/build/tests/*.erg
  Form: (LET ((*REOPEN-OPEN-FILE* NIL)) (WITH-OPEN-FILE (COPY S
:DIRECTION :OUTPUT) (STREAMP COPY)))
  CORRECT: T
  CLISP  : ERROR
  OS-FILE-ERROR(13): Permission denied
  OUT:
  "[OS-FILE-ERROR]: OS-FILE-ERROR(13): Permission denied
  "

That Lisp form tries to open the stdout file descriptor, but in this
case it's failing with EACCES (Permission denied).

AFAICS, running `strace` on the test shows that on the OBS instance,
`stdout` is piped, causing the access issue:

  [  148s] lstat("/proc/2867342/fd/2", {st_mode=S_IFLNK|0300,
st_size=64, ...}) = 0
  [  148s] readlink("/proc/2867342/fd/2", "pipe:[4065697]", 64) = 14
  [  148s] stat("/proc/2867342/fd/2", {st_mode=S_IFIFO|0600, st_size=0,
...}) = 0
  [  148s] openat(AT_FDCWD, "/proc/2867342/fd/2",
O_WRONLY|O_CREAT|O_TRUNC, 0644) = -1 EACCES (Permission denied)

However, on a "local" build, `stdout` going directly to the pseudo
terminal doesn't raise any issue:

  lstat("/proc/2901662/fd/2", {st_mode=S_IFLNK|0700, st_size=64, ...}) = 0
  readlink("/proc/2901662/fd/2", "/dev/pts/1", 64) = 10
  readlink("/dev", 0x7fff7e9fbd30, 4095)  = -1 EINVAL (Invalid argument)
  readlink("/dev/pts", 0x7fff7e9fbd30, 4095) = -1 EINVAL (Invalid argument)
  lstat("/dev/pts/1", {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x1),
...}) = 0
  stat("/dev/pts/1", {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x1),
...}) = 0
  openat(AT_FDCWD, "/dev/pts/1", O_WRONLY|O_CREAT|O_TRUNC, 0644) = 10

This seems to be related to how output fds are handled. AFAICS there's a
difference between these two cases, see:


https://salsa.debian.org/common-lisp-team/clisp/-/blob/0a8137b7caa777e1c08e06fa7161784f947fa5af/src/pathname.d#L5426

Is this an issue and should be checked? What's the expected behaviour on
these cases?

Thanks in advance.

Regards,
Ariel D'Alessandro

#990244#10
Date:
2021-06-26 09:02:29 UTC
From:
To:
It could be that OBS opens the pipe as a privileged process, forks,
then drops privileges afterwards such that the child process is
precluded from reopening the inode that backs the file descriptor
clisp operates on.

You'd somehow have to find a way to invoke "stat -L
/proc/<pid>/fd/<fdno>" on the failing file quickly enough to find out
if this is the case.  Inserting sleeps into the test could buy you
more time.

Alternatively (and assuming you use ocs) tools like socat/ptywrap
could maybe be used via $OSC_SU_WRAPPER to hook the child process onto
a pty (which should also be created after dropping the privileges).
Or you could try to run the build as root (osc build
--userootforbuild) and see if the error persists.

Regards,
Dennis

#990244#19
Date:
2021-12-10 02:20:52 UTC
From:
To:
Hi Dennis,

Thanks for your helpful feedback.

Makes sense, agreed, this is probably the sequence.

lstat("/proc/1880876/fd/2", {st_mode=S_IFLNK|0300, st_size=64, ...}) = 0
readlink("/proc/1880876/fd/2", "pipe:[53088390]", 64) = 15
stat("/proc/1880876/fd/2", {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
openat(AT_FDCWD, "/proc/1880876/fd/2", O_WRONLY|O_CREAT|O_TRUNC, 0644) =
-1 EACCES (Permission denied)

This matches the case described above, opening the pipe as root, then
dropping and running clisp (during tests):

$ stat -L proc/1880876/fd/2
  File: proc/1880876/fd/2
  Size: 0         	Blocks: 0          IO Block: 4096   fifo
Device: ch/12d	Inode: 53088390    Links: 1
Access: (0600/prw-------)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2021-12-09 21:55:16.645805190 -0300
Modify: 2021-12-09 21:55:16.645805190 -0300
Change: 2021-12-09 21:55:16.645805190 -0300
 Birth: -

$ head proc/1880876/status
Name:	lisp.run
Umask:	0022
State:	S (sleeping)
Tgid:	1880876
Ngid:	0
Pid:	1880876
PPid:	1880873
TracerPid:	1880873
Uid:	399	399	399	399
Gid:	399	399	399	399
is no longer happening. Of course, now the pty is used instead of the
(root owned) pipe, so permissions are fine to re-open the file.

So, moving forward. This is the failing test simplified:

(streamp (setq s (make-stream :error))) T
(let ((*reopen-open-file* nil)) ; stdout can be a file, it will be detected!
  (with-open-file (copy s :direction :output) (streamp copy))) T

My question here would be: is this test expected to always succeed?

AFAIU, we can't assume to have the permissions to re-open the file
related to the file descriptor, which is what the code is explicitly
trying to do.

Please correct me if I'm wrong as I'm not used to clisp syntax, just
going through the source code :-)

From what I see, it responsibility of the clisp test code to check if it
has the permissions to re-open the file. Otherwise, it's not guaranteed
to succeed. For example, having this fd pointing to a (root owned) pipe
is a valid situation and should be properly handled by this test.

Thanks,
Ariel

#990244#24
Date:
2021-12-10 19:57:09 UTC
From:
To:
control: forwarded -1 https://sourceforge.net/p/clisp/bugs/747/
X-Debbugs-CC: Ariel D'Alessandro <ariel.dalessandro@collabora.com>

You'd have to ask the original author of the test that.  In a perfect
world every test would be accompanied by a detailed description of
what exactly it tests for, what conditions it expects and what
constitutes a pass or failure.

In a test suite you usually can since it is probably not supposed to
be run by a privileged user in such a way that it fails.  Build
servers and CI pipelines, which are almost always involved in exposing
such bugs, really ought to put in more effort to make their
environment more similar to the one the developer develops and tests
the code in instead of silently expecting everyone else to put in the
additional effort to placate them.  Fooling a build/test suite into
thinking it is hooked to a terminal with a tool like ptywrap is really
not that big of an ask and it should be a default feature in all such
products.  Alternatively the privileged invoker could just as easily
call chown(2) on the pipe after opening it and solve the issue this
way.

That's debatable.  Yes, it could be argued that the test should do a
better job at disarming itself by testing if it can reopen the file
with e.g. access(2).  OTOH it could also be argued that it is the
invoker's burden to ensure conditions that allow the test to succeed.
Writing test suites is annoying enough as is.  No need to make it more
annoying by also expecting accommodation for build servers and CI
pipelines if they themselves could do the accommodation much more
easily.

Regards.

#990244#31
Date:
2021-12-10 20:23:19 UTC
From:
To:
FYI, adding +Cc Sam Steingold.